diff --git a/.ember-cli b/.ember-cli index ee64cfed2a..8c1812cff8 100644 --- a/.ember-cli +++ b/.ember-cli @@ -5,5 +5,11 @@ Setting `disableAnalytics` to true will prevent any data from being sent. */ - "disableAnalytics": false + "disableAnalytics": false, + + /** + Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript + rather than JavaScript by default, when a TypeScript version of a given blueprint is available. + */ + "isTypeScriptProject": false } diff --git a/.eslintignore b/.eslintignore index 42f2fcb285..5712961348 100644 --- a/.eslintignore +++ b/.eslintignore @@ -15,8 +15,13 @@ # misc /coverage/ !.* +.*/ +.eslintcache # ember-try /.node_modules.ember-try/ /bower.json.ember-try +/npm-shrinkwrap.json.ember-try /package.json.ember-try +/package-lock.json.ember-try +/yarn.lock.ember-try diff --git a/.eslintrc.js b/.eslintrc.js index cbeb5c658e..eaf563f3e4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,16 +2,22 @@ module.exports = { root: true, + parser: '@babel/eslint-parser', parserOptions: { - ecmaVersion: 2018, - sourceType: 'module' + ecmaVersion: 'latest', + sourceType: 'module', + requireConfigFile: false, + babelOptions: { + plugins: [ + ['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }], + ], + }, }, - plugins: [ - 'ember' - ], + plugins: ['ember'], extends: [ 'eslint:recommended', - 'plugin:ember/recommended' + 'plugin:ember/recommended', + 'plugin:prettier/recommended', ], env: { browser: true, @@ -317,21 +323,23 @@ module.exports = { // node files { files: [ - '.eslintrc.js', - '.template-lintrc.js', - 'ember-cli-build.js', - 'testem.js', - 'blueprints/*/index.js', - 'config/**/*.js', - 'lib/*/index.js', - 'server/**/*.js' + './.eslintrc.js', + './.prettierrc.js', + './.stylelintrc.js', + './.template-lintrc.js', + './ember-cli-build.js', + './testem.js', + './blueprints/*/index.js', + './config/**/*.js', + './lib/*/index.js', + './server/**/*.js', ], parserOptions: { - sourceType: 'script' + sourceType: 'script', }, env: { browser: false, - node: true + node: true, } }, @@ -347,13 +355,22 @@ module.exports = { triggerCopyError: true, signInUser: true, withFeature: true, - percySnapshot: true, waitForElement: true }, + plugins: ['node'], + extends: ['plugin:node/recommended'], rules: { + // this can be removed once the following is fixed + // https://github.com/mysticatea/eslint-plugin-node/issues/77 + 'node/no-unpublished-require': 'off', 'max-len': 0, 'no-useless-escape': 0, - } - } - ] + }, + }, + { + // test files + files: ['tests/**/*-test.{js,ts}'], + extends: ['plugin:qunit/recommended'], + }, + ], }; diff --git a/.gitignore b/.gitignore index 3d5203f0f9..57b77ca506 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ /.env /.pnp* /.sass-cache +/.eslintcache /connect.lock /coverage/ /libpeerconnection.log @@ -19,11 +20,15 @@ /testem.log /yarn-error.log .DS_Store +env # ember-try /.node_modules.ember-try/ /bower.json.ember-try +/npm-shrinkwrap.json.ember-try /package.json.ember-try +/package-lock.json.ember-try +/yarn.lock.ember-try # vs code JavaScript config file jsconfig.json @@ -31,3 +36,7 @@ jsconfig.json # IDEs folders .idea .history + + +# broccoli-debug +/DEBUG/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000..9221655522 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,21 @@ +# unconventional js +/blueprints/*/files/ +/vendor/ + +# compiled output +/dist/ +/tmp/ + +# dependencies +/bower_components/ +/node_modules/ + +# misc +/coverage/ +!.* +.eslintcache + +# ember-try +/.node_modules.ember-try/ +/bower.json.ember-try +/package.json.ember-try diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000000..e5f7b6d1ee --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,12 @@ +'use strict'; + +module.exports = { + overrides: [ + { + files: '*.{js,ts}', + options: { + singleQuote: true, + }, + }, + ], +}; diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000000..379a703682 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,32 @@ +require: rubocop-performance + +Documentation: + Enabled: false +Metrics/ClassLength: + Enabled: false +Style/ClassAndModuleChildren: + Enabled: false +Metrics/LineLength: + Enabled: false +Metrics/MethodLength: + Max: 40 +Style/AsciiComments: + Enabled: false +Metrics/AbcSize: + Enabled: false +Style/GuardClause: + Enabled: false +Style/FormatStringToken: + Enabled: false +Lint/AssignmentInCondition: + Enabled: false +Style/IfUnlessModifier: + Enabled: false +Naming/MemoizedInstanceVariableName: + EnforcedStyleForLeadingUnderscores: required +Style/MultilineBlockChain: + Enabled: false +Lint/ConstantDefinitionInBlock: + Enabled: false +Naming/VariableNumber: + Enabled: false diff --git a/.ruby-version b/.ruby-version index 8e8299dcc0..be94e6f53d 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.4.2 +3.2.2 diff --git a/.stylelintignore b/.stylelintignore new file mode 100644 index 0000000000..a0cf71cbd1 --- /dev/null +++ b/.stylelintignore @@ -0,0 +1,8 @@ +# unconventional files +/blueprints/*/files/ + +# compiled output +/dist/ + +# addons +/.node_modules.ember-try/ diff --git a/.stylelintrc.js b/.stylelintrc.js new file mode 100644 index 0000000000..021c539ad0 --- /dev/null +++ b/.stylelintrc.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + extends: ['stylelint-config-standard', 'stylelint-prettier/recommended'], +}; diff --git a/.template-lintrc.js b/.template-lintrc.js index b45e96ffdd..f35f61c7b3 100644 --- a/.template-lintrc.js +++ b/.template-lintrc.js @@ -1,5 +1,5 @@ 'use strict'; module.exports = { - extends: 'recommended' + extends: 'recommended', }; diff --git a/.travis.yml b/.travis.yml index e63d64adc3..0fec4027e4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,25 +1,29 @@ --- language: node_js -node_js: 10 + +node_js: + - "18" env: global: - PATH=/snap/bin:$PATH - PERCY_ENABLE=0 - JOBS=1 + - NODE_OPTIONS=--no-experimental-fetch # See https://git.io/vdao3 for details. os: linux -dist: xenial +dist: focal addons: - chrome: stable cache: npm: true + directories: + - $HOME/.npm before_install: - - npm config set spin false - npm install -g greenkeeper-lockfile@1 install: @@ -47,7 +51,7 @@ jobs: env: TRY_CONFIG=ember-beta - if: type = cron env: TRY_CONFIG=ember-data-beta - - node_js: 10 + - node_js: 18 - stage: ":ship: it to quay.io" before_install: skip install: skip @@ -58,3 +62,4 @@ jobs: allow_failures: - env: TRY_CONFIG=ember-beta - env: TRY_CONFIG=ember-data-beta + diff --git a/Dockerfile b/Dockerfile index 47853d60e2..888a4a3203 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:2.4 +FROM ruby:3.2.2 LABEL maintainer Travis CI GmbH @@ -6,7 +6,8 @@ RUN groupadd --gid 1000 node \ && useradd --uid 1000 --gid node --shell /bin/bash --create-home node ENV NPM_CONFIG_LOGLEVEL info -ENV NODE_VERSION 10.7.0 +ENV NODE_VERSION 18.17.1 +ENV NODE_OPTIONS --no-experimental-fetch RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \ && tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \ @@ -37,7 +38,7 @@ RUN npm install --silent -g ember-cli COPY . /app -RUN npm ci --silent +RUN npm ci --silent --force RUN ember build --environment=production RUN cp -a public/* dist/ diff --git a/Gemfile b/Gemfile index 310f476c44..721522ec7e 100644 --- a/Gemfile +++ b/Gemfile @@ -1,28 +1,35 @@ -ruby "~> 2.4.2" +# frozen_string_literal: true + +ruby '~> 3.2.2' source 'https://rubygems.org' -gem 'travis-web', path: 'waiter' -gem 'puma', '~> 3.12.4' -gem 'rack-ssl', '~> 1.4' -gem 'rack-protection', '~> 1.4' -gem 'rack-mobile-detect' -gem 'sinatra' gem 'hashr' +gem 'puma', '~> 6' +gem 'rack-mobile-detect' +gem 'rack-protection', '~> 3.0' +gem 'rack-ssl', '~> 1.4' gem 'sanitize' +gem 'sinatra' +gem 'travis-web', path: 'waiter' group :development, :test do gem 'rake' end - group :development do # gem 'debugger' gem 'foreman' + gem 'rubocop' + gem 'rubocop-performance' + gem 'rubocop-rspec' + gem 'simplecov' + gem 'simplecov-console' end group :test do - gem 'rspec', '~> 2.11' - gem 'test-unit' + gem 'rspec', '~> 3.12' + gem 'rack-test' gem 'sinatra-contrib' + gem 'test-unit' end diff --git a/Gemfile.lock b/Gemfile.lock index ca0403923c..f6ef837989 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -6,78 +6,138 @@ PATH GEM remote: https://rubygems.org/ specs: - backports (3.6.8) + ansi (1.5.0) + ast (2.4.2) crass (1.0.6) - diff-lcs (1.2.5) - foreman (0.82.0) - thor (~> 0.19.1) - hashr (2.0.0) - mini_portile2 (2.4.0) - multi_json (1.12.1) - nokogiri (1.10.9) - mini_portile2 (~> 2.4.0) - nokogumbo (2.0.2) - nokogiri (~> 1.8, >= 1.8.4) - power_assert (0.4.1) - puma (3.12.6) - rack (1.6.12) + diff-lcs (1.5.0) + docile (1.4.0) + foreman (0.87.2) + hashr (2.0.1) + json (2.6.3) + language_server-protocol (3.17.0.3) + multi_json (1.15.0) + mustermann (3.0.0) + ruby2_keywords (~> 0.0.1) + nio4r (2.5.9) + nokogiri (1.15.2-x86_64-linux) + racc (~> 1.4) + parallel (1.23.0) + parser (3.2.2.3) + ast (~> 2.4.1) + racc + power_assert (2.0.3) + puma (6.3.0) + nio4r (~> 2.0) + racc (1.7.1) + rack (2.2.7) rack-mobile-detect (0.4.0) rack - rack-protection (1.5.5) + rack-protection (3.0.6) rack rack-ssl (1.4.1) rack - rack-test (0.6.3) - rack (>= 1.0) - rake (12.3.3) - rspec (2.99.0) - rspec-core (~> 2.99.0) - rspec-expectations (~> 2.99.0) - rspec-mocks (~> 2.99.0) - rspec-core (2.99.2) - rspec-expectations (2.99.2) - diff-lcs (>= 1.1.3, < 2.0) - rspec-mocks (2.99.4) - sanitize (5.2.1) + rack-test (2.1.0) + rack (>= 1.3) + rainbow (3.1.1) + rake (13.0.6) + regexp_parser (2.8.1) + rexml (3.2.5) + rspec (3.12.0) + rspec-core (~> 3.12.0) + rspec-expectations (~> 3.12.0) + rspec-mocks (~> 3.12.0) + rspec-core (3.12.2) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-support (3.12.1) + rubocop (1.54.0) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.2.2.3) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.28.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.29.0) + parser (>= 3.2.1.0) + rubocop-capybara (2.18.0) + rubocop (~> 1.41) + rubocop-factory_bot (2.23.1) + rubocop (~> 1.33) + rubocop-performance (1.18.0) + rubocop (>= 1.7.0, < 2.0) + rubocop-ast (>= 0.4.0) + rubocop-rspec (2.22.0) + rubocop (~> 1.33) + rubocop-capybara (~> 2.17) + rubocop-factory_bot (~> 2.22) + ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) + sanitize (6.0.1) crass (~> 1.0.2) - nokogiri (>= 1.8.0) - nokogumbo (~> 2.0) - sinatra (1.4.8) - rack (~> 1.5) - rack-protection (~> 1.4) - tilt (>= 1.3, < 3) - sinatra-contrib (1.4.7) - backports (>= 2.0) + nokogiri (>= 1.12.0) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-console (0.9.1) + ansi + simplecov + terminal-table + simplecov-html (0.12.3) + simplecov_json_formatter (0.1.4) + sinatra (3.0.6) + mustermann (~> 3.0) + rack (~> 2.2, >= 2.2.4) + rack-protection (= 3.0.6) + tilt (~> 2.0) + sinatra-contrib (3.0.6) multi_json - rack-protection - rack-test - sinatra (~> 1.4.0) - tilt (>= 1.3, < 3) - test-unit (3.2.3) + mustermann (~> 3.0) + rack-protection (= 3.0.6) + sinatra (= 3.0.6) + tilt (~> 2.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + test-unit (3.6.1) power_assert - thor (0.19.1) - tilt (2.0.8) + tilt (2.2.0) + unicode-display_width (2.4.2) PLATFORMS - ruby + x86_64-linux DEPENDENCIES foreman hashr - puma (~> 3.12.4) + puma (~> 6) rack-mobile-detect - rack-protection (~> 1.4) + rack-protection (~> 3.0) rack-ssl (~> 1.4) + rack-test rake - rspec (~> 2.11) + rspec (~> 3.12) + rubocop + rubocop-performance + rubocop-rspec sanitize + simplecov + simplecov-console sinatra sinatra-contrib test-unit travis-web! RUBY VERSION - ruby 2.4.2p198 + ruby 3.2.2p53 BUNDLED WITH - 1.17.3 + 2.4.14 diff --git a/MODULE_REPORT.md b/MODULE_REPORT.md index 11f809a925..d5e8ccb61f 100644 --- a/MODULE_REPORT.md +++ b/MODULE_REPORT.md @@ -17,11 +17,11 @@ if (!Ember.testing) { **Global**: `Ember.Handlebars` -**Location**: `app/helpers/github-commit-link.js` at line 7 +**Location**: `app/helpers/commit-link.js` at line 7 ```js +import formatCommit from 'travis/utils/format-commit'; -import Ember from 'ember'; const { escapeExpression: escape } = Ember.Handlebars.Utils; export default Helper.extend({ @@ -31,7 +31,7 @@ export default Helper.extend({ **Global**: `Ember.testing` -**Location**: `app/controllers/build.js` at line 32 +**Location**: `app/controllers/build.js` at line 31 ```js init() { @@ -59,7 +59,7 @@ export default Helper.extend({ **Global**: `Ember.testing` -**Location**: `app/controllers/repo.js` at line 63 +**Location**: `app/controllers/repo.js` at line 66 ```js init() { @@ -78,7 +78,7 @@ export default Helper.extend({ ```js // Acceptance tests will wait for the promise to resolve, so skip in tests - if (this.get('isOpen') && !Ember.testing) { + if (this.isOpen && !Ember.testing) { yield new EmberPromise(resolve => later(resolve, 10000)); ``` @@ -87,7 +87,7 @@ export default Helper.extend({ **Global**: `Ember.Handlebars` -**Location**: `app/components/build-message.js` at line 7 +**Location**: `app/components/build-message.js` at line 10 ```js import Ember from 'ember'; @@ -101,13 +101,13 @@ export default Component.extend({ **Global**: `Ember.testing` -**Location**: `app/components/dashboard-row.js` at line 26 +**Location**: `app/components/queued-jobs.js` at line 16 ```js - this.toggleProperty('dropupIsOpen'); - + init() { + this._super(...arguments); if (!Ember.testing) { - later((() => { this.set('dropupIsOpen', false); }), 4000); + return Visibility.every(config.intervals.updateTimes, this.updateTimes.bind(this)); } ``` @@ -115,21 +115,21 @@ export default Component.extend({ **Global**: `Ember.testing` -**Location**: `app/components/queued-jobs.js` at line 13 +**Location**: `app/components/repository-sidebar.js` at line 45 ```js - init() { - this._super(...arguments); - if (!Ember.testing) { - return Visibility.every(config.intervals.updateTimes, this.updateTimes.bind(this)); } + + if (!Ember.testing) { + Visibility.every(config.intervals.updateTimes, () => { + const callback = (record) => record.get('currentBuild'); ``` ### Unknown Global **Global**: `Ember.testing` -**Location**: `app/components/running-jobs.js` at line 18 +**Location**: `app/components/running-jobs.js` at line 16 ```js init() { @@ -143,49 +143,21 @@ export default Component.extend({ **Global**: `Ember.testing` -**Location**: `app/components/repository-sidebar.js` at line 46 - -```js - } - - if (!Ember.testing) { - Visibility.every(config.intervals.updateTimes, () => { - const callback = (record) => record.get('currentBuild'); -``` - -### Unknown Global - -**Global**: `Ember.testing` - -**Location**: `app/components/top-bar.js` at line 38 +**Location**: `app/components/top-bar.js` at line 82 ```js didInsertElement() { if (Ember.testing) { - this._super(...arguments); + super.didInsertElement(...arguments); return; ``` ### Unknown Global -**Global**: `Ember.testing` - -**Location**: `app/controllers/dashboard/builds.js` at line 12 - -```js - init() { - this._super(...arguments); - if (!Ember.testing) { - Visibility.every(config.intervals.updateTimes, this.updateTimes.bind(this)); - } -``` - -### Unknown Global - **Global**: `Ember.Test` -**Location**: `tests/acceptance/builds/invalid-build-test.js` at line 14 +**Location**: `tests/acceptance/job/invalid-log-test.js` at line 16 ```js @@ -199,7 +171,7 @@ export default Component.extend({ **Global**: `Ember.Logger` -**Location**: `tests/acceptance/builds/invalid-build-test.js` at line 15 +**Location**: `tests/acceptance/job/invalid-log-test.js` at line 17 ```js hooks.beforeEach(function () { @@ -213,7 +185,7 @@ export default Component.extend({ **Global**: `Ember.Test` -**Location**: `tests/acceptance/builds/invalid-build-test.js` at line 16 +**Location**: `tests/acceptance/job/invalid-log-test.js` at line 18 ```js adapterException = Ember.Test.adapter.exception; @@ -227,7 +199,7 @@ export default Component.extend({ **Global**: `Ember.Logger` -**Location**: `tests/acceptance/builds/invalid-build-test.js` at line 17 +**Location**: `tests/acceptance/job/invalid-log-test.js` at line 19 ```js loggerError = Ember.Logger.error; @@ -241,7 +213,7 @@ export default Component.extend({ **Global**: `Ember.Test` -**Location**: `tests/acceptance/builds/invalid-build-test.js` at line 21 +**Location**: `tests/acceptance/job/invalid-log-test.js` at line 23 ```js @@ -255,7 +227,7 @@ export default Component.extend({ **Global**: `Ember.Logger` -**Location**: `tests/acceptance/builds/invalid-build-test.js` at line 22 +**Location**: `tests/acceptance/job/invalid-log-test.js` at line 24 ```js hooks.afterEach(function () { @@ -269,7 +241,7 @@ export default Component.extend({ **Global**: `Ember.Test` -**Location**: `tests/acceptance/owner/not-found-test.js` at line 22 +**Location**: `tests/acceptance/owner/not-found-test.js` at line 24 ```js // Ignore promise rejection. @@ -283,7 +255,7 @@ export default Component.extend({ **Global**: `Ember.Logger` -**Location**: `tests/acceptance/owner/not-found-test.js` at line 23 +**Location**: `tests/acceptance/owner/not-found-test.js` at line 25 ```js // Original exception will fail test on promise rejection. @@ -297,7 +269,7 @@ export default Component.extend({ **Global**: `Ember.Test` -**Location**: `tests/acceptance/owner/not-found-test.js` at line 24 +**Location**: `tests/acceptance/owner/not-found-test.js` at line 26 ```js adapterException = Ember.Test.adapter.exception; @@ -311,7 +283,7 @@ export default Component.extend({ **Global**: `Ember.Logger` -**Location**: `tests/acceptance/owner/not-found-test.js` at line 25 +**Location**: `tests/acceptance/owner/not-found-test.js` at line 27 ```js loggerError = Ember.Logger.error; @@ -325,7 +297,7 @@ export default Component.extend({ **Global**: `Ember.Test` -**Location**: `tests/acceptance/owner/not-found-test.js` at line 29 +**Location**: `tests/acceptance/owner/not-found-test.js` at line 31 ```js @@ -339,7 +311,7 @@ export default Component.extend({ **Global**: `Ember.Logger` -**Location**: `tests/acceptance/owner/not-found-test.js` at line 30 +**Location**: `tests/acceptance/owner/not-found-test.js` at line 32 ```js hooks.afterEach(function () { @@ -353,27 +325,27 @@ export default Component.extend({ **Global**: `Ember.Test` -**Location**: `tests/acceptance/job/invalid-log-test.js` at line 14 +**Location**: `tests/acceptance/repo/not-found-test.js` at line 21 ```js - - hooks.beforeEach(function () { + // Ignore promise rejection. + // Original exception will fail test on promise rejection. adapterException = Ember.Test.adapter.exception; loggerError = Ember.Logger.error; - Ember.Test.adapter.exception = () => {}; + Ember.Test.adapter.exception = () => null; ``` ### Unknown Global **Global**: `Ember.Logger` -**Location**: `tests/acceptance/job/invalid-log-test.js` at line 15 +**Location**: `tests/acceptance/repo/not-found-test.js` at line 22 ```js - hooks.beforeEach(function () { + // Original exception will fail test on promise rejection. adapterException = Ember.Test.adapter.exception; loggerError = Ember.Logger.error; - Ember.Test.adapter.exception = () => {}; + Ember.Test.adapter.exception = () => null; Ember.Logger.error = () => null; ``` @@ -381,12 +353,12 @@ export default Component.extend({ **Global**: `Ember.Test` -**Location**: `tests/acceptance/job/invalid-log-test.js` at line 16 +**Location**: `tests/acceptance/repo/not-found-test.js` at line 23 ```js adapterException = Ember.Test.adapter.exception; loggerError = Ember.Logger.error; - Ember.Test.adapter.exception = () => {}; + Ember.Test.adapter.exception = () => null; Ember.Logger.error = () => null; }); ``` @@ -395,11 +367,11 @@ export default Component.extend({ **Global**: `Ember.Logger` -**Location**: `tests/acceptance/job/invalid-log-test.js` at line 17 +**Location**: `tests/acceptance/repo/not-found-test.js` at line 24 ```js loggerError = Ember.Logger.error; - Ember.Test.adapter.exception = () => {}; + Ember.Test.adapter.exception = () => null; Ember.Logger.error = () => null; }); @@ -409,7 +381,7 @@ export default Component.extend({ **Global**: `Ember.Test` -**Location**: `tests/acceptance/job/invalid-log-test.js` at line 21 +**Location**: `tests/acceptance/repo/not-found-test.js` at line 28 ```js @@ -423,7 +395,7 @@ export default Component.extend({ **Global**: `Ember.Logger` -**Location**: `tests/acceptance/job/invalid-log-test.js` at line 22 +**Location**: `tests/acceptance/repo/not-found-test.js` at line 29 ```js hooks.afterEach(function () { @@ -435,29 +407,71 @@ export default Component.extend({ ### Unknown Global +**Global**: `Ember.testing` + +**Location**: `app/services/animation.js` at line 6 + +```js +import fade from 'ember-animated/transitions/fade'; + +const isTest = Ember.testing; + +export const DURATION_NAMES = { +``` + +### Unknown Global + +**Global**: `Ember.testing` + +**Location**: `app/services/animation.js` at line 6 + +```js +import fade from 'ember-animated/transitions/fade'; + +const isTest = Ember.testing; + +export const DURATION_NAMES = { +``` + +### Unknown Global + +**Global**: `Ember.testing` + +**Location**: `app/controllers/dashboard/builds.js` at line 12 + +```js + init() { + this._super(...arguments); + if (!Ember.testing) { + Visibility.every(config.intervals.updateTimes, this.updateTimes.bind(this)); + } +``` + +### Unknown Global + **Global**: `Ember.Test` -**Location**: `tests/acceptance/repo/not-found-test.js` at line 19 +**Location**: `tests/acceptance/builds/invalid-build-test.js` at line 16 ```js - // Ignore promise rejection. - // Original exception will fail test on promise rejection. + + hooks.beforeEach(function () { adapterException = Ember.Test.adapter.exception; loggerError = Ember.Logger.error; - Ember.Test.adapter.exception = () => null; + Ember.Test.adapter.exception = () => {}; ``` ### Unknown Global **Global**: `Ember.Logger` -**Location**: `tests/acceptance/repo/not-found-test.js` at line 20 +**Location**: `tests/acceptance/builds/invalid-build-test.js` at line 17 ```js - // Original exception will fail test on promise rejection. + hooks.beforeEach(function () { adapterException = Ember.Test.adapter.exception; loggerError = Ember.Logger.error; - Ember.Test.adapter.exception = () => null; + Ember.Test.adapter.exception = () => {}; Ember.Logger.error = () => null; ``` @@ -465,12 +479,12 @@ export default Component.extend({ **Global**: `Ember.Test` -**Location**: `tests/acceptance/repo/not-found-test.js` at line 21 +**Location**: `tests/acceptance/builds/invalid-build-test.js` at line 18 ```js adapterException = Ember.Test.adapter.exception; loggerError = Ember.Logger.error; - Ember.Test.adapter.exception = () => null; + Ember.Test.adapter.exception = () => {}; Ember.Logger.error = () => null; }); ``` @@ -479,11 +493,11 @@ export default Component.extend({ **Global**: `Ember.Logger` -**Location**: `tests/acceptance/repo/not-found-test.js` at line 22 +**Location**: `tests/acceptance/builds/invalid-build-test.js` at line 19 ```js loggerError = Ember.Logger.error; - Ember.Test.adapter.exception = () => null; + Ember.Test.adapter.exception = () => {}; Ember.Logger.error = () => null; }); @@ -493,7 +507,7 @@ export default Component.extend({ **Global**: `Ember.Test` -**Location**: `tests/acceptance/repo/not-found-test.js` at line 26 +**Location**: `tests/acceptance/builds/invalid-build-test.js` at line 23 ```js @@ -507,7 +521,7 @@ export default Component.extend({ **Global**: `Ember.Logger` -**Location**: `tests/acceptance/repo/not-found-test.js` at line 27 +**Location**: `tests/acceptance/builds/invalid-build-test.js` at line 24 ```js hooks.afterEach(function () { diff --git a/README.md b/README.md index 5b0627469c..50b0f59c0d 100644 --- a/README.md +++ b/README.md @@ -90,9 +90,8 @@ You can also start an interactive test runner for easier development: ### Linting -* `npm run lint:hbs` -* `npm run lint:js` -* `npm run lint:js -- --fix` +* `npm run lint` +* `npm run lint:fix` ### Feature Flags diff --git a/app/.eslintrc.js b/app/.eslintrc.js index 0fc7e8c190..2b8da3fd31 100644 --- a/app/.eslintrc.js +++ b/app/.eslintrc.js @@ -6,7 +6,7 @@ module.exports = { ecmaVersion: 6, sourceType: 'module' }, - parser: 'babel-eslint', + parser: '@babel/eslint-parser', extends: 'eslint:recommended', env: { 'browser': true, diff --git a/app/adapters/build.js b/app/adapters/build.js index 77d7719318..9cd44c79d7 100644 --- a/app/adapters/build.js +++ b/app/adapters/build.js @@ -4,7 +4,7 @@ import Ember from 'ember'; let includes = 'build.commit,build.branch,build.request,build.created_by'; // TODO this is a workaround for an infinite loop in Mirage serialising 😞 -if (!Ember.testing) { +if (Ember.testing) { includes += ',build.repository'; } diff --git a/app/adapters/cron.js b/app/adapters/cron.js index b8dab37228..75d3d6b2ff 100644 --- a/app/adapters/cron.js +++ b/app/adapters/cron.js @@ -14,7 +14,12 @@ export default V3Adapter.extend({ dont_run_if_recent_build_exists: data.dont_run_if_recent_build_exists, interval: data.interval } - }); + }).then(response => { + if (!response.id) { + response.id = 'temp-id-' + new Date().getTime(); // we do not need id at least in tests but Ember needs it. + } + return response; + });; }, query(store, type, query) { diff --git a/app/adapters/v3.js b/app/adapters/v3.js index b5951a79b8..15ad7ad0b7 100644 --- a/app/adapters/v3.js +++ b/app/adapters/v3.js @@ -1,8 +1,7 @@ -import { assign } from '@ember/polyfills'; import { underscore } from '@ember/string'; import { pluralize } from 'ember-inflector'; import config from 'travis/config/environment'; -import RESTAdapter from 'ember-data/adapters/rest'; +import RESTAdapter from '@ember-data/adapter/rest'; import { inject as service } from '@ember/service'; export default RESTAdapter.extend({ @@ -22,7 +21,7 @@ export default RESTAdapter.extend({ ajaxOptions: function (url, type = 'GET', options) { options = options || {}; options.data = options.data || {}; - options.data = assign({}, options.data); // clone + options.data = Object.assign({}, options.data); // clone for (let key in options.data) { let value = options.data[key]; diff --git a/app/app.js b/app/app.js index 51e7680014..c6a4432e26 100644 --- a/app/app.js +++ b/app/app.js @@ -1,10 +1,9 @@ /* global Travis */ import Evented from '@ember/object/evented'; - import Application from '@ember/application'; import Resolver from './resolver'; import loadInitializers from 'ember-load-initializers'; -import config from './config/environment'; +import config from 'travis/config/environment'; // This can be set per environment in config/environment.js const debuggingEnabled = config.featureFlags['debug-logging']; diff --git a/app/components/add-cron-job.js b/app/components/add-cron-job.js index 3a320487d5..724ddc5b53 100644 --- a/app/components/add-cron-job.js +++ b/app/components/add-cron-job.js @@ -5,6 +5,7 @@ import BranchSearching from 'travis/mixins/branch-searching'; export default Component.extend(BranchSearching, { store: service(), + flashes: service(), classNames: ['form--cron'], @@ -41,7 +42,7 @@ export default Component.extend(BranchSearching, { const cron = this.store.createRecord('cron', { branch: this.selectedBranch, interval: this.selectedInterval.toLowerCase(), - dont_run_if_recent_build_exists: this.selectedOption.value + dont_run_if_recent_build_exists: this.selectedOption ? this.selectedOption.value : null }); try { yield cron.save(); diff --git a/app/components/add-env-var.js b/app/components/add-env-var.js index d423a2340a..f656c6ca53 100644 --- a/app/components/add-env-var.js +++ b/app/components/add-env-var.js @@ -32,8 +32,8 @@ export default Component.extend(BranchSearching, { save: task(function* () { const envVar = this.store.createRecord('env_var', { - name: this.name.trim(), - value: this.value.trim(), + name: (this.name || "").trim(), + value: (this.value || "").trim(), 'public': this.public, repo: this.repo, branch: this.branch diff --git a/app/components/billing-manual.js b/app/components/billing-manual.js new file mode 100644 index 0000000000..bb93d73f35 --- /dev/null +++ b/app/components/billing-manual.js @@ -0,0 +1,4 @@ +import Component from '@ember/component'; + +export default Component.extend({ +}); diff --git a/app/components/billing-summary-status.js b/app/components/billing-summary-status.js index 827d1ebb7e..2e236941a9 100644 --- a/app/components/billing-summary-status.js +++ b/app/components/billing-summary-status.js @@ -5,12 +5,12 @@ import { isEmpty } from '@ember/utils'; export default Component.extend({ account: null, - subcription: null, + subscription: null, trial: reads('account.trial'), isGithubTrial: and('subscription.isGithub', 'trial.hasActiveTrial'), hasGithubTrialEnded: and('subscription.isGithub', 'trial.isEnded'), noSubscription: empty('subscription'), isDefaultEducationView: computed('subscription', 'account.education', 'subscription.plan_name', function () { - return this.get('subscription') && !isEmpty(this.get('subscription')) && this.get('account.education'); - }) + return this.subscription && !isEmpty(this.subscription) && this.get('account.education'); + }), }); diff --git a/app/components/billing/account.js b/app/components/billing/account.js index ba11a7c456..27baa4f54a 100644 --- a/app/components/billing/account.js +++ b/app/components/billing/account.js @@ -1,47 +1,57 @@ import Component from '@ember/component'; import { inject as service } from '@ember/service'; -import { computed } from '@ember/object'; +import { computed, action } from '@ember/object'; import { reads, empty, bool, not, and, or } from '@ember/object/computed'; +import {tracked} from "@glimmer/tracking"; -export default Component.extend({ - store: service(), - accounts: service(), +export default class BillingAccount extends Component { + @service store; + @service accounts; - account: null, + @tracked account = null; - subscription: reads('account.subscription'), - v2subscription: reads('account.v2subscription'), - isV2SubscriptionEmpty: empty('v2subscription'), - isSubscriptionEmpty: empty('subscription'), - isSubscriptionsEmpty: and('isSubscriptionEmpty', 'isV2SubscriptionEmpty'), - hasV2Subscription: not('isV2SubscriptionEmpty'), - trial: reads('account.trial'), - isEducationalAccount: bool('account.education'), - isNotEducationalAccount: not('isEducationalAccount'), + @reads('account.subscription') subscription; + @reads('account.v2subscription') v2subscription; + @empty('v2subscription') isV2SubscriptionEmpty; + @empty('subscription') isSubscriptionEmpty; + @and('isSubscriptionEmpty', 'isV2SubscriptionEmpty') isSubscriptionsEmpty; + @not('isV2SubscriptionEmpty') hasV2Subscription; + @reads('account.trial') trial; + @bool('account.education') isEducationalAccount; + @not('isEducationalAccount') isNotEducationalAccount; - isTrial: and('isSubscriptionsEmpty', 'isNotEducationalAccount'), - isManual: bool('subscription.isManual'), - isManaged: bool('subscription.managedSubscription'), - isEducation: and('isSubscriptionsEmpty', 'isEducationalAccount'), - isSubscription: computed('isManaged', 'hasV2Subscription', 'isTrialProcessCompleted', 'isEduProcessCompleted', function () { + @and('isSubscriptionsEmpty', 'isNotEducationalAccount') isTrial; + @bool('subscription.isManual') isManual; + @bool('subscription.managedSubscription') isManaged; + @and('isSubscriptionsEmpty', 'isEducationalAccount') isEducation; + + @computed('isManaged', 'hasV2Subscription', 'isTrialProcessCompleted', 'isEduProcessCompleted') + get isSubscription() { return (this.isManaged || this.hasV2Subscription) && this.isTrialProcessCompleted && this.isEduProcessCompleted; - }), - showInvoices: computed('showPlansSelector', 'showAddonsSelector', function () { + } + + @computed('showPlansSelector', 'showAddonsSelector') + get showInvoices() { return !this.showPlansSelector && !this.showAddonsSelector && this.invoices; - }), + } - isLoading: or('accounts.fetchSubscriptions.isRunning', 'accounts.fetchV2Subscriptions.isRunning'), + @or('accounts.fetchSubscriptions.isRunning', 'accounts.fetchV2Subscriptions.isRunning') isLoading; - showPlansSelector: false, - showAddonsSelector: false, - isTrialProcessCompleted: computed(function () { + showPlansSelector = false; + showAddonsSelector = false; + + @computed('isTrial') + get isTrialProcessCompleted() { return !this.isTrial; - }), - isEduProcessCompleted: computed(function () { + } + + @computed('isEducation') + get isEduProcessCompleted() { return !this.isEducation; - }), + } - newV2Subscription: computed(function () { + @computed('store') + get newV2Subscription() { const plan = this.store.createRecord('v2-plan-config'); const billingInfo = this.store.createRecord('v2-billing-info'); const creditCardInfo = this.store.createRecord('v2-credit-card-info'); @@ -63,9 +73,10 @@ export default Component.extend({ plan, creditCardInfo, }); - }), + } - invoices: computed('subscription.id', 'v2subscription.id', function () { + @computed('subscription.id', 'v2subscription.id') + get invoices() { const subscriptionId = this.isV2SubscriptionEmpty ? this.get('subscription.id') : this.get('v2subscription.id'); const type = this.isV2SubscriptionEmpty ? 1 : 2; if (subscriptionId) { @@ -73,5 +84,5 @@ export default Component.extend({ } else { return []; } - }), -}); + } +} diff --git a/app/components/billing/address.js b/app/components/billing/address.js index ad663ce6ae..26b1ea0c15 100644 --- a/app/components/billing/address.js +++ b/app/components/billing/address.js @@ -2,7 +2,10 @@ import Component from '@ember/component'; import { task } from 'ember-concurrency'; import { inject as service } from '@ember/service'; import { observer } from '@ember/object'; -import { countries, nonZeroVatThresholdCountries } from 'travis/utils/countries'; +import { + countries, + nonZeroVatThresholdCountries +} from 'travis/utils/countries'; export default Component.extend({ countries, diff --git a/app/components/billing/authorization.js b/app/components/billing/authorization.js index 39519fd4e9..7dc933b210 100644 --- a/app/components/billing/authorization.js +++ b/app/components/billing/authorization.js @@ -23,7 +23,6 @@ export default Component.extend({ requiresSource: equal('subscription.paymentIntent.status', 'requires_source'), lastPaymentIntentError: reads('subscription.paymentIntent.last_payment_error'), retryAuthorizationClientSecret: reads('subscription.paymentIntent.client_secret'), - hasSubscriptionPermissions: reads('account.hasSubscriptionPermissions'), notChargeInvoiceSubscription: not('subscription.chargeUnpaidInvoices.lastSuccessful.value'), freeV2Plan: equal('subscription.plan.startingPrice', 0), isSubscribed: reads('subscription.isSubscribed'), @@ -32,6 +31,10 @@ export default Component.extend({ canCancelSubscription: computed('isSubscribed', 'hasSubscriptionPermissions', 'freeV2Plan', 'isTrial', function () { return this.isSubscribed && this.hasSubscriptionPermissions && !this.freeV2Plan && !this.isTrial; }), + + hasSubscriptionPermissions: computed('account.hasSubscriptionPermissions', 'account.permissions', function () { + return this.account.hasSubscriptionPermissions && (!this.account.isOrganization || this.account.permissions.plan_create); + }), cancelSubscriptionLoading: reads('subscription.cancelSubscription.isRunning'), isTrial: reads('subscription.plan.isTrial'), isLoading: or('accounts.fetchSubscriptions.isRunning', 'accounts.fetchV2Subscriptions.isRunning', @@ -67,7 +70,7 @@ export default Component.extend({ }); const { client_secret: clientSecret } = yield this.subscription.chargeUnpaidInvoices.perform(); yield this.stripe.handleStripePayment.perform(clientSecret); - yield this.accounts.fetchV2Subscriptions.perform(); + yield this.accounts.fetchV2Subscriptions.linked().perform(); } } catch (error) { this.flashes.error('An error occurred when creating your subscription. Please try again.'); @@ -77,7 +80,7 @@ export default Component.extend({ editPlan: task(function* () { yield this.subscription.changePlan.perform(this.selectedPlan.id); yield this.accounts.fetchSubscriptions.perform(); - yield this.accounts.fetchV2Subscriptions.perform(); + yield this.accounts.fetchV2Subscriptions.linked().perform(); yield this.retryAuthorization.perform(); }).drop(), @@ -87,7 +90,7 @@ export default Component.extend({ yield this.stripe.handleStripePayment.perform(result.payment_intent.client_secret); } else { yield this.accounts.fetchSubscriptions.perform(); - yield this.accounts.fetchV2Subscriptions.perform(); + yield this.accounts.fetchV2Subscriptions.linked().perform(); } }).drop(), diff --git a/app/components/billing/billing-details.js b/app/components/billing/billing-details.js new file mode 100644 index 0000000000..bb93d73f35 --- /dev/null +++ b/app/components/billing/billing-details.js @@ -0,0 +1,4 @@ +import Component from '@ember/component'; + +export default Component.extend({ +}); diff --git a/app/components/billing/credits-calculator.js b/app/components/billing/credits-calculator.js index 7ee9a6e69a..d250d16ee5 100644 --- a/app/components/billing/credits-calculator.js +++ b/app/components/billing/credits-calculator.js @@ -18,12 +18,12 @@ export default Component.extend({ selectPlan: null, bestPlan: computed('totalCredits', 'plans.[]', function () { - return this.get('plans').find(item => item.annual === this.get('isAnnual') && item.get('privateCredits') > this.get('totalCredits')); + return this.plans.find(item => item.annual === this.isAnnual && item.get('privateCredits') > this.totalCredits); }), totalPrice: computed('configurations.[]', function () { let sum = 0; - for (const configuration of this.get('configurations')) { + for (const configuration of this.configurations) { sum += configuration.price; } @@ -32,7 +32,7 @@ export default Component.extend({ totalCredits: computed('configurations.[]', function () { let sum = 0; - for (const configuration of this.get('configurations')) { + for (const configuration of this.configurations) { sum += configuration.credits; } @@ -44,7 +44,7 @@ export default Component.extend({ users: this.users, executions: [] }; - for (const build of this.get('builds')) { + for (const build of this.builds) { if (build.os.value !== undefined && parseInt(build.minutes) > 0) { let execution = { os: build.os.value, @@ -65,7 +65,7 @@ export default Component.extend({ this.api.post('/credits_calculator', { data }) .then((result) => { - this.get('configurations').clear(); + this.configurations.clear(); for (const creditResult of result.credits_results) { let config = { credits: creditResult.credits, @@ -97,7 +97,7 @@ export default Component.extend({ } } - this.get('configurations').pushObject(config); + this.configurations.pushObject(config); } }); }, @@ -121,23 +121,23 @@ export default Component.extend({ } } - this.get('builds').clear(); - this.get('builds').pushObject({ os: selectedOs, vmSize: selectedVmSize, minutes: result.minutes }); + this.builds.clear(); + this.builds.pushObject({ os: selectedOs, vmSize: selectedVmSize, minutes: result.minutes }); this.set('users', result.users); this.calculate(); }); }, addBuild() { - this.get('builds').pushObject({ os: {}, vmSize: {}, minutes: '' }); + this.builds.pushObject({ os: {}, vmSize: {}, minutes: '' }); }, close() { this.hideCalculator(); - this.get('builds').clear(); + this.builds.clear(); this.addBuild(); this.set('users', ''); - this.get('configurations').clear(); + this.configurations.clear(); }, actions: { @@ -160,7 +160,7 @@ export default Component.extend({ }, selectPlan() { - this.set('selectedPlan', this.get('bestPlan')); + this.set('selectedPlan', this.bestPlan); this.close(); this.selectPlan(this.form); }, diff --git a/app/components/billing/first-plan.js b/app/components/billing/first-plan.js index b9d5e96fc2..e235807c23 100644 --- a/app/components/billing/first-plan.js +++ b/app/components/billing/first-plan.js @@ -4,7 +4,14 @@ import { inject as service } from '@ember/service'; import { not, reads, filterBy, alias } from '@ember/object/computed'; import { computed } from '@ember/object'; import config from 'travis/config/environment'; -import { countries, states, zeroVatThresholdCountries, nonZeroVatThresholdCountries, stateCountries } from 'travis/utils/countries'; +import { A } from '@ember/array'; +import { + countries, + states, + zeroVatThresholdCountries, + nonZeroVatThresholdCountries, + stateCountries +} from 'travis/utils/countries'; export default Component.extend({ stripe: service(), @@ -36,7 +43,11 @@ export default Component.extend({ displayedPlans: reads('availablePlans'), - selectedPlan: computed('displayedPlans.[].id', 'defaultPlanId', function () { + selectedPlanOverride: null, + selectedPlan: computed('selectedPlanOverride','displayedPlans.[].id', 'defaultPlanId', function () { + if (this.selectedPlanOverride !== null) + return this.selectedPlanOverride; + let plan = this.storage.selectedPlanId; if (plan == null) { plan = this.defaultPlanId; @@ -168,7 +179,7 @@ export default Component.extend({ }); yield this.subscription.save(); yield this.subscription.changePlan.perform(selectedPlan.id, this.couponId); - yield this.accounts.fetchV2Subscriptions.perform(); + yield this.accounts.fetchV2Subscriptions.linked().perform(); yield this.retryAuthorization.perform(); } this.metrics.trackEvent({ button: 'pay-button' }); @@ -176,13 +187,13 @@ export default Component.extend({ this.storage.clearSelectedPlanId(); this.storage.wizardStep = 2; this.wizard.update.perform(2); - yield this.accounts.fetchV2Subscriptions.perform().then(() => { + this.accounts.fetchV2Subscriptions.perform().then(() => { this.router.transitionTo('/account/repositories'); }); } this.flashes.success('Your account has been successfully activated'); } catch (error) { - yield this.accounts.fetchV2Subscriptions.perform().then(() => { + this.accounts.fetchV2Subscriptions.perform().then(() => { if (this.accounts.user.subscription || this.accounts.user.v2subscription) { this.storage.clearBillingData(); this.storage.clearSelectedPlanId(); diff --git a/app/components/billing/information.js b/app/components/billing/information.js index 0992308027..eeaf639643 100644 --- a/app/components/billing/information.js +++ b/app/components/billing/information.js @@ -7,8 +7,9 @@ export default Component.extend({ billingInfo: reads('subscription.billingInfo'), actions: { - updateEmails(values) { + updateEmails(values) + { this.billingInfo.set('billingEmail', values.join(',')); - }, + } } }); diff --git a/app/components/billing/invoices.js b/app/components/billing/invoices.js index fe56841df3..634a9135e5 100644 --- a/app/components/billing/invoices.js +++ b/app/components/billing/invoices.js @@ -1,10 +1,11 @@ import Component from '@ember/component'; import { computed } from '@ember/object'; import { reads } from '@ember/object/computed'; +import { A } from '@ember/array' export default Component.extend({ - invoices: null, + invoices: A([]), invoiceYears: computed('invoices.@each.createdAt', function () { return this.invoices.mapBy('year').uniq().sort((a, b) => b - a); diff --git a/app/components/billing/payment-details-tab.js b/app/components/billing/payment-details-tab.js index 8128a620fb..76bac4c1e5 100644 --- a/app/components/billing/payment-details-tab.js +++ b/app/components/billing/payment-details-tab.js @@ -5,7 +5,13 @@ import { empty, not, reads, and } from '@ember/object/computed'; import { computed } from '@ember/object'; import config from 'travis/config/environment'; import { underscore } from '@ember/string'; -import { countries, states, stateCountries, nonZeroVatThresholdCountries, zeroVatThresholdCountries } from 'travis/utils/countries'; +import { + countries, + states, + stateCountries, + nonZeroVatThresholdCountries, + zeroVatThresholdCountries +} from 'travis/utils/countries'; export default Component.extend({ api: service(), @@ -15,6 +21,8 @@ export default Component.extend({ metrics: service(), countries, + + model: reads('activeModel'), states: computed('country', function () { const { country } = this; @@ -26,7 +34,7 @@ export default Component.extend({ couponId: null, options: computed('disableForm', function () { let configStripe = config.stripeOptions; - configStripe['disabled'] = this.get('disableForm'); + configStripe['disabled'] = this.disableForm; return configStripe; }), showSwitchToFreeModal: false, @@ -37,9 +45,15 @@ export default Component.extend({ isV2SubscriptionEmpty: empty('v2subscription'), isSubscriptionEmpty: empty('v1subscription'), isSubscriptionsEmpty: and('isSubscriptionEmpty', 'isV2SubscriptionEmpty'), + canViewBilling: computed('account.isOrganization', 'account.permissions.billing_view', function () { + return !this.account.isOrganization || this.account.permissions.billing_view; + }), + canEditBilling: computed('account.isOrganization', 'account.permissions.billing_update', function () { + return !this.account.isOrganization || this.account.permissions.billing_update; + }), hasV2Subscription: not('isV2SubscriptionEmpty'), subscription: computed('v1subscription', 'v2subscription', function () { - return this.isV2SubscriptionEmpty ? this.get('v1subscription') : this.get('v2subscription'); + return this.isV2SubscriptionEmpty ? this.v1subscription : this.v2subscription; }), invoices: computed('v1subscription.id', 'v2subscription.id', function () { const subscriptionId = this.isV2SubscriptionEmpty ? this.get('v1subscription.id') : this.get('v2subscription.id'); @@ -52,8 +66,8 @@ export default Component.extend({ }), disableForm: computed('account.allowance.paymentChangesBlockCredit', 'account.allowance.paymentChangesBlockCaptcha', function () { - const paymentChangesBlockCredit = this.account.allowance.get('paymentChangesBlockCredit'); - const paymentChangesBlockCaptcha = this.account.allowance.get('paymentChangesBlockCaptcha'); + const paymentChangesBlockCredit = this.account.allowance.paymentChangesBlockCredit; + const paymentChangesBlockCaptcha = this.account.allowance.paymentChangesBlockCaptcha; return paymentChangesBlockCaptcha || paymentChangesBlockCredit; }), diff --git a/app/components/billing/payment.js b/app/components/billing/payment.js index 11cdd6035b..69c5b10eda 100644 --- a/app/components/billing/payment.js +++ b/app/components/billing/payment.js @@ -110,7 +110,7 @@ export default Component.extend({ v1SubscriptionId: this.v1SubscriptionId, }); const { clientSecret } = yield subscription.save(); - yield this.stripe.handleStripePayment.perform(clientSecret); + this.stripe.handleStripePayment.linked().perform(clientSecret); } else { this.metrics.trackEvent({ action: 'Change Plan Pay Button Clicked', @@ -119,12 +119,12 @@ export default Component.extend({ yield this.subscription.changePlan.perform(this.selectedPlan.id, this.couponId); } } - yield this.accounts.fetchV2Subscriptions.perform(); - yield this.retryAuthorization.perform(); + this.accounts.fetchV2Subscriptions.linked().perform(); + yield this.retryAuthorization.linked().perform(); this.storage.clearBillingData(); this.set('showPlansSelector', false); this.set('showAddonsSelector', false); - this.set('isProcessCompleted', true); + this.set('hasV2Subscription', true); } }).drop(), @@ -144,10 +144,10 @@ export default Component.extend({ v1SubscriptionId: this.v1SubscriptionId, }); yield subscription.save(); - yield this.accounts.fetchV2Subscriptions.perform(); + this.accounts.fetchV2Subscriptions.linked().perform(); this.storage.clearBillingData(); this.set('showPlansSelector', false); - this.set('isProcessCompleted', true); + this.set('hasV2Subscription', true); } catch (error) { this.handleError(error); } @@ -180,7 +180,8 @@ export default Component.extend({ coupon: this.couponId }); const { clientSecret } = yield subscription.save(); - yield this.stripe.handleStripePayment.perform(clientSecret); + + this.stripe.handleStripePayment.linked().perform(clientSecret); } else { yield this.subscription.creditCardInfo.updateToken.perform({ subscriptionId: this.subscription.id, @@ -189,13 +190,13 @@ export default Component.extend({ }); yield subscription.save(); yield subscription.changePlan.perform(selectedPlan.id, this.couponId); - yield this.accounts.fetchV2Subscriptions.perform(); + this.accounts.fetchV2Subscriptions.linked().perform(); yield this.retryAuthorization.perform(); } this.metrics.trackEvent({ button: 'pay-button' }); this.storage.clearBillingData(); this.set('showPlansSelector', false); - this.set('isProcessCompleted', true); + this.set('hasV2Subscription', true); } } catch (error) { this.handleError(error); @@ -225,7 +226,7 @@ export default Component.extend({ this.set('showSwitchToFreeModal', false); this.storage.clearBillingData(); this.set('showPlansSelector', false); - this.set('isProcessCompleted', true); + this.set('hasV2Subscription', true); }, closePlanSwitchWarning: function () { diff --git a/app/components/billing/postal-address.js b/app/components/billing/postal-address.js index 0b44ba7f0b..2b1185f855 100644 --- a/app/components/billing/postal-address.js +++ b/app/components/billing/postal-address.js @@ -1,7 +1,13 @@ import Component from '@ember/component'; import { reads } from '@ember/object/computed'; import { computed } from '@ember/object'; -import { countries, states, zeroVatThresholdCountries, nonZeroVatThresholdCountries, stateCountries } from 'travis/utils/countries'; +import { + countries, + states, + zeroVatThresholdCountries, + nonZeroVatThresholdCountries, + stateCountries +} from 'travis/utils/countries'; export default Component.extend({ countries, diff --git a/app/components/billing/price-v2.js b/app/components/billing/price-v2.js new file mode 100644 index 0000000000..bb93d73f35 --- /dev/null +++ b/app/components/billing/price-v2.js @@ -0,0 +1,4 @@ +import Component from '@ember/component'; + +export default Component.extend({ +}); diff --git a/app/components/billing/process.js b/app/components/billing/process.js index b2a3aba844..fbf844dcd9 100644 --- a/app/components/billing/process.js +++ b/app/components/billing/process.js @@ -19,7 +19,11 @@ export default Component.extend({ showCancelButton: false, - currentStep: computed(function () { + currentStepOverride: null, + + currentStep: computed('currentStepOverride', 'storage.billingStep', function () { + if (this.currentStepOverride !== null) + return this.currentStepOverride; return this.storage.billingStep || STEPS.ONE; }), @@ -65,7 +69,7 @@ export default Component.extend({ actions: { goToFirstStep() { - this.set('currentStep', STEPS.ONE); + this.set('currentStepOverride', STEPS.ONE); this.persistBillingData(STEPS.ONE); this.updateBillingQueryParams(STEPS.ONE); }, @@ -78,11 +82,11 @@ export default Component.extend({ const nextIndex = Math.min(lastIndex, currentIndex + 1); if ((this.billingInfoExists && this.currentStep === STEPS.ONE) || this.selectedPlan.startingPrice === 0) { const currentStep = STEPS.THREE; - this.set('currentStep', currentStep); + this.set('currentStepOverride', currentStep); this.set('billingInfo', this.existingBillingInfo); } else { const currentStep = this.steps[nextIndex]; - this.set('currentStep', currentStep); + this.set('currentStepOverride', currentStep); } this.updateBillingQueryParams(this.currentStep); this.persistBillingData(this.currentStep); @@ -93,13 +97,13 @@ export default Component.extend({ const currentIndex = this.steps.indexOf(this.currentStep); const prevIndex = Math.max(0, currentIndex - 1); const currentStep = this.steps[prevIndex]; - this.set('currentStep', currentStep); + this.set('currentStepOverride', currentStep); this.updateBillingQueryParams(currentStep); this.persistBillingData(currentStep); }, cancel() { - this.set('currentStep', STEPS.ONE); + this.set('currentStepOverride', STEPS.ONE); this.updateBillingQueryParams(STEPS.ONE); }, diff --git a/app/components/billing/select-plan.js b/app/components/billing/select-plan.js index d12358cd82..0719157cde 100644 --- a/app/components/billing/select-plan.js +++ b/app/components/billing/select-plan.js @@ -4,6 +4,7 @@ import { task } from 'ember-concurrency'; import { computed } from '@ember/object'; import { later } from '@ember/runloop'; import { or, reads, filterBy } from '@ember/object/computed'; +import { A } from '@ember/array'; export default Component.extend({ accounts: service(), @@ -20,7 +21,11 @@ export default Component.extend({ displayedPlans: reads('availablePlans'), - selectedPlan: computed('displayedPlans.[].name', 'defaultPlanName', function () { + selectedPlanOverride: null, + selectedPlan: computed('selectedPlanOverride', 'displayedPlans.[].name', 'defaultPlanName', function () { + if (this.selectedPlanOverride !== null) + return this.selectedPlanOverride; + return this.displayedPlans.findBy('name', this.defaultPlanName); }), @@ -31,6 +36,9 @@ export default Component.extend({ return false; } }), + hasPlanChangePermission: computed('account', function () { + return !this.account.isOrganization || this.account.permissions.plan_create; + }), save: task(function* () { if (this.next.perform) { @@ -41,13 +49,13 @@ export default Component.extend({ }).drop(), reactivatePlan(plan, form) { - this.set('selectedPlan', plan); + this.set('selectedPlanOverride', plan); this.set('isReactivation', true); later(form.submit, 500); }, selectAndSubmit(plan, form) { - this.set('selectedPlan', plan); + this.set('selectedPlanOverride', plan); later(form.submit, 500); }, @@ -78,6 +86,6 @@ export default Component.extend({ hideCalculator() { this.set('showCalculator', false); - } + }, } }); diff --git a/app/components/billing/summary-v2.js b/app/components/billing/summary-v2.js index d4d5579366..23dee3a5ac 100644 --- a/app/components/billing/summary-v2.js +++ b/app/components/billing/summary-v2.js @@ -13,7 +13,12 @@ export default Component.extend({ isIncomplete: reads('subscription.isIncomplete'), isComplete: not('isIncomplete'), authenticationNotRequired: not('subscription.clientSecret'), - isPending: and('subscription.isPending', 'authenticationNotRequired'), + isPendingOverride: null, + isPending: computed('subscription.isPending', 'authenticationNotRequired', 'isPendingOverride', function() { + if (this.isPendingOverride !== null) + return this.isPendingOverride + return this.authenticationNotRequired && this.subscription.isPending; + }), isNotCanceled: not('isCanceled'), isNotPending: not('isPending'), hasNotExpired: not('isExpired'), diff --git a/app/components/billing/summary.js b/app/components/billing/summary.js index cace58387c..bb4577bddd 100644 --- a/app/components/billing/summary.js +++ b/app/components/billing/summary.js @@ -9,13 +9,25 @@ export default Component.extend({ subscription: null, account: null, - selectedPlan: reads('subscription.plan'), + selectedPlanOverride: null, + + selectedPlan: computed('selectedPlanOverride', 'subscription.plan', function () { + if (this.selectedPlanOverride !== null) + return this.selectedPlanOverride; + + return this.subscription.plan + }), isEditPlanLoading: reads('subscription.changePlan.isLoading'), isIncomplete: reads('subscription.isIncomplete'), isComplete: not('isIncomplete'), authenticationNotRequired: not('subscription.clientSecret'), - isPending: and('subscription.isPending', 'authenticationNotRequired'), + isPendingOverride: null, + isPending: computed('subscription.isPending', 'authenticationNotRequired', 'isPendingOverride', function() { + if (this.isPendingOverride !== null) + return this.isPendingOverride + return this.authenticationNotRequired && this.subscription.isPending; + }), isNotCanceled: not('isCanceled'), isNotPending: not('isPending'), hasNotExpired: not('isExpired'), diff --git a/app/components/billing/user-usage.js b/app/components/billing/user-usage.js index 79fbf98c42..2585cf3f9f 100644 --- a/app/components/billing/user-usage.js +++ b/app/components/billing/user-usage.js @@ -13,7 +13,7 @@ export default Component.extend({ usersUsageReceived: reads('account.allowance.isFulfilled'), usersUsageRejected: reads('account.allowance.isRejected'), usersUsage: computed('account.allowance.userUsage', 'addonUsage', function () { - const userUsage = this.get('account').get('allowance').get('userUsage'); + const userUsage = this.account?.allowance?.userUsage; if (userUsage === undefined) { return true; } diff --git a/app/components/branch-row.js b/app/components/branch-row.js index 804dd211c5..e714de4ee1 100644 --- a/app/components/branch-row.js +++ b/app/components/branch-row.js @@ -22,18 +22,22 @@ export default Component.extend({ commitUrl: computed('branch.repository.slug', 'branch.last_build.commit.sha', 'vcsType', function () { const [owner, repo] = this.get('branch.repository.slug').split('/'); - const vcsType = this.get('vcsType'); + const vcsType = this.vcsType; const commit = this.get('branch.last_build.commit.sha'); return this.externalLinks.commitUrl(vcsType, { owner, repo, commit }); }), + vcsTypeOverride: null, vcsType: computed('branch.repository.id', function () { + if (this.vcsTypeOverride) + return this.vcsTypeOverride; + const repository = this.store.peekRecord('repo', this.get('branch.repository.id')); return repository.vcsType; }), provider: computed('vcsType', function () { - return this.get('vcsType') && this.get('vcsType').toLowerCase().replace('repository', ''); + return this.vcsType && this.vcsType.toLowerCase().replace('repository', ''); }), rawCreatedBy: alias('branch.last_build.created_by'), @@ -108,7 +112,7 @@ export default Component.extend({ } run(() => { - lastBuilds.set('count', response['@pagination'].count); + lastBuilds.set('count', response['@pagination']?.count || 0); lastBuilds.set('content', array); lastBuilds.set('isLoading', false); }); diff --git a/app/components/build-header.js b/app/components/build-header.js index 59d72ba1f7..3dc5f03270 100644 --- a/app/components/build-header.js +++ b/app/components/build-header.js @@ -4,6 +4,7 @@ import jobConfigArch from 'travis/utils/job-config-arch'; import jobConfigLanguage from 'travis/utils/job-config-language'; import { reads, not } from '@ember/object/computed'; import { inject as service } from '@ember/service'; +import { capitalize } from '@ember/string'; const commitMessageLimit = 72; @@ -110,7 +111,7 @@ export default Component.extend({ if (serverType === 'svn') { return 'SVN'; } else { - return serverType.capitalize(); + return capitalize(serverType); } }), diff --git a/app/components/build-message.js b/app/components/build-message.js index bc9175e716..55b417b8e9 100644 --- a/app/components/build-message.js +++ b/app/components/build-message.js @@ -2,12 +2,10 @@ import Component from '@ember/component'; import { computed } from '@ember/object'; import { and, notEmpty } from '@ember/object/computed'; -import { htmlSafe } from '@ember/string'; +import { htmlSafe } from '@ember/template'; import { typeOf } from '@ember/utils'; import { codeblockName } from 'travis/utils/format-config'; -import Ember from 'ember'; - -const { escapeExpression: escape } = Ember.Handlebars.Utils; +import { escape } from 'travis/helpers/format-message'; export default Component.extend({ tagName: '', @@ -16,7 +14,7 @@ export default Component.extend({ const { code, key, args } = this.message; if (this[code]) { - return htmlSafe(this[code](key, args)); + return htmlSafe(`${this[code](key, args)}`); } else { return htmlSafe(`unrecognised message code ${format(code)}`); } diff --git a/app/components/build-messages-list.js b/app/components/build-messages-list.js index 15e5da6633..3a39f49956 100644 --- a/app/components/build-messages-list.js +++ b/app/components/build-messages-list.js @@ -21,7 +21,7 @@ export default Component.extend(WithConfigValidation, { messages: reads('request.messages'), toggleStatusClass: computed('isExpanded', function () { - return this.get('isExpanded') ? 'expanded' : 'collapsed'; + return this.isExpanded ? 'expanded' : 'collapsed'; }), sortedMessages: sort('request.messages', (lft, rgt) => @@ -33,11 +33,11 @@ export default Component.extend(WithConfigValidation, { }), iconClass: computed('maxLevel', function () { - return `icon icon-${this.get('maxLevel')}`; + return `icon icon-${this.maxLevel}`; }), summary: computed('sortedMessages', function () { - let counts = countBy(this.get('sortedMessages'), 'level'); + let counts = countBy(this.sortedMessages, 'level'); if (Object.entries(counts).length > 0) { return Object.entries(counts).map((entry) => formatLevel(...entry)).join(', '); } diff --git a/app/components/caches-item.js b/app/components/caches-item.js index 9e421cd9f0..1864703798 100644 --- a/app/components/caches-item.js +++ b/app/components/caches-item.js @@ -10,6 +10,7 @@ export default Component.extend({ tagName: 'li', classNames: ['cache-item'], classNameBindings: ['cache.type'], + injectedFunction: null, delete: task(function* () { if (config.skipConfirmations || confirm('Are you sure?')) { @@ -20,7 +21,8 @@ export default Component.extend({ try { yield this.api.delete(url); - this.caches.removeObject(this.cache); + const caches = this.caches.filter(item => item !== this.cache); + this.injectedFunction(caches, this.component); } catch (e) { this.flashes.error('Could not delete the cache'); } diff --git a/app/components/dashboard-row.js b/app/components/dashboard-row.js index 3d340d0549..0e6530a75c 100644 --- a/app/components/dashboard-row.js +++ b/app/components/dashboard-row.js @@ -4,6 +4,7 @@ import { inject as service } from '@ember/service'; import { alias, reads } from '@ember/object/computed'; import { task, timeout } from 'ember-concurrency'; import config from 'travis/config/environment'; +import { capitalize } from '@ember/string'; export default Component.extend({ permissionsService: service('permissions'), @@ -28,7 +29,7 @@ export default Component.extend({ displayMenuTofu: alias('repo.permissions.create_request'), repositoryProvider: computed('repo.provider', function () { - return this.repo.provider.capitalize(); + return capitalize(this.repo.provider); }), repositoryType: computed('repo.serverType', function () { diff --git a/app/components/dialogs/migrate-beta.js b/app/components/dialogs/migrate-beta.js index 8b54cc937b..1d9e370c9a 100644 --- a/app/components/dialogs/migrate-beta.js +++ b/app/components/dialogs/migrate-beta.js @@ -15,7 +15,7 @@ export default Component.extend({ selectableAccounts: computed('accounts.organizations.[]', 'user', function () { const accountOrgs = this.accounts.organizations || []; - const organizations = accountOrgs.toArray() || []; + const organizations = accountOrgs || []; return [this.user, ...organizations]; // user account must be first item, so that it couldn't be removed from selected options }), selectableOptions: map('selectableAccounts', makeOptionFromAccount), @@ -24,7 +24,7 @@ export default Component.extend({ register: task(function* () { try { - yield this.user.joinMigrateBeta(this.selectedAccounts.without(this.user).toArray()); + yield this.user.joinMigrateBeta(this.selectedAccounts.without(this.user)); this.onClose(); this.flashes.clear(); this.flashes.success('You have successfully joined the beta!'); diff --git a/app/components/dialogs/plan-switch-warning.js b/app/components/dialogs/plan-switch-warning.js index 8b54cc937b..1d9e370c9a 100644 --- a/app/components/dialogs/plan-switch-warning.js +++ b/app/components/dialogs/plan-switch-warning.js @@ -15,7 +15,7 @@ export default Component.extend({ selectableAccounts: computed('accounts.organizations.[]', 'user', function () { const accountOrgs = this.accounts.organizations || []; - const organizations = accountOrgs.toArray() || []; + const organizations = accountOrgs || []; return [this.user, ...organizations]; // user account must be first item, so that it couldn't be removed from selected options }), selectableOptions: map('selectableAccounts', makeOptionFromAccount), @@ -24,7 +24,7 @@ export default Component.extend({ register: task(function* () { try { - yield this.user.joinMigrateBeta(this.selectedAccounts.without(this.user).toArray()); + yield this.user.joinMigrateBeta(this.selectedAccounts.without(this.user)); this.onClose(); this.flashes.clear(); this.flashes.success('You have successfully joined the beta!'); diff --git a/app/components/dialogs/user-management-modal.js b/app/components/dialogs/user-management-modal.js index dc03b284a7..8404a0d725 100644 --- a/app/components/dialogs/user-management-modal.js +++ b/app/components/dialogs/user-management-modal.js @@ -55,7 +55,7 @@ export default Component.extend({ }), isAllSelected: computed('selectedUserIds', 'buildPermissionsToShow', function () { - const selectedUserIds = this.get('selectedUserIds'); + const selectedUserIds = this.selectedUserIds; if (Object.keys(selectedUserIds).length === 0) { return false; } diff --git a/app/components/enterprise-banner.js b/app/components/enterprise-banner.js index 35aad7f44f..d97be9d03d 100644 --- a/app/components/enterprise-banner.js +++ b/app/components/enterprise-banner.js @@ -3,7 +3,7 @@ import Component from '@ember/component'; import { computed } from '@ember/object'; import { and, alias } from '@ember/object/computed'; import { inject as service } from '@ember/service'; -import { htmlSafe } from '@ember/string'; +import { htmlSafe } from '@ember/template'; import { task } from 'ember-concurrency'; import timeAgoInWords from 'travis/utils/time-ago-in-words'; @@ -69,7 +69,8 @@ export default Component.extend({ expirationTimeFromNow: computed('expirationTime', function () { let expirationTime = this.expirationTime; - return new htmlSafe(timeAgoInWords(expirationTime) || '-'); + let timeText = timeAgoInWords(expirationTime) || '-'; + return new htmlSafe(`${timeText}`); }), expiring: computed('daysUntilExpiry', function () { diff --git a/app/components/error-page-layout.js b/app/components/error-page-layout.js new file mode 100644 index 0000000000..bb93d73f35 --- /dev/null +++ b/app/components/error-page-layout.js @@ -0,0 +1,4 @@ +import Component from '@ember/component'; + +export default Component.extend({ +}); diff --git a/app/components/flashes/negative-balance-private-and-public.js b/app/components/flashes/negative-balance-private-and-public.js new file mode 100644 index 0000000000..bb93d73f35 --- /dev/null +++ b/app/components/flashes/negative-balance-private-and-public.js @@ -0,0 +1,4 @@ +import Component from '@ember/component'; + +export default Component.extend({ +}); diff --git a/app/components/flashes/negative-balance-private.js b/app/components/flashes/negative-balance-private.js new file mode 100644 index 0000000000..bb93d73f35 --- /dev/null +++ b/app/components/flashes/negative-balance-private.js @@ -0,0 +1,4 @@ +import Component from '@ember/component'; + +export default Component.extend({ +}); diff --git a/app/components/flashes/negative-balance-public.js b/app/components/flashes/negative-balance-public.js new file mode 100644 index 0000000000..bb93d73f35 --- /dev/null +++ b/app/components/flashes/negative-balance-public.js @@ -0,0 +1,4 @@ +import Component from '@ember/component'; + +export default Component.extend({ +}); diff --git a/app/components/flashes/payment-details-edit-lock.js b/app/components/flashes/payment-details-edit-lock.js new file mode 100644 index 0000000000..bb93d73f35 --- /dev/null +++ b/app/components/flashes/payment-details-edit-lock.js @@ -0,0 +1,4 @@ +import Component from '@ember/component'; + +export default Component.extend({ +}); diff --git a/app/components/flashes/pending-user-licenses.js b/app/components/flashes/pending-user-licenses.js new file mode 100644 index 0000000000..bb93d73f35 --- /dev/null +++ b/app/components/flashes/pending-user-licenses.js @@ -0,0 +1,4 @@ +import Component from '@ember/component'; + +export default Component.extend({ +}); diff --git a/app/components/flashes/read-only-mode.js b/app/components/flashes/read-only-mode.js new file mode 100644 index 0000000000..bb93d73f35 --- /dev/null +++ b/app/components/flashes/read-only-mode.js @@ -0,0 +1,4 @@ +import Component from '@ember/component'; + +export default Component.extend({ +}); diff --git a/app/components/flashes/scheduled-plan-change.js b/app/components/flashes/scheduled-plan-change.js new file mode 100644 index 0000000000..bb93d73f35 --- /dev/null +++ b/app/components/flashes/scheduled-plan-change.js @@ -0,0 +1,4 @@ +import Component from '@ember/component'; + +export default Component.extend({ +}); diff --git a/app/components/flashes/users-limit-exceeded.js b/app/components/flashes/users-limit-exceeded.js new file mode 100644 index 0000000000..bb93d73f35 --- /dev/null +++ b/app/components/flashes/users-limit-exceeded.js @@ -0,0 +1,4 @@ +import Component from '@ember/component'; + +export default Component.extend({ +}); diff --git a/app/components/forms/form-field.js b/app/components/forms/form-field.js index 009761fada..7b328597f4 100644 --- a/app/components/forms/form-field.js +++ b/app/components/forms/form-field.js @@ -43,13 +43,18 @@ export default Component.extend({ multipleInputsValue: null, validator: null, - required: equal('validator.kind', presense), + requiredOverride: null, + required: computed('requiredOverride', 'validator.kind', function() { + if (this.requiredOverride !== null) { + return this.requiredOverride; + } + return this.validator && this.validator.kind === presense + }), autoValidate: true, errorMessage: '', isFocused: false, - isDefault: equal('state', FIELD_STATE.DEFAULT), isValid: equal('state', FIELD_STATE.VALID), isError: equal('state', FIELD_STATE.ERROR), @@ -59,7 +64,14 @@ export default Component.extend({ showClear: and('allowClear', 'value'), showIcon: notEmpty('icon'), showFrame: not('disableFrame'), - showValidationStatusIcons: and('enableValidationStatusIcons', 'requiresValidation'), + showValidationStatusIconsOverride: null, + showValidationStatusIcons: computed('showValidationStatusIconsOverride', 'enableValidationStatusIcons', 'requiresValidation', function() { + if (this.showValidationStatusIconsOverride !== null) { + return this.showValidationStatusIconsOverride; + } + + return this.enableValidationStatusIcon && this.requiresValidation; + }), showValidationStatusMessage: and('enableValidationStatusMessage', 'requiresValidation'), selectComponent: computed('multiple', function () { @@ -67,6 +79,7 @@ export default Component.extend({ }), validate(value, isFormValidation = false) { + if (!this.validateOnField && !isFormValidation) return true; let validator = this.validator; diff --git a/app/components/forms/form-select-multiple.js b/app/components/forms/form-select-multiple.js index 92b48505c9..e2716d7dff 100644 --- a/app/components/forms/form-select-multiple.js +++ b/app/components/forms/form-select-multiple.js @@ -1,5 +1,34 @@ import EmberPowerSelectMultiple from 'ember-power-select/components/power-select-multiple'; -import FormSelectMixin from 'travis/mixins/components/form-select'; +import { computed } from '@ember/object'; -export default EmberPowerSelectMultiple.extend(FormSelectMixin, { -}); +const OPTIONS_FOR_SEARCH = 5; + +const CSS_CLASSES = { + DISABLED: 'travis-form__field-component--disabled', + FIELD_COMPONENT: 'travis-form__field-component', + FIELD_SELECT: 'travis-form__field-select' +}; +export default class extends EmberPowerSelectMultiple { + disabled = false; + placeholder = ''; + + get onChange() {} + get searchEnabled() { + return this.options.length >= OPTIONS_FOR_SEARCH || !!this.search; + } + + searchPlaceholder = 'Type to filter options...'; + + allowClear = false; + horizontalPosition = 'auto'; + verticalPosition = 'below'; + + @computed('disabled') + get triggerClass() { + const classes = [CSS_CLASSES.FIELD_COMPONENT, CSS_CLASSES.FIELD_SELECT]; + if (this.disabled) { + classes.push(CSS_CLASSES.DISABLED); + } + return classes.join(' '); + } +} diff --git a/app/components/forms/form-select.js b/app/components/forms/form-select.js index 4fac43e36d..9d23dc1d13 100644 --- a/app/components/forms/form-select.js +++ b/app/components/forms/form-select.js @@ -1,5 +1,34 @@ import EmberPowerSelect from 'ember-power-select/components/power-select'; -import FormSelectMixin from 'travis/mixins/components/form-select'; +import { computed } from '@ember/object'; -export default EmberPowerSelect.extend(FormSelectMixin, { -}); +const OPTIONS_FOR_SEARCH = 5; + +const CSS_CLASSES = { + DISABLED: 'travis-form__field-component--disabled', + FIELD_COMPONENT: 'travis-form__field-component', + FIELD_SELECT: 'travis-form__field-select' +}; +export default class extends EmberPowerSelect { + disabled = false; + placeholder = ''; + + get onChange() {} + get searchEnabled() { + return this.options.length >= OPTIONS_FOR_SEARCH || !!this.search; + } + + searchPlaceholder = 'Type to filter options...'; + + allowClear = false; + horizontalPosition = 'auto'; + verticalPosition = 'below'; + + @computed('disabled') + get triggerClass() { + const classes = [CSS_CLASSES.FIELD_COMPONENT, CSS_CLASSES.FIELD_SELECT]; + if (this.disabled) { + classes.push(CSS_CLASSES.DISABLED); + } + return classes.join(' '); + } +} diff --git a/app/components/forms/multiple-inputs-field.js b/app/components/forms/multiple-inputs-field.js index 0e6b60079e..cb75948819 100644 --- a/app/components/forms/multiple-inputs-field.js +++ b/app/components/forms/multiple-inputs-field.js @@ -8,11 +8,24 @@ export default Component.extend({ initialValue: '', value: reads('initialValue'), - fields: computed('value', { + fieldsValue: computed('value', { get() { - return (this.value || '').split(this.delimeter).map(value => ({ value })); + return (this.value || '').split(this.delimiter).map(value => ({ value })); }, set(_, value) { + // Handle the setter logic if needed. + // For example, you can parse the 'value' and update it. + this.set('value', value.join(this.delimiter)); + return value; + } + }), + + fields: computed('fieldsValue', { + get() { + return this.fieldsValue; + }, + set(_, value) { + this.set('fieldsValue', value); return value; } }), @@ -35,13 +48,14 @@ export default Component.extend({ this.updateValues(values); }, + actions: { handleBlur() { const values = this.fields.map(input => input.value); this.handleValidation(values); const value = values.join(this.delimeter); - this.set('value', value); + this.set('valueValue', value); }, handleChange(index, { target }) { @@ -50,7 +64,7 @@ export default Component.extend({ fields[index] = { value }; const values = fields.map(input => input.value); this.handleValidation(values); - this.set('fields', fields); + this.set('fieldsValue', fields); }, removeInput(inputIndex, e) { @@ -58,12 +72,12 @@ export default Component.extend({ const filteredFields = this.fields.filter((_, index) => index !== inputIndex); const values = filteredFields.map(input => input.value); this.handleValidation(values); - this.set('fields', filteredFields); + this.set('fieldsValue', filteredFields); }, addInput(e) { e.preventDefault(); - this.set('fields', [...this.fields, { value: '' }]); + this.set('fieldsValue', [...this.fields, { value: '' }]); }, } }); diff --git a/app/components/github-apps-repository.js b/app/components/github-apps-repository.js index dac327d63e..e8c484f146 100644 --- a/app/components/github-apps-repository.js +++ b/app/components/github-apps-repository.js @@ -9,6 +9,7 @@ import { import hasErrorWithStatus from 'travis/utils/api-errors'; import { task } from 'ember-concurrency'; import { vcsLinks } from 'travis/services/external-links'; +import { capitalize } from '@ember/string'; export default Component.extend({ accounts: service(), @@ -23,7 +24,7 @@ export default Component.extend({ isNotMatchGithub: not('isMatchGithub'), repositoryProvider: computed('repository.provider', function () { - return this.repository.provider.capitalize(); + return capitalize(this.repository.provider); }), repositoryType: computed('repository.serverType', function () { @@ -41,9 +42,18 @@ export default Component.extend({ return this.user && vcsLinks.accessSettingsUrl(this.user.vcsType, { owner: this.user.login }); }), + hasActivatePermission: computed('permissions.all', 'repository', function () { + let repo = this.repository; + let forRepo = (repo.owner.id == this.user.id && repo.ownerType == 'user') || + ((repo.shared || repo.ownerType != 'user') && repo.permissions?.activate); + return forRepo; + }), + hasSettingsPermission: computed('permissions.all', 'repository', function () { let repo = this.repository; - return this.permissions.hasPushPermission(repo); + let forRepo = (repo.owner.id == this.user.id && repo.ownerType == 'user') || + ((repo.shared || repo.ownerType != 'user') && repo.permissions?.settings_read); + return forRepo &&this.permissions.hasPushPermission(repo); }), hasEmailSubscription: computed('repository', 'repository.emailSubscribed', function () { @@ -73,7 +83,7 @@ export default Component.extend({ try { yield repository.toggle(); yield repository.reload(); - this.pusher.subscribe(`repo-${repository.id}`); + Travis.pusher.subscribe(`repo-${repository.id}`); } catch (error) { this.set('apiError', error); } diff --git a/app/components/header-links.js b/app/components/header-links.js index e30a3737cd..54cc49d24b 100644 --- a/app/components/header-links.js +++ b/app/components/header-links.js @@ -2,7 +2,7 @@ import { VERSION } from '@ember/version'; import Component from '@ember/component'; -import { htmlSafe } from '@ember/string'; +import { htmlSafe } from '@ember/template'; import { inject as service } from '@ember/service'; import { computed } from '@ember/object'; import config from 'travis/config/environment'; diff --git a/app/components/insights-glance.js b/app/components/insights-glance.js index 8d5d95c829..0a639ee23c 100644 --- a/app/components/insights-glance.js +++ b/app/components/insights-glance.js @@ -18,15 +18,15 @@ export default Component.extend({ deltaTitle: '', deltaText: '', - labels: computed(() => []), - values: computed(() => []), + labels: [], + values: [], datasetTitle: 'Data', centerline: null, showPlaceholder: or('isLoading', 'isEmpty'), // Chart component data - data: computed('values.[]', 'labels.[]', 'datasetTitle', function () { + data: computed('values', 'labels', 'datasetTitle', function () { return { type: 'spline', x: 'x', diff --git a/app/components/insights-tabs.js b/app/components/insights-tabs.js index fdfdbf6686..d67966f72b 100644 --- a/app/components/insights-tabs.js +++ b/app/components/insights-tabs.js @@ -1,7 +1,8 @@ import Component from '@ember/component'; import { INSIGHTS_INTERVALS } from 'travis/services/insights'; +import { capitalize } from '@ember/string'; -export const INSIGHTS_TABS = Object.values(INSIGHTS_INTERVALS).map(slug => ({ slug, title: slug.capitalize() })); +export const INSIGHTS_TABS = Object.values(INSIGHTS_INTERVALS).map(slug => ({ slug, title: capitalize(slug) })); export default Component.extend({ tagName: 'ul', diff --git a/app/components/job-not-found.js b/app/components/job-not-found.js new file mode 100644 index 0000000000..bb93d73f35 --- /dev/null +++ b/app/components/job-not-found.js @@ -0,0 +1,4 @@ +import Component from '@ember/component'; + +export default Component.extend({ +}); diff --git a/app/components/jobs-item.js b/app/components/jobs-item.js index 7d483e0e58..94f8ec0603 100644 --- a/app/components/jobs-item.js +++ b/app/components/jobs-item.js @@ -3,6 +3,7 @@ import { computed } from '@ember/object'; import { reads } from '@ember/object/computed'; import jobConfigArch from 'travis/utils/job-config-arch'; import jobConfigLanguage from 'travis/utils/job-config-language'; +import { capitalize } from '@ember/string'; export default Component.extend({ tagName: 'li', @@ -69,7 +70,7 @@ export default Component.extend({ if (serverType === 'svn') { return 'SVN'; } else { - return serverType.capitalize(); + return capitalize(serverType); } }), }); diff --git a/app/components/jobs-list.js b/app/components/jobs-list.js index 8b6824820b..be43b2c865 100644 --- a/app/components/jobs-list.js +++ b/app/components/jobs-list.js @@ -1,6 +1,7 @@ import { get, computed } from '@ember/object'; import Component from '@ember/component'; import { alias, mapBy } from '@ember/object/computed'; +import { A } from '@ember/array' export default Component.extend({ tagName: 'section', @@ -66,10 +67,10 @@ export default Component.extend({ } const jobsAllowedToFail = filteredJobs.filterBy('allowFailure'); - const relevantJobs = jobsAllowedToFail.filterBy('isFinished').rejectBy('state', 'passed'); + const relevantJobs = A(jobsAllowedToFail.filterBy('isFinished')).rejectBy('state', 'passed'); - const failedJobsNotAllowedToFail = this.filteredJobs.rejectBy('allowFailure') - .filterBy('isFinished').rejectBy('state', 'passed'); + const failedJobsNotAllowedToFail = A(A(this.filteredJobs).rejectBy('allowFailure') + .filterBy('isFinished')).rejectBy('state', 'passed'); if (relevantJobs.length > 0) { let jobList; diff --git a/app/components/log-content.js b/app/components/log-content.js index c72db2c278..5f394d90b1 100644 --- a/app/components/log-content.js +++ b/app/components/log-content.js @@ -76,6 +76,7 @@ export default Component.extend({ externalLinks: service(), router: service(), scroller: service(), + logToLogContent: service(), classNameBindings: ['logIsVisible:is-open'], logIsVisible: false, @@ -85,6 +86,8 @@ export default Component.extend({ isShowingRemoveLogModal: false, didInsertElement() { + this.logToLogContent.setLogContent(this); + this.logToLogContent.setLog(this.log); if (this.get('features.debugLogging')) { // eslint-disable-next-line console.log('log view: did insert'); @@ -105,10 +108,6 @@ export default Component.extend({ let parts, ref; if (log || (log = this.log)) { parts = log.get('parts'); - parts.removeArrayObserver(this, { - didChange: 'partsDidChange', - willChange: 'noop' - }); parts.destroy(); log.notifyPropertyChange('parts'); if ((ref = this.lineSelector) != null) { @@ -176,10 +175,6 @@ export default Component.extend({ let parts; if (log || (log = this.log)) { parts = log.get('parts'); - parts.addArrayObserver(this, { - didChange: 'partsDidChange', - willChange: 'noop' - }); parts = parts.slice(0); this.partsDidChange(parts, 0, null, parts.length); } @@ -220,12 +215,13 @@ export default Component.extend({ return this.permissions.hasPermission(repo); }), - canRemoveLog: computed('job', 'job.canRemoveLog', 'hasPermission', function () { + canRemoveLog: computed('job', 'job.canRemoveLog', 'hasPermission', 'currentUser', function () { let job = this.job; let canRemoveLog = this.get('job.canRemoveLog'); let hasPermission = this.hasPermission; + let access = this.currentUser && this.currentUser.hasPermissionToRepo(this.get('job.repo'), 'log_delete'); if (job) { - return canRemoveLog && hasPermission; + return canRemoveLog && hasPermission && access; } }), @@ -247,7 +243,14 @@ export default Component.extend({ }, toggleLog() { - this.toggleProperty('logIsVisible'); + let access = this.currentUser && this.currentUser.hasPermissionToRepo(this.get('job.repo'), 'log_view'); + if (access) { + this.toggleProperty('logIsVisible'); + } else { + if (this.logIsVisible) { + this.toggleProperty('logIsVisible'); + } + } }, toggleRemoveLogModal() { diff --git a/app/components/manage-subscription-button.js b/app/components/manage-subscription-button.js new file mode 100644 index 0000000000..bb93d73f35 --- /dev/null +++ b/app/components/manage-subscription-button.js @@ -0,0 +1,4 @@ +import Component from '@ember/component'; + +export default Component.extend({ +}); diff --git a/app/components/missing-notice.js b/app/components/missing-notice.js new file mode 100644 index 0000000000..bb93d73f35 --- /dev/null +++ b/app/components/missing-notice.js @@ -0,0 +1,4 @@ +import Component from '@ember/component'; + +export default Component.extend({ +}); diff --git a/app/components/no-account.js b/app/components/no-account.js new file mode 100644 index 0000000000..bb93d73f35 --- /dev/null +++ b/app/components/no-account.js @@ -0,0 +1,4 @@ +import Component from '@ember/component'; + +export default Component.extend({ +}); diff --git a/app/components/no-builds.js b/app/components/no-builds.js new file mode 100644 index 0000000000..bb93d73f35 --- /dev/null +++ b/app/components/no-builds.js @@ -0,0 +1,4 @@ +import Component from '@ember/component'; + +export default Component.extend({ +}); diff --git a/app/components/not-active.js b/app/components/not-active.js index 0d4433b02c..2ab6ce59b9 100644 --- a/app/components/not-active.js +++ b/app/components/not-active.js @@ -64,7 +64,7 @@ export default Component.extend({ const response = yield this.api.post(`/repo/${repoId}/activate`); if (response.active) { - this.pusher.subscribe(`repo-${repoId}`); + Travis.pusher.subscribe(`repo-${repoId}`); this.repo.set('active', true); this.flashes.success('Repository has been successfully activated.'); diff --git a/app/components/notice-banner.js b/app/components/notice-banner.js new file mode 100644 index 0000000000..bb93d73f35 --- /dev/null +++ b/app/components/notice-banner.js @@ -0,0 +1,4 @@ +import Component from '@ember/component'; + +export default Component.extend({ +}); diff --git a/app/components/owner/migrate.js b/app/components/owner/migrate.js index 0ccb10f2fe..e2bf3b5b3e 100644 --- a/app/components/owner/migrate.js +++ b/app/components/owner/migrate.js @@ -70,9 +70,9 @@ export default Component.extend({ const { isAllSelected, selectableRepositories, selectedRepositories } = this; if (isAllSelected) { - selectedRepositories.removeObjects(selectableRepositories.toArray()); + selectedRepositories.removeObjects(selectableRepositories); } else { - selectedRepositories.addObjects(selectableRepositories.toArray()); + selectedRepositories.addObjects(selectableRepositories); } }, diff --git a/app/components/owner/wizard.js b/app/components/owner/wizard.js index 85064117dd..66b416c682 100644 --- a/app/components/owner/wizard.js +++ b/app/components/owner/wizard.js @@ -30,7 +30,7 @@ export default Component.extend({ actions: { nextStep() { this.updateStep.perform(1); - if (this.wizardStep > 3) this.get('onClose')(); + if (this.wizardStep > 3) this.onClose(); }, previousStep() { this.updateStep.perform(-1); diff --git a/app/components/plan-usage.js b/app/components/plan-usage.js index 7ff2bcf338..ba66a69301 100644 --- a/app/components/plan-usage.js +++ b/app/components/plan-usage.js @@ -169,7 +169,7 @@ export default Component.extend({ await this.owner.fetchExecutions.perform(moment(this.dateRange.start).format('YYYY-MM-DD'), moment(this.dateRange.end || this.dateRange.start).format('YYYY-MM-DD')); const header = ['Job Id', 'Started at', 'Finished at', 'OS', 'Credits consumed', 'Minutes consumed', 'Repository', 'Owner', 'Sender']; - const data = this.get('executionsDataForCsv'); + const data = this.executionsDataForCsv; this.download.asCSV(fileName, header, data); }, @@ -182,7 +182,7 @@ export default Component.extend({ await this.owner.fetchExecutions.perform(moment(this.dateRange.start).format('YYYY-MM-DD'), moment(this.dateRange.end || this.dateRange.start).format('YYYY-MM-DD')); const header = ['Job Id', 'Sender', 'Credits consumed', 'Date']; - const data = await this.get('userLicenseExecutionsDataForCsv'); + const data = await this.userLicenseExecutionsDataForCsv; this.download.asCSV(fileName, header, data); }, diff --git a/app/components/profile-nav.js b/app/components/profile-nav.js index c0e2c13c7d..46930e262c 100644 --- a/app/components/profile-nav.js +++ b/app/components/profile-nav.js @@ -64,24 +64,47 @@ export default Component.extend({ isOrganization: reads('model.isOrganization'), hasAdminPermissions: reads('model.permissions.admin'), + hasPlanViewPermissions: reads('model.permissions.plan_view'), + hasPlanUsagePermissions: reads('model.permissions.plan_usage'), + hasPlanCreatePermissions: reads('model.permissions.plan_create'), + hasBillingViewPermissions: reads('model.permissions.billing_view'), + hasInvoicesViewPermissions: reads('model.permissions.plan_invoices'), + hasSettingsReadPermissions: reads('model.permissions.settings_read'), isOrganizationAdmin: and('isOrganization', 'hasAdminPermissions'), - showOrganizationSettings: and('isOrganizationAdmin', 'isProVersion'), - - showSubscriptionTab: computed('features.enterpriseVersion', 'model.isAssembla', 'model.isUser', function () { - const isAssemblaUser = this.model.isUser && this.model.isAssembla; - const isEnterprise = this.features.get('enterpriseVersion'); - return !isEnterprise && !isAssemblaUser && !!billingEndpoint; + showOrganizationSettings: computed('isOrganizationAdmin', 'isProVersion', 'hasSettingsReadPermissions', function () { + const forOrganization = !this.isOrganization || this.hasSettingsReadPermissions; + return this.isOrganizationAdmin && this.isProVersion && forOrganization; }), - showPaymentDetailsTab: computed('showSubscriptionTab', 'isOrganization', 'isOrganizationAdmin', 'model.isNotGithubOrManual', function () { - if (this.isOrganization) { - return this.showSubscriptionTab && this.isOrganizationAdmin && this.model.get('isNotGithubOrManual'); - } else { - return this.showSubscriptionTab && this.model.get('isNotGithubOrManual'); - } + + showSubscriptionTab: computed('features.enterpriseVersion', 'hasPlanViewPermissions', + 'hasPlanCreatePermissions', 'model.isAssembla', 'model.isUser', + 'isOrganization', function () { + const forOrganization = !this.isOrganization || + ((this.model.hasSubscription || this.model.hasV2Subscription) && !!this.hasPlanViewPermissions) || + !!this.hasPlanCreatePermissions; + + const isAssemblaUser = this.model.isUser && this.model.isAssembla; + const isEnterprise = this.features.get('enterpriseVersion'); + return !isEnterprise && !isAssemblaUser && !!billingEndpoint && !!forOrganization; + }), + showPaymentDetailsTab: computed('showSubscriptionTab', 'isOrganization', 'isOrganizationAdmin', + 'hasBillingViewPermissions', 'hasInvoicesViewPermissions', 'model.isNotGithubOrManual', function () { + if (this.isOrganization) { + const forOrganization = !this.isOrganization || this.hasBillingViewPermissions || this.hasInvoicesViewPermissions; + + return this.showSubscriptionTab && this.model.get('isNotGithubOrManual') && (this.isOrganizationAdmin || forOrganization); + } else { + return this.showSubscriptionTab && this.model.get('isNotGithubOrManual'); + } + }), + showPlanUsageTab: computed('showSubscriptionTab', 'model.hasCredits', 'hasPlanUsagePermissions', function () { + const forOrganization = !this.isOrganization || this.hasPlanUsagePermissions; + return this.showSubscriptionTab && this.model.hasCredits && forOrganization; }), - showPlanUsageTab: and('showSubscriptionTab', 'model.hasCredits'), - usersUsage: computed('account.allowance.userUsage', 'addonUsage', function () { - const userUsage = this.model.allowance.get('userUsage'); + + usersUsage: computed('account.allowance.userUsage', 'addonUsage', 'hasPlanUsagePermissions', function () { + // const forOrganization = !this.isOrganization || this.hasPlanUsagePermissions; + const userUsage = this.model.allowance.userUsage; if (userUsage === undefined) { return true; } @@ -106,26 +129,26 @@ export default Component.extend({ return; } - if (allowance.get('paymentChangesBlockCredit') || allowance.get('paymentChangesBlockCaptcha')) { + if (allowance.paymentChangesBlockCredit || allowance.paymentChangesBlockCaptcha) { let time; - if (allowance.get('paymentChangesBlockCaptcha')) time = allowance.get('captchaBlockDuration'); - if (allowance.get('paymentChangesBlockCredit')) time = allowance.get('creditCardBlockDuration'); + if (allowance.paymentChangesBlockCaptcha) time = allowance.captchaBlockDuration; + if (allowance.paymentChangesBlockCredit) time = allowance.creditCardBlockDuration; this.flashes.custom('flashes/payment-details-edit-lock', { owner: this.model, isUser: this.model.isUser, time: time}, 'warning'); } - if (allowance.get('subscriptionType') !== 2) { + if (allowance.subscriptionType !== 2) { return; } - if (!allowance.get('privateRepos') && !allowance.get('publicRepos') && (this.isOrganizationAdmin || this.model.isUser)) { + if (!allowance.privateRepos && !allowance.publicRepos && (this.isOrganizationAdmin || this.model.isUser)) { this.flashes.custom('flashes/negative-balance-private-and-public', { owner: this.model, isUser: this.model.isUser }, 'warning'); - } else if (!allowance.get('privateRepos') && (this.isOrganizationAdmin || this.model.isUser)) { + } else if (!allowance.privateRepos && (this.isOrganizationAdmin || this.model.isUser)) { this.flashes.custom('flashes/negative-balance-private', { owner: this.model, isUser: this.model.isUser }, 'warning'); - } else if (!allowance.get('publicRepos') && (this.isOrganizationAdmin || this.model.isUser)) { + } else if (!allowance.publicRepos && (this.isOrganizationAdmin || this.model.isUser)) { this.flashes.custom('flashes/negative-balance-public', { owner: this.model, isUser: this.model.isUser }, 'warning'); } - if (allowance.get('pendingUserLicenses')) { + if (allowance.pendingUserLicenses) { this.flashes.custom('flashes/pending-user-licenses', { owner: this.model, isUser: this.model.isUser }, 'warning'); } else if (!this.usersUsage) { this.flashes.custom('flashes/users-limit-exceeded', { owner: this.model, isUser: this.model.isUser }, 'warning'); @@ -135,7 +158,7 @@ export default Component.extend({ willDestroyElement() { const allowance = this.model.allowance; - if (allowance && allowance.get('subscriptionType') === 2) { + if (allowance && allowance.subscriptionType === 2) { this.flashes.removeCustomsByClassName('warning'); } }, diff --git a/app/components/raw-config.js b/app/components/raw-config.js index e827b9c28a..4c4d660731 100644 --- a/app/components/raw-config.js +++ b/app/components/raw-config.js @@ -33,7 +33,7 @@ export default Component.extend({ try { return JSON.stringify(JSON.parse(config), null, 2); } catch (e) { - return config; + return config || "{}"; } }), diff --git a/app/components/repo-actions.js b/app/components/repo-actions.js index 586e45c753..02803a77dd 100644 --- a/app/components/repo-actions.js +++ b/app/components/repo-actions.js @@ -4,6 +4,7 @@ import { computed } from '@ember/object'; import { alias, and, not, or, reads } from '@ember/object/computed'; import eventually from 'travis/utils/eventually'; import { task, taskGroup } from 'ember-concurrency'; +import { capitalize } from "@ember/string"; export default Component.extend({ flashes: service(), @@ -38,6 +39,7 @@ export default Component.extend({ userHasPermissionForRepo: computed('repo.id', 'user', 'user.permissions.[]', function () { let repo = this.repo; let user = this.user; + if (user && repo) { return user.hasAccessToRepo(repo); } @@ -58,6 +60,27 @@ export default Component.extend({ return user.hasPushAccessToRepo(repo); } }), + userHasCancelPermissionForRepo: computed('repo.id', 'user', function () { + let repo = this.repo; + let user = this.user; + if (user && repo) { + return user.hasPermissionToRepo(repo, 'build_cancel'); + } + }), + userHasRestartPermissionForRepo: computed('repo.id', 'user', function () { + let repo = this.repo; + let user = this.user; + if (user && repo) { + return user.hasPermissionToRepo(repo, 'build_restart'); + } + }), + userHasDebugPermissionForRepo: computed('repo.id', 'user', function () { + let repo = this.repo; + let user = this.user; + if (user && repo) { + return user.hasPermissionToRepo(repo, 'build_debug'); + } + }), canOwnerBuild: reads('repo.canOwnerBuild'), ownerRoMode: reads('repo.owner.ro_mode'), @@ -68,9 +91,9 @@ export default Component.extend({ showPriority: true, showPrioritizeBuildModal: false, - canCancel: and('userHasPullPermissionForRepo', 'item.canCancel'), - canRestart: and('userHasPullPermissionForRepo', 'item.canRestart'), - canDebug: and('userHasPushPermissionForRepo', 'item.canDebug'), + canCancel: and('userHasCancelPermissionForRepo', 'item.canCancel'), + canRestart: and('userHasRestartPermissionForRepo', 'item.canRestart'), + canDebug: and('userHasDebugPermissionForRepo', 'item.canDebug'), isHighPriority: or('item.priority', 'item.build.priority'), isNotAlreadyHighPriority: not('isHighPriority'), hasPrioritizePermission: or('item.permissions.prioritize', 'item.build.permissions.prioritize'), @@ -83,7 +106,7 @@ export default Component.extend({ yield eventually(this.item, (record) => { record.cancel().then(() => { - this.flashes.success(`${type.capitalize()} has been successfully cancelled.`); + this.flashes.success(`${capitalize(type)} has been successfully cancelled.`); }, (xhr) => { this.displayFlashError(xhr.status, 'cancel'); }); diff --git a/app/components/repo-show-tools.js b/app/components/repo-show-tools.js index 9272a08d4c..a349b9d0fe 100644 --- a/app/components/repo-show-tools.js +++ b/app/components/repo-show-tools.js @@ -27,12 +27,16 @@ export default Component.extend({ displaySettingsLink: computed('permissions.all', 'repo', function () { let repo = this.repo; - return this.permissions.hasPushPermission(repo); + const forRepo = repo.permissions?.settings_read; + + return forRepo && this.permissions.hasPushPermission(repo); }), displayCachesLink: computed('permissions.all', 'repo', function () { let repo = this.repo; - return this.permissions.hasPushPermission(repo) && config.endpoints.caches; + const forRepo = repo.permissions?.cache_view; + + return forRepo && this.permissions.hasPushPermission(repo) && config.endpoints.caches; }), displayStatusImages: computed('permissions.all', 'repo', function () { @@ -49,11 +53,12 @@ export default Component.extend({ let canTriggerBuild = this.get('repo.permissions.create_request'); let enterprise = this.get('features.enterpriseVersion'); let pro = this.get('features.proVersion'); + const forRepo = this.repo.permissions?.build_create; if (enterprise || pro) { - return canTriggerBuild; + return canTriggerBuild && forRepo; } - return canTriggerBuild && migrationStatus !== 'migrated'; + return canTriggerBuild && migrationStatus !== 'migrated' && forRepo; } ), diff --git a/app/components/repository-filter.js b/app/components/repository-filter.js index 254fd89b10..963272f925 100644 --- a/app/components/repository-filter.js +++ b/app/components/repository-filter.js @@ -4,7 +4,7 @@ import config from 'travis/config/environment'; import { inject as service } from '@ember/service'; import { or, notEmpty } from '@ember/object/computed'; import { isPresent } from '@ember/utils'; -import { htmlSafe } from '@ember/string'; +import { htmlSafe } from '@ember/template'; import fuzzyMatch from 'travis/utils/fuzzy-match'; export default Component.extend({ @@ -25,7 +25,8 @@ export default Component.extend({ }).restartable(), computeName(name, query) { - return isPresent(query) ? htmlSafe(fuzzyMatch(name, query)) : name; + const fuzzyMatchConst = fuzzyMatch(name, query) + return isPresent(query) ? htmlSafe(`${fuzzyMatchConst}`) : name; }, didReceiveAttrs() { diff --git a/app/components/repository-layout.js b/app/components/repository-layout.js index 8a98668ef5..037e918101 100644 --- a/app/components/repository-layout.js +++ b/app/components/repository-layout.js @@ -2,12 +2,14 @@ import Component from '@ember/component'; import { computed } from '@ember/object'; import { inject as service } from '@ember/service'; import { alias, reads } from '@ember/object/computed'; +import { capitalize } from "@ember/string"; export default Component.extend({ auth: service(), externalLinks: service(), features: service(), flashes: service(), + router: service(), isProVersion: reads('features.proVersion'), isShowingTriggerBuildModal: false, isShowingStatusBadgeModal: false, @@ -16,7 +18,7 @@ export default Component.extend({ scansEnabled: reads('features.logScanner'), repositoryProvider: computed('repo.provider', function () { - return this.repo.provider.capitalize(); + return capitalize(this.repo.provider); }), repositoryType: computed('repo.serverType', function () { @@ -30,6 +32,10 @@ export default Component.extend({ } }), + currentRouteName: computed('router.currentRouteName', function () { + return this.router.currentRouteName; + }), + repoUrl: computed('repo.{ownerName,vcsName,vcsType}', function () { const owner = this.get('repo.ownerName'); const repo = this.get('repo.vcsName'); @@ -57,11 +63,13 @@ export default Component.extend({ }, toggleTriggerBuildModal() { this.toggleProperty('isShowingTriggerBuildModal'); - } + }, + + }, didRender() { - const repo = this.get('repo'); + const repo = this.repo; if (repo.hasBuildBackups === undefined) { repo.fetchInitialBuildBackups.perform(); @@ -77,9 +85,9 @@ export default Component.extend({ } else { this.flashes.custom('flashes/negative-balance-public', { owner: repo.owner, isUser: isUser }, 'warning'); } - if (allowance.get('pendingUserLicenses')) { + if (allowance.pendingUserLicenses) { this.flashes.custom('flashes/pending-user-licenses', { owner: repo.owner, isUser: isUser }, 'warning'); - } else if (!allowance.get('userUsage')) { + } else if (allowance && !allowance.userUsage) { this.flashes.custom('flashes/users-limit-exceeded', { owner: repo.owner, isUser: isUser }, 'warning'); } } else if (this.userRoMode && ownerRoMode) { diff --git a/app/components/repository-sidebar.js b/app/components/repository-sidebar.js index e10502b416..fa06ec8f1e 100644 --- a/app/components/repository-sidebar.js +++ b/app/components/repository-sidebar.js @@ -61,6 +61,8 @@ export default Component.extend({ }, onQueryChange(query) { + if (query.target) // might be KeyboardEvent + query = query.target.value; if (query === '' || query === this.get('repositories.searchQuery')) { return; } this.set('repositories.searchQuery', query); this.get('repositories.showSearchResults').perform(); diff --git a/app/components/repository-status-toggle.js b/app/components/repository-status-toggle.js index d8c7cd7100..5372e8d67f 100644 --- a/app/components/repository-status-toggle.js +++ b/app/components/repository-status-toggle.js @@ -74,7 +74,7 @@ export default Component.extend({ try { yield repository.toggle(); yield repository.reload(); - this.pusher.subscribe(`repo-${repository.id}`); + Travis.pusher.subscribe(`repo-${repository.id}`); } catch (error) { this.set('apiError', error); } diff --git a/app/components/repository-visibility-icon.js b/app/components/repository-visibility-icon.js new file mode 100644 index 0000000000..bb93d73f35 --- /dev/null +++ b/app/components/repository-visibility-icon.js @@ -0,0 +1,4 @@ +import Component from '@ember/component'; + +export default Component.extend({ +}); diff --git a/app/components/request-config.js b/app/components/request-config.js index 7dbb4fdf74..11a3e4ba92 100644 --- a/app/components/request-config.js +++ b/app/components/request-config.js @@ -15,11 +15,11 @@ export default Component.extend({ }), formattedConfig: computed('config', 'slug', function () { - const config = this.get('config'); + const config = this.config; try { return JSON.stringify(config, null, 2); } catch (e) { - return config; + return config ? config : "{}"; } }), diff --git a/app/components/requests-item.js b/app/components/requests-item.js index d4094cf438..9d04bd6884 100644 --- a/app/components/requests-item.js +++ b/app/components/requests-item.js @@ -1,6 +1,7 @@ import Component from '@ember/component'; import { computed } from '@ember/object'; import { reads } from '@ember/object/computed'; +import { capitalize } from "@ember/string"; export default Component.extend({ classNames: ['request-item'], @@ -24,7 +25,7 @@ export default Component.extend({ status: computed('request.result', function () { let result = this.get('request.result'); - return result.capitalize(); + return capitalize(result); }), message: computed('features.proVersion', 'request.message', function () { diff --git a/app/components/resubscribe-button.js b/app/components/resubscribe-button.js new file mode 100644 index 0000000000..bb93d73f35 --- /dev/null +++ b/app/components/resubscribe-button.js @@ -0,0 +1,4 @@ +import Component from '@ember/component'; + +export default Component.extend({ +}); diff --git a/app/components/scan-result-item.js b/app/components/scan-result-item.js new file mode 100644 index 0000000000..bb93d73f35 --- /dev/null +++ b/app/components/scan-result-item.js @@ -0,0 +1,4 @@ +import Component from '@ember/component'; + +export default Component.extend({ +}); diff --git a/app/components/status-images.js b/app/components/status-images.js index 002ab34207..630e2d6248 100644 --- a/app/components/status-images.js +++ b/app/components/status-images.js @@ -2,7 +2,10 @@ import Component from '@ember/component'; import { inject as service } from '@ember/service'; import { computed } from '@ember/object'; import { reads } from '@ember/object/computed'; -import { bindKeyboardShortcuts, unbindKeyboardShortcuts } from 'ember-keyboard-shortcuts'; +import { + bindKeyboardShortcuts, + unbindKeyboardShortcuts +} from 'ember-keyboard-shortcuts'; import { task } from 'ember-concurrency'; import { IMAGE_FORMATS } from 'travis/services/status-images'; import BranchSearching from 'travis/mixins/branch-searching'; diff --git a/app/components/top-bar.js b/app/components/top-bar.js index 3b1bf4ac1f..d6962a6ac5 100644 --- a/app/components/top-bar.js +++ b/app/components/top-bar.js @@ -1,87 +1,113 @@ import { scheduleOnce } from '@ember/runloop'; import Component from '@ember/component'; import Ember from 'ember'; -import { computed, setProperties, set } from '@ember/object'; +import { + computed, + setProperties, + set, + action +} from '@ember/object'; import { reads } from '@ember/object/computed'; import { inject as service } from '@ember/service'; -import InViewportMixin from 'ember-in-viewport'; - -export default Component.extend(InViewportMixin, { - auth: service(), - store: service(), - externalLinks: service(), - features: service(), - flashes: service(), - router: service(), - storage: service(), - - tagName: 'header', - classNames: ['top'], - classNameBindings: ['isWhite:top--white'], - isWhite: false, - landingPage: false, - isNavigationOpen: false, - isActivation: false, - - activeModel: null, - model: reads('activeModel'), - - user: reads('auth.currentUser'), - isUnconfirmed: computed('user.confirmedAt', function () { +export default class TopBar extends Component { + @service auth; + @service store; + @service externalLinks; + @service features; + @service flashes; + @service router; + @service storage; + @service inViewport + + tagName = 'header'; + classNames = ['top']; + classNameBindings = ['isWhite:top--white']; + isWhite = false; + landingPage = false; + isNavigationOpen = false; + isActivation = false; + viewportTolerance = { top: 0, bottom: 0, left: 0, right: 0 }; + activeModel = null; + @reads('activeModel') model; + + @reads('auth.currentUser') user; + + @computed('user.confirmedAt') + get isUnconfirmed() { if (!this.user || - (this.storage.wizardStep > 0 && this.storage.wizardStep <= 1) || - this.router.currentRouteName == 'first_sync' || - this.router.currentRouteName == 'github_apps_installation') + (this.storage.wizardStep > 0 && this.storage.wizardStep <= 1) || + this.router.currentRouteName == 'first_sync' || + this.router.currentRouteName == 'github_apps_installation') { return false; + } return !this.user.confirmedAt; - }), + } - userName: computed('user.{login,name}', function () { - let login = this.get('user.login'); - let name = this.get('user.name'); + @computed('user.{login,name}') + get userName() { + let login = this.user.login; + let name = this.user.name; return name || login; - }), + } - showCta: computed('auth.signedIn', 'landingPage', 'features.landingPageCta', function () { - let signedIn = this.get('auth.signedIn'); + @computed('auth.signedIn', 'landingPage', 'features.landingPageCta') + get showCta() { + let signedIn = this.auth.signedIn; let landingPage = this.landingPage; - let ctaEnabled = this.get('features.landingPageCta'); + let ctaEnabled = this.features.landingPageCta; return !signedIn && !landingPage && ctaEnabled; - }), + } - hasNoPlan: computed('model.allowance.subscriptionType', 'model.hasV2Subscription', 'model.subscription', function () { - return !this.get('model.hasV2Subscription') && this.get('model.subscription') === undefined && this.get('model.allowance.subscriptionType') === 3; - }), + @computed('model.allowance.subscriptionType', 'model.hasV2Subscription', 'model.subscription') + get hasNoPlan() { + if(!this.model) return false; // logged out + return !this.model.hasV2Subscription && this.model.subscription === undefined && this.model.allowance && this.model.allowance.subscriptionType === 3; + } + + @action + setupInViewport() { + const loader = document.getElementById('loader'); + const viewportTolerance = { bottom: 200 }; + const { onEnter, _onExit } = this.inViewport.watchElement(loader, { viewportTolerance }); + // pass the bound method to `onEnter` or `onExit` + onEnter(this.didEnterViewport.bind(this)); + } + + willDestroy() { + // need to manage cache yourself if you don't use the mixin + const loader = document.getElementById('loader'); + this.inViewport.stopWatching(loader); + + super.willDestroy(...arguments); + } didInsertElement() { if (Ember.testing) { - this._super(...arguments); + super.didInsertElement(...arguments); return; } - setProperties(this, { - viewportSpy: true - }); - this._super(...arguments); + + set(this, 'viewportSpy', true); + super.didInsertElement(...arguments); scheduleOnce('afterRender', this, () => { const { clientHeight = 76 } = this.element; set(this, 'viewportTolerance.top', clientHeight); }); - }, + } didEnterViewport() { this.flashes.set('topBarVisible', true); - }, + } didExitViewport() { this.flashes.set('topBarVisible', false); - }, + } - actions: { - toggleNavigation() { - this.toggleProperty('isNavigationOpen'); - } + @action + toggleNavigation() { + this.toggleProperty('isNavigationOpen'); } -}); +} diff --git a/app/components/travis-form.js b/app/components/travis-form.js index dbc01993d8..0ee088a94b 100644 --- a/app/components/travis-form.js +++ b/app/components/travis-form.js @@ -17,7 +17,7 @@ export default Component.extend({ onSubmit() {}, registerField(field) { - this.fields.addObject(field); + this.fields.push(field); }, unregisterField(field) { diff --git a/app/components/trigger-custom-build.js b/app/components/trigger-custom-build.js index 8cf2ce42a9..e7d65f1963 100644 --- a/app/components/trigger-custom-build.js +++ b/app/components/trigger-custom-build.js @@ -1,6 +1,6 @@ import Component from '@ember/component'; import { task, timeout } from 'ember-concurrency'; -import YAML from 'yamljs'; +import yaml from 'js-yaml'; import config from 'travis/config/environment'; import { inject as service } from '@ember/service'; import { @@ -91,7 +91,7 @@ export default Component.extend(BranchSearching, { return { request: { branch, - config: YAML.parse(triggerBuildConfig), + config: yaml.load(triggerBuildConfig), message: message || undefined } }; diff --git a/app/components/ui-kit/button-signin.js b/app/components/ui-kit/button-signin.js index 8fa797baef..1c693c66f3 100644 --- a/app/components/ui-kit/button-signin.js +++ b/app/components/ui-kit/button-signin.js @@ -2,6 +2,7 @@ import Component from '@ember/component'; import { inject as service } from '@ember/service'; import { computed } from '@ember/object'; import { or, reads } from '@ember/object/computed'; +import { capitalize } from "@ember/string"; export default Component.extend({ tagName: '', @@ -13,7 +14,8 @@ export default Component.extend({ account: null, isSignup: false, - provider: or('account.provider', 'multiVcs.primaryProvider'), + overriddenProvider: null, + provider: or('overriddenProvider', 'account.provider', 'multiVcs.primaryProvider'), isLogoVisible: true, isLogoSeparatorVisible: true, isBetaBadgeVisible: reads('isBetaProvider'), @@ -21,8 +23,13 @@ export default Component.extend({ isLoading: false, + vcnTypeOverride: null, + vcsType: computed('provider', function () { - return `${this.provider.replace('-', '').capitalize()}User`; + if (this.vcsTypeOverride) + return this.vcsTypeOverride; + + return `${capitalize(this.provider.replace('-', ''))}User`; }), isPrimaryProvider: computed('provider', function () { diff --git a/app/components/ui-kit/button.js b/app/components/ui-kit/button.js index cf9f932ae1..1e9c330f4a 100644 --- a/app/components/ui-kit/button.js +++ b/app/components/ui-kit/button.js @@ -59,11 +59,14 @@ export default Component.extend({ width: DEFAULT_WIDTH, invert: false, disabled: false, + customBgColor: null, onClick() {}, // Private - bgColor: computed('color', 'disabled', 'invert', function () { + bgColor: computed('customBgColor', 'color', 'disabled', 'invert', function () { + if (this.customBgColor) + return this.customBgColor; return this.invert ? BG_COLORS['invert'] : this.disabled diff --git a/app/components/ui-kit/link.js b/app/components/ui-kit/link.js index c53bb5a69a..3a45d59215 100644 --- a/app/components/ui-kit/link.js +++ b/app/components/ui-kit/link.js @@ -1,6 +1,10 @@ import Component from '@ember/component'; import { checkColor } from 'travis/utils/ui-kit/assertions'; -import { COLORS, TEXT_COLORS, DEFAULT_TEXT_COLOR } from 'travis/components/ui-kit/text'; +import { + COLORS, + TEXT_COLORS, + DEFAULT_TEXT_COLOR +} from 'travis/components/ui-kit/text'; import prefix from 'travis/utils/ui-kit/prefix'; import concat from 'travis/utils/ui-kit/concat'; diff --git a/app/components/unconfirmed-user-banner.js b/app/components/unconfirmed-user-banner.js new file mode 100644 index 0000000000..bb93d73f35 --- /dev/null +++ b/app/components/unconfirmed-user-banner.js @@ -0,0 +1,4 @@ +import Component from '@ember/component'; + +export default Component.extend({ +}); diff --git a/app/components/visibility-setting-list.js b/app/components/visibility-setting-list.js index 4fd653635c..e2e863833f 100644 --- a/app/components/visibility-setting-list.js +++ b/app/components/visibility-setting-list.js @@ -13,6 +13,7 @@ import { bindKeyboardShortcuts, unbindKeyboardShortcuts } from 'ember-keyboard-shortcuts'; +import { A } from '@ember/array'; export default Component.extend({ classNames: ['visibility-setting-list'], @@ -33,7 +34,7 @@ export default Component.extend({ // `displayValue` is used to generate text for the modal // `description` is for the label next to the radio button // `modalText` can be used to override the generated modal text - options: computed(() => []), + options: [], isEmpty: empty('options'), isVisible: not('isEmpty'), diff --git a/app/controllers/account/settings.js b/app/controllers/account/settings.js index eed5cdf1d4..006492c82b 100644 --- a/app/controllers/account/settings.js +++ b/app/controllers/account/settings.js @@ -33,6 +33,7 @@ export default Controller.extend({ auth: service(), preferences: service(), flashes: service(), + store: service(), queryParams: ['section'], section: SECTION.NONE, @@ -116,11 +117,11 @@ export default Controller.extend({ this.toggleProperty('isShowingAddKeyModal'); }, customKeyDeleted(key) { - const keys = this.get('customKeysLoaded'); + const keys = this.customKeysLoaded; this.set('customKeysLoaded', keys.filter(obj => obj.id !== key.id)); }, customKeyAdded(key) { - this.get('customKeysLoaded').pushObject(key); + this.customKeysLoaded.pushObject(key); } }, diff --git a/app/controllers/branches.js b/app/controllers/branches.js index 59233cb07c..9429f4b987 100644 --- a/app/controllers/branches.js +++ b/app/controllers/branches.js @@ -2,6 +2,7 @@ import { isNone } from '@ember/utils'; import { get, computed } from '@ember/object'; import Controller, { inject as controller } from '@ember/controller'; import { alias, notEmpty, filter } from '@ember/object/computed'; +import {A} from '@ember/array'; export default Controller.extend({ repoController: controller('repo'), @@ -34,8 +35,7 @@ export default Controller.extend({ return isNone(finishedAt); }); - const sortedFinished = branches - .filterBy('last_build.finished_at') + const sortedFinished = A(branches.filterBy('last_build.finished_at')) .sortBy('last_build.finished_at') .reverse(); diff --git a/app/controllers/build.js b/app/controllers/build.js index 1cce3aae3b..0af9928ea3 100644 --- a/app/controllers/build.js +++ b/app/controllers/build.js @@ -13,6 +13,8 @@ export default Controller.extend(Polling, { updateTimesService: service('updateTimes'), repoController: controller('repo'), + queryParams: ['currentTab'], + currentTab: null, config, diff --git a/app/controllers/builds.js b/app/controllers/builds.js index c31a36e3bb..940465cd0c 100644 --- a/app/controllers/builds.js +++ b/app/controllers/builds.js @@ -12,9 +12,10 @@ export default Controller.extend(...mixins, { features: service(), externalLinks: service(), permissions: service(), + refreshService: service(), buildsSorting: ['number:desc'], - builds: sort('model', 'buildsSorting'), + builds: sort('model.content', 'buildsSorting'), oldBuilds: [], repoController: controller('repo'), @@ -35,7 +36,7 @@ export default Controller.extend(...mixins, { hasBuildBackups: reads('repo.hasBuildBackups'), displayShowMoreButton: computed('tab', 'loadMoreBuilds.isRunning', 'builds', function () { - const builds = this.get('builds'); + const builds = this.builds; let tab = this.tab; if (this.oldBuilds.length === builds.length) { diff --git a/app/controllers/caches.js b/app/controllers/caches.js index 147abf708b..f6d53cd11d 100644 --- a/app/controllers/caches.js +++ b/app/controllers/caches.js @@ -2,33 +2,47 @@ import EmberObject, { computed } from '@ember/object'; import Controller from '@ember/controller'; import config from 'travis/config/environment'; import { inject as service } from '@ember/service'; -import { alias } from '@ember/object/computed'; +import { alias, reads } from '@ember/object/computed'; import { task } from 'ember-concurrency'; +import {tracked} from "@glimmer/tracking"; -export default Controller.extend({ - api: service(), - flashes: service(), +export default class extends Controller { + @service api; + @service flashes; - repo: alias('model.repo'), + config = config; - config, + @tracked pushes; + @tracked pullRequests; + @tracked repo; - cachesExist: computed('model.pushes.[]', 'model.pullRequests.[]', function () { - let pushes = this.get('model.pushes'); - let pullRequests = this.get('model.pullRequests'); - if (pushes || pullRequests) { - return pushes.length || pullRequests.length; - } - }), + constructor() { + super(...arguments); + } + + @computed('pushes.[]', 'pullRequests.[]') + get cachesExist() { + return this.pushes?.length || this.pullRequests?.length; + } - deleteRepoCache: task(function* () { + @task(function* () { if (config.skipConfirmations || confirm('Are you sure?')) { try { - yield this.api.delete(`/repo/${this.get('repo.id')}/caches`); - this.set('model', EmberObject.create()); + yield this.api.delete(`/repo/${this.repo.id}/caches`); + this.set('pullRequests', EmberObject.create()); + this.set('pushes', EmberObject.create()); } catch (e) { this.flashes.error('Could not delete the caches'); } } }).drop() -}); + deleteRepoCache; + + reassignPullRequests(val, component) { + component.set('pullRequests', val); + } + + reassignPushes(val, component) { + component.set('pushes', val); + } +} diff --git a/app/controllers/dashboard.js b/app/controllers/dashboard.js index ccc1d37e8d..bd39e460a0 100644 --- a/app/controllers/dashboard.js +++ b/app/controllers/dashboard.js @@ -40,7 +40,7 @@ export default Controller.extend({ 'model.starredRepos.@each.currentBuildFinishedAt', function () { let repositories = this.get('model.starredRepos'); - return repositories.toArray().sort(dashboardRepositoriesSort); + return (repositories.content || []).sort(dashboardRepositoriesSort); } ) }); diff --git a/app/controllers/dashboard/repositories.js b/app/controllers/dashboard/repositories.js index 9508bc561f..df3405b953 100644 --- a/app/controllers/dashboard/repositories.js +++ b/app/controllers/dashboard/repositories.js @@ -18,7 +18,7 @@ export default Controller.extend({ 'model.starredRepos.@each.currentBuildFinishedAt', function () { let repositories = this.get('model.starredRepos'); - return repositories.toArray().sort(dashboardRepositoriesSort); + return (repositories.content || []).sort(dashboardRepositoriesSort); } ), diff --git a/app/controllers/index.js b/app/controllers/index.js index fd9d6999e5..bd4005b699 100644 --- a/app/controllers/index.js +++ b/app/controllers/index.js @@ -19,7 +19,7 @@ export default Controller.extend({ init() { this._super(...arguments); if (!Ember.testing) { - return Visibility.every(config.intervals.updateTimes, this.updateTimes.bind(this)); + return Visibility.every(config.intervals.updateTimes, this.updateTimes.bind(this)); } }, diff --git a/app/controllers/organization/settings.js b/app/controllers/organization/settings.js index 28567531f4..d827970eda 100644 --- a/app/controllers/organization/settings.js +++ b/app/controllers/organization/settings.js @@ -81,7 +81,7 @@ export default Controller.extend({ }, customKeyDeleted(key) { - const keys = this.get('customKeys'); + const keys = this.customKeys; this.set('model.organization.customKeys', keys.filter(obj => obj.id !== key.id)); }, diff --git a/app/controllers/repo.js b/app/controllers/repo.js index ebc33cb8b4..617a336253 100644 --- a/app/controllers/repo.js +++ b/app/controllers/repo.js @@ -19,6 +19,7 @@ export default Controller.extend({ queryParams: ['migrationStatus', 'serverType'], serverType: null, migrationStatus: null, + observing: false, jobController: controller('job'), buildController: controller('build'), @@ -111,11 +112,15 @@ export default Controller.extend({ }, stopObservingLastBuild() { + if (!this.observing) + return; + this.set('observing', false); return this.removeObserver('repo.currentBuild', this, 'currentBuildDidChange'); }, observeLastBuild() { this.currentBuildDidChange(); + this.set('observing', true); return this.addObserver('repo.currentBuild', this, 'currentBuildDidChange'); } }); diff --git a/app/controllers/repo/index.js b/app/controllers/repo/index.js index a2b253b71e..6a015dddf8 100644 --- a/app/controllers/repo/index.js +++ b/app/controllers/repo/index.js @@ -2,6 +2,7 @@ import Controller from '@ember/controller'; import { computed } from '@ember/object'; import { reads, and, or, not } from '@ember/object/computed'; import { inject as service } from '@ember/service'; +import { A } from '@ember/array'; export default Controller.extend({ auth: service(), diff --git a/app/helpers/commit-link.js b/app/helpers/commit-link.js index 0ff5fe5039..43326ce816 100644 --- a/app/helpers/commit-link.js +++ b/app/helpers/commit-link.js @@ -1,11 +1,9 @@ -import Ember from 'ember'; -import { htmlSafe } from '@ember/string'; +import { escape } from 'travis/helpers/format-message'; +import { htmlSafe } from '@ember/template'; import Helper from '@ember/component/helper'; import { inject as service } from '@ember/service'; import formatCommit from 'travis/utils/format-commit'; -const { escapeExpression: escape } = Ember.Handlebars.Utils; - export default Helper.extend({ externalLinks: service(), @@ -24,6 +22,6 @@ export default Helper.extend({ const commitUrl = this.externalLinks.commitUrl(vcsType, { owner, repo, commit }); const url = escape(commitUrl); const string = `${commit}`; - return new htmlSafe(string); + return new htmlSafe(`${string}`); } }); diff --git a/app/helpers/format-commit.js b/app/helpers/format-commit.js index b84d519277..0ee96b671b 100644 --- a/app/helpers/format-commit.js +++ b/app/helpers/format-commit.js @@ -1,10 +1,12 @@ -import { htmlSafe } from '@ember/string'; +import { htmlSafe } from '@ember/template'; import { helper } from '@ember/component/helper'; import formatCommit from 'travis/utils/format-commit'; export default helper((params) => { const [commit] = params; if (commit) { - return new htmlSafe(formatCommit(commit.get('sha'), commit.get('branch'))); + + const theHtml = formatCommit(commit.get('sha'), commit.get('branch')); + return new htmlSafe(`${theHtml}`); } }); diff --git a/app/helpers/format-duration.js b/app/helpers/format-duration.js index 726a3a76bd..763eda86f0 100644 --- a/app/helpers/format-duration.js +++ b/app/helpers/format-duration.js @@ -1,9 +1,9 @@ -import { htmlSafe } from '@ember/string'; +import { htmlSafe } from '@ember/template'; import { helper } from '@ember/component/helper'; import timeInWords from 'travis/utils/time-in-words'; export default helper((params) => { const [time] = params; const timeText = timeInWords(time); - return new htmlSafe(timeText); + return new htmlSafe(`${timeText}`); }); diff --git a/app/helpers/format-message.js b/app/helpers/format-message.js index fcead42df9..c0efd16aba 100644 --- a/app/helpers/format-message.js +++ b/app/helpers/format-message.js @@ -1,5 +1,5 @@ import { helper } from '@ember/component/helper'; -import { htmlSafe } from '@ember/string'; +import { htmlSafe } from '@ember/template'; import { get } from '@ember/object'; import EmojiConvertor from 'emoji-js'; @@ -12,8 +12,8 @@ emojiConvertor.img_sets.apple.path = `${config.emojiPrepend}/images/emoji/`; emojiConvertor.include_title = true; emojiConvertor.allow_native = false; -function escape(text) { - return text +export function escape(text) { + return text.toString() .replace(/&/g, '&') .replace(//g, '>'); @@ -68,6 +68,7 @@ function formatMessage(message, options) { message = handleEventType(message, options.eventType); message = emojiConvertor.replace_colons(message); + console.log(message); return message; } @@ -130,5 +131,5 @@ export default helper((params, options) => { const message = params[0] || ''; const formattedMessage = formatMessage(message, options); - return new htmlSafe(formattedMessage); + return new htmlSafe(`${formattedMessage}`); }); diff --git a/app/helpers/format-sha.js b/app/helpers/format-sha.js index 157ad312eb..7198d32b15 100644 --- a/app/helpers/format-sha.js +++ b/app/helpers/format-sha.js @@ -1,4 +1,4 @@ -import { htmlSafe } from '@ember/string'; +import { htmlSafe } from '@ember/template'; import { helper } from '@ember/component/helper'; import formatSha from 'travis/utils/format-sha'; @@ -6,5 +6,6 @@ export default helper((params) => { let [sha] = params; if (sha && sha.includes('@')) sha = sha.split('@')[1]; const formattedSha = formatSha(sha); - return new htmlSafe(formattedSha); + + return new htmlSafe(`${formattedSha}`); }); diff --git a/app/helpers/format-time.js b/app/helpers/format-time.js index d6f2829d42..aa1c049e29 100644 --- a/app/helpers/format-time.js +++ b/app/helpers/format-time.js @@ -1,9 +1,10 @@ -import { htmlSafe } from '@ember/string'; +import { htmlSafe } from '@ember/template'; import { helper } from '@ember/component/helper'; import timeAgoInWords from 'travis/utils/time-ago-in-words'; export default helper((params) => { const [time] = params; const timeText = timeAgoInWords(time) || '-'; - return new htmlSafe(timeText); + + return new htmlSafe(`${timeText}`); }); diff --git a/app/helpers/pretty-date.js b/app/helpers/pretty-date.js index 0ec5f6234c..5811a19f2f 100644 --- a/app/helpers/pretty-date.js +++ b/app/helpers/pretty-date.js @@ -1,4 +1,4 @@ -import { htmlSafe } from '@ember/string'; +import { htmlSafe } from '@ember/template'; import { helper } from '@ember/component/helper'; @@ -6,7 +6,9 @@ import moment from 'moment'; export function prettyDate(params) { let date = new Date(params[0]); - return new htmlSafe(moment(date).format('MMMM D, YYYY H:mm:ss') || '-'); + const theMoment = moment(date).format('MMMM D, YYYY H:mm:ss') || '-'; + + return new htmlSafe(`${theMoment}`); } export default helper(prettyDate); diff --git a/app/index.html b/app/index.html index 54cfe0c9ca..f6be457f69 100644 --- a/app/index.html +++ b/app/index.html @@ -12,6 +12,9 @@ {{content-for "head"}} + + + diff --git a/app/initializers/array.js b/app/initializers/array.js new file mode 100644 index 0000000000..b05adf327f --- /dev/null +++ b/app/initializers/array.js @@ -0,0 +1,160 @@ +import { A } from '@ember/array' + +export function initialize() { + if (!Array.prototype.compact) { + Array.prototype.compact = function(...params) { + return A(this).compact(...params) + }; + } + + if (!Array.prototype.removeObject) { + Array.prototype.removeObject = function(item) { + const index = this.indexOf(item); + if (index !== -1) { + this.splice(index, 1); + } + }; + } + + if (!Array.prototype.firstObject) { + Object.defineProperty(Array.prototype, 'firstObject', { + get: function() { + return this[0]; + } + }); + } + + if (!Array.prototype.lastObject) { + Object.defineProperty(Array.prototype, 'lastObject', { + get: function() { + return this[this.length - 1]; + } + }); + } + + if (!Array.prototype.pushObject) { + Array.prototype.pushObject = function(item) { + this.push(item); + return this.length; + }; + } + + if (!Array.prototype.uniq) { + Array.prototype.uniq = function() { + return Array.from(new Set(this)); + }; + } + + if (!Array.prototype.addObject) { + Array.prototype.addObject = function (item) { + if (this.indexOf(item) === -1) { + this.push(item); + } + return this; + }; + } + + if (!Array.prototype.any) { + Array.prototype.any = function(...args) { + return this.some(...args); + } + } + + + if (!Array.prototype.mapBy) { + Array.prototype.mapBy = function(property) { + return this.map(item => item[property]); + }; + } + + if (!Array.prototype.addObjects) { + Array.prototype.addObjects = function(items) { + items.forEach(item => { + this.addObject(item); + }); + return this; + }; + } + + if (!Array.prototype.filterBy) { + Array.prototype.filterBy = function(...params) { + return A(this).filterBy(...params); + } + } + + if (!Array.prototype.get) { + Array.prototype.get = function (what) { + let properties = what.split('.'); + let result = A(this); + + for (let i = 0; i < properties.length; i++) { + // 5 is my own limit so can be .get[aa.bb.cc.dd.ee] - max nesting of 5 + if (result === undefined || result === null || i === 5) { + return undefined; + } + result = result[properties[i]]; + } + + return result; + } + } + + if (!Array.prototype.isAny) { + Array.prototype.isAny = function(...params) { + return A(this).isAny(...params) + } + } + + if (!Array.prototype.findBy) { + Array.prototype.findBy = function(...params) { + return A(this).findBy(...params) + } + } + + if (!Array.prototype.uniqBy) { + Array.prototype.uniqBy = function(...params) { + return A(this).uniqBy(...params) + } + } + + if (!Array.prototype.unshiftObject) { + Array.prototype.unshiftObject = function(...params) { + return A(this).unshiftObject(...params) + } + } + + if (!Array.prototype.pushObjects) { + Array.prototype.pushObjects = function(...objects) { + this.push(...objects); + } + + return this.length; + } + + if (!Array.prototype.without) { + Array.prototype.without = function(...params) { + return A(this).without(...params) + } + } + + + Array.prototype.sort = function(...params) { + return A(this).sort(...params) + } + + if (!Array.prototype.rejectBy) { + Array.prototype.rejectBy = function(...params) { + return A(this).rejectBy(...params) + } + } + + if (!Array.prototype.sortBy) { + Array.prototype.sortBy = function(...params) { + return A(this).sortBy(...params) + } + } +} + +export default { + initialize +}; diff --git a/app/initializers/pretender.js b/app/initializers/pretender.js new file mode 100644 index 0000000000..08afb46385 --- /dev/null +++ b/app/initializers/pretender.js @@ -0,0 +1,25 @@ +import Pretender from 'pretender'; +import config from 'travis/config/environment'; + +const { validAuthToken } = config; + +const pretender = { + name: 'pretender', + + initialize: function () { + const originalHandlerFor = Pretender.prototype._handlerFor; + + Pretender.prototype._handlerFor = function (verb, path, request) { + const authHeader = request.requestHeaders.Authorization; + if (authHeader && authHeader !== `token ${validAuthToken}`) { + // Handle unauthorized case + return originalHandlerFor.call(this, 'GET', '/unauthorized', request); + } + + // Proceed with original behavior + return originalHandlerFor.apply(this, arguments); + }; + } +} + +export default pretender; diff --git a/app/initializers/store.js b/app/initializers/store.js new file mode 100644 index 0000000000..47aff8911b --- /dev/null +++ b/app/initializers/store.js @@ -0,0 +1,10 @@ +// app/initializers/store-initializer.js +import ExtendedStore from 'travis/services/store'; // Adjust the path to your ExtendedStore + +export function initialize(application) { + // application.register('service:store', ExtendedStore, { singleton: true, instantiate: true }); +} + +export default { + initialize +}; diff --git a/app/instance-initializers/pusher.js b/app/instance-initializers/pusher.js index a662711b71..fe2ffb8543 100644 --- a/app/instance-initializers/pusher.js +++ b/app/instance-initializers/pusher.js @@ -10,8 +10,6 @@ export function initialize(applicationInstance) { instantiate: false }); } - app.inject('route', 'pusher', 'pusher:main'); - app.inject('component', 'pusher', 'pusher:main'); app.pusher.store = applicationInstance.lookup('service:store'); app.pusher.pusherService = applicationInstance.lookup('service:pusher'); } diff --git a/app/mixins/builds/load-more.js b/app/mixins/builds/load-more.js index 73cd02aa56..70b3bb1c8e 100644 --- a/app/mixins/builds/load-more.js +++ b/app/mixins/builds/load-more.js @@ -4,6 +4,8 @@ import { task } from 'ember-concurrency'; export default Mixin.create({ tabStates: service(), + store: service(), + refreshService: service(), loadMoreBuilds: task(function* () { let number = this.get('builds.lastObject.number'); @@ -30,13 +32,18 @@ export default Mixin.create({ const singularTab = tabName.substr(0, tabName.length - 1); const type = tabName === 'builds' ? 'push' : singularTab; const options = this._constructOptions(type); - yield this.store.query('build', options); + // yield this.store.query('build', options); + if (type === 'push') { + this.refreshService.refreshBuildsInRepos.perform(this.repo); + } else { + this.refreshService.refreshRequestsInRepos.perform(this.repo); + } }).drop(), _constructOptions(type) { let options = { repository_id: this.get('repo.id'), - offset: this.get('builds.length'), + offset: this.get('builds.length'), }; if (type != null) { options.event_type = type.replace(/s$/, ''); diff --git a/app/mixins/components/with-config-validation.js b/app/mixins/components/with-config-validation.js index e4b710bf3b..bd79eb8e86 100644 --- a/app/mixins/components/with-config-validation.js +++ b/app/mixins/components/with-config-validation.js @@ -2,6 +2,7 @@ import Mixin from '@ember/object/mixin'; import { and, gt, reads } from '@ember/object/computed'; import { computed } from '@ember/object'; import { inject as service } from '@ember/service'; +import { A } from '@ember/array'; export default Mixin.create({ auth: service(), @@ -17,7 +18,7 @@ export default Mixin.create({ messagesMaxLevel: computed('messages.@each.level', function () { if (this.hasMessages) { - return this.messages.sortBy('level').lastObject.level; + return A(this.messages).sortBy('level').lastObject.level; } }), diff --git a/app/models/allowance.js b/app/models/allowance.js index 664f7ca7ee..2e892686c5 100644 --- a/app/models/allowance.js +++ b/app/models/allowance.js @@ -12,5 +12,16 @@ export default Model.extend({ creditCardBlockDuration: attr('number'), captchaBlockDuration: attr('number'), - owner: belongsTo('owner') + owner: { + name: 'owner', + type: 'owner', + kind: 'belongsTo', + options: { + as: 'allowance', + async: true, + polymorphic: true, + inverse: 'allowance' + } + }, + organisation: belongsTo('organization', { inverse: 'allowances' }) }); diff --git a/app/models/beta-feature.js b/app/models/beta-feature.js index 602adb1a64..b3e0725840 100644 --- a/app/models/beta-feature.js +++ b/app/models/beta-feature.js @@ -1,5 +1,5 @@ import Model, { attr } from '@ember-data/model'; -import { dasherize } from '@ember/string'; +import { dasherize, capitalize } from '@ember/string'; import { computed } from '@ember/object'; export default Model.extend({ @@ -15,7 +15,7 @@ export default Model.extend({ displayName: computed('dasherizedName', function () { return this.dasherizedName .split('-') - .map(x => x.capitalize()) + .map(x => capitalize(x)) .join(' '); }) }); diff --git a/app/models/build.js b/app/models/build.js index b52e616ca3..56ab1bd34a 100644 --- a/app/models/build.js +++ b/app/models/build.js @@ -134,7 +134,9 @@ export default Model.extend(DurationCalculations, { return !isEmpty(jobs.filterBy('canCancel')); }), - canRestart: alias('isFinished'), + canRestart: computed('isFinished', function () { + return this.isFinished; + }), cancel() { const url = `/build/${this.id}/cancel`; diff --git a/app/models/commit.js b/app/models/commit.js index 5f06f74a3a..0a1c5e2684 100644 --- a/app/models/commit.js +++ b/app/models/commit.js @@ -53,7 +53,7 @@ export default Model.extend({ const owner = this.get('build.repo.ownerName'); const repo = this.get('build.repo.vcsName'); const vcsType = this.get('build.repo.vcsType'); - const commit = this.get('sha'); + const commit = this.sha; return this.externalLinks.commitUrl(vcsType, { owner, repo, commit }); }), diff --git a/app/models/job.js b/app/models/job.js index 4d07b6fe1e..3a770b6b1a 100644 --- a/app/models/job.js +++ b/app/models/job.js @@ -2,7 +2,7 @@ import Model, { attr, belongsTo } from '@ember-data/model'; import { observer, computed } from '@ember/object'; -import { alias, and, equal, not, reads } from '@ember/object/computed'; +import { alias, and, equal, reads } from '@ember/object/computed'; import { inject as service } from '@ember/service'; import { isEqual } from '@ember/utils'; import { getOwner } from '@ember/application'; @@ -154,11 +154,13 @@ export default Model.extend(DurationCalculations, DurationAttributes, { canCancel: computed('isFinished', 'state', function () { let isFinished = this.isFinished; let state = this.state; - // not(isFinished) is insufficient since it will be true when state is undefined. return !isFinished && !!state; }), - canRestart: alias('isFinished'), + canRestart: computed('isFinished', function () { + let isFinished = this.isFinished; + return isFinished; + }), canDebug: and('isFinished', 'repo.private'), cancel() { @@ -234,16 +236,14 @@ export default Model.extend(DurationCalculations, DurationAttributes, { } }), - canRemoveLog: not('log.removed'), + canRemoveLog: computed('log.removed', function () { + let removed = !!this.log.removed; + return !removed; + }), slug: computed('repo.slug', 'number', function () { let slug = this.get('repo.slug'); let number = this.number; return `${slug} #${number}`; }), - - didLoad() { - if (this.number) - this.set('jobIdNumber', this.number); - } }); diff --git a/app/models/log.js b/app/models/log.js index a9b4f752af..066802ad01 100644 --- a/app/models/log.js +++ b/app/models/log.js @@ -9,6 +9,7 @@ export default EmberObject.extend({ features: service(), auth: service(), storage: service(), + logToLogContent: service(), version: 0, length: 0, @@ -68,7 +69,11 @@ export default EmberObject.extend({ this.noRendering) { return; } - return this.parts.pushObject(part); + + + const pushed = this.parts.pushObject(part); + this.logToLogContent.logContent.partsDidChange(this.parts, this.parts.length -1, null, this.parts.length) + return pushed; }, loadParts(parts) { diff --git a/app/models/organization.js b/app/models/organization.js index 97d393f719..267a9e8e4f 100644 --- a/app/models/organization.js +++ b/app/models/organization.js @@ -1,13 +1,24 @@ import Owner from 'travis/models/owner'; -import { attr } from '@ember-data/model'; import { reads } from '@ember/object/computed'; import { task } from 'ember-concurrency'; import { computed } from '@ember/object'; +import { attr, hasMany } from '@ember-data/model'; export default Owner.extend({ type: 'organization', allowMigration: attr('boolean'), customKeys: attr(), + allowance: { + name: 'allowance', + type: 'allowance', + kind: 'belongsTo', + options: { + as: 'owner', + async: true, + polymorphic: false, + inverse: 'owner' + } + }, buildPermissions: reads('fetchBuildPermissions.lastSuccessful.value'), diff --git a/app/models/owner.js b/app/models/owner.js index 99164198d5..d3ab44864d 100644 --- a/app/models/owner.js +++ b/app/models/owner.js @@ -131,7 +131,8 @@ export default VcsEntity.extend({ }, migrationBetaRequests: computed('tasks.fetchBetaMigrationRequestsTask.lastSuccessful.value.[]', 'login', function () { - const requests = this.tasks.fetchBetaMigrationRequestsTask.get('lastSuccessful.value') || []; + const lastSuccessful = this.tasks.fetchBetaMigrationRequestsTask.lastSuccessful; + const requests = lastSuccessful && lastSuccessful.value || []; return requests.filter(request => this.isUser && request.ownerName == this.login || request.organizations.mapBy('login').includes(this.login) ); diff --git a/app/models/plan.js b/app/models/plan.js index db6812e41d..58cca82f4b 100644 --- a/app/models/plan.js +++ b/app/models/plan.js @@ -1,4 +1,4 @@ -import Model, { attr } from '@ember-data/model'; +import Model, { attr, hasMany } from '@ember-data/model'; import { equal } from '@ember/object/computed'; export default Model.extend({ @@ -13,5 +13,6 @@ export default Model.extend({ isEnabled: attr('boolean'), isDefault: attr('boolean'), isAnnual: attr('boolean'), - isFree: equal('price', 0) + isFree: equal('price', 0), + subscriptions: hasMany('subscription') }); diff --git a/app/models/repo.js b/app/models/repo.js index 41bb6d98f2..22259bed90 100644 --- a/app/models/repo.js +++ b/app/models/repo.js @@ -31,6 +31,8 @@ const Repo = VcsEntity.extend({ auth: service(), features: service(), store: service(), + tasks: service(), + refreshService: service(), permissions: attr(), slug: attr('string'), @@ -51,9 +53,11 @@ const Repo = VcsEntity.extend({ historyMigrationStatus: attr('string'), scanFailedAt: attr('date'), serverType: attr('string', { defaultValue: 'git' }), + buildsRefreshToken: null, + requestsRefreshToken: null, currentScan: computed('scanFailedAt', function () { - let scanFailedAt = this.get('scanFailedAt'); + let scanFailedAt = this.scanFailedAt; return { icon: scanFailedAt ? 'errored' : 'passed', state: scanFailedAt ? 'issue' : 'passed' @@ -91,14 +95,7 @@ const Repo = VcsEntity.extend({ return this.repoOwnerAllowance; }), - repoOwnerAllowance: reads('fetchRepoOwnerAllowance.lastSuccessful.value'), - - fetchRepoOwnerAllowance: task(function* () { - const allowance = this.store.peekRecord('allowance', this.owner.id); - if (allowance) - return allowance; - return yield this.store.queryRecord('allowance', { login: this.owner.login, provider: this.provider }); - }).drop(), + repoOwnerAllowance: reads('tasks.fetchRepoOwnerAllowance.lastSuccessful.value'), buildPermissions: reads('fetchBuildPermissions.lastSuccessful.value'), @@ -132,7 +129,7 @@ const Repo = VcsEntity.extend({ return false; const isPro = this.get('features.proVersion'); const enterprise = !!this.get('features.enterpriseVersion'); - const roMode = this.get('owner').ro_mode || false; + const roMode = this.owner.ro_mode || false; if (!isPro || enterprise) { return !roMode; @@ -161,6 +158,7 @@ const Repo = VcsEntity.extend({ _branches: hasMany('branch'), isCurrentUserACollaborator: computed('auth.currentUser.permissions.[]', function () { + console.log("I am isCurrentUserACollaborator"); let permissions = this.get('auth.currentUser.permissions'); if (permissions) { @@ -182,7 +180,7 @@ const Repo = VcsEntity.extend({ urlOwnerName: computed('slug', function () { const { slug = '', ownerName } = this; - return slug.split('/').firstObject || ownerName; + return slug.split('/') && slug.split('/').firstObject || ownerName; }), formattedSlug: computed('owner.login', 'name', function () { @@ -193,7 +191,7 @@ const Repo = VcsEntity.extend({ sshKey: function () { this.store.find('ssh_key', this.id); - return this.store.recordForId('ssh_key', this.id); + return this.store.findRecord('ssh_key', this.id); }, envVars: computed('id', function () { @@ -211,6 +209,9 @@ const Repo = VcsEntity.extend({ fetchSettings: task(function* () { if (!this.auth.signedIn) return {}; + + const hasPermissions = this.permissions.settings_read; + if (hasPermissions === false) return {}; try { const response = yield this.api.get(`/repo/${this.id}/settings`); return this._convertV3SettingsToV2(response.settings); @@ -228,11 +229,11 @@ const Repo = VcsEntity.extend({ content: [] }); array.load(builds); - array.observe(builds); + // array.observe(builds); return array; }, - builds: computed('id', function () { + builds: computed('id', 'buildsRefreshToken', function () { let id = this.id; const builds = this.store.filter('build', { event_type: ['push', 'api', 'cron'], @@ -240,7 +241,7 @@ const Repo = VcsEntity.extend({ }, (b) => { let eventTypes = ['push', 'api', 'cron']; return this._buildRepoMatches(b, id) && eventTypes.includes(b.get('eventType')); - }); + }, [''], true); return this._buildObservableArray(builds); }), @@ -255,11 +256,11 @@ const Repo = VcsEntity.extend({ content: [] }); array.load(requests); - array.observe(requests); + // array.observe(requests); return array; }, - requests: computed('id', function () { + requests: computed('id', 'requestsRefreshToken', function () { let id = this.id; const requests = this.store.filter( 'request', @@ -277,9 +278,9 @@ const Repo = VcsEntity.extend({ }), cronJobs: computed('id', 'fetchCronJobs.lastSuccessful.value', function () { - const crons = this.fetchCronJobs.get('lastSuccessful.value'); + const crons = this.fetchCronJobs.lastSuccessful && this.fetchCronJobs.lastSuccessful.value; if (!crons) { - this.get('fetchCronJobs').perform(); + this.fetchCronJobs.perform(); } return crons || []; }), @@ -434,7 +435,7 @@ Repo.reopenClass({ if (!isEmpty(loadedRepos)) { return EmberPromise.resolve(loadedRepos.firstObject); } - return store.queryRecord('repo', { slug, provider, serverType }); + return store.smartQueryRecord('repo', { slug, provider, serverType }); }, }); diff --git a/app/models/request.js b/app/models/request.js index 63b4b59733..ca724e122b 100644 --- a/app/models/request.js +++ b/app/models/request.js @@ -38,6 +38,7 @@ export default Model.extend({ build: belongsTo('build', { async: true }), api: service(), + tasks: service(), isAccepted: computed('result', 'build.id', function () { // For some reason some of the requests have a null result beside the fact that @@ -57,22 +58,13 @@ export default Model.extend({ isDraft: equal('pullRequestMergeable', PULL_REQUEST_MERGEABLE.DRAFT), - messages: computed('repo.id', 'build.request.id', 'fetchMessages.last.value', function () { - const messages = this.fetchMessages.get('lastSuccessful.value'); + messages: computed('repo.id', 'build.request.id', 'tasks.fetchMessages.last.value', function () { + const messages = this.tasks.fetchMessages.lastSuccessful.value; if (!messages) { - this.fetchMessages.perform(); + this.tasks.fetchMessages.perform(); } return messages || []; }), - fetchMessages: task(function* () { - const repoId = this.get('repo.id'); - const requestId = this.get('build.request.id'); - if (repoId && requestId) { - const response = yield this.api.get(`/repo/${repoId}/request/${requestId}/messages`) || {}; - return response.messages; - } - }).drop(), - hasMessages: gt('messages.length', 0), }); diff --git a/app/models/subscription.js b/app/models/subscription.js index 241233fde6..573fb76e36 100644 --- a/app/models/subscription.js +++ b/app/models/subscription.js @@ -15,6 +15,7 @@ let sourceToWords = { export default Model.extend({ api: service(), accounts: service(), + store: service(), source: attr(), status: attr(), @@ -32,7 +33,7 @@ export default Model.extend({ creditCardInfo: belongsTo('credit-card-info', { async: false }), invoices: hasMany('invoice'), owner: belongsTo('owner', { polymorphic: true }), - plan: belongsTo(), + plan: belongsTo('plan'), isSubscribed: equal('status', 'subscribed'), isCanceled: equal('status', 'canceled'), diff --git a/app/models/user.js b/app/models/user.js index 91c7f31402..a63d1fc283 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -12,6 +12,7 @@ export default Owner.extend({ accounts: service(), permissionsService: service('permissions'), wizardStateService: service('wizardState'), + store: service(), email: attr('string'), emails: attr(), // list of all known user emails @@ -73,6 +74,13 @@ export default Owner.extend({ } }, + hasPermissionToRepo(repo, permission) { + let permissions = repo.get ? repo.get('permissions') : null; + if (permissions) { + return permissions[permission] || false; + } + }, + sync(isOrganization) { this.set('isSyncing', true); this.set('applyFilterRepos', !isOrganization); @@ -114,7 +122,8 @@ export default Owner.extend({ reload(options = {}) { const { authToken } = this; - return this.store.queryRecord('user', Object.assign({}, options, { current: true, authToken })); + const queryParams = Object.assign({}, options, { current: true, authToken }); + return this.store.smartQueryRecord('user', queryParams); }, applyReposFilter() { diff --git a/app/models/v2-billing-info.js b/app/models/v2-billing-info.js index 4089457796..d56f97118b 100644 --- a/app/models/v2-billing-info.js +++ b/app/models/v2-billing-info.js @@ -12,7 +12,13 @@ export default Model.extend({ country: attr('string'), vatId: attr('string'), billingEmail: attr('string'), + billingEmailRead: attr('string'), + hasLocalRegistration: attr('boolean'), - subscription: belongsTo('v2-subscription') + subscription: belongsTo('v2-subscription'), + + didUpdate() { + this.setAttribute('billingEmailRead', this.billingEmail) + } }); diff --git a/app/models/v2-plan-config.js b/app/models/v2-plan-config.js index a7d762a7d9..ddca7cd6ae 100644 --- a/app/models/v2-plan-config.js +++ b/app/models/v2-plan-config.js @@ -31,13 +31,14 @@ export default Model.extend({ addonConfigs: attr(), hasCreditAddons: computed('addonConfigs', 'addonConfigs.@each.type', function () { - return this.addonConfigs.filter(addon => addon.type === 'credit_private').length > 0; + + return (this.addonConfigs || []).filter(addon => addon.type === 'credit_private').length > 0; }), hasOSSCreditAddons: computed('addonConfigs', 'addonConfigs.@each.type', function () { - return this.addonConfigs.filter(addon => addon.type === 'credit_public').length > 0; + return (this.addonConfigs || []).filter(addon => addon.type === 'credit_public').length > 0; }), hasUserLicenseAddons: computed('addonConfigs', 'addonConfigs.@each.type', function () { - return this.addonConfigs.filter(addon => addon.type === 'user_license').length > 0; + return (this.addonConfigs || []).filter(addon => addon.type === 'user_license').length > 0; }), hasCredits: or('hasCreditAddons', 'hasOSSCreditAddons'), diff --git a/app/models/v2-subscription.js b/app/models/v2-subscription.js index 4742eb4de5..c11a9fa384 100644 --- a/app/models/v2-subscription.js +++ b/app/models/v2-subscription.js @@ -14,6 +14,7 @@ let sourceToWords = { export default Model.extend({ api: service(), accounts: service(), + store: service(), source: attr('string'), status: attr('string'), @@ -200,25 +201,25 @@ export default Model.extend({ yield this.api.patch(`/v2_subscription/${this.id}/changetofree`, { data: { reason, reason_details: details } }); - yield this.accounts.fetchV2Subscriptions.perform(); + yield this.accounts.fetchV2Subscriptions.linked().perform(); }).drop(), changePlan: task(function* (plan, coupon) { const data = { plan, coupon }; yield this.api.patch(`/v2_subscription/${this.id}/plan`, { data }); - yield this.accounts.fetchV2Subscriptions.perform(); + yield this.accounts.fetchV2Subscriptions.linked().perform(); }).drop(), buyAddon: task(function* (addon) { yield this.api.post(`/v2_subscription/${this.id}/addon/${addon.id}`); - yield this.accounts.fetchV2Subscriptions.perform(); + yield this.accounts.fetchV2Subscriptions.linked().perform(); }).drop(), autoRefillToggle: task(function* (ownerId, value) { const data = { enabled: value }; yield this.api.patch(`/v2_subscription/${this.id}/auto_refill`, { data }); - yield this.accounts.fetchV2Subscriptions.perform(); + yield this.accounts.fetchV2Subscriptions.linked().perform(); }).drop(), autoRefillAddonId: reads('auto_refill.addon_id'), autoRefillEnabled: reads('auto_refill.enabled'), @@ -230,13 +231,13 @@ export default Model.extend({ const data = { addon_id: this.autoRefillAddonId, threshold: parseInt(threshold), amount: parseInt(amount) }; yield this.api.patch(`/v2_subscription/${this.id}/update_auto_refill`, { data }); - yield this.accounts.fetchV2Subscriptions.perform(); + yield this.accounts.fetchV2Subscriptions.linked().perform(); }).drop(), cancelSubscription: task(function* (data) { yield this.api.post(`/v2_subscription/${this.id}/cancel`, { data }); - yield this.accounts.fetchV2Subscriptions.perform(); + this.accounts.fetchV2Subscriptions.perform(); }).drop(), }); diff --git a/app/router.js b/app/router.js index 23a17a84f1..9f655041cf 100644 --- a/app/router.js +++ b/app/router.js @@ -1,5 +1,5 @@ import EmberRouter from '@ember/routing/router'; -import config from './config/environment'; +import config from 'travis/config/environment'; const Router = EmberRouter.extend({ location: config.locationType, @@ -63,6 +63,10 @@ Router.map(function () { }); this.route('repo', { path: '/:provider/:owner/:name' }, function () { + this.route('active-on-org'); + this.route('not-active'); + this.route('no-build'); + this.route('index', { path: '/' }); this.route('branches', { path: '/branches', resetNamespace: true }); this.route('builds', { path: '/builds', resetNamespace: true }); diff --git a/app/routes/account/billing.js b/app/routes/account/billing.js index 5095052323..afa115e9ec 100644 --- a/app/routes/account/billing.js +++ b/app/routes/account/billing.js @@ -7,5 +7,5 @@ export default TravisRoute.extend(AccountBillingMixin, { return hash({ account: this.modelFor('account'), }); - } + }, }); diff --git a/app/routes/application.js b/app/routes/application.js index 32f906b9aa..ccc57aef39 100644 --- a/app/routes/application.js +++ b/app/routes/application.js @@ -8,8 +8,10 @@ import { bindKeyboardShortcuts, unbindKeyboardShortcuts } from 'ember-keyboard-shortcuts'; +import {asObservableArray} from "travis/utils/observable_array"; export default TravisRoute.extend(BuildFaviconMixin, { + store: service(), auth: service(), features: service(), featureFlags: service(), @@ -43,7 +45,7 @@ export default TravisRoute.extend(BuildFaviconMixin, { } if (this.auth.signedIn) { this.wizard.fetch.perform().then(() => { this.storage.wizardStep = this.wizard.state; }); - return this.get('featureFlags.fetchTask').perform(); + return this.get('featureFlags.fetchTask').unlinked().perform(); } }, @@ -62,19 +64,26 @@ export default TravisRoute.extend(BuildFaviconMixin, { // visitor is subscribed to all of the public repos in the store as long as // they're not a collaborator. It also sets up an observer to subscribe to any // new repo that enters the store. + setupRepoSubscriptions() { - this.store.filter('repo', null, - (repo) => !repo.get('private') && !repo.get('isCurrentUserACollaborator'), - ['private', 'isCurrentUserACollaborator'] - ).then((repos) => { - repos.forEach(repo => this.subscribeToRepo(repo)); - repos.addArrayObserver(this, { + this.store.filter('repo', null, (repo) => { + return !repo.get('private') && !repo.get('isCurrentUserACollaborator'); + }, ['private', 'isCurrentUserACollaborator']).then((repos) => { + let plainRepos = [] + repos.forEach(repo => { + this.subscribeToRepo(repo) + plainRepos.push(repo) + }); + plainRepos = asObservableArray(plainRepos); + this.set('repos', plainRepos); + plainRepos.addArrayObserver(this, { willChange: 'reposWillChange', didChange: 'reposDidChange' }); }); }, + reposWillChange(array, start, removedCount, addedCount) { let removedRepos = array.slice(start, start + removedCount); return removedRepos.forEach(repo => this.unsubscribeFromRepo(repo)); @@ -86,14 +95,14 @@ export default TravisRoute.extend(BuildFaviconMixin, { }, unsubscribeFromRepo: function (repo) { - if (this.pusher && repo) { - this.pusher.unsubscribe(`repo-${repo.get('id')}`); + if (Travis.pusher && repo) { + Travis.pusher.unsubscribe(`repo-${repo.get('id')}`); } }, subscribeToRepo: function (repo) { - if (this.pusher) { - this.pusher.subscribe(`repo-${repo.get('id')}`); + if (Travis.pusher) { + Travis.pusher.subscribe(`repo-${repo.get('id')}`); } }, diff --git a/app/routes/branches.js b/app/routes/branches.js index 036da1af8e..ec650fe03e 100644 --- a/app/routes/branches.js +++ b/app/routes/branches.js @@ -7,9 +7,10 @@ export default TravisRoute.extend({ tabStates: service(), api: service(), auth: service(), + tasks: service(), model() { - const repoId = this.modelFor('repo').get('id'); + const repoId = this.modelFor('repo').id; let allTheBranches = ArrayProxy.create(); const path = `/repo/${repoId}/branches`; @@ -32,7 +33,7 @@ export default TravisRoute.extend({ beforeModel() { const repo = this.modelFor('repo'); if (repo && !repo.repoOwnerAllowance) { - repo.fetchRepoOwnerAllowance.perform(); + this.tasks.fetchRepoOwnerAllowance.perform(repo); } } }); diff --git a/app/routes/build.js b/app/routes/build.js index d6494d4b06..b5d44ab86f 100644 --- a/app/routes/build.js +++ b/app/routes/build.js @@ -3,6 +3,8 @@ import { inject as service } from '@ember/service'; export default TravisRoute.extend({ tabStates: service(), + store: service(), + tasks: service(), titleToken(model) { return `Build #${model.get('number')}`; @@ -17,16 +19,15 @@ export default TravisRoute.extend({ setupController(controller, model) { if (model && !model.get) { - model = this.store.recordForId('build', model); + model = this.store.findRecord('build', model); this.store.find('build', model); } + const currentTab = controller.currentTab + this.tabStates.setMainTab(currentTab || 'build'); const repo = this.controllerFor('repo'); controller.set('build', model); - return repo.activate('build'); - }, - - activate() { - this.set('tabStates.mainTab', 'build'); + if (!currentTab) + return repo.activate('build'); }, model(params) { @@ -34,15 +35,15 @@ export default TravisRoute.extend({ }, afterModel(model) { - const slug = this.modelFor('repo').get('slug'); + const slug = this.modelFor('repo').slug; this.ensureBuildOwnership(model, slug); - return model.get('request').then(request => request && request.fetchMessages.perform()); + return model.get('request').then(request => request && this.tasks.fetchMessages.perform(request)); }, beforeModel() { const repo = this.modelFor('repo'); if (repo && !repo.repoOwnerAllowance) { - repo.fetchRepoOwnerAllowance.perform(); + this.tasks.fetchRepoOwnerAllowance.perform(repo); } }, diff --git a/app/routes/build/config.js b/app/routes/build/config.js index c0cdba8d57..27e3fd7834 100644 --- a/app/routes/build/config.js +++ b/app/routes/build/config.js @@ -1,13 +1,15 @@ import TravisRoute from 'travis/routes/basic'; +import { inject as service } from '@ember/service' export default TravisRoute.extend({ titleToken: 'Config', + tasks: service(), model() { - return this.modelFor('build').get('request'); + return this.modelFor('build').request; }, afterModel(request) { - return request.fetchMessages.perform(); + return this.tasks.fetchMessages.perform(request); } }); diff --git a/app/routes/builds.js b/app/routes/builds.js index 13e2f929db..c16a7fe0bd 100644 --- a/app/routes/builds.js +++ b/app/routes/builds.js @@ -4,6 +4,8 @@ import { inject as service } from '@ember/service'; export default TravisRoute.extend({ tabStates: service(), auth: service(), + tasks: service(), + refreshService: service(), activate(...args) { this._super(args); @@ -19,13 +21,23 @@ export default TravisRoute.extend({ }, model() { - return this.modelFor('repo').get('builds'); + const that = this; + const repo = this.modelFor('repo'); + repo.addObserver('buildsRefreshToken', function () { + that.refresh() + } + ); + return repo.builds; }, beforeModel() { const repo = this.modelFor('repo'); if (repo && !repo.repoOwnerAllowance) { - repo.fetchRepoOwnerAllowance.perform(); + this.tasks.fetchRepoOwnerAllowance.perform(repo); } - } + }, + + refreshRoute() { + this.refresh(); + }, }); diff --git a/app/routes/caches.js b/app/routes/caches.js index 7e08519180..6e063dc93c 100644 --- a/app/routes/caches.js +++ b/app/routes/caches.js @@ -6,11 +6,24 @@ export default TravisRoute.extend({ needsAuth: true, - setupController(/* controller*/) { + setupController(controller, model) { this._super(...arguments); + controller.pushes = model.pushes; + controller.pullRequests = model.pullRequests; + controller.repo = this.modelFor('repo'); + return this.controllerFor('repo').activate('caches'); }, + beforeModel() { + const repo = this.modelFor('repo'); + if (!repo.permissions?.cache_view) { + this.transitionTo('repo.index'); + this.flashes.error('Your permissions are insufficient to access this repository\'s cache'); + } + }, + + model() { const repo = this.modelFor('repo'); const url = `/repo/${repo.get('id')}/caches`; diff --git a/app/routes/dashboard.js b/app/routes/dashboard.js index 12097e9e72..1c0e29ee68 100644 --- a/app/routes/dashboard.js +++ b/app/routes/dashboard.js @@ -7,6 +7,7 @@ export default TravisRoute.extend({ features: service(), accounts: service(), + store: service(), model(params) { return hash({ diff --git a/app/routes/dashboard/builds.js b/app/routes/dashboard/builds.js index bce6b1b078..96b08c9af6 100644 --- a/app/routes/dashboard/builds.js +++ b/app/routes/dashboard/builds.js @@ -3,6 +3,7 @@ import { inject as service } from '@ember/service'; export default TravisRoute.extend({ auth: service(), + store: service(), model(params) { let currentUserId = this.get('auth.currentUser.id'); diff --git a/app/routes/dashboard/repositories.js b/app/routes/dashboard/repositories.js index 7586a16f05..528fe2a59d 100644 --- a/app/routes/dashboard/repositories.js +++ b/app/routes/dashboard/repositories.js @@ -7,6 +7,7 @@ import { inject as service } from '@ember/service'; export default TravisRoute.extend({ features: service(), accounts: service(), + store: service(), queryParams: { page: { diff --git a/app/routes/index.js b/app/routes/index.js index 1e35582300..9e3a937e14 100644 --- a/app/routes/index.js +++ b/app/routes/index.js @@ -32,7 +32,7 @@ export default Route.extend({ }, activate(...args) { - this._super(args); + this._super(...args); if (this.get('auth.signedIn')) { this.tabStates.set('sidebarTab', 'owned'); this.set('tabStates.mainTab', 'current'); diff --git a/app/routes/job.js b/app/routes/job.js index fdc7c8c28e..795da0127c 100644 --- a/app/routes/job.js +++ b/app/routes/job.js @@ -1,8 +1,11 @@ import TravisRoute from 'travis/routes/basic'; import { inject as service } from '@ember/service'; +import {task} from "ember-concurrency"; export default TravisRoute.extend({ router: service(), + store: service(), + tasks: service(), titleToken(model) { return `Job #${model.get('number')}`; @@ -19,7 +22,7 @@ export default TravisRoute.extend({ let buildController, repo; if (model && !model.get) { - model = this.store.recordForId('job', model); + model = this.store.findRecord('job', model); this.store.find('job', model); } repo = this.controllerFor('repo'); @@ -30,7 +33,7 @@ export default TravisRoute.extend({ let buildPromise = model.get('build'); if (buildPromise) { buildPromise.then(build => { - build = this.store.recordForId('build', build.get('id')); + build = this.store.findRecord('build', build.get('id')); return buildController.set('build', build); }); } @@ -46,17 +49,18 @@ export default TravisRoute.extend({ }, afterModel(job) { - const slug = this.modelFor('repo').get('slug'); + const slug = this.modelFor('repo').slug; this.ensureJobOwnership(job, slug); return job .get('build.request') - .then(request => request && request.fetchMessages.perform()); + .then(request => request && this.tasks.fetchMessages.perform(request)); }, - beforeModel() { - const repo = this.modelFor('repo'); + beforeModel() { + let repo = this.modelFor('repo'); + // move this to service to be sure it is present... if (repo && !repo.repoOwnerAllowance) { - repo.fetchRepoOwnerAllowance.perform(); + this.tasks.fetchRepoOwnerAllowance.perform(repo); } }, @@ -75,5 +79,5 @@ export default TravisRoute.extend({ this.controllerFor('build').set('build', null); this.controllerFor('job').set('job', null); return this._super(...arguments); - } + }, }); diff --git a/app/routes/job/config.js b/app/routes/job/config.js index da954849c1..1f10921a05 100644 --- a/app/routes/job/config.js +++ b/app/routes/job/config.js @@ -1,16 +1,20 @@ import TravisRoute from 'travis/routes/basic'; +import { inject as service } from '@ember/service'; export default TravisRoute.extend({ + store: service(), + tasks: service(), titleToken: 'Config', model() { - return this.modelFor('job').get('build').then(build => { - let requestId = build.get('build.request.id') || build.belongsTo('request').id(); + let build = this.modelFor('job').build + return build.then((build_) => { + let requestId = build_.get('build.request.id') || build_.belongsTo('request').id(); return this.store.findRecord('request', requestId); }); }, afterModel(request) { - return request.fetchMessages.perform(); + return this.tasks.fetchMessages.perform(request); } }); diff --git a/app/routes/legacy-repo-url.js b/app/routes/legacy-repo-url.js index 98e7ff6fa7..68fd70547c 100644 --- a/app/routes/legacy-repo-url.js +++ b/app/routes/legacy-repo-url.js @@ -1,6 +1,10 @@ import Route from '@ember/routing/route'; -import { vcsConfigByUrlPrefix, defaultVcsConfig } from 'travis/utils/vcs'; +import { + vcsConfigByUrlPrefix, + defaultVcsConfig +} from 'travis/utils/vcs'; import { isEmpty } from '@ember/utils'; +import { singularize } from 'ember-inflector'; export default Route.extend({ templateName: 'error404', @@ -47,7 +51,7 @@ export default Route.extend({ routeName = method; } if (id) { - routeName = method.singularize(); + routeName = singularize(method); routeModels.push(id); } if (view) { diff --git a/app/routes/organization.js b/app/routes/organization.js index dc045d71b1..30ebfd9015 100644 --- a/app/routes/organization.js +++ b/app/routes/organization.js @@ -1,5 +1,6 @@ import TravisRoute from 'travis/routes/basic'; import { inject as service } from '@ember/service'; +import { A } from '@ember/array'; export default TravisRoute.extend({ accounts: service(), diff --git a/app/routes/organization/billing.js b/app/routes/organization/billing.js index a28a8682bb..30a1ee308a 100644 --- a/app/routes/organization/billing.js +++ b/app/routes/organization/billing.js @@ -5,7 +5,7 @@ import AccountBillingMixin from 'travis/mixins/route/account/billing'; export default TravisRoute.extend(AccountBillingMixin, { model() { const organization = this.modelFor('organization'); - if (organization.permissions && organization.permissions.admin !== true) { + if (organization.permissions && organization.permissions.plan_view !== true) { this.transitionTo('organization.repositories', organization); } return hash({ diff --git a/app/routes/organization/plan_usage.js b/app/routes/organization/plan_usage.js index fcd1a4b612..9d99faadc2 100644 --- a/app/routes/organization/plan_usage.js +++ b/app/routes/organization/plan_usage.js @@ -5,7 +5,7 @@ import { hash } from 'rsvp'; export default TravisRoute.extend(AccountPlanUsageMixin, { model() { const organization = this.modelFor('organization'); - if (organization.permissions && organization.permissions.admin !== true) { + if (organization.permissions && organization.permissions.plan_usage !== true) { this.transitionTo('organization.repositories', organization); } return hash({ diff --git a/app/routes/organization/settings.js b/app/routes/organization/settings.js index 6083988bcd..c064bb0748 100644 --- a/app/routes/organization/settings.js +++ b/app/routes/organization/settings.js @@ -13,9 +13,6 @@ export default TravisRoute.extend({ model() { const organization = this.modelFor('organization'); - if (organization.permissions.admin !== true) { - this.transitionTo('organization.repositories', organization); - } const preferences = this.store.query('preference', { organization_id: organization.id }); return hash({ organization, preferences }); }, diff --git a/app/routes/owner.js b/app/routes/owner.js index 739ab69b01..d41ee30303 100644 --- a/app/routes/owner.js +++ b/app/routes/owner.js @@ -1,8 +1,11 @@ import TravisRoute from 'travis/routes/basic'; import { inject as service } from '@ember/service'; +import { A } from '@ember/array' export default TravisRoute.extend({ auth: service(), + store: service(), + deactivate() { return this.controllerFor('loading').set('layoutName', null); }, @@ -13,12 +16,12 @@ export default TravisRoute.extend({ }, model({ provider, owner }) { - return this.store.queryRecord('owner', { provider, login: owner }); + return this.store.smartQueryRecord('owner', { provider, login: owner }); }, actions: { error(error, /* transition, originRoute*/) { - const is404 = error.status === 404 || error.errors.firstObject.status === '404'; + const is404 = error.status === 404 || A(error.errors || []).firstObject?.status === '404'; if (!is404) { let message = 'There was an error while loading data, please try again.'; diff --git a/app/routes/owner/repositories.js b/app/routes/owner/repositories.js index fde55bb96c..b00a56d01f 100644 --- a/app/routes/owner/repositories.js +++ b/app/routes/owner/repositories.js @@ -6,6 +6,7 @@ import { OWNER_TABS } from 'travis/controllers/owner/repositories'; export default TravisRoute.extend({ features: service(), insights: service(), + store: service(), needsAuth: false, diff --git a/app/routes/plans/index.js b/app/routes/plans/index.js index 287e93f89c..919fd0ac47 100644 --- a/app/routes/plans/index.js +++ b/app/routes/plans/index.js @@ -1,10 +1,12 @@ import TravisRoute from 'travis/routes/basic'; import { inject as service } from '@ember/service'; import config from 'travis/config/environment'; +import { pushPayload } from 'travis/serializers/plan' export default TravisRoute.extend({ auth: service(), router: service(), + store: service(), beforeModel() { if (this.auth.signedIn) { @@ -13,7 +15,7 @@ export default TravisRoute.extend({ }, model() { - this.store.pushPayload('plan', { '@type': 'plans', plans: config.plans }); + pushPayload(this.store, { '@type': 'plans', plans: config.plans }); return { plans: this.store.peekAll('plan'), diff --git a/app/routes/provider.js b/app/routes/provider.js index daedd06b8e..6e730ab024 100644 --- a/app/routes/provider.js +++ b/app/routes/provider.js @@ -1,5 +1,8 @@ import Route from '@ember/routing/route'; -import { vcsConfigByUrlPrefix, defaultVcsConfig } from 'travis/utils/vcs'; +import { + vcsConfigByUrlPrefix, + defaultVcsConfig +} from 'travis/utils/vcs'; export default Route.extend({ beforeModel(transition) { diff --git a/app/routes/pull-requests.js b/app/routes/pull-requests.js index 1576d1e67a..d3817d3303 100644 --- a/app/routes/pull-requests.js +++ b/app/routes/pull-requests.js @@ -4,6 +4,7 @@ import { inject as service } from '@ember/service'; export default TravisRoute.extend({ tabStates: service(), auth: service(), + tasks: service(), activate(...args) { this._super(args); @@ -15,7 +16,12 @@ export default TravisRoute.extend({ }, model() { - return this.modelFor('repo'); + const that = this; + const repo = this.modelFor('repo'); + repo.addObserver('requestsRefreshToken', function () { + that.refresh() + }); + return repo; }, titleToken() { @@ -25,7 +31,7 @@ export default TravisRoute.extend({ beforeModel() { const repo = this.modelFor('repo'); if (repo && !repo.repoOwnerAllowance) { - repo.fetchRepoOwnerAllowance.perform(); + this.tasks.fetchRepoOwnerAllowance.perform(repo); } } }); diff --git a/app/routes/repo.js b/app/routes/repo.js index 66166441ff..310b007cd0 100644 --- a/app/routes/repo.js +++ b/app/routes/repo.js @@ -1,4 +1,4 @@ -import { getWithDefault, computed } from '@ember/object'; +import { computed } from '@ember/object'; import TravisRoute from 'travis/routes/basic'; import Repo from 'travis/models/repo'; import ScrollResetMixin from 'travis/mixins/scroll-reset'; @@ -9,6 +9,7 @@ export default TravisRoute.extend(ScrollResetMixin, { tabStates: service(), auth: service(), features: service(), + tasks: service(), slug: null, @@ -40,13 +41,14 @@ export default TravisRoute.extend(ScrollResetMixin, { if (model && !model.get) { model = this.store.find('repo', model.id); } + return controller.set('repo', model); }, serialize(repo) { // slugs are sometimes unknown ??? - const slug = getWithDefault(repo, 'slug', 'unknown/unknown'); - const [owner, name] = slug.split('/'); + const slug = repo ? repo.get('slug') : 'unknown/unknown'; + const [owner, name] = (slug || 'unknown/unknown').split('/'); const provider = repo.get('vcsProvider.urlPrefix'); return { provider, owner, name }; @@ -61,7 +63,7 @@ export default TravisRoute.extend(ScrollResetMixin, { beforeModel() { const repo = this.modelFor('repo'); if (repo && !repo.repoOwnerAllowance) { - repo.fetchRepoOwnerAllowance.perform(); + this.tasks.fetchRepoOwnerAllowance.perform(repo); } }, diff --git a/app/routes/repo/index.js b/app/routes/repo/index.js index 978659e8f5..4b08697507 100644 --- a/app/routes/repo/index.js +++ b/app/routes/repo/index.js @@ -4,15 +4,11 @@ import { inject as service } from '@ember/service'; export default TravisRoute.extend({ features: service(), tabStates: service(), - - afterModel(repo) { - try { - return repo.get('currentBuild.request').then(request => request && request.fetchMessages.perform()); - } catch (error) {} - }, - + tasks: service(), + router: service(), setupController(controller, model) { this._super(...arguments); + this.activate(); this.controllerFor('repo').activate('current'); controller.set('repo', model); }, @@ -21,49 +17,36 @@ export default TravisRoute.extend({ this.controllerFor('build').set('build', null); this.controllerFor('job').set('job', null); this.controllerFor('repo').set('migrationStatus', null); - this.stopObservingRepoStatus(); return this._super(...arguments); }, activate() { - this.observeRepoStatus(); - this.set('tabStates.mainTab', 'current'); + this.tabStates.setMainTab('current'); return this._super(...arguments); }, - observeRepoStatus() { - let controller = this.controllerFor('repo'); - controller.addObserver('repo.active', this, 'renderTemplate'); - controller.addObserver('repo.currentBuildId', this, 'renderTemplate'); - }, - - stopObservingRepoStatus() { - let controller = this.controllerFor('repo'); - controller.removeObserver('repo.active', this, 'renderTemplate'); - controller.removeObserver('repo.currentBuildId', this, 'renderTemplate'); - }, - beforeModel() { const repo = this.modelFor('repo'); if (repo && !repo.repoOwnerAllowance) { - repo.fetchRepoOwnerAllowance.perform(); + this.tasks.fetchRepoOwnerAllowance.perform(repo); } }, - renderTemplate() { - let controller = this.controllerFor('repo'); + afterModel(repo, _transition) { + try { + repo.get('currentBuild.request').then(request => request && this.tasks.fetchMessages.perform(request)); + } catch (error) {} if (this.get('features.github-apps') && - controller.get('repo.active_on_org') && - controller.migrationStatus !== 'success') { - this.render('repo/active-on-org'); - } else if (!controller.get('repo.active')) { - this.render('repo/not-active'); - } else if (!controller.get('repo.currentBuildId')) { - this.render('repo/no-build'); + repo.active_on_org && + repo.migrationStatus !== 'success') { + this.transitionTo('repo.active-on-org'); + } else if (!repo.active) { + this.transitionTo('repo.not-active'); + } else if (!repo.currentBuildId) { + this.transitionTo('repo.no-build'); } else { - this.render('build'); - this.render('build/index', { into: 'build', controller: 'build' }); + this.transitionTo('build.index', repo.currentBuildId, { queryParams: { currentTab: 'current' } }); } } }); diff --git a/app/routes/repo/no-build.js b/app/routes/repo/no-build.js new file mode 100644 index 0000000000..5f0bdab421 --- /dev/null +++ b/app/routes/repo/no-build.js @@ -0,0 +1,15 @@ +import Route from '@ember/routing/route'; +import {inject as service} from "@ember/service"; + +export default class NoBuild extends Route { + @service tabStates; + model() { + return this.modelFor('repo'); + } + + setupController(controller, model, transition) { + this.tabStates.setMainTab('current'); + super.setupController(controller, model, transition); + controller.set('repo', model); + } +} diff --git a/app/routes/repo/not-active.js b/app/routes/repo/not-active.js new file mode 100644 index 0000000000..d678b89dc1 --- /dev/null +++ b/app/routes/repo/not-active.js @@ -0,0 +1,15 @@ +import Route from '@ember/routing/route'; +import {inject as service} from "@ember/service"; + +export default class NotActive extends Route { + @service tabStates; + model() { + return this.modelFor('repo'); + } + + setupController(controller, model, transition) { + this.tabStates.setMainTab('current'); + super.setupController(controller, model, transition); + controller.set('repo', model); + } +} diff --git a/app/routes/repo/repo-active-on-org-route.js b/app/routes/repo/repo-active-on-org-route.js new file mode 100644 index 0000000000..970dd8572b --- /dev/null +++ b/app/routes/repo/repo-active-on-org-route.js @@ -0,0 +1,15 @@ +import Route from '@ember/routing/route'; +import {inject as service} from "@ember/service"; + +export default class RepoActiveOnOrgRoute extends Route { + @service tabStates; + model() { + return this.modelFor('repo'); + } + + setupController(controller, model, transition) { + this.tabStates.setMainTab('current'); + super.setupController(controller, model, transition); + controller.set('repo', model); + } +} diff --git a/app/routes/requests.js b/app/routes/requests.js index fc3aa60143..aa5966c3d0 100644 --- a/app/routes/requests.js +++ b/app/routes/requests.js @@ -1,19 +1,31 @@ import TravisRoute from 'travis/routes/basic'; +import { inject as service } from '@ember/service'; export default TravisRoute.extend({ + tasks: service(), setupController() { this._super(...arguments); return this.controllerFor('repo').activate('requests'); }, model() { - return this.modelFor('repo').get('requests'); + const that = this; + const repo = this.modelFor('repo'); + repo.addObserver('requestsRefreshToken', function () { + that.refresh() + } + ); + + return this.modelFor('repo').requests; }, beforeModel() { const repo = this.modelFor('repo'); if (repo && !repo.repoOwnerAllowance) { - repo.fetchRepoOwnerAllowance.perform(); + this.tasks.fetchRepoOwnerAllowance.perform(repo); } - } + }, + refreshRoute() { + this.refresh(); + }, }); diff --git a/app/routes/scan-results.js b/app/routes/scan-results.js index aadfdd8079..59a96a7b7a 100644 --- a/app/routes/scan-results.js +++ b/app/routes/scan-results.js @@ -8,6 +8,7 @@ export default TravisRoute.extend({ tabStates: service(), auth: service(), needsAuth: true, + tasks: service(), page: 1, @@ -50,7 +51,7 @@ export default TravisRoute.extend({ beforeModel() { const repo = this.modelFor('repo'); if (repo && !repo.repoOwnerAllowance) { - repo.fetchRepoOwnerAllowance.perform(); + this.tasks.fetchRepoOwnerAllowance.perform(repo); } } }); diff --git a/app/routes/settings.js b/app/routes/settings.js index bc781cfb1a..59ff18b59f 100644 --- a/app/routes/settings.js +++ b/app/routes/settings.js @@ -10,6 +10,7 @@ export default TravisRoute.extend({ permissions: service(), raven: service(), flashes: service(), + store: service(), needsAuth: true, @@ -28,7 +29,7 @@ export default TravisRoute.extend({ fetchCustomSshKey() { if (config.endpoints.sshKey) { const repo = this.modelFor('repo'); - return this.store.find('ssh_key', repo.get('id')).then(((result) => { + return this.store.find('ssh_key', repo.id).then(((result) => { if (!result.get('isNew')) { return result; } @@ -61,28 +62,30 @@ export default TravisRoute.extend({ }, fetchRepositoryActiveFlag() { - const repoId = this.modelFor('repo').get('id'); + const repoId = this.modelFor('repo').id; return this.api.get(`/repo/${repoId}`).then(response => response.active); }, beforeModel() { const repo = this.modelFor('repo'); - const hasPushPermission = this.permissions.hasPushPermission(repo); - if (!hasPushPermission) { + if (!repo.permissions?.settings_read) { this.transitionTo('repo.index'); this.flashes.error('Your permissions are insufficient to access this repository\'s settings'); } }, - model() { + model(params) { const repo = this.modelFor('repo'); + let sshKey; + if (params.ssh_key_id) + sshKey = this.store.findRecord('ssh_key', params.ssh_key_id); return hash({ settings: repo.fetchSettings.perform(), repository: repo, envVars: this.fetchEnvVars(), sshKey: this.fetchSshKey(), - customSshKey: this.fetchCustomSshKey(), + customSshKey: this.fetchCustomSshKey() || sshKey, hasPushAccess: this.permissions.hasPushPermission(repo), repositoryActive: this.fetchRepositoryActiveFlag() }); diff --git a/app/serializers/beta-migration-request.js b/app/serializers/beta-migration-request.js index d835f5633b..437dfa109e 100644 --- a/app/serializers/beta-migration-request.js +++ b/app/serializers/beta-migration-request.js @@ -2,12 +2,6 @@ import ApplicationSerializer from './application'; export default ApplicationSerializer.extend({ - pushPayload(store, payload) { - const modelClass = store.modelFor('beta-migration-request'); - const json = this.normalizeArrayResponse(store, modelClass, payload); - store.push(json); - }, - normalize(modelClass, payload = {}) { if (payload.organizations) { payload['organization_ids'] = payload.organizations; diff --git a/app/serializers/build_v2_fallback.js b/app/serializers/build_v2_fallback.js index 6eaea966b2..6c2a826f2a 100644 --- a/app/serializers/build_v2_fallback.js +++ b/app/serializers/build_v2_fallback.js @@ -1,4 +1,5 @@ import V2FallbackSerializer from 'travis/serializers/v2_fallback'; +import { A } from '@ember/array'; export default V2FallbackSerializer.extend({ normalizeSingleResponse: function (store, primaryModelClass, payload/* , id, requestType*/) { diff --git a/app/serializers/job_v2_fallback.js b/app/serializers/job_v2_fallback.js index 3f52692e1e..00e0a8a227 100644 --- a/app/serializers/job_v2_fallback.js +++ b/app/serializers/job_v2_fallback.js @@ -1,4 +1,5 @@ import V2FallbackSerializer from 'travis/serializers/v2_fallback'; +import { A } from '@ember/array'; export default V2FallbackSerializer.extend({ keyForV2Relationship(key/* , typeClass, method*/) { diff --git a/app/serializers/plan.js b/app/serializers/plan.js index 610128669e..660a1b5229 100644 --- a/app/serializers/plan.js +++ b/app/serializers/plan.js @@ -1,9 +1,11 @@ -import ApplicationSerializer from 'travis/serializers/application'; +import ApplicationSerializer from "./application"; export default ApplicationSerializer.extend({ - pushPayload(store, payload) { - const modelClass = store.modelFor('plan'); - const json = this.normalizeArrayResponse(store, modelClass, payload); - store.push(json); - } + }); + +export function pushPayload(store, payloads) { + const applicationSerializer = store.serializerFor('application'); + const json = applicationSerializer.normalizeArrayResponse(store, store.modelFor('plan'), payloads); + store.push(json); +} diff --git a/app/serializers/repo.js b/app/serializers/repo.js index 4105f32a34..e6fd65d74c 100644 --- a/app/serializers/repo.js +++ b/app/serializers/repo.js @@ -1,5 +1,5 @@ import RepoV2FallbackSerializer from 'travis/serializers/repo_v2_fallback'; -import EmbeddedRecordsMixin from 'ember-data/serializers/embedded-records-mixin'; +import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; let Serializer = RepoV2FallbackSerializer.extend(EmbeddedRecordsMixin, { attrs: { diff --git a/app/serializers/request.js b/app/serializers/request.js index df142a5c40..61777e3834 100644 --- a/app/serializers/request.js +++ b/app/serializers/request.js @@ -1,4 +1,5 @@ import V2FallbackSerializer from 'travis/serializers/v2_fallback'; +import { A } from '@ember/array'; let Serializer = V2FallbackSerializer.extend({ @@ -13,7 +14,7 @@ let Serializer = V2FallbackSerializer.extend({ normalizeArrayResponse: function (store, primaryModelClass, payload/* , id, requestType*/) { if (payload.commits) { payload.requests.forEach((request) => { - let commit = commit = payload.commits.findBy('id', request.commit_id); + let commit = payload.commits.findBy('id', request.commit_id); if (commit) { request.commit = commit; return delete request.commit_id; diff --git a/app/serializers/v2_fallback.js b/app/serializers/v2_fallback.js index b313b4f0ef..cda657bf98 100644 --- a/app/serializers/v2_fallback.js +++ b/app/serializers/v2_fallback.js @@ -1,6 +1,7 @@ import { isArray } from '@ember/array'; import V3Serializer from 'travis/serializers/v3'; import wrapWithArray from 'travis/utils/wrap-with-array'; +import { underscore } from '@ember/string'; export default V3Serializer.extend({ @@ -14,7 +15,7 @@ export default V3Serializer.extend({ // V2 API payload let relationship = null; let relationshipKey = this.keyForV2Relationship(key, relationshipMeta.kind, 'deserialize'); - let alternativeRelationshipKey = key.underscore(); + let alternativeRelationshipKey = underscore(key); let hashWithAltRelKey = resourceHash[alternativeRelationshipKey]; let hashWithRelKey = resourceHash[relationshipKey]; @@ -101,6 +102,6 @@ export default V3Serializer.extend({ if (key === 'repo') { return 'repository'; } - return `${key.underscore()}_id`; + return `${underscore(key)}_id`; } }); diff --git a/app/serializers/v3.js b/app/serializers/v3.js index 6e547bd4d0..6d1a4444f3 100644 --- a/app/serializers/v3.js +++ b/app/serializers/v3.js @@ -2,7 +2,7 @@ import { underscore } from '@ember/string'; import { isArray } from '@ember/array'; import { assert } from '@ember/debug'; import { isNone, typeOf } from '@ember/utils'; -import JSONSerializer from 'ember-data/serializers/json'; +import JSONSerializer from '@ember-data/serializer/json'; import wrapWithArray from 'travis/utils/wrap-with-array'; import traverse from 'travis/utils/traverse-payload'; @@ -163,8 +163,8 @@ export default JSONSerializer.extend({ keyForRelationship(key/* , typeClass, method*/) { if (key === 'repo') { return 'repository'; - } else if (key && key.underscore) { - return key.underscore(); + } else if (key) { + return underscore(key); } else { return key; } @@ -211,10 +211,13 @@ export default JSONSerializer.extend({ if (type) { items = payload[type]; } else { - const plural = `${primaryModelClass.modelName.underscore()}s`; + const plural = `${underscore(primaryModelClass.modelName)}s`; items = payload[plural]; } + if (!items) + return {data: []}; + documentHash.data = items.map((item) => { let { data, included } = this.normalize(primaryModelClass, item); if (included) { diff --git a/app/services/accounts.js b/app/services/accounts.js index a1f5beb717..9329e100b1 100644 --- a/app/services/accounts.js +++ b/app/services/accounts.js @@ -24,7 +24,7 @@ export default Service.extend({ all: computed('user', 'organizations.@each', function () { let user = this.user; let organizations = this.organizations || []; - return organizations.toArray().concat([user]); + return organizations.concat([user]); }), fetchOrganizations: task(function* () { @@ -49,8 +49,10 @@ export default Service.extend({ fetchV2Subscriptions: task(function* () { this.set('v2SubscriptionError', false); + console.log("fetchV2Subscriptions"); try { - const subscriptions = yield this.store.findAll('v2-subscription') || []; + const subscriptions = yield this.store.findAll('v2-subscription') || []; + if (subscriptions.any(s => s.isSubscribed && !s.belongsTo('plan').id())) { this.logMissingPlanException(); @@ -58,6 +60,7 @@ export default Service.extend({ return subscriptions; } catch (e) { + console.log("any error?", e); this.set('v2SubscriptionError', true); } }), diff --git a/app/services/auth.js b/app/services/auth.js index a1874089ca..00ddf27662 100644 --- a/app/services/auth.js +++ b/app/services/auth.js @@ -17,7 +17,12 @@ import { import { getOwner } from '@ember/application'; import config from 'travis/config/environment'; import { task, didCancel } from 'ember-concurrency'; -import { availableProviders, vcsConfigByUrlPrefixOrType } from 'travis/utils/vcs'; +import { + availableProviders, + vcsConfigByUrlPrefixOrType +} from 'travis/utils/vcs'; +import { A } from '@ember/array'; +import { debug } from '@ember/debug'; const { authEndpoint, apiEndpoint } = config; @@ -25,7 +30,7 @@ const { authEndpoint, apiEndpoint } = config; // and ensures the future fetches don't override previously loaded includes let includes = ['owner.installation', 'user.emails']; -const afterSignOutCallbacks = []; +const afterSignOutCallbacks = A([]); const STATE = { SIGNED_OUT: 'signed-out', @@ -103,7 +108,8 @@ export default Service.extend({ }, switchAccount(id, redirectUrl) { - this.store.unloadAll(); + if (!this.store.isDestroyed && !this.store.isDestroying) + this.store.unloadAll(); const targetAccount = this.accounts.findBy('id', id); this.storage.set('activeAccount', targetAccount); if (redirectUrl) @@ -129,7 +135,8 @@ export default Service.extend({ this.clearNonAuthFlashes(); runAfterSignOutCallbacks(); } - this.store.unloadAll(); + if (!this.store.isDestroyed && !this.store.isDestroying) + this.store.unloadAll(); const { currentRouteName } = this.router; if (currentRouteName && currentRouteName !== 'signin') { @@ -185,15 +192,16 @@ export default Service.extend({ autoSignIn() { this.set('state', STATE.SIGNING_IN); + try { const promise = this.storage.user ? this.handleNewLogin() : this.reloadCurrentUser(); return promise - .then(() => this.permissionsService.fetchPermissions.perform()) + .then(() => { this.permissionsService.fetchPermissions.perform()}) .then(() => { - const { currentUser } = this; - this.set('state', STATE.SIGNED_IN); - Travis.trigger('user:signed_in', currentUser); - Travis.trigger('user:refreshed', currentUser); + const {currentUser} = this; + this.set('state', STATE.SIGNED_IN); + Travis.trigger('user:signed_in', currentUser); + Travis.trigger('user:refreshed', currentUser); }) .catch(error => { if (!didCancel(error)) { @@ -212,22 +220,20 @@ export default Service.extend({ storage.clearLoginData(); if (!user || !token) throw new Error('No login data'); - const userData = getProperties(user, USER_FIELDS); const installationData = getProperties(user, ['installation']); if (installationData && installationData.installation) { storage.set('activeAccountInstallation', installationData.installation); } - this.validateUserData(userData, isBecome); const userRecord = pushUserToStore(this.store, userData); userRecord.set('authToken', token); return this.reloadUser(userRecord).then(() => { - storage.accounts.addObject(userRecord); - storage.set('activeAccount', userRecord); - this.reportNewUser(); - this.reportToIntercom(); + storage.accounts.push(userRecord); + storage.set('activeAccount', userRecord); + this.reportNewUser(); + this.reportToIntercom(); }); }, @@ -243,13 +249,12 @@ export default Service.extend({ fetchUser: task(function* (userRecord) { try { - return yield userRecord.reload({ included: includes.join(',') }); + const reloadedUser = userRecord.reload({ included: includes.join(',') }); + return yield reloadedUser; } catch (error) { const status = +error.status || +get(error, 'errors.firstObject.status'); - if (status === 401 || status === 403 || status === 500) { - this.flashes.error(TOKEN_EXPIRED_MSG); - this.signOut(); - } + this.flashes.error(TOKEN_EXPIRED_MSG); + yield this.signOut(); } }).keepLatest(), @@ -346,7 +351,8 @@ export default Service.extend({ }); function pushUserToStore(store, user) { - const record = store.push(store.normalize('user', user)); + let theUser = store.normalize('user', user); + const record = store.push(theUser); const installation = store.peekAll('installation').findBy('owner.id', user.id) || null; record.setProperties({ installation }); return record; diff --git a/app/services/broadcasts.js b/app/services/broadcasts.js index c6e083f714..a6ec3835ae 100644 --- a/app/services/broadcasts.js +++ b/app/services/broadcasts.js @@ -2,6 +2,7 @@ import { run } from '@ember/runloop'; import EmberObject, { computed } from '@ember/object'; import ArrayProxy from '@ember/array/proxy'; import Service, { inject as service } from '@ember/service'; +import { A } from '@ember/array'; export default Service.extend({ api: service(), diff --git a/app/services/flashes.js b/app/services/flashes.js index 846f276f43..eb3c3b1847 100644 --- a/app/services/flashes.js +++ b/app/services/flashes.js @@ -45,7 +45,7 @@ export default Service.extend({ let flashes = this.flashes; let model = []; if (flashes.length) { - model.pushObjects(flashes.toArray()); + model.pushObjects(...flashes.toArray()); } return model.uniq(); }), diff --git a/app/services/insights.js b/app/services/insights.js index 66723a2e7a..9f5477a29b 100644 --- a/app/services/insights.js +++ b/app/services/insights.js @@ -1,6 +1,6 @@ import Service, { inject as service } from '@ember/service'; import moment from 'moment'; -import { assign } from '@ember/polyfills'; + import { task } from 'ember-concurrency'; import { singularize } from 'ember-inflector'; @@ -54,7 +54,7 @@ export default Service.extend({ // deep so all we need to do is loop through each interval and do a shallow merge const settings = Object.values(INSIGHTS_INTERVALS).reduce((settings, interval) => { settings[interval] = {}; - assign(settings[interval], defaultIntervalSettings[interval], customIntervalSettings[interval]); + Object.assign(settings[interval], defaultIntervalSettings[interval], customIntervalSettings[interval]); return settings; }, {}); return settings; @@ -150,7 +150,7 @@ function getMetricAPISettings(subject, func, subInterval, metricNames, owner, st } function mergeMetricSettings(options, func) { - const currentOptions = assign({}, defaultOptions, options); + const currentOptions = Object.assign({}, defaultOptions, options); currentOptions.aggregator = currentOptions.aggregator || func; currentOptions.serializer = currentOptions.serializer || func; return currentOptions; @@ -200,7 +200,7 @@ function formatTimeKey(time, subInterval) { function aggregateMetrics(metricNames, metrics, aggregatorName, labels, subInterval) { const defaultData = metricNames.reduce((map, metric) => { - map[metric] = assign({}, labels); + map[metric] = Object.assign({}, labels); return map; }, {}); diff --git a/app/services/job-config-fetcher.js b/app/services/job-config-fetcher.js index e34375ae6a..530c94b572 100644 --- a/app/services/job-config-fetcher.js +++ b/app/services/job-config-fetcher.js @@ -38,7 +38,7 @@ export default Service.extend({ resolve(job._config); } else { const build = yield job.build; - yield this.store.queryRecord('build', { id: build.id, include: 'build.jobs,job.config' }); + yield this.store.smartQueryRecord('build', { id: build.id, include: 'build.jobs,job.config' }); resolve(job._config); } } catch (e) { diff --git a/app/services/live-updates-record-fetcher.js b/app/services/live-updates-record-fetcher.js index b2ccffbe34..4fc1d07df0 100644 --- a/app/services/live-updates-record-fetcher.js +++ b/app/services/live-updates-record-fetcher.js @@ -62,7 +62,7 @@ export default Service.extend({ if (needToFetchBuild || jobsData.length > 1) { let index = buildIds.indexOf(buildId); buildIds.splice(index, 1); - this.store.queryRecord('build', { id: buildId, include: 'build.jobs' }); + this.store.smartQueryRecord('build', { id: buildId, include: 'build.jobs' }); } else { this.store.findRecord('job', jobsData[0], { reload: true }); } diff --git a/app/services/log-to-log-content.js b/app/services/log-to-log-content.js new file mode 100644 index 0000000000..277e429f4a --- /dev/null +++ b/app/services/log-to-log-content.js @@ -0,0 +1,21 @@ +import Service from "@ember/service"; + + +export default Service.extend({ + log: null, + logContent: null, + + + setLogContent(logContent) { + this.set('logContent', logContent); + }, + + setLog(log) { + this.set('log', log); + }, + + clear() { + this.set('log', null); + this.set('logContent', null); + } +}); diff --git a/app/services/multi-vcs.js b/app/services/multi-vcs.js index 9fe7a7e842..dc7f4280eb 100644 --- a/app/services/multi-vcs.js +++ b/app/services/multi-vcs.js @@ -1,7 +1,11 @@ import Service, { inject as service } from '@ember/service'; import { computed } from '@ember/object'; import { not, or, reads } from '@ember/object/computed'; -import { defaultVcsConfig, vcsConfig, vcsConfigByUrlPrefix } from 'travis/utils/vcs'; +import { + defaultVcsConfig, + vcsConfig, + vcsConfigByUrlPrefix +} from 'travis/utils/vcs'; export default Service.extend({ auth: service(), diff --git a/app/services/permissions.js b/app/services/permissions.js index 6e84f68743..5d8242db2a 100644 --- a/app/services/permissions.js +++ b/app/services/permissions.js @@ -31,7 +31,9 @@ export default Service.extend({ let id = isNaN(repo) ? repo.get('id') : repo; let currentUser = this.currentUser; if (currentUser) { - return currentUser.get(permissionsType).includes(parseInt(id)); + const permType = currentUser.get(permissionsType) || currentUser[permissionsType]; + if (!permType) return false; + return permType.includes(parseInt(id)); } else { return false; } diff --git a/app/services/polling.js b/app/services/polling.js index 278c7b5a96..ff76cb2429 100644 --- a/app/services/polling.js +++ b/app/services/polling.js @@ -33,7 +33,7 @@ export default Service.extend({ let sources; sources = this.sources; if (!sources.includes(source)) { - return sources.pushObject(source); + return sources.push(source); } }, @@ -47,7 +47,7 @@ export default Service.extend({ let watchedModels; watchedModels = this.watchedModels; if (!watchedModels.includes(model)) { - return watchedModels.pushObject(model); + return watchedModels.push(model); } }, diff --git a/app/services/pusher.js b/app/services/pusher.js index 920ce7798c..639c2f1a6d 100644 --- a/app/services/pusher.js +++ b/app/services/pusher.js @@ -6,12 +6,23 @@ export default Service.extend({ store: service(), jobState: service(), liveUpdatesRecordFetcher: service(), + refreshService: service(), + + refreshEntities(event, data) { + switch(event) { + case 'build:created': + this.refreshService.refreshBuildsInRepos.perform(data.repository.id); + case 'request:created': + this.refreshService.refreshRequestsInRepos.perform(data.repository.id); + } + }, receive(event, data) { let build, commit, job; let store = this.store; let [name, type] = event.split(':'); + if (name === 'repository' && type === 'migration') { const repository = store.peekRecord('repo', data.repositoryId); repository.set('migrationStatus', data.status); @@ -59,6 +70,7 @@ export default Service.extend({ }; delete data.build.commit; store.push(store.normalize('commit', commit)); + this.refreshEntities(event, data); } } @@ -76,12 +88,11 @@ export default Service.extend({ if (event === 'job:log') { data = data.job ? data.job : data; - job = store.recordForId('job', data.id); - return job.appendLog({ + return store.findRecord('job', data.id).then((job) => job.appendLog({ number: parseInt(data.number), content: data._log, final: data.final - }); + })); } else if (data[name]) { if (data._no_full_payload) { // if payload is too big, travis-live will send us only the id of the diff --git a/app/services/raven.js b/app/services/raven.js index 58f3374baa..882b9b719f 100644 --- a/app/services/raven.js +++ b/app/services/raven.js @@ -1,6 +1,7 @@ import RavenLogger from 'ember-cli-sentry/services/raven'; import config from 'travis/config/environment'; import { inject as service } from '@ember/service'; +import { A } from '@ember/array'; export default RavenLogger.extend({ features: service(), diff --git a/app/services/refresh-service.js b/app/services/refresh-service.js new file mode 100644 index 0000000000..b4fb38dc6f --- /dev/null +++ b/app/services/refresh-service.js @@ -0,0 +1,26 @@ +import Service from "@ember/service"; +import {A} from "@ember/array"; +import {task} from "ember-concurrency"; +import {inject as service} from '@ember/service'; + + +export default class RefreshService extends Service { + @service store; + + @task( function* (arg) { + const repo = arg.get ? arg : this.store.peekRecord('repo', arg); + const offset = repo.builds.length; + yield this.store.query('build', {offset: offset, repository_id: repo.id, event_type: ['push', 'api', 'cron']}); + repo.set('buildsRefreshToken', Date.now()); + }) + refreshBuildsInRepos; + + + @task( function* (arg) { + const repo = arg.get ? arg : this.store.peekRecord('repo', arg); + const offset = repo.builds.length; + yield this.store.query('build', {offset: offset, repository_id: repo.id, event_type: ['request', 'pull_request']}); + repo.set('requestsRefreshToken', Date.now()); + }) + refreshRequestsInRepos; +} diff --git a/app/services/repositories.js b/app/services/repositories.js index 6c2baa75a5..e5a04a839c 100644 --- a/app/services/repositories.js +++ b/app/services/repositories.js @@ -5,6 +5,7 @@ import config from 'travis/config/environment'; import Repo from 'travis/models/repo'; import { task, timeout } from 'ember-concurrency'; import { computed } from '@ember/object'; +import { A } from '@ember/array'; export default Service.extend({ auth: service(), @@ -28,7 +29,7 @@ export default Service.extend({ loadingData: computed('tasks.@each.isRunning', function () { let tasks = this.tasks; - return tasks.any(task => task.get('isRunning')); + return tasks.any(task => task.isRunning); }), performSearchRequest: task(function* () { @@ -87,10 +88,8 @@ export default Service.extend({ ), sortData(repos) { - if (repos && repos.toArray) { + if (repos && repos.toArray) repos = repos.toArray(); - } - if (repos && repos.sort) { return repos.sort((repo1, repo2) => { let buildId1 = repo1.get('currentBuild.id'); diff --git a/app/services/status-images.js b/app/services/status-images.js index c4d9d4f503..677f05fd1f 100644 --- a/app/services/status-images.js +++ b/app/services/status-images.js @@ -26,11 +26,11 @@ export default Service.extend({ prefix = config.apiEndpoint; } - let slug = repo.get('slug'); + let slug = repo.slug; // In Enterprise you can toggle public mode, where even "public" repositories are hidden // in which cases we need to generate a token for all images - if (!config.publicMode || repo.get('private')) { + if (!config.publicMode || repo.private) { const token = this.auth.assetToken; return `${prefix}/${slug}.svg?token=${token}${branch ? `&branch=${branch}` : ''}`; } else { diff --git a/app/services/storage.js b/app/services/storage.js index 04921811c1..39137c438f 100644 --- a/app/services/storage.js +++ b/app/services/storage.js @@ -3,6 +3,7 @@ import Service, { inject as service } from '@ember/service'; export default Service.extend({ auth: service('storage/auth'), utm: service('storage/utm'), + store: service(), get billingStep() { return +this.getItem('travis.billing_step'); @@ -33,7 +34,52 @@ export default Service.extend({ return this.parseWithDefault('travis.billing_info', {}); }, set billingInfo(value) { - this.setItem('travis.billing_info', JSON.stringify(value)); + if(!value) + return this.setItem('travis.billing_info', value); + + const data + = (({ + address, + address2, + billingEmail, + billingEmailRead, + city, + company, + country, + firstName, + lastName, + hasLocalRegistration, + id, + isReloading, + state, + subscription, + vatId, + zipCode, + notifications}) => + ({ + address, + address2, + billingEmail, + billingEmailRead, + city, + company, + country, + firstName, + lastName, + hasLocalRegistration, + id, + isReloading, + state, + subscription, + vatId, + zipCode, + notifications + }))(value); + + return this.dataSubscription(data).then((datum) => { + return this.setItem('travis.billing_info', JSON.stringify(datum)); + }); + }, get billingPlan() { @@ -43,6 +89,17 @@ export default Service.extend({ this.setItem('travis.billing_plan', JSON.stringify(value)); }, + async dataSubscription(data) { + data.subscription = await data.subscription; + const model = data.subscription; + const snapshot = model._createSnapshot(); + const serializer = this.store.serializerFor('subscription'); + const serializedData = serializer.serialize(snapshot); + data.subscription = serializedData; + + return data; + }, + clearPreferencesData() { this.removeItem('travis.features'); }, diff --git a/app/services/storage/auth.js b/app/services/storage/auth.js index 6e1bc0aab4..0683748bb4 100644 --- a/app/services/storage/auth.js +++ b/app/services/storage/auth.js @@ -1,128 +1,144 @@ -import { computed } from '@ember/object'; +import Service, { inject as service } from '@ember/service'; +import {action, computed} from '@ember/object'; import { assert } from '@ember/debug'; import { parseWithDefault } from '../storage'; -import Service, { inject as service } from '@ember/service'; +import { underscoreKeys } from "travis/utils/underscore-keys"; +import {tracked} from "@glimmer/tracking"; const storage = getStorage(); -export default Service.extend({ - store: service(), +export default class Auth extends Service { + @service store; - token: computed({ - get() { - return storage.getItem('travis.token') || null; - }, - set(key, token) { - assert('Token storage is read-only', token === null); - storage.removeItem('travis.token'); - return null; - } - }), - - rssToken: computed({ - get() { - return storage.getItem('travis.rssToken') || null; - }, - set(key, token) { - assert('RSS Token storage is read-only', token === null); - storage.removeItem('travis.rssToken'); - return null; - } - }), - - user: computed({ - get() { - const data = parseWithDefault(storage.getItem('travis.user'), null); - return data && data.user || data; - }, - set(key, user) { - assert('User storage is read-only', user === null); - storage.removeItem('travis.user'); - return null; - } - }), - - accounts: computed({ - get() { - const accountsData = storage.getItem('travis.auth.accounts'); - const accounts = parseWithDefault(accountsData, []).map(account => - extractAccountRecord(this.store, account) - ); - accounts.addArrayObserver(this, { - willChange: 'persistAccounts', - didChange: 'persistAccounts' - }); - return accounts; - }, - set(key, accounts) { - this.persistAccounts(accounts); - return accounts; - } - }).volatile(), + @tracked _accounts = []; + + constructor() { + super(...arguments); + this.loadAccounts(); + } + + loadAccounts() { + const accountsData = storage.getItem('travis.auth.accounts'); + console.log("accounts Data", accountsData); + this.accounts = parseWithDefault(accountsData, []).map(account => + extractAccountRecord(this.store, account) + ); + } + + get accounts() { + return this._accounts; + } + + set accounts(accounts) { + this.setAccounts(accounts); + } + + @action + setAccounts(accounts) { + this._accounts = accounts; + this.persistAccounts(accounts); + } persistAccounts(newValue) { const records = (newValue || []).map(record => serializeUserRecord(record)); storage.setItem('travis.auth.accounts', JSON.stringify(records)); - }, - - activeAccountId: computed({ - get() { - return +storage.getItem('travis.auth.activeAccountId'); - }, - set(key, id) { - if (id === null) { - storage.removeItem('travis.auth.activeAccountId'); - return null; - } else { - storage.setItem('travis.auth.activeAccountId', id); - return id; - } - } - }), - - activeAccountInstallation: computed({ - get() { - return +storage.getItem('travis.auth.activeAccountInstallation'); - }, - set(key, id) { - if (id === null) { - storage.removeItem('travis.auth.activeAccountInstallation'); - return null; - } else { - storage.setItem('travis.auth.activeAccountInstallation', id); - return id; - } + } + + @computed + get token() { + return storage.getItem('travis.token') || null; + } + + set token(value) { + assert('Token storage is read-only', value === null); + storage.removeItem('travis.token'); + return null; + } + + @computed + get rssToken() { + return storage.getItem('travis.rssToken') || null; + } + + set rssToken(value) { + assert('RSS Token storage is read-only', value === null); + storage.removeItem('travis.rssToken'); + return null; + } + + @computed + get user() { + const data = parseWithDefault(storage.getItem('travis.user'), {}); + return underscoreKeys(data && data.user || data); + } + + set user(value) { + assert('User storage is read-only', value === null); + storage.removeItem('travis.user'); + return null; + } + + @computed('accounts.[]', 'activeAccountId') + get activeAccount() { + const { accounts, activeAccountId } = this; + return accounts.find(account => +account.id === activeAccountId); + } + + set activeAccount(value) { + if (value) + this.setAccounts([value]); + const id = value && value.id || null; + this.activeAccountId = id; + return value; + } + + @computed + get activeAccountId() { + return +storage.getItem('travis.auth.activeAccountId'); + } + + set activeAccountId(value) { + if (value === null) { + storage.removeItem('travis.auth.activeAccountId'); + return null; + } else { + storage.setItem('travis.auth.activeAccountId', value); + return value; } - }), - - activeAccount: computed({ - get() { - const { accounts, activeAccountId } = this; - return accounts.find(account => +account.id === activeAccountId); - }, - set(key, account) { - const id = account && account.id || null; - this.set('activeAccountId', id); - return account; + } + + @computed + get activeAccountInstallation() { + return +storage.getItem('travis.auth.activeAccountInstallation'); + } + + set activeAccountInstallation(value) { + if (value === null) { + storage.removeItem('travis.auth.activeAccountInstallation'); + return null; + } else { + storage.setItem('travis.auth.activeAccountInstallation', value); + return value; } - }), + } - isBecome: computed(() => !!storage.getItem('travis.auth.become')), + @computed + get isBecome() { + return !!storage.getItem('travis.auth.become'); + } clearLoginData() { storage.removeItem('travis.token'); storage.removeItem('travis.user'); storage.removeItem('travis.auth.become'); - }, + } clear() { this.clearLoginData(); storage.removeItem('travis.auth.accounts'); storage.removeItem('travis.auth.activeAccountId'); } - -}); - +} // HELPERS function getStorage() { @@ -135,6 +151,7 @@ function getStorage() { // primary storage for auth is the one in which auth data was updated last const sessionStorageUpdatedAt = +sessionStorage.getItem('travis.auth.updatedAt'); const localStorageUpdatedAt = +localStorage.getItem('travis.auth.updatedAt'); + return sessionStorageUpdatedAt > localStorageUpdatedAt ? sessionStorage : localStorage; } diff --git a/app/services/store.js b/app/services/store.js index f082e1dd5a..2d5a5dece2 100644 --- a/app/services/store.js +++ b/app/services/store.js @@ -1,169 +1,193 @@ /* eslint-disable camelcase */ -import Store from 'ember-data/store'; +import Store, {CacheHandler} from '@ember-data/store'; +import RequestManager from '@ember-data/request'; +import {LegacyNetworkHandler} from '@ember-data/legacy-compat'; + import PaginatedCollectionPromise from 'travis/utils/paginated-collection-promise'; -import { inject as service } from '@ember/service'; +import {inject as service} from '@ember/service'; import FilteredArrayManager from 'travis/utils/filtered-array-manager'; import fetchLivePaginatedCollection from 'travis/utils/fetch-live-paginated-collection'; -export default Store.extend({ - auth: service(), - - defaultAdapter: 'application', - adapter: 'application', - - init() { - this._super(...arguments); - this.shouldAssertMethodCallsOnDestroyedStore = true; - this.filteredArraysManager = FilteredArrayManager.create({ store: this }); - }, - - // Fetch a filtered collection. - // - // modelName - a type of the model passed as a string, for example 'repo' - // queryParams - params that will be passed to the store.query function when - // fetching records on the initial call. Passing null or - // undefined here will stop any requests from happening, - // filtering will be based only on existing records - // filterFunction - a function that will be called to determine wheather a - // record should be included in the filtered collection. A - // passed function will be called with a record as an - // argument - // dependencies - a list of dependencies that will trigger the re-evaluation - // of a record. When one of the dependencies changes on any - // of the records in the store, it may be added or removed - // from a filtered array. - // forceReload - if set to true, store.query will be run on each call - // instead of only running it on the first run - // - // Example: - // - // store.filter( - // 'repo', - // { starred: true }, - // (repo) => repo.get('starred'), - // ['starred'], - // true - // ) - // - // Rationale for our own implementation of store.filter: - // - // The default implementation of filter is rather limited and misses a few - // scenarios important to us. The problem is that when you use the default - // store.filter implementation, it evaluates if a record should be added to a - // filtered array only when a new record is added to the store or when a - // property on a record itself changes. That means that we can't observe - // computed properties that depend on anything else than defined properties. - // Our implementation allows to pass dependencies as an optional argument, - // which allows to pass any property as a dependency. - // - // One more change in relation to the default filter representation is that - // the default store.filter implementation will always fetch new records. The - // new implementation has an identity map built in and it will always fetch - // the same array for the same set of arguments. Thanks to that running - // store.filter multiple times will return immediately on the second and - // subsequent tries. - // - // If you need to also fetch new results each time the function is run, you - // can set forceReload option to true, but it will still resolve immediately - // once a first query is already finished. - // - // For more info you may also see comments in FilteredArraysManager. - filter(modelName, queryParams, filterFunction, dependencies, forceReload) { - if (arguments.length === 0) { - throw new Error('store.filter called with no arguments'); +export default class ExtendedStore extends Store { + @service auth; + + defaultAdapter = 'application'; + adapter = 'application'; + + constructor() { + super(...arguments); + this.shouldAssertMethodCallsOnDestroyedStore = true; + this.filteredArraysManager = FilteredArrayManager.create({store: this}); + this.requestManager = new RequestManager(); + this.requestManager.use([LegacyNetworkHandler]); + this.requestManager.useCache(CacheHandler); } - if (arguments.length === 1) { - return this.peekAll(modelName); + + // Fetch a filtered collection. + // + // modelName - a type of the model passed as a string, for example 'repo' + // queryParams - params that will be passed to the store.query function when + // fetching records on the initial call. Passing null or + // undefined here will stop any requests from happening, + // filtering will be based only on existing records + // filterFunction - a function that will be called to determine wheather a + // record should be included in the filtered collection. A + // passed function will be called with a record as an + // argument + // dependencies - a list of dependencies that will trigger the re-evaluation + // of a record. When one of the dependencies changes on any + // of the records in the store, it may be added or removed + // from a filtered array. + // forceReload - if set to true, store.query will be run on each call + // instead of only running it on the first run + // + // Example: + // + // store.filter( + // 'repo', + // { starred: true }, + // (repo) => repo.get('starred'), + // ['starred'], + // true + // ) + // + // Rationale for our own implementation of store.filter: + // + // The default implementation of filter is rather limited and misses a few + // scenarios important to us. The problem is that when you use the default + // store.filter implementation, it evaluates if a record should be added to a + // filtered array only when a new record is added to the store or when a + // property on a record itself changes. That means that we can't observe + // computed properties that depend on anything else than defined properties. + // Our implementation allows to pass dependencies as an optional argument, + // which allows to pass any property as a dependency. + // + // One more change in relation to the default filter representation is that + // the default store.filter implementation will always fetch new records. The + // new implementation has an identity map built in and it will always fetch + // the same array for the same set of arguments. Thanks to that running + // store.filter multiple times will return immediately on the second and + // subsequent tries. + // + // If you need to also fetch new results each time the function is run, you + // can set forceReload option to true, but it will still resolve immediately + // once a first query is already finished. + // + // For more info you may also see comments in FilteredArraysManager. + + filter(modelName, queryParams, filterFunction, dependencies, forceReload) { + if (arguments.length === 0) { + throw new Error('store.filter called with no arguments'); + } + if (arguments.length === 1) { + return this.peekAll(modelName); + } + if (arguments.length === 2) { + filterFunction = queryParams; + return this.filteredArraysManager.filter(modelName, null, filterFunction, ['']); + } + + if (!dependencies) { + return this.filteredArraysManager.filter(modelName, queryParams, filterFunction, ['']); + } else { + return this.filteredArraysManager.fetchArray(modelName, queryParams, filterFunction, dependencies, forceReload); + } } - if (arguments.length === 2) { - filterFunction = queryParams; - return this.filteredArraysManager.filter(modelName, null, filterFunction, ['']); + + smartQueryRecord(type, ...params) { + return this.queryRecord(type, ...params); } - if (!dependencies) { - return this.filteredArraysManager.filter(modelName, queryParams, filterFunction, ['']); - } else { - return this.filteredArraysManager.fetchArray(modelName, queryParams, filterFunction, dependencies, forceReload); + push(object) { + console.log(`single push ${JSON.stringify(object)}`) + const id = object.data.id + const type = object.data.type; + + if (this.shouldAdd(object)) { + const included = object.included ? JSON.parse(JSON.stringify(object.included)) : null; + if (included) { + object.included = included.filter(single => { + return this.shouldAdd({data: single}) + }); + } + return super.push(object); + } else { + return this.peekRecord(type, id); + } } - }, - - // Returns a collection with pagination data. If the first page is requested, - // the collection will be live updated. Otherwise keeping the calculations and - // figuring out if the record should be put on the page is not easily - // achieveable (or even impossible in some cases). - // - // modelName - a type of the model as a string, for example 'repo' - // queryParams - params for a store.query call that will be fired to fetch the - // data from the server - // options - additional options: - // filter - a filter function that will be used to test if a - // record should be added or removed from the array. It - // will be called with a record under test as an - // argument. It only matters for live updates - // sort - either a string or a function to sort the collection - // with. If it's a string, it should be the name of the - // property to sort by, with an optional ':asc' or - // ':desc' suffixes, for example 'id:desc'. If it's a - // function it will be called with 2 records to compare - // as an argument - // dependencies - a set of dependencies that will be watched to - // re-evaluate if a record should be a part of a - // collection - // forceReload - if set to true, store.query will be run on - // call - // - // Examples: - // - // store.paginated( - // 'repo', - // { active: true, offset: 0, limit: 10 }, - // { - // filter: (repo) => repo.get('active'), - // sort: 'id:desc', - // dependencies: ['active'], - // forceReload: true - // } - // - paginated(modelName, queryParams, options = {}) { - let allowLive = !options.hasOwnProperty('live') || options.live; - if (!parseInt(queryParams.offset) && allowLive) { - // we're on the first page, live updates can be enabled - return fetchLivePaginatedCollection(this, ...arguments); - } else { - return PaginatedCollectionPromise.create({ - content: this.query(...arguments) - }); + + shouldAdd(object) { + const data = object.data; + const type = data.type; + const newUpdatedAt = data.attributes ? data.attributes.updatedAt : null; + const id = data.id + if (newUpdatedAt) { + const record = this.peekRecord(type, id); + if (record) { + const existingUpdatedAt = record.get('updatedAt'); + return !existingUpdatedAt || existingUpdatedAt <= newUpdatedAt; + } else { + return true + } + } else { + return true + } } - }, - - // We shouldn't override private methods, but at the moment I don't see any - // other way to prevent updating records with outdated data. - // _pushInternalModel seems to be the entry point for all of the data loading - // related functions, so it's the best place to override to check the - // updated_at field - _pushInternalModel(data) { - let type = data.type; - let newUpdatedAt = data.attributes ? data.attributes.updatedAt : null; - - if (newUpdatedAt) { - let internalModel = this._internalModelForId(type, data.id), - record = internalModel.getRecord(), - existingUpdatedAt = record.get('updatedAt'); - - if (!existingUpdatedAt || existingUpdatedAt <= newUpdatedAt) { - return this._super(...arguments); - } else { - // record to push is older than the existing one, we need to skip, - // but we still need to return the result - return internalModel; - } - } else { - return this._super(...arguments); + + + // Returns a collection with pagination data. If the first page is requested, + // the collection will be live updated. Otherwise keeping the calculations and + // figuring out if the record should be put on the page is not easily + // achieveable (or even impossible in some cases). + // + // modelName - a type of the model as a string, for example 'repo' + // queryParams - params for a store.query call that will be fired to fetch the + // data from the server + // options - additional options: + // filter - a filter function that will be used to test if a + // record should be added or removed from the array. It + // will be called with a record under test as an + // argument. It only matters for live updates + // sort - either a string or a function to sort the collection + // with. If it's a string, it should be the name of the + // property to sort by, with an optional ':asc' or + // ':desc' suffixes, for example 'id:desc'. If it's a + // function it will be called with 2 records to compare + // as an argument + // dependencies - a set of dependencies that will be watched to + // re-evaluate if a record should be a part of a + // collection + // forceReload - if set to true, store.query will be run on + // call + // + // Examples: + // + // store.paginated( + // 'repo', + // { active: true, offset: 0, limit: 10 }, + // { + // filter: (repo) => repo.get('active'), + // sort: 'id:desc', + // dependencies: ['active'], + // forceReload: true + // } + // + + paginated(modelName, queryParams, options = {}) { + let allowLive = !options.hasOwnProperty('live') || options.live; + if (!parseInt(queryParams.offset) && allowLive) { + // we're on the first page, live updates can be enabled + return fetchLivePaginatedCollection(this, ...arguments); + } else { + return PaginatedCollectionPromise.create({ + content: this.query(...arguments) + }); + } } - }, - destroy() { - this._super(...arguments); - this.filteredArraysManager.destroy(); - } -}); + + destroy() { + this.filteredArraysManager.destroy(); + super.destroy(); + } +} diff --git a/app/services/stripe.js b/app/services/stripe.js index 0e27207836..ef3baf9b02 100644 --- a/app/services/stripe.js +++ b/app/services/stripe.js @@ -24,7 +24,7 @@ export default Service.extend({ if (clientSecret) { yield this.stripev3.handleCardPayment(clientSecret); } - yield this.accounts.fetchV2Subscriptions.perform(); + yield this.accounts.fetchV2Subscriptions.linked().perform(); }).drop(), handleError(stripeError) { diff --git a/app/services/tab-states.js b/app/services/tab-states.js index 294dee0694..27b58de93e 100644 --- a/app/services/tab-states.js +++ b/app/services/tab-states.js @@ -18,6 +18,9 @@ export default Service.extend({ switchSidebar(state) { this.set('sidebarTab', state); }, + setMainTab(tabName) { + this.set('mainTab', tabName); + }, switchSidebarToOwned() { this.switchSidebar(SIDEBAR_TAB_STATES.OWNED); }, diff --git a/app/services/tasks.js b/app/services/tasks.js index 988491ea70..ef8604fa58 100644 --- a/app/services/tasks.js +++ b/app/services/tasks.js @@ -1,5 +1,6 @@ import Service, { inject as service } from '@ember/service'; import { task } from 'ember-concurrency'; +import { pushPayload } from 'travis/serializers/beta-migration-request' /** * Service for shared Ember Concurrency tasks. @@ -22,10 +23,28 @@ export default Service.extend({ }); if (data) { - this.store.pushPayload('beta-migration-request', data); + const modelClass = this.store.modelFor('beta-migration-request'); + const serializer = this.store.serializerFor('application') + const json = serializer.normalizeArrayResponse(this.store, modelClass, data); + this.store.push(json); } return this.store.peekAll('beta-migration-request'); - }).drop() + }).drop(), + + fetchRepoOwnerAllowance: task(function* (repo) { + const allowance = this.store.peekRecord('allowance', repo.id); + if (allowance) + return allowance; + return yield this.store.smartQueryRecord('allowance', { login: repo.owner.login, provider: repo.provider || 'github' }); + }).drop(), + fetchMessages: task(function* (request) { + const repoId = request.get('repo.id'); + const requestId = request.get('build.request.id'); + if (repoId && requestId) { + const response = yield request.api.get(`/repo/${repoId}/request/${requestId}/messages`) || {}; + return response.messages; + } + }).drop() }); diff --git a/app/services/update-times.js b/app/services/update-times.js index e5afa4ad4c..10c2e48f82 100644 --- a/app/services/update-times.js +++ b/app/services/update-times.js @@ -3,31 +3,41 @@ import Service from '@ember/service'; import config from 'travis/config/environment'; import eventually from 'travis/utils/eventually'; import Visibility from 'visibilityjs'; +import { task, timeout } from 'ember-concurrency'; +import { on } from '@ember/object/evented'; export default Service.extend({ allowFinishedBuilds: false, + isDestroyedOrDestroying: false, init() { - const visibilityId = Visibility.every(config.intervals.updateTimes, bind(this, 'updateTimes')); - const intervalId = setInterval(this.resetAllowFinishedBuilds.bind(this), 60000); const records = []; - this.setProperties({ visibilityId, intervalId, records }); + this.setProperties({ records }); return this._super(...arguments); }, + updateTimesTask: task(function* () { + while (true) { + yield this.updateTimes(); + yield timeout(config.intervals.updateTimes); + } + }).on('init'), + + willDestroy() { - Visibility.stop(this.visibilityId); - clearInterval(this.intervalId); + this.set('isDestroyedOrDestroying', true); this._super(...arguments); }, resetAllowFinishedBuilds() { + if (this.isDestroyedOrDestroying) { return; } this.set('allowFinishedBuilds', true); }, updateTimes() { + if (this.isDestroyedOrDestroying) { return; } let records = this.records; records.filter(record => this.allowFinishedBuilds || !record.get('isFinished')) diff --git a/app/services/utm.js b/app/services/utm.js index 1b75f8ba7a..ac0cfaba3a 100644 --- a/app/services/utm.js +++ b/app/services/utm.js @@ -51,7 +51,7 @@ export default Service.extend({ }), hasParamsInUrl: computed('searchParams', function () { - return UTM_FIELD_NAMES.any(field => this.searchParams.has(field)); + return UTM_FIELD_NAMES.some(field => this.searchParams.has(field)); }), peek(fields, includeEmpty = true) { diff --git a/app/styles/app.css b/app/styles/app.css new file mode 100644 index 0000000000..2763afa4cf --- /dev/null +++ b/app/styles/app.css @@ -0,0 +1 @@ +/* Ember supports plain CSS out of the box. More info: https://cli.emberjs.com/release/advanced-use/stylesheets/ */ diff --git a/app/templates/account-error.hbs b/app/templates/account-error.hbs index d77a1bcac3..b4a4819ad4 100644 --- a/app/templates/account-error.hbs +++ b/app/templates/account-error.hbs @@ -16,6 +16,5 @@ If you believe you've received this message in error, please contact support. - -

+

diff --git a/app/templates/account/settings.hbs b/app/templates/account/settings.hbs index 0f554f4dae..41935681ec 100644 --- a/app/templates/account/settings.hbs +++ b/app/templates/account/settings.hbs @@ -7,7 +7,7 @@

Confirm account

- + {{#if this.userHasNoEmails}}
We don’t have your email address. @@ -110,8 +110,7 @@ @query={{hash tab="insights"}} > View {{this.account.fullName}}'s Insights - -

+

diff --git a/app/templates/branches.hbs b/app/templates/branches.hbs index ec1cb865d2..298d7d53d3 100644 --- a/app/templates/branches.hbs +++ b/app/templates/branches.hbs @@ -6,7 +6,7 @@ Default Branch
    - +
{{/if}} @@ -17,7 +17,7 @@
    {{#each this.activeBranches as |branch|}} - + {{/each}}
@@ -29,7 +29,7 @@
    {{#each this.inactiveBranches as |branch|}} - + {{/each}}
diff --git a/app/templates/builds.hbs b/app/templates/builds.hbs index 7f8a16ef0d..275d59a785 100644 --- a/app/templates/builds.hbs +++ b/app/templates/builds.hbs @@ -1,7 +1,7 @@ {{#if this.model.isLoaded}}
    {{#each this.builds as |build|}} - + {{else}} {{/each}} diff --git a/app/templates/caches.hbs b/app/templates/caches.hbs index 52993fc772..a4d210b69c 100644 --- a/app/templates/caches.hbs +++ b/app/templates/caches.hbs @@ -9,36 +9,40 @@
- {{#if this.model.pushes.length}} + {{#if this.pushes.length}}

Pushes

{{/if}} - {{#if this.model.pullRequests.length}} + {{#if this.pullRequests.length}}

{{vcs-vocab this.repo.vcsType 'pullRequest' plural=true}}

diff --git a/app/templates/components/.gitkeep b/app/templates/components/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/templates/components/account-token.hbs b/app/templates/components/account-token.hbs index e8b5201244..21eea25818 100644 --- a/app/templates/components/account-token.hbs +++ b/app/templates/components/account-token.hbs @@ -21,14 +21,14 @@
Copy token - {{/if}} diff --git a/app/templates/components/add-custom-key.hbs b/app/templates/components/add-custom-key.hbs index 4d14415966..d61fc9abeb 100644 --- a/app/templates/components/add-custom-key.hbs +++ b/app/templates/components/add-custom-key.hbs @@ -19,7 +19,7 @@