Commit c20c9e29 authored by Marin Jankovski's avatar Marin Jankovski

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce

parents 49d689fb 2ca9bda4

Too many changes to show.

To preserve performance only 1000 of 1000+ files are displayed.

......@@ -6,8 +6,8 @@
/doc/ @axil @marcia @eread @mikelewis
# Frontend maintainers should see everything in `app/assets/`
app/assets/ @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann @kushalpandya
*.scss @annabeldunstone @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann @kushalpandya
app/assets/ @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann @kushalpandya @pslaughter
*.scss @annabeldunstone @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann @kushalpandya @pslaughter
# Someone from the database team should review changes in `db/`
db/ @abrandl @NikolayS
......@@ -19,3 +19,5 @@ db/ @abrandl @NikolayS
/lib/gitlab/ci/templates/ @nolith @zj
/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @DylanGriffith @mayra-cabrera @tkuah
/lib/gitlab/ci/templates/Security/ @plafoucriere @gonzoyumo @twoodham
/ee/app/models/project_alias.rb @patrickbajao
/ee/lib/api/project_aliases.rb @patrickbajao
......@@ -12,7 +12,9 @@
# Trigger a manual docs build in gitlab-docs only on non docs-only branches.
# Useful to preview the docs changes live.
review-docs-deploy-manual:
<<: *review-docs
extends:
- .review-docs
- .no-docs-and-no-qa
stage: build
script:
- gem install gitlab --no-document
......@@ -21,9 +23,6 @@ review-docs-deploy-manual:
only:
- branches@gitlab-org/gitlab-ce
- branches@gitlab-org/gitlab-ee
except:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
# Always trigger a docs build in gitlab-docs only on docs-only branches.
# Useful to preview the docs changes live.
......@@ -68,8 +67,8 @@ docs lint:
- cd /tmp/gitlab-docs
# Lint Markdown
# https://github.com/markdownlint/markdownlint/blob/master/docs/RULES.md
- bundle exec mdl content/$DOCS_GITLAB_REPO_SUFFIX/**/*.md --rules \
MD032
- bundle exec mdl content/$DOCS_GITLAB_REPO_SUFFIX/**/*.md --ignore-front-matter --rules \
MD004,MD032,MD034
# Build HTML from Markdown
- bundle exec nanoc
# Check the internal links
......
......@@ -8,7 +8,7 @@
.use-pg: &use-pg
services:
- name: postgres:9.6
- name: postgres:9.6.11
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine
......@@ -32,7 +32,7 @@
DOCKER_HOST: tcp://docker:2375
script:
- node --version
- retry yarn install --frozen-lockfile --production --cache-folder .yarn-cache
- retry yarn install --frozen-lockfile --production --cache-folder .yarn-cache --prefer-offline
- free -m
- retry bundle exec rake gitlab:assets:compile
- time scripts/build_assets_image
......@@ -70,9 +70,10 @@ gitlab:assets:compile pull-cache:
cache:
policy: pull
except:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- /(^docs[\/-].*|.*-docs$)/
refs:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- /(^docs[\/-].*|.*-docs$)/
.compile-assets-metadata:
extends: .dedicated-runner
......@@ -81,7 +82,7 @@ gitlab:assets:compile pull-cache:
stage: prepare
script:
- node --version
- retry yarn install --frozen-lockfile --cache-folder .yarn-cache
- retry yarn install --frozen-lockfile --cache-folder .yarn-cache --prefer-offline
- free -m
- retry bundle exec rake gitlab:assets:compile
- scripts/clean-old-cached-assets
......@@ -107,9 +108,10 @@ compile-assets pull-cache:
cache:
policy: pull
except:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- /(^docs[\/-].*|.*-docs$)/
refs:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- /(^docs[\/-].*|.*-docs$)/
gitlab:ui:visual:
extends: .dedicated-runner
......@@ -166,8 +168,8 @@ karma:
paths:
- chrome_debug.log
- coverage-javascript/
reports:
junit: junit_karma.xml
# reports:
# junit: junit_karma.xml
jest:
extends: .dedicated-no-docs-and-no-qa-pull-cache-job
......@@ -189,8 +191,8 @@ jest:
paths:
- coverage-frontend/
- junit_jest.xml
reports:
junit: junit_jest.xml
# reports:
# junit: junit_jest.xml
cache:
key: jest
paths:
......@@ -229,7 +231,7 @@ qa:selectors:
before_script: []
script:
- date
- yarn install --frozen-lockfile --cache-folder .yarn-cache
- yarn install --frozen-lockfile --cache-folder .yarn-cache --prefer-offline
- date
- yarn run webpack-prod
......
......@@ -30,7 +30,14 @@
.no-docs:
except:
- /(^docs[\/-].*|.*-docs$)/
refs:
- /(^docs[\/-].*|.*-docs$)/
.no-docs-and-no-qa:
except:
refs:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
.dedicated-no-docs-pull-cache-job:
extends:
......@@ -38,10 +45,9 @@
- .no-docs
.dedicated-no-docs-and-no-qa-pull-cache-job:
extends: .dedicated-pull-cache-job
except:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
extends:
- .dedicated-pull-cache-job
- .no-docs-and-no-qa
# Jobs that do not need a DB
.dedicated-no-docs-no-db-pull-cache-job:
......
......@@ -17,3 +17,26 @@ memory-static:
- tmp/memory_*.txt
reports:
metrics: tmp/memory_metrics.txt
# Show memory usage caused by invoking require per gem.
# Unlike `memory-static`, it hits the app with one request to ensure that any last minute require-s have been called.
# The application is booted in `production` environment.
# All tests are run without a webserver (directly using Rack::Mock by default).
memory-on-boot:
extends: .rspec-metadata-pg-10
variables:
NODE_ENV: "production"
RAILS_ENV: "production"
SETUP_DB: "true"
SKIP_STORAGE_VALIDATION: "true"
# we override the max_old_space_size to prevent OOM errors
NODE_OPTIONS: --max_old_space_size=3584
script:
# Both bootsnap and derailed monkey-patch Kernel#require, which leads to circular dependency
- DISABLE_BOOTSNAP=true PATH_TO_HIT="/users/sign_in" CUT_OFF=0.3 bundle exec derailed exec perf:mem >> 'tmp/memory_on_boot.txt'
- scripts/generate-memory-metrics-on-boot tmp/memory_on_boot.txt >> 'tmp/memory_on_boot_metrics.txt'
artifacts:
paths:
- tmp/memory_*.txt
reports:
metrics: tmp/memory_on_boot_metrics.txt
.use-pg: &use-pg
services:
- name: postgres:9.6
- name: postgres:9.6.11
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine
......@@ -20,8 +20,9 @@
- master@gitlab/gitlab-ee
.gitlab-setup: &gitlab-setup
extends: .dedicated-no-docs-and-no-qa-pull-cache-job
<<: *use-pg
extends:
- .dedicated-no-docs-and-no-qa-pull-cache-job
- .use-pg
variables:
SETUP_DB: "false"
script:
......@@ -43,7 +44,9 @@
- bundle exec rake $CI_JOB_NAME
.rspec-metadata: &rspec-metadata
extends: .dedicated-pull-cache-job
extends:
- .dedicated-pull-cache-job
- .no-docs-and-no-qa
stage: test
script:
- JOB_NAME=( $CI_JOB_NAME )
......@@ -74,11 +77,8 @@
- rspec_flaky/
- rspec_profiling/
- tmp/capybara/
reports:
junit: junit_rspec.xml
except:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
# reports:
# junit: junit_rspec.xml
.rspec-metadata-pg: &rspec-metadata-pg
<<: *rspec-metadata
......@@ -122,8 +122,10 @@
- setup-test-env
setup-test-env:
extends: .dedicated-runner-default-cache
<<: *use-pg
extends:
- .dedicated-runner-default-cache
- .no-docs
- .use-pg
stage: prepare
script:
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
......@@ -134,12 +136,10 @@ setup-test-env:
- tmp/tests
- config/secrets.yml
- vendor/gitaly-ruby
except:
- /(^docs[\/-].*|.*-docs$)/
rspec unit pg:
<<: *rspec-metadata-pg
parallel: 20
parallel: 25
rspec integration pg:
<<: *rspec-metadata-pg
......@@ -152,7 +152,7 @@ rspec system pg:
rspec unit pg-10:
<<: *rspec-metadata-pg-10
<<: *only-schedules-master
parallel: 20
parallel: 25
rspec integration pg-10:
<<: *rspec-metadata-pg-10
......@@ -200,11 +200,12 @@ static-analysis:
downtime_check:
<<: *rake-exec
except:
- master
- tags
- /^[\d-]+-stable(-ee)?$/
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
refs:
- master
- tags
- /^[\d-]+-stable(-ee)?$/
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
dependencies:
- setup-test-env
......@@ -212,12 +213,13 @@ ee_compat_check:
<<: *rake-exec
dependencies: []
except:
- master
- tags
- /[\d-]+-stable(-ee)?/
- /^security-/
- branches@gitlab-org/gitlab-ee
- branches@gitlab/gitlab-ee
refs:
- master
- tags
- /[\d-]+-stable(-ee)?/
- /^security-/
- branches@gitlab-org/gitlab-ee
- branches@gitlab/gitlab-ee
retry: 0
artifacts:
name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}"
......@@ -243,8 +245,8 @@ migration:path-pg:
.db-rollback: &db-rollback
extends: .dedicated-no-docs-and-no-qa-pull-cache-job
script:
- bundle exec rake db:migrate VERSION=20170523121229
- bundle exec rake db:migrate
- bundle exec rake db:migrate VERSION=20180101160629
- bundle exec rake db:migrate SKIP_SCHEMA_VERSION_CHECK=true
dependencies:
- setup-test-env
......@@ -261,7 +263,9 @@ gitlab:setup-pg:
coverage:
# Don't include dedicated-no-docs-no-db-pull-cache-job here since we need to
# download artifacts from all the rspec jobs instead of from setup-test-env only
extends: .dedicated-runner-default-cache
extends:
- .dedicated-runner-default-cache
- .no-docs-and-no-qa
cache:
policy: pull
variables:
......@@ -276,6 +280,3 @@ coverage:
paths:
- coverage/index.html
- coverage/assets/
except:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
......@@ -236,5 +236,5 @@ danger-review:
script:
- git version
- node --version
- yarn install --frozen-lockfile --cache-folder .yarn-cache
- yarn install --frozen-lockfile --cache-folder .yarn-cache --prefer-offline
- danger --fail-on-errors=true
......@@ -15,7 +15,9 @@ cache gems:
- setup-test-env
gitlab_git_test:
extends: .dedicated-runner
extends:
- .dedicated-runner
- .no-docs-and-no-qa
variables:
SETUP_DB: "false"
before_script: []
......@@ -23,12 +25,11 @@ gitlab_git_test:
cache: {}
script:
- spec/support/prepare-gitlab-git-test-for-commit --check-for-changes
except:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
no_ee_check:
extends: .dedicated-runner
extends:
- .dedicated-runner
- .no-docs-and-no-qa
variables:
SETUP_DB: "false"
before_script: []
......@@ -38,6 +39,3 @@ no_ee_check:
- scripts/no-ee-check
only:
- /.+/@gitlab-org/gitlab-ce
except:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
......@@ -12,7 +12,9 @@
- rspec_profiling/
retrieve-tests-metadata:
<<: *tests-metadata-state
extends:
- .tests-metadata-state
- .no-docs-and-no-qa
stage: prepare
cache:
key: tests_metadata
......@@ -25,9 +27,6 @@ retrieve-tests-metadata:
- mkdir -p rspec_profiling/
- wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH
- '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}'
except:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
update-tests-metadata:
<<: *tests-metadata-state
......@@ -69,9 +68,10 @@ flaky-examples-check:
only:
- branches
except:
- master
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
refs:
- master
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
artifacts:
expire_in: 30d
paths:
......
#### Database Reviewer Checklist
Thank you for becoming a ~database reviewer! Please work on the list
below to complete your setup. For any question, reach out to #database
an mention `@abrandl`.
- [ ] Change issue title to include your name: `Database Reviewer Checklist: Your Name`
- [ ] Review general [code review guide](https://docs.gitlab.com/ee/development/code_review.html)
- [ ] Review [database review documentation](https://about.gitlab.com/handbook/engineering/workflow/code-review/database.html)
- [ ] Familiarize with [migration helpers](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/database/migration_helpers.rb) and review usage in existing migrations
- [ ] Read [database migration style guide](https://docs.gitlab.com/ee/development/migration_style_guide.html)
- [ ] Familiarize with best practices in [database guides](https://docs.gitlab.com/ee/development/#database-guides)
- [ ] Watch [Optimising Rails Database Queries: Episode 1](https://www.youtube.com/watch?v=79GurlaxhsI)
- [ ] Read [Understanding EXPLAIN plans](https://docs.gitlab.com/ee/development/understanding_explain_plans.html)
- [ ] Review [database best practices](https://docs.gitlab.com/ee/development/#best-practices)
- [ ] Review how we use [database instances restored from a backup](https://ops.gitlab.net/gitlab-com/gl-infra/gitlab-restore/postgres-gprd) for testing and make sure you're set up to execute pipelines (check [README.md](https://ops.gitlab.net/gitlab-com/gl-infra/gitlab-restore/postgres-gprd/blob/master/README.md) and reach out to @abrandl since this is currently subject to being changed)
- [ ] Get yourself added to [`@gl-database`](https://gitlab.com/groups/gl-database/-/group_members) group and respond to @-mentions to the group (reach out to any maintainer on the group to get added). You will get TODOs on gitlab.com for group mentions.
- [ ] Make sure you have proper access to at least a read-only replica in staging and production
- [ ] Indicate in `data/team.yml` your role as a database reviewer ([example MR](https://gitlab.com/gitlab-com/www-gitlab-com/merge_requests/19600/diffs)). Assign MR to your manager for merge.
- [ ] Send one MR to improve the [review documentation](https://about.gitlab.com/handbook/engineering/workflow/code-review/database.html) or the [issue template](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab/issue_templates/Database%20Reviewer.md)
Note that *approving and accepting* merge requests is *restricted* to
Database Maintainers only. As a reviewer, pass the MR to a maintainer
for approval.
You're all set! Watch out for TODOs on GitLab.com.
###### Where to go for questions?
Reach out to `#database` on Slack and mention `@abrandl` for any questions.
cc @abrandl
/label ~meta ~database
......@@ -19,4 +19,5 @@ unless helper.release_automation?
danger.import_dangerfile(path: 'danger/single_codebase')
danger.import_dangerfile(path: 'danger/gitlab_ui_wg')
danger.import_dangerfile(path: 'danger/ce_ee_vue_templates')
danger.import_dangerfile(path: 'danger/only_documentation')
end
......@@ -11,7 +11,7 @@ gem 'responders', '~> 2.0'
gem 'sprockets', '~> 3.7.0'
# Default values for AR models
gem 'gitlab-default_value_for', '~> 3.1.1', require: 'default_value_for'
gem 'default_value_for', '~> 3.2.0'
# Supported DBs
gem 'mysql2', '~> 0.4.10', group: :mysql
......@@ -211,7 +211,7 @@ gem 'discordrb-webhooks-blackst0ne', '~> 3.3', require: false
# HipChat integration
gem 'hipchat', '~> 1.5.0'
# JIRA integration
# Jira integration
gem 'jira-ruby', '~> 1.4'
# Flowdock integration
......@@ -309,7 +309,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false
# Prometheus
gem 'prometheus-client-mmap', '~> 0.9.4'
gem 'prometheus-client-mmap', '~> 0.9.6'
gem 'raindrops', '~> 0.18'
end
......
......@@ -163,6 +163,8 @@ GEM
html-pipeline
declarative (0.0.10)
declarative-option (0.1.0)
default_value_for (3.2.0)
activerecord (>= 3.2.0, < 6.0)
derailed_benchmarks (1.3.5)
benchmark-ips (~> 2)
get_process_mem (~> 0)
......@@ -304,8 +306,6 @@ GEM
gitaly-proto (1.32.0)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab-default_value_for (3.1.1)
activerecord (>= 3.2.0, < 6.0)
gitlab-labkit (0.3.0)
actionpack (~> 5)
activesupport (~> 5)
......@@ -652,7 +652,7 @@ GEM
parser
unparser
procto (0.0.3)
prometheus-client-mmap (0.9.4)
prometheus-client-mmap (0.9.6)
pry (0.11.3)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
......@@ -1056,6 +1056,7 @@ DEPENDENCIES
creole (~> 0.5.0)
database_cleaner (~> 1.7.0)
deckar01-task_list (= 2.2.0)
default_value_for (~> 3.2.0)
derailed_benchmarks
device_detector
devise (~> 4.6)
......@@ -1093,7 +1094,6 @@ DEPENDENCIES
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 1.32.0)
github-markup (~> 1.7.0)
gitlab-default_value_for (~> 3.1.1)
gitlab-labkit (~> 0.3.0)
gitlab-markup (~> 1.7.0)
gitlab-sidekiq-fetcher (~> 0.4.0)
......@@ -1173,7 +1173,7 @@ DEPENDENCIES
peek-redis (~> 1.2.0)
pg (~> 1.1)
premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.9.4)
prometheus-client-mmap (~> 0.9.6)
pry-byebug (~> 3.5.1)
pry-rails (~> 0.3.4)
puma (~> 3.12)
......
......@@ -84,43 +84,28 @@ star, smile, etc.). Some good tips about code reviews can be found in our
[Code Review Guidelines]: https://docs.gitlab.com/ce/development/code_review.html
## Feature freeze on the 7th for the release on the 22nd
The feature freeze on the 7th has been discontinued. The [transition period overview](https://gitlab.com/gitlab-org/release/docs/blob/21cbd409dd5f157fe252f254f3e897f01908abe2/general/deploy/auto-deploy-transition.md#transition)
describes the change to this process. During the transition period, the only guarantee that
a change will be included in the release on the 22nd is if the change has been
deployed to GitLab.com prior to this date.
## Feature flags
### Feature flags
Overview and details of feature flag processes in development of GitLab itself is described in [feature flags process documentation](https://docs.gitlab.com/ee/development/feature_flags/process.html).
Merge requests that make changes hidden behind a feature flag, or remove an
existing feature flag because a feature is deemed stable, may be merged (and
picked into the stable branches) up to the 19th of the month. Such merge
requests should have the ~"feature flag" label assigned, and don't require a
corresponding exception request to be created.
Guides on how to include feature flags in your backend/frontend code while developing GitLab are described in [developing with feature flags documentation](https://docs.gitlab.com/ee/development/feature_flags/developing.html).
A level of common sense should be applied when deciding whether to have a feature
behind a feature flag off or on by default.
Getting access and how to expose the feature to users is detailed in [controlling feature flags documentation](https://docs.gitlab.com/ee/development/feature_flags/controls.html).
The following guidelines can be applied to help make this decision:
## Feature proposals from the 22nd to the 1st
* If the feature is not fully ready or functioning, the feature flag should be disabled by default.
* If the feature is ready but there are concerns about performance or impact, the feature flag should be enabled by default, but
disabled via chatops before deployment on GitLab.com environments. If the performance concern is confirmed, the final release should have the feature flag disabled by default.
* In most other cases, the feature flag can be enabled by default.
To allow the Product and Engineering teams time to discuss issues that will be placed into an upcoming milestone,
Product Managers must have their proposal for that milestone ready by the 22nd of each month.
For more information on rolling out changes using feature flags, read [through the documentation](https://docs.gitlab.com/ee/development/rolling_out_changes_using_feature_flags.html).
This proposal will be shared with Engineering for discussion, feedback, and planning.
The plan for the upcoming milestone must be finalized by the 1st of the month, one week before kickoff on the 8th.
In order to build the final package and present the feature for self-hosted
customers, the feature flag should be removed. This should happen before the
22nd, ideally _at least_ 2 days before. That means MRs with feature
flags being picked at the 19th would have quite a tight schedule, so picking
these _earlier_ is preferable.
## Feature freeze on the 7th for the release on the 22nd
While rare, release managers may decide to reject picking a change into a stable
branch, even when feature flags are used. This might be necessary if the changes
are deemed problematic, too invasive, or there simply isn't enough time to
properly test how the changes behave on GitLab.com.
The feature freeze on the 7th has been discontinued. [Transition period overview](https://gitlab.com/gitlab-org/release/docs/blob/21cbd409dd5f157fe252f254f3e897f01908abe2/general/deploy/auto-deploy-transition.md#transition)
describes the change to this process. During the transition period, the only guarantee that
a change will be included in the release on the 22nd is if the change has been
deployed to GitLab.com prior to this date.
### Between the 1st and the 7th
......
import $ from 'jquery';
import Sortable from 'sortablejs';
import Vue from 'vue';
import { n__ } from '~/locale';
import { n__, s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip';
import AccessorUtilities from '../../lib/utils/accessor';
......@@ -53,12 +54,19 @@ export default Vue.extend({
const { issuesSize } = this.list;
return `${n__('%d issue', '%d issues', issuesSize)}`;
},
caretTooltip() {
return this.list.isExpanded ? s__('Boards|Collapse') : s__('Boards|Expand');
},
isNewIssueShown() {
return (
this.list.type === 'backlog' ||
(!this.disabled && this.list.type !== 'closed' && this.list.type !== 'blank')
);
},
uniqueKey() {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
return `boards.${this.boardId}.${this.list.type}.${this.list.id}`;
},
},
watch: {
filter: {
......@@ -72,31 +80,34 @@ export default Vue.extend({
},
},
mounted() {
this.sortableOptions = getBoardSortableDefaultOptions({
const instance = this;
const sortableOptions = getBoardSortableDefaultOptions({
disabled: this.disabled,
group: 'boards',
draggable: '.is-draggable',
handle: '.js-board-handle',
onEnd: e => {
onEnd(e) {
sortableEnd();
const sortable = this;
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
const order = this.sortable.toArray();
const order = sortable.toArray();
const list = boardsStore.findList('id', parseInt(e.item.dataset.id, 10));
this.$nextTick(() => {
instance.$nextTick(() => {
boardsStore.moveList(list, order);
});
}
},
});
this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
Sortable.create(this.$el.parentNode, sortableOptions);
},
created() {
if (this.list.isExpandable && AccessorUtilities.isLocalStorageAccessSafe()) {
const isCollapsed =
localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false';
const isCollapsed = localStorage.getItem(`${this.uniqueKey}.expanded`) === 'false';
this.list.isExpanded = !isCollapsed;
}
......@@ -105,16 +116,17 @@ export default Vue.extend({
showNewIssueForm() {
this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
},
toggleExpanded(e) {
if (this.list.isExpandable && !e.target.classList.contains('js-no-trigger-collapse')) {
toggleExpanded() {
if (this.list.isExpandable) {
this.list.isExpanded = !this.list.isExpanded;
if (AccessorUtilities.isLocalStorageAccessSafe()) {
localStorage.setItem(
`boards.${this.boardId}.${this.list.type}.expanded`,
this.list.isExpanded,
);
localStorage.setItem(`${this.uniqueKey}.expanded`, this.list.isExpanded);
}
// When expanding/collapsing, the tooltip on the caret button sometimes stays open.
// Close all tooltips manually to prevent dangling tooltips.
$('.tooltip').tooltip('hide');
}
},
},
......
<script>
import { __ } from '~/locale';
/* global ListLabel */
import Cookies from 'js-cookie';
import boardsStore from '../stores/boards_store';
......@@ -7,8 +8,8 @@ export default {
data() {
return {
predefinedLabels: [
new ListLabel({ title: 'To Do', color: '#F0AD4E' }),
new ListLabel({ title: 'Doing', color: '#5CB85C' }),
new ListLabel({ title: __('To Do'), color: '#F0AD4E' }),
new ListLabel({ title: __('Doing'), color: '#5CB85C' }),
],
};
},
......@@ -58,7 +59,11 @@ export default {
<template>
<div class="board-blank-state p-3">
<p>Add the following default lists to your Issue Board with one click:</p>
<p>
{{
__('BoardBlankState|Add the following default lists to your Issue Board with one click:')
}}
</p>
<ul class="list-unstyled board-blank-state-list">
<li v-for="(label, index) in predefinedLabels" :key="index">
<span
......@@ -70,18 +75,21 @@ export default {
</li>
</ul>
<p>
Starting out with the default set of lists will get you right on the way to making the most of
your board.
{{
__(
'BoardBlankState|Starting out with the default set of lists will get you right on the way to making the most of your board.',
)
}}
</p>
<button
class="btn btn-success btn-inverted btn-block"
type="button"
@click.stop="addDefaultLists"
>
Add default lists
{{ __('BoardBlankState|Add default lists') }}
</button>
<button class="btn btn-default btn-block" type="button" @click.stop="clearBlankState">
Nevermind, I'll use my own
{{ __("BoardBlankState|Nevermind, I'll use my own") }}
</button>
</div>
</template>
......@@ -227,7 +227,7 @@ export default {
:class="{ 'd-none': !list.isExpanded, 'd-flex flex-column': list.isExpanded }"
class="board-list-component position-relative h-100"
>
<div v-if="loading" class="board-list-loading text-center" aria-label="Loading issues">
<div v-if="loading" class="board-list-loading text-center" :aria-label="__('Loading issues')">
<gl-loading-icon />
</div>
<board-new-issue
......@@ -257,7 +257,7 @@ export default {
/>
<li v-if="showCount" class="board-list-count text-center" data-issue-id="-1">
<gl-loading-icon v-show="list.loadingMore" label="Loading more issues" />
<span v-if="list.issues.length === list.issuesSize"> Showing all issues </span>
<span v-if="list.issues.length === list.issuesSize">{{ __('Showing all issues') }}</span>
<span v-else> Showing {{ list.issues.length }} of {{ list.issuesSize }} issues </span>
</li>
</ul>
......
......@@ -102,9 +102,9 @@ export default {
<div class="board-card position-relative p-3 rounded">
<form @submit="submit($event)">
<div v-if="error" class="flash-container">
<div class="flash-alert">An error occurred. Please try again.</div>
<div class="flash-alert">{{ __('An error occurred. Please try again.') }}</div>
</div>
<label :for="list.id + '-title'" class="label-bold"> Title </label>
<label :for="list.id + '-title'" class="label-bold">{{ __('Title') }}</label>
<input
:id="list.id + '-title'"
ref="input"
......@@ -122,12 +122,11 @@ export default {
class="float-left"
variant="success"
type="submit"
>{{ __('Submit issue') }}</gl-button
>
Submit issue
</gl-button>
<gl-button class="float-right" type="button" variant="default" @click="cancel">
Cancel
</gl-button>
<gl-button class="float-right" type="button" variant="default" @click="cancel">{{
__('Cancel')
}}</gl-button>
</div>
</form>
</div>
......
......@@ -124,7 +124,7 @@ export default {
return `${this.rootPath}${assignee.username}`;
},
avatarUrlTitle(assignee) {
return `Avatar for ${assignee.name}`;
return sprintf(__(`Avatar for %{assigneeName}`), { assigneeName: assignee.name });
},
showLabel(label) {
if (!label.id) return false;
......@@ -160,9 +160,10 @@ export default {
:title="__('Confidential')"
class="confidential-icon append-right-4"
:aria-label="__('Confidential')"
/><a :href="issue.path" :title="issue.title" class="js-no-trigger" @mousemove.stop>{{
issue.title
}}</a>
/>
<a :href="issue.path" :title="issue.title" class="js-no-trigger" @mousemove.stop>
{{ issue.title }}
</a>
</h4>
</div>
<div v-if="showLabelFooter" class="board-card-labels prepend-top-4 d-flex flex-wrap">
......@@ -204,13 +205,13 @@ export default {
placement="bottom"
class="board-issue-path block-truncated bold"
>{{ issueReferencePath }}</tooltip-on-truncate
>#{{ issue.iid }}
>
#{{ issue.iid }}
</span>
<span class="board-info-items prepend-top-8 d-inline-block">
<issue-due-date v-if="issue.dueDate" :date="issue.dueDate" /><issue-time-estimate
v-if="issue.timeEstimate"
:estimate="issue.timeEstimate"
/><issue-card-weight
<issue-due-date v-if="issue.dueDate" :date="issue.dueDate" />
<issue-time-estimate v-if="issue.timeEstimate" :estimate="issue.timeEstimate" />
<issue-card-weight
v-if="issue.weight"
:weight="issue.weight"
@click="filterByWeight(issue.weight)"
......@@ -230,7 +231,8 @@ export default {
tooltip-placement="bottom"
>
<span class="js-assignee-tooltip">
<span class="bold d-block">Assignee</span> {{ assignee.name }}
<span class="bold d-block">{{ __('Assignee') }}</span>
{{ assignee.name }}
<span class="text-white-50">@{{ assignee.username }}</span>
</span>
</user-avatar-link>
......@@ -240,9 +242,8 @@ export default {
:title="assigneeCounterTooltip"
class="avatar-counter"
data-placement="bottom"
>{{ assigneeCounterLabel }}</span
>
{{ assigneeCounterLabel }}
</span>
</div>
</div>
</div>
......
<script>
import { __, sprintf } from '~/locale';
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
......@@ -20,19 +21,20 @@ export default {
computed: {
contents() {
const obj = {
title: "You haven't added any issues to your project yet",
content: `
An issue can be a bug, a todo or a feature request that needs to be
discussed in a project. Besides, issues are searchable and filterable.
`,
title: __("You haven't added any issues to your project yet"),
content: __(
'An issue can be a bug, a todo or a feature request that needs to be discussed in a project. Besides, issues are searchable and filterable.',
),
};
if (this.activeTab === 'selected') {
obj.title = "You haven't selected any issues yet";
obj.content = `
Go back to <strong>Open issues</strong> and select some issues
to add to your board.
`;
obj.title = __("You haven't selected any issues yet");
obj.content = sprintf(
__(
'Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board.',
),
{ startTag: '<strong>', endTag: '</strong>' },
);
}
return obj;
......@@ -51,16 +53,16 @@ export default {
<div class="text-content">
<h4>{{ contents.title }}</h4>
<p v-html="contents.content"></p>
<a v-if="activeTab === 'all'" :href="newIssuePath" class="btn btn-success btn-inverted">
New issue
</a>
<a v-if="activeTab === 'all'" :href="newIssuePath" class="btn btn-success btn-inverted">{{
__('New issue')
}}</a>
<button
v-if="activeTab === 'selected'"
class="btn btn-default"
type="button"
@click="changeTab('all')"
>
Open issues
{{ __('Open issues') }}
</button>
</div>
</div>
......
<script>
import Flash from '../../../flash';
import { __ } from '../../../locale';
import { __, n__ } from '../../../locale';
import ListsDropdown from './lists_dropdown.vue';
import { pluralize } from '../../../lib/utils/text_utility';
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
import boardsStore from '../../stores/boards_store';
......@@ -24,8 +23,8 @@ export default {
},
submitText() {
const count = ModalStore.selectedCount();
return `Add ${count > 0 ? count : ''} ${pluralize('issue', count)}`;
if (!count) return __('Add issues');
return n__(`Add %d issue`, `Add %d issues`, count);
},
},
methods: {
......@@ -68,11 +67,11 @@ export default {
<button :disabled="submitDisabled" class="btn btn-success" type="button" @click="addIssues">
{{ submitText }}
</button>
<span class="inline add-issues-footer-to-list"> to list </span>
<span class="inline add-issues-footer-to-list">{{ __('to list') }}</span>
<lists-dropdown />
</div>
<button class="btn btn-default float-right" type="button" @click="toggleModal(false)">
Cancel
{{ __('Cancel') }}
</button>
</footer>
</template>
<script>
import { __ } from '~/locale';
import ModalFilters from './filters';
import ModalTabs from './tabs.vue';
import ModalStore from '../../stores/modal_store';
......@@ -30,10 +31,10 @@ export default {
computed: {
selectAllText() {
if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
return 'Select all';
return __('Select all');
}
return 'Deselect all';
return __('Deselect all');
},
showSearch() {
return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
......@@ -57,7 +58,7 @@ export default {
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
:aria-label="__('Close')"
@click="toggleModal(false)"
>
<span aria-hidden="true">×</span>
......
......@@ -123,7 +123,9 @@ export default {
class="empty-state add-issues-empty-state-filter text-center"
>
<div class="svg-content"><img :src="emptyStateSvg" /></div>
<div class="text-content"><h4>There are no issues to show.</h4></div>
<div class="text-content">
<h4>{{ __('There are no issues to show.') }}</h4>
</div>
</div>
<div v-for="(group, index) in groupedIssues" :key="index" class="add-issues-list-column">
<div v-for="issue in group" v-if="showIssue(issue)" :key="issue.id" class="board-card-parent">
......
<script>
import { __ } from '~/locale';
import $ from 'jquery';
import _ from 'underscore';
import Icon from '~/vue_shared/components/icon.vue';
......@@ -27,7 +28,7 @@ export default {
},
computed: {
selectedProjectName() {
return this.selectedProject.name || 'Select a project';
return this.selectedProject.name || __('Select a project');
},
},
mounted() {
......@@ -81,7 +82,7 @@ export default {
<template>
<div>
<label class="label-bold prepend-top-10"> Project </label>
<label class="label-bold prepend-top-10">{{ __('Project') }}</label>
<div ref="projectsDropdown" class="dropdown dropdown-projects">
<button
class="dropdown-menu-toggle wide"
......@@ -92,9 +93,9 @@ export default {
{{ selectedProjectName }} <icon name="chevron-down" />
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width">
<div class="dropdown-title">Projects</div>
<div class="dropdown-title">{{ __('Projects') }}</div>
<div class="dropdown-input">
<input class="dropdown-input-field" type="search" placeholder="Search projects" />
<input class="dropdown-input-field" type="search" :placeholder="__('Search projects')" />
<icon name="search" class="dropdown-input-search" data-hidden="true" />
</div>
<div class="dropdown-content"></div>
......
......@@ -76,7 +76,7 @@ export default Vue.extend({
<template>
<div class="block list">
<button class="btn btn-default btn-block" type="button" @click="removeIssue">
Remove from board
{{ __('Remove from board') }}
</button>
</div>
</template>
......@@ -20,7 +20,7 @@ export function getBoardSortableDefaultOptions(obj) {
'ontouchstart' in window || (window.DocumentTouch && document instanceof DocumentTouch);
const defaultSortOptions = Object.assign({}, sortableConfig, {
filter: '.board-delete, .btn',
filter: '.no-drag',
delay: touchEnabled ? 100 : 0,
scrollSensitivity: touchEnabled ? 60 : 100,
scrollSpeed: 20,
......
......@@ -26,6 +26,12 @@ const TYPES = {
isExpandable: false,
isBlank: true,
},
default: {
// includes label, assignee, and milestone lists
isPreset: false,
isExpandable: true,
isBlank: false,
},
};
class List {
......@@ -249,7 +255,7 @@ class List {
}
getTypeInfo(type) {
return TYPES[type] || {};
return TYPES[type] || TYPES.default;
}
onNewIssueResponse(issue, data) {
......
import Vue from 'vue';
import { __ } from '../locale';
import createFlash from '../flash';
import axios from '../lib/utils/axios_utils';
import DivergenceGraph from './components/divergence_graph.vue';
export default () => {
document.querySelectorAll('.js-branch-divergence-graph').forEach(el => {
const { distance, aheadCount, behindCount, defaultBranch, maxCommits } = el.dataset;
return new Vue({
el,
render(h) {
return h(DivergenceGraph, {
props: {
defaultBranch,
distance: distance ? parseInt(distance, 10) : null,
aheadCount: parseInt(aheadCount, 10),
behindCount: parseInt(behindCount, 10),
maxCommits: parseInt(maxCommits, 10),
},
});
},
});
export function createGraphVueApp(el, data, maxCommits) {
return new Vue({
el,
render(h) {
return h(DivergenceGraph, {
props: {
defaultBranch: 'master',
distance: data.distance ? parseInt(data.distance, 10) : null,
aheadCount: parseInt(data.ahead, 10),
behindCount: parseInt(data.behind, 10),
maxCommits,
},
});
},
});
}
export default endpoint => {
const names = [...document.querySelectorAll('.js-branch-item')].map(
({ dataset }) => dataset.name,
);
return axios
.get(endpoint, {
params: { names },
})
.then(({ data }) => {
const maxCommits = Object.entries(data).reduce((acc, [, val]) => {
const max = Math.max(...Object.values(val));
return max > acc ? max : acc;
}, 100);
Object.entries(data).forEach(([branchName, val]) => {
const el = document.querySelector(`.js-branch-${branchName} .js-branch-divergence-graph`);
if (!el) return;
createGraphVueApp(el, val, maxCommits);
});
})
.catch(() =>
createFlash(__('Error fetching diverging counts for branches. Please try again.')),
);
};
......@@ -207,7 +207,7 @@ export default {
return __('Updating');
}
return __('Updated');
return this.updateSuccessful ? __('Updated to') : __('Updated');
},
updateFailureDescription() {
return s__('ClusterIntegration|Update failed. Please check the logs and try again.');
......@@ -331,8 +331,6 @@ export default {
class="form-text text-muted label p-0 js-cluster-application-update-details"
>
{{ versionLabel }}
<span v-if="updateSuccessful">to</span>
<gl-link
v-if="updateSuccessful"
:href="chartRepo"
......
......@@ -2,7 +2,7 @@
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import { GlLoadingIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import { __, s__ } from '~/locale';
import { APPLICATION_STATUS } from '~/clusters/constants';
......@@ -32,7 +32,7 @@ export default {
return [UPDATING].includes(this.knative.status);
},
saveButtonLabel() {
return this.saving ? this.__('Saving') : this.__('Save changes');
return this.saving ? __('Saving') : __('Save changes');
},
knativeInstalled() {
return this.knative.installed;
......@@ -122,9 +122,9 @@ export default {
`ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`,
)
}}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }}
</a>
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
__('More information')
}}</a>
</p>
<p
......
<script>
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import { APPLICATION_STATUS } from '~/clusters/constants';
import { __ } from '~/locale';
const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
......@@ -22,7 +23,7 @@ export default {
return this.status === UNINSTALLING;
},
label() {
return this.loading ? this.__('Uninstalling') : this.__('Uninstall');
return this.loading ? __('Uninstalling') : __('Uninstall');
},
},
};
......
......@@ -14,7 +14,9 @@ const CUSTOM_APP_WARNING_TEXT = {
[PROMETHEUS]: s__('ClusterIntegration|All data will be deleted and cannot be restored.'),
[RUNNER]: s__('ClusterIntegration|Any running pipelines will be canceled.'),
[KNATIVE]: s__('ClusterIntegration|The associated IP will be deleted and cannot be restored.'),
[JUPYTER]: '',
[JUPYTER]: s__(
'ClusterIntegration|All data not committed to GitLab will be deleted and cannot be restored.',
),
};
export default {
......
<script>
import { mapGetters } from 'vuex';
import NoteSignedOutWidget from '~/notes/components/note_signed_out_widget.vue';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
export default {
name: 'DiffDiscussionReply',
components: {
NoteSignedOutWidget,
ReplyPlaceholder,
UserAvatarLink,
},
props: {
hasForm: {
type: Boolean,
required: false,
default: false,
},
renderReplyPlaceholder: {
type: Boolean,
required: true,
},
},
computed: {
...mapGetters({
currentUser: 'getUserData',
userCanReply: 'userCanReply',
}),
},
};
</script>
<template>
<div class="discussion-reply-holder d-flex clearfix">
<template v-if="userCanReply">
<slot v-if="hasForm" name="form"></slot>
<template v-else-if="renderReplyPlaceholder">
<user-avatar-link
:link-href="currentUser.path"
:img-src="currentUser.avatar_url"
:img-alt="currentUser.name"
:img-size="40"
class="d-none d-sm-block"
/>
<reply-placeholder
class="qa-discussion-reply"
:button-text="__('Start a new discussion...')"
@onClick="$emit('showNewDiscussionForm')"
/>
</template>
</template>
<note-signed-out-widget v-else />
</div>
</template>
......@@ -80,7 +80,6 @@ export default {
v-show="isExpanded(discussion)"
:discussion="discussion"
:render-diff-file="false"
:always-expanded="true"
:discussions-by-diff-order="true"
:line="line"
:help-page-path="helpPagePath"
......
......@@ -151,7 +151,11 @@ export default {
stickyMonitor(this.$refs.header, contentTop() - fileHeaderHeight - 1, false);
},
methods: {
...mapActions('diffs', ['toggleFileDiscussions', 'toggleFullDiff']),
...mapActions('diffs', [
'toggleFileDiscussions',
'toggleFileDiscussionWrappers',
'toggleFullDiff',
]),
handleToggleFile(e, checkTarget) {
if (
!checkTarget ||
......@@ -165,7 +169,7 @@ export default {
this.$emit('showForkMessage');
},
handleToggleDiscussions() {
this.toggleFileDiscussions(this.diffFile);
this.toggleFileDiscussionWrappers(this.diffFile);
},
handleFileNameClick(e) {
const isLinkToOtherPage =
......
<script>
import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import { pluralize, truncate } from '~/lib/utils/text_utility';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
......@@ -19,11 +18,13 @@ export default {
type: Array,
required: true,
},
discussionsExpanded: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
discussionsExpanded() {
return this.discussions.every(discussion => discussion.expanded);
},
allDiscussions() {
return this.discussions.reduce((acc, note) => acc.concat(note.notes), []);
},
......@@ -45,26 +46,14 @@ export default {
},
},
methods: {
...mapActions(['toggleDiscussion']),
getTooltipText(noteData) {
let { note } = noteData;
if (note.length > LENGTH_OF_AVATAR_TOOLTIP) {
note = truncate(note, LENGTH_OF_AVATAR_TOOLTIP);
}
return `${noteData.author.name}: ${note}`;
},
toggleDiscussions() {
const forceExpanded = this.discussions.some(discussion => !discussion.expanded);
this.discussions.forEach(discussion => {
this.toggleDiscussion({
discussionId: discussion.id,
forceExpanded,
});
});
},
},
};
</script>
......@@ -76,7 +65,7 @@ export default {
type="button"
:aria-label="__('Show comments')"
class="diff-notes-collapse js-diff-comment-avatar js-diff-comment-button"
@click="toggleDiscussions"
@click="$emit('toggleLineDiscussions')"
>
<icon :size="12" name="collapse" />
</button>
......@@ -87,7 +76,7 @@ export default {
:img-src="note.author.avatar_url"
:tooltip-text="getTooltipText(note)"
class="diff-comment-avatar js-diff-comment-avatar"
@click.native="toggleDiscussions"
@click.native="$emit('toggleLineDiscussions')"
/>
<span
v-if="moreText"
......@@ -97,7 +86,7 @@ export default {
data-container="body"
data-placement="top"
role="button"
@click="toggleDiscussions"
@click="$emit('toggleLineDiscussions')"
>+{{ moreCount }}</span
>
</template>
......
......@@ -105,7 +105,13 @@ export default {
},
},
methods: {
...mapActions('diffs', ['loadMoreLines', 'showCommentForm', 'setHighlightedRow']),
...mapActions('diffs', [
'loadMoreLines',
'showCommentForm',
'setHighlightedRow',
'toggleLineDiscussions',
'toggleLineDiscussionWrappers',
]),
handleCommentButton() {
this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash });
},
......@@ -184,7 +190,14 @@ export default {
@click="setHighlightedRow(lineCode)"
>
</a>
<diff-gutter-avatars v-if="shouldShowAvatarsOnGutter" :discussions="line.discussions" />
<diff-gutter-avatars
v-if="shouldShowAvatarsOnGutter"
:discussions="line.discussions"
:discussions-expanded="line.discussionsExpanded"
@toggleLineDiscussions="
toggleLineDiscussions({ lineCode, fileHash, expanded: !line.discussionsExpanded })
"
/>
</template>
</div>
</template>
<script>
import diffDiscussions from './diff_discussions.vue';
import diffLineNoteForm from './diff_line_note_form.vue';
import { mapActions } from 'vuex';
import DiffDiscussions from './diff_discussions.vue';
import DiffLineNoteForm from './diff_line_note_form.vue';
import DiffDiscussionReply from './diff_discussion_reply.vue';
export default {
components: {
diffDiscussions,
diffLineNoteForm,
DiffDiscussions,
DiffLineNoteForm,
DiffDiscussionReply,
},
props: {
line: {
......@@ -32,10 +35,12 @@ export default {
if (!this.line.discussions || !this.line.discussions.length) {
return false;
}
return this.line.discussions.every(discussion => discussion.expanded);
return this.line.discussionsExpanded;
},
},
methods: {
...mapActions('diffs', ['showCommentForm']),
},
};
</script>
......@@ -49,13 +54,22 @@ export default {
:discussions="line.discussions"
:help-page-path="helpPagePath"
/>
<diff-line-note-form
v-if="line.hasForm"
:diff-file-hash="diffFileHash"
:line="line"
:note-target-line="line"
:help-page-path="helpPagePath"
/>
<diff-discussion-reply
:has-form="line.hasForm"
:render-reply-placeholder="Boolean(line.discussions.length)"
@showNewDiscussionForm="
showCommentForm({ lineCode: line.line_code, fileHash: diffFileHash })
"
>
<template #form>
<diff-line-note-form
:diff-file-hash="diffFileHash"
:line="line"
:note-target-line="line"
:help-page-path="helpPagePath"
/>
</template>
</diff-discussion-reply>
</div>
</td>
</tr>
......
<script>
import diffDiscussions from './diff_discussions.vue';
import diffLineNoteForm from './diff_line_note_form.vue';
import { mapActions } from 'vuex';
import DiffDiscussions from './diff_discussions.vue';
import DiffLineNoteForm from './diff_line_note_form.vue';
import DiffDiscussionReply from './diff_discussion_reply.vue';
export default {
components: {
diffDiscussions,
diffLineNoteForm,
DiffDiscussions,
DiffLineNoteForm,
DiffDiscussionReply,
},
props: {
line: {
......@@ -29,24 +32,30 @@ export default {
computed: {
hasExpandedDiscussionOnLeft() {
return this.line.left && this.line.left.discussions.length
? this.line.left.discussions.every(discussion => discussion.expanded)
? this.line.left.discussionsExpanded
: false;
},
hasExpandedDiscussionOnRight() {
return this.line.right && this.line.right.discussions.length
? this.line.right.discussions.every(discussion => discussion.expanded)
? this.line.right.discussionsExpanded
: false;
},
hasAnyExpandedDiscussion() {
return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight;
},
shouldRenderDiscussionsOnLeft() {
return this.line.left && this.line.left.discussions && this.hasExpandedDiscussionOnLeft;
return (
this.line.left &&
this.line.left.discussions &&
this.line.left.discussions.length &&
this.hasExpandedDiscussionOnLeft
);
},
shouldRenderDiscussionsOnRight() {
return (
this.line.right &&
this.line.right.discussions &&
this.line.right.discussions.length &&
this.hasExpandedDiscussionOnRight &&
this.line.right.type
);
......@@ -81,6 +90,22 @@ export default {
return hasCommentFormOnLeft || hasCommentFormOnRight;
},
shouldRenderReplyPlaceholderOnLeft() {
return Boolean(
this.line.left && this.line.left.discussions && this.line.left.discussions.length,
);
},
shouldRenderReplyPlaceholderOnRight() {
return Boolean(
this.line.right && this.line.right.discussions && this.line.right.discussions.length,
);
},
},
methods: {
...mapActions('diffs', ['showCommentForm']),
showNewDiscussionForm() {
this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.diffFileHash });
},
},
};
</script>
......@@ -90,37 +115,49 @@ export default {
<td class="notes-content parallel old" colspan="2">
<div v-if="shouldRenderDiscussionsOnLeft" class="content">
<diff-discussions
v-if="line.left.discussions.length"
:discussions="line.left.discussions"
:line="line.left"
:help-page-path="helpPagePath"
/>
</div>
<diff-line-note-form
v-if="showLeftSideCommentForm"
:diff-file-hash="diffFileHash"
:line="line.left"
:note-target-line="line.left"
:help-page-path="helpPagePath"
line-position="left"
/>
<diff-discussion-reply
:has-form="showLeftSideCommentForm"
:render-reply-placeholder="shouldRenderReplyPlaceholderOnLeft"
@showNewDiscussionForm="showNewDiscussionForm"
>
<template #form>
<diff-line-note-form
:diff-file-hash="diffFileHash"
:line="line.left"
:note-target-line="line.left"
:help-page-path="helpPagePath"
line-position="left"
/>
</template>
</diff-discussion-reply>
</td>
<td class="notes-content parallel new" colspan="2">
<div v-if="shouldRenderDiscussionsOnRight" class="content">
<diff-discussions
v-if="line.right.discussions.length"
:discussions="line.right.discussions"
:line="line.right"
:help-page-path="helpPagePath"
/>
</div>
<diff-line-note-form
v-if="showRightSideCommentForm"
:diff-file-hash="diffFileHash"
:line="line.right"
:note-target-line="line.right"
line-position="right"
/>
<diff-discussion-reply
:has-form="showRightSideCommentForm"
:render-reply-placeholder="shouldRenderReplyPlaceholderOnRight"
@showNewDiscussionForm="showNewDiscussionForm"
>
<template #form>
<diff-line-note-form
:diff-file-hash="diffFileHash"
:line="line.right"
:note-target-line="line.right"
line-position="right"
/>
</template>
</diff-discussion-reply>
</td>
</tr>
</template>
......@@ -12,6 +12,7 @@ import {
getNoteFormData,
convertExpandLines,
idleCallback,
allDiscussionWrappersExpanded,
} from './utils';
import * as types from './mutation_types';
import {
......@@ -79,6 +80,7 @@ export const assignDiscussionsToDiff = (
discussions = rootState.notes.discussions,
) => {
const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles);
const hash = getLocationHash();
discussions
.filter(discussion => discussion.diff_discussion)
......@@ -86,6 +88,7 @@ export const assignDiscussionsToDiff = (
commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, {
discussion,
diffPositionByLineCode,
hash,
});
});
......@@ -99,6 +102,10 @@ export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => {
commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash: file_hash, lineCode: line_code, id });
};
export const toggleLineDiscussions = ({ commit }, options) => {
commit(types.TOGGLE_LINE_DISCUSSIONS, options);
};
export const renderFileForDiscussionId = ({ commit, rootState, state }, discussionId) => {
const discussion = rootState.notes.discussions.find(d => d.id === discussionId);
......@@ -257,6 +264,31 @@ export const toggleFileDiscussions = ({ getters, dispatch }, diff) => {
});
};
export const toggleFileDiscussionWrappers = ({ commit }, diff) => {
const discussionWrappersExpanded = allDiscussionWrappersExpanded(diff);
let linesWithDiscussions;
if (diff.highlighted_diff_lines) {
linesWithDiscussions = diff.highlighted_diff_lines.filter(line => line.discussions.length);
}
if (diff.parallel_diff_lines) {
linesWithDiscussions = diff.parallel_diff_lines.filter(
line =>
(line.left && line.left.discussions.length) ||
(line.right && line.right.discussions.length),
);
}
if (linesWithDiscussions.length) {
linesWithDiscussions.forEach(line => {
commit(types.TOGGLE_LINE_DISCUSSIONS, {
fileHash: diff.file_hash,
lineCode: line.line_code,
expanded: !discussionWrappersExpanded,
});
});
}
};
export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => {
const postData = getNoteFormData({
commit: state.commit,
......@@ -267,7 +299,7 @@ export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => {
return dispatch('saveNote', postData, { root: true })
.then(result => dispatch('updateDiscussion', result.discussion, { root: true }))
.then(discussion => dispatch('assignDiscussionsToDiff', [discussion]))
.then(() => dispatch('updateResolvableDiscussonsCounts', null, { root: true }))
.then(() => dispatch('updateResolvableDiscussionsCounts', null, { root: true }))
.then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.file_hash))
.catch(() => createFlash(s__('MergeRequests|Saving the comment failed')));
};
......
......@@ -35,3 +35,5 @@ export const ADD_CURRENT_VIEW_DIFF_FILE_LINES = 'ADD_CURRENT_VIEW_DIFF_FILE_LINE
export const TOGGLE_DIFF_FILE_RENDERING_MORE = 'TOGGLE_DIFF_FILE_RENDERING_MORE';
export const SET_SHOW_SUGGEST_POPOVER = 'SET_SHOW_SUGGEST_POPOVER';
export const TOGGLE_LINE_DISCUSSIONS = 'TOGGLE_LINE_DISCUSSIONS';
......@@ -6,6 +6,7 @@ import {
addContextLines,
prepareDiffData,
isDiscussionApplicableToLine,
updateLineInFile,
} from './utils';
import * as types from './mutation_types';
......@@ -109,7 +110,7 @@ export default {
}));
},
[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode }) {
[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode, hash }) {
const { latestDiff } = state;
const discussionLineCode = discussion.line_code;
......@@ -130,13 +131,27 @@ export default {
: [],
});
const setDiscussionsExpanded = line => {
const isLineNoteTargeted = line.discussions.some(
disc => disc.notes && disc.notes.find(note => hash === `note_${note.id}`),
);
return {
...line,
discussionsExpanded:
line.discussions && line.discussions.length
? line.discussions.some(disc => !disc.resolved) || isLineNoteTargeted
: false,
};
};
state.diffFiles = state.diffFiles.map(diffFile => {
if (diffFile.file_hash === fileHash) {
const file = { ...diffFile };
if (file.highlighted_diff_lines) {
file.highlighted_diff_lines = file.highlighted_diff_lines.map(line =>
lineCheck(line) ? mapDiscussions(line) : line,
setDiscussionsExpanded(lineCheck(line) ? mapDiscussions(line) : line),
);
}
......@@ -148,8 +163,10 @@ export default {
if (left || right) {
return {
...line,
left: line.left ? mapDiscussions(line.left) : null,
right: line.right ? mapDiscussions(line.right, () => !left) : null,
left: line.left ? setDiscussionsExpanded(mapDiscussions(line.left)) : null,
right: line.right
? setDiscussionsExpanded(mapDiscussions(line.right, () => !left))
: null,
};
}
......@@ -173,32 +190,11 @@ export default {
[types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) {
const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash);
if (selectedFile) {
if (selectedFile.parallel_diff_lines) {
const targetLine = selectedFile.parallel_diff_lines.find(
line =>
(line.left && line.left.line_code === lineCode) ||
(line.right && line.right.line_code === lineCode),
);
if (targetLine) {
const side = targetLine.left && targetLine.left.line_code === lineCode ? 'left' : 'right';
Object.assign(targetLine[side], {
discussions: targetLine[side].discussions.filter(discussion => discussion.notes.length),
});
}
}
if (selectedFile.highlighted_diff_lines) {
const targetInlineLine = selectedFile.highlighted_diff_lines.find(
line => line.line_code === lineCode,
);
if (targetInlineLine) {
Object.assign(targetInlineLine, {
discussions: targetInlineLine.discussions.filter(discussion => discussion.notes.length),
});
}
}
updateLineInFile(selectedFile, lineCode, line =>
Object.assign(line, {
discussions: line.discussions.filter(discussion => discussion.notes.length),
}),
);
if (selectedFile.discussions && selectedFile.discussions.length) {
selectedFile.discussions = selectedFile.discussions.filter(
......@@ -207,6 +203,15 @@ export default {
}
}
},
[types.TOGGLE_LINE_DISCUSSIONS](state, { fileHash, lineCode, expanded }) {
const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash);
updateLineInFile(selectedFile, lineCode, line =>
Object.assign(line, { discussionsExpanded: expanded }),
);
},
[types.TOGGLE_FOLDER_OPEN](state, path) {
state.treeEntries[path].opened = !state.treeEntries[path].opened;
},
......
......@@ -454,3 +454,48 @@ export const convertExpandLines = ({
};
export const idleCallback = cb => requestIdleCallback(cb);
export const updateLineInFile = (selectedFile, lineCode, updateFn) => {
if (selectedFile.parallel_diff_lines) {
const targetLine = selectedFile.parallel_diff_lines.find(
line =>
(line.left && line.left.line_code === lineCode) ||
(line.right && line.right.line_code === lineCode),
);
if (targetLine) {
const side = targetLine.left && targetLine.left.line_code === lineCode ? 'left' : 'right';
updateFn(targetLine[side]);
}
}
if (selectedFile.highlighted_diff_lines) {
const targetInlineLine = selectedFile.highlighted_diff_lines.find(
line => line.line_code === lineCode,
);
if (targetInlineLine) {
updateFn(targetInlineLine);
}
}
};
export const allDiscussionWrappersExpanded = diff => {
const discussionsExpandedArray = [];
if (diff.parallel_diff_lines) {
diff.parallel_diff_lines.forEach(line => {
if (line.left && line.left.discussions.length) {
discussionsExpandedArray.push(line.left.discussionsExpanded);
}
if (line.right && line.right.discussions.length) {
discussionsExpandedArray.push(line.right.discussionsExpanded);
}
});
} else if (diff.highlighted_diff_lines) {
diff.parallel_diff_lines.forEach(line => {
if (line.discussions.length) {
discussionsExpandedArray.push(line.discussionsExpanded);
}
});
}
return discussionsExpandedArray.every(el => el);
};
<script>
import { s__, sprintf } from '~/locale';
import { __, s__, sprintf } from '~/locale';
import { formatTime } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
......@@ -28,7 +28,7 @@ export default {
},
computed: {
title() {
return 'Deploy to...';
return __('Deploy to...');
},
},
methods: {
......@@ -80,7 +80,8 @@ export default {
data-toggle="dropdown"
>
<span>
<icon name="play" /> <icon name="chevron-down" />
<icon name="play" />
<icon name="chevron-down" />
<gl-loading-icon v-if="isLoading" />
</span>
</button>
......@@ -94,9 +95,10 @@ export default {
class="js-manual-action-link no-btn btn d-flex align-items-center"
@click="onClickAction(action)"
>
<span class="flex-fill"> {{ action.name }} </span>
<span class="flex-fill">{{ action.name }}</span>
<span v-if="action.scheduledAt" class="text-secondary">
<icon name="clock" /> {{ remainingTime(action) }}
<icon name="clock" />
{{ remainingTime(action) }}
</span>
</button>
</li>
......
<script>
import { __, sprintf } from '~/locale';
import Timeago from 'timeago.js';
import _ from 'underscore';
import { GlTooltipDirective } from '@gitlab/ui';
......@@ -172,7 +173,9 @@ export default {
this.model.last_deployment.user &&
this.model.last_deployment.user.username
) {
return `${this.model.last_deployment.user.username}'s avatar'`;
return sprintf(__("%{username}'s avatar"), {
username: this.model.last_deployment.user.username,
});
}
return '';
},
......@@ -293,6 +296,9 @@ export default {
* @returns {Boolean|Undefined}
*/
isLastDeployment() {
// TODO: when the vue i18n rules are merged need to disable @gitlab/i18n/no-non-i18n-strings
// name: 'last?' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
// Vue i18n ESLint rules issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/63560
return this.model && this.model.last_deployment && this.model.last_deployment['last?'];
},
......
<script>
import { __ } from '~/locale';
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
......@@ -21,7 +22,7 @@ export default {
},
computed: {
title() {
return 'Monitoring';
return __('Monitoring');
},
},
};
......
......@@ -5,6 +5,7 @@
*/
import { GlTooltipDirective } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale';
export default {
components: {
......@@ -27,7 +28,7 @@ export default {
},
computed: {
title() {
return 'Terminal';
return __('Terminal');
},
},
};
......
......@@ -49,9 +49,9 @@ export default {
</p>
</div>
<div class="form-group" :class="{ 'gl-show-field-errors': connectError }">
<label class="label-bold" for="error-tracking-token">{{
s__('ErrorTracking|Auth Token')
}}</label>
<label class="label-bold" for="error-tracking-token">
{{ s__('ErrorTracking|Auth Token') }}
</label>
<div class="row">
<div class="col-8 col-md-9 gl-pr-0">
<gl-form-input
......@@ -65,9 +65,8 @@ export default {
<gl-button
class="js-error-tracking-connect prepend-left-5"
@click="$emit('handle-connect')"
>{{ __('Connect') }}</gl-button
>
{{ __('Connect') }}
</gl-button>
<icon
v-show="connectSuccessful"
class="js-error-tracking-connect-success prepend-left-5 text-success align-middle"
......
......@@ -59,7 +59,7 @@ export default {
<template>
<div>
<div v-if="!isLocalStorageAvailable" class="dropdown-info-note">
This feature requires local storage to be enabled
{{ __('This feature requires local storage to be enabled') }}
</div>
<ul v-else-if="hasItems">
<li v-for="(item, index) in processedItems" :key="`processed-items-${index}`">
......@@ -90,10 +90,10 @@ export default {
class="filtered-search-history-clear-button"
@click="onRequestClearRecentSearches($event)"
>
Clear recent searches
{{ __('Clear recent searches') }}
</button>
</li>
</ul>
<div v-else class="dropdown-info-note">You don't have any recent searches</div>
<div v-else class="dropdown-info-note">{{ __("You don't have any recent searches") }}</div>
</div>
</template>
import $ from 'jquery';
import { slugifyWithHyphens } from './lib/utils/text_utility';
import { slugify } from './lib/utils/text_utility';
export default class Group {
constructor() {
......@@ -14,7 +14,7 @@ export default class Group {
}
update() {
const slug = slugifyWithHyphens(this.groupName.val());
const slug = slugify(this.groupName.val());
this.groupPath.val(slug);
}
......
......@@ -107,7 +107,8 @@ export default {
@click="openFileInEditor"
>
<span class="multi-file-commit-list-file-path d-flex align-items-center">
<file-icon :file-name="file.name" class="append-right-8" />{{ file.name }}
<file-icon :file-name="file.name" class="append-right-8" />
{{ file.name }}
</span>
<div class="ml-auto d-flex align-items-center">
<div class="d-flex align-items-center ide-commit-list-changed-icon">
......
......@@ -27,7 +27,7 @@ export default {
target="_blank"
rel="noopener noreferrer"
>
<span class="vertical-align-middle">Open in file view</span>
<span class="vertical-align-middle">{{ __('Open in file view') }}</span>
<icon :size="16" name="external-link" css-classes="vertical-align-middle space-right" />
</a>
</div>
......
......@@ -8,6 +8,7 @@ import { activityBarViews, viewerTypes } from '../constants';
import Editor from '../lib/editor';
import ExternalLink from './external_link.vue';
import FileTemplatesBar from './file_templates/bar.vue';
import { __ } from '~/locale';
export default {
components: {
......@@ -40,27 +41,36 @@ export default {
},
showContentViewer() {
return (
(this.shouldHideEditor || this.file.viewMode === 'preview') &&
(this.shouldHideEditor || this.isPreviewViewMode) &&
(this.viewer !== viewerTypes.mr || !this.file.mrChange)
);
},
showDiffViewer() {
return this.shouldHideEditor && this.file.mrChange && this.viewer === viewerTypes.mr;
},
isEditorViewMode() {
return this.file.viewMode === 'editor';
},
isPreviewViewMode() {
return this.file.viewMode === 'preview';
},
editTabCSS() {
return {
active: this.file.viewMode === 'editor',
active: this.isEditorViewMode,
};
},
previewTabCSS() {
return {
active: this.file.viewMode === 'preview',
active: this.isPreviewViewMode,
};
},
fileType() {
const info = viewerInformationForPath(this.file.path);
return (info && info.id) || '';
},
showEditor() {
return !this.shouldHideEditor && this.isEditorViewMode;
},
},
watch: {
file(newVal, oldVal) {
......@@ -89,7 +99,7 @@ export default {
}
},
rightPanelCollapsed() {
this.editor.updateDimensions();
this.refreshEditorDimensions();
},
viewer() {
if (!this.file.pending) {
......@@ -98,11 +108,17 @@ export default {
},
panelResizing() {
if (!this.panelResizing) {
this.editor.updateDimensions();
this.refreshEditorDimensions();
}
},
rightPaneIsOpen() {
this.editor.updateDimensions();
this.refreshEditorDimensions();
},
showEditor(val) {
if (val) {
// We need to wait for the editor to actually be rendered.
this.$nextTick(() => this.refreshEditorDimensions());
}
},
},
beforeDestroy() {
......@@ -145,7 +161,14 @@ export default {
this.createEditorInstance();
})
.catch(err => {
flash('Error setting up editor. Please try again.', 'alert', document, null, false, true);
flash(
__('Error setting up editor. Please try again.'),
'alert',
document,
null,
false,
true,
);
throw err;
});
},
......@@ -212,6 +235,11 @@ export default {
eol: this.model.eol,
});
},
refreshEditorDimensions() {
if (this.showEditor) {
this.editor.updateDimensions();
}
},
},
viewerTypes,
};
......@@ -227,12 +255,8 @@ export default {
role="button"
@click.prevent="setFileViewMode({ file, viewMode: 'editor' })"
>
<template v-if="viewer === $options.viewerTypes.edit">
{{ __('Edit') }}
</template>
<template v-else>
{{ __('Review') }}
</template>
<template v-if="viewer === $options.viewerTypes.edit">{{ __('Edit') }}</template>
<template v-else>{{ __('Review') }}</template>
</a>
</li>
<li v-if="file.previewMode" :class="previewTabCSS">
......@@ -240,16 +264,15 @@ export default {
href="javascript:void(0);"
role="button"
@click.prevent="setFileViewMode({ file, viewMode: 'preview' })"
>{{ file.previewMode.previewTitle }}</a
>
{{ file.previewMode.previewTitle }}
</a>
</li>
</ul>
<external-link :file="file" />
</div>
<file-templates-bar v-if="showFileTemplatesBar(file.name)" />
<div
v-show="!shouldHideEditor && file.viewMode === 'editor'"
v-show="showEditor"
ref="editor"
:class="{
'is-readonly': isCommitModeActive,
......
<script>
import { __, sprintf } from '~/locale';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import '~/lib/utils/datetime_utility';
......@@ -18,7 +19,9 @@ export default {
},
computed: {
lockTooltip() {
return `Locked by ${this.file.file_lock.user.name}`;
return sprintf(__(`Locked by %{fileLockUserName}`), {
fileLockUserName: this.file.file_lock.user.name,
});
},
},
};
......
<script>
import { __, sprintf } from '~/locale';
import { mapActions } from 'vuex';
import FileIcon from '~/vue_shared/components/file_icon.vue';
......@@ -27,9 +28,9 @@ export default {
computed: {
closeLabel() {
if (this.fileHasChanged) {
return `${this.tab.name} changed`;
return sprintf(__(`%{tabname} changed`), { tabname: this.tab.name });
}
return `Close ${this.tab.name}`;
return sprintf(__(`Close %{tabname}`, { tabname: this.tab.name }));
},
showChangedIcon() {
if (this.tab.pending) return true;
......
......@@ -62,7 +62,7 @@ export const createTempEntry = (
new Promise(resolve => {
const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
if (state.entries[name]) {
if (state.entries[name] && !state.entries[name].deleted) {
flash(
`The name "${name.split('/').pop()}" is already taken in this directory.`,
'alert',
......@@ -208,6 +208,7 @@ export const deleteEntry = ({ commit, dispatch, state }, path) => {
}
commit(types.DELETE_ENTRY, path);
dispatch('stageChange', path);
dispatch('triggerFilesChange');
};
......
......@@ -186,6 +186,8 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true });
commit(rootTypes.CLEAR_REPLACED_FILES, null, { root: true });
setTimeout(() => {
commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
}, 5000);
......
......@@ -60,6 +60,8 @@ export const CLEAR_STAGED_CHANGES = 'CLEAR_STAGED_CHANGES';
export const STAGE_CHANGE = 'STAGE_CHANGE';
export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE';
export const CLEAR_REPLACED_FILES = 'CLEAR_REPLACED_FILES';
export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
......
......@@ -56,6 +56,11 @@ export default {
stagedFiles: [],
});
},
[types.CLEAR_REPLACED_FILES](state) {
Object.assign(state, {
replacedFiles: [],
});
},
[types.SET_ENTRIES](state, entries) {
Object.assign(state, {
entries,
......@@ -70,6 +75,13 @@ export default {
Object.assign(state.entries, {
[key]: entry,
});
} else if (foundEntry.deleted) {
Object.assign(state.entries, {
[key]: {
...entry,
replaces: true,
},
});
} else {
const tree = entry.tree.filter(
f => foundEntry.tree.find(e => e.path === f.path) === undefined,
......@@ -144,6 +156,7 @@ export default {
raw: file.content,
changed: Boolean(changedFile),
staged: false,
replaces: false,
prevPath: '',
moved: false,
lastCommitSha: lastCommit.commit.id,
......
......@@ -170,12 +170,16 @@ export default {
entries: Object.assign(state.entries, {
[path]: Object.assign(state.entries[path], {
staged: true,
changed: false,
}),
}),
});
if (stagedFile) {
Object.assign(state, {
replacedFiles: state.replacedFiles.concat({
...stagedFile,
}),
});
Object.assign(stagedFile, {
...state.entries[path],
});
......
......@@ -6,6 +6,7 @@ export default () => ({
currentMergeRequestId: '',
changedFiles: [],
stagedFiles: [],
replacedFiles: [],
endpoints: {},
lastCommitMsg: '',
lastCommitPath: '',
......
......@@ -18,6 +18,7 @@ export const dataStructure = () => ({
active: false,
changed: false,
staged: false,
replaces: false,
lastCommitPath: '',
lastCommitSha: '',
lastCommit: {
......@@ -119,7 +120,7 @@ export const commitActionForFile = file => {
return commitActionTypes.move;
} else if (file.deleted) {
return commitActionTypes.delete;
} else if (file.tempFile) {
} else if (file.tempFile && !file.replaces) {
return commitActionTypes.create;
}
......@@ -151,7 +152,8 @@ export const createCommitPayload = ({
previous_path: f.prevPath === '' ? undefined : f.prevPath,
content: f.prevPath ? null : f.content || undefined,
encoding: f.base64 ? 'base64' : 'text',
last_commit_id: newBranch || f.deleted || f.prevPath ? undefined : f.lastCommitSha,
last_commit_id:
newBranch || f.deleted || f.prevPath || f.replaces ? undefined : f.lastCommitSha,
})),
start_sha: newBranch ? rootGetters.lastCommit.short_id : undefined,
});
......
......@@ -22,7 +22,7 @@ export default (resolvers = {}, config = {}) => {
return new ApolloClient({
link: ApolloLink.split(
operation => operation.getContext().hasUpload,
operation => operation.getContext().hasUpload || operation.getContext().isSingleRequest,
createUploadLink(httpOptions),
new BatchHttpLink(httpOptions),
),
......
......@@ -44,11 +44,18 @@ export const pluralize = (str, count) => str + (count > 1 || count === 0 ? 's' :
export const dasherize = str => str.replace(/[_\s]+/g, '-');
/**
* Replaces whitespaces with hyphens and converts to lower case
* Replaces whitespaces with hyphens, convert to lower case and remove non-allowed special characters
* @param {String} str
* @returns {String}
*/
export const slugifyWithHyphens = str => str.toLowerCase().replace(/\s+/g, '-');
export const slugify = str => {
const slug = str
.trim()
.toLowerCase()
.replace(/[^a-zA-Z0-9_.-]+/g, '-');
return slug === '-' ? '' : slug;
};
/**
* Replaces whitespaces with underscore and converts to lower case
......
......@@ -21,7 +21,7 @@ const updateIssue = (url, issueList, { move_before_id, move_after_id }) =>
const initManualOrdering = () => {
const issueList = document.querySelector('.manual-ordering');
if (!issueList || !(gon.features && gon.features.manualSorting)) {
if (!issueList || !(gon.features && gon.features.manualSorting) || !(gon.current_user_id > 0)) {
return;
}
......
<script>
import { __ } from '~/locale';
import { GlLink } from '@gitlab/ui';
import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat';
import { debounceByAnimationFrame, roundOffFloat } from '~/lib/utils/common_utils';
......@@ -14,6 +15,7 @@ export default {
components: {
GlAreaChart,
GlChartSeriesLabel,
GlLink,
Icon,
},
inheritAttrs: false,
......@@ -44,6 +46,10 @@ export default {
required: false,
default: () => [],
},
projectPath: {
type: String,
required: true,
},
thresholds: {
type: Array,
required: false,
......@@ -55,6 +61,7 @@ export default {
tooltip: {
title: '',
content: [],
commitUrl: '',
isDeployment: false,
sha: '',
},
......@@ -195,12 +202,13 @@ export default {
this.tooltip.title = dateFormat(params.value, 'dd mmm yyyy, h:MMTT');
this.tooltip.content = [];
params.seriesData.forEach(seriesData => {
if (seriesData.componentSubType === graphTypes.deploymentData) {
this.tooltip.isDeployment = true;
this.tooltip.isDeployment = seriesData.componentSubType === graphTypes.deploymentData;
if (this.tooltip.isDeployment) {
const [deploy] = this.recentDeployments.filter(
deployment => deployment.createdAt === seriesData.value[0],
);
this.tooltip.sha = deploy.sha.substring(0, 8);
this.tooltip.commitUrl = deploy.commitUrl;
} else {
const { seriesName, color } = seriesData;
// seriesData.value contains the chart's [x, y] value pair
......@@ -259,7 +267,7 @@ export default {
</template>
<div slot="tooltipContent" class="d-flex align-items-center">
<icon name="commit" class="mr-2" />
{{ tooltip.sha }}
<gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link>
</div>
</template>
<template v-else>
......
......@@ -359,6 +359,7 @@ export default {
<monitor-area-chart
v-for="(graphData, graphIndex) in chartsWithData(groupData.metrics)"
:key="graphIndex"
:project-path="projectPath"
:graph-data="graphData"
:deployment-data="deploymentData"
:thresholds="getGraphAlertValues(graphData.queries)"
......
......@@ -8,10 +8,12 @@ export default (props = {}) => {
const el = document.getElementById('prometheus-graphs');
if (el && el.dataset) {
store.dispatch('monitoringDashboard/setFeatureFlags', {
prometheusEndpointEnabled: gon.features.environmentMetricsUsePrometheusEndpoint,
multipleDashboardsEnabled: gon.features.environmentMetricsShowMultipleDashboards,
});
if (gon.features) {
store.dispatch('monitoringDashboard/setFeatureFlags', {
prometheusEndpointEnabled: gon.features.environmentMetricsUsePrometheusEndpoint,
multipleDashboardsEnabled: gon.features.environmentMetricsShowMultipleDashboards,
});
}
const [currentDashboard] = getParameterValues('dashboard');
......
......@@ -40,7 +40,11 @@ export default {
<template>
<div class="discussion-with-resolve-btn">
<reply-placeholder class="qa-discussion-reply" @onClick="$emit('showReplyForm')" />
<reply-placeholder
:button-text="s__('MergeRequests|Reply...')"
class="qa-discussion-reply"
@onClick="$emit('showReplyForm')"
/>
<resolve-discussion-button
v-if="discussion.resolvable"
:is-resolving="isResolving"
......@@ -53,6 +57,17 @@ export default {
v-if="shouldShowJumpToNextDiscussion"
@onClick="$emit('jumpToNextDiscussion')"
/>
<resolve-with-issue-button
v-if="discussion.resolvable && resolveWithIssuePath"
:url="resolveWithIssuePath"
/>
</div>
<div
v-if="discussion.resolvable && shouldShowJumpToNextDiscussion"
class="btn-group discussion-actions ml-sm-2"
>
<jump-to-next-discussion-button @onClick="$emit('jumpToNextDiscussion')" />
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { mapGetters, mapActions } from 'vuex';
import { SYSTEM_NOTE } from '../constants';
import { __ } from '~/locale';
import NoteableNote from './noteable_note.vue';
import PlaceholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import PlaceholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
import SystemNote from '~/vue_shared/components/notes/system_note.vue';
import NoteableNote from './noteable_note.vue';
import ToggleRepliesWidget from './toggle_replies_widget.vue';
import NoteEditedText from './note_edited_text.vue';
......@@ -72,6 +72,7 @@ export default {
},
},
methods: {
...mapActions(['toggleDiscussion']),
componentName(note) {
if (note.isPlaceholderNote) {
if (note.placeholderType === SYSTEM_NOTE) {
......@@ -101,12 +102,12 @@ export default {
<component
:is="componentName(firstNote)"
:note="componentData(firstNote)"
:line="line"
:line="line || diffLine"
:commit="commit"
:help-page-path="helpPagePath"
:show-reply-button="userCanReply"
@handle-delete-note="$emit('deleteNote')"
@start-replying="$emit('startReplying')"
@handleDeleteNote="$emit('deleteNote')"
@startReplying="$emit('startReplying')"
>
<note-edited-text
v-if="discussion.resolved"
......@@ -118,23 +119,29 @@ export default {
/>
<slot slot="avatar-badge" name="avatar-badge"></slot>
</component>
<toggle-replies-widget
v-if="hasReplies"
:collapsed="!isExpanded"
:replies="replies"
@toggle="$emit('toggleDiscussion')"
/>
<template v-if="isExpanded">
<component
:is="componentName(note)"
v-for="note in replies"
:key="note.id"
:note="componentData(note)"
:help-page-path="helpPagePath"
:line="line"
@handle-delete-note="$emit('deleteNote')"
<div
:class="discussion.diff_discussion ? 'discussion-collapsible bordered-box clearfix' : ''"
>
<toggle-replies-widget
v-if="hasReplies"
:collapsed="!isExpanded"
:replies="replies"
:class="{ 'discussion-toggle-replies': discussion.diff_discussion }"
@toggle="toggleDiscussion({ discussionId: discussion.id })"
/>
</template>
<template v-if="isExpanded">
<component
:is="componentName(note)"
v-for="note in replies"
:key="note.id"
:note="componentData(note)"
:help-page-path="helpPagePath"
:line="line"
@handleDeleteNote="$emit('deleteNote')"
/>
</template>
<slot :show-replies="isExpanded || !hasReplies" name="footer"></slot>
</div>
</template>
<template v-else>
<component
......@@ -144,12 +151,12 @@ export default {
:note="componentData(note)"
:help-page-path="helpPagePath"
:line="diffLine"
@handle-delete-note="$emit('deleteNote')"
@handleDeleteNote="$emit('deleteNote')"
>
<slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot>
</component>
<slot :show-replies="isExpanded || !hasReplies" name="footer"></slot>
</template>
</ul>
<slot :show-replies="isExpanded || !hasReplies" name="footer"></slot>
</div>
</template>
<script>
export default {
name: 'ReplyPlaceholder',
props: {
buttonText: {
type: String,
required: true,
},
},
};
</script>
......@@ -12,6 +18,6 @@ export default {
:title="s__('MergeRequests|Add a reply')"
@click="$emit('onClick')"
>
{{ s__('MergeRequests|Reply...') }}
{{ buttonText }}
</button>
</template>
......@@ -126,16 +126,13 @@ export default {
return this.discussion.resolved_by_push ? __('Automatically resolved') : __('Resolved');
},
shouldShowJumpToNextDiscussion() {
return this.showJumpToNextDiscussion(
this.discussion.id,
this.discussionsByDiffOrder ? 'diff' : 'discussion',
);
return this.showJumpToNextDiscussion(this.discussionsByDiffOrder ? 'diff' : 'discussion');
},
shouldRenderDiffs() {
return this.discussion.diff_discussion && this.renderDiffFile;
},
shouldGroupReplies() {
return !this.shouldRenderDiffs && !this.discussion.diff_discussion;
return !this.shouldRenderDiffs;
},
wrapperComponent() {
return this.shouldRenderDiffs ? diffWithNote : 'div';
......@@ -253,6 +250,11 @@ export default {
clearDraft(this.autosaveKey);
},
saveReply(noteText, form, callback) {
if (!noteText) {
this.cancelReplyForm();
callback();
return;
}
const postData = {
in_reply_to_discussion_id: this.discussion.reply_id,
target_type: this.getNoteableData.targetType,
......@@ -366,7 +368,6 @@ Please check your network connection and try again.`;
:line="line"
:should-group-replies="shouldGroupReplies"
@startReplying="showReplyForm"
@toggleDiscussion="toggleDiscussionHandler"
@deleteNote="deleteNoteHandler"
>
<slot slot="avatar-badge" name="avatar-badge"></slot>
......@@ -379,7 +380,7 @@ Please check your network connection and try again.`;
<div
v-else-if="showReplies"
:class="{ 'is-replying': isReplying }"
class="discussion-reply-holder"
class="discussion-reply-holder clearfix"
>
<user-avatar-link
v-if="!isReplying && userCanReply"
......
......@@ -51,7 +51,7 @@ export const fetchDiscussions = ({ commit, dispatch }, { path, filter }) =>
.then(res => res.json())
.then(discussions => {
commit(types.SET_INITIAL_DISCUSSIONS, discussions);
dispatch('updateResolvableDiscussonsCounts');
dispatch('updateResolvableDiscussionsCounts');
});
export const updateDiscussion = ({ commit, state }, discussion) => {
......@@ -67,7 +67,7 @@ export const deleteNote = ({ commit, dispatch, state }, note) =>
commit(types.DELETE_NOTE, note);
dispatch('updateMergeRequestWidget');
dispatch('updateResolvableDiscussonsCounts');
dispatch('updateResolvableDiscussionsCounts');
if (isInMRPage()) {
dispatch('diffs/removeDiscussionsFromDiff', discussion);
......@@ -117,7 +117,7 @@ export const replyToDiscussion = ({ commit, state, getters, dispatch }, { endpoi
dispatch('updateMergeRequestWidget');
dispatch('startTaskList');
dispatch('updateResolvableDiscussonsCounts');
dispatch('updateResolvableDiscussionsCounts');
} else {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res);
}
......@@ -135,7 +135,7 @@ export const createNewNote = ({ commit, dispatch }, { endpoint, data }) =>
dispatch('updateMergeRequestWidget');
dispatch('startTaskList');
dispatch('updateResolvableDiscussonsCounts');
dispatch('updateResolvableDiscussionsCounts');
}
return res;
});
......@@ -168,7 +168,7 @@ export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved,
commit(mutationType, res);
dispatch('updateResolvableDiscussonsCounts');
dispatch('updateResolvableDiscussionsCounts');
dispatch('updateMergeRequestWidget');
});
......@@ -442,7 +442,7 @@ export const startTaskList = ({ dispatch }) =>
}),
);
export const updateResolvableDiscussonsCounts = ({ commit }) =>
export const updateResolvableDiscussionsCounts = ({ commit }) =>
commit(types.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS);
export const submitSuggestion = (
......
......@@ -61,15 +61,13 @@ export const unresolvedDiscussionsCount = state => state.unresolvedDiscussionsCo
export const resolvableDiscussionsCount = state => state.resolvableDiscussionsCount;
export const hasUnresolvedDiscussions = state => state.hasUnresolvedDiscussions;
export const showJumpToNextDiscussion = (state, getters) => (discussionId, mode = 'discussion') => {
export const showJumpToNextDiscussion = (state, getters) => (mode = 'discussion') => {
const orderedDiffs =
mode !== 'discussion'
? getters.unresolvedDiscussionsIdsByDiff
: getters.unresolvedDiscussionsIdsByDate;
const indexOf = orderedDiffs.indexOf(discussionId);
return indexOf !== -1 && indexOf < orderedDiffs.length - 1;
return orderedDiffs.length > 1;
};
export const isDiscussionResolved = (state, getters) => discussionId =>
......
......@@ -5,5 +5,5 @@ import initDiverganceGraph from '~/branches/divergence_graph';
document.addEventListener('DOMContentLoaded', () => {
AjaxLoadingSpinner.init();
new DeleteModal(); // eslint-disable-line no-new
initDiverganceGraph();
initDiverganceGraph(document.querySelector('.js-branch-list').dataset.divergingCountsEndpoint);
});
<script>
import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue';
import projectFeatureToggle from '~/vue_shared/components/toggle_button.vue';
import { featureAccessLevelNone } from '../constants';
export default {
components: {
......@@ -43,7 +44,7 @@ export default {
if (this.featureEnabled) {
return this.options;
}
return [[0, 'Enable feature to choose access level']];
return [featureAccessLevelNone];
},
displaySelectInput() {
......
<script>
import settingsMixin from 'ee_else_ce/pages/projects/shared/permissions/mixins/settings_pannel_mixin';
import { __ } from '~/locale';
import projectFeatureSetting from './project_feature_setting.vue';
import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue';
import projectFeatureToggle from '~/vue_shared/components/toggle_button.vue';
import projectSettingRow from './project_setting_row.vue';
import { visibilityOptions, visibilityLevelDescriptions } from '../constants';
import {
visibilityOptions,
visibilityLevelDescriptions,
featureAccessLevelMembers,
featureAccessLevelEveryone,
} from '../constants';
import { toggleHiddenClassBySelector } from '../external';
const PAGE_FEATURE_ACCESS_LEVEL = __('Everyone');
export default {
components: {
projectFeatureSetting,
projectFeatureToggle,
projectSettingRow,
},
mixins: [settingsMixin],
props: {
currentSettings: {
......@@ -37,6 +47,11 @@ export default {
required: false,
default: false,
},
packagesAvailable: {
type: Boolean,
required: false,
default: false,
},
visibilityHelpPath: {
type: String,
required: false,
......@@ -67,8 +82,12 @@ export default {
required: false,
default: '',
},
packagesHelpPath: {
type: String,
required: false,
default: '',
},
},
data() {
const defaults = {
visibilityOptions,
......@@ -91,9 +110,9 @@ export default {
computed: {
featureAccessLevelOptions() {
const options = [[10, 'Only Project Members']];
const options = [featureAccessLevelMembers];
if (this.visibilityLevel !== visibilityOptions.PRIVATE) {
options.push([20, 'Everyone With Access']);
options.push(featureAccessLevelEveryone);
}
return options;
},
......@@ -106,7 +125,7 @@ export default {
pagesFeatureAccessLevelOptions() {
if (this.visibilityLevel !== visibilityOptions.PUBLIC) {
return this.featureAccessLevelOptions.concat([[30, 'Everyone']]);
return this.featureAccessLevelOptions.concat([[30, PAGE_FEATURE_ACCESS_LEVEL]]);
}
return this.featureAccessLevelOptions;
},
......@@ -148,24 +167,6 @@ export default {
}
},
repositoryAccessLevel(value, oldValue) {
if (value < oldValue) {
// sub-features cannot have more premissive access level
this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value);
this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value);
if (value === 0) {
this.containerRegistryEnabled = false;
this.lfsEnabled = false;
}
} else if (oldValue === 0) {
this.mergeRequestsAccessLevel = value;
this.buildsAccessLevel = value;
this.containerRegistryEnabled = true;
this.lfsEnabled = true;
}
},
issuesAccessLevel(value, oldValue) {
if (value === 0) toggleHiddenClassBySelector('.issues-feature', true);
else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false);
......@@ -207,23 +208,20 @@ export default {
<option
:value="visibilityOptions.PRIVATE"
:disabled="!visibilityAllowed(visibilityOptions.PRIVATE)"
>{{ __('Private') }}</option
>
Private
</option>
<option
:value="visibilityOptions.INTERNAL"
:disabled="!visibilityAllowed(visibilityOptions.INTERNAL)"
>{{ __('Internal') }}</option
>
Internal
</option>
<option
:value="visibilityOptions.PUBLIC"
:disabled="!visibilityAllowed(visibilityOptions.PUBLIC)"
>{{ __('Public') }}</option
>
Public
</option>
</select>
<i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"> </i>
<i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i>
</div>
</div>
<span class="form-text text-muted">{{ visibilityLevelDescription }}</span>
......@@ -299,6 +297,18 @@ export default {
name="project[lfs_enabled]"
/>
</project-setting-row>
<project-setting-row
v-if="packagesAvailable"
:help-path="packagesHelpPath"
label="Packages"
help-text="Every project can have its own space to store its packages"
>
<project-feature-toggle
v-model="packagesEnabled"
:disabled-input="!repositoryEnabled"
name="project[packages_enabled]"
/>
</project-setting-row>
</div>
<project-setting-row label="Wiki" help-text="Pages for project documentation">
<project-feature-setting
......
......@@ -15,3 +15,30 @@ export const visibilityLevelDescriptions = {
'The project can be accessed by anyone, regardless of authentication.',
),
};
const featureAccessLevel = {
NOT_ENABLED: 0,
PROJECT_MEMBERS: 10,
EVERYONE: 20,
};
const featureAccessLevelDescriptions = {
[featureAccessLevel.NOT_ENABLED]: __('Enable feature to choose access level'),
[featureAccessLevel.PROJECT_MEMBERS]: __('Only Project Members'),
[featureAccessLevel.EVERYONE]: __('Everyone With Access'),
};
export const featureAccessLevelNone = [
featureAccessLevel.NOT_ENABLED,
featureAccessLevelDescriptions[featureAccessLevel.NOT_ENABLED],
];
export const featureAccessLevelMembers = [
featureAccessLevel.PROJECT_MEMBERS,
featureAccessLevelDescriptions[featureAccessLevel.PROJECT_MEMBERS],
];
export const featureAccessLevelEveryone = [
featureAccessLevel.EVERYONE,
featureAccessLevelDescriptions[featureAccessLevel.EVERYONE],
];
export default {
data() {
return {
packagesEnabled: false,
};
},
watch: {
repositoryAccessLevel(value, oldValue) {
if (value < oldValue) {
// sub-features cannot have more premissive access level
this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value);
this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value);
if (value === 0) {
this.containerRegistryEnabled = false;
this.lfsEnabled = false;
}
} else if (oldValue === 0) {
this.mergeRequestsAccessLevel = value;
this.buildsAccessLevel = value;
this.containerRegistryEnabled = true;
this.lfsEnabled = true;
}
},
},
};
......@@ -92,7 +92,9 @@ export default {
</template>
<template v-else>
<tr>
<td>No {{ header.toLowerCase() }} for this request.</td>
<td>
{{ sprintf(__('No %{header} for this request.'), { header: header.toLowerCase() }) }}
</td>
</tr>
</template>
</table>
......
......@@ -4,13 +4,12 @@ import { glEmojiTag } from '~/emoji';
import detailedMetric from './detailed_metric.vue';
import requestSelector from './request_selector.vue';
import simpleMetric from './simple_metric.vue';
import { s__ } from '~/locale';
export default {
components: {
detailedMetric,
requestSelector,
simpleMetric,
},
props: {
store: {
......@@ -35,15 +34,20 @@ export default {
},
},
detailedMetrics: [
{ metric: 'pg', header: 'SQL queries', details: 'queries', keys: ['sql'] },
{ metric: 'pg', header: s__('PerformanceBar|SQL queries'), details: 'queries', keys: ['sql'] },
{
metric: 'gitaly',
header: 'Gitaly calls',
header: s__('PerformanceBar|Gitaly calls'),
details: 'details',
keys: ['feature', 'request'],
},
{
metric: 'redis',
header: 'Redis calls',
details: 'details',
keys: ['cmd'],
},
],
simpleMetrics: ['redis'],
data() {
return { currentRequestId: '' };
},
......@@ -99,7 +103,8 @@ export default {
class="current-host"
:class="{ canary: currentRequest.details.host.canary }"
>
<span v-html="birdEmoji"></span> {{ currentRequest.details.host.hostname }}
<span v-html="birdEmoji"></span>
{{ currentRequest.details.host.hostname }}
</span>
</div>
<detailed-metric
......@@ -118,16 +123,10 @@ export default {
data-toggle="modal"
data-target="#modal-peek-line-profile"
>
profile
{{ s__('PerformanceBar|profile') }}
</button>
<a v-else :href="profileUrl"> profile </a>
<a v-else :href="profileUrl">{{ s__('PerformanceBar|profile') }}</a>
</div>
<simple-metric
v-for="metric in $options.simpleMetrics"
:key="metric"
:current-request="currentRequest"
:metric="metric"
/>
<div id="peek-view-gc" class="view">
<span v-if="currentRequest.details" class="bold">
<span title="Invoke Time">{{ currentRequest.details.gc.gc_time }}</span
......@@ -139,7 +138,7 @@ export default {
id="peek-view-trace"
class="view"
>
<a :href="currentRequest.details.tracing.tracing_url"> trace </a>
<a :href="currentRequest.details.tracing.tracing_url">{{ s__('PerformanceBar|trace') }}</a>
</div>
<request-selector
v-if="currentRequest"
......
<script>
export default {
props: {
currentRequest: {
type: Object,
required: true,
},
metric: {
type: String,
required: true,
},
},
computed: {
duration() {
return (
this.currentRequest.details[this.metric] &&
this.currentRequest.details[this.metric].duration
);
},
calls() {
return (
this.currentRequest.details[this.metric] && this.currentRequest.details[this.metric].calls
);
},
},
};
</script>
<template>
<div :id="`peek-view-${metric}`" class="view">
<span v-if="currentRequest.details" class="bold"> {{ duration }} / {{ calls }} </span>
{{ metric }}
</div>
</template>
......@@ -2,6 +2,7 @@
import { GlLoadingIcon } from '@gitlab/ui';
import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import eventHub from '../event_hub';
import { __ } from '~/locale';
export default {
name: 'PipelineHeaderSection',
......@@ -54,7 +55,7 @@ export default {
if (this.pipeline.retry_path) {
actions.push({
label: 'Retry',
label: __('Retry'),
path: this.pipeline.retry_path,
cssClass: 'js-retry-button btn btn-inverted-secondary',
type: 'button',
......@@ -64,7 +65,7 @@ export default {
if (this.pipeline.cancel_path) {
actions.push({
label: 'Cancel running',
label: __('Cancel running'),
path: this.pipeline.cancel_path,
cssClass: 'js-btn-cancel-pipeline btn btn-danger',
type: 'button',
......
......@@ -94,9 +94,8 @@ export default {
tabindex="0"
class="js-pipeline-url-autodevops badge badge-info autodevops-badge"
role="button"
>{{ __('Auto DevOps') }}</gl-link
>
Auto DevOps
</gl-link>
<span v-if="pipeline.flags.stuck" class="js-pipeline-url-stuck badge badge-warning">
{{ __('stuck') }}
</span>
......
......@@ -44,6 +44,11 @@ export default {
cancelingPipeline: null,
};
},
watch: {
pipelines() {
this.cancelingPipeline = null;
},
},
created() {
eventHub.$on('openConfirmationModal', this.setModalData);
},
......
......@@ -241,7 +241,11 @@ export default {
return this.cancelingPipeline === this.pipeline.id;
},
},
watch: {
pipeline() {
this.isRetrying = false;
},
},
methods: {
handleCancelClick() {
eventHub.$emit('openConfirmationModal', {
......
......@@ -107,8 +107,8 @@ export default {
}
// Stop polling
this.poll.stop();
// Update the table
return this.getPipelines().then(() => this.poll.restart());
// Restarting the poll also makes an initial request
this.poll.restart();
},
fetchPipelines() {
if (!this.isMakingRequest) {
......@@ -153,7 +153,7 @@ export default {
postAction(endpoint) {
this.service
.postAction(endpoint)
.then(() => this.fetchPipelines())
.then(() => this.updateTable())
.catch(() => Flash(__('An error occurred while making the request.')));
},
},
......
import $ from 'jquery';
import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils';
import { slugifyWithHyphens } from '../lib/utils/text_utility';
import { slugify } from '../lib/utils/text_utility';
import { s__ } from '~/locale';
let hasUserDefinedProjectPath = false;
......@@ -34,7 +34,7 @@ const deriveProjectPathFromUrl = $projectImportUrl => {
};
const onProjectNameChange = ($projectNameInput, $projectPathInput) => {
const slug = slugifyWithHyphens($projectNameInput.val());
const slug = slugify($projectNameInput.val());
$projectPathInput.val(slug);
};
......
......@@ -3,7 +3,7 @@ import Visibility from 'visibilityjs';
import ciIcon from '~/vue_shared/components/ci_icon.vue';
import Poll from '~/lib/utils/poll';
import Flash from '~/flash';
import { s__, sprintf } from '~/locale';
import { __, s__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import { GlLoadingIcon } from '@gitlab/ui';
import CommitPipelineService from '../services/commit_pipeline_service';
......@@ -56,7 +56,7 @@ export default {
},
errorCallback() {
this.ciStatus = {
text: 'not found',
text: __('not found'),
icon: 'status_notfound',
group: 'notfound',
};
......
......@@ -3,22 +3,81 @@ import { mapGetters, mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import store from '../stores';
import CollapsibleContainer from './collapsible_container.vue';
import SvgMessage from './svg_message.vue';
import { s__, sprintf } from '../../locale';
export default {
name: 'RegistryListApp',
components: {
CollapsibleContainer,
GlLoadingIcon,
SvgMessage,
},
props: {
endpoint: {
type: String,
required: true,
},
characterError: {
type: Boolean,
required: false,
default: false,
},
helpPagePath: {
type: String,
required: true,
},
noContainersImage: {
type: String,
required: true,
},
containersErrorImage: {
type: String,
required: true,
},
repositoryUrl: {
type: String,
required: true,
},
},
store,
computed: {
...mapGetters(['isLoading', 'repos']),
dockerConnectionErrorText() {
return sprintf(
s__(`ContainerRegistry|We are having trouble connecting to Docker, which could be due to an
issue with your project name or path. For more information, please review the
%{docLinkStart}Container Registry documentation%{docLinkEnd}.`),
{
docLinkStart: `<a href="${this.helpPagePath}#docker-connection-error">`,
docLinkEnd: '</a>',
},
false,
);
},
introText() {
return sprintf(
s__(`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every
project can have its own space to store its Docker images. Learn more about the
%{docLinkStart}Container Registry%{docLinkEnd}.`),
{
docLinkStart: `<a href="${this.helpPagePath}">`,
docLinkEnd: '</a>',
},
false,
);
},
noContainerImagesText() {
return sprintf(
s__(`ContainerRegistry|With the Container Registry, every project can have its own space to
store its Docker images. Learn more about the %{docLinkStart}Container Registry%{docLinkEnd}.`),
{
docLinkStart: `<a href="${this.helpPagePath}">`,
docLinkEnd: '</a>',
},
false,
);
},
},
created() {
this.setMainEndpoint(this.endpoint);
......@@ -33,20 +92,44 @@ export default {
</script>
<template>
<div>
<gl-loading-icon v-if="isLoading" size="md" />
<svg-message v-if="characterError" id="invalid-characters" :svg-path="containersErrorImage">
<h4>
{{ s__('ContainerRegistry|Docker connection error') }}
</h4>
<p v-html="dockerConnectionErrorText"></p>
</svg-message>
<gl-loading-icon v-else-if="isLoading" size="md" class="prepend-top-16" />
<div v-else-if="!isLoading && !characterError && repos.length">
<h4>{{ s__('ContainerRegistry|Container Registry') }}</h4>
<p v-html="introText"></p>
<collapsible-container v-for="item in repos" :key="item.id" :repo="item" />
</div>
<svg-message
v-else-if="!isLoading && !characterError && !repos.length"
id="no-container-images"
:svg-path="noContainersImage"
>
<h4>
{{ s__('ContainerRegistry|There are no container images stored for this project') }}
</h4>
<p v-html="noContainerImagesText"></p>
<collapsible-container
v-for="item in repos"
v-else-if="!isLoading && repos.length"
:key="item.id"
:repo="item"
/>
<h5>{{ s__('ContainerRegistry|Quick Start') }}</h5>
<p>
{{
s__(
'ContainerRegistry|You can add an image to this registry with the following commands:',
)
}}
</p>
<p v-else-if="!isLoading && !repos.length">
{{
__(`No container images stored for this project.
Add one by following the instructions above.`)
}}
</p>
<pre>
docker build -t {{ repositoryUrl }} .
docker push {{ repositoryUrl }}
</pre>
</svg-message>
</div>
</template>
<script>
export default {
name: 'RegistrySvgMessage',
props: {
id: {
type: String,
required: true,
},
svgPath: {
type: String,
required: true,
},
},
};
</script>
<template>
<div :id="id" class="empty-state container-message mw-70p">
<div class="svg-content">
<img :src="svgPath" class="flex-align-self-center" />
</div>
<slot></slot>
</div>
</template>
......@@ -14,12 +14,22 @@ export default () =>
const { dataset } = document.querySelector(this.$options.el);
return {
endpoint: dataset.endpoint,
characterError: Boolean(dataset.characterError),
helpPagePath: dataset.helpPagePath,
noContainersImage: dataset.noContainersImage,
containersErrorImage: dataset.containersErrorImage,
repositoryUrl: dataset.repositoryUrl,
};
},
render(createElement) {
return createElement('registry-app', {
props: {
endpoint: this.endpoint,
characterError: this.characterError,
helpPagePath: this.helpPagePath,
noContainersImage: this.noContainersImage,
containersErrorImage: this.containersErrorImage,
repositoryUrl: this.repositoryUrl,
},
});
},
......
......@@ -4,7 +4,7 @@ import { GlTooltipDirective, GlLink, GlBadge } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { sprintf } from '../../locale';
import { __, sprintf } from '../../locale';
export default {
name: 'ReleaseBlock',
......@@ -27,13 +27,13 @@ export default {
},
computed: {
releasedTimeAgo() {
return sprintf('released %{time}', {
time: this.timeFormated(this.release.created_at),
return sprintf(__('released %{time}'), {
time: this.timeFormated(this.release.released_at),
});
},
userImageAltDescription() {
return this.author && this.author.username
? sprintf("%{username}'s avatar", { username: this.author.username })
? sprintf(__("%{username}'s avatar"), { username: this.author.username })
: null;
},
commit() {
......@@ -56,8 +56,8 @@ export default {
<div class="card-body">
<h2 class="card-title mt-0">
{{ release.name }}
<gl-badge v-if="release.pre_release" variant="warning" class="align-middle">{{
__('Pre-release')
<gl-badge v-if="release.upcoming_release" variant="warning" class="align-middle">{{
__('Upcoming Release')
}}</gl-badge>
</h2>
......@@ -74,7 +74,7 @@ export default {
<div class="append-right-4">
&bull;
<span v-gl-tooltip.bottom :title="tooltipTitle(release.created_at)">
<span v-gl-tooltip.bottom :title="tooltipTitle(release.released_at)">
{{ releasedTimeAgo }}
</span>
</div>
......
<script>
import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
import { components, componentNames } from '~/reports/components/issue_body';
import { components, componentNames } from 'ee_else_ce/reports/components/issue_body';
export default {
name: 'ReportItem',
......
<script>
import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui';
import { GlTooltipDirective, GlLink, GlButton, GlLoadingIcon } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import Icon from '../../vue_shared/components/icon.vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
import CommitPipelineStatus from '../../projects/tree/components/commit_pipeline_status_component.vue';
import CiIcon from '../../vue_shared/components/ci_icon.vue';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import getRefMixin from '../mixins/get_ref';
......@@ -16,11 +15,11 @@ export default {
Icon,
UserAvatarLink,
TimeagoTooltip,
CommitPipelineStatus,
ClipboardButton,
CiIcon,
GlLink,
GlButton,
GlLoadingIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -39,7 +38,10 @@ export default {
path: this.currentPath.replace(/^\//, ''),
};
},
update: data => data.project.repository.tree.commit,
update: data => data.project.repository.tree.lastCommit,
context: {
isSingleRequest: true,
},
},
},
props: {
......@@ -59,14 +61,14 @@ export default {
computed: {
statusTitle() {
return sprintf(s__('Commits|Commit: %{commitText}'), {
commitText: this.commit.pipeline.detailedStatus.text,
commitText: this.commit.latestPipeline.detailedStatus.text,
});
},
isLoading() {
return this.$apollo.queries.commit.loading;
},
showCommitId() {
return this.commit.id.substr(0, 8);
return this.commit.sha.substr(0, 8);
},
},
methods: {
......@@ -78,68 +80,75 @@ export default {
</script>
<template>
<div v-if="!isLoading" class="info-well d-none d-sm-flex project-last-commit commit p-3">
<user-avatar-link
v-if="commit.author"
:link-href="commit.author.webUrl"
:img-src="commit.author.avatarUrl"
:img-size="40"
class="avatar-cell"
/>
<div class="commit-detail flex-list">
<div class="commit-content qa-commit-content">
<gl-link :href="commit.webUrl" class="commit-row-message item-title">
{{ commit.title }}
</gl-link>
<gl-button
v-if="commit.description"
:class="{ open: showDescription }"
:aria-label="__('Show commit description')"
class="text-expander"
@click="toggleShowDescription"
>
<icon name="ellipsis_h" />
</gl-button>
<div class="committer">
<div class="info-well d-none d-sm-flex project-last-commit commit p-3">
<gl-loading-icon v-if="isLoading" size="md" class="mx-auto" />
<template v-else>
<user-avatar-link
v-if="commit.author"
:link-href="commit.author.webUrl"
:img-src="commit.author.avatarUrl"
:img-size="40"
class="avatar-cell"
/>
<div class="commit-detail flex-list">
<div class="commit-content qa-commit-content">
<gl-link :href="commit.webUrl" class="commit-row-message item-title">
{{ commit.title }}
</gl-link>
<gl-button
v-if="commit.description"
:class="{ open: showDescription }"
:aria-label="__('Show commit description')"
class="text-expander"
@click="toggleShowDescription"
>
<icon name="ellipsis_h" />
</gl-button>
<div class="committer">
<gl-link
v-if="commit.author"
:href="commit.author.webUrl"
class="commit-author-link js-user-link"
>
{{ commit.author.name }}
</gl-link>
authored
<timeago-tooltip :time="commit.authoredDate" tooltip-placement="bottom" />
</div>
<pre
v-if="commit.description"
v-show="showDescription"
class="commit-row-description append-bottom-8"
>
{{ commit.description }}
</pre>
</div>
<div class="commit-actions flex-row">
<gl-link
v-if="commit.author"
:href="commit.author.webUrl"
class="commit-author-link js-user-link"
v-if="commit.latestPipeline"
v-gl-tooltip
:href="commit.latestPipeline.detailedStatus.detailsPath"
:title="statusTitle"
class="js-commit-pipeline"
>
{{ commit.author.name }}
<ci-icon
:status="commit.latestPipeline.detailedStatus"
:size="24"
:aria-label="statusTitle"
/>
</gl-link>
authored
<timeago-tooltip :time="commit.authoredDate" tooltip-placement="bottom" />
</div>
<pre
v-if="commit.description"
v-show="showDescription"
class="commit-row-description append-bottom-8"
>
{{ commit.description }}
</pre>
</div>
<div class="commit-actions flex-row">
<gl-link
v-if="commit.pipeline"
v-gl-tooltip
:href="commit.pipeline.detailedStatus.detailsPath"
:title="statusTitle"
class="js-commit-pipeline"
>
<ci-icon :status="commit.pipeline.detailedStatus" :size="24" :aria-label="statusTitle" />
</gl-link>
<div class="commit-sha-group d-flex">
<div class="label label-monospace monospace">
{{ showCommitId }}
<div class="commit-sha-group d-flex">
<div class="label label-monospace monospace">
{{ showCommitId }}
</div>
<clipboard-button
:text="commit.sha"
:title="__('Copy commit SHA to clipboard')"
tooltip-placement="bottom"
/>
</div>
<clipboard-button
:text="commit.id"
:title="__('Copy commit SHA to clipboard')"
tooltip-placement="bottom"
/>
</div>
</div>
</div>
</template>
</div>
</template>
......@@ -110,9 +110,7 @@ export default {
<component :is="linkComponent" :to="routerLinkTo" :href="url" class="str-truncated">
{{ fullPath }}
</component>
<gl-badge v-if="lfsOid" variant="default" class="label-lfs ml-1">
LFS
</gl-badge>
<gl-badge v-if="lfsOid" variant="default" class="label-lfs ml-1">LFS</gl-badge>
<template v-if="isSubmodule">
@ <gl-link href="#" class="commit-sha">{{ shortSha }}</gl-link>
</template>
......
......@@ -50,23 +50,19 @@ export default function setupVueRepositoryList() {
},
});
const commitEl = document.getElementById('js-last-commit');
if (commitEl) {
// eslint-disable-next-line no-new
new Vue({
el: commitEl,
router,
apolloProvider,
render(h) {
return h(LastCommit, {
props: {
currentPath: this.$route.params.pathMatch,
},
});
},
});
}
// eslint-disable-next-line no-new
new Vue({
el: document.getElementById('js-last-commit'),
router,
apolloProvider,
render(h) {
return h(LastCommit, {
props: {
currentPath: this.$route.params.pathMatch,
},
});
},
});
return new Vue({
el,
......
......@@ -2,8 +2,8 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
project(fullPath: $projectPath) {
repository {
tree(path: $path, ref: $ref) {
commit {
id
lastCommit {
sha
title
message
webUrl
......@@ -13,7 +13,7 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
avatarUrl
webUrl
}
pipeline {
latestPipeline {
detailedStatus {
detailsPath
icon
......
......@@ -4,6 +4,7 @@ import { debounceByAnimationFrame } from '~/lib/utils/common_utils';
import dateFormat from 'dateformat';
import { X_INTERVAL } from '../constants';
import { validateGraphData } from '../utils';
import { __ } from '~/locale';
let debouncedResize;
......@@ -42,7 +43,7 @@ export default {
},
generateSeries() {
return {
name: 'Invocations',
name: __('Invocations'),
type: 'line',
data: this.chartData.requests.map(data => [data.time, data.value]),
symbolSize: 0,
......@@ -124,7 +125,9 @@ export default {
<div class="prometheus-graph">
<div class="prometheus-graph-header">
<h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5>
<div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div>
<div ref="graphWidgets" class="prometheus-graph-widgets">
<slot></slot>
</div>
</div>
<gl-area-chart
ref="areaChart"
......@@ -135,12 +138,8 @@ export default {
:width="width"
:include-legend-avg-max="false"
>
<template slot="tooltipTitle">
{{ tooltipPopoverTitle }}
</template>
<template slot="tooltipContent">
{{ tooltipPopoverContent }}
</template>
<template slot="tooltipTitle">{{ tooltipPopoverTitle }}</template>
<template slot="tooltipContent">{{ tooltipPopoverContent }}</template>
</gl-area-chart>
</div>
</template>
......@@ -89,7 +89,9 @@ export default {
}}
</p>
</div>
<div v-else><p>No pods loaded at this time.</p></div>
<div v-else>
<p>{{ s__('ServerlessDetails|No pods loaded at this time.') }}</p>
</div>
<area-chart v-if="hasPrometheusData" :graph-data="graphData" :container-width="elWidth" />
<missing-prometheus
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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