Commit 519ffa1e authored by Mike Greiling's avatar Mike Greiling

Merge branch 'master' into sh-headless-chrome-support

* master: (297 commits)
  Fix deletion of container registry or images returning an error
  The fog-aliyun gem had a bug in v0.1.0 for file storage creation/update. This merge requests update the gem to v0.2.0 which contains the fix:
  Decrease ABC threshold to 54.28
  Update VERSION to 10.2.0-pre
  Update CHANGELOG.md for 10.1.0
  Document `CI_SHARED_ENVIRONMENT` and `CI_DISPOSABLE_ENVIRONMENT`
  Fix the external URLs generated for online view of HTML artifacts
  Use title as placeholder instead of issue title for reusability
  Fix failure in current_settings_spec.rb
  Clarify the difference between project_update and project_rename
  URI decode Page-Title header to preserve UTF-8 characters
  Update Gitaly version to v0.49.0
  Decrease Perceived Complexity threshold to 14
  Resolve "Remove help text regarding group issues on group issues page (and group merge requests page)"
  Force non diff resolved discussion to display when collapse toggled
  Added submodule support in multi-file editor
  add note about after_script being run separately
  Check for element before evaluate_script
  Merge branch 'master-i18n' into 'master'
  Update Prometheus gem to fix problems with other files overwriting current file
  ...
parents 1e78c627 5d74973d
...@@ -49,11 +49,11 @@ stages: ...@@ -49,11 +49,11 @@ stages:
- gitlab-org - gitlab-org
.tests-metadata-state: &tests-metadata-state .tests-metadata-state: &tests-metadata-state
services: [] <<: *dedicated-runner
variables: variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
TESTS_METADATA_S3_BUCKET: "gitlab-ce-cache" TESTS_METADATA_S3_BUCKET: "gitlab-ce-cache"
before_script:
- source scripts/utils.sh
artifacts: artifacts:
expire_in: 31d expire_in: 31d
paths: paths:
...@@ -80,6 +80,7 @@ stages: ...@@ -80,6 +80,7 @@ stages:
.rspec-metadata: &rspec-metadata .rspec-metadata: &rspec-metadata
<<: *dedicated-runner <<: *dedicated-runner
<<: *pull-cache <<: *pull-cache
<<: *except-docs
stage: test stage: test
script: script:
- JOB_NAME=( $CI_JOB_NAME ) - JOB_NAME=( $CI_JOB_NAME )
...@@ -109,16 +110,15 @@ stages: ...@@ -109,16 +110,15 @@ stages:
.rspec-metadata-pg: &rspec-metadata-pg .rspec-metadata-pg: &rspec-metadata-pg
<<: *rspec-metadata <<: *rspec-metadata
<<: *use-pg <<: *use-pg
<<: *except-docs
.rspec-metadata-mysql: &rspec-metadata-mysql .rspec-metadata-mysql: &rspec-metadata-mysql
<<: *rspec-metadata <<: *rspec-metadata
<<: *use-mysql <<: *use-mysql
<<: *except-docs
.spinach-metadata: &spinach-metadata .spinach-metadata: &spinach-metadata
<<: *dedicated-runner <<: *dedicated-runner
<<: *pull-cache <<: *pull-cache
<<: *except-docs
stage: test stage: test
script: script:
- JOB_NAME=( $CI_JOB_NAME ) - JOB_NAME=( $CI_JOB_NAME )
...@@ -141,12 +141,10 @@ stages: ...@@ -141,12 +141,10 @@ stages:
.spinach-metadata-pg: &spinach-metadata-pg .spinach-metadata-pg: &spinach-metadata-pg
<<: *spinach-metadata <<: *spinach-metadata
<<: *use-pg <<: *use-pg
<<: *except-docs
.spinach-metadata-mysql: &spinach-metadata-mysql .spinach-metadata-mysql: &spinach-metadata-mysql
<<: *spinach-metadata <<: *spinach-metadata
<<: *use-mysql <<: *use-mysql
<<: *except-docs
.only-canonical-masters: &only-canonical-masters .only-canonical-masters: &only-canonical-masters
only: only:
...@@ -157,12 +155,8 @@ stages: ...@@ -157,12 +155,8 @@ stages:
# Trigger a package build in omnibus-gitlab repository # Trigger a package build in omnibus-gitlab repository
build-package: build-package:
image: ruby:2.3-alpine image: ruby:2.4-alpine
before_script: [] before_script: []
services: []
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
stage: build stage: build
cache: {} cache: {}
when: manual when: manual
...@@ -183,13 +177,9 @@ build-package: ...@@ -183,13 +177,9 @@ build-package:
- apk add --update openssl - apk add --update openssl
- wget https://gitlab.com/gitlab-org/gitlab-ce/raw/master/scripts/trigger-build-docs - wget https://gitlab.com/gitlab-org/gitlab-ce/raw/master/scripts/trigger-build-docs
- chmod 755 trigger-build-docs - chmod 755 trigger-build-docs
services: []
cache: {} cache: {}
dependencies: [] dependencies: []
artifacts: {}
variables: variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
GIT_STRATEGY: none GIT_STRATEGY: none
when: manual when: manual
only: only:
...@@ -222,7 +212,6 @@ review-docs-cleanup: ...@@ -222,7 +212,6 @@ review-docs-cleanup:
# Retrieve knapsack and rspec_flaky reports # Retrieve knapsack and rspec_flaky reports
retrieve-tests-metadata: retrieve-tests-metadata:
<<: *tests-metadata-state <<: *tests-metadata-state
<<: *dedicated-runner
<<: *except-docs <<: *except-docs
stage: prepare stage: prepare
cache: cache:
...@@ -240,7 +229,6 @@ retrieve-tests-metadata: ...@@ -240,7 +229,6 @@ retrieve-tests-metadata:
update-tests-metadata: update-tests-metadata:
<<: *tests-metadata-state <<: *tests-metadata-state
<<: *dedicated-runner
<<: *only-canonical-masters <<: *only-canonical-masters
stage: post-test stage: post-test
cache: cache:
...@@ -305,69 +293,69 @@ setup-test-env: ...@@ -305,69 +293,69 @@ setup-test-env:
- public/assets - public/assets
- tmp/tests - tmp/tests
rspec-pg 0 25: *rspec-metadata-pg rspec-pg 0 26: *rspec-metadata-pg
rspec-pg 1 25: *rspec-metadata-pg rspec-pg 1 26: *rspec-metadata-pg
rspec-pg 2 25: *rspec-metadata-pg rspec-pg 2 26: *rspec-metadata-pg
rspec-pg 3 25: *rspec-metadata-pg rspec-pg 3 26: *rspec-metadata-pg
rspec-pg 4 25: *rspec-metadata-pg rspec-pg 4 26: *rspec-metadata-pg
rspec-pg 5 25: *rspec-metadata-pg rspec-pg 5 26: *rspec-metadata-pg
rspec-pg 6 25: *rspec-metadata-pg rspec-pg 6 26: *rspec-metadata-pg
rspec-pg 7 25: *rspec-metadata-pg rspec-pg 7 26: *rspec-metadata-pg
rspec-pg 8 25: *rspec-metadata-pg rspec-pg 8 26: *rspec-metadata-pg
rspec-pg 9 25: *rspec-metadata-pg rspec-pg 9 26: *rspec-metadata-pg
rspec-pg 10 25: *rspec-metadata-pg rspec-pg 10 26: *rspec-metadata-pg
rspec-pg 11 25: *rspec-metadata-pg rspec-pg 11 26: *rspec-metadata-pg
rspec-pg 12 25: *rspec-metadata-pg rspec-pg 12 26: *rspec-metadata-pg
rspec-pg 13 25: *rspec-metadata-pg rspec-pg 13 26: *rspec-metadata-pg
rspec-pg 14 25: *rspec-metadata-pg rspec-pg 14 26: *rspec-metadata-pg
rspec-pg 15 25: *rspec-metadata-pg rspec-pg 15 26: *rspec-metadata-pg
rspec-pg 16 25: *rspec-metadata-pg rspec-pg 16 26: *rspec-metadata-pg
rspec-pg 17 25: *rspec-metadata-pg rspec-pg 17 26: *rspec-metadata-pg
rspec-pg 18 25: *rspec-metadata-pg rspec-pg 18 26: *rspec-metadata-pg
rspec-pg 19 25: *rspec-metadata-pg rspec-pg 19 26: *rspec-metadata-pg
rspec-pg 20 25: *rspec-metadata-pg rspec-pg 20 26: *rspec-metadata-pg
rspec-pg 21 25: *rspec-metadata-pg rspec-pg 21 26: *rspec-metadata-pg
rspec-pg 22 25: *rspec-metadata-pg rspec-pg 22 26: *rspec-metadata-pg
rspec-pg 23 25: *rspec-metadata-pg rspec-pg 23 26: *rspec-metadata-pg
rspec-pg 24 25: *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 0 26: *rspec-metadata-mysql
rspec-mysql 2 25: *rspec-metadata-mysql rspec-mysql 1 26: *rspec-metadata-mysql
rspec-mysql 3 25: *rspec-metadata-mysql rspec-mysql 2 26: *rspec-metadata-mysql
rspec-mysql 4 25: *rspec-metadata-mysql rspec-mysql 3 26: *rspec-metadata-mysql
rspec-mysql 5 25: *rspec-metadata-mysql rspec-mysql 4 26: *rspec-metadata-mysql
rspec-mysql 6 25: *rspec-metadata-mysql rspec-mysql 5 26: *rspec-metadata-mysql
rspec-mysql 7 25: *rspec-metadata-mysql rspec-mysql 6 26: *rspec-metadata-mysql
rspec-mysql 8 25: *rspec-metadata-mysql rspec-mysql 7 26: *rspec-metadata-mysql
rspec-mysql 9 25: *rspec-metadata-mysql rspec-mysql 8 26: *rspec-metadata-mysql
rspec-mysql 10 25: *rspec-metadata-mysql rspec-mysql 9 26: *rspec-metadata-mysql
rspec-mysql 11 25: *rspec-metadata-mysql rspec-mysql 10 26: *rspec-metadata-mysql
rspec-mysql 12 25: *rspec-metadata-mysql rspec-mysql 11 26: *rspec-metadata-mysql
rspec-mysql 13 25: *rspec-metadata-mysql rspec-mysql 12 26: *rspec-metadata-mysql
rspec-mysql 14 25: *rspec-metadata-mysql rspec-mysql 13 26: *rspec-metadata-mysql
rspec-mysql 15 25: *rspec-metadata-mysql rspec-mysql 14 26: *rspec-metadata-mysql
rspec-mysql 16 25: *rspec-metadata-mysql rspec-mysql 15 26: *rspec-metadata-mysql
rspec-mysql 17 25: *rspec-metadata-mysql rspec-mysql 16 26: *rspec-metadata-mysql
rspec-mysql 18 25: *rspec-metadata-mysql rspec-mysql 17 26: *rspec-metadata-mysql
rspec-mysql 19 25: *rspec-metadata-mysql rspec-mysql 18 26: *rspec-metadata-mysql
rspec-mysql 20 25: *rspec-metadata-mysql rspec-mysql 19 26: *rspec-metadata-mysql
rspec-mysql 21 25: *rspec-metadata-mysql rspec-mysql 20 26: *rspec-metadata-mysql
rspec-mysql 22 25: *rspec-metadata-mysql rspec-mysql 21 26: *rspec-metadata-mysql
rspec-mysql 23 25: *rspec-metadata-mysql rspec-mysql 22 26: *rspec-metadata-mysql
rspec-mysql 24 25: *rspec-metadata-mysql rspec-mysql 23 26: *rspec-metadata-mysql
rspec-mysql 24 26: *rspec-metadata-mysql
spinach-pg 0 5: *spinach-metadata-pg rspec-mysql 25 26: *rspec-metadata-mysql
spinach-pg 1 5: *spinach-metadata-pg
spinach-pg 2 5: *spinach-metadata-pg spinach-pg 0 4: *spinach-metadata-pg
spinach-pg 3 5: *spinach-metadata-pg spinach-pg 1 4: *spinach-metadata-pg
spinach-pg 4 5: *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 0 4: *spinach-metadata-mysql
spinach-mysql 2 5: *spinach-metadata-mysql spinach-mysql 1 4: *spinach-metadata-mysql
spinach-mysql 3 5: *spinach-metadata-mysql spinach-mysql 2 4: *spinach-metadata-mysql
spinach-mysql 4 5: *spinach-metadata-mysql spinach-mysql 3 4: *spinach-metadata-mysql
# Static analysis jobs # Static analysis jobs
.ruby-static-analysis: &ruby-static-analysis .ruby-static-analysis: &ruby-static-analysis
......
...@@ -624,7 +624,7 @@ Style/PredicateName: ...@@ -624,7 +624,7 @@ Style/PredicateName:
# branches, and conditions. # branches, and conditions.
Metrics/AbcSize: Metrics/AbcSize:
Enabled: true Enabled: true
Max: 55.25 Max: 54.28
# This cop checks if the length of a block exceeds some maximum value. # This cop checks if the length of a block exceeds some maximum value.
Metrics/BlockLength: Metrics/BlockLength:
...@@ -665,7 +665,7 @@ Metrics/ParameterLists: ...@@ -665,7 +665,7 @@ Metrics/ParameterLists:
# A complexity metric geared towards measuring complexity for a human reader. # A complexity metric geared towards measuring complexity for a human reader.
Metrics/PerceivedComplexity: Metrics/PerceivedComplexity:
Enabled: true Enabled: true
Max: 15 Max: 14
# Lint ######################################################################## # Lint ########################################################################
......
This diff is collapsed.
0.46.0 0.49.0
\ No newline at end of file
...@@ -102,7 +102,7 @@ gem 'fog-google', '~> 0.5' ...@@ -102,7 +102,7 @@ gem 'fog-google', '~> 0.5'
gem 'fog-local', '~> 0.3' gem 'fog-local', '~> 0.3'
gem 'fog-openstack', '~> 0.1' gem 'fog-openstack', '~> 0.1'
gem 'fog-rackspace', '~> 0.1.1' gem 'fog-rackspace', '~> 0.1.1'
gem 'fog-aliyun', '~> 0.1.0' gem 'fog-aliyun', '~> 0.2.0'
# for Google storage # for Google storage
gem 'google-api-client', '~> 0.13.6' gem 'google-api-client', '~> 0.13.6'
...@@ -281,7 +281,7 @@ group :metrics do ...@@ -281,7 +281,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false gem 'influxdb', '~> 0.2', require: false
# Prometheus # Prometheus
gem 'prometheus-client-mmap', '~>0.7.0.beta14' gem 'prometheus-client-mmap', '~>0.7.0.beta17'
gem 'raindrops', '~> 0.18' gem 'raindrops', '~> 0.18'
end end
...@@ -398,7 +398,7 @@ group :ed25519 do ...@@ -398,7 +398,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # 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 gem 'toml-rb', '~> 0.3.15', require: false
......
...@@ -215,7 +215,7 @@ GEM ...@@ -215,7 +215,7 @@ GEM
flowdock (0.7.1) flowdock (0.7.1)
httparty (~> 0.7) httparty (~> 0.7)
multi_json multi_json
fog-aliyun (0.1.0) fog-aliyun (0.2.0)
fog-core (~> 1.27) fog-core (~> 1.27)
fog-json (~> 1.0) fog-json (~> 1.0)
ipaddress (~> 0.8) ipaddress (~> 0.8)
...@@ -274,7 +274,7 @@ GEM ...@@ -274,7 +274,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.41.0) gitaly-proto (0.45.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -325,7 +325,9 @@ GEM ...@@ -325,7 +325,9 @@ GEM
mime-types (~> 3.0) mime-types (~> 3.0)
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.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) googleauth (0.5.3)
faraday (~> 0.12) faraday (~> 0.12)
jwt (~> 1.4) jwt (~> 1.4)
...@@ -352,8 +354,9 @@ GEM ...@@ -352,8 +354,9 @@ GEM
rake rake
grape_logging (1.7.0) grape_logging (1.7.0)
grape grape
grpc (1.6.0) grpc (1.6.6)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0)
googleauth (~> 0.5.1) googleauth (~> 0.5.1)
haml (4.0.7) haml (4.0.7)
tilt tilt
...@@ -616,7 +619,7 @@ GEM ...@@ -616,7 +619,7 @@ GEM
parser parser
unparser unparser
procto (0.0.3) procto (0.0.3)
prometheus-client-mmap (0.7.0.beta14) prometheus-client-mmap (0.7.0.beta17)
mmap2 (~> 2.2, >= 2.2.7) mmap2 (~> 2.2, >= 2.2.7)
pry (0.10.4) pry (0.10.4)
coderay (~> 1.1.0) coderay (~> 1.1.0)
...@@ -1008,7 +1011,7 @@ DEPENDENCIES ...@@ -1008,7 +1011,7 @@ DEPENDENCIES
flay (~> 2.8.0) flay (~> 2.8.0)
flipper (~> 0.10.2) flipper (~> 0.10.2)
flipper-active_record (~> 0.10.2) flipper-active_record (~> 0.10.2)
fog-aliyun (~> 0.1.0) fog-aliyun (~> 0.2.0)
fog-aws (~> 1.4) fog-aws (~> 1.4)
fog-core (~> 1.44) fog-core (~> 1.44)
fog-google (~> 0.5) fog-google (~> 0.5)
...@@ -1023,7 +1026,7 @@ DEPENDENCIES ...@@ -1023,7 +1026,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.41.0) gitaly-proto (~> 0.45.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2) gitlab-markup (~> 1.6.2)
...@@ -1098,7 +1101,7 @@ DEPENDENCIES ...@@ -1098,7 +1101,7 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3) peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2) pg (~> 0.18.2)
premailer-rails (~> 1.9.7) 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-byebug (~> 3.4.1)
pry-rails (~> 0.3.4) pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1) rack-attack (~> 4.4.1)
......
# GitLab Maintenance Policy # GitLab Maintenance Policy
GitLab follows the [Semantic Versioning](http://semver.org/) for its releases: See [doc/policy/maintenance.md](doc/policy/maintenance.md)
`(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
10.1.0-pre 10.2.0-pre
...@@ -15,6 +15,7 @@ const Api = { ...@@ -15,6 +15,7 @@ const Api = {
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
usersPath: '/api/:version/users.json', usersPath: '/api/:version/users.json',
commitPath: '/api/:version/projects/:id/repository/commits', commitPath: '/api/:version/projects/:id/repository/commits',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
group(groupId, callback) { group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath) const url = Api.buildUrl(Api.groupPath)
...@@ -123,6 +124,19 @@ const Api = { ...@@ -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 // Return text for a specific license
licenseText(key, data, callback) { licenseText(key, data, callback) {
const url = Api.buildUrl(Api.licensePath) const url = Api.buildUrl(Api.licensePath)
......
/* eslint-disable func-names, object-shorthand, prefer-arrow-callback */ /* eslint-disable func-names, object-shorthand, prefer-arrow-callback */
/* global Dropzone */ import Dropzone from 'dropzone';
import '../lib/utils/url_utility'; import '../lib/utils/url_utility';
import { HIDDEN_CLASS } from '../lib/utils/constants'; import { HIDDEN_CLASS } from '../lib/utils/constants';
import csrf from '../lib/utils/csrf'; import csrf from '../lib/utils/csrf';
......
...@@ -9,6 +9,7 @@ import Flash from '../../flash'; ...@@ -9,6 +9,7 @@ import Flash from '../../flash';
import eventHub from '../../sidebar/event_hub'; import eventHub from '../../sidebar/event_hub';
import AssigneeTitle from '../../sidebar/components/assignees/assignee_title'; import AssigneeTitle from '../../sidebar/components/assignees/assignee_title';
import Assignees from '../../sidebar/components/assignees/assignees'; import Assignees from '../../sidebar/components/assignees/assignees';
import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue'; import './sidebar/remove_issue';
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
...@@ -113,7 +114,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({ ...@@ -113,7 +114,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({
mounted () { mounted () {
new IssuableContext(this.currentUser); new IssuableContext(this.currentUser);
new MilestoneSelect(); new MilestoneSelect();
new gl.DueDateSelectors(); new DueDateSelectors();
new LabelsSelect(); new LabelsSelect();
new Sidebar(); new Sidebar();
gl.Subscription.bindAll('.subscription'); gl.Subscription.bindAll('.subscription');
......
...@@ -7,7 +7,7 @@ class BoardService { ...@@ -7,7 +7,7 @@ class BoardService {
this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, { this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, {
issues: { issues: {
method: 'GET', 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}`, {}, { this.lists = Vue.resource(`${listsEndpoint}{/id}`, {}, {
...@@ -16,7 +16,7 @@ class BoardService { ...@@ -16,7 +16,7 @@ class BoardService {
url: `${listsEndpoint}/generate.json` 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`, {}, { this.issues = Vue.resource(`${listsEndpoint}{/id}/issues`, {}, {
bulkUpdate: { bulkUpdate: {
method: 'POST', method: 'POST',
......
/* 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() {
$(function() { const previewColor = $(this).val();
var previewPath; $('div.broadcast-message-preview').css('background-color', previewColor);
$('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() {
var previewColor; $('input#broadcast_message_font').on('input', function onMessageFontInput() {
previewColor = $(this).val(); const previewColor = $(this).val();
return $('div.broadcast-message-preview').css('color', previewColor); $('div.broadcast-message-preview').css('color', previewColor);
}); });
previewPath = $('textarea#broadcast_message_message').data('preview-path');
return $('textarea#broadcast_message_message').on('input', function() { const previewPath = $('textarea#broadcast_message_message').data('preview-path');
var message;
message = $(this).val(); $('textarea#broadcast_message_message').on('input', _.debounce(function onMessageInput() {
const message = $(this).val();
if (message === '') { if (message === '') {
return $('.js-broadcast-message-preview').text("Your message here"); $('.js-broadcast-message-preview').text('Your message here');
} else { } else {
return $.ajax({ $.ajax({
url: previewPath, url: previewPath,
type: "POST", type: 'POST',
data: { data: {
broadcast_message: { broadcast_message: { message },
message: message },
}
}
}); });
} }
}); }, 250));
}); }
...@@ -3,7 +3,8 @@ import Visibility from 'visibilityjs'; ...@@ -3,7 +3,8 @@ import Visibility from 'visibilityjs';
import axios from 'axios'; import axios from 'axios';
import Poll from './lib/utils/poll'; import Poll from './lib/utils/poll';
import { s__ } from './locale'; import { s__ } from './locale';
import './flash'; import initSettingsPanels from './settings_panels';
import Flash from './flash';
/** /**
* Cluster page has 2 separate parts: * Cluster page has 2 separate parts:
...@@ -24,6 +25,8 @@ class ClusterService { ...@@ -24,6 +25,8 @@ class ClusterService {
export default class Clusters { export default class Clusters {
constructor() { constructor() {
initSettingsPanels();
const dataset = document.querySelector('.js-edit-cluster-form').dataset; const dataset = document.querySelector('.js-edit-cluster-form').dataset;
this.state = { this.state = {
......
/* 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 */ /* 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 ProjectSelect */
/* global ShortcutsNavigation */
/* global IssuableIndex */ /* global IssuableIndex */
/* global ShortcutsIssuable */
/* global Milestone */ /* global Milestone */
/* global IssuableForm */ /* global IssuableForm */
/* global LabelsSelect */ /* global LabelsSelect */
...@@ -31,10 +29,7 @@ import CILintEditor from './ci_lint_editor'; ...@@ -31,10 +29,7 @@ import CILintEditor from './ci_lint_editor';
/* global ProjectImport */ /* global ProjectImport */
import Labels from './labels'; import Labels from './labels';
import LabelManager from './label_manager'; import LabelManager from './label_manager';
/* global Shortcuts */
/* global ShortcutsFindFile */
/* global Sidebar */ /* global Sidebar */
/* global ShortcutsWiki */
import CommitsList from './commits'; import CommitsList from './commits';
import Issue from './issue'; import Issue from './issue';
...@@ -70,6 +65,7 @@ import initSettingsPanels from './settings_panels'; ...@@ -70,6 +65,7 @@ import initSettingsPanels from './settings_panels';
import initExperimentalFlags from './experimental_flags'; import initExperimentalFlags from './experimental_flags';
import OAuthRememberMe from './oauth_remember_me'; import OAuthRememberMe from './oauth_remember_me';
import PerformanceBar from './performance_bar'; import PerformanceBar from './performance_bar';
import initBroadcastMessagesForm from './broadcast_message';
import initNotes from './init_notes'; import initNotes from './init_notes';
import initLegacyFilters from './init_legacy_filters'; import initLegacyFilters from './init_legacy_filters';
import initIssuableSidebar from './init_issuable_sidebar'; import initIssuableSidebar from './init_issuable_sidebar';
...@@ -77,12 +73,20 @@ import initProjectVisibilitySelector from './project_visibility'; ...@@ -77,12 +73,20 @@ import initProjectVisibilitySelector from './project_visibility';
import GpgBadges from './gpg_badges'; import GpgBadges from './gpg_badges';
import UserFeatureHelper from './helpers/user_feature_helper'; import UserFeatureHelper from './helpers/user_feature_helper';
import initChangesDropdown from './init_changes_dropdown'; import initChangesDropdown from './init_changes_dropdown';
import NewGroupChild from './groups/new_group_child';
import AbuseReports from './abuse_reports'; import AbuseReports from './abuse_reports';
import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
import AjaxLoadingSpinner from './ajax_loading_spinner'; import AjaxLoadingSpinner from './ajax_loading_spinner';
import GlFieldErrors from './gl_field_errors'; import GlFieldErrors from './gl_field_errors';
import GLForm from './gl_form'; 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 U2FAuthenticate from './u2f/authenticate';
import Members from './members';
import memberExpirationDate from './member_expiration_date';
import DueDateSelectors from './due_date_select';
(function() { (function() {
var Dispatcher; var Dispatcher;
...@@ -166,9 +170,6 @@ import U2FAuthenticate from './u2f/authenticate'; ...@@ -166,9 +170,6 @@ import U2FAuthenticate from './u2f/authenticate';
const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests'); const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
filteredSearchManager.setup(); filteredSearchManager.setup();
} }
if (page === 'projects:merge_requests:index') {
new UserCallout({ setCalloutPerProject: true });
}
const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_'; const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_';
IssuableIndex.init(pagePrefix); IssuableIndex.init(pagePrefix);
...@@ -232,7 +233,7 @@ import U2FAuthenticate from './u2f/authenticate'; ...@@ -232,7 +233,7 @@ import U2FAuthenticate from './u2f/authenticate';
case 'groups:milestones:edit': case 'groups:milestones:edit':
case 'groups:milestones:update': case 'groups:milestones:update':
new ZenMode(); new ZenMode();
new gl.DueDateSelectors(); new DueDateSelectors();
new GLForm($('.milestone-form'), true); new GLForm($('.milestone-form'), true);
break; break;
case 'projects:compare:show': case 'projects:compare:show':
...@@ -350,7 +351,10 @@ import U2FAuthenticate from './u2f/authenticate'; ...@@ -350,7 +351,10 @@ import U2FAuthenticate from './u2f/authenticate';
case 'projects:show': case 'projects:show':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new NotificationsForm(); new NotificationsForm();
new UserCallout({ setCalloutPerProject: true }); new UserCallout({
setCalloutPerProject: true,
className: 'js-autodevops-banner',
});
if ($('#tree-slider').length) new TreeView(); if ($('#tree-slider').length) new TreeView();
if ($('.blob-viewer').length) new BlobViewer(); if ($('.blob-viewer').length) new BlobViewer();
...@@ -370,9 +374,6 @@ import U2FAuthenticate from './u2f/authenticate'; ...@@ -370,9 +374,6 @@ import U2FAuthenticate from './u2f/authenticate';
case 'projects:pipelines:new': case 'projects:pipelines:new':
new NewBranchForm($('.js-new-pipeline-form')); new NewBranchForm($('.js-new-pipeline-form'));
break; break;
case 'projects:pipelines:index':
new UserCallout({ setCalloutPerProject: true });
break;
case 'projects:pipelines:builds': case 'projects:pipelines:builds':
case 'projects:pipelines:failures': case 'projects:pipelines:failures':
case 'projects:pipelines:show': case 'projects:pipelines:show':
...@@ -393,21 +394,26 @@ import U2FAuthenticate from './u2f/authenticate'; ...@@ -393,21 +394,26 @@ import U2FAuthenticate from './u2f/authenticate';
new gl.Activities(); new gl.Activities();
break; break;
case 'groups:show': case 'groups:show':
const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup');
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new NotificationsForm(); new NotificationsForm();
new NotificationsDropdown(); new NotificationsDropdown();
new ProjectsList(); new ProjectsList();
if (newGroupChildWrapper) {
new NewGroupChild(newGroupChildWrapper);
}
break; break;
case 'groups:group_members:index': case 'groups:group_members:index':
new gl.MemberExpirationDate(); memberExpirationDate();
new gl.Members(); new Members();
new UsersSelect(); new UsersSelect();
break; break;
case 'projects:project_members:index': case 'projects:project_members:index':
new gl.MemberExpirationDate('.js-access-expiration-date-groups'); memberExpirationDate('.js-access-expiration-date-groups');
new GroupsSelect(); new GroupsSelect();
new gl.MemberExpirationDate(); memberExpirationDate();
new gl.Members(); new Members();
new UsersSelect(); new UsersSelect();
break; break;
case 'groups:new': case 'groups:new':
...@@ -430,7 +436,6 @@ import U2FAuthenticate from './u2f/authenticate'; ...@@ -430,7 +436,6 @@ import U2FAuthenticate from './u2f/authenticate';
new TreeView(); new TreeView();
new BlobViewer(); new BlobViewer();
new NewCommitForm($('.js-create-dir-form')); new NewCommitForm($('.js-create-dir-form'));
new UserCallout({ setCalloutPerProject: true });
$('#tree-slider').waitForImages(function() { $('#tree-slider').waitForImages(function() {
ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
}); });
...@@ -528,7 +533,7 @@ import U2FAuthenticate from './u2f/authenticate'; ...@@ -528,7 +533,7 @@ import U2FAuthenticate from './u2f/authenticate';
break; break;
case 'profiles:personal_access_tokens:index': case 'profiles:personal_access_tokens:index':
case 'admin:impersonation_tokens:index': case 'admin:impersonation_tokens:index':
new gl.DueDateSelectors(); new DueDateSelectors();
break; break;
case 'projects:clusters:show': case 'projects:clusters:show':
import(/* webpackChunkName: "clusters" */ './clusters') import(/* webpackChunkName: "clusters" */ './clusters')
...@@ -553,6 +558,9 @@ import U2FAuthenticate from './u2f/authenticate'; ...@@ -553,6 +558,9 @@ import U2FAuthenticate from './u2f/authenticate';
case 'admin': case 'admin':
new Admin(); new Admin();
switch (path[1]) { switch (path[1]) {
case 'broadcast_messages':
initBroadcastMessagesForm();
break;
case 'cohorts': case 'cohorts':
new UsagePing(); new UsagePing();
break; break;
......
This diff is collapsed.
/* 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 */ /* global dateFormat */
import Pikaday from 'pikaday'; import Pikaday from 'pikaday';
import DateFix from './lib/utils/datefix'; import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
class DueDateSelect { class DueDateSelect {
constructor({ $dropdown, $loading } = {}) { constructor({ $dropdown, $loading } = {}) {
...@@ -17,8 +16,8 @@ class DueDateSelect { ...@@ -17,8 +16,8 @@ class DueDateSelect {
this.$value = $block.find('.value'); this.$value = $block.find('.value');
this.$valueContent = $block.find('.value-content'); this.$valueContent = $block.find('.value-content');
this.$sidebarValue = $('.js-due-date-sidebar-value', $block); this.$sidebarValue = $('.js-due-date-sidebar-value', $block);
this.fieldName = $dropdown.data('field-name'), this.fieldName = $dropdown.data('field-name');
this.abilityName = $dropdown.data('ability-name'), this.abilityName = $dropdown.data('ability-name');
this.issueUpdateURL = $dropdown.data('issue-update'); this.issueUpdateURL = $dropdown.data('issue-update');
this.rawSelectedDate = null; this.rawSelectedDate = null;
...@@ -39,20 +38,20 @@ class DueDateSelect { ...@@ -39,20 +38,20 @@ class DueDateSelect {
hidden: () => { hidden: () => {
this.$selectbox.hide(); this.$selectbox.hide();
this.$value.css('display', ''); this.$value.css('display', '');
} },
}); });
} }
initDatePicker() { initDatePicker() {
const $dueDateInput = $(`input[name='${this.fieldName}']`); const $dueDateInput = $(`input[name='${this.fieldName}']`);
const dateFix = DateFix.dashedFix($dueDateInput.val());
const calendar = new Pikaday({ const calendar = new Pikaday({
field: $dueDateInput.get(0), field: $dueDateInput.get(0),
theme: 'gitlab-theme', theme: 'gitlab-theme',
format: 'yyyy-mm-dd', format: 'yyyy-mm-dd',
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
onSelect: (dateText) => { onSelect: (dateText) => {
const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd'); $dueDateInput.val(calendar.toString(dateText));
$dueDateInput.val(formattedDate);
if (this.$dropdown.hasClass('js-issue-boards-due-date')) { if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
gl.issueBoards.BoardsStore.detail.issue.dueDate = $dueDateInput.val(); gl.issueBoards.BoardsStore.detail.issue.dueDate = $dueDateInput.val();
...@@ -60,10 +59,10 @@ class DueDateSelect { ...@@ -60,10 +59,10 @@ class DueDateSelect {
} else { } else {
this.saveDueDate(true); this.saveDueDate(true);
} }
} },
}); });
calendar.setDate(dateFix); calendar.setDate(parsePikadayDate($dueDateInput.val()));
this.$datePicker.append(calendar.el); this.$datePicker.append(calendar.el);
this.$datePicker.data('pikaday', calendar); this.$datePicker.data('pikaday', calendar);
} }
...@@ -79,8 +78,8 @@ class DueDateSelect { ...@@ -79,8 +78,8 @@ class DueDateSelect {
gl.issueBoards.BoardsStore.detail.issue.dueDate = ''; gl.issueBoards.BoardsStore.detail.issue.dueDate = '';
this.updateIssueBoardIssue(); this.updateIssueBoardIssue();
} else { } else {
$("input[name='" + this.fieldName + "']").val(''); $(`input[name='${this.fieldName}']`).val('');
return this.saveDueDate(false); this.saveDueDate(false);
} }
}); });
} }
...@@ -111,7 +110,7 @@ class DueDateSelect { ...@@ -111,7 +110,7 @@ class DueDateSelect {
this.datePayload = datePayload; this.datePayload = datePayload;
} }
updateIssueBoardIssue () { updateIssueBoardIssue() {
this.$loading.fadeIn(); this.$loading.fadeIn();
this.$dropdown.trigger('loading.gl.dropdown'); this.$dropdown.trigger('loading.gl.dropdown');
this.$selectbox.hide(); this.$selectbox.hide();
...@@ -149,8 +148,8 @@ class DueDateSelect { ...@@ -149,8 +148,8 @@ class DueDateSelect {
return selectedDateValue.length ? return selectedDateValue.length ?
$('.js-remove-due-date-holder').removeClass('hidden') : $('.js-remove-due-date-holder').removeClass('hidden') :
$('.js-remove-due-date-holder').addClass('hidden'); $('.js-remove-due-date-holder').addClass('hidden');
} },
}).done((data) => { }).done(() => {
if (isDropdown) { if (isDropdown) {
this.$dropdown.trigger('loaded.gl.dropdown'); this.$dropdown.trigger('loaded.gl.dropdown');
this.$dropdown.dropdown('toggle'); this.$dropdown.dropdown('toggle');
...@@ -160,27 +159,28 @@ class DueDateSelect { ...@@ -160,27 +159,28 @@ class DueDateSelect {
} }
} }
class DueDateSelectors { export default class DueDateSelectors {
constructor() { constructor() {
this.initMilestoneDatePicker(); this.initMilestoneDatePicker();
this.initIssuableSelect(); this.initIssuableSelect();
} }
// eslint-disable-next-line class-methods-use-this
initMilestoneDatePicker() { initMilestoneDatePicker() {
$('.datepicker').each(function() { $('.datepicker').each(function initPikadayMilestone() {
const $datePicker = $(this); const $datePicker = $(this);
const dateFix = DateFix.dashedFix($datePicker.val());
const calendar = new Pikaday({ const calendar = new Pikaday({
field: $datePicker.get(0), field: $datePicker.get(0),
theme: 'gitlab-theme animate-picker', theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd', format: 'yyyy-mm-dd',
container: $datePicker.parent().get(0), container: $datePicker.parent().get(0),
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
onSelect(dateText) { 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); $datePicker.data('pikaday', calendar);
}); });
...@@ -191,19 +191,17 @@ class DueDateSelectors { ...@@ -191,19 +191,17 @@ class DueDateSelectors {
calendar.setDate(null); calendar.setDate(null);
}); });
} }
// eslint-disable-next-line class-methods-use-this
initIssuableSelect() { initIssuableSelect() {
const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide(); const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide();
$('.js-due-date-select').each((i, dropdown) => { $('.js-due-date-select').each((i, dropdown) => {
const $dropdown = $(dropdown); const $dropdown = $(dropdown);
// eslint-disable-next-line no-new
new DueDateSelect({ new DueDateSelect({
$dropdown, $dropdown,
$loading $loading,
}); });
}); });
} }
} }
window.gl = window.gl || {};
window.gl.DueDateSelectors = DueDateSelectors;
...@@ -6,10 +6,11 @@ import _ from 'underscore'; ...@@ -6,10 +6,11 @@ import _ from 'underscore';
*/ */
export default class FilterableList { export default class FilterableList {
constructor(form, filter, holder) { constructor(form, filter, holder, filterInputField = 'filter_groups') {
this.filterForm = form; this.filterForm = form;
this.listFilterElement = filter; this.listFilterElement = filter;
this.listHolderElement = holder; this.listHolderElement = holder;
this.filterInputField = filterInputField;
this.isBusy = false; this.isBusy = false;
} }
...@@ -32,10 +33,10 @@ export default class FilterableList { ...@@ -32,10 +33,10 @@ export default class FilterableList {
onFilterInput() { onFilterInput() {
const $form = $(this.filterForm); const $form = $(this.filterForm);
const queryData = {}; const queryData = {};
const filterGroupsParam = $form.find('[name="filter_groups"]').val(); const filterGroupsParam = $form.find(`[name="${this.filterInputField}"]`).val();
if (filterGroupsParam) { if (filterGroupsParam) {
queryData.filter_groups = filterGroupsParam; queryData[this.filterInputField] = filterGroupsParam;
} }
this.filterResults(queryData); this.filterResults(queryData);
......
...@@ -123,8 +123,8 @@ class FilteredSearchVisualTokens { ...@@ -123,8 +123,8 @@ class FilteredSearchVisualTokens {
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
tokenValueContainer.dataset.originalValue = tokenValue; tokenValueContainer.dataset.originalValue = tokenValue;
tokenValueElement.innerHTML = ` tokenValueElement.innerHTML = `
<img class="avatar s20" src="${user.avatar_url}" alt="${user.name}'s avatar"> <img class="avatar s20" src="${user.avatar_url}" alt="">
${user.name} ${_.escape(user.name)}
`; `;
/* eslint-enable no-param-reassign */ /* eslint-enable no-param-reassign */
}) })
......
...@@ -40,6 +40,10 @@ const createFlashEl = (message, type, isInContentWrapper = false) => ` ...@@ -40,6 +40,10 @@ const createFlashEl = (message, type, isInContentWrapper = false) => `
</div> </div>
`; `;
const removeFlashClickListener = (flashEl, fadeTransition) => {
flashEl.parentNode.addEventListener('click', () => hideFlash(flashEl, fadeTransition));
};
/* /*
* Flash banner supports different types of Flash configurations * Flash banner supports different types of Flash configurations
* along with ability to provide actionConfig which can be used to show * along with ability to provide actionConfig which can be used to show
...@@ -70,7 +74,7 @@ const createFlash = function createFlash( ...@@ -70,7 +74,7 @@ const createFlash = function createFlash(
flashContainer.innerHTML = createFlashEl(message, type, isInContentWrapper); flashContainer.innerHTML = createFlashEl(message, type, isInContentWrapper);
const flashEl = flashContainer.querySelector(`.flash-${type}`); const flashEl = flashContainer.querySelector(`.flash-${type}`);
flashEl.addEventListener('click', () => hideFlash(flashEl, fadeTransition)); removeFlashClickListener(flashEl, fadeTransition);
if (actionConfig) { if (actionConfig) {
flashEl.innerHTML += createAction(actionConfig); flashEl.innerHTML += createAction(actionConfig);
...@@ -90,5 +94,6 @@ export { ...@@ -90,5 +94,6 @@ export {
createFlashEl, createFlashEl,
createAction, createAction,
hideFlash, hideFlash,
removeFlashClickListener,
}; };
window.Flash = createFlash; window.Flash = createFlash;
/* global DropzoneInput */
/* global autosize */ /* global autosize */
import GfmAutoComplete from './gfm_auto_complete'; import GfmAutoComplete from './gfm_auto_complete';
import dropzoneInput from './dropzone_input';
export default class GLForm { export default class GLForm {
constructor(form, enableGFM = false) { constructor(form, enableGFM = false) {
...@@ -41,7 +41,7 @@ export default class GLForm { ...@@ -41,7 +41,7 @@ export default class GLForm {
mergeRequests: this.enableGFM, mergeRequests: this.enableGFM,
labels: this.enableGFM, labels: this.enableGFM,
}); });
new DropzoneInput(this.form); // eslint-disable-line no-new dropzoneInput(this.form);
autosize(this.textarea); autosize(this.textarea);
} }
// form and textarea event listeners // form and textarea event listeners
......
<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>
<script> <script>
import { n__ } from '../../locale';
import { MAX_CHILDREN_COUNT } from '../constants';
export default { export default {
props: { props: {
groups: { parentGroup: {
type: Object,
required: true,
},
baseGroup: {
type: Object, type: Object,
required: false, required: false,
default: () => ({}), 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> </script>
...@@ -20,8 +32,20 @@ export default { ...@@ -20,8 +32,20 @@ export default {
v-for="(group, index) in groups" v-for="(group, index) in groups"
:key="index" :key="index"
:group="group" :group="group"
:base-group="baseGroup" :parent-group="parentGroup"
:collection="groups"
/> />
<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> </ul>
</template> </template>
...@@ -2,49 +2,28 @@ ...@@ -2,49 +2,28 @@
import identicon from '../../vue_shared/components/identicon.vue'; import identicon from '../../vue_shared/components/identicon.vue';
import eventHub from '../event_hub'; 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 { export default {
components: { components: {
identicon, identicon,
itemCaret,
itemTypeIcon,
itemStats,
itemActions,
}, },
props: { props: {
group: { parentGroup: {
type: Object,
required: true,
},
baseGroup: {
type: Object, type: Object,
required: false, required: false,
default: () => ({}), default: () => ({}),
}, },
collection: { group: {
type: Object, type: Object,
required: false, required: true,
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: { computed: {
...@@ -53,51 +32,33 @@ export default { ...@@ -53,51 +32,33 @@ export default {
}, },
rowClass() { rowClass() {
return { return {
'group-row': true,
'is-open': this.group.isOpen, 'is-open': this.group.isOpen,
'has-subgroups': this.group.hasSubgroups, 'has-children': this.hasChildren,
'no-description': !this.group.description, 'has-description': this.group.description,
'being-removed': this.group.isBeingRemoved,
}; };
}, },
visibilityIcon() { hasChildren() {
return { return this.group.childrenCount > 0;
fa: true,
'fa-globe': this.group.visibility === 'public',
'fa-shield': this.group.visibility === 'internal',
'fa-lock': this.group.visibility === 'private',
};
}, },
fullPath() { hasAvatar() {
let fullPath = ''; return this.group.avatarUrl !== null;
},
if (this.group.isOrphan) { isGroup() {
// check if current group is baseGroup return this.group.type === 'group';
if (Object.keys(this.baseGroup).length > 0 && this.baseGroup !== this.group) { },
// Remove baseGroup prefix from our current group.fullName. e.g: },
// baseGroup.fullName: `level1` methods: {
// group.fullName: `level1 / level2 / level3` onClickRowGroup(e) {
// Result: `level2 / level3` const NO_EXPAND_CLS = 'no-expand';
const gfn = this.group.fullName; if (!(e.target.classList.contains(NO_EXPAND_CLS) ||
const bfn = this.baseGroup.fullName; e.target.parentElement.classList.contains(NO_EXPAND_CLS))) {
const length = bfn.length; if (this.hasChildren) {
const start = gfn.indexOf(bfn); eventHub.$emit('toggleChildren', this.group);
const extraPrefixChars = 3;
fullPath = gfn.substr(start + length + extraPrefixChars);
} else { } else {
fullPath = this.group.fullName; gl.utils.visitUrl(this.group.relativePath);
} }
} else {
fullPath = this.group.name;
} }
return fullPath;
},
hasGroups() {
return Object.keys(this.group.subGroups).length > 0;
},
hasAvatar() {
return this.group.avatarUrl && this.group.avatarUrl.indexOf('/assets/no_group_avatar') === -1;
}, },
}, },
}; };
...@@ -108,98 +69,36 @@ export default { ...@@ -108,98 +69,36 @@ export default {
@click.stop="onClickRowGroup" @click.stop="onClickRowGroup"
:id="groupDomId" :id="groupDomId"
:class="rowClass" :class="rowClass"
class="group-row"
> >
<div <div
class="group-row-contents"> class="group-row-contents">
<div <item-actions
class="controls"> v-if="isGroup"
<a :group="group"
v-if="group.canEdit" :parent-group="parentGroup"
class="edit-group btn" />
:href="group.editPath"> <item-stats
<i :item="group"
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>
<div <div
class="folder-toggle-wrap"> class="folder-toggle-wrap">
<span <item-caret
class="folder-caret" :is-group-open="group.isOpen"
v-if="group.hasSubgroups"> />
<i <item-type-icon
v-if="group.isOpen" :item-type="group.type"
class="fa fa-caret-down" :is-group-open="group.isOpen"
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>
</div> </div>
<div <div
class="avatar-container s40 hidden-xs"> class="avatar-container s40 hidden-xs"
:class="{ 'content-loading': group.isChildrenLoading }"
>
<a <a
:href="group.groupPath"> :href="group.relativePath"
class="no-expand"
>
<img <img
v-if="hasAvatar" v-if="hasAvatar"
class="avatar s40" class="avatar s40"
...@@ -215,19 +114,22 @@ export default { ...@@ -215,19 +114,22 @@ export default {
<div <div
class="title"> class="title">
<a <a
:href="group.groupPath">{{fullPath}}</a> :href="group.relativePath"
<template v-if="group.permissions.humanGroupAccess"> class="no-expand">{{group.fullName}}</a>
as <span
<span class="access-type">{{group.permissions.humanGroupAccess}}</span> v-if="group.permission"
</template> class="access-type"
>
{{s__('GroupsTreeRole|as')}} {{group.permission}}
</span>
</div> </div>
<div <div
class="description">{{group.description}}</div> class="description">{{group.description}}</div>
</div> </div>
<group-folder <group-folder
v-if="group.isOpen && hasGroups" v-if="group.isOpen && hasChildren"
:groups="group.subGroups" :parent-group="group"
:baseGroup="group" :groups="group.children"
/> />
</li> </li>
</template> </template>
...@@ -4,24 +4,33 @@ import eventHub from '../event_hub'; ...@@ -4,24 +4,33 @@ import eventHub from '../event_hub';
import { getParameterByName } from '../../lib/utils/common_utils'; import { getParameterByName } from '../../lib/utils/common_utils';
export default { export default {
components: {
tablePagination,
},
props: { props: {
groups: { groups: {
type: Object, type: Array,
required: true, required: true,
}, },
pageInfo: { pageInfo: {
type: Object, type: Object,
required: true, required: true,
}, },
}, searchEmpty: {
components: { type: Boolean,
tablePagination, required: true,
},
searchEmptyMessage: {
type: String,
required: true,
},
}, },
methods: { methods: {
change(page) { change(page) {
const filterGroupsParam = getParameterByName('filter_groups'); const filterGroupsParam = getParameterByName('filter_groups');
const sortParam = getParameterByName('sort'); 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 { ...@@ -29,10 +38,17 @@ export default {
<template> <template>
<div class="groups-list-tree-container"> <div class="groups-list-tree-container">
<div
v-if="searchEmpty"
class="has-no-search-results">
{{searchEmptyMessage}}
</div>
<group-folder <group-folder
v-if="!searchEmpty"
:groups="groups" :groups="groups"
/> />
<table-pagination <table-pagination
v-if="!searchEmpty"
:change="change" :change="change"
:pageInfo="pageInfo" :pageInfo="pageInfo"
/> />
......
<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>
<script> <script>
const RepoFileOptions = { export default {
props: { props: {
isMini: { isGroupOpen: {
type: Boolean, type: Boolean,
required: false, required: true,
default: false, default: false,
}, },
projectName: { },
type: String, computed: {
required: true, iconClass() {
return this.isGroupOpen ? 'fa-caret-down' : 'fa-caret-right';
}, },
}, },
}; };
export default RepoFileOptions;
</script> </script>
<template> <template>
<tr v-if="isMini" class="repo-file-options"> <span class="folder-caret">
<td> <i
<span class="title">{{projectName}}</span> :class="iconClass"
</td> class="fa"
</tr> aria-hidden="true"/>
</span>
</template> </template>
<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>
<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>
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'; ...@@ -3,12 +3,13 @@ import eventHub from './event_hub';
import { getParameterByName } from '../lib/utils/common_utils'; import { getParameterByName } from '../lib/utils/common_utils';
export default class GroupFilterableList extends FilterableList { export default class GroupFilterableList extends FilterableList {
constructor({ form, filter, holder, filterEndpoint, pagePath }) { constructor({ form, filter, holder, filterEndpoint, pagePath, dropdownSel, filterInputField }) {
super(form, filter, holder); super(form, filter, holder, filterInputField);
this.form = form; this.form = form;
this.filterEndpoint = filterEndpoint; this.filterEndpoint = filterEndpoint;
this.pagePath = pagePath; this.pagePath = pagePath;
this.$dropdown = $('.js-group-filter-dropdown-wrap'); this.filterInputField = filterInputField;
this.$dropdown = $(dropdownSel);
} }
getFilterEndpoint() { getFilterEndpoint() {
...@@ -24,30 +25,34 @@ export default class GroupFilterableList extends FilterableList { ...@@ -24,30 +25,34 @@ export default class GroupFilterableList extends FilterableList {
bindEvents() { bindEvents() {
super.bindEvents(); super.bindEvents();
this.onFormSubmitWrapper = this.onFormSubmit.bind(this);
this.onFilterOptionClikWrapper = this.onOptionClick.bind(this); this.onFilterOptionClikWrapper = this.onOptionClick.bind(this);
this.filterForm.addEventListener('submit', this.onFormSubmitWrapper);
this.$dropdown.on('click', 'a', this.onFilterOptionClikWrapper); this.$dropdown.on('click', 'a', this.onFilterOptionClikWrapper);
} }
onFormSubmit(e) { onFilterInput() {
e.preventDefault();
const $form = $(this.form);
const filterGroupsParam = $form.find('[name="filter_groups"]').val();
const queryData = {}; const queryData = {};
const $form = $(this.form);
const archivedParam = getParameterByName('archived', window.location.href);
const filterGroupsParam = $form.find(`[name="${this.filterInputField}"]`).val();
if (filterGroupsParam) { if (filterGroupsParam) {
queryData.filter_groups = filterGroupsParam; queryData[this.filterInputField] = filterGroupsParam;
}
if (archivedParam) {
queryData.archived = archivedParam;
} }
this.filterResults(queryData); this.filterResults(queryData);
this.setDefaultFilterOption();
if (this.setDefaultFilterOption) {
this.setDefaultFilterOption();
}
} }
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); this.$dropdown.find('.dropdown-label').text(defaultOption);
} }
...@@ -55,23 +60,42 @@ export default class GroupFilterableList extends FilterableList { ...@@ -55,23 +60,42 @@ export default class GroupFilterableList extends FilterableList {
e.preventDefault(); e.preventDefault();
const queryData = {}; 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) { if (sortParam) {
queryData.sort = sortParam; queryData.sort = sortParam;
} }
if (archivedParam) {
queryData.archived = archivedParam;
}
this.filterResults(queryData); this.filterResults(queryData);
// Active selected option // 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 // Clear current value on search form
this.form.querySelector('[name="filter_groups"]').value = ''; this.form.querySelector(`[name="${this.filterInputField}"]`).value = '';
} }
onFilterSuccess(data, xhr, queryData) { onFilterSuccess(data, xhr, queryData) {
super.onFilterSuccess(data, xhr, queryData); const currentPath = this.getPagePath(queryData);
const paginationData = { const paginationData = {
'X-Per-Page': xhr.getResponseHeader('X-Per-Page'), 'X-Per-Page': xhr.getResponseHeader('X-Per-Page'),
...@@ -82,7 +106,11 @@ export default class GroupFilterableList extends FilterableList { ...@@ -82,7 +106,11 @@ export default class GroupFilterableList extends FilterableList {
'X-Prev-Page': xhr.getResponseHeader('X-Prev-Page'), '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); eventHub.$emit('updatePagination', paginationData);
} }
} }
import Vue from 'vue'; import Vue from 'vue';
import Flash from '../flash'; import Translate from '../vue_shared/translate';
import GroupFilterableList from './groups_filterable_list'; import GroupFilterableList from './groups_filterable_list';
import GroupsComponent from './components/groups.vue'; import GroupsStore from './store/groups_store';
import GroupFolder from './components/group_folder.vue'; import GroupsService from './service/groups_service';
import GroupItem from './components/group_item.vue';
import GroupsStore from './stores/groups_store'; import groupsApp from './components/app.vue';
import GroupsService from './services/groups_service'; import groupFolderComponent from './components/group_folder.vue';
import eventHub from './event_hub'; import groupItemComponent from './components/group_item.vue';
import { getParameterByName } from '../lib/utils/common_utils';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => { 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) // Don't do anything if element doesn't exist (No groups)
// This is for when the user enters directly to the page via URL // This is for when the user enters directly to the page via URL
...@@ -18,176 +19,56 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -18,176 +19,56 @@ document.addEventListener('DOMContentLoaded', () => {
return; return;
} }
Vue.component('groups-component', GroupsComponent); Vue.component('group-folder', groupFolderComponent);
Vue.component('group-folder', GroupFolder); Vue.component('group-item', groupItemComponent);
Vue.component('group-item', GroupItem);
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el, el,
components: {
groupsApp,
},
data() { data() {
this.store = new GroupsStore(); const dataset = this.$options.el.dataset;
this.service = new GroupsService(el.dataset.endpoint); const hideProjects = dataset.hideProjects === 'true';
const store = new GroupsStore(hideProjects);
const service = new GroupsService(dataset.endpoint);
return { return {
store: this.store, store,
isLoading: true, service,
state: this.store.state, hideProjects,
loading: true, 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() { beforeMount() {
const dataset = this.$options.el.dataset;
let groupFilterList = null; let groupFilterList = null;
const form = document.querySelector('form#group-filter-form'); const form = document.querySelector(dataset.formSel);
const filter = document.querySelector('.js-groups-list-filter'); const filter = document.querySelector(dataset.filterSel);
const holder = document.querySelector('.js-groups-list-holder'); const holder = document.querySelector(dataset.holderSel);
const opts = { const opts = {
form, form,
filter, filter,
holder, holder,
filterEndpoint: el.dataset.endpoint, filterEndpoint: dataset.endpoint,
pagePath: el.dataset.path, pagePath: dataset.path,
dropdownSel: dataset.dropdownSel,
filterInputField: 'filter',
}; };
groupFilterList = new GroupFilterableList(opts); groupFilterList = new GroupFilterableList(opts);
groupFilterList.initSearch(); groupFilterList.initSearch();
}, },
mounted() { render(createElement) {
this.fetchGroups() return createElement('groups-app', {
.then((response) => { props: {
this.updatePagination(response.headers); store: this.store,
this.isLoading = false; service: this.service,
}) hideProjects: this.hideProjects,
.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);
}, },
}); });
}); });
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 { ...@@ -8,7 +8,7 @@ export default class GroupsService {
this.groups = Vue.resource(endpoint); this.groups = Vue.resource(endpoint);
} }
getGroups(parentId, page, filterGroups, sort) { getGroups(parentId, page, filterGroups, sort, archived) {
const data = {}; const data = {};
if (parentId) { if (parentId) {
...@@ -20,12 +20,16 @@ export default class GroupsService { ...@@ -20,12 +20,16 @@ export default class GroupsService {
} }
if (filterGroups) { if (filterGroups) {
data.filter_groups = filterGroups; data.filter = filterGroups;
} }
if (sort) { if (sort) {
data.sort = sort; data.sort = sort;
} }
if (archived) {
data.archived = archived;
}
} }
return this.groups.get(data); return this.groups.get(data);
......
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);
}
}
}
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 @@ ...@@ -4,6 +4,8 @@
/* global IssuableContext */ /* global IssuableContext */
/* global Sidebar */ /* global Sidebar */
import DueDateSelectors from './due_date_select';
export default () => { export default () => {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML); const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
...@@ -13,6 +15,6 @@ export default () => { ...@@ -13,6 +15,6 @@ export default () => {
new LabelsSelect(); new LabelsSelect();
new IssuableContext(sidebarOptions.currentUser); new IssuableContext(sidebarOptions.currentUser);
gl.Subscription.bindAll('.subscription'); gl.Subscription.bindAll('.subscription');
new gl.DueDateSelectors(); new DueDateSelectors();
window.sidebar = new Sidebar(); window.sidebar = new Sidebar();
}; };
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */ /* eslint-disable func-names, 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 GitLab */
/* global Autosave */ /* global Autosave */
/* global dateFormat */
import Pikaday from 'pikaday'; import Pikaday from 'pikaday';
import UsersSelect from './users_select'; import UsersSelect from './users_select';
import GfmAutoComplete from './gfm_auto_complete'; import GfmAutoComplete from './gfm_auto_complete';
import ZenMode from './zen_mode'; import ZenMode from './zen_mode';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
(function() { (function() {
this.IssuableForm = (function() { this.IssuableForm = (function() {
...@@ -38,11 +38,13 @@ import ZenMode from './zen_mode'; ...@@ -38,11 +38,13 @@ import ZenMode from './zen_mode';
theme: 'gitlab-theme animate-picker', theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd', format: 'yyyy-mm-dd',
container: $issuableDueDate.parent().get(0), container: $issuableDueDate.parent().get(0),
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
onSelect: function(dateText) { 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 { ...@@ -24,6 +24,11 @@ export default {
required: true, required: true,
type: Boolean, type: Boolean,
}, },
showInlineEditButton: {
type: Boolean,
required: false,
default: false,
},
issuableRef: { issuableRef: {
type: String, type: String,
required: true, required: true,
...@@ -222,20 +227,25 @@ export default { ...@@ -222,20 +227,25 @@ export default {
<div v-else> <div v-else>
<title-component <title-component
:issuable-ref="issuableRef" :issuable-ref="issuableRef"
:can-update="canUpdate"
:title-html="state.titleHtml" :title-html="state.titleHtml"
:title-text="state.titleText" /> :title-text="state.titleText"
:show-inline-edit-button="showInlineEditButton"
/>
<description-component <description-component
v-if="state.descriptionHtml" v-if="state.descriptionHtml"
:can-update="canUpdate" :can-update="canUpdate"
:description-html="state.descriptionHtml" :description-html="state.descriptionHtml"
:description-text="state.descriptionText" :description-text="state.descriptionText"
:updated-at="state.updatedAt" :updated-at="state.updatedAt"
:task-status="state.taskStatus" /> :task-status="state.taskStatus"
/>
<edited-component <edited-component
v-if="hasUpdated" v-if="hasUpdated"
:updated-at="state.updatedAt" :updated-at="state.updatedAt"
:updated-by-name="state.updatedByName" :updated-by-name="state.updatedByName"
:updated-by-path="state.updatedByPath" /> :updated-by-path="state.updatedByPath"
/>
</div> </div>
</div> </div>
</template> </template>
...@@ -16,15 +16,15 @@ ...@@ -16,15 +16,15 @@
<fieldset> <fieldset>
<label <label
class="sr-only" class="sr-only"
for="issue-title"> for="issuable-title">
Title Title
</label> </label>
<input <input
id="issue-title" id="issuable-title"
class="form-control" class="form-control"
type="text" type="text"
placeholder="Issue title" placeholder="Title"
aria-label="Issue title" aria-label="Title"
v-model="formState.title" v-model="formState.title"
@keydown.meta.enter="updateIssuable" @keydown.meta.enter="updateIssuable"
@keydown.ctrl.enter="updateIssuable" /> @keydown.ctrl.enter="updateIssuable" />
......
<script> <script>
import animateMixin from '../mixins/animate'; 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 { export default {
mixins: [animateMixin], mixins: [animateMixin],
...@@ -15,6 +18,11 @@ ...@@ -15,6 +18,11 @@
type: String, type: String,
required: true, required: true,
}, },
canUpdate: {
required: false,
type: Boolean,
default: false,
},
titleHtml: { titleHtml: {
type: String, type: String,
required: true, required: true,
...@@ -23,6 +31,14 @@ ...@@ -23,6 +31,14 @@
type: String, type: String,
required: true, required: true,
}, },
showInlineEditButton: {
type: Boolean,
required: false,
default: false,
},
},
directives: {
tooltip,
}, },
watch: { watch: {
titleHtml() { titleHtml() {
...@@ -30,24 +46,46 @@ ...@@ -30,24 +46,46 @@
this.animateChange(); this.animateChange();
}, },
}, },
computed: {
pencilIcon() {
return spriteIcon('pencil', 'link-highlight');
},
},
methods: { methods: {
setPageTitle() { setPageTitle() {
const currentPageTitleScope = this.titleEl.innerText.split('·'); const currentPageTitleScope = this.titleEl.innerText.split('·');
currentPageTitleScope[0] = `${this.titleText} (${this.issuableRef}) `; currentPageTitleScope[0] = `${this.titleText} (${this.issuableRef}) `;
this.titleEl.textContent = currentPageTitleScope.join('·'); this.titleEl.textContent = currentPageTitleScope.join('·');
}, },
edit() {
eventHub.$emit('open.form');
},
}, },
}; };
</script> </script>
<template> <template>
<h2 <div class="title-container">
class="title" <h2
:class="{ class="title"
'issue-realtime-pre-pulse': preAnimation, :class="{
'issue-realtime-trigger-pulse': pulseAnimation 'issue-realtime-pre-pulse': preAnimation,
}" 'issue-realtime-trigger-pulse': pulseAnimation
v-html="titleHtml" }"
> v-html="titleHtml"
</h2> >
</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> </template>
...@@ -43,16 +43,6 @@ ...@@ -43,16 +43,6 @@
type: 'link', 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; return actions;
}, },
}, },
......
...@@ -403,7 +403,11 @@ export const setCiStatusFavicon = (pageUrl) => { ...@@ -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}`; export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
......
const DateFix = {
dashedFix(val) { export const pad = (val, len = 2) => (`0${val}`).slice(-len);
const [y, m, d] = val.split('-');
return new Date(y, m - 1, d); /**
}, * 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) { ...@@ -85,7 +85,7 @@ w.gl.utils.getLocationHash = function(url) {
return hashIndex === -1 ? null : url.substring(hashIndex + 1); 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 // eslint-disable-next-line import/prefer-default-export
export function visitUrl(url, external = false) { export function visitUrl(url, external = false) {
...@@ -96,7 +96,7 @@ export function visitUrl(url, external = false) { ...@@ -96,7 +96,7 @@ export function visitUrl(url, external = false) {
otherWindow.opener = null; otherWindow.opener = null;
otherWindow.location = url; otherWindow.location = url;
} else { } else {
document.location.href = url; window.location.href = url;
} }
} }
......
...@@ -21,15 +21,6 @@ window._ = _; ...@@ -21,15 +21,6 @@ window._ = _;
window.Dropzone = Dropzone; window.Dropzone = Dropzone;
window.Sortable = Sortable; 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 // templates
import './templates/issuable_template_selector'; import './templates/issuable_template_selector';
import './templates/issuable_template_selectors'; import './templates/issuable_template_selectors';
...@@ -53,7 +44,6 @@ import './aside'; ...@@ -53,7 +44,6 @@ import './aside';
import './autosave'; import './autosave';
import loadAwardsHandler from './awards_handler'; import loadAwardsHandler from './awards_handler';
import bp from './breakpoints'; import bp from './breakpoints';
import './broadcast_message';
import './commits'; import './commits';
import './compare'; import './compare';
import './compare_autocomplete'; import './compare_autocomplete';
...@@ -61,10 +51,8 @@ import './confirm_danger_modal'; ...@@ -61,10 +51,8 @@ import './confirm_danger_modal';
import './copy_as_gfm'; import './copy_as_gfm';
import './copy_to_clipboard'; import './copy_to_clipboard';
import './diff'; import './diff';
import './dropzone_input';
import './due_date_select';
import './files_comment_button'; import './files_comment_button';
import Flash from './flash'; import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown'; import './gl_dropdown';
import './gl_field_error'; import './gl_field_error';
import './gl_field_errors'; import './gl_field_errors';
...@@ -84,8 +72,6 @@ import './layout_nav'; ...@@ -84,8 +72,6 @@ import './layout_nav';
import LazyLoader from './lazy_loader'; import LazyLoader from './lazy_loader';
import './line_highlighter'; import './line_highlighter';
import './logo'; import './logo';
import './member_expiration_date';
import './members';
import './merge_request'; import './merge_request';
import './merge_request_tabs'; import './merge_request_tabs';
import './milestone'; import './milestone';
...@@ -339,4 +325,10 @@ $(function () { ...@@ -339,4 +325,10 @@ $(function () {
event.preventDefault(); event.preventDefault();
gl.utils.visitUrl(`${action}${$(this).serialize()}`); gl.utils.visitUrl(`${action}${$(this).serialize()}`);
}); });
const flashContainer = document.querySelector('.flash-container');
if (flashContainer && flashContainer.children.length) {
removeFlashClickListener(flashContainer.children[0]);
}
}); });
/* global dateFormat */
import Pikaday from 'pikaday'; import Pikaday from 'pikaday';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
(() => {
// Add datepickers to all `js-access-expiration-date` elements. If those elements are // 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 // 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 // `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. // datepicker, and make clicking on that element clear the field.
// //
window.gl = window.gl || {}; export default function memberExpirationDate(selector = '.js-access-expiration-date') {
gl.MemberExpirationDate = (selector = '.js-access-expiration-date') => { function toggleClearInput() {
function toggleClearInput() { $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
$(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== ''); }
} const inputs = $(selector);
const inputs = $(selector);
inputs.each((i, el) => {
inputs.each((i, el) => { const $input = $(el);
const $input = $(el);
const calendar = new Pikaday({
const calendar = new Pikaday({ field: $input.get(0),
field: $input.get(0), theme: 'gitlab-theme animate-picker',
theme: 'gitlab-theme animate-picker', format: 'yyyy-mm-dd',
format: 'yyyy-mm-dd', minDate: new Date(),
minDate: new Date(), container: $input.parent().get(0),
container: $input.parent().get(0), parse: dateString => parsePikadayDate(dateString),
onSelect(dateText) { toString: date => pikadayToString(date),
$input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); onSelect(dateText) {
$input.val(calendar.toString(dateText));
$input.trigger('change');
$input.trigger('change');
toggleClearInput.call($input);
}, toggleClearInput.call($input);
}); },
calendar.setDate(new Date($input.val()));
$input.data('pikaday', calendar);
}); });
inputs.next('.js-clear-input').on('click', function clicked(event) { calendar.setDate(parsePikadayDate($input.val()));
event.preventDefault(); $input.data('pikaday', calendar);
});
const input = $(this).closest('.clearable-input').find(selector); inputs.next('.js-clear-input').on('click', function clicked(event) {
const calendar = input.data('pikaday'); event.preventDefault();
calendar.setDate(null); const input = $(this).closest('.clearable-input').find(selector);
input.trigger('change'); const calendar = input.data('pikaday');
toggleClearInput.call(input);
}); calendar.setDate(null);
input.trigger('change');
toggleClearInput.call(input);
});
inputs.on('blur', toggleClearInput); inputs.on('blur', toggleClearInput);
inputs.each(toggleClearInput); inputs.each(toggleClearInput);
}; }
}).call(window);
/* eslint-disable class-methods-use-this */ export default class Members {
(() => { constructor() {
window.gl = window.gl || {}; this.addListeners();
this.initGLDropdown();
class Members { }
constructor() {
this.addListeners();
this.initGLDropdown();
}
addListeners() { addListeners() {
$('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow); $('.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-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)); $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change'); gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
} }
initGLDropdown() { initGLDropdown() {
$('.js-member-permissions-dropdown').each((i, btn) => { $('.js-member-permissions-dropdown').each((i, btn) => {
const $btn = $(btn); const $btn = $(btn);
$btn.glDropdown({ $btn.glDropdown({
selectable: true, selectable: true,
isSelectable(selected, $el) { isSelectable(selected, $el) {
return !$el.hasClass('is-active'); return !$el.hasClass('is-active');
}, },
fieldName: $btn.data('field-name'), fieldName: $btn.data('field-name'),
id(selected, $el) { id(selected, $el) {
return $el.data('id'); return $el.data('id');
}, },
toggleLabel(selected, $el) { toggleLabel(selected, $el) {
return $el.text(); return $el.text();
}, },
clicked: (options) => { clicked: (options) => {
this.formSubmit(null, options.$el); this.formSubmit(null, options.$el);
}, },
});
}); });
} });
}
removeRow(e) { // eslint-disable-next-line class-methods-use-this
const $target = $(e.target); removeRow(e) {
const $target = $(e.target);
if ($target.hasClass('btn-remove')) { if ($target.hasClass('btn-remove')) {
$target.closest('.member') $target.closest('.member')
.fadeOut(function fadeOutMemberRow() { .fadeOut(function fadeOutMemberRow() {
$(this).remove(); $(this).remove();
}); });
}
} }
}
formSubmit(e, $el = null) { formSubmit(e, $el = null) {
const $this = e ? $(e.currentTarget) : $el; const $this = e ? $(e.currentTarget) : $el;
const { $toggle, $dateInput } = this.getMemberListItems($this); const { $toggle, $dateInput } = this.getMemberListItems($this);
$this.closest('form').trigger('submit.rails');
$toggle.disable();
$dateInput.disable();
}
formSuccess(e) { $this.closest('form').trigger('submit.rails');
const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member'));
$toggle.enable(); $toggle.disable();
$dateInput.enable(); $dateInput.disable();
} }
getMemberListItems($el) { formSuccess(e) {
const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`); const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member'));
return { $toggle.enable();
$memberListItem, $dateInput.enable();
$toggle: $memberListItem.find('.dropdown-menu-toggle'),
$dateInput: $memberListItem.find('.js-access-expiration-date'),
};
}
} }
// eslint-disable-next-line class-methods-use-this
getMemberListItems($el) {
const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`);
gl.Members = Members; return {
})(); $memberListItem,
$toggle: $memberListItem.find('.dropdown-menu-toggle'),
$dateInput: $memberListItem.find('.js-access-expiration-date'),
};
}
}
...@@ -146,7 +146,9 @@ import _ from 'underscore'; ...@@ -146,7 +146,9 @@ import _ from 'underscore';
clicked: function(options) { clicked: function(options) {
const { $el, e } = options; const { $el, e } = options;
let selected = options.selectedObj; let selected = options.selectedObj;
var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore; var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore;
if (!selected) return;
page = $('body').attr('data-page'); page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index'; isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index'); isMRIndex = (page === page && page === 'projects:merge_requests:index');
......
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, max-len */ /* 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'; import Network from './network';
$(function() { $(function() {
......
...@@ -13,7 +13,6 @@ import $ from 'jquery'; ...@@ -13,7 +13,6 @@ import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import autosize from 'vendor/autosize'; import autosize from 'vendor/autosize';
import Dropzone from 'dropzone';
import 'vendor/jquery.caret'; // required by jquery.atwho import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho'; import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache'; import AjaxCache from '~/lib/utils/ajax_cache';
...@@ -22,13 +21,11 @@ import CommentTypeToggle from './comment_type_toggle'; ...@@ -22,13 +21,11 @@ import CommentTypeToggle from './comment_type_toggle';
import GLForm from './gl_form'; import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler'; import loadAwardsHandler from './awards_handler';
import './autosave'; import './autosave';
import './dropzone_input';
import TaskList from './task_list'; import TaskList from './task_list';
import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils'; import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index'; import imageDiffHelper from './image_diff/helpers/index';
window.autosize = autosize; window.autosize = autosize;
window.Dropzone = Dropzone;
function normalizeNewlines(str) { function normalizeNewlines(str) {
return str.replace(/\r\n/g, '\n'); return str.replace(/\r\n/g, '\n');
......
...@@ -57,7 +57,7 @@ ...@@ -57,7 +57,7 @@
}, },
showError(message) { showError(message) {
Flash((errorMessages[message])); Flash(errorMessages[message]);
}, },
}, },
}; };
......
...@@ -57,7 +57,7 @@ ...@@ -57,7 +57,7 @@
}, },
showError(message) { showError(message) {
Flash((errorMessages[message])); Flash(errorMessages[message]);
}, },
}, },
}; };
......
...@@ -29,11 +29,9 @@ export const fetchList = ({ commit }, { repo, page }) => { ...@@ -29,11 +29,9 @@ export const fetchList = ({ commit }, { repo, page }) => {
}); });
}; };
export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath) export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath);
.then(res => res.json());
export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath) export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath);
.then(res => res.json());
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data); export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING); export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
...@@ -38,7 +38,7 @@ export default { ...@@ -38,7 +38,7 @@ export default {
tag: element.name, tag: element.name,
revision: element.revision, revision: element.revision,
shortRevision: element.short_revision, shortRevision: element.short_revision,
size: element.size, size: element.total_size,
layers: element.layers, layers: element.layers,
location: element.location, location: element.location,
createdAt: element.created_at, createdAt: element.created_at,
......
...@@ -11,7 +11,9 @@ import Helper from '../helpers/repo_helper'; ...@@ -11,7 +11,9 @@ import Helper from '../helpers/repo_helper';
import MonacoLoaderHelper from '../helpers/monaco_loader_helper'; import MonacoLoaderHelper from '../helpers/monaco_loader_helper';
export default { export default {
data: () => Store, data() {
return Store;
},
mixins: [RepoMixin], mixins: [RepoMixin],
components: { components: {
RepoSidebar, RepoSidebar,
......
...@@ -3,12 +3,20 @@ import Flash from '../../flash'; ...@@ -3,12 +3,20 @@ import Flash from '../../flash';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin'; import RepoMixin from '../mixins/repo_mixin';
import Service from '../services/repo_service'; import Service from '../services/repo_service';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import { visitUrl } from '../../lib/utils/url_utility';
export default { export default {
data: () => Store,
mixins: [RepoMixin], mixins: [RepoMixin],
data() {
return Store;
},
components: {
PopupDialog,
},
computed: { computed: {
showCommitable() { showCommitable() {
return this.isCommitable && this.changedFiles.length; return this.isCommitable && this.changedFiles.length;
...@@ -28,7 +36,16 @@ export default { ...@@ -28,7 +36,16 @@ export default {
}, },
methods: { 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 // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const commitMessage = this.commitMessage; const commitMessage = this.commitMessage;
const actions = this.changedFiles.map(f => ({ const actions = this.changedFiles.map(f => ({
...@@ -36,19 +53,63 @@ export default { ...@@ -36,19 +53,63 @@ export default {
file_path: f.path, file_path: f.path,
content: f.newContent, content: f.newContent,
})); }));
const branch = newBranch ? `${this.currentBranch}-${this.currentShortHash}` : this.currentBranch;
const payload = { const payload = {
branch: Store.currentBranch, branch,
commit_message: commitMessage, commit_message: commitMessage,
actions, actions,
}; };
Store.submitCommitsLoading = true; if (newBranch) {
payload.start_branch = this.currentBranch;
}
this.submitCommitsLoading = true;
Service.commitFiles(payload) Service.commitFiles(payload)
.then(this.resetCommitState) .then(() => {
.catch(() => Flash('An error occurred while committing your changes')); 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() { resetCommitState() {
this.submitCommitsLoading = false; this.submitCommitsLoading = false;
this.openedFiles = this.openedFiles.map((file) => {
const f = file;
f.changed = false;
return f;
});
this.changedFiles = []; this.changedFiles = [];
this.commitMessage = ''; this.commitMessage = '';
this.editMode = false; this.editMode = false;
...@@ -62,9 +123,17 @@ export default { ...@@ -62,9 +123,17 @@ export default {
<div <div
v-if="showCommitable" v-if="showCommitable"
id="commit-area"> 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 <form
class="form-horizontal" class="form-horizontal"
@submit.prevent="makeCommit"> @submit.prevent="tryCommit">
<fieldset> <fieldset>
<div class="form-group"> <div class="form-group">
<label class="col-md-4 control-label staged-files"> <label class="col-md-4 control-label staged-files">
...@@ -117,7 +186,7 @@ export default { ...@@ -117,7 +186,7 @@ export default {
class="btn btn-success"> class="btn btn-success">
<i <i
v-if="submitCommitsLoading" v-if="submitCommitsLoading"
class="fa fa-spinner fa-spin" class="js-commit-loading-icon fa fa-spinner fa-spin"
aria-hidden="true" aria-hidden="true"
aria-label="loading"> aria-label="loading">
</i> </i>
...@@ -126,6 +195,14 @@ export default { ...@@ -126,6 +195,14 @@ export default {
</span> </span>
</button> </button>
</div> </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> </fieldset>
</form> </form>
</div> </div>
......
...@@ -3,7 +3,9 @@ import Store from '../stores/repo_store'; ...@@ -3,7 +3,9 @@ import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin'; import RepoMixin from '../mixins/repo_mixin';
export default { export default {
data: () => Store, data() {
return Store;
},
mixins: [RepoMixin], mixins: [RepoMixin],
computed: { computed: {
buttonLabel() { buttonLabel() {
......
...@@ -5,7 +5,9 @@ import Service from '../services/repo_service'; ...@@ -5,7 +5,9 @@ import Service from '../services/repo_service';
import Helper from '../helpers/repo_helper'; import Helper from '../helpers/repo_helper';
const RepoEditor = { const RepoEditor = {
data: () => Store, data() {
return Store;
},
destroyed() { destroyed() {
if (Helper.monacoInstance) { if (Helper.monacoInstance) {
...@@ -22,7 +24,8 @@ const RepoEditor = { ...@@ -22,7 +24,8 @@ const RepoEditor = {
const monacoInstance = Helper.monaco.editor.create(this.$el, { const monacoInstance = Helper.monaco.editor.create(this.$el, {
model: null, model: null,
readOnly: false, readOnly: false,
contextmenu: false, contextmenu: true,
scrollBeyondLastLine: false,
}); });
Helper.monacoInstance = monacoInstance; Helper.monacoInstance = monacoInstance;
...@@ -92,7 +95,7 @@ const RepoEditor = { ...@@ -92,7 +95,7 @@ const RepoEditor = {
}, },
blobRaw() { blobRaw() {
if (Helper.monacoInstance && !this.isTree) { if (Helper.monacoInstance) {
this.setupEditor(); this.setupEditor();
} }
}, },
......
<script> <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 = { export default {
mixins: [TimeAgoMixin], mixins: [
props: { repoMixin,
file: { timeAgoMixin,
type: Object, ],
required: true, props: {
file: {
type: Object,
required: true,
},
}, },
isMini: { computed: {
type: Boolean, fileIcon() {
required: false, const classObj = {
default: false, '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: { methods: {
type: Object, linkClicked(file) {
required: false, eventHub.$emit('fileNameClicked', file);
default() { return { tree: false }; }, },
}, },
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> </script>
<template> <template>
<tr <tr
v-if="canShowFile" class="file"
class="file" @click.prevent="linkClicked(file)">
:class="activeFileClass" <td>
@click.prevent="linkClicked(file)"> <i
<td> class="fa fa-fw file-icon"
<i :class="fileIcon"
class="fa fa-fw file-icon" :style="levelIndentation"
:class="fileIcon" aria-hidden="true"
:style="fileIndentation" >
aria-label="file icon"> </i>
</i> <a
<a :href="file.url"
:href="file.url" class="repo-file-name"
class="repo-file-name" >
:title="file.url"> {{ file.name }}
{{file.name}} </a>
</a> <template v-if="file.type === 'submodule' && file.id">
</td> @
<span class="commit-sha">
<a
@click.stop
:href="file.tree_url"
>
{{ shortId }}
</a>
</span>
</template>
</td>
<template v-if="!isMini"> <template v-if="!isMini">
<td class="hidden-sm hidden-xs"> <td class="hidden-sm hidden-xs">
<div class="commit-message"> <a
<a @click.stop :href="file.lastCommitUrl"> @click.stop
{{file.lastCommitMessage}} :href="file.lastCommit.url"
class="commit-message"
>
{{ file.lastCommit.message }}
</a> </a>
</div> </td>
</td>
<td class="hidden-xs text-right"> <td class="commit-update hidden-xs text-right">
<span <span
class="commit-update" v-if="file.lastCommit.updatedAt"
:title="tooltipTitle(file.lastCommitUpdate)"> :title="tooltipTitle(file.lastCommit.updatedAt)"
{{timeFormated(file.lastCommitUpdate)}} >
</span> {{ timeFormated(file.lastCommit.updatedAt) }}
</td> </span>
</template> </td>
</tr> </template>
</tr>
</template> </template>
...@@ -4,7 +4,9 @@ import Helper from '../helpers/repo_helper'; ...@@ -4,7 +4,9 @@ import Helper from '../helpers/repo_helper';
import RepoMixin from '../mixins/repo_mixin'; import RepoMixin from '../mixins/repo_mixin';
const RepoFileButtons = { const RepoFileButtons = {
data: () => Store, data() {
return Store;
},
mixins: [RepoMixin], mixins: [RepoMixin],
......
<script> <script>
const RepoLoadingFile = { import repoMixin from '../mixins/repo_mixin';
props: {
loading: {
type: Object,
required: false,
default: {},
},
hasFiles: {
type: Boolean,
required: false,
default: false,
},
isMini: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
showGhostLines() {
return this.loading.tree && !this.hasFiles;
},
},
methods: { export default {
lineOfCode(n) { mixins: [
return `skeleton-line-${n}`; repoMixin,
],
methods: {
lineOfCode(n) {
return `skeleton-line-${n}`;
},
}, },
}, };
};
export default RepoLoadingFile;
</script> </script>
<template> <template>
<tr <tr
v-if="showGhostLines" class="loading-file"
class="loading-file"> aria-label="Loading files"
>
<td> <td>
<div <div
class="animation-container animation-container-small"> class="animation-container animation-container-small">
...@@ -48,29 +28,28 @@ export default RepoLoadingFile; ...@@ -48,29 +28,28 @@ export default RepoLoadingFile;
</div> </div>
</div> </div>
</td> </td>
<template v-if="!isMini">
<td <td
v-if="!isMini" class="hidden-sm hidden-xs">
class="hidden-sm hidden-xs"> <div class="animation-container">
<div class="animation-container"> <div
<div v-for="n in 6"
v-for="n in 6" :key="n"
:key="n" :class="lineOfCode(n)">
:class="lineOfCode(n)"> </div>
</div> </div>
</div> </td>
</td>
<td <td
v-if="!isMini" class="hidden-xs">
class="hidden-xs"> <div class="animation-container animation-container-small animation-container-right">
<div class="animation-container animation-container-small"> <div
<div v-for="n in 6"
v-for="n in 6" :key="n"
:key="n" :class="lineOfCode(n)">
:class="lineOfCode(n)"> </div>
</div> </div>
</div> </td>
</td> </template>
</tr> </tr>
</template> </template>
<script> <script>
import RepoMixin from '../mixins/repo_mixin'; import eventHub from '../event_hub';
import repoMixin from '../mixins/repo_mixin';
const RepoPreviousDirectory = { export default {
props: { mixins: [
prevUrl: { repoMixin,
type: String, ],
required: true, props: {
prevUrl: {
type: String,
required: true,
},
}, },
}, computed: {
colSpanCondition() {
mixins: [RepoMixin], return this.isMini ? undefined : 3;
},
computed: {
colSpanCondition() {
return this.isMini ? undefined : 3;
}, },
}, methods: {
linkClicked(file) {
methods: { eventHub.$emit('goToPreviousDirectoryClicked', file);
linkClicked(file) { },
this.$emit('linkclicked', file);
}, },
}, };
};
export default RepoPreviousDirectory;
</script> </script>
<template> <template>
<tr class="prev-directory"> <tr class="file prev-directory">
<td <td
:colspan="colSpanCondition" :colspan="colSpanCondition"
@click.prevent="linkClicked(prevUrl)"> class="table-cell"
<a :href="prevUrl">..</a> @click.prevent="linkClicked(prevUrl)"
</td> >
</tr> <a :href="prevUrl">...</a>
</td>
</tr>
</template> </template>
...@@ -4,7 +4,9 @@ ...@@ -4,7 +4,9 @@
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
export default { export default {
data: () => Store, data() {
return Store;
},
computed: { computed: {
html() { html() {
return this.activeFile.html; return this.activeFile.html;
......
<script> <script>
import _ from 'underscore';
import Service from '../services/repo_service'; import Service from '../services/repo_service';
import Helper from '../helpers/repo_helper'; import Helper from '../helpers/repo_helper';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import eventHub from '../event_hub';
import RepoPreviousDirectory from './repo_prev_directory.vue'; import RepoPreviousDirectory from './repo_prev_directory.vue';
import RepoFileOptions from './repo_file_options.vue';
import RepoFile from './repo_file.vue'; import RepoFile from './repo_file.vue';
import RepoLoadingFile from './repo_loading_file.vue'; import RepoLoadingFile from './repo_loading_file.vue';
import RepoMixin from '../mixins/repo_mixin'; import RepoMixin from '../mixins/repo_mixin';
...@@ -11,21 +12,35 @@ import RepoMixin from '../mixins/repo_mixin'; ...@@ -11,21 +12,35 @@ import RepoMixin from '../mixins/repo_mixin';
export default { export default {
mixins: [RepoMixin], mixins: [RepoMixin],
components: { components: {
'repo-file-options': RepoFileOptions,
'repo-previous-directory': RepoPreviousDirectory, 'repo-previous-directory': RepoPreviousDirectory,
'repo-file': RepoFile, 'repo-file': RepoFile,
'repo-loading-file': RepoLoadingFile, 'repo-loading-file': RepoLoadingFile,
}, },
created() { created() {
window.addEventListener('popstate', this.checkHistory); window.addEventListener('popstate', this.checkHistory);
}, },
destroyed() { destroyed() {
eventHub.$off('fileNameClicked', this.fileClicked);
eventHub.$off('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked);
window.removeEventListener('popstate', this.checkHistory); 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: { methods: {
checkHistory() { checkHistory() {
let selectedFile = this.files.find(file => location.pathname.indexOf(file.url) > -1); let selectedFile = this.files.find(file => location.pathname.indexOf(file.url) > -1);
...@@ -52,21 +67,25 @@ export default { ...@@ -52,21 +67,25 @@ export default {
}, },
fileClicked(clickedFile, lineNumber) { fileClicked(clickedFile, lineNumber) {
let file = clickedFile; const file = clickedFile;
if (file.loading) return; if (file.loading) return;
file.loading = true;
if (file.type === 'tree' && file.opened) { if (file.type === 'tree' && file.opened) {
file = Store.removeChildFilesOfTree(file); Helper.setDirectoryToClosed(file);
file.loading = false;
Store.setActiveLine(lineNumber); Store.setActiveLine(lineNumber);
} else if (file.type === 'submodule') {
file.loading = true;
gl.utils.visitUrl(file.url);
} else { } else {
const openFile = Helper.getFileFromPath(file.url); const openFile = Helper.getFileFromPath(file.url);
if (openFile) { if (openFile) {
file.loading = false;
Store.setActiveFiles(openFile); Store.setActiveFiles(openFile);
Store.setActiveLine(lineNumber); Store.setActiveLine(lineNumber);
} else { } else {
file.loading = true;
Service.url = file.url; Service.url = file.url;
Helper.getContent(file) Helper.getContent(file)
.then(() => { .then(() => {
...@@ -81,7 +100,7 @@ export default { ...@@ -81,7 +100,7 @@ export default {
goToPreviousDirectoryClicked(prevURL) { goToPreviousDirectoryClicked(prevURL) {
Service.url = prevURL; Service.url = prevURL;
Helper.getContent(null) Helper.getContent(null, true)
.then(() => Helper.scrollTabsRight()) .then(() => Helper.scrollTabsRight())
.catch(Helper.loadingError); .catch(Helper.loadingError);
}, },
...@@ -92,38 +111,43 @@ export default { ...@@ -92,38 +111,43 @@ export default {
<template> <template>
<div id="sidebar" :class="{'sidebar-mini' : isMini}"> <div id="sidebar" :class="{'sidebar-mini' : isMini}">
<table class="table"> <table class="table">
<thead v-if="!isMini"> <thead>
<tr> <tr>
<th class="name">Name</th> <th
<th class="hidden-sm hidden-xs last-commit">Last commit</th> v-if="isMini"
<th class="hidden-xs last-update text-right">Last update</th> 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> </tr>
</thead> </thead>
<tbody> <tbody>
<repo-file-options
:is-mini="isMini"
:project-name="projectName"
/>
<repo-previous-directory <repo-previous-directory
v-if="isRoot" v-if="!isRoot && !loading.tree"
:prev-url="prevURL" :prev-url="prevURL"
@linkclicked="goToPreviousDirectoryClicked(prevURL)"/> />
<repo-loading-file <repo-loading-file
v-if="!flattendFiles.length && loading.tree"
v-for="n in 5" v-for="n in 5"
:key="n" :key="n"
:loading="loading"
:has-files="!!files.length"
:is-mini="isMini"
/> />
<repo-file <repo-file
v-for="file in files" v-for="file in flattendFiles"
:key="file.id" :key="file.id"
:file="file" :file="file"
:is-mini="isMini"
@linkclicked="fileClicked(file)"
:is-tree="isTree"
:has-files="!!files.length"
:active-file="activeFile"
/> />
</tbody> </tbody>
</table> </table>
......
...@@ -26,11 +26,13 @@ const RepoTab = { ...@@ -26,11 +26,13 @@ const RepoTab = {
}, },
methods: { methods: {
tabClicked: Store.setActiveFiles, tabClicked(file) {
Store.setActiveFiles(file);
},
closeTab(file) { closeTab(file) {
if (file.changed) return; if (file.changed) return;
this.$emit('tabclosed', file);
Store.removeFromOpenedFiles(file);
}, },
}, },
}; };
...@@ -39,25 +41,28 @@ export default RepoTab; ...@@ -39,25 +41,28 @@ export default RepoTab;
</script> </script>
<template> <template>
<li @click="tabClicked(tab)"> <li
<a :class="{ active : tab.active }"
href="#0" @click="tabClicked(tab)"
class="close" >
@click.stop.prevent="closeTab(tab)" <button
:aria-label="closeLabel"> type="button"
<i class="close-btn"
class="fa" @click.stop.prevent="closeTab(tab)"
:class="changedClass" :aria-label="closeLabel">
aria-hidden="true"> <i
</i> class="fa"
</a> :class="changedClass"
aria-hidden="true">
</i>
</button>
<a <a
href="#" href="#"
class="repo-tab" class="repo-tab"
:title="tab.url" :title="tab.url"
@click.prevent="tabClicked(tab)"> @click.prevent="tabClicked(tab)">
{{tab.name}} {{tab.name}}
</a> </a>
</li> </li>
</template> </template>
<script> <script>
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import RepoTab from './repo_tab.vue'; import RepoTab from './repo_tab.vue';
import RepoMixin from '../mixins/repo_mixin'; import RepoMixin from '../mixins/repo_mixin';
const RepoTabs = { export default {
mixins: [RepoMixin], mixins: [RepoMixin],
components: {
components: { 'repo-tab': RepoTab,
'repo-tab': RepoTab,
},
data: () => Store,
methods: {
tabClosed(file) {
Store.removeFromOpenedFiles(file);
}, },
}, data() {
}; return Store;
},
export default RepoTabs; };
</script> </script>
<template> <template>
<ul id="tabs"> <ul
<repo-tab id="tabs"
v-for="tab in openedFiles" class="list-unstyled"
:key="tab.id" >
:tab="tab" <repo-tab
:class="{'active' : tab.active}" v-for="tab in openedFiles"
@tabclosed="tabClosed" :key="tab.id"
/> :tab="tab"
<li class="tabs-divider" /> />
</ul> <li class="tabs-divider" />
</ul>
</template> </template>
import Vue from 'vue';
export default new Vue();
import { convertPermissionToBoolean } from '../../lib/utils/common_utils';
import Service from '../services/repo_service'; import Service from '../services/repo_service';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import Flash from '../../flash'; import Flash from '../../flash';
...@@ -25,10 +26,6 @@ const RepoHelper = { ...@@ -25,10 +26,6 @@ const RepoHelper = {
key: '', key: '',
isTree(data) {
return Object.hasOwnProperty.call(data, 'blobs');
},
Time: window.performance Time: window.performance
&& window.performance.now && window.performance.now
? window.performance ? window.performance
...@@ -58,13 +55,20 @@ const RepoHelper = { ...@@ -58,13 +55,20 @@ const RepoHelper = {
}, },
setDirectoryOpen(tree, title) { setDirectoryOpen(tree, title) {
const file = tree; if (!tree) return;
if (!file) return undefined;
file.opened = true; Object.assign(tree, {
file.icon = 'fa-folder-open'; opened: true,
RepoHelper.updateHistoryEntry(file.url, title); });
return file;
RepoHelper.updateHistoryEntry(tree.url, title);
},
setDirectoryToClosed(entry) {
Object.assign(entry, {
opened: false,
files: [],
});
}, },
isRenderable() { isRenderable() {
...@@ -81,63 +85,23 @@ const RepoHelper = { ...@@ -81,63 +85,23 @@ const RepoHelper = {
.catch(RepoHelper.loadingError); .catch(RepoHelper.loadingError);
}, },
// when you open a directory you need to put the directory files under getContent(treeOrFile, emptyFiles = false) {
// the directory... This will merge the list of the current directory and the new list. let file = treeOrFile;
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) { if (!Store.files.length) {
// the url we are requesting -> split by the project URL. Grab the right side. Store.loading.tree = true;
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) {
let file = treeOrFile;
return Service.getContent() return Service.getContent()
.then((response) => { .then((response) => {
const data = response.data; 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 (file && file.type === 'blob') {
if (!Store.isTree) {
if (!file) file = data; if (!file) file = data;
Store.binary = data.binary; Store.binary = data.binary;
...@@ -145,38 +109,40 @@ const RepoHelper = { ...@@ -145,38 +109,40 @@ const RepoHelper = {
// file might be undefined // file might be undefined
RepoHelper.setBinaryDataAsBase64(data); RepoHelper.setBinaryDataAsBase64(data);
Store.setViewToPreview(); Store.setViewToPreview();
} else if (!Store.isPreviewView()) { } else if (!Store.isPreviewView() && !data.render_error) {
if (!data.render_error) { Service.getRaw(data.raw_path)
Service.getRaw(data.raw_path) .then((rawResponse) => {
.then((rawResponse) => { Store.blobRaw = rawResponse.data;
Store.blobRaw = rawResponse.data; data.plain = rawResponse.data;
data.plain = rawResponse.data; RepoHelper.setFile(data, file);
RepoHelper.setFile(data, file); }).catch(RepoHelper.loadingError);
}).catch(RepoHelper.loadingError);
}
} }
if (Store.isPreviewView()) { if (Store.isPreviewView()) {
RepoHelper.setFile(data, file); RepoHelper.setFile(data, file);
} }
} else {
Store.loading.tree = false;
RepoHelper.setDirectoryOpen(file, data.pageTitle || data.name);
// if the file tree is empty if (emptyFiles) {
if (Store.files.length === 0) { Store.files = [];
const parentURL = Service.blobURLtoParentTree(Service.url);
Service.url = parentURL;
RepoHelper.getContent();
} }
} else {
// it's a tree this.addToDirectory(file, data);
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.prevURL = Service.blobURLtoParentTree(Service.url); Store.prevURL = Service.blobURLtoParentTree(Service.url);
} }
}).catch(RepoHelper.loadingError); }).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) { setFile(data, file) {
const newFile = data; const newFile = data;
newFile.url = file.url || Service.url; // Grab the URL from service, happens on page refresh. newFile.url = file.url || Service.url; // Grab the URL from service, happens on page refresh.
...@@ -190,57 +156,41 @@ const RepoHelper = { ...@@ -190,57 +156,41 @@ const RepoHelper = {
Store.setActiveFiles(newFile); Store.setActiveFiles(newFile);
}, },
serializeBlob(blob) { serializeRepoEntity(type, entity, level = 0) {
const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob); const { id, url, name, icon, last_commit, tree_url } = entity;
simpleBlob.lastCommitMessage = blob.last_commit.message;
simpleBlob.lastCommitUpdate = blob.last_commit.committed_date;
simpleBlob.loading = false;
return simpleBlob; return {
}, id,
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 = {
type, type,
name, name,
url, url,
tree_url,
level,
icon: `fa-${icon}`, icon: `fa-${icon}`,
level: 0, files: [],
loading: false, 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() { scrollTabsRight() {
// wait for the transition. 0.1 seconds. const tabs = document.getElementById('tabs');
setTimeout(() => { if (!tabs) return;
const tabs = document.getElementById('tabs'); tabs.scrollLeft = tabs.scrollWidth;
if (!tabs) return;
tabs.scrollLeft = tabs.scrollWidth;
}, 200);
}, },
dataToListOfFiles(data) { dataToListOfFiles(data, level) {
const { blobs, trees, submodules } = data; const { blobs, trees, submodules } = data;
return [ return [
...blobs.map(blob => RepoHelper.serializeBlob(blob)), ...trees.map(tree => RepoHelper.serializeRepoEntity('tree', tree, level)),
...trees.map(tree => RepoHelper.serializeTree(tree)), ...submodules.map(submodule => RepoHelper.serializeRepoEntity('submodule', submodule, level)),
...submodules.map(submodule => RepoHelper.serializeSubmodule(submodule)), ...blobs.map(blob => RepoHelper.serializeRepoEntity('blob', blob, level)),
]; ];
}, },
......
This diff is collapsed.
...@@ -64,6 +64,10 @@ const RepoService = { ...@@ -64,6 +64,10 @@ const RepoService = {
return urlArray.join('/'); return urlArray.join('/');
}, },
getBranch() {
return Api.branchSingle(Store.projectId, Store.currentBranch);
},
commitFiles(payload) { commitFiles(payload) {
return Api.commitMultiple(Store.projectId, payload) return Api.commitMultiple(Store.projectId, payload)
.then(this.commitFlash); .then(this.commitFlash);
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment