diff options
125 files changed, 3330 insertions, 536 deletions
diff --git a/.eslintignore b/.eslintignore index c742b08c005..1605e483e9e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,3 +7,4 @@ /vendor/ karma.config.js webpack.config.js +/app/assets/javascripts/locale/**/*.js diff --git a/.gitignore b/.gitignore index bb818213de1..0fb97ffb98e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ *.log *.swp +*.mo +*.edit.po .DS_Store .bundle .chef @@ -54,3 +56,4 @@ eslint-report.html /shared/* /.gitlab_workhorse_secret /webpack-report/ +/locale/**/LC_MESSAGES diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 44620d390ad..588f255eff8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -23,6 +23,7 @@ before_script: - source scripts/prepare_build.sh stages: +- build - prepare - test - post-test @@ -137,6 +138,28 @@ stages: <<: *only-master-and-ee-or-mysql <<: *except-docs +# Trigger a package build on omnibus-gitlab repository + +build-package: + services: [] + variables: + SETUP_DB: "false" + USE_BUNDLE_INSTALL: "false" + stage: build + when: manual + script: + # If no branch in omnibus is specified, trigger pipeline against master + - if [ -z "$OMNIBUS_BRANCH" ] ; then export OMNIBUS_BRANCH=master ;fi + - echo "token=${BUILD_TRIGGER_TOKEN}" > version_details + - echo "ref=${OMNIBUS_BRANCH}" >> version_details + - echo "variables[ALTERNATIVE_SOURCES]=true" >> version_details + - echo "variables[GITLAB_VERSION]=${CI_COMMIT_SHA}" >> version_details + # Collect version details of all components + - for f in *_VERSION; do echo "variables[$f]=$(cat $f)" >> version_details; done + # Trigger the API and pass values collected above as parameters to it + - cat version_details | tr '\n' '&' | curl -X POST https://gitlab.com/api/v4/projects/20699/trigger/pipeline --data-binary @- + - rm version_details + # Prepare and merge knapsack tests knapsack: <<: *knapsack-state @@ -256,6 +256,11 @@ gem 'sentry-raven', '~> 2.4.0' gem 'premailer-rails', '~> 1.9.0' +# I18n +gem 'gettext_i18n_rails', '~> 1.8.0' +gem 'gettext_i18n_rails_js', '~> 1.2.0' +gem 'gettext', '~> 3.2.2', require: false, group: :development + # Metrics group :metrics do gem 'allocations', '~> 1.0', require: false, platform: :mri diff --git a/Gemfile.lock b/Gemfile.lock index b822a325861..c7e3f9935da 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -198,6 +198,7 @@ GEM faraday_middleware-multi_json (0.0.6) faraday_middleware multi_json + fast_gettext (1.4.0) ffaker (2.4.0) ffi (1.9.10) flay (2.8.1) @@ -251,6 +252,16 @@ GEM gemojione (3.0.1) json get_process_mem (0.2.0) + gettext (3.2.2) + locale (>= 2.0.5) + text (>= 1.3.0) + gettext_i18n_rails (1.8.0) + fast_gettext (>= 0.9.0) + gettext_i18n_rails_js (1.2.0) + gettext (>= 3.0.2) + gettext_i18n_rails (>= 0.7.1) + po_to_json (>= 1.0.0) + rails (>= 3.2.0) gherkin-ruby (0.3.2) gitaly (0.5.0) google-protobuf (~> 3.1) @@ -422,6 +433,7 @@ GEM licensee (8.7.0) rugged (~> 0.24) little-plugger (1.1.4) + locale (2.1.2) logging (2.1.0) little-plugger (~> 1.1) multi_json (~> 1.10) @@ -525,6 +537,8 @@ GEM ast (~> 2.2) path_expander (1.0.1) pg (0.18.4) + po_to_json (1.0.1) + json (>= 1.6.0) poltergeist (1.9.0) capybara (~> 2.1) cliver (~> 0.3.1) @@ -777,6 +791,7 @@ GEM temple (0.7.7) test_after_commit (1.1.0) activerecord (>= 3.2) + text (1.3.1) thin (1.7.0) daemons (~> 1.0, >= 1.0.9) eventmachine (~> 1.0, >= 1.0.4) @@ -904,6 +919,9 @@ DEPENDENCIES fuubar (~> 2.0.0) gemnasium-gitlab-service (~> 0.2) gemojione (~> 3.0) + gettext (~> 3.2.2) + gettext_i18n_rails (~> 1.8.0) + gettext_i18n_rails_js (~> 1.2.0) gitaly (~> 0.5.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) diff --git a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js index abe48572347..8d3d34f836f 100644 --- a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js +++ b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js @@ -9,9 +9,9 @@ export default { <span v-if="count === 50" class="events-info pull-right"> <i class="fa fa-warning has-tooltip" aria-hidden="true" - title="Limited to showing 50 events at most" + :title="n__('Limited to showing %d event at most', 'Limited to showing %d events at most', 50)" data-placement="top"></i> - Showing 50 events + {{ n__('Showing %d event', 'Showing %d events', 50) }} </span> `, }; diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js index 80bd2df6f42..0d9ad197abf 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js @@ -28,11 +28,11 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({ <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> · <span> - Opened + {{ __('OpenedNDaysAgo|Opened') }} <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> </span> <span> - by + {{ __('ByAuthor|by') }} <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> </span> </div> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js index 20a43798fbe..ad285874643 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js @@ -28,11 +28,11 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({ <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> · <span> - Opened + {{ __('OpenedNDaysAgo|Opened') }} <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a> </span> <span> - by + {{ __('ByAuthor|by') }} <a :href="issue.author.webUrl" class="issue-author-link"> {{ issue.author.name }} </a> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js index f33cac3da82..222084deee9 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js @@ -31,10 +31,10 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({ </a> </h5> <span> - First + {{ __('FirstPushedBy|First') }} <span class="commit-icon">${iconCommit}</span> <a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a> - pushed by + {{ __('FirstPushedBy|pushed by') }} <a :href="commit.author.webUrl" class="commit-author-link"> {{ commit.author.name }} </a> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js index 657f5385374..a14ebc3ece9 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js @@ -28,11 +28,11 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({ <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> · <span> - Opened + {{ __('OpenedNDaysAgo|Opened') }} <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a> </span> <span> - by + {{ __('ByAuthor|by') }} <a :href="issue.author.webUrl" class="issue-author-link"> {{ issue.author.name }} </a> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js index 8a801300647..1a5bf9bc0b5 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js @@ -28,11 +28,11 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({ <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> · <span> - Opened + {{ __('OpenedNDaysAgo|Opened') }} <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> </span> <span> - by + {{ __('ByAuthor|by') }} <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> </span> <template v-if="mergeRequest.state === 'closed'"> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js index 4a286379588..b1e9362434f 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js @@ -32,7 +32,7 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({ </h5> <span> <a :href="build.url" class="build-date">{{ build.date }}</a> - by + {{ __('ByAuthor|by') }} <a :href="build.author.webUrl" class="issue-author-link"> {{ build.author.name }} </a> diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.js b/app/assets/javascripts/cycle_analytics/components/total_time_component.js index 77edcb76273..d5e6167b2a8 100644 --- a/app/assets/javascripts/cycle_analytics/components/total_time_component.js +++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.js @@ -12,10 +12,10 @@ global.cycleAnalytics.TotalTimeComponent = Vue.extend({ template: ` <span class="total-time"> <template v-if="Object.keys(time).length"> - <template v-if="time.days">{{ time.days }} <span>{{ time.days === 1 ? 'day' : 'days' }}</span></template> - <template v-if="time.hours">{{ time.hours }} <span>hr</span></template> - <template v-if="time.mins && !time.days">{{ time.mins }} <span>mins</span></template> - <template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>s</span></template> + <template v-if="time.days">{{ time.days }} <span>{{ n__('day', 'days', time.days) }}</span></template> + <template v-if="time.hours">{{ time.hours }} <span>{{ n__('Time|hr', 'Time|hrs', time.hours) }}</span></template> + <template v-if="time.mins && !time.days">{{ time.mins }} <span>{{ n__('Time|min', 'Time|mins', time.mins) }}</span></template> + <template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template> </template> <template v-else> -- diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 48cab437e02..c8e53cb554e 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import Cookies from 'js-cookie'; +import Translate from '../vue_shared/translate'; import LimitWarningComponent from './components/limit_warning_component'; require('./components/stage_code_component'); @@ -16,6 +17,8 @@ require('./cycle_analytics_service'); require('./cycle_analytics_store'); require('./default_event_objects'); +Vue.use(Translate); + $(() => { const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; const cycleAnalyticsEl = document.querySelector('#cycle-analytics'); diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js index 681d6eef565..6504d7db2f2 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js @@ -30,7 +30,7 @@ class CycleAnalyticsService { startDate, } = options; - return $.get(`${this.requestPath}/events/${stage.title.toLowerCase()}.json`, { + return $.get(`${this.requestPath}/events/${stage.name}.json`, { cycle_analytics: { start_date: startDate, }, diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js index 6536a8fd7fa..50bd394e90e 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js @@ -1,4 +1,5 @@ /* eslint-disable no-param-reassign */ +import { __ } from '../locale'; require('../lib/utils/text_utility'); const DEFAULT_EVENT_OBJECTS = require('./default_event_objects'); @@ -7,13 +8,13 @@ const global = window.gl || (window.gl = {}); global.cycleAnalytics = global.cycleAnalytics || {}; const EMPTY_STAGE_TEXTS = { - issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.', - plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.', - code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.', - test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.', - review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.', - staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.', - production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.', + issue: __('The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.'), + plan: __('The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.'), + code: __('The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.'), + test: __('The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.'), + review: __('The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.'), + staging: __('The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.'), + production: __('The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.'), }; global.cycleAnalytics.CycleAnalyticsStore = { @@ -38,7 +39,7 @@ global.cycleAnalytics.CycleAnalyticsStore = { }); newData.stages.forEach((item) => { - const stageSlug = gl.text.dasherize(item.title.toLowerCase()); + const stageSlug = gl.text.dasherize(item.name.toLowerCase()); item.active = false; item.isUserAllowed = data.permissions[stageSlug]; item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug]; diff --git a/app/assets/javascripts/locale/de/app.js b/app/assets/javascripts/locale/de/app.js new file mode 100644 index 00000000000..e96090da80e --- /dev/null +++ b/app/assets/javascripts/locale/de/app.js @@ -0,0 +1 @@ +var locales = locales || {}; locales['de'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-04-12 22:37-0500","Last-Translator":"FULL NAME <EMAIL@ADDRESS>","Language-Team":"German","Language":"de","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","lang":"de","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"ByAuthor|by":[""],"Commit":["",""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":[""],"CycleAnalyticsStage|Code":[""],"CycleAnalyticsStage|Issue":[""],"CycleAnalyticsStage|Plan":[""],"CycleAnalyticsStage|Production":[""],"CycleAnalyticsStage|Review":[""],"CycleAnalyticsStage|Staging":[""],"CycleAnalyticsStage|Test":[""],"Deploy":["",""],"FirstPushedBy|First":[""],"FirstPushedBy|pushed by":[""],"From issue creation until deploy to production":[""],"From merge request merge until deploy to production":[""],"Introducing Cycle Analytics":[""],"Last %d day":["",""],"Limited to showing %d event at most":["",""],"Median":[""],"New Issue":["",""],"Not available":[""],"Not enough data":[""],"OpenedNDaysAgo|Opened":[""],"Pipeline Health":[""],"ProjectLifecycle|Stage":[""],"Read more":[""],"Related Commits":[""],"Related Deployed Jobs":[""],"Related Issues":[""],"Related Jobs":[""],"Related Merge Requests":[""],"Related Merged Requests":[""],"Showing %d event":["",""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":[""],"The collection of events added to the data gathered for that stage.":[""],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":[""],"The phase of the development lifecycle.":[""],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":[""],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":[""],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":[""],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":[""],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":[""],"The time taken by each data entry gathered by that stage.":[""],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":[""],"Time before an issue gets scheduled":[""],"Time before an issue starts implementation":[""],"Time between merge request creation and merge/close":[""],"Time until first merge request":[""],"Time|hr":["",""],"Time|min":["",""],"Time|s":[""],"Total Time":[""],"Total test time for all commits/merges":[""],"Want to see the data? Please ask an administrator for access.":[""],"We don't have enough data to show this stage.":[""],"You need permission.":[""],"day":["",""]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/en/app.js b/app/assets/javascripts/locale/en/app.js new file mode 100644 index 00000000000..ade9b667b3c --- /dev/null +++ b/app/assets/javascripts/locale/en/app.js @@ -0,0 +1 @@ +var locales = locales || {}; locales['en'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-04-12 22:36-0500","Last-Translator":"FULL NAME <EMAIL@ADDRESS>","Language-Team":"English","Language":"en","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","lang":"en","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"ByAuthor|by":[""],"Commit":["",""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":[""],"CycleAnalyticsStage|Code":[""],"CycleAnalyticsStage|Issue":[""],"CycleAnalyticsStage|Plan":[""],"CycleAnalyticsStage|Production":[""],"CycleAnalyticsStage|Review":[""],"CycleAnalyticsStage|Staging":[""],"CycleAnalyticsStage|Test":[""],"Deploy":["",""],"FirstPushedBy|First":[""],"FirstPushedBy|pushed by":[""],"From issue creation until deploy to production":[""],"From merge request merge until deploy to production":[""],"Introducing Cycle Analytics":[""],"Last %d day":["",""],"Limited to showing %d event at most":["",""],"Median":[""],"New Issue":["",""],"Not available":[""],"Not enough data":[""],"OpenedNDaysAgo|Opened":[""],"Pipeline Health":[""],"ProjectLifecycle|Stage":[""],"Read more":[""],"Related Commits":[""],"Related Deployed Jobs":[""],"Related Issues":[""],"Related Jobs":[""],"Related Merge Requests":[""],"Related Merged Requests":[""],"Showing %d event":["",""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":[""],"The collection of events added to the data gathered for that stage.":[""],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":[""],"The phase of the development lifecycle.":[""],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":[""],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":[""],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":[""],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":[""],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":[""],"The time taken by each data entry gathered by that stage.":[""],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":[""],"Time before an issue gets scheduled":[""],"Time before an issue starts implementation":[""],"Time between merge request creation and merge/close":[""],"Time until first merge request":[""],"Time|hr":["",""],"Time|min":["",""],"Time|s":[""],"Total Time":[""],"Total test time for all commits/merges":[""],"Want to see the data? Please ask an administrator for access.":[""],"We don't have enough data to show this stage.":[""],"You need permission.":[""],"day":["",""]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/es/app.js b/app/assets/javascripts/locale/es/app.js new file mode 100644 index 00000000000..3dafa21f235 --- /dev/null +++ b/app/assets/javascripts/locale/es/app.js @@ -0,0 +1 @@ +var locales = locales || {}; locales['es'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-05-04 19:24-0500","Language-Team":"Spanish","Language":"es","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","Last-Translator":"","X-Generator":"Poedit 2.0.1","lang":"es","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"ByAuthor|by":["por"],"Commit":["Cambio","Cambios"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["Cycle Analytics ofrece una visión general de cuánto tiempo tarda en pasar de idea a producción en su proyecto."],"CycleAnalyticsStage|Code":["Código"],"CycleAnalyticsStage|Issue":["Incidencia"],"CycleAnalyticsStage|Plan":["Planificación"],"CycleAnalyticsStage|Production":["Producción"],"CycleAnalyticsStage|Review":["Revisión"],"CycleAnalyticsStage|Staging":["Puesta en escena"],"CycleAnalyticsStage|Test":["Pruebas"],"Deploy":["Despliegue","Despliegues"],"FirstPushedBy|First":["Primer"],"FirstPushedBy|pushed by":["enviado por"],"From issue creation until deploy to production":["Desde la creación de la incidencia hasta el despliegue a producción"],"From merge request merge until deploy to production":["Desde la integración de la solicitud de fusión hasta el despliegue a producción"],"Introducing Cycle Analytics":["Introducción a Cycle Analytics"],"Last %d day":["Último %d dÃa","Últimos %d dÃas"],"Limited to showing %d event at most":["Limitado a mostrar máximo %d evento","Limitado a mostrar máximo %d eventos"],"Median":["Mediana"],"New Issue":["Nueva incidencia","Nuevas incidencias"],"Not available":["No disponible"],"Not enough data":["No hay suficientes datos"],"OpenedNDaysAgo|Opened":["Abierto"],"Pipeline Health":["Estado del Pipeline"],"ProjectLifecycle|Stage":["Etapa"],"Read more":["Leer más"],"Related Commits":["Cambios Relacionados"],"Related Deployed Jobs":["Trabajos Desplegados Relacionados"],"Related Issues":["Incidencias Relacionadas"],"Related Jobs":["Trabajos Relacionados"],"Related Merge Requests":["Solicitudes de fusión Relacionadas"],"Related Merged Requests":["Solicitudes de fusión Relacionadas"],"Showing %d event":["Mostrando %d evento","Mostrando %d eventos"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["La etapa de codificación muestra el tiempo desde el primer cambio hasta la creación de la solicitud de fusión. Los datos serán automáticamente incorporados aquà una vez creada tu primera solicitud de fusión."],"The collection of events added to the data gathered for that stage.":["La colección de eventos agregados a los datos recopilados para esa etapa."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["La etapa de incidencia muestra el tiempo que toma desde la creación de un tema hasta asignar el tema a un hito, o añadir el tema a una lista en el panel de temas. Empieza a crear temas para ver los datos de esta etapa."],"The phase of the development lifecycle.":["La etapa del ciclo de vida de desarrollo."],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["La etapa de planificación muestra el tiempo desde el paso anterior hasta el envÃo de tu primera confirmación. Este tiempo se añadirá automáticamente una vez que usted envÃe el primer cambio."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["La etapa de producción muestra el tiempo total que tarda entre la creación de una incidencia y el despliegue del código a producción. Los datos se añadirán automáticamente una vez haya finalizado por completo el ciclo de idea a producción."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["La etapa de revisión muestra el tiempo desde la creación de la solicitud de fusión hasta que los cambios se fusionaron. Los datos se añadirán automáticamente después de fusionar su primera solicitud de fusión."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["La etapa de puesta en escena muestra el tiempo entre la fusión y el despliegue de código en el entorno de producción. Los datos se añadirán automáticamente una vez que se despliega a producción por primera vez."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["La etapa de pruebas muestra el tiempo que GitLab CI toma para ejecutar cada pipeline para la solicitud de fusión relacionada. Los datos se añadirán automáticamente luego de que el primer pipeline termine de ejecutarse."],"The time taken by each data entry gathered by that stage.":["El tiempo utilizado por cada entrada de datos obtenido por esa etapa."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["El valor en el punto medio de una serie de valores observados. Por ejemplo, entre 3, 5, 9, la mediana es 5. Entre 3, 5, 7, 8, la mediana es (5 + 7) / 2 = 6."],"Time before an issue gets scheduled":["Tiempo antes de que una incidencia sea programada"],"Time before an issue starts implementation":["Tiempo antes de que empieze la implementación de una incidencia"],"Time between merge request creation and merge/close":["Tiempo entre la creación de la solicitud de fusión y la integración o cierre de ésta"],"Time until first merge request":["Tiempo hasta la primera solicitud de fusión"],"Time|hr":["hr","hrs"],"Time|min":["min","mins"],"Time|s":["s"],"Total Time":["Tiempo Total"],"Total test time for all commits/merges":["Tiempo total de pruebas para todos los cambios o integraciones"],"Want to see the data? Please ask an administrator for access.":["¿Quieres ver los datos? Por favor pide acceso al administrador."],"We don't have enough data to show this stage.":["No hay suficientes datos para mostrar en esta etapa."],"You need permission.":["Necesitas permisos."],"day":["dÃa","dÃas"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js new file mode 100644 index 00000000000..7ba676d6d20 --- /dev/null +++ b/app/assets/javascripts/locale/index.js @@ -0,0 +1,70 @@ +import Jed from 'jed'; + +/** + This is required to require all the translation folders in the current directory + this saves us having to do this manually & keep up to date with new languages +**/ +function requireAll(requireContext) { return requireContext.keys().map(requireContext); } + +const allLocales = requireAll(require.context('./', true, /^(?!.*(?:index.js$)).*\.js$/)); +const locales = allLocales.reduce((d, obj) => { + const data = d; + const localeKey = Object.keys(obj)[0]; + + data[localeKey] = obj[localeKey]; + + return data; +}, {}); + +let lang = document.querySelector('html').getAttribute('lang') || 'en'; +lang = lang.replace(/-/g, '_'); + +const locale = new Jed(locales[lang]); + +/** + Translates `text` + + @param text The text to be translated + @returns {String} The translated text +**/ +const gettext = locale.gettext.bind(locale); + +/** + Translate the text with a number + if the number is more than 1 it will use the `pluralText` translation. + This method allows for contexts, see below re. contexts + + @param text Singular text to translate (eg. '%d day') + @param pluralText Plural text to translate (eg. '%d days') + @param count Number to decide which translation to use (eg. 2) + @returns {String} Translated text with the number replaced (eg. '2 days') +**/ +const ngettext = (text, pluralText, count) => { + const translated = locale.ngettext(text, pluralText, count).replace(/%d/g, count).split('|'); + + return translated[translated.length - 1]; +}; + +/** + Translate context based text + Either pass in the context translation like `Context|Text to translate` + or allow for dynamic text by doing passing in the context first & then the text to translate + + @param keyOrContext Can be either the key to translate including the context + (eg. 'Context|Text') or just the context for the translation + (eg. 'Context') + @param key Is the dynamic variable you want to be translated + @returns {String} Translated context based text +**/ +const pgettext = (keyOrContext, key) => { + const normalizedKey = key ? `${keyOrContext}|${key}` : keyOrContext; + const translated = gettext(normalizedKey).split('|'); + + return translated[translated.length - 1]; +}; + +export { lang }; +export { gettext as __ }; +export { ngettext as n__ }; +export { pgettext as s__ }; +export default locale; diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js index 42ecf0d6cb2..6f6ae9bde92 100644 --- a/app/assets/javascripts/merge_request_widget.js +++ b/app/assets/javascripts/merge_request_widget.js @@ -291,7 +291,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; MergeRequestWidget.prototype.updateCommitUrls = function(id) { const commitsUrl = this.opts.commits_path; - $('.js-commit-link').text(`#${id}`).attr('href', [commitsUrl, id].join('/')); + $('.js-commit-link').text(id).attr('href', [commitsUrl, id].join('/')); }; MergeRequestWidget.prototype.initMiniPipelineGraph = function() { diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js index 5828f460a23..67046d52a65 100644 --- a/app/assets/javascripts/new_branch_form.js +++ b/app/assets/javascripts/new_branch_form.js @@ -34,6 +34,7 @@ filterByText: true, remote: false, fieldName: $branchSelect.data('field-name'), + filterInput: 'input[type="search"]', selectable: true, isSelectable: function(branch, $el) { return !$el.hasClass('is-active'); @@ -50,6 +51,21 @@ } } }); + + const $dropdownContainer = $branchSelect.closest('.dropdown'); + const $fieldInput = $(`input[name="${$branchSelect.data('field-name')}"]`, $dropdownContainer); + const $filterInput = $('input[type="search"]', $dropdownContainer); + + $filterInput.on('keyup', (e) => { + const keyCode = e.keyCode || e.which; + if (keyCode !== 13) return; + + const text = $filterInput.val(); + $fieldInput.val(text); + $('.dropdown-toggle-text', $branchSelect).text(text); + + $dropdownContainer.removeClass('open'); + }); }; NewBranchForm.prototype.setupRestrictions = function() { diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js index 07eea98e737..4a3df2fd465 100644 --- a/app/assets/javascripts/preview_markdown.js +++ b/app/assets/javascripts/preview_markdown.js @@ -2,8 +2,9 @@ // MarkdownPreview // -// Handles toggling the "Write" and "Preview" tab clicks, rendering the preview, -// and showing a warning when more than `x` users are referenced. +// Handles toggling the "Write" and "Preview" tab clicks, rendering the preview +// (including the explanation of slash commands), and showing a warning when +// more than `x` users are referenced. // (function () { var lastTextareaPreviewed; @@ -17,32 +18,45 @@ // Minimum number of users referenced before triggering a warning MarkdownPreview.prototype.referenceThreshold = 10; + MarkdownPreview.prototype.emptyMessage = 'Nothing to preview.'; MarkdownPreview.prototype.ajaxCache = {}; MarkdownPreview.prototype.showPreview = function ($form) { var mdText; var preview = $form.find('.js-md-preview'); + var url = preview.data('url'); if (preview.hasClass('md-preview-loading')) { return; } mdText = $form.find('textarea.markdown-area').val(); if (mdText.trim().length === 0) { - preview.text('Nothing to preview.'); + preview.text(this.emptyMessage); this.hideReferencedUsers($form); } else { preview.addClass('md-preview-loading').text('Loading...'); - this.fetchMarkdownPreview(mdText, (function (response) { - preview.removeClass('md-preview-loading').html(response.body); + this.fetchMarkdownPreview(mdText, url, (function (response) { + var body; + if (response.body.length > 0) { + body = response.body; + } else { + body = this.emptyMessage; + } + + preview.removeClass('md-preview-loading').html(body); preview.renderGFM(); this.renderReferencedUsers(response.references.users, $form); + + if (response.references.commands) { + this.renderReferencedCommands(response.references.commands, $form); + } }).bind(this)); } }; - MarkdownPreview.prototype.fetchMarkdownPreview = function (text, success) { - if (!window.preview_markdown_path) { + MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) { + if (!url) { return; } if (text === this.ajaxCache.text) { @@ -51,7 +65,7 @@ } $.ajax({ type: 'POST', - url: window.preview_markdown_path, + url: url, data: { text: text }, @@ -83,6 +97,22 @@ } }; + MarkdownPreview.prototype.hideReferencedCommands = function ($form) { + $form.find('.referenced-commands').hide(); + }; + + MarkdownPreview.prototype.renderReferencedCommands = function (commands, $form) { + var referencedCommands; + referencedCommands = $form.find('.referenced-commands'); + if (commands.length > 0) { + referencedCommands.html(commands); + referencedCommands.show(); + } else { + referencedCommands.html(''); + referencedCommands.hide(); + } + }; + return MarkdownPreview; }()); @@ -137,6 +167,8 @@ $form.find('.md-write-holder').show(); $form.find('textarea.markdown-area').focus(); $form.find('.md-preview-holder').hide(); + + markdownPreview.hideReferencedCommands($form); }); $(document).on('markdown-preview:toggle', function (e, keyboardEvent) { diff --git a/app/assets/javascripts/vue_shared/translate.js b/app/assets/javascripts/vue_shared/translate.js new file mode 100644 index 00000000000..f83c4b00761 --- /dev/null +++ b/app/assets/javascripts/vue_shared/translate.js @@ -0,0 +1,42 @@ +import { + __, + n__, + s__, +} from '../locale'; + +export default (Vue) => { + Vue.mixin({ + methods: { + /** + Translates `text` + + @param text The text to be translated + @returns {String} The translated text + **/ + __, + /** + Translate the text with a number + if the number is more than 1 it will use the `pluralText` translation. + This method allows for contexts, see below re. contexts + + @param text Singular text to translate (eg. '%d day') + @param pluralText Plural text to translate (eg. '%d days') + @param count Number to decide which translation to use (eg. 2) + @returns {String} Translated text with the number replaced (eg. '2 days') + **/ + n__, + /** + Translate context based text + Either pass in the context translation like `Context|Text to translate` + or allow for dynamic text by doing passing in the context first & then the text to translate + + @param keyOrContext Can be either the key to translate including the context + (eg. 'Context|Text') or just the context for the translation + (eg. 'Context') + @param key Is the dynamic variable you want to be translated + @returns {String} Translated context based text + **/ + s__, + }, + }); +}; diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 403724cd68a..52bbb753af3 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -175,7 +175,7 @@ } .stage-nav-item { - display: block; + display: flex; line-height: 65px; border-top: 1px solid transparent; border-bottom: 1px solid transparent; @@ -209,14 +209,10 @@ } .stage-nav-item-cell { - float: left; - - &.stage-name { - width: 65%; - } - &.stage-median { - width: 35%; + margin-left: auto; + margin-right: $gl-padding; + min-width: calc(35% - #{$gl-padding}); } } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index e48f0963ef4..d2c13da6917 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -21,6 +21,8 @@ class ApplicationController < ActionController::Base before_action :configure_permitted_parameters, if: :devise_controller? before_action :require_email, unless: :devise_controller? + around_action :set_locale + protect_from_forgery with: :exception helper_method :can?, :current_application_settings @@ -269,4 +271,12 @@ class ApplicationController < ActionController::Base def u2f_app_id request.base_url end + + def set_locale + Gitlab::I18n.set_locale(current_user) + + yield + ensure + Gitlab::I18n.reset_locale + end end diff --git a/app/controllers/concerns/markdown_preview.rb b/app/controllers/concerns/markdown_preview.rb deleted file mode 100644 index 40eff267348..00000000000 --- a/app/controllers/concerns/markdown_preview.rb +++ /dev/null @@ -1,19 +0,0 @@ -module MarkdownPreview - private - - def render_markdown_preview(text, markdown_context = {}) - render json: { - body: view_context.markdown(text, markdown_context), - references: { - users: preview_referenced_users(text) - } - } - end - - def preview_referenced_users(text) - extractor = Gitlab::ReferenceExtractor.new(@project, current_user) - extractor.analyze(text, author: current_user) - - extractor.users.map(&:username) - end -end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 58d50ad647b..2a8c8ca4bad 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -67,7 +67,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController def omniauth_error @provider = params[:provider] @error = params[:error] - render 'errors/omniauth_error', layout: "errors", status: 422 + render 'errors/omniauth_error', layout: "oauth_error", status: 422 end def cas3 diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 987b95e89b9..57e23cea00e 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -85,7 +85,8 @@ class ProfilesController < Profiles::ApplicationController :twitter, :username, :website_url, - :organization + :organization, + :preferred_language ) end end diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 96125684da0..887d18dbec3 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -1,6 +1,4 @@ class Projects::WikisController < Projects::ApplicationController - include MarkdownPreview - before_action :authorize_read_wiki! before_action :authorize_create_wiki!, only: [:edit, :create, :history] before_action :authorize_admin_wiki!, only: :destroy @@ -97,9 +95,14 @@ class Projects::WikisController < Projects::ApplicationController end def preview_markdown - context = { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] } - - render_markdown_preview(params[:text], context) + result = PreviewMarkdownService.new(@project, current_user, params).execute + + render json: { + body: view_context.markdown(result[:text], pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id]), + references: { + users: result[:users] + } + } end private diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 9f6ee4826e6..69310b26e76 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,7 +1,6 @@ class ProjectsController < Projects::ApplicationController include IssuableCollections include ExtractsPath - include MarkdownPreview before_action :authenticate_user!, except: [:index, :show, :activity, :refs] before_action :project, except: [:index, :new, :create] @@ -240,7 +239,15 @@ class ProjectsController < Projects::ApplicationController end def preview_markdown - render_markdown_preview(params[:text]) + result = PreviewMarkdownService.new(@project, current_user, params).execute + + render json: { + body: view_context.markdown(result[:text]), + references: { + users: result[:users], + commands: view_context.markdown(result[:commands]) + } + } end private diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index da1ae9a34d9..afed27a41d1 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -3,7 +3,6 @@ class SnippetsController < ApplicationController include ToggleAwardEmoji include SpammableActions include SnippetsActions - include MarkdownPreview include RendersBlob before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] @@ -90,7 +89,14 @@ class SnippetsController < ApplicationController end def preview_markdown - render_markdown_preview(params[:text], skip_project_check: true) + result = PreviewMarkdownService.new(@project, current_user, params).execute + + render json: { + body: view_context.markdown(result[:text], skip_project_check: true), + references: { + users: result[:users] + } + } end protected diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 1336c676134..88f9a691a17 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -122,6 +122,10 @@ module GitlabRoutingHelper namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args) end + def preview_markdown_path(project, *args) + preview_markdown_namespace_project_path(project.namespace, project, *args) + end + def toggle_subscription_path(entity, *args) if entity.is_a?(Issue) toggle_subscription_namespace_project_issue_path(entity.project.namespace, entity.project, entity) diff --git a/app/models/event.rb b/app/models/event.rb index b780c1faf81..e6fad46077a 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -30,6 +30,7 @@ class Event < ActiveRecord::Base # Callbacks after_create :reset_project_activity + after_create :set_last_repository_updated_at, if: :push? # Scopes scope :recent, -> { reorder(id: :desc) } @@ -357,4 +358,9 @@ class Event < ActiveRecord::Base def recent_update? project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago end + + def set_last_repository_updated_at + Project.unscoped.where(id: project_id). + update_all(last_repository_updated_at: created_at) + end end diff --git a/app/models/project.rb b/app/models/project.rb index 025db89ebfd..edbca3b537b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -53,6 +53,11 @@ class Project < ActiveRecord::Base update_column(:last_activity_at, self.created_at) end + after_create :set_last_repository_updated_at + def set_last_repository_updated_at + update_column(:last_repository_updated_at, self.created_at) + end + after_destroy :remove_pages # update visibility_level of forks diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 70eef359cdd..189c106b70b 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -183,6 +183,6 @@ class ProjectWiki end def update_project_activity - @project.touch(:last_activity_at) + @project.touch(:last_activity_at, :last_repository_updated_at) end end diff --git a/app/models/user.rb b/app/models/user.rb index 2b7ebe6c1a7..43c5fdc038d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -23,6 +23,7 @@ class User < ActiveRecord::Base default_value_for :hide_no_password, false default_value_for :project_view, :files default_value_for :notified_of_own_activity, false + default_value_for :preferred_language, I18n.default_locale attr_encrypted :otp_secret, key: Gitlab::Application.secrets.otp_key_base, diff --git a/app/serializers/analytics_stage_entity.rb b/app/serializers/analytics_stage_entity.rb index 69bf693de8d..564612202b5 100644 --- a/app/serializers/analytics_stage_entity.rb +++ b/app/serializers/analytics_stage_entity.rb @@ -2,6 +2,7 @@ class AnalyticsStageEntity < Grape::Entity include EntityDateHelper expose :title + expose :name expose :legend expose :description diff --git a/app/serializers/analytics_summary_entity.rb b/app/serializers/analytics_summary_entity.rb index 91803ec07f5..9c37afd53e1 100644 --- a/app/serializers/analytics_summary_entity.rb +++ b/app/serializers/analytics_summary_entity.rb @@ -1,7 +1,4 @@ class AnalyticsSummaryEntity < Grape::Entity expose :value, safe: true - - expose :title do |object| - object.title.pluralize(object.value) - end + expose :title end diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb new file mode 100644 index 00000000000..10d45bbf73c --- /dev/null +++ b/app/services/preview_markdown_service.rb @@ -0,0 +1,45 @@ +class PreviewMarkdownService < BaseService + def execute + text, commands = explain_slash_commands(params[:text]) + users = find_user_references(text) + + success( + text: text, + users: users, + commands: commands.join(' ') + ) + end + + private + + def explain_slash_commands(text) + return text, [] unless %w(Issue MergeRequest).include?(commands_target_type) + + slash_commands_service = SlashCommands::InterpretService.new(project, current_user) + slash_commands_service.explain(text, find_commands_target) + end + + def find_user_references(text) + extractor = Gitlab::ReferenceExtractor.new(project, current_user) + extractor.analyze(text, author: current_user) + extractor.users.map(&:username) + end + + def find_commands_target + if commands_target_id.present? + finder = commands_target_type == 'Issue' ? IssuesFinder : MergeRequestsFinder + finder.new(current_user, project_id: project.id).find(commands_target_id) + else + collection = commands_target_type == 'Issue' ? project.issues : project.merge_requests + collection.build + end + end + + def commands_target_type + params[:slash_commands_target_type] + end + + def commands_target_id + params[:slash_commands_target_id] + end +end diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb index 6aeebc26685..f1bbc7032d5 100644 --- a/app/services/slash_commands/interpret_service.rb +++ b/app/services/slash_commands/interpret_service.rb @@ -2,7 +2,7 @@ module SlashCommands class InterpretService < BaseService include Gitlab::SlashCommands::Dsl - attr_reader :issuable, :options + attr_reader :issuable # Takes a text and interprets the commands that are extracted from it. # Returns the content without commands, and hash of changes to be applied to a record. @@ -12,23 +12,21 @@ module SlashCommands @issuable = issuable @updates = {} - opts = { - issuable: issuable, - current_user: current_user, - project: project, - params: params - } - - content, commands = extractor.extract_commands(content, opts) + content, commands = extractor.extract_commands(content, context) + extract_updates(commands, context) + [content, @updates] + end - commands.each do |name, arg| - definition = self.class.command_definitions_by_name[name.to_sym] - next unless definition + # Takes a text and interprets the commands that are extracted from it. + # Returns the content without commands, and array of changes explained. + def explain(content, issuable) + return [content, []] unless current_user.can?(:use_slash_commands) - definition.execute(self, opts, arg) - end + @issuable = issuable - [content, @updates] + content, commands = extractor.extract_commands(content, context) + commands = explain_commands(commands, context) + [content, commands] end private @@ -40,6 +38,9 @@ module SlashCommands desc do "Close this #{issuable.to_ability_name.humanize(capitalize: false)}" end + explanation do + "Closes this #{issuable.to_ability_name.humanize(capitalize: false)}." + end condition do issuable.persisted? && issuable.open? && @@ -52,6 +53,9 @@ module SlashCommands desc do "Reopen this #{issuable.to_ability_name.humanize(capitalize: false)}" end + explanation do + "Reopens this #{issuable.to_ability_name.humanize(capitalize: false)}." + end condition do issuable.persisted? && issuable.closed? && @@ -62,6 +66,7 @@ module SlashCommands end desc 'Merge (when the pipeline succeeds)' + explanation 'Merges this merge request when the pipeline succeeds.' condition do last_diff_sha = params && params[:merge_request_diff_head_sha] issuable.is_a?(MergeRequest) && @@ -73,6 +78,9 @@ module SlashCommands end desc 'Change title' + explanation do |title_param| + "Changes the title to \"#{title_param}\"." + end params '<New title>' condition do issuable.persisted? && @@ -83,18 +91,25 @@ module SlashCommands end desc 'Assign' + explanation do |user| + "Assigns #{user.to_reference}." if user + end params '@user' condition do current_user.can?(:"admin_#{issuable.to_ability_name}", project) end - command :assign do |assignee_param| - user = extract_references(assignee_param, :user).first - user ||= User.find_by(username: assignee_param) - + parse_params do |assignee_param| + extract_references(assignee_param, :user).first || + User.find_by(username: assignee_param) + end + command :assign do |user| @updates[:assignee_id] = user.id if user end desc 'Remove assignee' + explanation do + "Removes assignee #{issuable.assignee.to_reference}." + end condition do issuable.persisted? && issuable.assignee_id? && @@ -105,19 +120,26 @@ module SlashCommands end desc 'Set milestone' + explanation do |milestone| + "Sets the milestone to #{milestone.to_reference}." if milestone + end params '%"milestone"' condition do current_user.can?(:"admin_#{issuable.to_ability_name}", project) && project.milestones.active.any? end - command :milestone do |milestone_param| - milestone = extract_references(milestone_param, :milestone).first - milestone ||= project.milestones.find_by(title: milestone_param.strip) - + parse_params do |milestone_param| + extract_references(milestone_param, :milestone).first || + project.milestones.find_by(title: milestone_param.strip) + end + command :milestone do |milestone| @updates[:milestone_id] = milestone.id if milestone end desc 'Remove milestone' + explanation do + "Removes #{issuable.milestone.to_reference(format: :name)} milestone." + end condition do issuable.persisted? && issuable.milestone_id? && @@ -128,6 +150,11 @@ module SlashCommands end desc 'Add label(s)' + explanation do |labels_param| + labels = find_label_references(labels_param) + + "Adds #{labels.join(' ')} #{'label'.pluralize(labels.count)}." if labels.any? + end params '~label1 ~"label 2"' condition do available_labels = LabelsFinder.new(current_user, project_id: project.id).execute @@ -147,6 +174,14 @@ module SlashCommands end desc 'Remove all or specific label(s)' + explanation do |labels_param = nil| + if labels_param.present? + labels = find_label_references(labels_param) + "Removes #{labels.join(' ')} #{'label'.pluralize(labels.count)}." if labels.any? + else + 'Removes all labels.' + end + end params '~label1 ~"label 2"' condition do issuable.persisted? && @@ -169,6 +204,10 @@ module SlashCommands end desc 'Replace all label(s)' + explanation do |labels_param| + labels = find_label_references(labels_param) + "Replaces all labels with #{labels.join(' ')} #{'label'.pluralize(labels.count)}." if labels.any? + end params '~label1 ~"label 2"' condition do issuable.persisted? && @@ -187,6 +226,7 @@ module SlashCommands end desc 'Add a todo' + explanation 'Adds a todo.' condition do issuable.persisted? && !TodoService.new.todo_exist?(issuable, current_user) @@ -196,6 +236,7 @@ module SlashCommands end desc 'Mark todo as done' + explanation 'Marks todo as done.' condition do issuable.persisted? && TodoService.new.todo_exist?(issuable, current_user) @@ -205,6 +246,9 @@ module SlashCommands end desc 'Subscribe' + explanation do + "Subscribes to this #{issuable.to_ability_name.humanize(capitalize: false)}." + end condition do issuable.persisted? && !issuable.subscribed?(current_user, project) @@ -214,6 +258,9 @@ module SlashCommands end desc 'Unsubscribe' + explanation do + "Unsubscribes from this #{issuable.to_ability_name.humanize(capitalize: false)}." + end condition do issuable.persisted? && issuable.subscribed?(current_user, project) @@ -223,18 +270,23 @@ module SlashCommands end desc 'Set due date' + explanation do |due_date| + "Sets the due date to #{due_date.to_s(:medium)}." if due_date + end params '<in 2 days | this Friday | December 31st>' condition do issuable.respond_to?(:due_date) && current_user.can?(:"admin_#{issuable.to_ability_name}", project) end - command :due do |due_date_param| - due_date = Chronic.parse(due_date_param).try(:to_date) - + parse_params do |due_date_param| + Chronic.parse(due_date_param).try(:to_date) + end + command :due do |due_date| @updates[:due_date] = due_date if due_date end desc 'Remove due date' + explanation 'Removes the due date.' condition do issuable.persisted? && issuable.respond_to?(:due_date) && @@ -245,8 +297,11 @@ module SlashCommands @updates[:due_date] = nil end - desc do - "Toggle the Work In Progress status" + desc 'Toggle the Work In Progress status' + explanation do + verb = issuable.work_in_progress? ? 'Unmarks' : 'Marks' + noun = issuable.to_ability_name.humanize(capitalize: false) + "#{verb} this #{noun} as Work In Progress." end condition do issuable.persisted? && @@ -257,45 +312,72 @@ module SlashCommands @updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip' end - desc 'Toggle emoji reward' + desc 'Toggle emoji award' + explanation do |name| + "Toggles :#{name}: emoji award." if name + end params ':emoji:' condition do issuable.persisted? end - command :award do |emoji| - name = award_emoji_name(emoji) + parse_params do |emoji_param| + match = emoji_param.match(Banzai::Filter::EmojiFilter.emoji_pattern) + match[1] if match + end + command :award do |name| if name && issuable.user_can_award?(current_user, name) @updates[:emoji_award] = name end end desc 'Set time estimate' + explanation do |time_estimate| + time_estimate = Gitlab::TimeTrackingFormatter.output(time_estimate) + + "Sets time estimate to #{time_estimate}." if time_estimate + end params '<1w 3d 2h 14m>' condition do current_user.can?(:"admin_#{issuable.to_ability_name}", project) end - command :estimate do |raw_duration| - time_estimate = Gitlab::TimeTrackingFormatter.parse(raw_duration) - + parse_params do |raw_duration| + Gitlab::TimeTrackingFormatter.parse(raw_duration) + end + command :estimate do |time_estimate| if time_estimate @updates[:time_estimate] = time_estimate end end desc 'Add or substract spent time' + explanation do |time_spent| + if time_spent + if time_spent > 0 + verb = 'Adds' + value = time_spent + else + verb = 'Substracts' + value = -time_spent + end + + "#{verb} #{Gitlab::TimeTrackingFormatter.output(value)} spent time." + end + end params '<1h 30m | -1h 30m>' condition do current_user.can?(:"admin_#{issuable.to_ability_name}", issuable) end - command :spend do |raw_duration| - time_spent = Gitlab::TimeTrackingFormatter.parse(raw_duration) - + parse_params do |raw_duration| + Gitlab::TimeTrackingFormatter.parse(raw_duration) + end + command :spend do |time_spent| if time_spent @updates[:spend_time] = { duration: time_spent, user: current_user } end end desc 'Remove time estimate' + explanation 'Removes time estimate.' condition do issuable.persisted? && current_user.can?(:"admin_#{issuable.to_ability_name}", project) @@ -305,6 +387,7 @@ module SlashCommands end desc 'Remove spent time' + explanation 'Removes spent time.' condition do issuable.persisted? && current_user.can?(:"admin_#{issuable.to_ability_name}", project) @@ -318,19 +401,28 @@ module SlashCommands params '@user' command :cc - desc 'Defines target branch for MR' + desc 'Define target branch for MR' + explanation do |branch_name| + "Sets target branch to #{branch_name}." + end params '<Local branch name>' condition do issuable.respond_to?(:target_branch) && (current_user.can?(:"update_#{issuable.to_ability_name}", issuable) || issuable.new_record?) end - command :target_branch do |target_branch_param| - branch_name = target_branch_param.strip + parse_params do |target_branch_param| + target_branch_param.strip + end + command :target_branch do |branch_name| @updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name) end desc 'Move issue from one column of the board to another' + explanation do |target_list_name| + label = find_label_references(target_list_name).first + "Moves issue to #{label} column in the board." if label + end params '~"Target column"' condition do issuable.is_a?(Issue) && @@ -352,11 +444,35 @@ module SlashCommands end end + def find_labels(labels_param) + extract_references(labels_param, :label) | + LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute + end + + def find_label_references(labels_param) + find_labels(labels_param).map(&:to_reference) + end + def find_label_ids(labels_param) - label_ids_by_reference = extract_references(labels_param, :label).map(&:id) - labels_ids_by_name = LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute.select(:id) + find_labels(labels_param).map(&:id) + end - label_ids_by_reference | labels_ids_by_name + def explain_commands(commands, opts) + commands.map do |name, arg| + definition = self.class.definition_by_name(name) + next unless definition + + definition.explain(self, opts, arg) + end.compact + end + + def extract_updates(commands, opts) + commands.each do |name, arg| + definition = self.class.definition_by_name(name) + next unless definition + + definition.execute(self, opts, arg) + end end def extract_references(arg, type) @@ -366,9 +482,13 @@ module SlashCommands ext.references(type) end - def award_emoji_name(emoji) - match = emoji.match(Banzai::Filter::EmojiFilter.emoji_pattern) - match[1] if match + def context + { + issuable: issuable, + current_user: current_user, + project: project, + params: params + } end end end diff --git a/app/views/errors/omniauth_error.html.haml b/app/views/errors/omniauth_error.html.haml index 72508b91134..20b7fa471a0 100644 --- a/app/views/errors/omniauth_error.html.haml +++ b/app/views/errors/omniauth_error.html.haml @@ -1,16 +1,15 @@ - content_for(:title, 'Auth Error') -%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') } - %h1 - 422 + .container + = render "shared/errors/graphic_422.svg" %h3 Sign-in using #{@provider} auth failed - %hr - %p Sign-in failed because #{@error}. - %p There are couple of steps you can take: -%ul - %li Try logging in using your email - %li Try logging in using your username - %li If you have forgotten your password, try recovering it using #{ link_to "Password recovery", new_password_path(resource_name) } + %p.light.subtitle Sign-in failed because #{@error}. + + %p Try logging in using your username or email. If you have forgotten your password, try recovering it -%p If none of the options work, try contacting the GitLab administrator. + = link_to "Sign in", new_session_path(:user), class: 'btn primary' + = link_to "Recover password", new_password_path(resource_name), class: 'btn secondary' + + %hr + %p.light If none of the options work, try contacting a GitLab administrator. diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml index 8d3aa4d1a74..7c7573862d0 100644 --- a/app/views/groups/milestones/new.html.haml +++ b/app/views/groups/milestones/new.html.haml @@ -26,7 +26,7 @@ .form-group.milestone-description = f.label :description, "Description", class: "control-label" .col-sm-10 - = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do + = render layout: 'projects/md_preview', locals: { url: '' } do = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...' .clearfix .error-alert diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 36543edc040..dc926a615c7 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,5 +1,5 @@ !!! 5 -%html{ lang: "en", class: "#{page_class}" } +%html{ lang: I18n.locale, class: "#{page_class}" } = render "layouts/head" %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } = Gon::Base.render_data diff --git a/app/views/layouts/oauth_error.html.haml b/app/views/layouts/oauth_error.html.haml new file mode 100644 index 00000000000..34bcd2a8b3a --- /dev/null +++ b/app/views/layouts/oauth_error.html.haml @@ -0,0 +1,127 @@ +!!! 5 +%html{ lang: "en" } + %head + %meta{ :content => "width=device-width, initial-scale=1, maximum-scale=1", :name => "viewport" } + %title= yield(:title) + :css + body { + color: #666; + text-align: center; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + margin: auto; + font-size: 16px; + } + + .container { + margin: auto 20px; + } + + h3 { + color: #456; + font-size: 22px; + font-weight: bold; + margin-bottom: 6px; + } + + p { + max-width: 470px; + margin: 16px auto; + } + + .subtitle { + margin: 0 auto 20px; + } + + svg { + width: 280px; + height: 280px; + display: block; + margin: 40px auto; + } + + .tv-screen path { + animation: move-lines 1s linear infinite; + } + + + @keyframes move-lines { + 0% {transform: translateY(0)} + 50% {transform: translateY(-10px)} + 100% {transform: translateY(-20px)} + } + + .tv-screen path:nth-child(1) { + animation-delay: .2s + } + + .tv-screen path:nth-child(2) { + animation-delay: .4s + } + + .tv-screen path:nth-child(3) { + animation-delay: .6s + } + + .tv-screen path:nth-child(4) { + animation-delay: .8s + } + + .tv-screen path:nth-child(5) { + animation-delay: 2s + } + + .text-422 { + animation: flicker 1s infinite; + } + + @keyframes flicker { + 0% {opacity: 0.3;} + 10% {opacity: 1;} + 15% {opacity: .3;} + 20% {opacity: .5;} + 25% {opacity: 1;} + } + + .light { + color: #8D8D8D; + } + + hr { + max-width: 600px; + margin: 18px auto; + border: 0; + border-top: 1px solid #EEE; + } + + .btn { + padding: 8px 14px; + border-radius: 3px; + border: 1px solid; + display: inline-block; + text-decoration: none; + margin: 4px 8px; + font-size: 14px; + } + + .primary { + color: #fff; + background-color: #1aaa55; + border-color: #168f48; + } + + .primary:hover { + background-color: #168f48; + } + + .secondary { + color: #1aaa55; + background-color: #fff; + border-color: #1aaa55; + } + + .secondary:hover { + background-color: #f3fff8; + } + +%body + = yield diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index e9e06e5c8e3..3f5b0c54e50 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -5,14 +5,9 @@ - content_for :project_javascripts do - project = @target_project || @project - - if @project_wiki && @page - - preview_markdown_path = namespace_project_wiki_preview_markdown_path(project.namespace, project, @page.slug) - - else - - preview_markdown_path = preview_markdown_namespace_project_path(project.namespace, project) - if current_user :javascript window.uploads_path = "#{namespace_project_uploads_path project.namespace,project}"; - window.preview_markdown_path = "#{preview_markdown_path}"; - content_for :header_content do .js-dropdown-menu-projects diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index c74b3249a13..4a1438aa68e 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -73,6 +73,11 @@ = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), { include_blank: 'Do not show on profile' }, class: "select2" %span.help-block This email will be displayed on your public profile. .form-group + = f.label :preferred_language, class: "label-light" + = f.select :preferred_language, Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] }, + {}, class: "select2" + %span.help-block This feature is experimental and translations are not complete yet. + .form-group = f.label :skype, class: "label-light" = f.text_field :skype, class: "form-control" .form-group diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 23e27c1105c..d0698285f84 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -1,3 +1,5 @@ +- referenced_users = local_assigns.fetch(:referenced_users, nil) + .md-area .md-header %ul.nav-links.clearfix @@ -28,9 +30,10 @@ .md-write-holder = yield - .md.md-preview-holder.js-md-preview.hide{ class: (preview_class if defined?(preview_class)) } + .md.md-preview-holder.js-md-preview.hide.md-preview{ data: { url: url } } + .referenced-commands.hide - - if defined?(referenced_users) && referenced_users + - if referenced_users .referenced-users.hide %span = icon("exclamation-triangle") diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index d3c3e40d518..796ecdfd014 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -1,4 +1,5 @@ - page_title "New Branch" +- default_ref = params[:ref] || @project.default_branch - if @error .alert.alert-danger @@ -16,12 +17,11 @@ .help-block.text-danger.js-branch-name-error .form-group = label_tag :ref, 'Create from', class: 'control-label' - .col-sm-10 - = hidden_field_tag :ref, params[:ref] || @project.default_branch - = dropdown_tag(params[:ref] || @project.default_branch, - options: { toggle_class: 'js-branch-select wide', - filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search branches", - data: { selected: params[:ref] || @project.default_branch, field_name: 'ref' } }) + .col-sm-10.dropdown.create-from + = hidden_field_tag :ref, default_ref + = button_tag type: 'button', title: default_ref, class: 'dropdown-toggle form-control js-branch-select', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do + .text-left.dropdown-toggle-text= default_ref + = render 'shared/ref_dropdown', dropdown_class: 'wide' .help-block Existing branch name, tag, or commit SHA .form-actions = button_tag 'Create branch', class: 'btn btn-create', tabindex: 3 diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index 0f080b6acee..1f4c9fac54c 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -9,7 +9,7 @@ = hidden_field_tag :from, params[:from] = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do .dropdown-toggle-text.str-truncated= params[:from] || 'Select branch/tag' - = render "ref_dropdown" + = render 'shared/ref_dropdown' .compare-ellipsis.inline ... .form-group.dropdown.compare-form-group.to.js-compare-to-dropdown .input-group.inline-input-group @@ -17,7 +17,7 @@ = hidden_field_tag :to, params[:to] = button_tag type: 'button', title: params[:to], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do .dropdown-toggle-text.str-truncated= params[:to] || 'Select branch/tag' - = render "ref_dropdown" + = render 'shared/ref_dropdown' = button_tag "Compare", class: "btn btn-create commits-compare-btn" - if @merge_request.present? diff --git a/app/views/projects/cycle_analytics/_empty_stage.html.haml b/app/views/projects/cycle_analytics/_empty_stage.html.haml index c3f95860e92..cdad0bc7231 100644 --- a/app/views/projects/cycle_analytics/_empty_stage.html.haml +++ b/app/views/projects/cycle_analytics/_empty_stage.html.haml @@ -2,6 +2,6 @@ .empty-stage .icon-no-data = custom_icon ('icon_no_data') - %h4 We don't have enough data to show this stage. + %h4 {{ __('We don\'t have enough data to show this stage.') }} %p {{currentStage.emptyStageText}} diff --git a/app/views/projects/cycle_analytics/_no_access.html.haml b/app/views/projects/cycle_analytics/_no_access.html.haml index 0ffc79b3181..c3eda398234 100644 --- a/app/views/projects/cycle_analytics/_no_access.html.haml +++ b/app/views/projects/cycle_analytics/_no_access.html.haml @@ -2,6 +2,6 @@ .no-access-stage .icon-lock = custom_icon ('icon_lock') - %h4 You need permission. + %h4 {{ __('You need permission.') }} %p - Want to see the data? Please ask administrator for access. + {{ __('Want to see the data? Please ask an administrator for access.') }} diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index dd3fa814716..819f29d3ca5 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -2,6 +2,7 @@ - page_title "Cycle Analytics" - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('common_vue') + = page_specific_javascript_bundle_tag('locale') = page_specific_javascript_bundle_tag('cycle_analytics') = render "projects/head" @@ -15,16 +16,16 @@ = custom_icon('icon_cycle_analytics_splash') .col-sm-8.col-xs-12.inner-content %h4 - Introducing Cycle Analytics + {{ __('Introducing Cycle Analytics') }} %p - Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project. + {{ __('Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.') }} - = link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn' + = link_to _('Read more'), help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn' = icon("spinner spin", "v-show" => "isLoading") .wrapper{ "v-show" => "!isLoading && !hasError" } .panel.panel-default .panel-heading - Pipeline Health + {{ __('Pipeline Health') }} .content-block .container-fluid .row @@ -34,15 +35,15 @@ .col-sm-3.col-xs-12.column .dropdown.inline.js-ca-dropdown %button.dropdown-menu-toggle{ "data-toggle" => "dropdown", :type => "button" } - %span.dropdown-label Last 30 days + %span.dropdown-label {{ n__('Last %d day', 'Last %d days', 30) }} %i.fa.fa-chevron-down %ul.dropdown-menu.dropdown-menu-align-right %li %a{ "href" => "#", "data-value" => "30" } - Last 30 days + {{ n__('Last %d day', 'Last %d days', 30) }} %li %a{ "href" => "#", "data-value" => "90" } - Last 90 days + {{ n__('Last %d day', 'Last %d days', 90) }} .stage-panel-container .panel.panel-default.stage-panel .panel-heading @@ -50,20 +51,20 @@ %ul %li.stage-header %span.stage-name - Stage - %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The phase of the development lifecycle.", "aria-hidden" => "true" } + {{ __('ProjectLifecycle|Stage') }} + %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The phase of the development lifecycle."), "aria-hidden" => "true" } %li.median-header %span.stage-name - Median - %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.", "aria-hidden" => "true" } + {{ __('Median') }} + %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6."), "aria-hidden" => "true" } %li.event-header %span.stage-name - {{ currentStage ? currentStage.legend : 'Related Issues' }} - %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The collection of events added to the data gathered for that stage.", "aria-hidden" => "true" } + {{ currentStage ? __(currentStage.legend) : __('Related Issues') }} + %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The collection of events added to the data gathered for that stage."), "aria-hidden" => "true" } %li.total-time-header %span.stage-name - Total Time - %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The time taken by each data entry gathered by that stage.", "aria-hidden" => "true" } + {{ __('Total Time') }} + %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The time taken by each data entry gathered by that stage."), "aria-hidden" => "true" } .stage-panel-body %nav.stage-nav %ul @@ -75,10 +76,10 @@ %span{ "v-if" => "stage.value" } {{ stage.value }} %span.stage-empty{ "v-else" => true } - Not enough data + {{ __('Not enough data') }} %template{ "v-else" => true } %span.not-available - Not available + {{ __('Not available') }} .section.stage-events %template{ "v-if" => "isLoadingStage" } = icon("spinner spin") diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 881ee9fd596..9e306d4543c 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -6,7 +6,7 @@ = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('diff_notes') -.merge-request{ 'data-url' => merge_request_path(@merge_request), 'data-project-path' => project_path(@merge_request.project) } +.merge-request{ 'data-url' => merge_request_path(@merge_request, format: :json), 'data-project-path' => project_path(@merge_request.project) } = render "projects/merge_requests/show/mr_title" .merge-request-details.issuable-details{ data: { id: @merge_request.project.id } } diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 0f4a8508751..2e978fda624 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -9,7 +9,7 @@ .form-group.milestone-description = f.label :description, "Description", class: "control-label" .col-sm-10 - = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do + = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project) } do = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...' = render 'projects/notes/hints' .clearfix diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index a1efc0b051a..00230b0bdf8 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -2,7 +2,7 @@ = form_tag '#', method: :put, class: 'edit-note common-note-form js-quick-submit' do = hidden_field_tag :target_id, '', class: 'js-form-target-id' = hidden_field_tag :target_type, '', class: 'js-form-target-type' - = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview', referenced_users: true } do + = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(project), referenced_users: true } do = render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..." = render 'projects/notes/hints' diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 0d835a9e949..46f785fefca 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -1,4 +1,8 @@ - supports_slash_commands = note_supports_slash_commands?(@note) +- if supports_slash_commands + - preview_url = preview_markdown_path(@project, slash_commands_target_type: @note.noteable_type, slash_commands_target_id: @note.noteable_id) +- else + - preview_url = preview_markdown_path(@project) = form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f| = hidden_field_tag :view, diff_view @@ -18,7 +22,7 @@ -# DiffNote = f.hidden_field :position - = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do + = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text', diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 555228623cc..2a66addb08a 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -1,7 +1,7 @@ %ul#notes-list.notes.main-notes-list.timeline = render "shared/notes/notes" -= render 'projects/notes/edit_form' += render 'projects/notes/edit_form', project: @project %ul.notes.notes-form.timeline %li.timeline-entry diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index 79d8d721aa9..faa24a3c88e 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -11,7 +11,7 @@ = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f| - = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do + = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..." = render 'projects/notes/hints' .error-alert diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index 160d4c7a223..a6894b9adc0 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -28,7 +28,7 @@ .form-group = label_tag :release_description, 'Release notes', class: 'control-label' .col-sm-10 - = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do + = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..." = render 'projects/notes/hints' .help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page. diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 0d2cd4a7476..00869aff27b 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -12,7 +12,7 @@ .form-group = f.label :content, class: 'control-label' .col-sm-10 - = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do + = render layout: 'projects/md_preview', locals: { url: namespace_project_wiki_preview_markdown_path(@project.namespace, @project, @page.slug) } do = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: 'Write your content or drag files here...' = render 'projects/notes/hints' diff --git a/app/views/projects/compare/_ref_dropdown.html.haml b/app/views/shared/_ref_dropdown.html.haml index 05fb37cdc0f..96f68c80c48 100644 --- a/app/views/projects/compare/_ref_dropdown.html.haml +++ b/app/views/shared/_ref_dropdown.html.haml @@ -1,4 +1,6 @@ -.dropdown-menu.dropdown-menu-selectable +- dropdown_class = local_assigns.fetch(:dropdown_class, '') + +.dropdown-menu.dropdown-menu-selectable{ class: dropdown_class } = dropdown_title "Select Git revision" = dropdown_filter "Filter by Git revision" = dropdown_content diff --git a/app/views/shared/errors/_graphic_422.svg b/app/views/shared/errors/_graphic_422.svg new file mode 100644 index 00000000000..87128ecd69d --- /dev/null +++ b/app/views/shared/errors/_graphic_422.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 260 246" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="0" width="178" height="136" rx="10"/><mask id="1" width="178" height="136" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask></defs><g fill="none" fill-rule="evenodd"><g fill="#e5e5e5" fill-rule="nonzero"><path d="m109.88 37.634c5.587-3.567 12.225-5.634 19.345-5.634 7.445 0 14.363 2.26 20.1 6.132l21.435-37.13c.554-.959 1.771-1.292 2.734-.736.957.552 1.284 1.777.73 2.736l-21.496 37.23c-.065.112-.138.215-.219.309 3.686 3.13 6.733 6.988 8.919 11.353l-3.393.002c-5.775-10.322-16.705-16.901-28.814-16.901-12.12 0-23.06 6.594-28.833 16.935l-3.393.002c2.32-4.646 5.616-8.72 9.618-11.954l-21.349-36.977c-.554-.959-.227-2.184.73-2.736.963-.556 2.181-.223 2.734.736l21.15 36.629"/><path d="m3 70v134c0 9.389 7.611 17 16.997 17h220.01c9.389 0 16.997-7.611 16.997-17v-134c0-9.389-7.611-17-16.997-17h-220.01c-9.389 0-16.997 7.611-16.997 17m-3 0c0-11.05 8.95-20 19.997-20h220.01c11.04 0 19.997 8.958 19.997 20v134c0 11.05-8.95 20-19.997 20h-220.01c-11.04 0-19.997-8.958-19.997-20v-134"/></g><ellipse cx="129" cy="241.5" fill="#f9f9f9" rx="89" ry="4.5"/><g fill-rule="nonzero" transform="translate(210 70)"><path fill="#eaeaea" d="m16 29c7.18 0 13-5.82 13-13 0-7.18-5.82-13-13-13-7.18 0-13 5.82-13 13 0 7.18 5.82 13 13 13m0 3c-8.837 0-16-7.163-16-16 0-8.837 7.163-16 16-16 8.837 0 16 7.163 16 16 0 8.837-7.163 16-16 16" id="2"/><path fill="#6b4fbb" d="m16 21c2.761 0 5-2.239 5-5 0-2.761-2.239-5-5-5-2.761 0-5 2.239-5 5 0 2.761 2.239 5 5 5m0 3c-4.418 0-8-3.582-8-8 0-4.418 3.582-8 8-8 4.418 0 8 3.582 8 8 0 4.418-3.582 8-8 8" id="3"/></g><g fill-rule="nonzero" transform="translate(210 109)"><use xlink:href="#2"/><use xlink:href="#3"/></g><g transform="translate(210 147)"><path fill="#e5e5e5" fill-rule="nonzero" d="m3 5.992v45.02c0 1.647 1.346 2.992 3 2.992h20c1.657 0 3-1.341 3-2.992v-45.02c0-1.647-1.346-2.992-3-2.992h-20c-1.657 0-3 1.341-3 2.992m-3 0c0-3.309 2.687-5.992 6-5.992h20c3.314 0 6 2.692 6 5.992v45.02c0 3.309-2.687 5.992-6 5.992h-20c-3.314 0-6-2.692-6-5.992v-45.02"/><rect width="16" height="4" x="8" y="27" fill="#fdb692" rx="2"/><rect width="16" height="4" x="8" y="19" fill="#fc9867" rx="2"/><rect width="16" height="4" x="8" y="11" fill="#fc6d26" rx="2"/><rect width="16" height="4" x="8" y="35" fill="#fed3bd" rx="2"/><rect width="16" height="4" x="8" y="43" fill="#fef0e9" rx="2"/></g><g transform="translate(16 69)"><use fill="#6b4fbb" fill-opacity=".1" stroke="#e5e5e5" stroke-width="6" mask="url(#1)" xlink:href="#0"/><g class="tv-screen" fill="#fff"><path opacity=".4" mix-blend-mode="overlay" d="m3 17h172v16h-172z"/><path opacity=".6" mix-blend-mode="overlay" d="m3 70h172v24h-172z"/><path opacity=".3" mix-blend-mode="overlay" d="m3 107h172v16h-172z"/><path opacity=".4" mix-blend-mode="overlay" d="m3 40h172v8h-172z"/><path opacity=".3" mix-blend-mode="overlay" d="m3 55h172v8h-172z"/></g></g><path class="text-422" d="m.693 19h5.808c.277 0 .498-.224.498-.5 0-.268-.223-.5-.498-.5h-5.808v-2.094l3.777-5.906h3.916l-4.124 6.454h6.259v-6.454h.978c.273 0 .5-.224.5-.5 0-.268-.224-.5-.5-.5h-.978v-2h4.698v6h-2.721c-.277 0-.498.224-.498.5 0 .268.223.5.498.5h2.721v2.454h2.723v4.2h-2.723v5.346h-4.698v-5.346h-9.828v-1.654m4.417-10l1.279-2h3.914l-1.278 2h-3.916m1.919-3l1.279-2h4.192c.27 0 .5-.224.5-.5 0-.268-.224-.5-.5-.5h-3.552l1.142-1.786h5.13v4.786h-8.191m31.09 19v1h-15.738v-2h5.118c.271 0 .503-.224.503-.5 0-.268-.225-.5-.503-.5h-5.118v-1.184l2.656-2.822c.682-.725 1.306-1.39 1.872-1.994h5.428c-.389.394-.808.815-1.256 1.264-1.428 1.428-2.562 2.568-3.403 3.42h10.442v2.316h-4.614c-.271 0-.503.224-.503.5 0 .268.225.5.503.5h4.614m-6.674-13c.493-.631.87-1.208 1.129-1.73.365-.736.548-1.464.548-2.183 0-1.107-.335-1.962-1-2.565-.67-.603-1.619-.905-2.847-.905-.874 0-1.857.174-2.947.523-1.09.349-2.227.855-3.412 1.519v-2.659h3.589c.27 0 .5-.224.5-.5 0-.268-.224-.5-.5-.5h-3.589v-.906c1.184-.432 2.344-.761 3.478-.988 1.134-.227 2.222-.34 3.262-.34 2.623 0 4.684.611 6.184 1.834.157.128.307.262.448.4h-2.782c-.27 0-.5.224-.5.5 0 .268.224.5.5.5h3.602c.654 1.01.981 2.209.981 3.605 0 .974-.163 1.887-.49 2.739-.326.852-.888 1.798-1.685 2.839-.397.509-1.261 1.448-2.594 2.816h-5.474c1.34-1.436 2.261-2.436 2.763-3h4.396c.271 0 .499-.224.499-.5 0-.268-.223-.5-.499-.5h-3.557m28.14 12v2h-15.738v-4.184l2.651-2.816h5.313c-1.087 1.089-1.976 1.983-2.668 2.684h10.442v1.316h-4.083c-.271 0-.503.224-.503.5 0 .268.225.5.503.5h4.083m-2.069-11c-.045.061-.092.122-.139.184-.567.727-2.089 2.333-4.568 4.816h-5.372c2.601-2.77 4.204-4.503 4.81-5.198.83-.952 1.428-1.796 1.793-2.532.365-.736.548-1.464.548-2.183 0-1.107-.335-1.962-1-2.565-.67-.603-1.619-.905-2.847-.905-.874 0-1.857.174-2.947.523-1.09.349-2.227.855-3.412 1.519v-2.659h3.117c.271 0 .503-.224.503-.5 0-.268-.225-.5-.503-.5h-3.117v-.906c1.184-.432 2.344-.761 3.478-.988 1.134-.227 2.222-.34 3.262-.34 2.623 0 4.684.611 6.184 1.834.157.128.307.262.448.4h-1.248c-.271 0-.503.224-.503.5 0 .268.225.5.503.5h2.069c.654 1.01.981 2.209.981 3.605 0 .844-.123 1.642-.368 2.395h-2.683c-.271 0-.503.224-.503.5 0 .268.225.5.503.5h2.272c-.159.321-.347.655-.566 1h-3.706c-.271 0-.503.224-.503.5 0 .268.225.5.503.5h3.01" transform="translate(75 124)" fill="#5c5c5c"/></g></svg> diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 17107f55a2d..7748351b333 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -17,7 +17,7 @@ = render 'shared/issuable/form/template_selector', issuable: issuable = render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:work_in_progress?) -= render 'shared/issuable/form/description', issuable: issuable, form: form += render 'shared/issuable/form/description', issuable: issuable, form: form, project: project - if issuable.respond_to?(:confidential) .form-group diff --git a/app/views/shared/issuable/form/_description.html.haml b/app/views/shared/issuable/form/_description.html.haml index dbace9ce401..cbc7125c0d5 100644 --- a/app/views/shared/issuable/form/_description.html.haml +++ b/app/views/shared/issuable/form/_description.html.haml @@ -1,15 +1,22 @@ +- project = local_assigns.fetch(:project) - issuable = local_assigns.fetch(:issuable) - form = local_assigns.fetch(:form) +- supports_slash_commands = issuable.new_record? + +- if supports_slash_commands + - preview_url = preview_markdown_path(project, slash_commands_target_type: issuable.class.name) +- else + - preview_url = preview_markdown_path(project) .form-group.detail-page-description = form.label :description, 'Description', class: 'control-label' .col-sm-10 - = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do + = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do = render 'projects/zen', f: form, attr: :description, classes: 'note-textarea', placeholder: "Write a comment or drag your files here...", - supports_slash_commands: !issuable.persisted? - = render 'projects/notes/hints', supports_slash_commands: !issuable.persisted? + supports_slash_commands: supports_slash_commands + = render 'projects/notes/hints', supports_slash_commands: supports_slash_commands .clearfix .error-alert diff --git a/changelogs/unreleased/29145-oauth-422.yml b/changelogs/unreleased/29145-oauth-422.yml new file mode 100644 index 00000000000..94e4cd84ad1 --- /dev/null +++ b/changelogs/unreleased/29145-oauth-422.yml @@ -0,0 +1,4 @@ +--- +title: Redesign auth 422 page +merge_request: +author: diff --git a/changelogs/unreleased/31810-commit-link.yml b/changelogs/unreleased/31810-commit-link.yml new file mode 100644 index 00000000000..857c9cb95c5 --- /dev/null +++ b/changelogs/unreleased/31810-commit-link.yml @@ -0,0 +1,4 @@ +--- +title: Remove `#` being added on commit sha in MR widget +merge_request: +author: diff --git a/changelogs/unreleased/implement-i18n-support.yml b/changelogs/unreleased/implement-i18n-support.yml new file mode 100644 index 00000000000..d304fbecf90 --- /dev/null +++ b/changelogs/unreleased/implement-i18n-support.yml @@ -0,0 +1,4 @@ +--- +title: Add support for i18n on Cycle Analytics page +merge_request: 10669 +author: diff --git a/changelogs/unreleased/merge-request-poll-json-endpoint.yml b/changelogs/unreleased/merge-request-poll-json-endpoint.yml new file mode 100644 index 00000000000..6c41984e9b7 --- /dev/null +++ b/changelogs/unreleased/merge-request-poll-json-endpoint.yml @@ -0,0 +1,4 @@ +--- +title: Fixed bug where merge request JSON would be displayed +merge_request: +author: diff --git a/changelogs/unreleased/preview-separate-slash-commands.yml b/changelogs/unreleased/preview-separate-slash-commands.yml new file mode 100644 index 00000000000..6240ccc957c --- /dev/null +++ b/changelogs/unreleased/preview-separate-slash-commands.yml @@ -0,0 +1,4 @@ +--- +title: Display slash commands outcome when previewing Markdown +merge_request: 10054 +author: Rares Sfirlogea diff --git a/config/application.rb b/config/application.rb index f2ecc4ce77c..32ad2393648 100644 --- a/config/application.rb +++ b/config/application.rb @@ -40,6 +40,9 @@ module Gitlab # config.i18n.default_locale = :de config.i18n.enforce_available_locales = false + # Translation for AR attrs is not working well for POROs like WikiPage + config.gettext_i18n_rails.use_for_active_record_attributes = false + # Configure the default encoding used in templates for Ruby 1.9. config.encoding = "utf-8" diff --git a/config/initializers/fast_gettext.rb b/config/initializers/fast_gettext.rb new file mode 100644 index 00000000000..bea85b43b95 --- /dev/null +++ b/config/initializers/fast_gettext.rb @@ -0,0 +1,5 @@ +FastGettext.add_text_domain 'gitlab', path: 'locale', type: :po +FastGettext.default_text_domain = 'gitlab' +FastGettext.default_available_locales = Gitlab::I18n.available_locales + +I18n.available_locales = Gitlab::I18n.available_locales diff --git a/config/initializers/gettext_rails_i18n_patch.rb b/config/initializers/gettext_rails_i18n_patch.rb new file mode 100644 index 00000000000..69118f464ca --- /dev/null +++ b/config/initializers/gettext_rails_i18n_patch.rb @@ -0,0 +1,42 @@ +require 'gettext_i18n_rails/haml_parser' +require 'gettext_i18n_rails_js/parser/javascript' + +VUE_TRANSLATE_REGEX = /((%[\w.-]+)(?:\s))?{{ (N|n|s)?__\((.*)\) }}/ + +module GettextI18nRails + class HamlParser + singleton_class.send(:alias_method, :old_convert_to_code, :convert_to_code) + + # We need to convert text in Mustache format + # to a format that can be parsed by Gettext scripts. + # If we found a content like "{{ __('Stage') }}" + # in a HAML file we convert it to "= _('Stage')", that way + # it can be processed by the "rake gettext:find" script. + # + # Overwrites: https://github.com/grosser/gettext_i18n_rails/blob/8396387a431e0f8ead72fc1cd425cad2fa4992f2/lib/gettext_i18n_rails/haml_parser.rb#L9 + def self.convert_to_code(text) + text.gsub!(VUE_TRANSLATE_REGEX, "\\2= \\3_(\\4)") + + old_convert_to_code(text) + end + end +end + +module GettextI18nRailsJs + module Parser + module Javascript + # This is required to tell the `rake gettext:find` script to use the Javascript + # parser for *.vue files. + # + # Overwrites: https://github.com/webhippie/gettext_i18n_rails_js/blob/46c58db6d2053a4f5f36a0eb024ea706ff5707cb/lib/gettext_i18n_rails_js/parser/javascript.rb#L36 + def target?(file) + [ + ".js", + ".jsx", + ".coffee", + ".vue" + ].include? ::File.extname(file) + end + end + end +end diff --git a/config/locales/de.yml b/config/locales/de.yml new file mode 100644 index 00000000000..533663a2704 --- /dev/null +++ b/config/locales/de.yml @@ -0,0 +1,219 @@ +--- +de: + activerecord: + errors: + messages: + record_invalid: 'Gültigkeitsprüfung ist fehlgeschlagen: %{errors}' + restrict_dependent_destroy: + has_one: Datensatz kann nicht gelöscht werden, da ein abhängiger %{record}-Datensatz + existiert. + has_many: Datensatz kann nicht gelöscht werden, da abhängige %{record} existieren. + date: + abbr_day_names: + - So + - Mo + - Di + - Mi + - Do + - Fr + - Sa + abbr_month_names: + - + - Jan + - Feb + - Mär + - Apr + - Mai + - Jun + - Jul + - Aug + - Sep + - Okt + - Nov + - Dez + day_names: + - Sonntag + - Montag + - Dienstag + - Mittwoch + - Donnerstag + - Freitag + - Samstag + formats: + default: "%d.%m.%Y" + long: "%e. %B %Y" + short: "%e. %b" + month_names: + - + - Januar + - Februar + - März + - April + - Mai + - Juni + - Juli + - August + - September + - Oktober + - November + - Dezember + order: + - :day + - :month + - :year + datetime: + distance_in_words: + about_x_hours: + one: etwa eine Stunde + other: etwa %{count} Stunden + about_x_months: + one: etwa ein Monat + other: etwa %{count} Monate + about_x_years: + one: etwa ein Jahr + other: etwa %{count} Jahre + almost_x_years: + one: fast ein Jahr + other: fast %{count} Jahre + half_a_minute: eine halbe Minute + less_than_x_minutes: + one: weniger als eine Minute + other: weniger als %{count} Minuten + less_than_x_seconds: + one: weniger als eine Sekunde + other: weniger als %{count} Sekunden + over_x_years: + one: mehr als ein Jahr + other: mehr als %{count} Jahre + x_days: + one: ein Tag + other: "%{count} Tage" + x_minutes: + one: eine Minute + other: "%{count} Minuten" + x_months: + one: ein Monat + other: "%{count} Monate" + x_seconds: + one: eine Sekunde + other: "%{count} Sekunden" + prompts: + day: Tag + hour: Stunden + minute: Minute + month: Monat + second: Sekunde + year: Jahr + errors: + format: "%{attribute} %{message}" + messages: + accepted: muss akzeptiert werden + blank: muss ausgefüllt werden + present: darf nicht ausgefüllt werden + confirmation: stimmt nicht mit %{attribute} überein + empty: muss ausgefüllt werden + equal_to: muss genau %{count} sein + even: muss gerade sein + exclusion: ist nicht verfügbar + greater_than: muss größer als %{count} sein + greater_than_or_equal_to: muss größer oder gleich %{count} sein + inclusion: ist kein gültiger Wert + invalid: ist nicht gültig + less_than: muss kleiner als %{count} sein + less_than_or_equal_to: muss kleiner oder gleich %{count} sein + model_invalid: 'Gültigkeitsprüfung ist fehlgeschlagen: %{errors}' + not_a_number: ist keine Zahl + not_an_integer: muss ganzzahlig sein + odd: muss ungerade sein + required: muss ausgefüllt werden + taken: ist bereits vergeben + too_long: + one: ist zu lang (mehr als 1 Zeichen) + other: ist zu lang (mehr als %{count} Zeichen) + too_short: + one: ist zu kurz (weniger als 1 Zeichen) + other: ist zu kurz (weniger als %{count} Zeichen) + wrong_length: + one: hat die falsche Länge (muss genau 1 Zeichen haben) + other: hat die falsche Länge (muss genau %{count} Zeichen haben) + other_than: darf nicht gleich %{count} sein + template: + body: 'Bitte überprüfen Sie die folgenden Felder:' + header: + one: 'Konnte %{model} nicht speichern: ein Fehler.' + other: 'Konnte %{model} nicht speichern: %{count} Fehler.' + helpers: + select: + prompt: Bitte wählen + submit: + create: "%{model} erstellen" + submit: "%{model} speichern" + update: "%{model} aktualisieren" + number: + currency: + format: + delimiter: "." + format: "%n %u" + precision: 2 + separator: "," + significant: false + strip_insignificant_zeros: false + unit: "€" + format: + delimiter: "." + precision: 2 + separator: "," + significant: false + strip_insignificant_zeros: false + human: + decimal_units: + format: "%n %u" + units: + billion: + one: Milliarde + other: Milliarden + million: + one: Million + other: Millionen + quadrillion: + one: Billiarde + other: Billiarden + thousand: Tausend + trillion: + one: Billion + other: Billionen + unit: '' + format: + delimiter: '' + precision: 3 + significant: true + strip_insignificant_zeros: true + storage_units: + format: "%n %u" + units: + byte: + one: Byte + other: Bytes + gb: GB + kb: KB + mb: MB + tb: TB + percentage: + format: + delimiter: '' + format: "%n %" + precision: + format: + delimiter: '' + support: + array: + last_word_connector: " und " + two_words_connector: " und " + words_connector: ", " + time: + am: vormittags + formats: + default: "%A, %d. %B %Y, %H:%M Uhr" + long: "%A, %d. %B %Y, %H:%M Uhr" + short: "%d. %B, %H:%M Uhr" + pm: nachmittags diff --git a/config/locales/es.yml b/config/locales/es.yml new file mode 100644 index 00000000000..87e79beee74 --- /dev/null +++ b/config/locales/es.yml @@ -0,0 +1,217 @@ +--- +es: + activerecord: + errors: + messages: + record_invalid: "La validación falló: %{errors}" + restrict_dependent_destroy: + has_one: No se puede eliminar el registro porque existe un %{record} dependiente + has_many: No se puede eliminar el registro porque existen %{record} dependientes + date: + abbr_day_names: + - dom + - lun + - mar + - mié + - jue + - vie + - sáb + abbr_month_names: + - + - ene + - feb + - mar + - abr + - may + - jun + - jul + - ago + - sep + - oct + - nov + - dic + day_names: + - domingo + - lunes + - martes + - miércoles + - jueves + - viernes + - sábado + formats: + default: "%d/%m/%Y" + long: "%d de %B de %Y" + short: "%d de %b" + month_names: + - + - enero + - febrero + - marzo + - abril + - mayo + - junio + - julio + - agosto + - septiembre + - octubre + - noviembre + - diciembre + order: + - :day + - :month + - :year + datetime: + distance_in_words: + about_x_hours: + one: alrededor de 1 hora + other: alrededor de %{count} horas + about_x_months: + one: alrededor de 1 mes + other: alrededor de %{count} meses + about_x_years: + one: alrededor de 1 año + other: alrededor de %{count} años + almost_x_years: + one: casi 1 año + other: casi %{count} años + half_a_minute: medio minuto + less_than_x_minutes: + one: menos de 1 minuto + other: menos de %{count} minutos + less_than_x_seconds: + one: menos de 1 segundo + other: menos de %{count} segundos + over_x_years: + one: más de 1 año + other: más de %{count} años + x_days: + one: 1 dÃa + other: "%{count} dÃas" + x_minutes: + one: 1 minuto + other: "%{count} minutos" + x_months: + one: 1 mes + other: "%{count} meses" + x_years: + one: 1 año + other: "%{count} años" + x_seconds: + one: 1 segundo + other: "%{count} segundos" + prompts: + day: DÃa + hour: Hora + minute: Minutos + month: Mes + second: Segundos + year: Año + errors: + format: "%{attribute} %{message}" + messages: + accepted: debe ser aceptado + blank: no puede estar en blanco + present: debe estar en blanco + confirmation: no coincide + empty: no puede estar vacÃo + equal_to: debe ser igual a %{count} + even: debe ser par + exclusion: está reservado + greater_than: debe ser mayor que %{count} + greater_than_or_equal_to: debe ser mayor que o igual a %{count} + inclusion: no está incluido en la lista + invalid: no es válido + less_than: debe ser menor que %{count} + less_than_or_equal_to: debe ser menor que o igual a %{count} + model_invalid: "La validación falló: %{errors}" + not_a_number: no es un número + not_an_integer: debe ser un entero + odd: debe ser impar + required: debe existir + taken: ya está en uso + too_long: + one: "es demasiado largo (1 carácter máximo)" + other: "es demasiado largo (%{count} caracteres máximo)" + too_short: + one: "es demasiado corto (1 carácter mÃnimo)" + other: "es demasiado corto (%{count} caracteres mÃnimo)" + wrong_length: + one: "no tiene la longitud correcta (1 carácter exactos)" + other: "no tiene la longitud correcta (%{count} caracteres exactos)" + other_than: debe ser distinto de %{count} + template: + body: 'Se encontraron problemas con los siguientes campos:' + header: + one: No se pudo guardar este/a %{model} porque se encontró 1 error + other: No se pudo guardar este/a %{model} porque se encontraron %{count} errores + helpers: + select: + prompt: Por favor seleccione + submit: + create: Crear %{model} + submit: Guardar %{model} + update: Actualizar %{model} + number: + currency: + format: + delimiter: "." + format: "%n %u" + precision: 2 + separator: "," + significant: false + strip_insignificant_zeros: false + unit: "€" + format: + delimiter: "." + precision: 3 + separator: "," + significant: false + strip_insignificant_zeros: false + human: + decimal_units: + format: "%n %u" + units: + billion: mil millones + million: + one: millón + other: millones + quadrillion: mil billones + thousand: mil + trillion: + one: billón + other: billones + unit: '' + format: + delimiter: '' + precision: 1 + significant: true + strip_insignificant_zeros: true + storage_units: + format: "%n %u" + units: + byte: + one: Byte + other: Bytes + gb: GB + kb: KB + mb: MB + tb: TB + percentage: + format: + delimiter: '' + format: "%n %" + precision: + format: + delimiter: '' + support: + array: + last_word_connector: " y " + two_words_connector: " y " + words_connector: ", " + time: + am: am + formats: + default: "%A, %d de %B de %Y %H:%M:%S %z" + long: "%d de %B de %Y %H:%M" + short: "%d de %b %H:%M" + pm: pm diff --git a/config/webpack.config.js b/config/webpack.config.js index 239bb5ec436..3abdb6c2d76 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -35,7 +35,9 @@ var config = { group: './group.js', groups_list: './groups_list.js', issuable: './issuable/issuable_bundle.js', + locale: './locale/index.js', issue_show: './issue_show/index.js', + locale: './locale/index.js', main: './main.js', merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js', merge_request_widget: './merge_request_widget/ci_bundle.js', @@ -88,6 +90,10 @@ var config = { exclude: /node_modules/, loader: 'file-loader', }, + { + test: /locale\/[a-z]+\/(.*)\.js$/, + loader: 'exports-loader?locales', + }, ] }, @@ -153,6 +159,14 @@ var config = { new webpack.optimize.CommonsChunkPlugin({ names: ['main', 'common', 'runtime'], }), + + // locale common library + new webpack.optimize.CommonsChunkPlugin({ + name: 'locale', + chunks: [ + 'cycle_analytics', + ], + }), ], resolve: { diff --git a/db/migrate/20170413035209_add_preferred_language_to_users.rb b/db/migrate/20170413035209_add_preferred_language_to_users.rb new file mode 100644 index 00000000000..92f1d6f2436 --- /dev/null +++ b/db/migrate/20170413035209_add_preferred_language_to_users.rb @@ -0,0 +1,16 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddPreferredLanguageToUsers < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + add_column :users, :preferred_language, :string + end + + def down + remove_column :users, :preferred_language + end +end diff --git a/db/migrate/20170503004125_add_last_repository_updated_at_to_projects.rb b/db/migrate/20170503004125_add_last_repository_updated_at_to_projects.rb new file mode 100644 index 00000000000..00c685cf342 --- /dev/null +++ b/db/migrate/20170503004125_add_last_repository_updated_at_to_projects.rb @@ -0,0 +1,7 @@ +class AddLastRepositoryUpdatedAtToProjects < ActiveRecord::Migration + DOWNTIME = false + + def change + add_column :projects, :last_repository_updated_at, :datetime + end +end diff --git a/db/migrate/20170503004425_add_index_to_last_repository_updated_at_on_projects.rb b/db/migrate/20170503004425_add_index_to_last_repository_updated_at_on_projects.rb new file mode 100644 index 00000000000..6144d74745c --- /dev/null +++ b/db/migrate/20170503004425_add_index_to_last_repository_updated_at_on_projects.rb @@ -0,0 +1,15 @@ +class AddIndexToLastRepositoryUpdatedAtOnProjects < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index(:projects, :last_repository_updated_at) + end + + def down + remove_concurrent_index(:projects, :last_repository_updated_at) if index_exists?(:projects, :last_repository_updated_at) + end +end diff --git a/db/schema.rb b/db/schema.rb index 01c0f00c924..132549ab34c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170502091007) do +ActiveRecord::Schema.define(version: 20170503004425) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -971,6 +971,7 @@ ActiveRecord::Schema.define(version: 20170502091007) do t.boolean "printing_merge_request_link_enabled", default: true, null: false t.string "import_jid" t.integer "cached_markdown_version" + t.datetime "last_repository_updated_at" end add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree @@ -979,6 +980,7 @@ ActiveRecord::Schema.define(version: 20170502091007) do add_index "projects", ["description"], name: "index_projects_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree add_index "projects", ["last_repository_check_failed"], name: "index_projects_on_last_repository_check_failed", using: :btree + add_index "projects", ["last_repository_updated_at"], name: "index_projects_on_last_repository_updated_at", using: :btree add_index "projects", ["name"], name: "index_projects_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"} add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree add_index "projects", ["path"], name: "index_projects_on_path", using: :btree @@ -1326,6 +1328,7 @@ ActiveRecord::Schema.define(version: 20170502091007) do t.boolean "notified_of_own_activity" t.boolean "require_two_factor_authentication_from_group", default: false, null: false t.integer "two_factor_grace_period", default: 48, null: false + t.string "preferred_language" end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree diff --git a/doc/development/README.md b/doc/development/README.md index 77bb0263374..d04380e5b33 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -41,6 +41,7 @@ - [Shell commands](shell_commands.md) in the GitLab codebase - [Sidekiq debugging](sidekiq_debugging.md) - [Object state models](object_state_models.md) +- [Building a package for testing purposes](build_test_package.md) ## Databases diff --git a/doc/development/build_test_package.md b/doc/development/build_test_package.md new file mode 100644 index 00000000000..2bc1a700844 --- /dev/null +++ b/doc/development/build_test_package.md @@ -0,0 +1,35 @@ +# Building a package for testing + +While developing a new feature or modifying an existing one, it is helpful if an +installable package (or a docker image) containing those changes is available +for testing. For this very purpose, a manual job is provided in the GitLab CI/CD +pipeline that can be used to trigger a pipeline in the omnibus-gitlab repository +that will create +1. A deb package for Ubuntu 16.04, available as a build artifact, and +2. A docker image, which is pushed to [Omnibus GitLab's container +registry](https://gitlab.com/gitlab-org/omnibus-gitlab/container_registry) +(images titled `gitlab-ce` and `gitlab-ee` respectively and image tag is the +commit which triggered the pipeline). + +When you push a commit to either the gitlab-ce or gitlab-ee project, the +pipeline for that commit will have a `build-package` manual action you can +trigger. + +## Specifying versions of components + +If you want to create a package from a specific branch, commit or tag of any of +the GitLab components (like GitLab Workhorse, Gitaly, GitLab Pages, etc.), you +can specify the branch name, commit sha or tag in the component's respective +`*_VERSION` file. For example, if you want to build a package that uses the +branch `0-1-stable`, modify the content of `GITALY_SERVER_VERSION` to +`0-1-stable` and push the commit. This will create a manual job that can be +used to trigger the build. + +## Specifying the branch in omnibus-gitlab repository + +In scenarios where a configuration change is to be introduced and omnibus-gitlab +repository already has the necessary changes in a specific branch, you can build +a package against that branch through an environment variable named +`OMNIBUS_BRANCH`. To do this, specify that environment variable with the name of +the branch as value in `.gitlab-ci.yml` and push a commit. This will create a +manual job that can be used to trigger the build. diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md index 1d2b0558948..d2d89517241 100644 --- a/doc/development/fe_guide/style_guide_js.md +++ b/doc/development/fe_guide/style_guide_js.md @@ -11,207 +11,205 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns. #### ESlint -- **Never** disable eslint rules unless you have a good reason. You may see a lot of legacy files with `/* eslint-disable some-rule, some-other-rule */` at the top, but legacy files are a special case. Any time you develop a new feature or refactor an existing one, you should abide by the eslint rules. - -- **Never Ever EVER** disable eslint globally for a file +1. **Never** disable eslint rules unless you have a good reason. +You may see a lot of legacy files with `/* eslint-disable some-rule, some-other-rule */` +at the top, but legacy files are a special case. Any time you develop a new feature or +refactor an existing one, you should abide by the eslint rules. +1. **Never Ever EVER** disable eslint globally for a file ```javascript - // bad - /* eslint-disable */ + // bad + /* eslint-disable */ - // better - /* eslint-disable some-rule, some-other-rule */ + // better + /* eslint-disable some-rule, some-other-rule */ - // best - // nothing :) + // best + // nothing :) ``` -- If you do need to disable a rule for a single violation, try to do it as locally as possible - +1. If you do need to disable a rule for a single violation, try to do it as locally as possible ```javascript - // bad - /* eslint-disable no-new */ + // bad + /* eslint-disable no-new */ - import Foo from 'foo'; + import Foo from 'foo'; - new Foo(); + new Foo(); - // better - import Foo from 'foo'; + // better + import Foo from 'foo'; - // eslint-disable-next-line no-new - new Foo(); + // eslint-disable-next-line no-new + new Foo(); ``` +1. There are few rules that we need to disable due to technical debt. Which are: + 1. [no-new][eslint-new] + 1. [class-methods-use-this][eslint-this] -- When they are needed _always_ place ESlint directive comment blocks on the first line of a script, followed by any global declarations, then a blank newline prior to any imports or code. - +1. When they are needed _always_ place ESlint directive comment blocks on the first line of a script, +followed by any global declarations, then a blank newline prior to any imports or code. ```javascript - // bad - /* global Foo */ - /* eslint-disable no-new */ - import Bar from './bar'; + // bad + /* global Foo */ + /* eslint-disable no-new */ + import Bar from './bar'; - // good - /* eslint-disable no-new */ - /* global Foo */ + // good + /* eslint-disable no-new */ + /* global Foo */ - import Bar from './bar'; + import Bar from './bar'; ``` -- **Never** disable the `no-undef` rule. Declare globals with `/* global Foo */` instead. - -- When declaring multiple globals, always use one `/* global [name] */` line per variable. +1. **Never** disable the `no-undef` rule. Declare globals with `/* global Foo */` instead. +1. When declaring multiple globals, always use one `/* global [name] */` line per variable. ```javascript - // bad - /* globals Flash, Cookies, jQuery */ + // bad + /* globals Flash, Cookies, jQuery */ - // good - /* global Flash */ - /* global Cookies */ - /* global jQuery */ + // good + /* global Flash */ + /* global Cookies */ + /* global jQuery */ ``` - -- Use up to 3 parameters for a function or class. If you need more accept an Object instead. +1. Use up to 3 parameters for a function or class. If you need more accept an Object instead. ```javascript - // bad - fn(p1, p2, p3, p4) {} + // bad + fn(p1, p2, p3, p4) {} - // good - fn(options) {} + // good + fn(options) {} ``` #### Modules, Imports, and Exports -- Use ES module syntax to import modules - +1. Use ES module syntax to import modules ```javascript - // bad - require('foo'); + // bad + require('foo'); - // good - import Foo from 'foo'; + // good + import Foo from 'foo'; - // bad - module.exports = Foo; + // bad + module.exports = Foo; - // good - export default Foo; + // good + export default Foo; ``` -- Relative paths - - Unless you are writing a test, always reference other scripts using relative paths instead of `~` +1. Relative paths: Unless you are writing a test, always reference other scripts using +relative paths instead of `~` + * In **app/assets/javascripts**: - In **app/assets/javascripts**: - ```javascript - // bad - import Foo from '~/foo' - - // good - import Foo from '../foo'; - ``` + ```javascript + // bad + import Foo from '~/foo' - In **spec/javascripts**: - ```javascript - // bad - import Foo from '../../app/assets/javascripts/foo' + // good + import Foo from '../foo'; + ``` + * In **spec/javascripts**: - // good - import Foo from '~/foo'; - ``` + ```javascript + // bad + import Foo from '../../app/assets/javascripts/foo' -- Avoid using IIFE. Although we have a lot of examples of files which wrap their contents in IIFEs (immediately-invoked function expressions), this is no longer necessary after the transition from Sprockets to webpack. Do not use them anymore and feel free to remove them when refactoring legacy code. + // good + import Foo from '~/foo'; + ``` -- Avoid adding to the global namespace. +1. Avoid using IIFE. Although we have a lot of examples of files which wrap their +contents in IIFEs (immediately-invoked function expressions), +this is no longer necessary after the transition from Sprockets to webpack. +Do not use them anymore and feel free to remove them when refactoring legacy code. +1. Avoid adding to the global namespace. ```javascript - // bad - window.MyClass = class { /* ... */ }; + // bad + window.MyClass = class { /* ... */ }; - // good - export default class MyClass { /* ... */ } + // good + export default class MyClass { /* ... */ } ``` -- Side effects are forbidden in any script which contains exports - +1. Side effects are forbidden in any script which contains exports ```javascript - // bad - export default class MyClass { /* ... */ } + // bad + export default class MyClass { /* ... */ } - document.addEventListener("DOMContentLoaded", function(event) { - new MyClass(); - } + document.addEventListener("DOMContentLoaded", function(event) { + new MyClass(); + } ``` #### Data Mutation and Pure functions -- Strive to write many small pure functions, and minimize where mutations occur. - +1. Strive to write many small pure functions, and minimize where mutations occur. ```javascript - // bad - const values = {foo: 1}; + // bad + const values = {foo: 1}; - function impureFunction(items) { - const bar = 1; + function impureFunction(items) { + const bar = 1; - items.foo = items.a * bar + 2; + items.foo = items.a * bar + 2; - return items.a; - } + return items.a; + } - const c = impureFunction(values); + const c = impureFunction(values); - // good - var values = {foo: 1}; + // good + var values = {foo: 1}; - function pureFunction (foo) { - var bar = 1; + function pureFunction (foo) { + var bar = 1; - foo = foo * bar + 2; + foo = foo * bar + 2; - return foo; - } + return foo; + } - var c = pureFunction(values.foo); + var c = pureFunction(values.foo); ``` -- Avoid constructors with side-effects +1. Avoid constructors with side-effects -- Prefer `.map`, `.reduce` or `.filter` over `.forEach` +1. Prefer `.map`, `.reduce` or `.filter` over `.forEach` A forEach will cause side effects, it will be mutating the array being iterated. Prefer using `.map`, `.reduce` or `.filter` - ```javascript - const users = [ { name: 'Foo' }, { name: 'Bar' } ]; + const users = [ { name: 'Foo' }, { name: 'Bar' } ]; - // bad - users.forEach((user, index) => { - user.id = index; - }); + // bad + users.forEach((user, index) => { + user.id = index; + }); - // good - const usersWithId = users.map((user, index) => { - return Object.assign({}, user, { id: index }); - }); + // good + const usersWithId = users.map((user, index) => { + return Object.assign({}, user, { id: index }); + }); ``` #### Parse Strings into Numbers -- `parseInt()` is preferable over `Number()` or `+` - +1. `parseInt()` is preferable over `Number()` or `+` ```javascript - // bad - +'10' // 10 + // bad + +'10' // 10 - // good - Number('10') // 10 + // good + Number('10') // 10 - // better - parseInt('10', 10); + // better + parseInt('10', 10); ``` #### CSS classes used for JavaScript -- If the class is being used in Javascript it needs to be prepend with `js-` +1. If the class is being used in Javascript it needs to be prepend with `js-` ```html // bad <button class="add-user"> @@ -226,234 +224,270 @@ A forEach will cause side effects, it will be mutating the array being iterated. ### Vue.js - #### Basic Rules -- Only include one Vue.js component per file. -- Export components as plain objects: - +1. The service has it's own file +1. The store has it's own file +1. Use a function in the bundle file to instantiate the Vue component: ```javascript - export default { - template: `<h1>I'm a component</h1> - } - ``` + // bad + class { + init() { + new Component({}) + } + } -#### Naming -- **Extensions**: Use `.vue` extension for Vue components. -- **Reference Naming**: Use PascalCase for Vue components and camelCase for their instances: + // good + document.addEventListener('DOMContentLoaded', () => new Vue({ + el: '#element', + components: { + componentName + }, + render: createElement => createElement('component-name'), + })); + ``` +1. Don not use a singleton for the service or the store ```javascript - // bad - import cardBoard from 'cardBoard'; + // bad + class Store { + constructor() { + if (!this.prototype.singleton) { + // do something + } + } + } - // good - import CardBoard from 'cardBoard' + // good + class Store { + constructor() { + // do something + } + } + ``` - // bad - components: { - CardBoard: CardBoard - }; +#### Naming +1. **Extensions**: Use `.vue` extension for Vue components. +1. **Reference Naming**: Use camelCase for their instances: + ```javascript + // good + import cardBoard from 'cardBoard' - // good - components: { - cardBoard: CardBoard - }; + components: { + cardBoard: + }; ``` -- **Props Naming:** -- Avoid using DOM component prop names. -- Use kebab-case instead of camelCase to provide props in templates. - +1. **Props Naming:** Avoid using DOM component prop names. +1. **Props Naming:** Use kebab-case instead of camelCase to provide props in templates. ```javascript - // bad - <component class="btn"> + // bad + <component class="btn"> - // good - <component css-class="btn"> + // good + <component css-class="btn"> - // bad - <component myProp="prop" /> + // bad + <component myProp="prop" /> - // good - <component my-prop="prop" /> -``` + // good + <component my-prop="prop" /> + ``` #### Alignment -- Follow these alignment styles for the template method: - +1. Follow these alignment styles for the template method: ```javascript - // bad - <component v-if="bar" - param="baz" /> + // bad + <component v-if="bar" + param="baz" /> - <button class="btn">Click me</button> + <button class="btn">Click me</button> - // good - <component - v-if="bar" - param="baz" - /> + // good + <component + v-if="bar" + param="baz" + /> - <button class="btn"> - Click me - </button> + <button class="btn"> + Click me + </button> - // if props fit in one line then keep it on the same line - <component bar="bar" /> + // if props fit in one line then keep it on the same line + <component bar="bar" /> ``` #### Quotes -- Always use double quotes `"` inside templates and single quotes `'` for all other JS. - +1. Always use double quotes `"` inside templates and single quotes `'` for all other JS. ```javascript - // bad - template: ` - <button :class='style'>Button</button> - ` - - // good - template: ` - <button :class="style">Button</button> - ` + // bad + template: ` + <button :class='style'>Button</button> + ` + + // good + template: ` + <button :class="style">Button</button> + ` ``` #### Props -- Props should be declared as an object - +1. Props should be declared as an object ```javascript - // bad - props: ['foo'] - - // good - props: { - foo: { - type: String, - required: false, - default: 'bar' + // bad + props: ['foo'] + + // good + props: { + foo: { + type: String, + required: false, + default: 'bar' + } } - } ``` -- Required key should always be provided when declaring a prop - +1. Required key should always be provided when declaring a prop ```javascript - // bad - props: { - foo: { - type: String, + // bad + props: { + foo: { + type: String, + } } - } - - // good - props: { - foo: { - type: String, - required: false, - default: 'bar' + + // good + props: { + foo: { + type: String, + required: false, + default: 'bar' + } } - } ``` -- Default key should always be provided if the prop is not required: - +1. Default key should always be provided if the prop is not required: ```javascript - // bad - props: { - foo: { - type: String, - required: false, + // bad + props: { + foo: { + type: String, + required: false, + } } - } - - // good - props: { - foo: { - type: String, - required: false, - default: 'bar' + + // good + props: { + foo: { + type: String, + required: false, + default: 'bar' + } } - } - // good - props: { - foo: { - type: String, - required: true + // good + props: { + foo: { + type: String, + required: true + } } - } ``` #### Data -- `data` method should always be a function +1. `data` method should always be a function ```javascript - // bad - data: { - foo: 'foo' - } - - // good - data() { - return { + // bad + data: { foo: 'foo' - }; - } + } + + // good + data() { + return { + foo: 'foo' + }; + } ``` #### Directives -- Shorthand `@` is preferable over `v-on` - +1. Shorthand `@` is preferable over `v-on` ```javascript - // bad - <component v-on:click="eventHandler"/> + // bad + <component v-on:click="eventHandler"/> - // good - <component @click="eventHandler"/> + // good + <component @click="eventHandler"/> ``` -- Shorthand `:` is preferable over `v-bind` - +1. Shorthand `:` is preferable over `v-bind` ```javascript - // bad - <component v-bind:class="btn"/> + // bad + <component v-bind:class="btn"/> - // good - <component :class="btn"/> + // good + <component :class="btn"/> ``` #### Closing tags -- Prefer self closing component tags - +1. Prefer self closing component tags ```javascript - // bad - <component></component> + // bad + <component></component> - // good - <component /> + // good + <component /> ``` #### Ordering -- Order for a Vue Component: +1. Order for a Vue Component: 1. `name` - 2. `props` - 3. `data` - 4. `components` - 5. `computedProps` - 6. `methods` - 7. lifecycle methods - 1. `beforeCreate` - 2. `created` - 3. `beforeMount` - 4. `mounted` - 5. `beforeUpdate` - 6. `updated` - 7. `activated` - 8. `deactivated` - 9. `beforeDestroy` - 10. `destroyed` - 8. `template` + 1. `props` + 1. `mixins` + 1. `data` + 1. `components` + 1. `computedProps` + 1. `methods` + 1. `beforeCreate` + 1. `created` + 1. `beforeMount` + 1. `mounted` + 1. `beforeUpdate` + 1. `updated` + 1. `activated` + 1. `deactivated` + 1. `beforeDestroy` + 1. `destroyed` + +#### Vue and Boostrap +1. Tooltips: Do not rely on `has-tooltip` class name for vue components + ```javascript + // bad + <span class="has-tooltip"> + Text + </span> + + // good + <span data-toggle="tooltip"> + Text + </span> + ``` + +1. Tooltips: When using a tooltip, include the tooltip mixin + +1. Don't change `data-original-title`. + ```javascript + // bad + <span data-original-title="tooltip text">Foo</span> + + // good + <span title="tooltip text">Foo</span> + + $('span').tooltip('fixTitle'); + ``` ## SCSS @@ -461,3 +495,5 @@ A forEach will cause side effects, it will be mutating the array being iterated. [airbnb-js-style-guide]: https://github.com/airbnb/javascript [eslintrc]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.eslintrc +[eslint-this]: http://eslint.org/docs/rules/class-methods-use-this +[eslint-new]: http://eslint.org/docs/rules/no-new diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md index 73d2ffc1bdc..a984bb6c94c 100644 --- a/doc/development/fe_guide/vue.md +++ b/doc/development/fe_guide/vue.md @@ -387,6 +387,10 @@ describe('Todos App', () => { }); }); ``` +#### Test the component's output +The main return value of a Vue component is the rendered output. In order to test the component we +need to test the rendered output. [Vue][vue-test] guide's to unit test show us exactly that: + ### Stubbing API responses [Vue Resource Interceptors][vue-resource-interceptor] allow us to add a interceptor with @@ -419,6 +423,16 @@ the response we need: }); ``` +1. Use `$.mount()` to mount the component +```javascript + // bad + new Component({ + el: document.createElement('div') + }); + + // good + new Component().$mount(); +``` [vue-docs]: http://vuejs.org/guide/index.html [issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards @@ -429,5 +443,6 @@ the response we need: [one-way-data-flow]: https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow [vue-resource-repo]: https://github.com/pagekit/vue-resource [vue-resource-interceptor]: https://github.com/pagekit/vue-resource/blob/develop/docs/http.md#interceptors +[vue-test]: https://vuejs.org/v2/guide/unit-testing.html [issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6 [flux]: https://facebook.github.io/flux diff --git a/lib/api/api.rb b/lib/api/api.rb index 1bf20f76ad6..302ccd3e532 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -44,6 +44,9 @@ module API end before { allow_access_with_scope :api } + before { Gitlab::I18n.set_locale(current_user) } + + after { Gitlab::I18n.reset_locale } rescue_from Gitlab::Access::AccessDeniedError do rack_response({ 'message' => '403 Forbidden' }.to_json, 403) diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb index 559e3939da6..cac31ea8cff 100644 --- a/lib/gitlab/cycle_analytics/base_stage.rb +++ b/lib/gitlab/cycle_analytics/base_stage.rb @@ -17,7 +17,7 @@ module Gitlab end def title - name.to_s.capitalize + raise NotImplementedError.new("Expected #{self.name} to implement title") end def median diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb index 1e52b6614a1..5f9dc9a4303 100644 --- a/lib/gitlab/cycle_analytics/code_stage.rb +++ b/lib/gitlab/cycle_analytics/code_stage.rb @@ -13,12 +13,16 @@ module Gitlab :code end + def title + s_('CycleAnalyticsStage|Code') + end + def legend - "Related Merge Requests" + _("Related Merge Requests") end def description - "Time until first merge request" + _("Time until first merge request") end end end diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb index 213994988a5..7b03811efb2 100644 --- a/lib/gitlab/cycle_analytics/issue_stage.rb +++ b/lib/gitlab/cycle_analytics/issue_stage.rb @@ -14,12 +14,16 @@ module Gitlab :issue end + def title + s_('CycleAnalyticsStage|Issue') + end + def legend - "Related Issues" + _("Related Issues") end def description - "Time before an issue gets scheduled" + _("Time before an issue gets scheduled") end end end diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb index 45d51d30ccc..1a0afb56b4f 100644 --- a/lib/gitlab/cycle_analytics/plan_stage.rb +++ b/lib/gitlab/cycle_analytics/plan_stage.rb @@ -14,12 +14,16 @@ module Gitlab :plan end + def title + s_('CycleAnalyticsStage|Plan') + end + def legend - "Related Commits" + _("Related Commits") end def description - "Time before an issue starts implementation" + _("Time before an issue starts implementation") end end end diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb index 9f387a02945..0fa8a65cb99 100644 --- a/lib/gitlab/cycle_analytics/production_stage.rb +++ b/lib/gitlab/cycle_analytics/production_stage.rb @@ -15,12 +15,16 @@ module Gitlab :production end + def title + s_('CycleAnalyticsStage|Production') + end + def legend - "Related Issues" + _("Related Issues") end def description - "From issue creation until deploy to production" + _("From issue creation until deploy to production") end def query diff --git a/lib/gitlab/cycle_analytics/review_stage.rb b/lib/gitlab/cycle_analytics/review_stage.rb index 4744be834de..cfbbdc43fd9 100644 --- a/lib/gitlab/cycle_analytics/review_stage.rb +++ b/lib/gitlab/cycle_analytics/review_stage.rb @@ -13,12 +13,16 @@ module Gitlab :review end + def title + s_('CycleAnalyticsStage|Review') + end + def legend - "Relative Merged Requests" + _("Related Merged Requests") end def description - "Time between merge request creation and merge/close" + _("Time between merge request creation and merge/close") end end end diff --git a/lib/gitlab/cycle_analytics/staging_stage.rb b/lib/gitlab/cycle_analytics/staging_stage.rb index 3cdbe04fbaf..d5684bb9201 100644 --- a/lib/gitlab/cycle_analytics/staging_stage.rb +++ b/lib/gitlab/cycle_analytics/staging_stage.rb @@ -14,12 +14,16 @@ module Gitlab :staging end + def title + s_('CycleAnalyticsStage|Staging') + end + def legend - "Relative Deployed Builds" + _("Related Deployed Jobs") end def description - "From merge request merge until deploy to production" + _("From merge request merge until deploy to production") end end end diff --git a/lib/gitlab/cycle_analytics/summary/base.rb b/lib/gitlab/cycle_analytics/summary/base.rb index 43fa3795e5c..a917ddccac7 100644 --- a/lib/gitlab/cycle_analytics/summary/base.rb +++ b/lib/gitlab/cycle_analytics/summary/base.rb @@ -8,7 +8,7 @@ module Gitlab end def title - self.class.name.demodulize + raise NotImplementedError.new("Expected #{self.name} to implement title") end def value diff --git a/lib/gitlab/cycle_analytics/summary/commit.rb b/lib/gitlab/cycle_analytics/summary/commit.rb index 7b8faa4d854..bea78862757 100644 --- a/lib/gitlab/cycle_analytics/summary/commit.rb +++ b/lib/gitlab/cycle_analytics/summary/commit.rb @@ -2,6 +2,10 @@ module Gitlab module CycleAnalytics module Summary class Commit < Base + def title + n_('Commit', 'Commits', value) + end + def value @value ||= count_commits end diff --git a/lib/gitlab/cycle_analytics/summary/deploy.rb b/lib/gitlab/cycle_analytics/summary/deploy.rb index 06032e9200e..099d798aac6 100644 --- a/lib/gitlab/cycle_analytics/summary/deploy.rb +++ b/lib/gitlab/cycle_analytics/summary/deploy.rb @@ -2,6 +2,10 @@ module Gitlab module CycleAnalytics module Summary class Deploy < Base + def title + n_('Deploy', 'Deploys', value) + end + def value @value ||= @project.deployments.where("created_at > ?", @from).count end diff --git a/lib/gitlab/cycle_analytics/summary/issue.rb b/lib/gitlab/cycle_analytics/summary/issue.rb index 008468f24b9..9bbf7a2685f 100644 --- a/lib/gitlab/cycle_analytics/summary/issue.rb +++ b/lib/gitlab/cycle_analytics/summary/issue.rb @@ -9,7 +9,7 @@ module Gitlab end def title - 'New Issue' + n_('New Issue', 'New Issues', value) end def value diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb index e96943833bc..2b5f72bef89 100644 --- a/lib/gitlab/cycle_analytics/test_stage.rb +++ b/lib/gitlab/cycle_analytics/test_stage.rb @@ -13,12 +13,16 @@ module Gitlab :test end + def title + s_('CycleAnalyticsStage|Test') + end + def legend - "Relative Builds Trigger by Commits" + _("Related Jobs") end def description - "Total test time for all commits/merges" + _("Total test time for all commits/merges") end def stage_query diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb new file mode 100644 index 00000000000..3411516319f --- /dev/null +++ b/lib/gitlab/i18n.rb @@ -0,0 +1,26 @@ +module Gitlab + module I18n + extend self + + AVAILABLE_LANGUAGES = { + 'en' => 'English', + 'es' => 'Español', + 'de' => 'Deutsch' + }.freeze + + def available_locales + AVAILABLE_LANGUAGES.keys + end + + def set_locale(current_user) + requested_locale = current_user&.preferred_language || ::I18n.default_locale + locale = FastGettext.set_locale(requested_locale) + ::I18n.locale = locale + end + + def reset_locale + FastGettext.set_locale(::I18n.default_locale) + ::I18n.locale = ::I18n.default_locale + end + end +end diff --git a/lib/gitlab/slash_commands/command_definition.rb b/lib/gitlab/slash_commands/command_definition.rb index 60d35be2599..12a385f90fd 100644 --- a/lib/gitlab/slash_commands/command_definition.rb +++ b/lib/gitlab/slash_commands/command_definition.rb @@ -1,16 +1,19 @@ module Gitlab module SlashCommands class CommandDefinition - attr_accessor :name, :aliases, :description, :params, :condition_block, :action_block + attr_accessor :name, :aliases, :description, :explanation, :params, + :condition_block, :parse_params_block, :action_block def initialize(name, attributes = {}) @name = name - @aliases = attributes[:aliases] || [] - @description = attributes[:description] || '' - @params = attributes[:params] || [] + @aliases = attributes[:aliases] || [] + @description = attributes[:description] || '' + @explanation = attributes[:explanation] || '' + @params = attributes[:params] || [] @condition_block = attributes[:condition_block] - @action_block = attributes[:action_block] + @parse_params_block = attributes[:parse_params_block] + @action_block = attributes[:action_block] end def all_names @@ -28,14 +31,20 @@ module Gitlab context.instance_exec(&condition_block) end + def explain(context, opts, arg) + return unless available?(opts) + + if explanation.respond_to?(:call) + execute_block(explanation, context, arg) + else + explanation + end + end + def execute(context, opts, arg) return if noop? || !available?(opts) - if arg.present? - context.instance_exec(arg, &action_block) - elsif action_block.arity == 0 - context.instance_exec(&action_block) - end + execute_block(action_block, context, arg) end def to_h(opts) @@ -52,6 +61,23 @@ module Gitlab params: params } end + + private + + def execute_block(block, context, arg) + if arg.present? + parsed = parse_params(arg, context) + context.instance_exec(parsed, &block) + elsif block.arity == 0 + context.instance_exec(&block) + end + end + + def parse_params(arg, context) + return arg unless parse_params_block + + context.instance_exec(arg, &parse_params_block) + end end end end diff --git a/lib/gitlab/slash_commands/dsl.rb b/lib/gitlab/slash_commands/dsl.rb index 50b0937d267..614bafbe1b2 100644 --- a/lib/gitlab/slash_commands/dsl.rb +++ b/lib/gitlab/slash_commands/dsl.rb @@ -44,6 +44,22 @@ module Gitlab @params = params end + # Allows to give an explanation of what the command will do when + # executed. This explanation is shown when rendering the Markdown + # preview. + # + # Example: + # + # explanation do |arguments| + # "Adds label(s) #{arguments.join(' ')}" + # end + # command :command_key do |arguments| + # # Awesome code block + # end + def explanation(text = '', &block) + @explanation = block_given? ? block : text + end + # Allows to define conditions that must be met in order for the command # to be returned by `.command_names` & `.command_definitions`. # It accepts a block that will be evaluated with the context given to @@ -61,6 +77,24 @@ module Gitlab @condition_block = block end + # Allows to perform initial parsing of parameters. The result is passed + # both to `command` and `explanation` blocks, instead of the raw + # parameters. + # It accepts a block that will be evaluated with the context given to + # `CommandDefintion#to_h`. + # + # Example: + # + # parse_params do |raw| + # raw.strip + # end + # command :command_key do |parsed| + # # Awesome code block + # end + def parse_params(&block) + @parse_params_block = block + end + # Registers a new command which is recognizeable from body of email or # comment. # It accepts aliases and takes a block. @@ -75,11 +109,13 @@ module Gitlab definition = CommandDefinition.new( name, - aliases: aliases, - description: @description, - params: @params, - condition_block: @condition_block, - action_block: block + aliases: aliases, + description: @description, + explanation: @explanation, + params: @params, + condition_block: @condition_block, + parse_params_block: @parse_params_block, + action_block: block ) self.command_definitions << definition @@ -89,8 +125,14 @@ module Gitlab end @description = nil + @explanation = nil @params = nil @condition_block = nil + @parse_params_block = nil + end + + def definition_by_name(name) + command_definitions_by_name[name.to_sym] end end end diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake new file mode 100644 index 00000000000..0aa21a4bd13 --- /dev/null +++ b/lib/tasks/gettext.rake @@ -0,0 +1,14 @@ +require "gettext_i18n_rails/tasks" + +namespace :gettext do + # Customize list of translatable files + # See: https://github.com/grosser/gettext_i18n_rails#customizing-list-of-translatable-files + def files_to_translate + folders = %W(app lib config #{locale_path}).join(',') + exts = %w(rb erb haml slim rhtml js jsx vue coffee handlebars hbs mustache).join(',') + + Dir.glob( + "{#{folders}}/**/*.{#{exts}}" + ) + end +end diff --git a/locale/de/gitlab.po b/locale/de/gitlab.po new file mode 100644 index 00000000000..b804dc0436f --- /dev/null +++ b/locale/de/gitlab.po @@ -0,0 +1,207 @@ +# German translations for gitlab package. +# Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gitlab package. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2017. +# +msgid "" +msgstr "" +"Project-Id-Version: gitlab 1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2017-04-12 22:37-0500\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: German\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"\n" + +msgid "ByAuthor|by" +msgstr "" + +msgid "Commit" +msgid_plural "Commits" +msgstr[0] "" +msgstr[1] "" + +msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project." +msgstr "" + +msgid "CycleAnalyticsStage|Code" +msgstr "" + +msgid "CycleAnalyticsStage|Issue" +msgstr "" + +msgid "CycleAnalyticsStage|Plan" +msgstr "" + +msgid "CycleAnalyticsStage|Production" +msgstr "" + +msgid "CycleAnalyticsStage|Review" +msgstr "" + +msgid "CycleAnalyticsStage|Staging" +msgstr "" + +msgid "CycleAnalyticsStage|Test" +msgstr "" + +msgid "Deploy" +msgid_plural "Deploys" +msgstr[0] "" +msgstr[1] "" + +msgid "FirstPushedBy|First" +msgstr "" + +msgid "FirstPushedBy|pushed by" +msgstr "" + +msgid "From issue creation until deploy to production" +msgstr "" + +msgid "From merge request merge until deploy to production" +msgstr "" + +msgid "Introducing Cycle Analytics" +msgstr "" + +msgid "Last %d day" +msgid_plural "Last %d days" +msgstr[0] "" +msgstr[1] "" + +msgid "Limited to showing %d event at most" +msgid_plural "Limited to showing %d events at most" +msgstr[0] "" +msgstr[1] "" + +msgid "Median" +msgstr "" + +msgid "New Issue" +msgid_plural "New Issues" +msgstr[0] "" +msgstr[1] "" + +msgid "Not available" +msgstr "" + +msgid "Not enough data" +msgstr "" + +msgid "OpenedNDaysAgo|Opened" +msgstr "" + +msgid "Pipeline Health" +msgstr "" + +msgid "ProjectLifecycle|Stage" +msgstr "" + +msgid "Read more" +msgstr "" + +msgid "Related Commits" +msgstr "" + +msgid "Related Deployed Jobs" +msgstr "" + +msgid "Related Issues" +msgstr "" + +msgid "Related Jobs" +msgstr "" + +msgid "Related Merge Requests" +msgstr "" + +msgid "Related Merged Requests" +msgstr "" + +msgid "Showing %d event" +msgid_plural "Showing %d events" +msgstr[0] "" +msgstr[1] "" + +msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." +msgstr "" + +msgid "The collection of events added to the data gathered for that stage." +msgstr "" + +msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage." +msgstr "" + +msgid "The phase of the development lifecycle." +msgstr "" + +msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit." +msgstr "" + +msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle." +msgstr "" + +msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request." +msgstr "" + +msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time." +msgstr "" + +msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running." +msgstr "" + +msgid "The time taken by each data entry gathered by that stage." +msgstr "" + +msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6." +msgstr "" + +msgid "Time before an issue gets scheduled" +msgstr "" + +msgid "Time before an issue starts implementation" +msgstr "" + +msgid "Time between merge request creation and merge/close" +msgstr "" + +msgid "Time until first merge request" +msgstr "" + +msgid "Time|hr" +msgid_plural "Time|hrs" +msgstr[0] "" +msgstr[1] "" + +msgid "Time|min" +msgid_plural "Time|mins" +msgstr[0] "" +msgstr[1] "" + +msgid "Time|s" +msgstr "" + +msgid "Total Time" +msgstr "" + +msgid "Total test time for all commits/merges" +msgstr "" + +msgid "Want to see the data? Please ask an administrator for access." +msgstr "" + +msgid "We don't have enough data to show this stage." +msgstr "" + +msgid "You need permission." +msgstr "" + +msgid "day" +msgid_plural "days" +msgstr[0] "" +msgstr[1] "" diff --git a/locale/de/gitlab.po.time_stamp b/locale/de/gitlab.po.time_stamp new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/locale/de/gitlab.po.time_stamp diff --git a/locale/en/gitlab.po b/locale/en/gitlab.po new file mode 100644 index 00000000000..a43bafbbe28 --- /dev/null +++ b/locale/en/gitlab.po @@ -0,0 +1,207 @@ +# English translations for gitlab package. +# Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gitlab package. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2017. +# +msgid "" +msgstr "" +"Project-Id-Version: gitlab 1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2017-04-12 22:36-0500\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: English\n" +"Language: en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"\n" + +msgid "ByAuthor|by" +msgstr "" + +msgid "Commit" +msgid_plural "Commits" +msgstr[0] "" +msgstr[1] "" + +msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project." +msgstr "" + +msgid "CycleAnalyticsStage|Code" +msgstr "" + +msgid "CycleAnalyticsStage|Issue" +msgstr "" + +msgid "CycleAnalyticsStage|Plan" +msgstr "" + +msgid "CycleAnalyticsStage|Production" +msgstr "" + +msgid "CycleAnalyticsStage|Review" +msgstr "" + +msgid "CycleAnalyticsStage|Staging" +msgstr "" + +msgid "CycleAnalyticsStage|Test" +msgstr "" + +msgid "Deploy" +msgid_plural "Deploys" +msgstr[0] "" +msgstr[1] "" + +msgid "FirstPushedBy|First" +msgstr "" + +msgid "FirstPushedBy|pushed by" +msgstr "" + +msgid "From issue creation until deploy to production" +msgstr "" + +msgid "From merge request merge until deploy to production" +msgstr "" + +msgid "Introducing Cycle Analytics" +msgstr "" + +msgid "Last %d day" +msgid_plural "Last %d days" +msgstr[0] "" +msgstr[1] "" + +msgid "Limited to showing %d event at most" +msgid_plural "Limited to showing %d events at most" +msgstr[0] "" +msgstr[1] "" + +msgid "Median" +msgstr "" + +msgid "New Issue" +msgid_plural "New Issues" +msgstr[0] "" +msgstr[1] "" + +msgid "Not available" +msgstr "" + +msgid "Not enough data" +msgstr "" + +msgid "OpenedNDaysAgo|Opened" +msgstr "" + +msgid "Pipeline Health" +msgstr "" + +msgid "ProjectLifecycle|Stage" +msgstr "" + +msgid "Read more" +msgstr "" + +msgid "Related Commits" +msgstr "" + +msgid "Related Deployed Jobs" +msgstr "" + +msgid "Related Issues" +msgstr "" + +msgid "Related Jobs" +msgstr "" + +msgid "Related Merge Requests" +msgstr "" + +msgid "Related Merged Requests" +msgstr "" + +msgid "Showing %d event" +msgid_plural "Showing %d events" +msgstr[0] "" +msgstr[1] "" + +msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." +msgstr "" + +msgid "The collection of events added to the data gathered for that stage." +msgstr "" + +msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage." +msgstr "" + +msgid "The phase of the development lifecycle." +msgstr "" + +msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit." +msgstr "" + +msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle." +msgstr "" + +msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request." +msgstr "" + +msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time." +msgstr "" + +msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running." +msgstr "" + +msgid "The time taken by each data entry gathered by that stage." +msgstr "" + +msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6." +msgstr "" + +msgid "Time before an issue gets scheduled" +msgstr "" + +msgid "Time before an issue starts implementation" +msgstr "" + +msgid "Time between merge request creation and merge/close" +msgstr "" + +msgid "Time until first merge request" +msgstr "" + +msgid "Time|hr" +msgid_plural "Time|hrs" +msgstr[0] "" +msgstr[1] "" + +msgid "Time|min" +msgid_plural "Time|mins" +msgstr[0] "" +msgstr[1] "" + +msgid "Time|s" +msgstr "" + +msgid "Total Time" +msgstr "" + +msgid "Total test time for all commits/merges" +msgstr "" + +msgid "Want to see the data? Please ask an administrator for access." +msgstr "" + +msgid "We don't have enough data to show this stage." +msgstr "" + +msgid "You need permission." +msgstr "" + +msgid "day" +msgid_plural "days" +msgstr[0] "" +msgstr[1] "" diff --git a/locale/en/gitlab.po.time_stamp b/locale/en/gitlab.po.time_stamp new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/locale/en/gitlab.po.time_stamp diff --git a/locale/es/gitlab.po b/locale/es/gitlab.po new file mode 100644 index 00000000000..c14ddd3b94c --- /dev/null +++ b/locale/es/gitlab.po @@ -0,0 +1,208 @@ +# Spanish translations for gitlab package. +# Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gitlab package. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2017. +# +msgid "" +msgstr "" +"Project-Id-Version: gitlab 1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2017-05-04 19:24-0500\n" +"Language-Team: Spanish\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"Last-Translator: \n" +"X-Generator: Poedit 2.0.1\n" + +msgid "ByAuthor|by" +msgstr "por" + +msgid "Commit" +msgid_plural "Commits" +msgstr[0] "Cambio" +msgstr[1] "Cambios" + +msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project." +msgstr "Cycle Analytics ofrece una visión general de cuánto tiempo tarda en pasar de idea a producción en su proyecto." + +msgid "CycleAnalyticsStage|Code" +msgstr "Código" + +msgid "CycleAnalyticsStage|Issue" +msgstr "Incidencia" + +msgid "CycleAnalyticsStage|Plan" +msgstr "Planificación" + +msgid "CycleAnalyticsStage|Production" +msgstr "Producción" + +msgid "CycleAnalyticsStage|Review" +msgstr "Revisión" + +#, fuzzy +msgid "CycleAnalyticsStage|Staging" +msgstr "Puesta en escena" + +msgid "CycleAnalyticsStage|Test" +msgstr "Pruebas" + +msgid "Deploy" +msgid_plural "Deploys" +msgstr[0] "Despliegue" +msgstr[1] "Despliegues" + +msgid "FirstPushedBy|First" +msgstr "Primer" + +msgid "FirstPushedBy|pushed by" +msgstr "enviado por" + +msgid "From issue creation until deploy to production" +msgstr "Desde la creación de la incidencia hasta el despliegue a producción" + +msgid "From merge request merge until deploy to production" +msgstr "Desde la integración de la solicitud de fusión hasta el despliegue a producción" + +msgid "Introducing Cycle Analytics" +msgstr "Introducción a Cycle Analytics" + +msgid "Last %d day" +msgid_plural "Last %d days" +msgstr[0] "Último %d dÃa" +msgstr[1] "Últimos %d dÃas" + +msgid "Limited to showing %d event at most" +msgid_plural "Limited to showing %d events at most" +msgstr[0] "Limitado a mostrar máximo %d evento" +msgstr[1] "Limitado a mostrar máximo %d eventos" + +msgid "Median" +msgstr "Mediana" + +msgid "New Issue" +msgid_plural "New Issues" +msgstr[0] "Nueva incidencia" +msgstr[1] "Nuevas incidencias" + +msgid "Not available" +msgstr "No disponible" + +msgid "Not enough data" +msgstr "No hay suficientes datos" + +msgid "OpenedNDaysAgo|Opened" +msgstr "Abierto" + +msgid "Pipeline Health" +msgstr "Estado del Pipeline" + +msgid "ProjectLifecycle|Stage" +msgstr "Etapa" + +msgid "Read more" +msgstr "Leer más" + +msgid "Related Commits" +msgstr "Cambios Relacionados" + +msgid "Related Deployed Jobs" +msgstr "Trabajos Desplegados Relacionados" + +msgid "Related Issues" +msgstr "Incidencias Relacionadas" + +msgid "Related Jobs" +msgstr "Trabajos Relacionados" + +msgid "Related Merge Requests" +msgstr "Solicitudes de fusión Relacionadas" + +msgid "Related Merged Requests" +msgstr "Solicitudes de fusión Relacionadas" + +msgid "Showing %d event" +msgid_plural "Showing %d events" +msgstr[0] "Mostrando %d evento" +msgstr[1] "Mostrando %d eventos" + +msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." +msgstr "La etapa de codificación muestra el tiempo desde el primer cambio hasta la creación de la solicitud de fusión. Los datos serán automáticamente incorporados aquà una vez creada tu primera solicitud de fusión." + +msgid "The collection of events added to the data gathered for that stage." +msgstr "La colección de eventos agregados a los datos recopilados para esa etapa." + +msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage." +msgstr "La etapa de incidencia muestra el tiempo que toma desde la creación de un tema hasta asignar el tema a un hito, o añadir el tema a una lista en el panel de temas. Empieza a crear temas para ver los datos de esta etapa." + +msgid "The phase of the development lifecycle." +msgstr "La etapa del ciclo de vida de desarrollo." + +msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit." +msgstr "La etapa de planificación muestra el tiempo desde el paso anterior hasta el envÃo de tu primera confirmación. Este tiempo se añadirá automáticamente una vez que usted envÃe el primer cambio." + +msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle." +msgstr "La etapa de producción muestra el tiempo total que tarda entre la creación de una incidencia y el despliegue del código a producción. Los datos se añadirán automáticamente una vez haya finalizado por completo el ciclo de idea a producción." + +msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request." +msgstr "La etapa de revisión muestra el tiempo desde la creación de la solicitud de fusión hasta que los cambios se fusionaron. Los datos se añadirán automáticamente después de fusionar su primera solicitud de fusión." + +msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time." +msgstr "La etapa de puesta en escena muestra el tiempo entre la fusión y el despliegue de código en el entorno de producción. Los datos se añadirán automáticamente una vez que se despliega a producción por primera vez." + +msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running." +msgstr "La etapa de pruebas muestra el tiempo que GitLab CI toma para ejecutar cada pipeline para la solicitud de fusión relacionada. Los datos se añadirán automáticamente luego de que el primer pipeline termine de ejecutarse." + +msgid "The time taken by each data entry gathered by that stage." +msgstr "El tiempo utilizado por cada entrada de datos obtenido por esa etapa." + +msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6." +msgstr "El valor en el punto medio de una serie de valores observados. Por ejemplo, entre 3, 5, 9, la mediana es 5. Entre 3, 5, 7, 8, la mediana es (5 + 7) / 2 = 6." + +msgid "Time before an issue gets scheduled" +msgstr "Tiempo antes de que una incidencia sea programada" + +msgid "Time before an issue starts implementation" +msgstr "Tiempo antes de que empieze la implementación de una incidencia" + +msgid "Time between merge request creation and merge/close" +msgstr "Tiempo entre la creación de la solicitud de fusión y la integración o cierre de ésta" + +msgid "Time until first merge request" +msgstr "Tiempo hasta la primera solicitud de fusión" + +msgid "Time|hr" +msgid_plural "Time|hrs" +msgstr[0] "hr" +msgstr[1] "hrs" + +msgid "Time|min" +msgid_plural "Time|mins" +msgstr[0] "min" +msgstr[1] "mins" + +msgid "Time|s" +msgstr "s" + +msgid "Total Time" +msgstr "Tiempo Total" + +msgid "Total test time for all commits/merges" +msgstr "Tiempo total de pruebas para todos los cambios o integraciones" + +msgid "Want to see the data? Please ask an administrator for access." +msgstr "¿Quieres ver los datos? Por favor pide acceso al administrador." + +msgid "We don't have enough data to show this stage." +msgstr "No hay suficientes datos para mostrar en esta etapa." + +msgid "You need permission." +msgstr "Necesitas permisos." + +msgid "day" +msgid_plural "days" +msgstr[0] "dÃa" +msgstr[1] "dÃas" diff --git a/locale/es/gitlab.po.time_stamp b/locale/es/gitlab.po.time_stamp new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/locale/es/gitlab.po.time_stamp diff --git a/locale/gitlab.pot b/locale/gitlab.pot new file mode 100644 index 00000000000..3967d40ea9e --- /dev/null +++ b/locale/gitlab.pot @@ -0,0 +1,208 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gitlab package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: gitlab 1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-05-04 19:24-0500\n" +"PO-Revision-Date: 2017-05-04 19:24-0500\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +msgid "ByAuthor|by" +msgstr "" + +msgid "Commit" +msgid_plural "Commits" +msgstr[0] "" +msgstr[1] "" + +msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project." +msgstr "" + +msgid "CycleAnalyticsStage|Code" +msgstr "" + +msgid "CycleAnalyticsStage|Issue" +msgstr "" + +msgid "CycleAnalyticsStage|Plan" +msgstr "" + +msgid "CycleAnalyticsStage|Production" +msgstr "" + +msgid "CycleAnalyticsStage|Review" +msgstr "" + +msgid "CycleAnalyticsStage|Staging" +msgstr "" + +msgid "CycleAnalyticsStage|Test" +msgstr "" + +msgid "Deploy" +msgid_plural "Deploys" +msgstr[0] "" +msgstr[1] "" + +msgid "FirstPushedBy|First" +msgstr "" + +msgid "FirstPushedBy|pushed by" +msgstr "" + +msgid "From issue creation until deploy to production" +msgstr "" + +msgid "From merge request merge until deploy to production" +msgstr "" + +msgid "Introducing Cycle Analytics" +msgstr "" + +msgid "Last %d day" +msgid_plural "Last %d days" +msgstr[0] "" +msgstr[1] "" + +msgid "Limited to showing %d event at most" +msgid_plural "Limited to showing %d events at most" +msgstr[0] "" +msgstr[1] "" + +msgid "Median" +msgstr "" + +msgid "New Issue" +msgid_plural "New Issues" +msgstr[0] "" +msgstr[1] "" + +msgid "Not available" +msgstr "" + +msgid "Not enough data" +msgstr "" + +msgid "OpenedNDaysAgo|Opened" +msgstr "" + +msgid "Pipeline Health" +msgstr "" + +msgid "ProjectLifecycle|Stage" +msgstr "" + +msgid "Read more" +msgstr "" + +msgid "Related Commits" +msgstr "" + +msgid "Related Deployed Jobs" +msgstr "" + +msgid "Related Issues" +msgstr "" + +msgid "Related Jobs" +msgstr "" + +msgid "Related Merge Requests" +msgstr "" + +msgid "Related Merged Requests" +msgstr "" + +msgid "Showing %d event" +msgid_plural "Showing %d events" +msgstr[0] "" +msgstr[1] "" + +msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." +msgstr "" + +msgid "The collection of events added to the data gathered for that stage." +msgstr "" + +msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage." +msgstr "" + +msgid "The phase of the development lifecycle." +msgstr "" + +msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit." +msgstr "" + +msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle." +msgstr "" + +msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request." +msgstr "" + +msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time." +msgstr "" + +msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running." +msgstr "" + +msgid "The time taken by each data entry gathered by that stage." +msgstr "" + +msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6." +msgstr "" + +msgid "Time before an issue gets scheduled" +msgstr "" + +msgid "Time before an issue starts implementation" +msgstr "" + +msgid "Time between merge request creation and merge/close" +msgstr "" + +msgid "Time until first merge request" +msgstr "" + +msgid "Time|hr" +msgid_plural "Time|hrs" +msgstr[0] "" +msgstr[1] "" + +msgid "Time|min" +msgid_plural "Time|mins" +msgstr[0] "" +msgstr[1] "" + +msgid "Time|s" +msgstr "" + +msgid "Total Time" +msgstr "" + +msgid "Total test time for all commits/merges" +msgstr "" + +msgid "Want to see the data? Please ask an administrator for access." +msgstr "" + +msgid "We don't have enough data to show this stage." +msgstr "" + +msgid "You need permission." +msgstr "" + +msgid "day" +msgid_plural "days" +msgstr[0] "" +msgstr[1] "" diff --git a/package.json b/package.json index 9ed5e1a7475..63b2c190269 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,9 @@ "dropzone": "^4.2.0", "emoji-unicode-version": "^0.2.1", "eslint-plugin-html": "^2.0.1", + "exports-loader": "^0.6.4", "file-loader": "^0.11.1", + "jed": "^1.1.1", "jquery": "^2.2.1", "jquery-ujs": "^1.2.1", "js-cookie": "^2.1.3", diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb index b93275c330b..7c9d522273b 100644 --- a/spec/features/cycle_analytics_spec.rb +++ b/spec/features/cycle_analytics_spec.rb @@ -62,6 +62,25 @@ feature 'Cycle Analytics', feature: true, js: true do expect_issue_to_be_present end end + + context "when my preferred language is Spanish" do + before do + user.update_attribute(:preferred_language, 'es') + + project.team << [user, :master] + login_as(user) + visit namespace_project_cycle_analytics_path(project.namespace, project) + wait_for_ajax + end + + it 'shows the content in Spanish' do + expect(page).to have_content('Estado del Pipeline') + end + + it 'resets the language to English' do + expect(I18n.locale).to eq(:en) + end + end end context "as a guest" do diff --git a/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb b/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb new file mode 100644 index 00000000000..cfc782c98ad --- /dev/null +++ b/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe 'New Branch Ref Dropdown', :js, :feature do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:toggle) { find('.create-from .dropdown-toggle') } + + before do + project.add_master(user) + + login_as(user) + visit new_namespace_project_branch_path(project.namespace, project) + end + + it 'filters a list of branches and tags' do + toggle.click + + filter_by('v1.0.0') + + expect(items_count).to be(1) + + filter_by('video') + + expect(items_count).to be(1) + + find('.create-from .dropdown-content li').click + + expect(toggle).to have_content 'video' + end + + it 'accepts a manually entered commit SHA' do + toggle.click + + filter_by('somecommitsha') + + find('.create-from input[type=search]').send_keys(:enter) + + expect(toggle).to have_content 'somecommitsha' + end + + def items_count + all('.create-from .dropdown-content li').length + end + + def filter_by(filter_text) + fill_in 'Filter by Git revision', with: filter_text + end +end diff --git a/spec/javascripts/cycle_analytics/limit_warning_component_spec.js b/spec/javascripts/cycle_analytics/limit_warning_component_spec.js index 50000c5a5f5..2fb9eb0ca85 100644 --- a/spec/javascripts/cycle_analytics/limit_warning_component_spec.js +++ b/spec/javascripts/cycle_analytics/limit_warning_component_spec.js @@ -1,6 +1,9 @@ import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; import limitWarningComp from '~/cycle_analytics/components/limit_warning_component'; +Vue.use(Translate); + describe('Limit warning component', () => { let component; let LimitWarningComponent; diff --git a/spec/javascripts/vue_shared/translate_spec.js b/spec/javascripts/vue_shared/translate_spec.js new file mode 100644 index 00000000000..cbb3cbdff46 --- /dev/null +++ b/spec/javascripts/vue_shared/translate_spec.js @@ -0,0 +1,90 @@ +import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; + +Vue.use(Translate); + +describe('Vue translate filter', () => { + let el; + + beforeEach(() => { + el = document.createElement('div'); + + document.body.appendChild(el); + }); + + it('translate single text', (done) => { + const comp = new Vue({ + el, + template: ` + <span> + {{ __('testing') }} + </span> + `, + }).$mount(); + + Vue.nextTick(() => { + expect( + comp.$el.textContent.trim(), + ).toBe('testing'); + + done(); + }); + }); + + it('translate plural text with single count', (done) => { + const comp = new Vue({ + el, + template: ` + <span> + {{ n__('%d day', '%d days', 1) }} + </span> + `, + }).$mount(); + + Vue.nextTick(() => { + expect( + comp.$el.textContent.trim(), + ).toBe('1 day'); + + done(); + }); + }); + + it('translate plural text with multiple count', (done) => { + const comp = new Vue({ + el, + template: ` + <span> + {{ n__('%d day', '%d days', 2) }} + </span> + `, + }).$mount(); + + Vue.nextTick(() => { + expect( + comp.$el.textContent.trim(), + ).toBe('2 days'); + + done(); + }); + }); + + it('translate plural without replacing any text', (done) => { + const comp = new Vue({ + el, + template: ` + <span> + {{ n__('day', 'days', 2) }} + </span> + `, + }).$mount(); + + Vue.nextTick(() => { + expect( + comp.$el.textContent.trim(), + ).toBe('days'); + + done(); + }); + }); +}); diff --git a/spec/lib/gitlab/i18n_spec.rb b/spec/lib/gitlab/i18n_spec.rb new file mode 100644 index 00000000000..52f2614d5ca --- /dev/null +++ b/spec/lib/gitlab/i18n_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +module Gitlab + describe I18n, lib: true do + let(:user) { create(:user, preferred_language: 'es') } + + describe '.set_locale' do + it 'sets the locale based on current user preferred language' do + Gitlab::I18n.set_locale(user) + + expect(FastGettext.locale).to eq('es') + expect(::I18n.locale).to eq(:es) + end + end + + describe '.reset_locale' do + it 'resets the locale to the default language' do + Gitlab::I18n.set_locale(user) + + Gitlab::I18n.reset_locale + + expect(FastGettext.locale).to eq('en') + expect(::I18n.locale).to eq(:en) + end + end + end +end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index ebfaab4eacd..59c8b48a2be 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -351,6 +351,7 @@ Project: - auto_cancel_pending_pipelines - printing_merge_request_link_enabled - build_allow_git_fetch +- last_repository_updated_at Author: - name ProjectFeature: diff --git a/spec/lib/gitlab/slash_commands/command_definition_spec.rb b/spec/lib/gitlab/slash_commands/command_definition_spec.rb index c9c2f314e57..5b9173d3d3f 100644 --- a/spec/lib/gitlab/slash_commands/command_definition_spec.rb +++ b/spec/lib/gitlab/slash_commands/command_definition_spec.rb @@ -167,6 +167,58 @@ describe Gitlab::SlashCommands::CommandDefinition do end end end + + context 'when the command defines parse_params block' do + before do + subject.parse_params_block = ->(raw) { raw.strip } + subject.action_block = ->(parsed) { self.received_arg = parsed } + end + + it 'executes the command passing the parsed param' do + subject.execute(context, {}, 'something ') + + expect(context.received_arg).to eq('something') + end + end + end + end + end + + describe '#explain' do + context 'when the command is not available' do + before do + subject.condition_block = proc { false } + subject.explanation = 'Explanation' + end + + it 'returns nil' do + result = subject.explain({}, {}, nil) + + expect(result).to be_nil + end + end + + context 'when the explanation is a static string' do + before do + subject.explanation = 'Explanation' + end + + it 'returns this static string' do + result = subject.explain({}, {}, nil) + + expect(result).to eq 'Explanation' + end + end + + context 'when the explanation is dynamic' do + before do + subject.explanation = proc { |arg| "Dynamic #{arg}" } + end + + it 'invokes the proc' do + result = subject.explain({}, {}, 'explanation') + + expect(result).to eq 'Dynamic explanation' end end end diff --git a/spec/lib/gitlab/slash_commands/dsl_spec.rb b/spec/lib/gitlab/slash_commands/dsl_spec.rb index 2763d950716..33b49a5ddf9 100644 --- a/spec/lib/gitlab/slash_commands/dsl_spec.rb +++ b/spec/lib/gitlab/slash_commands/dsl_spec.rb @@ -11,67 +11,99 @@ describe Gitlab::SlashCommands::Dsl do end params 'The first argument' - command :one_arg, :once, :first do |arg1| - arg1 + explanation 'Static explanation' + command :explanation_with_aliases, :once, :first do |arg| + arg end desc do "A dynamic description for #{noteable.upcase}" end params 'The first argument', 'The second argument' - command :two_args do |arg1, arg2| - [arg1, arg2] + command :dynamic_description do |args| + args.split end command :cc + explanation do |arg| + "Action does something with #{arg}" + end condition do project == 'foo' end command :cond_action do |arg| arg end + + parse_params do |raw_arg| + raw_arg.strip + end + command :with_params_parsing do |parsed| + parsed + end end end describe '.command_definitions' do it 'returns an array with commands definitions' do - no_args_def, one_arg_def, two_args_def, cc_def, cond_action_def = DummyClass.command_definitions + no_args_def, explanation_with_aliases_def, dynamic_description_def, + cc_def, cond_action_def, with_params_parsing_def = + DummyClass.command_definitions expect(no_args_def.name).to eq(:no_args) expect(no_args_def.aliases).to eq([:none]) expect(no_args_def.description).to eq('A command with no args') + expect(no_args_def.explanation).to eq('') expect(no_args_def.params).to eq([]) expect(no_args_def.condition_block).to be_nil expect(no_args_def.action_block).to be_a_kind_of(Proc) + expect(no_args_def.parse_params_block).to be_nil - expect(one_arg_def.name).to eq(:one_arg) - expect(one_arg_def.aliases).to eq([:once, :first]) - expect(one_arg_def.description).to eq('') - expect(one_arg_def.params).to eq(['The first argument']) - expect(one_arg_def.condition_block).to be_nil - expect(one_arg_def.action_block).to be_a_kind_of(Proc) + expect(explanation_with_aliases_def.name).to eq(:explanation_with_aliases) + expect(explanation_with_aliases_def.aliases).to eq([:once, :first]) + expect(explanation_with_aliases_def.description).to eq('') + expect(explanation_with_aliases_def.explanation).to eq('Static explanation') + expect(explanation_with_aliases_def.params).to eq(['The first argument']) + expect(explanation_with_aliases_def.condition_block).to be_nil + expect(explanation_with_aliases_def.action_block).to be_a_kind_of(Proc) + expect(explanation_with_aliases_def.parse_params_block).to be_nil - expect(two_args_def.name).to eq(:two_args) - expect(two_args_def.aliases).to eq([]) - expect(two_args_def.to_h(noteable: "issue")[:description]).to eq('A dynamic description for ISSUE') - expect(two_args_def.params).to eq(['The first argument', 'The second argument']) - expect(two_args_def.condition_block).to be_nil - expect(two_args_def.action_block).to be_a_kind_of(Proc) + expect(dynamic_description_def.name).to eq(:dynamic_description) + expect(dynamic_description_def.aliases).to eq([]) + expect(dynamic_description_def.to_h(noteable: 'issue')[:description]).to eq('A dynamic description for ISSUE') + expect(dynamic_description_def.explanation).to eq('') + expect(dynamic_description_def.params).to eq(['The first argument', 'The second argument']) + expect(dynamic_description_def.condition_block).to be_nil + expect(dynamic_description_def.action_block).to be_a_kind_of(Proc) + expect(dynamic_description_def.parse_params_block).to be_nil expect(cc_def.name).to eq(:cc) expect(cc_def.aliases).to eq([]) expect(cc_def.description).to eq('') + expect(cc_def.explanation).to eq('') expect(cc_def.params).to eq([]) expect(cc_def.condition_block).to be_nil expect(cc_def.action_block).to be_nil + expect(cc_def.parse_params_block).to be_nil expect(cond_action_def.name).to eq(:cond_action) expect(cond_action_def.aliases).to eq([]) expect(cond_action_def.description).to eq('') + expect(cond_action_def.explanation).to be_a_kind_of(Proc) expect(cond_action_def.params).to eq([]) expect(cond_action_def.condition_block).to be_a_kind_of(Proc) expect(cond_action_def.action_block).to be_a_kind_of(Proc) + expect(cond_action_def.parse_params_block).to be_nil + + expect(with_params_parsing_def.name).to eq(:with_params_parsing) + expect(with_params_parsing_def.aliases).to eq([]) + expect(with_params_parsing_def.description).to eq('') + expect(with_params_parsing_def.explanation).to eq('') + expect(with_params_parsing_def.params).to eq([]) + expect(with_params_parsing_def.condition_block).to be_nil + expect(with_params_parsing_def.action_block).to be_a_kind_of(Proc) + expect(with_params_parsing_def.parse_params_block).to be_a_kind_of(Proc) end end end diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 8c90a538f57..a9c5b604268 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -15,13 +15,39 @@ describe Event, models: true do end describe 'Callbacks' do - describe 'after_create :reset_project_activity' do - let(:project) { create(:empty_project) } + let(:project) { create(:empty_project) } + describe 'after_create :reset_project_activity' do it 'calls the reset_project_activity method' do expect_any_instance_of(described_class).to receive(:reset_project_activity) - create_event(project, project.owner) + create_push_event(project, project.owner) + end + end + + describe 'after_create :set_last_repository_updated_at' do + context 'with a push event' do + it 'updates the project last_repository_updated_at' do + project.update(last_repository_updated_at: 1.year.ago) + + create_push_event(project, project.owner) + + project.reload + + expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now) + end + end + + context 'without a push event' do + it 'does not update the project last_repository_updated_at' do + project.update(last_repository_updated_at: 1.year.ago) + + create(:closed_issue_event, project: project, author: project.owner) + + project.reload + + expect(project.last_repository_updated_at).to be_within(1.minute).of(1.year.ago) + end end end end @@ -29,7 +55,7 @@ describe Event, models: true do describe "Push event" do let(:project) { create(:empty_project, :private) } let(:user) { project.owner } - let(:event) { create_event(project, user) } + let(:event) { create_push_event(project, user) } it do expect(event.push?).to be_truthy @@ -243,7 +269,7 @@ describe Event, models: true do expect(project).not_to receive(:update_column). with(:last_activity_at, a_kind_of(Time)) - create_event(project, project.owner) + create_push_event(project, project.owner) end end @@ -251,11 +277,11 @@ describe Event, models: true do it 'updates the project' do project.update(last_activity_at: 1.year.ago) - create_event(project, project.owner) + create_push_event(project, project.owner) project.reload - project.last_activity_at <= 1.minute.ago + expect(project.last_activity_at).to be_within(1.minute).of(Time.now) end end end @@ -278,7 +304,7 @@ describe Event, models: true do end end - def create_event(project, user, attrs = {}) + def create_push_event(project, user, attrs = {}) data = { before: Gitlab::Git::BLANK_SHA, after: "0220c11b9a3e6c69dc8fd35321254ca9a7b98f7e", diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 36ce3070a6e..316ece87faa 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1925,4 +1925,12 @@ describe Project, models: true do not_to raise_error end end + + describe '#last_repository_updated_at' do + it 'sets to created_at upon creation' do + project = create(:empty_project, created_at: 2.hours.ago) + + expect(project.last_repository_updated_at.to_i).to eq(project.created_at.to_i) + end + end end diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index b5b9cd024b0..969e9f7a130 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -213,9 +213,12 @@ describe ProjectWiki, models: true do end it 'updates project activity' do - expect(subject).to receive(:update_project_activity) - subject.create_page('Test Page', 'This is content') + + project.reload + + expect(project.last_activity_at).to be_within(1.minute).of(Time.now) + expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now) end end @@ -240,9 +243,12 @@ describe ProjectWiki, models: true do end it 'updates project activity' do - expect(subject).to receive(:update_project_activity) - subject.update_page(@gollum_page, 'Yet more content', :markdown, 'Updated page again') + + project.reload + + expect(project.last_activity_at).to be_within(1.minute).of(Time.now) + expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now) end end @@ -258,9 +264,12 @@ describe ProjectWiki, models: true do end it 'updates project activity' do - expect(subject).to receive(:update_project_activity) - subject.delete_page(@page) + + project.reload + + expect(project.last_activity_at).to be_within(1.minute).of(Time.now) + expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now) end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 1c2df4c9d97..13179174956 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1663,4 +1663,12 @@ describe User, models: true do expect(User.active.count).to eq(1) end end + + describe 'preferred language' do + it 'is English by default' do + user = create(:user) + + expect(user.preferred_language).to eq('en') + end + end end diff --git a/spec/services/preview_markdown_service_spec.rb b/spec/services/preview_markdown_service_spec.rb new file mode 100644 index 00000000000..b2fb5c91313 --- /dev/null +++ b/spec/services/preview_markdown_service_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe PreviewMarkdownService do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + + before do + project.add_developer(user) + end + + describe 'user references' do + let(:params) { { text: "Take a look #{user.to_reference}" } } + let(:service) { described_class.new(project, user, params) } + + it 'returns users referenced in text' do + result = service.execute + + expect(result[:users]).to eq [user.username] + end + end + + context 'new note with slash commands' do + let(:issue) { create(:issue, project: project) } + let(:params) do + { + text: "Please do it\n/assign #{user.to_reference}", + slash_commands_target_type: 'Issue', + slash_commands_target_id: issue.id + } + end + let(:service) { described_class.new(project, user, params) } + + it 'removes slash commands from text' do + result = service.execute + + expect(result[:text]).to eq 'Please do it' + end + + it 'explains slash commands effect' do + result = service.execute + + expect(result[:commands]).to eq "Assigns #{user.to_reference}." + end + end + + context 'merge request description' do + let(:params) do + { + text: "My work\n/estimate 2y", + slash_commands_target_type: 'MergeRequest' + } + end + let(:service) { described_class.new(project, user, params) } + + it 'removes slash commands from text' do + result = service.execute + + expect(result[:text]).to eq 'My work' + end + + it 'explains slash commands effect' do + result = service.execute + + expect(result[:commands]).to eq 'Sets time estimate to 2y.' + end + end +end diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb index 29e65fe7ce6..46cfac4a128 100644 --- a/spec/services/slash_commands/interpret_service_spec.rb +++ b/spec/services/slash_commands/interpret_service_spec.rb @@ -798,4 +798,211 @@ describe SlashCommands::InterpretService, services: true do end end end + + describe '#explain' do + let(:service) { described_class.new(project, developer) } + let(:merge_request) { create(:merge_request, source_project: project) } + + describe 'close command' do + let(:content) { '/close' } + + it 'includes issuable name' do + _, explanations = service.explain(content, issue) + + expect(explanations).to eq(['Closes this issue.']) + end + end + + describe 'reopen command' do + let(:content) { '/reopen' } + let(:merge_request) { create(:merge_request, :closed, source_project: project) } + + it 'includes issuable name' do + _, explanations = service.explain(content, merge_request) + + expect(explanations).to eq(['Reopens this merge request.']) + end + end + + describe 'title command' do + let(:content) { '/title This is new title' } + + it 'includes new title' do + _, explanations = service.explain(content, issue) + + expect(explanations).to eq(['Changes the title to "This is new title".']) + end + end + + describe 'assign command' do + let(:content) { "/assign @#{developer.username} do it!" } + + it 'includes only the user reference' do + _, explanations = service.explain(content, merge_request) + + expect(explanations).to eq(["Assigns @#{developer.username}."]) + end + end + + describe 'unassign command' do + let(:content) { '/unassign' } + let(:issue) { create(:issue, project: project, assignee: developer) } + + it 'includes current assignee reference' do + _, explanations = service.explain(content, issue) + + expect(explanations).to eq(["Removes assignee @#{developer.username}."]) + end + end + + describe 'milestone command' do + let(:content) { '/milestone %wrong-milestone' } + let!(:milestone) { create(:milestone, project: project, title: '9.10') } + + it 'is empty when milestone reference is wrong' do + _, explanations = service.explain(content, issue) + + expect(explanations).to eq([]) + end + end + + describe 'remove milestone command' do + let(:content) { '/remove_milestone' } + let(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) } + + it 'includes current milestone name' do + _, explanations = service.explain(content, merge_request) + + expect(explanations).to eq(['Removes %"9.10" milestone.']) + end + end + + describe 'label command' do + let(:content) { '/label ~missing' } + let!(:label) { create(:label, project: project) } + + it 'is empty when there are no correct labels' do + _, explanations = service.explain(content, issue) + + expect(explanations).to eq([]) + end + end + + describe 'unlabel command' do + let(:content) { '/unlabel' } + + it 'says all labels if no parameter provided' do + merge_request.update!(label_ids: [bug.id]) + _, explanations = service.explain(content, merge_request) + + expect(explanations).to eq(['Removes all labels.']) + end + end + + describe 'relabel command' do + let(:content) { '/relabel Bug' } + let!(:bug) { create(:label, project: project, title: 'Bug') } + let(:feature) { create(:label, project: project, title: 'Feature') } + + it 'includes label name' do + issue.update!(label_ids: [feature.id]) + _, explanations = service.explain(content, issue) + + expect(explanations).to eq(["Replaces all labels with ~#{bug.id} label."]) + end + end + + describe 'subscribe command' do + let(:content) { '/subscribe' } + + it 'includes issuable name' do + _, explanations = service.explain(content, issue) + + expect(explanations).to eq(['Subscribes to this issue.']) + end + end + + describe 'unsubscribe command' do + let(:content) { '/unsubscribe' } + + it 'includes issuable name' do + merge_request.subscribe(developer, project) + _, explanations = service.explain(content, merge_request) + + expect(explanations).to eq(['Unsubscribes from this merge request.']) + end + end + + describe 'due command' do + let(:content) { '/due April 1st 2016' } + + it 'includes the date' do + _, explanations = service.explain(content, issue) + + expect(explanations).to eq(['Sets the due date to Apr 1, 2016.']) + end + end + + describe 'wip command' do + let(:content) { '/wip' } + + it 'includes the new status' do + _, explanations = service.explain(content, merge_request) + + expect(explanations).to eq(['Marks this merge request as Work In Progress.']) + end + end + + describe 'award command' do + let(:content) { '/award :confetti_ball: ' } + + it 'includes the emoji' do + _, explanations = service.explain(content, issue) + + expect(explanations).to eq(['Toggles :confetti_ball: emoji award.']) + end + end + + describe 'estimate command' do + let(:content) { '/estimate 79d' } + + it 'includes the formatted duration' do + _, explanations = service.explain(content, merge_request) + + expect(explanations).to eq(['Sets time estimate to 3mo 3w 4d.']) + end + end + + describe 'spend command' do + let(:content) { '/spend -120m' } + + it 'includes the formatted duration and proper verb' do + _, explanations = service.explain(content, issue) + + expect(explanations).to eq(['Substracts 2h spent time.']) + end + end + + describe 'target branch command' do + let(:content) { '/target_branch my-feature ' } + + it 'includes the branch name' do + _, explanations = service.explain(content, merge_request) + + expect(explanations).to eq(['Sets target branch to my-feature.']) + end + end + + describe 'board move command' do + let(:content) { '/board_move ~bug' } + let!(:bug) { create(:label, project: project, title: 'bug') } + let!(:board) { create(:board, project: project) } + + it 'includes the label name' do + _, explanations = service.explain(content, issue) + + expect(explanations).to eq(["Moves issue to ~#{bug.id} column in the board."]) + end + end + end end diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb index 5bbe36d9b7f..6efcdd7a1de 100644 --- a/spec/support/features/issuable_slash_commands_shared_examples.rb +++ b/spec/support/features/issuable_slash_commands_shared_examples.rb @@ -257,4 +257,19 @@ shared_examples 'issuable record that supports slash commands in its description end end end + + describe "preview of note on #{issuable_type}" do + it 'removes slash commands from note and explains them' do + visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable) + + page.within('.js-main-target-form') do + fill_in 'note[note]', with: "Awesome!\n/assign @bob " + click_on 'Preview' + + expect(page).to have_content 'Awesome!' + expect(page).not_to have_content '/assign @bob' + expect(page).to have_content 'Assigns @bob.' + end + end + end end diff --git a/yarn.lock b/yarn.lock index fdef0665d15..8c72f36af3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2159,6 +2159,13 @@ expand-range@^1.8.1: dependencies: fill-range "^2.1.0" +exports-loader@^0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/exports-loader/-/exports-loader-0.6.4.tgz#d70fc6121975b35fc12830cf52754be2740fc886" + dependencies: + loader-utils "^1.0.2" + source-map "0.5.x" + express@^4.13.3, express@^4.14.1: version "4.14.1" resolved "https://registry.yarnpkg.com/express/-/express-4.14.1.tgz#646c237f766f148c2120aff073817b9e4d7e0d33" @@ -3102,6 +3109,10 @@ jasmine-jquery@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/jasmine-jquery/-/jasmine-jquery-2.1.1.tgz#d4095e646944a26763235769ab018d9f30f0d47b" +jed@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/jed/-/jed-1.1.1.tgz#7a549bbd9ffe1585b0cd0a191e203055bee574b4" + jodid25519@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/jodid25519/-/jodid25519-1.0.2.tgz#06d4912255093419477d425633606e0e90782967" @@ -5118,6 +5129,10 @@ source-map-support@^0.4.2: dependencies: source-map "^0.5.3" +source-map@0.5.x, source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" + source-map@^0.1.41: version "0.1.43" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" @@ -5130,10 +5145,6 @@ source-map@^0.4.4: dependencies: amdefine ">=0.0.4" -source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3: - version "0.5.6" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" - source-map@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d" |