Convert codebase to typescript and latest Ember (#157)

* Update eslint setup (#134)

* Convert services (#136)

* Convert login and logged-in routes (#137)

* Convert project routes and controllers (#138)

* Convert activities and comments routes (#140)

* Convert project edit sub-routes and controllers (#139)

* Convert files routes and controllers (#141)

* Convert revision routes and controllers (#142)

* Convert phoenix service (#143)

* Convert translation routes and controllers (#144)

* Convert versions routes and controllers (#145)

* Convert JIPT routes and controllers (#146)

* Convert Sass variables to CSS modules @value (#147)

* Convert CSS variables and fix projects page components

* Fix a bunch of components

* Fix another bunch of components

* Update styles and missing global for powerselect

* Fix typings

* Fix typescript warnings

* Add typings for phoenix and file-saver vendor

* Fix github ci

Co-authored-by: Charles Demers <cdemers@mirego.com>
Co-authored-by: Charles Demers <charles.demers6@gmail.com>
This commit is contained in:
Simon Prévost 2020-04-07 07:47:33 -04:00 committed by GitHub
parent cc48848377
commit 9567db9b4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
858 changed files with 27597 additions and 18244 deletions

View File

@ -1,27 +0,0 @@
{
"globals": {
"JsDiff": true
},
"plugins": [
"ember",
"mirego"
],
"extends": [
"plugin:mirego/recommended",
"prettier"
],
"env": {
"es6": true
},
"rules": {
"no-irregular-whitespace": 0,
"ember/jquery-ember-run": 2,
"ember/use-brace-expansion": 2,
"ember/no-side-effects": 2,
"ember/order-in-routes": 2,
"ember/order-in-models": 2,
"ember/order-in-controllers": 2,
"ember/no-empty-attrs": 2,
"ember/closure-actions": 2
}
}

231
.eslintrc.js Normal file
View File

@ -0,0 +1,231 @@
module.exports = {
root: true,
overrides: [
{
files: ['webapp/app/**/*', 'webapp/tests/**/*', 'webapp/types/**/*'],
parser: '@typescript-eslint/parser',
parserOptions: {
project: './webapp/tsconfig.json'
},
plugins: ['@typescript-eslint', 'ember', 'mirego'],
extends: [
'plugin:mirego/recommended',
'prettier',
'prettier/@typescript-eslint',
'typestrict'
],
env: {
es6: true,
browser: true
},
globals: {
JsDiff: true
},
rules: {
'complexity': 0,
'no-irregular-whitespace': 0,
'ember/closure-actions': 2,
'ember/named-functions-in-promises': 0,
'ember/new-module-imports': 2,
'ember/no-global-jquery': 2,
'ember/no-on-calls-in-components': 2,
'ember/no-duplicate-dependent-keys': 2,
'ember/no-side-effects': 2,
'ember/require-super-in-init': 2,
'ember/avoid-leaking-state-in-ember-objects': 2,
'ember/use-brace-expansion': 2,
'ember/jquery-ember-run': 2,
'ember/order-in-routes': 2,
'ember/order-in-controllers': 2,
'ember/no-empty-attrs': 2,
'@typescript-eslint/adjacent-overload-signatures': 2,
'@typescript-eslint/array-type': [2, {default: 'array-simple'}],
'@typescript-eslint/await-thenable': 2,
'@typescript-eslint/ban-ts-ignore': 2,
'@typescript-eslint/consistent-type-assertions': [
2,
{assertionStyle: 'as'}
],
'@typescript-eslint/consistent-type-definitions': [2, 'interface'],
'@typescript-eslint/member-delimiter-style': [
2,
{
multiline: {
delimiter: 'semi',
requireLast: true
},
singleline: {
delimiter: 'semi',
requireLast: false
}
}
],
'@typescript-eslint/member-ordering': 2,
'@typescript-eslint/no-empty-interface': 2,
'@typescript-eslint/no-floating-promises': 2,
'@typescript-eslint/no-misused-new': 2,
'@typescript-eslint/no-misused-promises': 2,
'@typescript-eslint/no-non-null-assertion': 2,
'@typescript-eslint/no-parameter-properties': 2,
'@typescript-eslint/no-require-imports': 2,
'@typescript-eslint/no-unnecessary-type-assertion': 2,
'@typescript-eslint/promise-function-async': 2,
'@typescript-eslint/require-await': 2,
'@typescript-eslint/type-annotation-spacing': 2,
'@typescript-eslint/unified-signatures': 2,
'no-unused-vars': 0,
'no-invalid-this': 0,
'@typescript-eslint/no-unused-vars': 2,
}
},
{
files: [
'webapp/.ember-cli.js',
'webapp/.eslintrc.js',
'webapp/.template-lintrc.js',
'webapp/ember-cli-build.js',
'webapp/testem.js',
'webapp/config/**/*.js',
'webapp/lib/*/index.js',
'webapp/scripts/**/*.js',
'webapp/node-server/**/*.js'
],
parserOptions: {
sourceType: 'script',
ecmaVersion: 2015
},
env: {
browser: false,
node: true
},
plugins: ['node'],
rules: {
'@typescript-eslint/no-require-imports': 0,
// this can be removed once the following is fixed
// https://github.com/mysticatea/eslint-plugin-node/issues/77
'node/no-unpublished-require': 'off'
},
extends: ['plugin:node/recommended']
},
{
files: ['cli/**/*'],
env: {
es6: true
},
globals: {
process: true
},
parser: '@typescript-eslint/parser',
parserOptions: {
project: './cli/tsconfig.json'
},
plugins: ['@typescript-eslint', 'mirego'],
extends: [
'plugin:mirego/recommended',
'prettier',
'prettier/@typescript-eslint'
],
rules: {
'complexity': 0,
'no-irregular-whitespace': 0,
'no-unused-vars': 0,
'no-console': 0,
'max-nested-callbacks': [2, {max: 3}],
'@typescript-eslint/adjacent-overload-signatures': 2,
'@typescript-eslint/array-type': [2, {default: 'array-simple'}],
'@typescript-eslint/await-thenable': 2,
'@typescript-eslint/ban-ts-ignore': 2,
'@typescript-eslint/consistent-type-assertions': [
2,
{assertionStyle: 'as'}
],
'@typescript-eslint/consistent-type-definitions': [2, 'interface'],
'@typescript-eslint/member-delimiter-style': [
2,
{
multiline: {
delimiter: 'semi',
requireLast: true
},
singleline: {
delimiter: 'semi',
requireLast: false
}
}
],
'@typescript-eslint/member-ordering': 2,
'@typescript-eslint/no-empty-interface': 2,
'@typescript-eslint/no-floating-promises': 2,
'@typescript-eslint/no-misused-new': 2,
'@typescript-eslint/no-misused-promises': 2,
'@typescript-eslint/no-non-null-assertion': 0,
'@typescript-eslint/no-parameter-properties': 2,
'@typescript-eslint/no-require-imports': 2,
'@typescript-eslint/no-unnecessary-type-assertion': 2,
'@typescript-eslint/promise-function-async': 2,
'@typescript-eslint/require-await': 2,
'@typescript-eslint/type-annotation-spacing': 2,
'@typescript-eslint/unified-signatures': 2,
'@typescript-eslint/no-unused-vars': 2
}
},
{
files: ['jipt/**/*'],
env: {
es6: true
},
parser: '@typescript-eslint/parser',
parserOptions: {
project: './jipt/tsconfig.json'
},
plugins: ['@typescript-eslint', 'mirego'],
extends: [
'plugin:mirego/recommended',
'prettier',
'prettier/@typescript-eslint'
],
rules: {
'complexity': 0,
'no-irregular-whitespace': 0,
'no-unused-vars': 0,
'@typescript-eslint/adjacent-overload-signatures': 2,
'@typescript-eslint/array-type': [2, {default: 'array-simple'}],
'@typescript-eslint/await-thenable': 2,
'@typescript-eslint/ban-ts-ignore': 2,
'@typescript-eslint/consistent-type-assertions': [
2,
{assertionStyle: 'as'}
],
'@typescript-eslint/consistent-type-definitions': [2, 'interface'],
'@typescript-eslint/member-delimiter-style': [
2,
{
multiline: {
delimiter: 'semi',
requireLast: true
},
singleline: {
delimiter: 'semi',
requireLast: false
}
}
],
'@typescript-eslint/member-ordering': 2,
'@typescript-eslint/no-empty-interface': 2,
'@typescript-eslint/no-floating-promises': 2,
'@typescript-eslint/no-misused-new': 2,
'@typescript-eslint/no-misused-promises': 2,
'@typescript-eslint/no-non-null-assertion': 2,
'@typescript-eslint/no-parameter-properties': 2,
'@typescript-eslint/no-require-imports': 2,
'@typescript-eslint/no-unnecessary-type-assertion': 2,
'@typescript-eslint/promise-function-async': 2,
'@typescript-eslint/require-await': 2,
'@typescript-eslint/type-annotation-spacing': 2,
'@typescript-eslint/unified-signatures': 2,
'@typescript-eslint/no-unused-vars': 2
}
}
]
};

View File

@ -1,38 +1,48 @@
name: CI
on: [push, pull_request]
jobs:
test:
env:
MIX_ENV: test
runs-on: ubuntu-latest
services:
db:
image: postgres:10
ports: ['5432:5432']
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
POSTGRES_DB: accent_test
POSTGRES_PASSWORD: password
ports: ["5432:5432"]
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
env:
MIX_ENV: test
DATABASE_URL: postgres://postgres:password@localhost/accent_test
steps:
- uses: actions/checkout@v1
- uses: actions/setup-elixir@v1.0.0
- uses: actions/checkout@v2
- uses: actions/setup-elixir@v1
with:
otp-version: 22.x
elixir-version: 1.9.x
- uses: actions/setup-node@v1
with:
node-version: 10.14.x
- name: Install System Dependencies
run: |
sudo apt-get update
sudo apt-get install -y gcc libyaml-dev python-yaml
- name: Install Elixir Dependencies
run: |
mix local.rebar --force
mix local.hex --force
mix deps.get
mix deps.compile
- name: Install NodeJS Dependencies
run: |
npm config set spin false
@ -40,15 +50,18 @@ jobs:
npm i --prefix webapp --no-audit --no-color
npm i --prefix cli --no-audit --no-color
npm i --prefix jipt --no-audit --no-color
- name: Build webapp production
run: npm run build-production-inline --prefix webapp
- name: Run Tests
run: |
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/accent_test mix ecto.setup
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/accent_test ./priv/scripts/ci-check.sh
mix ecto.setup
./priv/scripts/ci-check.sh
- name: Build CLI
run: npm --prefix cli run build
- name: Build JIPT
run: npm --prefix jipt run build-production-inline
- name: Coverage report
run: DATABASE_URL=postgresql://postgres:postgres@localhost:5432/accent_test mix coveralls.post --token ${{ secrets.COVERALLS_REPO_TOKEN }} --name 'github-actions' --branch ${{ github.ref }} --committer ${{ github.actor }} --sha ${{ github.sha }}
run: mix coveralls.post --token ${{ secrets.COVERALLS_REPO_TOKEN }} --name 'github-actions' --branch ${{ github.ref }} --committer ${{ github.actor }} --sha ${{ github.sha }}

View File

@ -71,7 +71,7 @@ compose-build: ## Build the Docker image from the docker-compose.yml file
# ----------
.PHONY: lint
lint: lint-compile lint-format lint-credo lint-eslint lint-prettier lint-tslint lint-template-hbs ## Run lint tools on the code
lint: lint-compile lint-format lint-credo lint-eslint lint-prettier lint-template-hbs ## Run lint tools on the code
.PHONY: lint-compile
lint-compile:
@ -87,19 +87,19 @@ lint-credo:
.PHONY: lint-eslint
lint-eslint:
./node_modules/.bin/eslint webapp/app/. webapp/tests/. cli/. jipt/.
.PHONY: lint-tslint
lint-tslint:
./node_modules/.bin/tslint -c tslint.json '{cli,jipt}/src/**/*.{js,ts,json}'
npx eslint --ext .js,.ts ./webapp/app ./cli ./jipt
.PHONY: lint-prettier
lint-prettier:
./node_modules/.bin/prettier --check './{webapp,jipt,cli}/!(node_modules)/**/*.{js,ts,json,svg,scss,md}' '*.md'
npx prettier --check './{webapp,jipt,cli}/!(node_modules)/**/*.{js,ts,json,svg,scss,md}' '*.md'
.PHONY: lint-template-hbs
lint-template-hbs:
./node_modules/.bin/ember-template-lint './webapp/app/**/*.hbs' --config-path './webapp/.template-lintrc'
npx ember-template-lint './webapp/app/**/*.hbs' --config-path './webapp/.template-lintrc'
.PHONY: type-check
type-check: ## Type-check typescript files
cd webapp && npx tsc
.PHONY: test
test: ## Run the test suite
@ -118,7 +118,7 @@ format-elixir:
.PHONY: format-prettier
format-prettier:
./node_modules/.bin/prettier --write --single-quote --no-bracket-spacing '{webapp,jipt,cli}/*.{js,json}' 'webapp/{app,config}/**/*.{js,ts,json,scss}' 'jipt/src/**/*.{js,ts,json,gql}' 'cli/{examples,src}/**/*.{js,ts,json,gql}' 'README.md'
npx prettier --write --single-quote --no-bracket-spacing './{webapp,jipt,cli}/!(node_modules)/**/*.{js,ts,json,svg,scss,md}' '*.md'
# Development targets
# -------------------

318
cli/package-lock.json generated
View File

@ -4,29 +4,6 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@fimbul/bifrost": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/@fimbul/bifrost/-/bifrost-0.17.0.tgz",
"integrity": "sha512-gVTkJAOef5HtN6LPmrtt5fAUmBywwlgmObsU3FBhPoNeXPLaIl2zywXkJEtvvVLQnaFmtff3x+wIj5lHRCDE3Q==",
"dev": true,
"requires": {
"@fimbul/ymir": "^0.17.0",
"get-caller-file": "^2.0.0",
"tslib": "^1.8.1",
"tsutils": "^3.5.0"
}
},
"@fimbul/ymir": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/@fimbul/ymir/-/ymir-0.17.0.tgz",
"integrity": "sha512-xMXM9KTXRLHLVS6dnX1JhHNEkmWHcAVCQ/4+DA1KKwC/AFnGHzu/7QfQttEPgw3xplT+ILf9e3i64jrFwB3JtA==",
"dev": true,
"requires": {
"inversify": "^5.0.0",
"reflect-metadata": "^0.1.12",
"tslib": "^1.8.1"
}
},
"@mrmlnc/readdir-enhanced": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz",
@ -565,16 +542,6 @@
"fancy-test": "^1.0.1"
}
},
"@oclif/tslint": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@oclif/tslint/-/tslint-3.1.1.tgz",
"integrity": "sha512-B1ZWbgzwxDhNZLzVnn+JjyFf9u+J9wNwsz/ZX9YvA9edRYcdiJz9JikCttGPi35V0NU0TUV4UqTqo/q/wQ06jQ==",
"dev": true,
"requires": {
"tslint-eslint-rules": "^5.4.0",
"tslint-xo": "^0.9.0"
}
},
"@types/chai": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.2.tgz",
@ -673,15 +640,6 @@
"resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz",
"integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk="
},
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"requires": {
"sprintf-js": "~1.0.2"
}
},
"arr-diff": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
@ -750,65 +708,6 @@
"integrity": "sha1-GcenYEc3dEaPILLS0DNyrX1Mv10=",
"dev": true
},
"babel-code-frame": {
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
"integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=",
"dev": true,
"requires": {
"chalk": "^1.1.3",
"esutils": "^2.0.2",
"js-tokens": "^3.0.2"
},
"dependencies": {
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"dev": true
},
"ansi-styles": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
"dev": true
},
"chalk": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"dev": true,
"requires": {
"ansi-styles": "^2.2.1",
"escape-string-regexp": "^1.0.2",
"has-ansi": "^2.0.0",
"strip-ansi": "^3.0.0",
"supports-color": "^2.0.0"
}
},
"esutils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
"integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
"dev": true
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true,
"requires": {
"ansi-regex": "^2.0.0"
}
},
"supports-color": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
"dev": true
}
}
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
@ -1364,16 +1263,6 @@
"path-type": "^3.0.0"
}
},
"doctrine": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-0.7.2.tgz",
"integrity": "sha1-fLhgNZujvpDgQLJrcpzkv6ZUxSM=",
"dev": true,
"requires": {
"esutils": "^1.1.6",
"isarray": "0.0.1"
}
},
"end-of-stream": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
@ -1402,12 +1291,6 @@
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz",
"integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw=="
},
"esutils": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz",
"integrity": "sha1-wBzKqa5LiXxtDD4hCuUvPHqEQ3U=",
"dev": true
},
"execa": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz",
@ -1693,12 +1576,6 @@
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"get-caller-file": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.1.tgz",
"integrity": "sha512-SpOZHfz845AH0wJYVuZk2jWDqFmu7Xubsx+ldIpwzy5pDUpu7OJHK7QYNSA2NPlDSKQwM1GFaAkciOWjjW92Sg==",
"dev": true
},
"get-func-name": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
@ -1783,23 +1660,6 @@
"integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==",
"dev": true
},
"has-ansi": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
"integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
"dev": true,
"requires": {
"ansi-regex": "^2.0.0"
},
"dependencies": {
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"dev": true
}
}
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
@ -1898,12 +1758,6 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"inversify": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/inversify/-/inversify-5.0.1.tgz",
"integrity": "sha512-Ieh06s48WnEYGcqHepdsJUIJUXpwH5o5vodAX+DK2JA/gjy4EbEcQZxw+uFfzysmKjiLXGYwNG3qDZsKVMcINQ==",
"dev": true
},
"is-accessor-descriptor": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
@ -2055,12 +1909,6 @@
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
"integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0="
},
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
"dev": true
},
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -2072,22 +1920,6 @@
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
"dev": true
},
"js-tokens": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=",
"dev": true
},
"js-yaml": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz",
"integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==",
"dev": true,
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
}
},
"json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
@ -2553,12 +2385,6 @@
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A="
},
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
},
"path-type": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
@ -2676,12 +2502,6 @@
"esprima": "~4.0.0"
}
},
"reflect-metadata": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
"integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==",
"dev": true
},
"regex-not": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
@ -2704,15 +2524,6 @@
"integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
"dev": true
},
"resolve": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz",
"integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==",
"dev": true,
"requires": {
"path-parse": "^1.0.5"
}
},
"resolve-url": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
@ -3030,12 +2841,6 @@
"extend-shallow": "^3.0.0"
}
},
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
"dev": true
},
"static-extend": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
@ -3303,129 +3108,6 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
"integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ=="
},
"tslint": {
"version": "5.11.0",
"resolved": "https://registry.npmjs.org/tslint/-/tslint-5.11.0.tgz",
"integrity": "sha1-mPMMAurjzecAYgHkwzywi0hYHu0=",
"dev": true,
"requires": {
"babel-code-frame": "^6.22.0",
"builtin-modules": "^1.1.1",
"chalk": "^2.3.0",
"commander": "^2.12.1",
"diff": "^3.2.0",
"glob": "^7.1.1",
"js-yaml": "^3.7.0",
"minimatch": "^3.0.4",
"resolve": "^1.3.2",
"semver": "^5.3.0",
"tslib": "^1.8.0",
"tsutils": "^2.27.2"
},
"dependencies": {
"commander": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz",
"integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==",
"dev": true
},
"tsutils": {
"version": "2.29.0",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
"integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
"dev": true,
"requires": {
"tslib": "^1.8.1"
}
}
}
},
"tslint-config-prettier": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/tslint-config-prettier/-/tslint-config-prettier-1.17.0.tgz",
"integrity": "sha512-NKWNkThwqE4Snn4Cm6SZB7lV5RMDDFsBwz6fWUkTxOKGjMx8ycOHnjIbhn7dZd5XmssW3CwqUjlANR6EhP9YQw=="
},
"tslint-consistent-codestyle": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/tslint-consistent-codestyle/-/tslint-consistent-codestyle-1.15.0.tgz",
"integrity": "sha512-6BNDBbZh2K0ibRXe70Mkl9gfVttxQ3t3hqV1BRDfpIcjrUoOgD946iH4SrXp+IggDgeMs3dJORjD5tqL5j4jXg==",
"dev": true,
"requires": {
"@fimbul/bifrost": "^0.17.0",
"tslib": "^1.7.1",
"tsutils": "^2.29.0"
},
"dependencies": {
"tsutils": {
"version": "2.29.0",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
"integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
"dev": true,
"requires": {
"tslib": "^1.8.1"
}
}
}
},
"tslint-eslint-rules": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/tslint-eslint-rules/-/tslint-eslint-rules-5.4.0.tgz",
"integrity": "sha512-WlSXE+J2vY/VPgIcqQuijMQiel+UtmXS+4nvK4ZzlDiqBfXse8FAvkNnTcYhnQyOTW5KFM+uRRGXxYhFpuBc6w==",
"dev": true,
"requires": {
"doctrine": "0.7.2",
"tslib": "1.9.0",
"tsutils": "^3.0.0"
},
"dependencies": {
"tslib": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz",
"integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==",
"dev": true
}
}
},
"tslint-microsoft-contrib": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/tslint-microsoft-contrib/-/tslint-microsoft-contrib-5.2.1.tgz",
"integrity": "sha512-PDYjvpo0gN9IfMULwKk0KpVOPMhU6cNoT9VwCOLeDl/QS8v8W2yspRpFFuUS7/c5EIH/n8ApMi8TxJAz1tfFUA==",
"dev": true,
"requires": {
"tsutils": "^2.27.2 <2.29.0"
},
"dependencies": {
"tsutils": {
"version": "2.28.0",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.28.0.tgz",
"integrity": "sha512-bh5nAtW0tuhvOJnx1GLRn5ScraRLICGyJV5wJhtRWOLsxW70Kk5tZtpK3O/hW6LDnqKS9mlUMPZj9fEMJ0gxqA==",
"dev": true,
"requires": {
"tslib": "^1.8.1"
}
}
}
},
"tslint-xo": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/tslint-xo/-/tslint-xo-0.9.0.tgz",
"integrity": "sha512-Zk5jBdQVUaHEmR9TUoh1TJOjjCr7/nRplA+jDZBvucyBMx65pt0unTr6H/0HvrtSlucFvOMYsyBZE1W8b4AOig==",
"dev": true,
"requires": {
"tslint-consistent-codestyle": "^1.11.0",
"tslint-eslint-rules": "^5.3.1",
"tslint-microsoft-contrib": "^5.0.2"
}
},
"tsutils": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.7.0.tgz",
"integrity": "sha512-n+e+3q7Jx2kfZw7tjfI9axEIWBY0sFMOlC+1K70X0SeXpO/UYSB+PN+E9tIJNqViB7oiXQdqD7dNchnvoneZew==",
"dev": true,
"requires": {
"tslib": "^1.8.1"
}
},
"tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",

View File

@ -23,13 +23,11 @@
"form-data": "2.3.3",
"glob": "7.1.3",
"node-fetch": "2.3.0",
"tslib": "1.9.3",
"tslint-config-prettier": "1.17.0"
"tslib": "1.9.3"
},
"devDependencies": {
"@oclif/dev-cli": "1.21.0",
"@oclif/test": "1.0.1",
"@oclif/tslint": "3.1.1",
"@types/chai": "4.1.2",
"@types/mkdirp": "0.5.2",
"@types/mocha": "5.0.0",
@ -38,7 +36,6 @@
"globby": "8.0.1",
"mocha": "5.0.5",
"ts-node": "7.0.1",
"tslint": "5.11.0",
"typescript": "3.2.2"
},
"engines": {

View File

@ -11,7 +11,7 @@ import ProjectFetcher from './services/project-fetcher';
// Types
import {Project} from './types/project';
const sleep = (ms: number) =>
const sleep = async (ms: number) =>
new Promise((resolve: () => void) => setTimeout(resolve, ms));
export default abstract class extends Command {

View File

@ -24,8 +24,8 @@ export default class Export extends Command {
'order-by': flags.string({
default: 'index',
description: 'Will be used in the export call as the order of the keys',
options: ['index', 'key-asc']
})
options: ['index', 'key-asc'],
}),
};
async run() {
@ -43,9 +43,9 @@ export default class Export extends Command {
const targets = new DocumentPathsFetcher().fetch(this.project!, document);
await Promise.all(
targets.map(({path, language, documentPath}) => {
targets.map(async ({path, language, documentPath}) => {
const localFile = document.fetchLocalFile(documentPath, path);
if (!localFile) return new Promise(resolve => resolve());
if (!localFile) return new Promise((resolve) => resolve());
formatter.log(localFile);
return document.export(localFile, language, documentPath, flags);

View File

@ -22,8 +22,8 @@ export default class Jipt extends Command {
{
description: 'The pseudo language for in-place-translation-editing',
name: 'pseudoLanguageName',
required: true
}
required: true,
},
];
static flags = {};
@ -45,7 +45,7 @@ export default class Jipt extends Command {
);
await Promise.all(
targets.map(({path, documentPath}) => {
targets.map(async ({path, documentPath}) => {
formatter.log(path);
return document.exportJipt(path, documentPath);

View File

@ -9,9 +9,11 @@ export default class Stats extends Command {
static examples = [`$ accent stats`];
/* eslint-disable @typescript-eslint/require-await */
async run() {
const formatter = new Formatter(this.project!);
formatter.log();
}
/* eslint-enable @typescript-eslint/require-await */
}

View File

@ -32,28 +32,29 @@ export default class Sync extends Command {
static flags = {
'add-translations': flags.boolean({
description:
'Add translations in Accent to help translators if you already have translated strings'
'Add translations in Accent to help translators if you already have translated strings',
}),
'dry-run': flags.boolean({
default: false,
description: 'Do not write the file from the export _after_ the operation'
description:
'Do not write the file from the export _after_ the operation',
}),
'merge-type': flags.string({
default: 'smart',
description:
'Will be used in the add translations call as the "merge_type" param',
options: ['smart', 'passive', 'force']
options: ['smart', 'passive', 'force'],
}),
'order-by': flags.string({
default: 'index',
description: 'Will be used in the export call as the order of the keys',
options: ['index', 'key-asc']
options: ['index', 'key-asc'],
}),
'sync-type': flags.string({
default: 'smart',
description: 'Will be used in the sync call as the "sync_type" param',
options: ['smart', 'passive']
})
options: ['smart', 'passive'],
}),
};
async run() {
@ -98,9 +99,9 @@ export default class Sync extends Command {
const targets = new DocumentPathsFetcher().fetch(this.project!, document);
await Promise.all(
targets.map(({path, language, documentPath}) => {
targets.map(async ({path, language, documentPath}) => {
const localFile = document.fetchLocalFile(documentPath, path);
if (!localFile) return new Promise(resolve => resolve());
if (!localFile) return new Promise((resolve) => resolve());
formatter.log(localFile);
@ -116,7 +117,7 @@ export default class Sync extends Command {
const {flags} = this.parse(Sync);
const formatter = new CommitOperationFormatter();
return document.paths.map(async path => {
return document.paths.map(async (path) => {
const operations = await document.sync(this.project!, path, flags);
const documentPath = document.parseDocumentName(path, document.config);

View File

@ -35,7 +35,7 @@ export default class ConfigFetcher {
files(): Document[] {
return this.config.files.map(
documentConfig => new Document(documentConfig, this.config)
(documentConfig) => new Document(documentConfig, this.config)
);
}
}

View File

@ -11,7 +11,7 @@ export default class DocumentJiptPathsFetcher {
): DocumentPath[] {
return project.documents.entries
.map(({path}) => path)
.map(path => {
.map((path) => {
const parsedTarget = document.target
.replace('%slug%', pseudoLanguageName)
.replace('%original_file_name%', path);
@ -19,7 +19,7 @@ export default class DocumentJiptPathsFetcher {
return {
documentPath: path,
language: pseudoLanguageName,
path: parsedTarget
path: parsedTarget,
};
});
}

View File

@ -10,7 +10,7 @@ export default class DocumentPathsFetcher {
const documentPaths = project.documents.entries.map(({path}) => path);
return languageSlugs.reduce((memo: DocumentPath[], slug) => {
documentPaths.forEach(path => {
documentPaths.forEach((path) => {
const parsedTarget = document.target
.replace('%slug%', slug)
.replace('%original_file_name%', path)

View File

@ -17,9 +17,11 @@ import {Project} from '../types/project';
const enum OperationName {
Sync = 'sync',
AddTranslation = 'addTranslations'
AddTranslation = 'addTranslations',
}
const ERROR_THRESHOLD_STATUS_CODE = 400;
export default class Document {
paths: string[];
readonly apiKey: string;
@ -40,7 +42,7 @@ export default class Document {
}
async sync(project: Project, file: string, options: any) {
const masterLanguage = fetchFromRevision(project!.masterRevision);
const masterLanguage = fetchFromRevision(project.masterRevision);
const formData = new FormData();
formData.append('file', fs.createReadStream(file));
formData.append('document_path', this.parseDocumentName(file, this.config));
@ -56,7 +58,7 @@ export default class Document {
const response = await fetch(url, {
body: formData,
headers: this.authorizationHeader(),
method: 'POST'
method: 'POST',
});
return this.handleResponse(response, options, OperationName.Sync);
@ -84,7 +86,7 @@ export default class Document {
const response = await fetch(url, {
body: formData,
headers: this.authorizationHeader(),
method: 'POST'
method: 'POST',
});
return this.handleResponse(response, options, OperationName.AddTranslation);
@ -110,12 +112,12 @@ export default class Document {
['document_path', documentPath],
['document_format', this.config.format],
['order_by', options['order-by']],
['language', language]
['language', language],
];
const url = `${this.apiUrl}/export?${this.encodeQuery(query)}`;
const response = await fetch(url, {
headers: this.authorizationHeader()
headers: this.authorizationHeader(),
});
return this.writeResponseToFile(response, file);
@ -124,12 +126,12 @@ export default class Document {
async exportJipt(file: string, documentPath: string) {
const query = [
['document_path', documentPath],
['document_format', this.config.format]
['document_format', this.config.format],
];
const url = `${this.apiUrl}/jipt-export?${this.encodeQuery(query)}`;
const response = await fetch(url, {
headers: this.authorizationHeader()
headers: this.authorizationHeader(),
});
return this.writeResponseToFile(response, file);
@ -182,7 +184,7 @@ export default class Document {
return config;
}
private writeResponseToFile(response: Response, file: string) {
private async writeResponseToFile(response: Response, file: string) {
return new Promise((resolve, reject) => {
mkdirp.sync(path.dirname(file));
@ -199,7 +201,7 @@ export default class Document {
operationName: OperationName
): Promise<OperationResponse> {
if (!options['dry-run']) {
if (response.status >= 400) {
if (response.status >= ERROR_THRESHOLD_STATUS_CODE) {
return {[operationName]: {success: false}, peek: false};
}

View File

@ -9,7 +9,7 @@ export default class HookRunnerFomatter {
log(name: string, commands: string[]) {
const operation = capitalizeFirstLetter(decamelize(name, ' '));
console.log(chalk.yellow('➤ '), chalk.bold(chalk.yellow(`${operation}:`)));
commands.forEach(command => {
commands.forEach((command) => {
console.log(' ', chalk.yellow(command));
});
console.log('');

View File

@ -11,7 +11,7 @@ export default class ProjectAddTranslationsFormatter {
log(project: Project) {
const languages = project.revisions
.filter(
revision =>
(revision) =>
fetchFromRevision(revision) !==
fetchFromRevision(project.masterRevision)
)

View File

@ -7,7 +7,7 @@ import {Document, Project, Revision} from '../../types/project';
// Services
import {
fetchFromRevision,
fetchNameFromRevision
fetchNameFromRevision,
} from '../revision-slug-fetcher';
export default class ProjectStatsFormatter {

View File

@ -17,6 +17,7 @@ export default class HookRunner {
this.hooks = document.config.hooks;
}
/* eslint-disable @typescript-eslint/require-await */
async run(name: Hooks) {
if (!this.hooks) return null;
const hooks = this.hooks[name];
@ -29,4 +30,5 @@ export default class HookRunner {
return this.document.refreshPaths();
}
/* eslint-disable @typescript-eslint/require-await */
}

View File

@ -18,7 +18,7 @@ export default class ProjectFetcher {
return data.data && data.data.viewer.project;
}
private graphql(config: Config) {
private async graphql(config: Config) {
const query = `query ProjectDetails($project_id: ID!) {
viewer {
project(id: $project_id) {
@ -68,9 +68,9 @@ export default class ProjectFetcher {
body: JSON.stringify({query}),
headers: {
'Content-Type': 'application/json',
authorization: `Bearer ${config.apiKey}`
authorization: `Bearer ${config.apiKey}`,
},
method: 'POST'
method: 'POST',
});
}
}

View File

@ -4,13 +4,13 @@ export enum Hooks {
beforeExport = 'beforeExport',
afterExport = 'afterExport',
beforeSync = 'beforeSync',
afterSync = 'afterSync'
afterSync = 'afterSync',
}
export enum NamePattern {
file = 'file',
fileWithSlugSuffix = 'fileWithSlugSuffix',
parentDirectory = 'parentDirectory'
parentDirectory = 'parentDirectory',
}
export interface HookConfig {

View File

@ -1,4 +1,4 @@
export interface OperationResponse {
peek: any;
[x: string]: any;
peek: any;
}

View File

@ -1,3 +1,3 @@
import Accent from './src/accent';
window['accent'].q.forEach(([fun, args]) => Accent[fun](args));
window['accent'].q.forEach(([fun, args]) => Accent[fun](args)); // eslint-disable-line dot-notation

2810
jipt/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,7 @@ export interface Config {
const state = new State({
nodes: new WeakMap(),
projectTranslations: {},
refs: new Map()
refs: new Map(),
});
const liveNode = new LiveNode(state);
@ -39,7 +39,7 @@ export const Accent = {
const mutation = new Mutation(liveNode);
mutation.bindEvents();
}
},
};
export default Accent;

View File

@ -10,7 +10,7 @@ const enum ACTIONS {
redirectIfEmbedded = 'redirectIfEmbedded',
login = 'login',
loggedIn = 'loggedIn',
changeText = 'changeText'
changeText = 'changeText',
}
interface Props {

View File

@ -24,7 +24,7 @@ export default class LiveNode {
}
matchAttributes(node: Element) {
Array.from(node.attributes).forEach(attribute => {
Array.from(node.attributes).forEach((attribute) => {
const translation = this.findTranslationByValue(attribute.value);
if (!translation || !translation.text) return;
@ -34,7 +34,7 @@ export default class LiveNode {
attribute.value = newAttribute;
this.state.addReference(node, translation, {
attributeName: attribute.name
attributeName: attribute.name,
});
});
}

View File

@ -12,6 +12,12 @@ interface Translation {
window nodes on mutation and messages FROM the Accent client.
*/
export default class Mutation {
private readonly liveNode: LiveNode;
constructor(liveNode: LiveNode) {
this.liveNode = liveNode;
}
static nodeChange(node: Element, meta: any, text: string) {
this.textNodeChange(node, meta, text);
this.attributeNodeChange(node, meta, text);
@ -55,12 +61,6 @@ export default class Mutation {
}, NODE_UPDATE_STYLE_TIMEOUT);
}
private readonly liveNode: LiveNode;
constructor(liveNode: LiveNode) {
this.liveNode = liveNode;
}
bindEvents() {
const onMutation = (instance: MutationRecord[]) => {
return instance.forEach(this.handleNodeMutation.bind(this));
@ -71,7 +71,7 @@ export default class Mutation {
characterData: true,
characterDataOldValue: true,
childList: true,
subtree: true
subtree: true,
});
}

View File

@ -10,6 +10,8 @@ interface Props {
state: State;
}
const CENTER_OFFSET = 6;
/*
The Pin component serves as the entrypoint for the user. The element it creates
is responsible to sending messages to the Accent UI.
@ -34,7 +36,7 @@ export default class Pin {
}
bindEvents() {
this.element.addEventListener('click', event => {
this.element.addEventListener('click', (event) => {
const target = event.target as HTMLElement;
if (target.dataset.id) {
@ -47,7 +49,7 @@ export default class Pin {
}
});
document.addEventListener('mouseover', event => {
document.addEventListener('mouseover', (event) => {
const node = event.target as HTMLElement;
if (this.liveNode.isLive(node)) this.showFor(node);
@ -61,7 +63,9 @@ export default class Pin {
);
styles.set(
this.element,
`top: ${top + height - 6}px; left: ${left - 6}px; ${styles.pin}`
`top: ${top + height - CENTER_OFFSET}px; left: ${
left - CENTER_OFFSET
}px; ${styles.pin}`
);
const ids = keys

View File

@ -1,5 +1,5 @@
const BASE = 36;
const LENGTH = 8;
/* Random class that should not conflict with the parent window styles */
export default () =>
`acc_${Math.random()
.toString(36)
.substring(8)}`;
export default () => `acc_${Math.random().toString(BASE).substring(LENGTH)}`;

View File

@ -52,5 +52,5 @@ export default {
set,
translationNode,
translationNodeConflicted,
translationNodeUpdated
translationNodeUpdated,
};

View File

@ -144,9 +144,7 @@ export default class UI {
element.innerHTML = `
<div class="${DISABLE_CLASS}" style="${styles.frameDisableButton}">×</div>
<div class="${EXPAND_CLASS}" style="${styles.frameExpandButton}"></div>
<div class="${COLLAPSE_CLASS}" style="${
styles.frameCollapseButton
}">×</div>
<div class="${COLLAPSE_CLASS}" style="${styles.frameCollapseButton}">×</div>
`;
return element;

View File

@ -8,6 +8,10 @@ defmodule Accent.Project do
field(:last_synced_at, :utc_datetime)
field(:locked_file_operations, :boolean, default: false)
field(:translations_count, :integer, virtual: true, default: :not_loaded)
field(:reviewed_count, :integer, virtual: true, default: :not_loaded)
field(:conflicts_count, :integer, virtual: true, default: :not_loaded)
has_many(:integrations, Accent.Integration)
has_many(:revisions, Accent.Revision)
has_many(:target_revisions, Accent.Revision, where: [master: false])

View File

@ -1,4 +1,6 @@
defmodule Accent.Scopes.Project do
import Ecto.Query
@doc """
## Examples
@ -15,4 +17,35 @@ defmodule Accent.Scopes.Project do
def from_search(query, term) do
Accent.Scopes.Search.from_search(query, term, :name)
end
@doc """
Fill `translations_count`, `conflicts_count` and `reviewed_count` for projects.
"""
@spec with_stats(Ecto.Queryable.t()) :: Ecto.Queryable.t()
def with_stats(query) do
translations =
from(
t in Accent.Translation,
inner_join: revisions in assoc(t, :revision),
select: %{field_id: revisions.project_id, count: count(t)},
where: [removed: false, locked: false],
where: is_nil(t.version_id),
group_by: revisions.project_id
)
reviewed = from(translations, where: [conflicted: false])
from(
projects in query,
left_join: translations in subquery(translations),
on: translations.field_id == projects.id,
left_join: reviewed in subquery(reviewed),
on: reviewed.field_id == projects.id,
select_merge: %{
translations_count: coalesce(translations.count, 0),
reviewed_count: coalesce(reviewed.count, 0),
conflicts_count: coalesce(translations.count, 0) - coalesce(reviewed.count, 0)
}
)
end
end

View File

@ -62,7 +62,7 @@ defmodule Accent.Scopes.Revision do
end
@doc """
Fill `translations_count`, `conflicts_count` and `reviewed_count` for documents.
Fill `translations_count`, `conflicts_count` and `reviewed_count` for revisions.
"""
@spec with_stats(Ecto.Queryable.t()) :: Ecto.Queryable.t()
def with_stats(query) do

View File

@ -3,12 +3,29 @@ defmodule Accent.Scopes.TranslationsCount do
def with_stats(query, column, options \\ []) do
exclude_empty_translations = Keyword.get(options, :exclude_empty_translations, false)
translations = countable_translations_query(column)
query
|> count_translations(translations, exclude_empty_translations)
|> count_reviewed(translations)
|> merge_select()
translations =
from(
t in Accent.Translation,
select: %{field_id: field(t, ^column), count: count(t)},
where: [removed: false, locked: false],
where: is_nil(t.version_id),
group_by: field(t, ^column)
)
query =
query
|> count_translations(translations, exclude_empty_translations)
|> count_reviewed(translations)
from(
[translations: t, reviewed: r] in query,
select_merge: %{
translations_count: coalesce(t.count, 0),
reviewed_count: coalesce(r.count, 0),
conflicts_count: coalesce(t.count, 0) - coalesce(r.count, 0)
}
)
end
defp count_translations(query, translations, _exclude_empty_translations = true) do
@ -23,25 +40,4 @@ defmodule Accent.Scopes.TranslationsCount do
reviewed = from(translations, where: [conflicted: false])
from(q in query, left_join: translations in subquery(reviewed), as: :reviewed, on: translations.field_id == q.id)
end
defp merge_select(query) do
from(
[translations: t, reviewed: r] in query,
select_merge: %{
translations_count: coalesce(t.count, 0),
reviewed_count: coalesce(r.count, 0),
conflicts_count: coalesce(t.count, 0) - coalesce(r.count, 0)
}
)
end
defp countable_translations_query(column) do
from(
t in Accent.Translation,
select: %{field_id: field(t, ^column), count: count(t)},
where: [removed: false, locked: false],
where: is_nil(t.version_id),
group_by: field(t, ^column)
)
end
end

View File

@ -78,6 +78,7 @@ defmodule Accent.GraphQL.Resolvers.Project do
|> Query.where([_, c], c.user_id == ^viewer.id)
|> Query.order_by([p, _], asc: p.name)
|> ProjectScope.from_search(args[:query])
|> ProjectScope.with_stats()
|> Repo.paginate(page: args[:page])
|> Paginated.format()
|> (&{:ok, &1}).()
@ -86,6 +87,7 @@ defmodule Accent.GraphQL.Resolvers.Project do
@spec show_viewer(any(), %{id: String.t()}, GraphQLContext.t()) :: {:ok, Project.t() | nil}
def show_viewer(_, %{id: id}, _) do
Project
|> ProjectScope.with_stats()
|> Repo.get(id)
|> (&{:ok, &1}).()
end

View File

@ -16,6 +16,11 @@ defmodule Accent.GraphQL.Types.Project do
field(:main_color, :string)
field(:last_synced_at, :datetime)
field(:logo, :string)
field(:translations_count, non_null(:integer))
field(:conflicts_count, non_null(:integer))
field(:reviewed_count, non_null(:integer))
field(:last_activity, :activity, resolve: &Accent.GraphQL.Resolvers.Project.last_activity/3)
field(:is_file_operations_locked, non_null(:boolean), resolve: field_alias(:locked_file_operations))

139
mix.lock
View File

@ -1,74 +1,73 @@
%{
"absinthe": {:hex, :absinthe, "1.4.16", "0933e4d9f12652b12115d5709c0293a1bf78a22578032e9ad0dad4efee6b9eb1", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"absinthe_error_payload": {:hex, :absinthe_error_payload, "1.1.1", "48c89baaff23c8fdf93313a3e2637648456a60f010e5cde34b1ed3309feceabc", [:make, :mix], [{:absinthe, "~> 1.3", [hex: :absinthe, repo: "hexpm", optional: false]}, {:ecto, "~> 3.1", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm"},
"absinthe_plug": {:hex, :absinthe_plug, "1.4.7", "939b6b9e1c7abc6b399a5b49faa690a1fbb55b195c670aa35783b14b08ccec7a", [:mix], [{:absinthe, "~> 1.4.11", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"bamboo": {:hex, :bamboo, "1.4.0", "7b9201c49a843e4802061cf45692405b2c00efcf1cebf8b7b64f015ead072392", [:mix], [{:hackney, ">= 1.13.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"bamboo_smtp": {:hex, :bamboo_smtp, "2.1.0", "4be58f3c51d9f7875dc169ae58a1d2f08e5b718bf3895f70d130548c0598f422", [:mix], [{:bamboo, "~> 1.2", [hex: :bamboo, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 0.15.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}], "hexpm"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
"canada": {:hex, :canada, "1.0.2", "040e4c47609b0a67d5773ac1fbe5e99f840cef173d69b739beda7c98453e0770", [:mix], [], "hexpm"},
"canary": {:hex, :canary, "1.1.1", "4138d5e05db8497c477e4af73902eb9ae06e49dceaa13c2dd9f0b55525ded48b", [:mix], [{:canada, "~> 1.0.1", [hex: :canada, repo: "hexpm", optional: false]}, {:ecto, ">= 1.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
"corsica": {:hex, :corsica, "1.1.3", "5f1de40bc9285753aa03afbdd10c364dac79b2ddbf2ba9c5c9c47b397ec06f40", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
"cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm"},
"credo": {:hex, :credo, "1.3.1", "082e8d9268a489becf8e7aa75671a7b9088b1277cd6c1b13f40a55554b3f5126", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
"credo_envvar": {:hex, :credo_envvar, "0.1.4", "40817c10334e400f031012c0510bfa0d8725c19d867e4ae39cf14f2cbebc3b20", [:mix], [{:credo, "~> 1.0", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm"},
"csv": {:hex, :csv, "2.3.1", "9ce11eff5a74a07baf3787b2b19dd798724d29a9c3a492a41df39f6af686da0e", [:mix], [{:parallel_stream, "~> 1.0.4", [hex: :parallel_stream, repo: "hexpm", optional: false]}], "hexpm"},
"dataloader": {:hex, :dataloader, "1.0.7", "58351b335673cf40601429bfed6c11fece6ce7ad169b2ac0f0fe83e716587391", [:mix], [{:ecto, ">= 0.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
"db_connection": {:hex, :db_connection, "2.2.1", "caee17725495f5129cb7faebde001dc4406796f12a62b8949f4ac69315080566", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"},
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm"},
"dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"},
"ecto": {:hex, :ecto, "3.4.0", "a7a83ab8359bf816ce729e5e65981ce25b9fc5adfc89c2ea3980f4fed0bfd7c1", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
"ecto_sql": {:hex, :ecto_sql, "3.4.1", "3c9136ba138f9b74d31286c73c61232a92bd19385f7c5607bdeb3a4587ef91f5", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm"},
"erlsom": {:hex, :erlsom, "1.5.0", "c5a5cdd0ee0e8dca62bcc4b13ff08da24fdefc16ccd8b25282a2fda2ba1be24a", [:rebar3], [], "hexpm"},
"ex_brace_expansion": {:hex, :ex_brace_expansion, "0.0.2", "7574fd9497f3f045346dfd9517f10f237f4d39137bf42142b0fbdcd4bacbc6ed", [:mix], [], "hexpm"},
"ex_minimatch": {:hex, :ex_minimatch, "0.0.1", "4b41726183c104ac227c5996f083ec370f97bd38c2232d74a847888c1bb715bc", [:mix], [{:ex_brace_expansion, "~> 0.0.1", [hex: :ex_brace_expansion, repo: "hexpm", optional: false]}], "hexpm"},
"excoveralls": {:hex, :excoveralls, "0.12.3", "2142be7cb978a3ae78385487edda6d1aff0e482ffc6123877bb7270a8ffbcfe0", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
"absinthe": {:hex, :absinthe, "1.4.16", "0933e4d9f12652b12115d5709c0293a1bf78a22578032e9ad0dad4efee6b9eb1", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "076b8bd9552f4966ba1242f412f6c439b731169a36a0ddaaffcd3893828f5bf6"},
"absinthe_error_payload": {:hex, :absinthe_error_payload, "1.0.1", "0696314517f2d42a4e942cdeaf0c42a05db4135c6e9257607b4b13ebc83f894f", [:make, :mix], [{:absinthe, "~> 1.3", [hex: :absinthe, repo: "hexpm", optional: false]}, {:ecto, "~> 3.1", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "03b300138a2fd4b0c012401ce4e1d038f89d22f3aa49f8066c959208902469b7"},
"absinthe_plug": {:hex, :absinthe_plug, "1.4.7", "939b6b9e1c7abc6b399a5b49faa690a1fbb55b195c670aa35783b14b08ccec7a", [:mix], [{:absinthe, "~> 1.4.11", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c6ecb0e56a963287ac252d0563e5b33b84b300ce8203d3d1410dddb5dc6d08e9"},
"bamboo": {:hex, :bamboo, "1.3.0", "9ab7c054f1c3435464efcba939396c29c5e1b28f73c34e1f169e0881297a3141", [:mix], [{:hackney, ">= 1.13.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "e1197188512d4a4458eaaad9a1659ce9eeb54a1b41574a9cd7507217b33e0f3e"},
"bamboo_smtp": {:hex, :bamboo_smtp, "2.1.0", "4be58f3c51d9f7875dc169ae58a1d2f08e5b718bf3895f70d130548c0598f422", [:mix], [{:bamboo, "~> 1.2", [hex: :bamboo, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 0.15.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}], "hexpm", "0aad00ef93d0e0c83a0e1ca6998fea070c8a720a990fbda13ce834136215ee49"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"canada": {:hex, :canada, "1.0.2", "040e4c47609b0a67d5773ac1fbe5e99f840cef173d69b739beda7c98453e0770", [:mix], [], "hexpm", "4269f74153fe89583fe50bd4d5de57bfe01f31258a6b676d296f3681f1483c68"},
"canary": {:hex, :canary, "1.1.1", "4138d5e05db8497c477e4af73902eb9ae06e49dceaa13c2dd9f0b55525ded48b", [:mix], [{:canada, "~> 1.0.1", [hex: :canada, repo: "hexpm", optional: false]}, {:ecto, ">= 1.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f348d9848693c830a65b707bba9e4dfdd6434e8c356a8d4477e4535afb0d653b"},
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
"corsica": {:hex, :corsica, "1.1.2", "5ad8b9dcbeeda4762d78a57c0c8c2f88e1eef8741508517c98cb79e0db1f107d", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "b7ae4485dc782266cab8d72fc7bb528f2393a4accf3a944834f86978c85fd8fa"},
"cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"},
"cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm", "79f954a7021b302186a950a32869dbc185523d99d3e44ce430cd1f3289f41ed4"},
"credo": {:hex, :credo, "1.1.5", "caec7a3cadd2e58609d7ee25b3931b129e739e070539ad1a0cd7efeeb47014f4", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d0bbd3222607ccaaac5c0340f7f525c627ae4d7aee6c8c8c108922620c5b6446"},
"credo_envvar": {:hex, :credo_envvar, "0.1.4", "40817c10334e400f031012c0510bfa0d8725c19d867e4ae39cf14f2cbebc3b20", [:mix], [{:credo, "~> 1.0", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "5055cdb4bcbaf7d423bc2bb3ac62b4e2d825e2b1e816884c468dee59d0363009"},
"csv": {:hex, :csv, "2.3.1", "9ce11eff5a74a07baf3787b2b19dd798724d29a9c3a492a41df39f6af686da0e", [:mix], [{:parallel_stream, "~> 1.0.4", [hex: :parallel_stream, repo: "hexpm", optional: false]}], "hexpm", "86626e1c89a4ad9a96d0d9c638f9e88c2346b89b4ba1611988594ebe72b5d5ee"},
"dataloader": {:hex, :dataloader, "1.0.6", "fb724d6d3fb6acb87d27e3b32dea3a307936ad2d245faf9cf5221d1323d6a4ba", [:mix], [{:ecto, ">= 0.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "d16b82bc004038243236c73b76bef39be634828a4f4f4f9d637fb498ff886a10"},
"db_connection": {:hex, :db_connection, "2.1.1", "a51e8a2ee54ef2ae6ec41a668c85787ed40cb8944928c191280fe34c15b76ae5", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "5a0e8c1c722dbcd31c0cbd1906b1d1074c863d335c295e4b994849b65a1fbe47"},
"decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm", "52694ef56e60108e5012f8af9673874c66ed58ac1c4fae9b5b7ded31786663f5"},
"dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm", "6c32a70ed5d452c6650916555b1f96c79af5fc4bf286997f8b15f213de786f73"},
"ecto": {:hex, :ecto, "3.2.5", "76c864b77948a479e18e69cc1d0f0f4ee7cced1148ffe6a093ff91eba644f0b5", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "01251d9b28081b7e0af02a1875f9b809b057f064754ca3b274949d5216ea6f5f"},
"ecto_sql": {:hex, :ecto_sql, "3.2.2", "d10845bc147b9f61ef485cbf0973c0a337237199bd9bd30dd9542db00aadc26b", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.2.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.2.0 or ~> 0.3.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4a68f58cd12f3df73ea37c253f9b957c81313e532e60191890f71066840c714e"},
"erlsom": {:hex, :erlsom, "1.5.0", "c5a5cdd0ee0e8dca62bcc4b13ff08da24fdefc16ccd8b25282a2fda2ba1be24a", [:rebar3], [], "hexpm", "55a9dbf9cfa77fcfc108bd8e2c4f9f784dea228a8f4b06ea10b684944946955a"},
"ex_brace_expansion": {:hex, :ex_brace_expansion, "0.0.2", "7574fd9497f3f045346dfd9517f10f237f4d39137bf42142b0fbdcd4bacbc6ed", [:mix], [], "hexpm", "d7470a00cffe4425f89e83d7288c24b641c3f6cbde136a08089e7420467cd237"},
"ex_minimatch": {:hex, :ex_minimatch, "0.0.1", "4b41726183c104ac227c5996f083ec370f97bd38c2232d74a847888c1bb715bc", [:mix], [{:ex_brace_expansion, "~> 0.0.1", [hex: :ex_brace_expansion, repo: "hexpm", optional: false]}], "hexpm", "3255bb8496635d3ef5d86ec6829958a3573ff730ca01534b0fead9c2e3af7de4"},
"excoveralls": {:hex, :excoveralls, "0.12.1", "a553c59f6850d0aff3770e4729515762ba7c8e41eedde03208182a8dc9d0ce07", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "5c1f717066a299b1b732249e736c5da96bb4120d1e55dc2e6f442d251e18a812"},
"fast_yaml": {:git, "https://github.com/processone/fast_yaml.git", "e789f68895f71b7ad31057177810ca0161bf790e", [ref: "e789f68895f71b7ad31057177810ca0161bf790e"]},
"file_system": {:hex, :file_system, "0.2.8", "f632bd287927a1eed2b718f22af727c5aeaccc9a98d8c2bd7bff709e851dc986", [:mix], [], "hexpm"},
"gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm"},
"gen_stage": {:hex, :gen_stage, "0.14.3", "d0c66f1c87faa301c1a85a809a3ee9097a4264b2edf7644bf5c123237ef732bf", [:mix], [], "hexpm"},
"gettext": {:hex, :gettext, "0.17.4", "f13088e1ec10ce01665cf25f5ff779e7df3f2dc71b37084976cf89d1aa124d5c", [:mix], [], "hexpm"},
"hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"httpoison": {:hex, :httpoison, "1.6.2", "ace7c8d3a361cebccbed19c283c349b3d26991eff73a1eaaa8abae2e3c8089b6", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
"jason": {:hex, :jason, "1.2.0", "10043418c42d2493d0ee212d3fddd25d7ffe484380afad769a0a38795938e448", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"jsone": {:hex, :jsone, "1.5.2", "87adea283c9cf24767b4deed44602989a5331156df5d60a2660e9c9114d54046", [:rebar3], [], "hexpm"},
"meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"},
"mochiweb": {:hex, :mochiweb, "2.20.1", "e4dbd0ed716f076366ecf62ada5755a844e1d95c781e8c77df1d4114be868cdf", [:rebar3], [], "hexpm"},
"mock": {:hex, :mock, "0.3.4", "c5862eb3b8c64237f45f586cf00c9d892ba07bb48305a43319d428ce3c2897dd", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
"mox": {:hex, :mox, "0.5.2", "55a0a5ba9ccc671518d068c8dddd20eeb436909ea79d1799e2209df7eaa98b6c", [:mix], [], "hexpm"},
"oauth2": {:hex, :oauth2, "2.0.0", "338382079fe16c514420fa218b0903f8ad2d4bfc0ad0c9f988867dfa246731b0", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"p1_utils": {:hex, :p1_utils, "1.0.15", "731f76ae1f31f4554afb2ae629cb5589d53bd13efc72b11f5a7c3b1242f91046", [:rebar3], [], "hexpm"},
"parallel_stream": {:hex, :parallel_stream, "1.0.6", "b967be2b23f0f6787fab7ed681b4c45a215a81481fb62b01a5b750fa8f30f76c", [:mix], [], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
"phoenix": {:hex, :phoenix, "1.4.16", "2cbbe0c81e6601567c44cc380c33aa42a1372ac1426e3de3d93ac448a7ec4308", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_html": {:hex, :phoenix_html, "2.14.1", "7dabafadedb552db142aacbd1f11de1c0bbaa247f90c449ca549d5e30bbc66b4", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.1", "274a4b07c4adbdd7785d45a8b0bb57634d0b4f45b18d2c508b26c0344bd59b8f", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm"},
"php_assoc_map": {:hex, :php_assoc_map, "0.5.2", "8b55283c2ffa762f8703cb30ef40085bf5c06ee08d5a82e38405fa4b949b2a6b", [:mix], [], "hexpm"},
"plug": {:hex, :plug, "1.10.0", "6508295cbeb4c654860845fb95260737e4a8838d34d115ad76cd487584e2fc4d", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"},
"plug_assign": {:hex, :plug_assign, "1.0.2", "da31479ac1e048af7c0630fe4b2a6d993b12cdd148405639fc1ad20c38eaf31b", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"plug_cowboy": {:hex, :plug_cowboy, "2.1.2", "8b0addb5908c5238fac38e442e81b6fcd32788eaa03246b4d55d147c47c5805e", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm"},
"postgrex": {:hex, :postgrex, "0.15.3", "5806baa8a19a68c4d07c7a624ccdb9b57e89cbc573f1b98099e3741214746ae4", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"},
"scrivener": {:hex, :scrivener, "2.7.0", "fa94cdea21fad0649921d8066b1833d18d296217bfdf4a5389a2f45ee857b773", [:mix], [], "hexpm"},
"scrivener_ecto": {:hex, :scrivener_ecto, "2.3.0", "057f9dd3c77315f0a470263c3565353860d0294404aed611b3524c6df9044189", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm"},
"sentry": {:hex, :sentry, "7.2.4", "b5bc90b594d40c2e653581e797a5fd2fdf994f2568f6bd66b7fa4971598be8d5", [:mix], [{:hackney, "~> 1.8 or 1.6.5", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm"},
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm"},
"ueberauth": {:hex, :ueberauth, "0.6.3", "d42ace28b870e8072cf30e32e385579c57b9cc96ec74fa1f30f30da9c14f3cc0", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"ueberauth_discord": {:hex, :ueberauth_discord, "0.4.0", "737951cbdeaadc882de33b4058e92afaa3d024c7d1858c3ee0c1826c52401aed", [:mix], [{:oauth2, "~> 0.8", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.4", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm"},
"ueberauth_github": {:hex, :ueberauth_github, "0.8.0", "2216c8cdacee0de6245b422fb397921b64a29416526985304e345dab6a799d17", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6.0", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm"},
"ueberauth_google": {:hex, :ueberauth_google, "0.9.0", "e098e1d6df647696b858b0289eae7e4dc8c662abee9e309d64bc115192c51bf5", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6.0", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm"},
"file_system": {:hex, :file_system, "0.2.7", "e6f7f155970975789f26e77b8b8d8ab084c59844d8ecfaf58cbda31c494d14aa", [:mix], [], "hexpm", "b4cfa2d69c7f0b18fd06db222b2398abeef743a72504e6bd7df9c52f171b047f"},
"gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"},
"gen_stage": {:hex, :gen_stage, "0.14.3", "d0c66f1c87faa301c1a85a809a3ee9097a4264b2edf7644bf5c123237ef732bf", [:mix], [], "hexpm", "8453e2289d94c3199396eb517d65d6715ef26bcae0ee83eb5ff7a84445458d76"},
"gettext": {:hex, :gettext, "0.17.1", "8baab33482df4907b3eae22f719da492cee3981a26e649b9c2be1c0192616962", [:mix], [], "hexpm", "f7d97341e536f95b96eef2988d6d4230f7262cf239cda0e2e63123ee0b717222"},
"hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"},
"httpoison": {:hex, :httpoison, "1.6.2", "ace7c8d3a361cebccbed19c283c349b3d26991eff73a1eaaa8abae2e3c8089b6", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "aa2c74bd271af34239a3948779612f87df2422c2fdcfdbcec28d9c105f0773fe"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fdf843bca858203ae1de16da2ee206f53416bbda5dc8c9e78f43243de4bc3afe"},
"jsone": {:hex, :jsone, "1.5.2", "87adea283c9cf24767b4deed44602989a5331156df5d60a2660e9c9114d54046", [:rebar3], [], "hexpm", "170c171ce7f6dd70c858065154a3305b8564833c6dcca17e10b676ca31ea976f"},
"meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm", "d34f013c156db51ad57cc556891b9720e6a1c1df5fe2e15af999c84d6cebeb1a"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm", "b93e2b1e564bdbadfecc297277f9e6d0902da645b417d6c9210f6038ac63489a"},
"mock": {:hex, :mock, "0.3.3", "42a433794b1291a9cf1525c6d26b38e039e0d3a360732b5e467bfc77ef26c914", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "a280d1f7b6f4bbcbd9282616e57502721781c66ee5b540720efabeaf627cc7eb"},
"mox": {:hex, :mox, "0.5.1", "f86bb36026aac1e6f924a4b6d024b05e9adbed5c63e8daa069bd66fb3292165b", [:mix], [], "hexpm", "052346cf322311c49a0f22789f3698eea030eec09b8c47367f0686ef2634ae14"},
"oauth2": {:hex, :oauth2, "2.0.0", "338382079fe16c514420fa218b0903f8ad2d4bfc0ad0c9f988867dfa246731b0", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "881b8364ac7385f9fddc7949379cbe3f7081da37233a1aa7aab844670a91e7e7"},
"p1_utils": {:hex, :p1_utils, "1.0.15", "731f76ae1f31f4554afb2ae629cb5589d53bd13efc72b11f5a7c3b1242f91046", [:rebar3], [], "hexpm", "1d308c3f37d7f770fb39abe3b86701b82d54414bc2499d9499edde3cb50bcf19"},
"parallel_stream": {:hex, :parallel_stream, "1.0.6", "b967be2b23f0f6787fab7ed681b4c45a215a81481fb62b01a5b750fa8f30f76c", [:mix], [], "hexpm", "639b2e8749e11b87b9eb42f2ad325d161c170b39b288ac8d04c4f31f8f0823eb"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"phoenix": {:hex, :phoenix, "1.4.11", "d112c862f6959f98e6e915c3b76c7a87ca3efd075850c8daa7c3c7a609014b0d", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ef19d737ca23b66f7333eaa873cbfc5e6fa6427ef5a0ffd358de1ba8e1a4b2f4"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"},
"phoenix_html": {:hex, :phoenix_html, "2.13.3", "850e292ff6e204257f5f9c4c54a8cb1f6fbc16ed53d360c2b780a3d0ba333867", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "8b01b3d6d39731ab18aa548d928b5796166d2500755f553725cfe967bafba7d9"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.1", "274a4b07c4adbdd7785d45a8b0bb57634d0b4f45b18d2c508b26c0344bd59b8f", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "41b4103a2fa282cfd747d377233baf213c648fdcc7928f432937676532490eee"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm", "1f13f9f0f3e769a667a6b6828d29dec37497a082d195cc52dbef401a9b69bf38"},
"php_assoc_map": {:hex, :php_assoc_map, "0.5.2", "8b55283c2ffa762f8703cb30ef40085bf5c06ee08d5a82e38405fa4b949b2a6b", [:mix], [], "hexpm", "c95f27f74075cdd5908e4217db96887709334a9fe1da30fc98706c225f3ceafd"},
"plug": {:hex, :plug, "1.8.3", "12d5f9796dc72e8ac9614e94bda5e51c4c028d0d428e9297650d09e15a684478", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "164baaeb382d19beee0ec484492aa82a9c8685770aee33b24ec727a0971b34d0"},
"plug_assign": {:hex, :plug_assign, "1.0.2", "da31479ac1e048af7c0630fe4b2a6d993b12cdd148405639fc1ad20c38eaf31b", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "341944464b45d44964b2946a0dad8226788481a49c1a95f60e236536b40979ce"},
"plug_cowboy": {:hex, :plug_cowboy, "2.1.0", "b75768153c3a8a9e8039d4b25bb9b14efbc58e9c4a6e6a270abff1cd30cbe320", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "6cd8ddd1bd1fbfa54d3fc61d4719c2057dae67615395d58d40437a919a46f132"},
"plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm", "73c1682f0e414cfb5d9b95c8e8cd6ffcfdae699e3b05e1db744e58b7be857759"},
"postgrex": {:hex, :postgrex, "0.15.1", "23ce3417de70f4c0e9e7419ad85bdabcc6860a6925fe2c6f3b1b5b1e8e47bf2f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "12cd418e207b8ed787dfe0f520fccd6c001f58d9108233feae7df36462593d1f"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
"scrivener": {:hex, :scrivener, "2.7.0", "fa94cdea21fad0649921d8066b1833d18d296217bfdf4a5389a2f45ee857b773", [:mix], [], "hexpm", "30da36a427f2519cf75993271fb7c5aad1759682a70f90d880a85c3d743d2c57"},
"scrivener_ecto": {:hex, :scrivener_ecto, "2.2.0", "53d5f1ba28f35f17891cf526ee102f8f225b7024d1cdaf8984875467158c9c5e", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm", "3eadfc0a762db4ba8acceee3450404f6ce5e710e52ccf04aae69fca5afe0cd2f"},
"sentry": {:hex, :sentry, "7.2.0", "37a367ae58b112cc548e17aa8640e5e329eb1d19b71bc368fcb7ccad919d5dac", [:mix], [{:hackney, "~> 1.8 or 1.6.5", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "0bea1c16bc9d26173847e43535750d0da90a718b27b2781d648980c53ed20baf"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"},
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
"ueberauth": {:hex, :ueberauth, "0.6.2", "25a31111249d60bad8b65438b2306a4dc91f3208faa62f5a8c33e8713989b2e8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "db9fbfb5ac707bc4f85a297758406340bf0358b4af737a88113c1a9eee120ac7"},
"ueberauth_discord": {:hex, :ueberauth_discord, "0.4.0", "737951cbdeaadc882de33b4058e92afaa3d024c7d1858c3ee0c1826c52401aed", [:mix], [{:oauth2, "~> 0.8", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.4", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "ff2e030ec61551e372cda422cbf694201e88a55d488ded57c66cbbf2d1fe258f"},
"ueberauth_github": {:hex, :ueberauth_github, "0.8.0", "2216c8cdacee0de6245b422fb397921b64a29416526985304e345dab6a799d17", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6.0", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "b65ccc001a7b0719ba069452f3333d68891f4613ae787a340cce31e2a43307a3"},
"ueberauth_google": {:hex, :ueberauth_google, "0.9.0", "e098e1d6df647696b858b0289eae7e4dc8c662abee9e309d64bc115192c51bf5", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6.0", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "5453ba074df7ee14fb5b121bb04a64cda5266cd23b28af8a2fdf02dd40959ab4"},
"ueberauth_slack": {:git, "https://github.com/ueberauth/ueberauth_slack.git", "525594c870f959aba67acc759d5c1a588ee75e9e", [ref: "525594c870f959ab"]},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"},
"xml_builder": {:hex, :xml_builder, "2.1.2", "90cb9ad382958934c78c6ddfbe6d385a8ce147d84b61cbfa83ec93a169d0feab", [:mix], [], "hexpm"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"},
"xml_builder": {:hex, :xml_builder, "2.1.2", "90cb9ad382958934c78c6ddfbe6d385a8ce147d84b61cbfa83ec93a169d0feab", [:mix], [], "hexpm", "b89046041da2fbc1d51d31493ba31b9d5fc6223c93384bf513a1a9e1df9ec081"},
}

1288
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,15 +8,19 @@
"node": ">= 8.5.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "2.25.0",
"@typescript-eslint/parser": "2.16.0",
"babel-eslint": "8.2.3",
"ember-template-lint": "1.8.2",
"eslint": "4.19.0",
"eslint-config-prettier": "2.9.0",
"eslint-plugin-ember": "5.1.0",
"eslint": "6.8.0",
"eslint-config-prettier": "6.9.0",
"eslint-config-typestrict": "1.0.0",
"eslint-plugin-ember": "7.7.2",
"eslint-plugin-mirego": "0.0.1",
"prettier": "1.16.4",
"tslint": "5.12.1",
"tslint-config-prettier": "1.17.0",
"typescript": "3.2.2"
}
"eslint-plugin-node": "11.0.0",
"eslint-plugin-sonarjs": "0.5.0",
"prettier": "2.0.2",
"typescript": "3.8.3"
},
"dependencies": {}
}

View File

@ -43,8 +43,11 @@ run make lint-prettier
header "Eslint code lint…"
run make lint-eslint
header "Tslint code lint…"
run make lint-tslint
header "Handlebar template code lint…"
run make lint-template-hbs
header "Type check Typescript files…"
run make type-check
if [ ${error_status} -ne 0 ]; then
echo "\n\n${YELLOW}▶▶ One of the checks ${RED_BOLD}failed${YELLOW}. Please fix it before committing.${NO_COLOR}"

View File

@ -7,6 +7,7 @@ defmodule AccentTest.GraphQL.Requests.Projects do
Project,
Repo,
Revision,
Translation,
User
}
@ -18,12 +19,16 @@ defmodule AccentTest.GraphQL.Requests.Projects do
project = %Project{main_color: "#f00", name: "My project", last_synced_at: DateTime.from_naive!(~N[2017-01-01T00:00:00], "Etc/UTC")} |> Repo.insert!()
%Collaborator{project_id: project.id, user_id: user.id, role: "admin"} |> Repo.insert!()
%Revision{language_id: french_language.id, project_id: project.id, master: true} |> Repo.insert!()
revision = %Revision{language_id: french_language.id, project_id: project.id, master: true} |> Repo.insert!()
{:ok, [user: user, project: project, language: french_language]}
{:ok, [user: user, project: project, language: french_language, revision: revision]}
end
test "list projects", %{user: user, project: project} do
test "list projects", %{user: user, project: project, revision: revision} do
%Translation{revision_id: revision.id, key: "A", conflicted: true} |> Repo.insert!()
%Translation{revision_id: revision.id, key: "B", conflicted: true} |> Repo.insert!()
%Translation{revision_id: revision.id, key: "C", conflicted: false} |> Repo.insert!()
{:ok, data} =
"""
query {
@ -33,6 +38,9 @@ defmodule AccentTest.GraphQL.Requests.Projects do
id
name
lastSyncedAt
translationsCount
conflictsCount
reviewedCount
}
}
}
@ -43,5 +51,8 @@ defmodule AccentTest.GraphQL.Requests.Projects do
assert get_in(data, [:data, "viewer", "projects", "entries", Access.at(0), "id"]) === project.id
assert get_in(data, [:data, "viewer", "projects", "entries", Access.at(0), "name"]) === project.name
assert get_in(data, [:data, "viewer", "projects", "entries", Access.at(0), "lastSyncedAt"]) === "2017-01-01T00:00:00Z"
assert get_in(data, [:data, "viewer", "projects", "entries", Access.at(0), "translationsCount"]) === 3
assert get_in(data, [:data, "viewer", "projects", "entries", Access.at(0), "reviewedCount"]) === 1
assert get_in(data, [:data, "viewer", "projects", "entries", Access.at(0), "conflictsCount"]) === 2
end
end

View File

@ -1,77 +0,0 @@
{
"extends": ["tslint:latest", "tslint-config-prettier"],
"rules": {
"adjacent-overload-signatures": true,
"arrow-return-shorthand": true,
"ban-comma-operator": true,
"class-name": true,
"comment-format": [true, "check-space"],
"curly": [true, "ignore-same-line"],
"encoding": true,
"interface-name": false,
"max-classes-per-file": false,
"member-access": [true, "no-public"],
"newline-before-return": true,
"no-arg": true,
"no-conditional-assignment": true,
"no-console": false,
"no-consecutive-blank-lines": true,
"no-debugger": true,
"no-duplicate-imports": true,
"no-duplicate-super": true,
"no-duplicate-switch-case": true,
"no-duplicate-variable": [true, "check-parameters"],
"no-empty-interface": false,
"no-empty": false,
"no-eval": true,
"no-implicit-dependencies": false,
"no-non-null-assertion": false,
"no-parameter-reassignment": true,
"no-return-await": true,
"no-shadowed-variable": false,
"no-string-literal": true,
"no-string-throw": true,
"no-submodule-imports": false,
"no-switch-case-fall-through": true,
"no-this-assignment": [true, {"allow-destructuring": true}],
"no-unsafe-finally": true,
"no-unused-expression": [true, "allow-fast-null-checks"],
"no-var-keyword": true,
"number-literal-format": true,
"object-literal-key-quotes": [true, "as-needed"],
"one-variable-per-declaration": [true, "ignore-for-loop"],
"prefer-conditional-expression": true,
"prefer-const": true,
"prefer-for-of": true,
"prefer-object-spread": true,
"prefer-template": true,
"radix": true,
"triple-equals": [true, "allow-undefined-check", "allow-null-check"],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
},
{
"call-signature": "onespace",
"index-signature": "onespace",
"parameter": "onespace",
"property-declaration": "onespace",
"variable-declaration": "onespace"
}
],
"unified-signatures": true,
"use-isnan": true,
"variable-name": [
true,
"ban-keywords",
"check-format",
"allow-leading-underscore",
"allow-pascal-case"
]
}
}

View File

@ -11,6 +11,9 @@ module.exports = {
'link-rel-noopener': true,
'no-abstract-roles': true,
'no-bare-strings': true,
'no-curly-component-invocation': {
allow: ['inline-svg', 't', 'string-diff', 'time-ago-in-words'],
},
'no-debugger': true,
'no-element-event-actions': true,
'no-duplicate-attributes': true,
@ -26,10 +29,11 @@ module.exports = {
'no-triple-curlies': false,
'no-unused-block-params': true,
quotes: 'double',
'require-valid-alt-text': false,
'self-closing-void-elements': true,
'simple-unless': false,
'style-concatenation': true,
'table-groups': true,
'template-length': [true, {min: 1, max: 200}]
}
'template-length': [true, {min: 1, max: 200}],
},
};

View File

@ -4,11 +4,12 @@ import loadInitializers from 'ember-load-initializers';
import config from './config/environment';
const {modulePrefix, podModulePrefix} = config;
const App = Application.extend({
modulePrefix,
podModulePrefix,
Resolver
});
class App extends Application {
modulePrefix = modulePrefix;
podModulePrefix = podModulePrefix;
Resolver = Resolver;
}
loadInitializers(App, modulePrefix);

View File

@ -1,4 +1,4 @@
export default (count, total) => {
export default (count: number, total: number) => {
const percentage = (count / total) * 100;
if (percentage) {

View File

@ -1,9 +0,0 @@
import {computed} from '@ember/object';
export default (errorsKey, property) => {
return computed(errorsKey, function() {
const errors = this.get(errorsKey);
return errors && errors.find(({field}) => field === property);
});
};

View File

@ -0,0 +1,6 @@
interface Error {
field: String;
}
export default (errors: [Error], property: String): Error | undefined =>
errors && errors.find(({field}) => field === property);

View File

@ -1,17 +0,0 @@
import EmberObject, {computed} from '@ember/object';
export default property => {
return computed(property, function() {
const key = this.get(property);
if (!key) return EmberObject.create({value: '', prefix: ''});
const splittedKey = key.split('|');
const isSplitted = !!splittedKey[1];
return EmberObject.create({
value: isSplitted ? splittedKey[1] : key,
prefix: isSplitted ? splittedKey[0] : ''
});
});
};

View File

@ -0,0 +1,16 @@
interface ParsedKey {
value: String;
prefix: String;
}
export default (key: String): ParsedKey => {
if (!key) return {value: '', prefix: ''};
const splittedKey = key.split('|');
const isSplitted = !!splittedKey[1];
return {
value: isSplitted ? splittedKey[1] : key,
prefix: isSplitted ? splittedKey[0] : '',
};
};

70
webapp/app/config/environment.d.ts vendored Normal file
View File

@ -0,0 +1,70 @@
/**
* Type declarations for
* import config from './config/environment'
*
* For now these need to be managed by the developer
* since different ember addons can materialize new entries.
*/
declare const config: {
environment: any;
modulePrefix: string;
podModulePrefix: string;
locationType: string;
rootURL: string;
EmberENV: {
EXTEND_PROTOTYPES: boolean;
LOG_VERSION: boolean;
};
APP: {
LOCAL_STORAGE: {
SESSION_NAMESPACE: string;
};
};
API: {
WS_HOST: string;
HOST: string;
AUTHENTICATION_PATH: string;
HOOKS_PATH: string;
PROJECT_PATH: string;
SYNC_PEEK_PROJECT_PATH: string;
SYNC_PROJECT_PATH: string;
MERGE_PEEK_PROJECT_PATH: string;
MERGE_REVISION_PATH: string;
EXPORT_DOCUMENT: string;
JIPT_EXPORT_DOCUMENT: string;
PERCENTAGE_REVIEWED_BADGE_SVG_PROJECT_PATH: string;
JIPT_SCRIPT_PATH: string;
};
SENTRY: {
DSN: string;
};
contentSecurityPolicy: {
'default-src': string | string[];
'script-src': string | string[];
'font-src': string | string[];
'connect-src': string | string[];
'img-src': string | string[];
'style-src': string | string[];
'media-src': string | string[];
'frame-src': string | string[];
};
flashMessageDefaults: {
timeout: number;
destroyOnClick: boolean;
extendedTimeout: number;
priority: number;
sticky: boolean;
showProgress: boolean;
type: string;
types: string[];
injectionFactories: [];
};
};
export default config;

View File

@ -3,22 +3,22 @@ export default {
hhmmss: {
hour: 'numeric',
minute: 'numeric',
second: 'numeric'
}
second: 'numeric',
},
},
date: {
hhmmss: {
hour: 'numeric',
minute: 'numeric',
second: 'numeric'
}
second: 'numeric',
},
},
number: {
USD: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}
}
maximumFractionDigits: 2,
},
},
};

View File

@ -1,31 +0,0 @@
import Ember from 'ember';
import {helper} from '@ember/component/helper';
import {htmlSafe} from '@ember/string';
import Diff from 'diff';
const {
Handlebars: {
Utils: {escapeExpression}
}
} = Ember;
const REMOVED_TAG_TEMPLATE = value => `<span class="removed">${value}</span>`;
const ADDED_TAG_TEMPLATE = value => `<span class="added">${value}</span>`;
const stringDiff = ([text1, text2]) => {
const diff = Diff.diffWords(text2 || '', text1 || '');
return htmlSafe(
diff
.map(part => {
const value = escapeExpression(part.value);
if (part.removed) return REMOVED_TAG_TEMPLATE(value);
if (part.added) return ADDED_TAG_TEMPLATE(value);
return value;
})
.join('')
);
};
export default helper(stringDiff);

View File

@ -0,0 +1,64 @@
import {helper} from '@ember/component/helper';
import {htmlSafe} from '@ember/string';
import Diff from 'diff';
const badChars = /[&<>"'`=]/g;
const possible = /[&<>"'`=]/;
const escape = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'`': '&#x60;',
'=': '&#x3D;',
};
const escapeChar = (chr: keyof typeof escape) => {
return escape[chr];
};
const escapeExpression = (string: any): string => {
if (typeof string !== 'string') {
// don't escape SafeStrings, since they're already safe
if (string && string.toHTML) {
return string.toHTML();
} else if (string === null || string === undefined) {
return '';
} else if (!string) {
return String(string);
}
// Force a string conversion as this will be done by the append regardless and
// the regex test will do this transparently behind the scenes, causing issues if
// an object's to string has escaped characters in it.
string = String(string);
}
if (!possible.test(string)) return string;
return string.replace(badChars, escapeChar);
};
const REMOVED_TAG_TEMPLATE = (value: string) =>
`<span class="removed">${value}</span>`;
const ADDED_TAG_TEMPLATE = (value: string) =>
`<span class="added">${value}</span>`;
const stringDiff = ([text1, text2]: [string, string]) => {
const diff = Diff.diffWords(text2 || '', text1 || '');
return htmlSafe(
diff
.map((part: {value: string; removed: string; added: string}) => {
const value = escapeExpression(part.value);
if (part.removed) return REMOVED_TAG_TEMPLATE(value);
if (part.added) return ADDED_TAG_TEMPLATE(value);
return value;
})
.join('')
);
};
export default helper(stringDiff);

View File

@ -4,10 +4,10 @@ import formatDistanceToNow from 'date-fns/formatDistanceToNow';
const OPTIONS = {
addSuffix: true,
includeSeconds: false
includeSeconds: false,
};
const timeAgoInWords = ([date]) => {
const timeAgoInWords = ([date]: [string]) => {
if (isBlank(date)) return '';
return formatDistanceToNow(new Date(date), OPTIONS);

View File

@ -1,7 +1,7 @@
import Raven from 'raven-js';
import config from 'accent-webapp/config/environment';
export const initialize = application => {
export const initialize = (application) => {
if (config.SENTRY.DSN) {
Raven.config(config.SENTRY.DSN).install();
@ -15,5 +15,5 @@ export const initialize = application => {
export default {
name: 'raven-setup',
initialize
initialize,
};

View File

@ -177,8 +177,8 @@
}
},
"date_tag": {
"formatted_date_time_format": "yyyy-MM-ddTHH:mm:ss",
"humanized_date_title_format": "MMMM Do yyyy, HH:mm:ss"
"formatted_date_time_format": "yyyy-MM-dd'T'HH:mm:ss",
"humanized_date_title_format": "EEEE, MMMM do yyyy, H:mm a"
},
"versions_list": {
"export": "Export",
@ -199,7 +199,7 @@
"save_button": "Save",
"cancel_button": "Cancel",
"item": {
"deleting_label": "Deleting",
"deleting_label": "Deleting {path}.{extension}…",
"path_label": "Path",
"path_help": "Used to scope your strings. The path will be used to export all your languages (en/<em>path</em>.format, fr/<em>path</em>.format)"
}
@ -621,8 +621,8 @@
"translations_link_title": "All strings"
},
"time_ago_in_words_tag": {
"formatted_date_time_format": "yyyy-MM-ddTHH:mm:ss",
"humanized_date_title_format": "dddd, MMMM Do yyyy, H:mm a"
"formatted_date_time_format": "yyyy-MM-dd'T'HH:mm:ss",
"humanized_date_title_format": "EEEE, MMMM do yyyy, H:mm a"
},
"translation_activities_list_item": {
"action_text": {

View File

@ -1,78 +0,0 @@
import {inject as service} from '@ember/service';
import Mixin from '@ember/object/mixin';
import EmberObject, {setProperties} from '@ember/object';
const PROPS_FN = data => data;
export default Mixin.create({
apollo: service('apollo'),
graphql(query, {options, props}) {
props = props || PROPS_FN;
const graphqlObject = () => this.modelFor(this.routeName);
this._createQuery(query, options);
this._createSubscription(props, graphqlObject);
return this._currentResult(props);
},
deactivate() {
this._super(...arguments);
this._clearSubscription();
},
_currentResult(props) {
const queryObservable = this.queryObservable;
const result = queryObservable.currentResult();
const mappedResult = this._mapResult(result, props);
return EmberObject.create(mappedResult);
},
_createQuery(query, options = {}) {
this._clearSubscription();
const queryObservable = this.apollo.client.watchQuery({
query,
...options
});
setProperties(this, {queryObservable});
},
_createSubscription(props, graphqlObject) {
const next = result => {
const o = graphqlObject();
if (!o) return;
const mappedResult = this._mapResult(result, props);
setProperties(o, mappedResult);
};
const querySubscription = this.queryObservable.subscribe({next});
setProperties(this, {querySubscription});
},
_clearSubscription() {
const subscription = this.querySubscription;
if (subscription) subscription.unsubscribe();
},
_mapResult(result, props) {
if (result.data && Object.keys(result.data).length) {
const data = props(result.data);
return {
...data,
loading: result.loading,
refetch: this.queryObservable.refetch,
fetchMore: this.queryObservable.fetchMore,
startPolling: this.queryObservable.startPolling,
stopPolling: this.queryObservable.stopPolling
};
} else {
return result;
}
}
});

View File

@ -1,12 +0,0 @@
import {inject as service} from '@ember/service';
import Mixin from '@ember/object/mixin';
export default Mixin.create({
session: service(),
redirect() {
if (!this.session.isAuthenticated) {
this.transitionTo('login');
}
}
});

View File

@ -1,9 +0,0 @@
import Mixin from '@ember/object/mixin';
export default Mixin.create({
activate() {
this._super();
window.scrollTo(0, 0);
}
});

View File

@ -1,32 +0,0 @@
import {inject as service} from '@ember/service';
import Route from '@ember/routing/route';
import raven from 'raven-js';
import config from 'accent-webapp/config/environment';
export default Route.extend({
session: service('session'),
intl: service('intl'),
beforeModel() {
this._super(...arguments);
this.intl.setLocale(['en-us']);
raven.config(config.SENTRY.DSN).install();
this._tryLoginAfterRedirect();
},
_tryLoginAfterRedirect() {
const match = window.location.search
.substring(window.location.search.indexOf('?') + 1)
.split('&')
.find(segment => segment.split('=')[0] === 'token');
const token = match && match.split('=')[1];
if (!token) return;
this.session.login({token}).then(data => {
if (data && data.user) this.transitionTo('logged-in.projects');
});
}
});

View File

@ -0,0 +1,43 @@
import {inject as service} from '@ember/service';
import Route from '@ember/routing/route';
import raven from 'raven-js';
import config from 'accent-webapp/config/environment';
import Session from 'accent-webapp/services/session';
import IntlService from 'ember-intl/services/intl';
import RouterService from '@ember/routing/router-service';
export default class ApplicationRoute extends Route {
@service('session')
session: Session;
@service('intl')
intl: IntlService;
@service('router')
router: RouterService;
async beforeModel() {
this.intl.setLocale('en-us');
raven.config(config.SENTRY.DSN).install();
await this.tryLoginAfterRedirect();
}
private async tryLoginAfterRedirect() {
const match = window.location.search
.substring(window.location.search.indexOf('?') + 1)
.split('&')
.find((segment) => segment.split('=')[0] === 'token');
const token = match && match.split('=')[1];
if (!token) return;
const data = await this.session.login({token});
if (data && data.user) {
this.router.transitionTo('logged-in.projects');
}
}
}

View File

@ -0,0 +1,13 @@
import Component from '@glimmer/component';
const FALLBACK_SOURCE =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAMKADAAQAAAABAAAAMAAAAAD4/042AAACj0lEQVRoBdVaPUsDQRCNCuIX+C0ELGy0sBD9AxIjiI2VoFhZ5e+IlVbWgo2gIFhE0EKshZRiIXZWJoqCGt87cmcSzO1uZpLsDTx2Lzc7895OdrN3mkr9WRe6OeAOKAJlz0BO5EaO5FpjaVzlAd9IN+JDruQcGNUkiXwoipyDSrAk4YdJa3M9IH8ATANJtCmWgItjKInswblEAfzaJNa6E8u8QrwVAm4RexdYAkaBEWAR4Ge8p25aO88LmGUs2K3Ah75aeVUCPYHQnAX50GUWnWdAQ4RKkM2QmUO77YuAGwfS9a5cE6IqaCzi03pWDtfnDr7/umoI4AmxWVPZlUQlBPPhZtljHE+UovwaFegTCOgXjA2GagiQnKMGfRAwISAxKRirVoFlAQnJWDUBqwIBWcHYaKhoF0CUN6A3imbfGYDrJyDKr7GISWTdnnfkuYFeM8KjAGFHNAMIwvEXYTCHVutFgqyEFQHfaOcdyC9UxmlMnooAEjl2EHDiowBWYcZCBH3oqzH7ZY1FHHJmrK3wIqbls4NaXrVAFcJ8y2EyGx9TjJr7KqVExEfA5mgwDr8HQCuvOFARZPYBErO1MTjuAa+ASAjLyQAuRv974Bq4Ai6BD6AZ41F8DcgCGYDbq/NXzHYGSHQH4Oy1yhibOZjLlpfR8QvB+Aah3caczG0SYnQ4bDfzqnzMHSvAZhs9qwrY7q5V7liFYCx54pIK5rYcy8+mAu9SFoLxfNaINZttlCI5C50w5ua5qaHZVKDh4Dbc+DHlsKmAKUZH7/teAePkUEDJ6OWvQ4kCCv7yMzIrUMCR0c1fh4A7F3IeiP3B8PA+OUcn13TCRJA8OdcY1fD/JvgHCz6k+FYRciI3coxm/hfolUaVtS+2pQAAAABJRU5ErkJggg==';
export default class AccAvatarImg extends Component {
fallbackImage(event: ErrorEvent) {
const target = event.target as HTMLImageElement;
target.src = FALLBACK_SOURCE;
target.style.opacity = '0.15';
}
}

View File

@ -0,0 +1,2 @@
{{!-- template-lint-disable no-invalid-interactive --}}
<img alt="" {{on "error" this.fallbackImage }} ...attributes>

View File

@ -1,5 +0,0 @@
import Component from '@ember/component';
export default Component.extend({
classNameBindings: ['link', 'primary', 'danger']
});

View File

@ -0,0 +1,9 @@
import Component from '@glimmer/component';
interface Args {
link?: boolean;
primary?: boolean;
danger?: boolean;
}
export default class Badge extends Component<Args> {}

View File

@ -1,54 +1,57 @@
& {
transition: $transition-speed $transition-easing;
@value transition-speed, transition-easing from 'accent-webapp/styles/variables/transitions';
@value color-grey, color-green, color-error from 'accent-webapp/styles/variables/colors';
.badge {
transition: transition-speed transition-easing;
transition-property: background;
display: inline-block;
padding: 1px 6px 1px 5px;
background: lighten($color-grey, 28%);
background: lighten(color-grey, 28%);
border-radius: 3px;
color: darken($color-grey, 10%);
color: darken(color-grey, 10%);
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
text-decoration: none;
}
&.primary {
background: $color-green;
.primary {
background: color-green;
background: var(--color-primary);
color: #fff;
}
&.danger {
background: $color-error;
.danger {
background: color-error;
color: #fff;
}
&.link {
.link {
padding: 0;
&:focus,
&:hover {
background: lighten($color-grey, 26%);
background: lighten(color-grey, 26%);
}
}
&.link.primary {
.link.primary {
padding: 0;
&:focus,
&:hover {
background: darken($color-green, 3%);
background: darken(color-green, 3%);
background: var(--color-primary-darken-10);
}
}
&.link a {
.link a {
display: inline-block;
padding: 1px 6px 1px 5px;
color: lighten(#000, 50%);
text-decoration: none;
}
&.link.primary a {
.link.primary a {
color: #fff;
}

View File

@ -0,0 +1,3 @@
<div local-class="badge {{if @link "link"}} {{if @primary "primary"}} {{if @danger "danger"}}">
{{yield}}
</div>

View File

@ -1,17 +0,0 @@
import Component from '@ember/component';
import EmojiButton from '@joeattardi/emoji-button';
export default Component.extend({
didInsertElement() {
const button = this.element;
const picker = new EmojiButton({
showPreview: false
});
picker.on('emoji', this.onPicked);
button.addEventListener('click', () => {
picker.pickerVisible ? picker.hidePicker() : picker.showPicker(button);
});
}
});

View File

@ -0,0 +1,37 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import EmojiButton from '@joeattardi/emoji-button';
interface Args {
onPicked: () => string;
}
export default class EmojiPicker extends Component<Args> {
picker: EmojiButton;
element: HTMLDivElement;
clickCallback = this.onClick.bind(this);
@action
initializePicker(element: HTMLDivElement) {
this.element = element;
this.picker = new EmojiButton({
showPreview: false,
});
this.picker.on('emoji', this.args.onPicked);
}
@action
destroyPicker() {
this.picker.off('emoji', this.args.onPicked);
}
@action
onClick() {
this.picker.pickerVisible
? this.picker.hidePicker()
: this.picker.showPicker(this.element);
}
}

View File

@ -1 +1,11 @@
{{yield}}
<button
{{did-insert this.initializePicker}}
{{will-destroy this.destroyPicker}}
{{on "click" (fn this.onClick)}}
...attributes
>
<div>
{{yield}}
</div>
</button>

View File

@ -1,31 +0,0 @@
import {computed} from '@ember/object';
import {readOnly} from '@ember/object/computed';
import Component from '@ember/component';
export default Component.extend({
classNameBindings: ['isExiting', 'type'],
isExiting: readOnly('flash.exiting'),
type: readOnly('flash.type'),
iconPath: computed('type', function() {
switch (this.type) {
case 'success':
return 'assets/check.svg';
case 'error':
return 'assets/x.svg';
case 'socket':
return 'assets/activity.svg';
default:
null;
}
}),
actions: {
close() {
const flash = this.flash;
if (flash) flash.destroyMessage();
}
}
});

View File

@ -0,0 +1,39 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {readOnly} from '@ember/object/computed';
interface Args {
flash: {
exiting: boolean;
type: string;
destroyMessage: () => void;
};
}
export default class FlashMessage extends Component<Args> {
@readOnly('args.flash.exiting')
isExiting: boolean;
@readOnly('args.flash.type')
type: 'info' | 'success' | 'error' | 'socket';
get iconPath() {
switch (this.type) {
case 'success':
return 'assets/check.svg';
case 'error':
return 'assets/x.svg';
case 'socket':
return 'assets/activity.svg';
default:
return null;
}
}
@action
close() {
const flash = this.args.flash;
if (flash) flash.destroyMessage();
}
}

View File

@ -1,4 +1,6 @@
& {
@value color-green, color-socket, color-error from 'accent-webapp/styles/variables/colors';
.flash {
position: relative;
margin-bottom: 10px;
width: 400px;
@ -8,35 +10,37 @@
text-shadow: 0 1px 1px rgba(#000, 0.1);
color: #fff;
font-weight: bold;
animation: 0.3s flash-message-in ease;
animation: 0.3s ease;
animation-name: flash-message-in;
pointer-events: all;
&.is-exiting {
animation: 0.3s flash-message-out forwards ease-in-out;
animation: 0.3s forwards ease-in-out;
animation-name: flash-message-out;
}
}
&.success {
background: $color-green;
.flash.success {
background: color-green;
.icon {
stroke: lighten($color-green, 35%);
stroke: lighten(color-green, 35%);
}
}
&.socket {
background: $color-socket;
.flash.socket {
background: color-socket;
.icon {
stroke: lighten($color-socket, 35%);
stroke: lighten(color-socket, 35%);
}
}
&.error {
background: $color-error;
.flash.error {
background: color-error;
.icon {
stroke: lighten($color-error, 35%);
stroke: lighten(color-error, 35%);
}
}

View File

@ -1,14 +1,16 @@
<button class="deleteButton" {{action "close"}}>
{{inline-svg "assets/x.svg" class="deleteButton-icon"}}
</button>
<div local-class="flash {{if this.isExiting "is-exiting"}} {{this.type}}">
<button local-class="deleteButton" {{on "click" (fn this.close)}}>
{{inline-svg "assets/x.svg" local-class="deleteButton-icon"}}
</button>
<div class="content">
{{#if iconPath}}
<div class="icon">
{{inline-svg iconPath class="icon"}}
</div>
{{/if}}
<p class="text">
{{flash.message}}
</p>
<div local-class="content">
{{#if this.iconPath}}
<div local-class="icon">
{{inline-svg this.iconPath}}
</div>
{{/if}}
<p local-class="text">
{{@flash.message}}
</p>
</div>
</div>

View File

@ -1,9 +0,0 @@
import Component from '@ember/component';
export default Component.extend({
actions: {
close() {
this.onClose();
}
}
});

View File

@ -0,0 +1,8 @@
import Component from '@glimmer/component';
interface Args {
small: boolean;
onClose: () => void;
}
export default class Modal extends Component<Args> {}

View File

@ -1,7 +1,7 @@
<EmberWormhole @to="modals">
<div class="acc-modal__wrapper">
<div role="button" class="acc-modal__overlay" {{action "close"}}></div>
<div role="dialog" class="acc-modal__container {{if small "acc-modal__container--small"}}">
<div role="button" class="acc-modal__overlay" {{on "click" (fn @onClose)}}></div>
<div role="dialog" class="acc-modal__container {{if @small "acc-modal__container--small"}}">
{{yield}}
</div>
</div>

View File

@ -1,3 +0,0 @@
import Component from '@ember/component';
export default Component.extend();

View File

@ -0,0 +1,15 @@
import Component from '@glimmer/component';
interface Args {
searchEnabled: boolean;
selected: any;
options: any[];
onchange: (value: any) => void;
placeholder: string;
search: (term: string) => Promise<any>;
searchPlaceholder: string;
matchTriggerWidth: boolean;
renderInPlace: boolean;
}
export default class Select extends Component<Args> {}

View File

@ -1,12 +1,13 @@
<PowerSelect
@options={{options}}
@matchTriggerWidth={{matchTriggerWidth}}
@searchEnabled={{searchEnabled}}
@selected={{selected}}
@search={{search}}
@placeholder={{placeholder}}
@searchPlaceholder={{searchPlaceholder}}
@onChange={{onchange}} as |option|
@options={{@options}}
@matchTriggerWidth={{@matchTriggerWidth}}
@searchEnabled={{@searchEnabled}}
@selected={{@selected}}
@search={{@search}}
@placeholder={{@placeholder}}
@searchPlaceholder={{@searchPlaceholder}}
@renderInPlace={{@renderInPlace}}
@onChange={{@onchange}} as |option|
>
{{option.label}}
</PowerSelect>

View File

@ -1,178 +0,0 @@
import {computed} from '@ember/object';
import {inject as service} from '@ember/service';
import {readOnly, reads, equal} from '@ember/object/computed';
import Component from '@ember/component';
import {underscore, dasherize} from '@ember/string';
import parsedKeyProperty from 'accent-webapp/computed-macros/parsed-key';
/* eslint camelcase:0 */
const ACTIONS_ICON_PATHS = {
version_new: 'assets/tag.svg',
add_to_version: 'assets/tag.svg',
create_version: 'assets/tag.svg',
sync: 'assets/sync.svg',
merge: 'assets/merge.svg',
rollback: 'assets/revert.svg',
update: 'assets/pencil.svg',
correct_conflict: 'assets/check.svg',
correct_all: 'assets/check.svg',
uncorrect_all: 'assets/revert.svg',
uncorrect_conflict: 'assets/revert.svg',
conflict_on_slave: 'assets/x.svg',
conflict_on_corrected: 'assets/x.svg',
conflict_on_proposed: 'assets/x.svg',
remove: 'assets/x.svg',
new_comment: 'assets/bubble.svg',
new_slave: 'assets/language.svg',
document_delete: 'assets/file.svg'
};
// Attributes:
// project: Object <project>
// permissions: Ember Object containing <permission>
// showTranslationLink: Boolean
// activity: Object <project-activity>
// componentTranslationPrefix: String
export default Component.extend({
intl: service('intl'),
classNameBindings: ['compact', 'activityItemClassName', 'rollbacked'],
action: readOnly('activity.action'),
rollbacked: reads('activity.isRollbacked'),
rollbackedOperationHasEmptyText: equal(
'activity.rollbackedOperation.valueType',
'EMPTY'
),
hasEmptyText: equal('activity.valueType', 'EMPTY'),
translationKey: parsedKeyProperty('activity.translation.key'),
activityItemClassName: computed('activity.action', function() {
return dasherize(this.activity.action);
}),
actionText: computed('action', function() {
return this._getActionText(this.action);
}),
rollbackedOperationActionText: computed(
'activity.rollbackedOperation.action',
function() {
return this._getActionText(this.activity.rollbackedOperation.action);
}
),
showFromOperationTranslationLink: computed(
'showTranslationLink',
'activity.rollbackedOperation.translation.id',
function() {
return (
this.showTranslationLink &&
this.activity.rollbackedOperation &&
this.activity.rollbackedOperation.translation &&
this.activity.rollbackedOperation.translation.id
);
}
),
showStats: computed('activity.stats.[]', function() {
return this.activity.stats;
}),
localizedStats: computed('activity.stats.[]', function() {
return this.activity.stats.map(stat => {
const text = this.intl.t(
`components.${this.componentTranslationPrefix}.stats_text.${underscore(
stat.action
)}`
);
const count = stat.count;
return {text, count};
});
}),
statsLabel: computed('componentTranslationPrefix', function() {
return this.intl.t(
`components.${this.componentTranslationPrefix}.stats_label_text`
);
}),
showDocumentInfo: computed('action', 'activity.document.path', function() {
const action = this.action;
const actionsWithDocument = ['sync', 'document_delete', 'merge'];
return (
actionsWithDocument.includes(action) && readOnly('activity.document.path')
);
}),
showVersionInfo: readOnly('activity.version.id'),
revisionName: computed('activity.revision.{name,language.name}', function() {
return this.activity.revision.name || this.activity.revision.language.name;
}),
showRevisionInfo: computed(
'action',
'activity.revision.language.id',
function() {
if (!this.activity.revision) return false;
const actionsWithRevision = [
'new',
'remove',
'renew',
'new_slave',
'merge',
'uncorrect_all',
'correct_all',
'batch_correct_conflict',
'batch_update',
'conflict_on_slave'
];
return (
actionsWithRevision.includes(this.action) &&
this.activity.revision.language.id
);
}
),
showFromOperationDocumentInfo: computed(
'activity.rollbackedOperation.{action,document.path}',
function() {
const action = this.activity.rollbackedOperation.action;
const actionsWithDocument = ['sync', 'document_delete', 'merge'];
return (
actionsWithDocument.includes(action) &&
readOnly('activity.rollbackedOperation.document.path')
);
}
),
isShowingTranslationLink: computed(
'showTranslationLink',
'activity.{action,translation}',
function() {
return (
this.showTranslationLink &&
this.activity.translation &&
this.activity.action !== 'rollback'
);
}
),
iconPath: computed('action', function() {
return ACTIONS_ICON_PATHS[this.action] || 'assets/add.svg';
}),
_getActionText(action) {
return this.intl.t(
`components.${this.componentTranslationPrefix}.action_text.${action}`
);
}
});

View File

@ -0,0 +1,176 @@
import {inject as service} from '@ember/service';
import {readOnly, equal} from '@ember/object/computed';
import Component from '@glimmer/component';
import {underscore, dasherize} from '@ember/string';
import parsedKeyProperty from 'accent-webapp/computed-macros/parsed-key';
import IntlService from 'ember-intl/services/intl';
/* eslint camelcase:0 */
const ACTIONS_ICON_PATHS = {
version_new: 'assets/tag.svg',
add_to_version: 'assets/tag.svg',
create_version: 'assets/tag.svg',
sync: 'assets/sync.svg',
merge: 'assets/merge.svg',
rollback: 'assets/revert.svg',
update: 'assets/pencil.svg',
correct_conflict: 'assets/check.svg',
correct_all: 'assets/check.svg',
uncorrect_all: 'assets/revert.svg',
uncorrect_conflict: 'assets/revert.svg',
conflict_on_slave: 'assets/x.svg',
conflict_on_corrected: 'assets/x.svg',
conflict_on_proposed: 'assets/x.svg',
remove: 'assets/x.svg',
new_comment: 'assets/bubble.svg',
new_slave: 'assets/language.svg',
document_delete: 'assets/file.svg',
};
interface Args {
compact: boolean;
permissions: Record<string, true>;
showTranslationLink: boolean;
componentTranslationPrefix: string;
activity: any;
project: any;
}
export default class ActivityItem extends Component<Args> {
@service('intl')
intl: IntlService;
@readOnly('args.activity.action')
action: keyof typeof ACTIONS_ICON_PATHS;
@readOnly('args.activity.isRollbacked')
rollbacked: boolean;
@equal('args.activity.rollbackedOperation.valueType', 'EMPTY')
rollbackedOperationHasEmptyText: boolean;
@equal('args.activity.fromOperation.text', 'EMPTY')
fromOperationHasEmptyText: boolean;
@equal('args.activity.valueType', 'EMPTY')
hasEmptyText: boolean;
@readOnly('args.activity.version.id')
showVersionInfo: boolean;
translationKey = parsedKeyProperty(this.args.activity.translation?.key);
get activityItemClassName() {
return dasherize(this.args.activity.action);
}
get actionText() {
return this.getActionText(this.action);
}
get rollbackedOperationActionText() {
return this.getActionText(this.args.activity.rollbackedOperation.action);
}
get showFromOperationTranslationLink() {
return (
this.args.showTranslationLink &&
this.args.activity.rollbackedOperation &&
this.args.activity.rollbackedOperation.translation &&
this.args.activity.rollbackedOperation.translation.id
);
}
get showStats() {
return this.args.activity.stats;
}
get localizedStats() {
return this.args.activity.stats.map((stat: any) => {
const text = this.intl.t(
`components.${
this.args.componentTranslationPrefix
}.stats_text.${underscore(stat.action)}`
);
const count = stat.count;
return {text, count};
});
}
get statsLabel() {
return this.intl.t(
`components.${this.args.componentTranslationPrefix}.stats_label_text`
);
}
get showDocumentInfo() {
const action = this.action;
const actionsWithDocument = ['sync', 'document_delete', 'merge'];
return (
actionsWithDocument.includes(action) &&
this.args.activity.document &&
this.args.activity.document.path
);
}
get revisionName() {
return (
this.args.activity.revision.name ||
this.args.activity.revision.language.name
);
}
get showRevisionInfo() {
if (!this.args.activity.revision) return false;
const actionsWithRevision = [
'new',
'remove',
'renew',
'new_slave',
'merge',
'uncorrect_all',
'correct_all',
'batch_correct_conflict',
'batch_update',
'conflict_on_slave',
];
return (
actionsWithRevision.includes(this.action) &&
this.args.activity.revision.language.id
);
}
get showFromOperationDocumentInfo() {
const action = this.args.activity.rollbackedOperation.action;
const actionsWithDocument = ['sync', 'document_delete', 'merge'];
return (
actionsWithDocument.includes(action) &&
this.args.activity.rollbackedOperation.document.path
);
}
get isShowingTranslationLink() {
return (
this.args.showTranslationLink &&
this.args.activity.translation &&
this.args.activity.action !== 'rollback'
);
}
get iconPath() {
return ACTIONS_ICON_PATHS[this.action] || 'assets/add.svg';
}
private getActionText(action: keyof typeof ACTIONS_ICON_PATHS) {
return this.intl.t(
`components.${this.args.componentTranslationPrefix}.action_text.${action}`
);
}
}

View File

@ -1,4 +1,8 @@
& {
@value transition-speed, transition-easing from 'accent-webapp/styles/variables/transitions';
@value color-border, color-light-background, color-green, color-grey, color-error, color-success, color-warning, color-black from 'accent-webapp/styles/variables/colors';
@value font-monospace from 'accent-webapp/styles/variables/fonts';
.item {
display: flex;
position: relative;
margin: 25px 0;
@ -10,17 +14,17 @@
}
}
&.rollback,
&.merge,
&.new-slave,
&.uncorrect-all,
&.correct-all,
&.sync {
left: 9px;
.item.rollback,
.item.merge,
.item.new-slave,
.item.uncorrect-all,
.item.correct-all,
.item.sync {
left: 8px;
width: calc(100% + -9px);
padding: 5px;
background: #fff;
border: 1px solid rgba($color-border, 0.5);
border: 1px solid rgba(color-border, 0.5);
.item-iconContainer {
top: 3px;
@ -29,29 +33,35 @@
.item-content {
width: 100%;
padding: 5px 5px 0;
padding: 5px 5px 0 2px;
margin-left: -21px;
background: #fff;
border-bottom: 0;
}
}
&.sync {
.item.sync {
position: relative;
padding: 6px 10px 5px;
background: $color-light-background;
border-color: transparent;
border-left-color: $color-green;
border-left-color: var(--color-primary);
&.compact {
.item-content {
padding-left: 0;
}
&:before {
display: block;
position: absolute;
top: 0;
left: 0;
content: '';
width: 100%;
height: 100%;
background: var(--color-primary);
box-shadow: 0 1px 10px var(--color-primary);
opacity: 0.16;
pointer-events: none;
}
.item-iconContainer {
left: -21px;
background: $color-green;
background: color-green;
background: var(--color-primary);
border-color: transparent;
box-shadow: none;
@ -63,63 +73,52 @@
.item-stats {
padding: 0;
background: lighten($color-green, 46%);
background: var(--color-primary-lighten-99);
color: darken($color-green, 25%);
background: transparent;
color: darken(color-green, 25%);
color: var(--color-primary-darken-50);
}
.item-date {
color: $color-grey;
color: rgba(#000, 0.3);
}
.item-details-link {
color: $color-green;
color: color-green;
color: var(--color-primary);
}
.item-content {
background: var(--color-primary-lighten-95);
font-size: 14px;
}
.item-documentPath {
color: darken($color-green, 25%);
color: darken(color-green, 25%);
color: var(--color-primary-darken-30);
font-size: 13px;
}
.item-user,
.item-header-content {
color: darken($color-green, 25%);
color: darken(color-green, 25%);
color: var(--color-primary-darken-30);
font-size: 13px;
}
.item-user.item-user--bot {
padding-left: 23px;
}
.item-user.item-user--pictureUrl {
padding-left: 28px;
}
.item-user-icon {
stroke: darken($color-green, 25%);
stroke: darken(color-green, 25%);
stroke: var(--color-primary);
width: 17px;
height: 17px;
}
.item-user-picture {
top: -1px;
width: 21px;
height: 21px;
color: rgba(color-black, 0.9);
}
&.compact {
.item-content {
padding-left: 0;
}
.item-header {
margin-bottom: 0;
}
.item-iconContainer-icon {
stroke: $color-green;
stroke: color-green;
stroke: var(--color-primary);
}
@ -139,9 +138,9 @@
}
}
&.rollback {
background: $color-light-background;
border: 1px solid rgba($color-border, 0.3);
.item.rollback {
background: color-light-background;
border: 1px solid rgba(color-border, 0.3);
.item-header {
margin-bottom: 4px;
@ -158,14 +157,14 @@
.item-content {
padding: 5px 0 0;
margin: 0;
background: $color-light-background;
background: color-light-background;
border-bottom: 0;
}
}
&.uncorrect-all {
.item.uncorrect-all {
.item-iconContainer {
background: $color-error;
background: color-error;
border-color: transparent;
}
@ -175,20 +174,20 @@
&.compact {
.item-iconContainer-icon {
stroke: $color-error;
stroke: color-error;
}
}
}
&.correct-all {
.item.correct-all {
&.compact {
.item-iconContainer-icon {
stroke: $color-success;
stroke: color-success;
}
}
.item-iconContainer {
background: $color-success;
background: color-success;
border-color: transparent;
}
@ -197,11 +196,11 @@
}
}
&.document-delete {
border-color: lighten($color-error, 35%);
.item.document-delete {
border-color: lighten(color-error, 35%);
.item-iconContainer {
background: $color-error;
background: color-error;
border-color: transparent;
}
@ -211,57 +210,57 @@
&.compact {
.item-iconContainer-icon {
stroke: $color-error;
stroke: color-error;
}
}
}
&.correct-conflict {
.item.correct-conflict {
.item-iconContainer {
border-color: rgba($color-success, 0.4);
border-color: rgba(color-success, 0.4);
}
.item-iconContainer-icon {
stroke: $color-success;
stroke: color-success;
}
}
&.uncorrect-conflict {
.item.uncorrect-conflict {
.item-iconContainer {
border-color: rgba($color-error, 0.2);
border-color: rgba(color-error, 0.2);
}
.item-iconContainer-icon {
stroke: $color-error;
stroke: color-error;
}
}
&.conflict-on-corrected {
.item.conflict-on-corrected {
.item-iconContainer {
border-color: lighten($color-error, 35%);
border-color: lighten(color-error, 35%);
}
.item-iconContainer-icon {
stroke: $color-error;
stroke: color-error;
}
}
&.conflict-on-proposed {
.item.conflict-on-proposed {
.item-iconContainer {
border-color: lighten($color-warning, 25%);
border-color: lighten(color-warning, 25%);
}
.item-iconContainer-icon {
stroke: $color-warning;
stroke: color-warning;
}
}
&.rollbacked {
.item.rollbacked {
.item-stats,
.item-translationText,
.item-actions,
.item-header {
opacity: 0.4;
opacity: 0.6;
font-size: 12px;
}
@ -272,13 +271,13 @@
.item-iconContainer {
background: #fff;
border-color: rgba($color-grey, 0.4);
border-color: rgba(color-grey, 0.4);
}
.item-iconContainer-icon {
width: 11px;
height: 11px;
stroke: rgba($color-grey, 0.5);
stroke: rgba(color-grey, 0.5);
}
&.rollback,
@ -286,7 +285,7 @@
&.new-slave,
&.uncorrect-all,
&.correct-all {
border-color: $color-border;
border-color: color-border;
.item-iconContainer {
left: -19px;
@ -295,7 +294,11 @@
}
&.sync {
border-color: rgba($color-green, 0.1);
border-color: rgba(color-green, 0.1);
&:before {
display: none;
}
.item-iconContainer {
left: -19px;
@ -304,7 +307,7 @@
}
}
&.rollback.compact {
.item.rollback.compact {
.item-iconContainer {
top: -2px;
left: -17px;
@ -316,8 +319,8 @@
}
}
&.rollbacked.compact,
&.compact {
.item.rollbacked.compact,
.item.compact {
width: 100%;
margin: 12px 0 2px;
border-color: transparent;
@ -395,6 +398,10 @@
width: 15px;
height: 15px;
}
.item-details-link {
color: rgba(color-black, 0.4);
}
}
.item-wrapper {
@ -406,7 +413,7 @@
display: inline-flex;
align-items: baseline;
font-size: 12px;
font-family: $font-monospace;
font-family: font-monospace;
color: #aaa;
}
@ -421,7 +428,7 @@
height: 21px;
margin-right: 10px;
border-radius: 50%;
border: 1px solid $color-grey;
border: 1px solid color-grey;
box-shadow: 0 0 0 5px #fff;
background: #fff;
}
@ -430,7 +437,7 @@
width: 11px;
height: 11px;
flex: 0 0 11px;
stroke: $color-grey;
stroke: color-grey;
}
.item-content {
@ -438,7 +445,7 @@
font-size: 13px;
}
&.compact {
.item.compact {
.item-header {
flex-direction: column;
}
@ -498,7 +505,7 @@
margin-left: 2px;
width: 14px;
height: 14px;
color: #565656;
color: rgba(color-black, 0.6);
}
.item-translationFromOperationText,
@ -506,7 +513,7 @@
.item-translationText {
margin: 5px 0 10px;
padding: 8px;
background: $color-light-background;
background: color-light-background;
font-size: 12px;
font-style: italic;
}
@ -536,7 +543,7 @@
.item-documentPath {
display: inline-block;
color: $color-black;
color: color-black;
color: var(--color-black);
font-weight: bold;
font-size: 12px;
@ -555,12 +562,12 @@
text-decoration: none;
font-size: 12px;
font-weight: bold;
transition: $transition-speed $transition-easing;
transition: transition-speed transition-easing;
transition-property: color;
&:hover,
&:focus {
color: $color-green;
color: color-green;
color: var(--color-primary);
}
@ -579,7 +586,7 @@
.item-revisionLink {
display: inline-block;
text-decoration: none;
color: $color-green;
color: color-green;
color: var(--color-primary);
&:hover,
@ -596,7 +603,7 @@
}
.item-date {
color: $color-grey;
color: rgba(#000, 0.3);
font-size: 11px;
}
@ -606,18 +613,18 @@
}
.item-rollback {
transition: $transition-speed $transition-easing;
transition: transition-speed transition-easing;
transition-property: opacity, color;
opacity: 0;
background: none;
padding: 0;
margin: 0 0 0 5px;
color: $color-grey;
color: color-grey;
font-size: 11px;
&:focus,
&:hover {
color: darken($color-grey, 25%);
color: darken(color-grey, 25%);
text-decoration: underline;
}
}
@ -649,21 +656,21 @@
margin-left: 5px;
border-left: 1px solid #ddd;
text-decoration: none;
color: $color-grey;
color: color-grey;
font-size: 11px;
font-weight: 500;
&:focus,
&:hover {
text-decoration: underline;
color: $color-green;
color: color-green;
color: var(--color-primary);
}
}
.item-content-rollbacked {
margin-bottom: 6px;
color: $color-error;
color: color-error;
font-size: 12px;
font-weight: 500;
}

View File

@ -1,219 +1,219 @@
<li class="item-wrapper">
<span class="item-iconContainer">
{{inline-svg iconPath class="item-iconContainer-icon"}}
</span>
<div class="item-content">
{{#if activity.isRollbacked}}
<div class="item-content-rollbacked">
{{t "components.activity_item.rollbacked"}}
<TimeAgoInWordsTag
@date={{activity.updatedAt}}
class="item-rollbacked-date"
/>
</div>
{{/if}}
<div class="item-header">
<div class="item-header-content">
{{#if activity.user.isBot}}
<span class="item-user item-user--bot">
{{inline-svg "assets/bot.svg" class="item-user-icon"}}
{{activity.user.fullname}}
<div local-class="item {{this.activityItemClassName}} {{if @compact "compact"}} {{if this.rollbacked "rollbacked"}}">
<li local-class="item-wrapper">
<span local-class="item-iconContainer">
{{inline-svg this.iconPath local-class="item-iconContainer-icon"}}
</span>
<div local-class="item-content">
{{#if @activity.isRollbacked}}
<div local-class="item-content-rollbacked">
{{t "components.activity_item.rollbacked"}}
<span local-class="item-rollbacked-date">
<TimeAgoInWordsTag @date={{@activity.updatedAt}} />
</span>
{{else}}
<span
class="item-user
{{if activity.user.pictureUrl "item-user--pictureUrl"}}"
>
{{#if activity.user.pictureUrl}}
<img
class="item-user-picture"
src="{{activity.user.pictureUrl}}"
>
{{/if}}
{{activity.user.fullname}}
</span>
{{/if}}
</div>
{{/if}}
{{actionText}}
{{#if showDocumentInfo}}
<LinkTo
@route="logged-in.project.files.export"
@models={{array project.id activity.document.id}}
class="item-documentPath"
>
{{activity.document.path}}
</LinkTo>
{{/if}}
{{#if showVersionInfo}}
<span class="item-version-tag">
{{activity.version.tag}}
</span>
{{/if}}
{{#if showRevisionInfo}}
<LinkTo
@route="logged-in.project.revision.translations"
@models={{array project.id activity.revision.id}}
class="item-revisionLink"
>
{{revisionName}}
</LinkTo>
{{/if}}
{{#if isShowingTranslationLink}}
{{#if activity.translation.isRemoved}}
<LinkTo
@route="logged-in.project.translation"
@models={{array project.id activity.translation.id}}
class="item-translationLink
item-translationLink--removed"
>
<small class="item-translationLink-prefix">
{{#if translationKey.prefix}}
{{translationKey.prefix}}
{{else}}
{{translation.document.path}}
{{/if}}
</small>
{{translationKey.value}}
</LinkTo>
<div local-class="item-header">
<div local-class="item-header-content">
{{#if @activity.user.isBot}}
<span local-class="item-user item-user--bot">
{{inline-svg "assets/bot.svg" local-class="item-user-icon"}}
{{@activity.user.fullname}}
</span>
{{else}}
<LinkTo
@route="logged-in.project.translation"
@models={{array project.id activity.translation.id}}
class="item-translationLink"
<span
local-class="item-user
{{if @activity.user.pictureUrl "item-user--pictureUrl"}}"
>
<small class="item-translationLink-prefix">
{{#if translationKey.prefix}}
{{translationKey.prefix}}
{{else}}
{{translation.document.path}}
{{/if}}
</small>
{{translationKey.value}}
{{#if @activity.user.pictureUrl}}
<AccAvatarImg
local-class="item-user-picture"
src="{{@activity.user.pictureUrl}}"
/>
{{/if}}
{{@activity.user.fullname}}
</span>
{{/if}}
{{this.actionText}}
{{#if this.showDocumentInfo}}
<LinkTo
@route="logged-in.project.files.export"
@models={{array @project.id @activity.document.id}}
local-class="item-documentPath"
>
{{@activity.document.path}}
</LinkTo>
{{/if}}
{{/if}}
</div>
<div class="item-actions">
<TimeAgoInWordsTag @date={{activity.insertedAt}} class="item-date" />
<LinkTo
@route="logged-in.project.activity"
@models={{array project.id activity.id}}
class="item-details-link"
>
{{t "components.activity_item.details"}}
</LinkTo>
</div>
</div>
{{#if this.showVersionInfo}}
<span local-class="item-version-tag">
{{@activity.version.tag}}
</span>
{{/if}}
{{#if activity.rollbackedOperation}}
<div class="item-rollback-content">
<div>
<span class="item-rollback-user">
{{activity.rollbackedOperation.user.fullname}}
</span>
{{rollbackedOperationActionText}}
{{#if this.showRevisionInfo}}
<LinkTo
@route="logged-in.project.revision.translations"
@models={{array @project.id @activity.revision.id}}
local-class="item-revisionLink"
>
{{this.revisionName}}
</LinkTo>
{{/if}}
{{#if showFromOperationTranslationLink}}
{{#if activity.rollbackedOperation.translation.isRemoved}}
{{#if this.isShowingTranslationLink}}
{{#if @activity.translation.isRemoved}}
<LinkTo
@route="logged-in.project.translation"
@models={{array
project.id
activity.rollbackedOperation.translation.id
}}
class="item-translationLink--removed"
@models={{array @project.id @activity.translation.id}}
local-class="item-translationLink
item-translationLink--removed"
>
{{activity.rollbackedOperation.translation.key}}
<small local-class="item-translationLink-prefix">
{{#if this.translationKey.prefix}}
{{this.translationKey.prefix}}
{{else}}
{{@activity.document.path}}
{{/if}}
</small>
{{this.translationKey.value}}
</LinkTo>
{{else}}
<LinkTo
@route="logged-in.project.translation"
@models={{array
project.id
activity.rollbackedOperation.translation.id
}}
class="item-translationLink"
@models={{array @project.id @activity.translation.id}}
local-class="item-translationLink"
>
{{activity.rollbackedOperation.translation.key}}
<small local-class="item-translationLink-prefix">
{{#if this.translationKey.prefix}}
{{this.translationKey.prefix}}
{{else}}
{{@activity.document.path}}
{{/if}}
</small>
{{this.translationKey.value}}
</LinkTo>
{{/if}}
{{/if}}
{{#if activity.fromOperation.text}}
<div class="item-translationFromOperationText">
{{activity.fromOperation.text}}
</div>
{{else if fromOperationHasEmptyText}}
<div class="item-translationFromOperationText">
<span class="item-translationText-emptyText">
{{t "components.activity_item.empty_text"}}
</span>
</div>
{{/if}}
{{#if showFromOperationDocumentInfo}}
<LinkTo
@route="logged-in.project.files.export"
@models={{array
project.id
activity.rollbackedOperation.document.id
}}
class="item-documentPath"
>
{{activity.rollbackedOperation.document.path}}
</LinkTo>
{{/if}}
</div>
{{#unless compact}}
<TimeAgoInWordsTag
@date={{activity.rollbackedOperation.insertedAt}}
class="item-date"
/>
<div local-class="item-actions">
<span local-class="item-date"><TimeAgoInWordsTag @date={{@activity.insertedAt}} /></span>
<LinkTo
@route="logged-in.project.activity"
@models={{array project.id activity.rollbackedOperation.id}}
class="item-details-link"
@models={{array @project.id @activity.id}}
local-class="item-details-link"
>
{{t "components.activity_item.details"}}
</LinkTo>
{{/unless}}
</div>
</div>
{{/if}}
{{#if activity.text}}
<div class="item-translationText">
<div class="item-translationText-text">{{activity.text}}</div>
</div>
{{else if hasEmptyText}}
<div class="item-translationText">
<span class="item-translationText-emptyText">
{{t "components.activity_item.empty_text"}}
</span>
</div>
{{/if}}
{{#if @activity.rollbackedOperation}}
<div local-class="item-rollback-content">
<div>
<span local-class="item-rollback-user">
{{@activity.rollbackedOperation.user.fullname}}
</span>
{{this.rollbackedOperationActionText}}
{{#if showStats}}
<ul class="item-stats">
<span class="item-stats-label">
{{statsLabel}}
</span>
{{#each localizedStats as |stat|}}
<li>
{{stat.text}}
:
<b>
{{stat.count}}
</b>
</li>
{{/each}}
</ul>
{{/if}}
</div>
</li>
{{#if this.showFromOperationTranslationLink}}
{{#if @activity.rollbackedOperation.translation.isRemoved}}
<LinkTo
@route="logged-in.project.translation"
@models={{array
@project.id
@activity.rollbackedOperation.translation.id
}}
local-class="item-translationLink--removed"
>
{{@activity.rollbackedOperation.translation.key}}
</LinkTo>
{{else}}
<LinkTo
@route="logged-in.project.translation"
@models={{array
@project.id
@activity.rollbackedOperation.translation.id
}}
local-class="item-translationLink"
>
{{@activity.rollbackedOperation.translation.key}}
</LinkTo>
{{/if}}
{{/if}}
{{#if @activity.fromOperation.text}}
<div local-class="item-translationFromOperationText">
{{@activity.fromOperation.text}}
</div>
{{else if this.fromOperationHasEmptyText}}
<div local-class="item-translationFromOperationText">
<span local-class="item-translationText-emptyText">
{{t "components.activity_item.empty_text"}}
</span>
</div>
{{/if}}
{{#if this.showFromOperationDocumentInfo}}
<LinkTo
@route="logged-in.project.files.export"
@models={{array
@project.id
@activity.rollbackedOperation.document.id
}}
local-class="item-documentPath"
>
{{@activity.rollbackedOperation.document.path}}
</LinkTo>
{{/if}}
</div>
{{#unless @compact}}
<span local-class="item-date">
<TimeAgoInWordsTag @date={{@activity.rollbackedOperation.insertedAt}} />
</span>
<LinkTo
@route="logged-in.project.activity"
@models={{array @project.id @activity.rollbackedOperation.id}}
local-class="item-details-link"
>
{{t "components.activity_item.details"}}
</LinkTo>
{{/unless}}
</div>
{{/if}}
{{#if @activity.text}}
<div local-class="item-translationText">
<div local-class="item-translationText-text">{{@activity.text}}</div>
</div>
{{else if this.hasEmptyText}}
<div local-class="item-translationText">
<span local-class="item-translationText-emptyText">
{{t "components.activity_item.empty_text"}}
</span>
</div>
{{/if}}
{{#if this.showStats}}
<ul local-class="item-stats">
<span local-class="item-stats-label">
{{this.statsLabel}}
</span>
{{#each this.localizedStats as |stat|}}
<li>
{{stat.text}}
:
<b>
{{stat.count}}
</b>
</li>
{{/each}}
</ul>
{{/if}}
</div>
</li>
</div>

View File

@ -1,5 +0,0 @@
import Component from '@ember/component';
export default Component.extend({
tagName: 'footer'
});

View File

@ -0,0 +1,3 @@
import Component from '@glimmer/component';
export default class ApplicationFooter extends Component {}

View File

@ -1,8 +1,10 @@
& {
@value color-light-background, color-grey from 'accent-webapp/styles/variables/colors';
.footer {
padding: 10px;
margin-top: 40px;
background: $color-light-background;
color: $color-grey;
background: color-light-background;
color: color-grey;
font-size: 12px;
text-align: right;
}

View File

@ -1,8 +1,10 @@
<div class="inner">
<div>
{{t "components.application_footer.text"}}
<a class="external-link" target="_blank" rel="noopener" href="http://mirego.com">
{{t "general.company_name"}}
</a>
<footer local-class="footer">
<div local-class="inner">
<div>
{{t "components.application_footer.text"}}
<a local-class="external-link" target="_blank" rel="noopener" href="https://mirego.com">
{{t "general.company_name"}}
</a>
</div>
</div>
</div>
</footer>

View File

@ -1,18 +0,0 @@
import {reads} from '@ember/object/computed';
import Component from '@ember/component';
export default Component.extend({
classNameBindings: [':button', 'loading:button--loading'],
tagName: 'button',
attributeBindings: ['disabled', 'type'],
disabled: reads('loading'),
loading: false,
click() {
if (this.disabled) return;
const click = this.onClick;
if (typeof click === 'function') click();
}
});

View File

@ -0,0 +1,21 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
interface Args {
onClick: () => void;
loading?: boolean;
disabled?: boolean;
}
export default class AsyncButton extends Component<Args> {
get disabled() {
return this.args.disabled || this.args.loading;
}
@action
onClick() {
if (this.args.disabled) return;
if (typeof this.args.onClick === 'function') this.args.onClick();
}
}

View File

@ -1,5 +1,8 @@
& {
padding: 0;
@value transition-speed, transition-easing from 'accent-webapp/styles/variables/transitions';
@value color-black from 'accent-webapp/styles/variables/colors';
.button {
padding: 0 !important;
&.button--loading {
cursor: default;
@ -14,13 +17,13 @@
}
}
&.button--filled {
&:global(.button--filled) {
.loading {
fill: #fff;
}
}
&.button--small {
&:global(.button--small) {
&.button--loading {
.loading {
width: 14px;
@ -40,7 +43,7 @@
}
.label {
transition: $transition-speed $transition-easing;
transition: transition-speed transition-easing;
transition-property: transform;
transform: translate3d(0, 0, 0);
display: flex;
@ -49,12 +52,12 @@
}
.loading {
transition: $transition-speed $transition-easing;
transition: transition-speed transition-easing;
transition-property: left;
will-change: left;
width: 15px;
position: absolute;
top: calc(50% - 8px);
left: 100%;
fill: $color-black;
fill: color-black;
}

View File

@ -1,5 +1,13 @@
<div class="content">
<span class="label">{{yield}}</span>
<button
local-class="button {{if @loading "button--loading"}}"
class="button"
disabled={{this.disabled}}
...attributes
{{on "click" (fn this.onClick)}}
>
<div local-class="content">
<span local-class="label" class="label">{{yield}}</span>
{{inline-svg "/assets/loading.svg" class="loading"}}
</div>
{{inline-svg "/assets/loading.svg" local-class="loading"}}
</div>
</button>

View File

@ -1,267 +0,0 @@
import {computed} from '@ember/object';
import {inject as service} from '@ember/service';
import {equal} from '@ember/object/computed';
import Component from '@ember/component';
const DEFAULT_PROPERTIES = {
isFileReading: false,
isFileRead: false,
isPeeking: false,
isPeekingDone: false,
isPeekingError: false,
isCommiting: false,
isCommitingDone: false,
isCommitingError: false,
file: null,
fileSource: null,
documentPath: null,
documentFormat: 'json'
};
// Attributes
// permissions: Ember Object containing <permission>
// revisions: Array of <revision>
// documents: Array of <document>
// commitButtonText: String
// onFileCancel: Function
// onPeek: Function
// onCommit: Function
export default Component.extend({
intl: service('intl'),
globalState: service('global-state'),
init(...args) {
this._super(...args);
this._initProperties();
},
mergeTypes: ['smart', 'passive', 'force'],
syncTypes: ['smart', 'passive'],
syncType: computed('mappedSyncTypes.[]', function() {
return this.mappedSyncTypes[0];
}),
mergeType: computed('mappedMergeTypes.[]', function() {
return this.mappedMergeTypes[0];
}),
revisionValue: computed('revision', 'mappedRevisions.[]', function() {
return (
this.mappedRevisions.find(({value}) => value === this.revision) ||
this.mappedRevisions[0]
);
}),
mappedMergeTypes: computed('mergeTypes.[]', function() {
return this.mergeTypes.map(name => ({
label: name,
value: name
}));
}),
mappedSyncTypes: computed('syncTypes.[]', function() {
return this.syncTypes.map(name => ({
label: name,
value: name
}));
}),
mappedRevisions: computed('revisions.[]', function() {
return this.revisions.map(({id, language}) => ({
label: language.name,
value: id
}));
}),
revision: computed('revisions.[]', function() {
return this.revisions.find(revision => revision.isMaster);
}),
isMerge: equal('commitAction', 'merge'),
isSync: equal('commitAction', 'sync'),
documentFormatValue: computed(
'documentFormat',
'documentFormatOptions',
function() {
return this.documentFormatOptions.find(
({value}) => value === this.documentFormat
);
}
),
documentFormatOptions: computed('globalState.documentFormats', function() {
if (!this.globalState.documentFormats) return [];
return this.globalState.documentFormats.map(({slug, name}) => ({
value: slug,
label: name
}));
}),
existingDocumentPath: computed(
'documentPath',
'documents.[].path',
function() {
if (!this.documentPath) return false;
if (!this.documents) return false;
const path = this.documentPath.replace(/\..+/, '');
return this.documents.find(document => document.path === path);
}
),
actions: {
onSelectMergeType(mergeType) {
this.set('mergeType', mergeType);
},
onSelectSyncType(syncType) {
this.set('syncType', syncType);
},
onSelectRevision(revision) {
this.set(
'revision',
this.revisions.find(({id}) => id === revision.value)
);
this.set('revisionValue', revision);
},
commit() {
this._onCommiting();
this.onCommit(
this.getProperties(
'fileSource',
'documentPath',
'documentFormat',
'revision',
'mergeType',
'syncType'
)
)
.then(this._onCommitingDone.bind(this))
.catch(this._onCommitingError.bind(this));
},
peek() {
this._onPeeking();
this.onPeek(
this.getProperties(
'fileSource',
'documentPath',
'documentFormat',
'revision',
'mergeType',
'syncType'
)
)
.then(this._onPeekingDone.bind(this))
.catch(this._onPeekingError.bind(this));
},
fileChange(files) {
const fileSource = files[0];
const filename = fileSource.name.split('.');
const fileExtension = filename.pop();
const documentPath = filename.join('.');
const documentFormat = this._formatFromExtension(fileExtension);
const isFileReading = true;
const isFileRead = false;
const reader = new FileReader();
this.setProperties({
fileSource,
documentPath,
isFileReading,
isFileRead,
documentFormat
});
reader.onload = this._fileRead.bind(this);
reader.readAsText(files[0]);
},
fileCancel() {
this.onFileCancel();
this._initProperties();
}
},
_formatFromExtension(fileExtension) {
if (!this.globalState.documentFormats) return null;
const documentFormatItem = this.globalState.documentFormats.find(
({extension}) => extension === fileExtension
);
return documentFormatItem
? documentFormatItem.slug
: this.globalState.documentFormats[0].slug;
},
/**
* Called after a file is read.
*
* @private
* @method
* @param {ProgressEvent} result Native progress event containing the file raw content and infos
*/
_fileRead(file) {
const isFileReading = false;
const isFileRead = true;
this.setProperties({
isFileReading,
isFileRead,
file
});
this.send('peek');
},
_onCommiting() {
this.setProperties({
isCommiting: true,
isCommitingDone: false,
isCommitingError: false,
isPeekingError: false
});
},
_onCommitingDone() {
this.setProperties({isCommiting: false, isCommitingDone: true});
},
_onCommitingError() {
this.setProperties({isCommiting: false, isCommitingError: true});
},
_onPeeking() {
this.setProperties({
isPeeking: true,
isPeekingDone: false,
isPeekingError: false,
isCommitingError: false
});
},
_onPeekingDone() {
this.setProperties({isPeeking: false, isPeekingDone: true});
},
_onPeekingError() {
this.setProperties({isPeeking: false, isPeekingError: true});
},
_initProperties() {
this.setProperties(DEFAULT_PROPERTIES);
}
});

View File

@ -0,0 +1,328 @@
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {equal} from '@ember/object/computed';
import Component from '@glimmer/component';
import IntlService from 'ember-intl/services/intl';
import GlobalState from 'accent-webapp/services/global-state';
import {tracked} from '@glimmer/tracking';
const DEFAULT_PROPERTIES = {
isFileReading: false,
isFileRead: false,
isPeeking: false,
isPeekingDone: false,
isPeekingError: false,
isCommiting: false,
isCommitingDone: false,
isCommitingError: false,
file: null,
fileSource: null,
documentPath: null,
documentFormat: 'json',
};
interface Args {
permissions: Record<string, true>;
revisions: any;
documents: any;
canCommit: boolean;
commitAction: 'merge' | 'sync';
peekAction: 'peek_merge' | 'peek_sync';
commitButtonText: string;
onFileCancel: () => void;
onPeek: (options: {
fileSource: any;
documentPath: string | null;
documentFormat: any;
revision: any;
mergeType: string;
syncType: string;
}) => Promise<void>;
onCommit: (options: {
fileSource: any;
documentPath: string | null;
documentFormat: any;
revision: any;
mergeType: string;
syncType: string;
}) => Promise<void>;
}
export default class CommitFile extends Component<Args> {
@service('intl')
intl: IntlService;
@service('global-state')
globalState: GlobalState;
@equal('args.commitAction', 'merge')
isMerge: boolean;
@equal('args.commitAction', 'sync')
isSync: boolean;
mergeTypes = ['smart', 'passive', 'force'];
syncTypes = ['smart', 'passive'];
@tracked
isFileReading = DEFAULT_PROPERTIES.isFileReading;
@tracked
isFileRead = DEFAULT_PROPERTIES.isFileRead;
@tracked
isPeeking = DEFAULT_PROPERTIES.isPeeking;
@tracked
isPeekingDone = DEFAULT_PROPERTIES.isPeekingDone;
@tracked
isPeekingError = DEFAULT_PROPERTIES.isPeekingError;
@tracked
isCommiting = DEFAULT_PROPERTIES.isCommiting;
@tracked
isCommitingDone = DEFAULT_PROPERTIES.isCommitingDone;
@tracked
isCommitingError = DEFAULT_PROPERTIES.isCommitingError;
@tracked
file: ProgressEvent<FileReader> | null = DEFAULT_PROPERTIES.file;
@tracked
fileSource: File | null = DEFAULT_PROPERTIES.fileSource;
@tracked
documentPath: string | null = DEFAULT_PROPERTIES.documentPath;
@tracked
documentFormat: string | null = DEFAULT_PROPERTIES.documentFormat;
@tracked
mergeType = this.mappedMergeTypes[0];
@tracked
syncType = this.mappedSyncTypes[0];
@tracked
revisionValue =
this.mappedRevisions.find(({value}) => value === this.revision) ||
this.mappedRevisions[0];
@tracked
revision = this.args.revisions.find((revision: any) => revision.isMaster);
get mappedMergeTypes() {
return this.mergeTypes.map((name) => ({
label: name,
value: name,
}));
}
get mappedSyncTypes() {
return this.syncTypes.map((name) => ({
label: name,
value: name,
}));
}
get mappedRevisions(): Array<{label: string; value: string}> {
return this.args.revisions.map(
({id, language}: {id: string; language: {name: string}}) => ({
label: language.name,
value: id,
})
);
}
get documentFormatValue() {
return this.documentFormatOptions.find(({value}) => {
return value === this.documentFormat;
});
}
get documentFormatOptions(): Array<{value: string; label: string}> {
if (!this.globalState.documentFormats) return [];
return this.globalState.documentFormats.map(({slug, name}) => ({
value: slug,
label: name,
}));
}
get existingDocumentPath() {
if (!this.documentPath) return false;
if (!this.args.documents) return false;
const path = this.documentPath.replace(/\..+/, '');
return this.args.documents.find((document: any) => document.path === path);
}
@action
onSelectMergeType(mergeType: {label: string; value: string}) {
this.mergeType = mergeType;
}
@action
onSelectSyncType(syncType: {label: string; value: string}) {
this.syncType = syncType;
}
@action
onSelectRevision(revision: {label: string; value: string}) {
this.revision = this.args.revisions.find(
({id}: {id: string}) => id === revision.value
);
this.revisionValue = revision;
}
@action
onSelectDocumentFormat(documentFormat: {label: string; value: string}) {
this.documentFormat = documentFormat.value;
}
@action
async commit() {
this.onCommiting();
try {
await this.args.onCommit({
fileSource: this.fileSource,
documentPath: this.documentPath,
documentFormat: this.documentFormat,
revision: this.revision,
mergeType: this.mergeType.value,
syncType: this.syncType.value,
});
this.onCommitingDone();
} catch (error) {
this.onCommitingError();
}
}
@action
async peek() {
this.onPeeking();
try {
await this.args.onPeek({
fileSource: this.fileSource,
documentPath: this.documentPath,
documentFormat: this.documentFormat,
revision: this.revision,
mergeType: this.mergeType.value,
syncType: this.syncType.value,
});
this.onPeekingDone();
} catch (error) {
this.onPeekingError();
}
}
@action
fileChange(files: File[]) {
const fileSource = files[0];
const filename = fileSource.name.split('.');
const fileExtension = filename.pop();
const documentPath = filename.join('.');
const documentFormat = this.formatFromExtension(fileExtension);
const isFileReading = true;
const isFileRead = false;
const reader = new FileReader();
this.fileSource = fileSource;
this.documentPath = documentPath;
this.isFileReading = isFileReading;
this.isFileRead = isFileRead;
this.documentFormat = documentFormat;
reader.onload = this.fileRead.bind(this);
reader.readAsText(files[0]);
}
@action
fileCancel() {
this.args.onFileCancel();
this.initProperties();
}
private formatFromExtension(fileExtension?: string) {
if (!this.globalState.documentFormats) return null;
const documentFormatItem = this.globalState.documentFormats.find(
({extension}) => {
return extension === fileExtension;
}
);
return documentFormatItem
? documentFormatItem.slug
: this.globalState.documentFormats[0].slug;
}
private async fileRead(event: ProgressEvent<FileReader>) {
this.isFileReading = false;
this.isFileRead = true;
this.file = event;
await this.peek();
}
private onCommiting() {
this.isCommiting = true;
this.isCommitingDone = false;
this.isCommitingError = false;
this.isPeekingError = false;
}
private onCommitingDone() {
this.isCommiting = false;
this.isCommitingDone = true;
}
private onCommitingError() {
this.isCommiting = false;
this.isCommitingError = true;
}
private onPeeking() {
this.isPeeking = true;
this.isPeekingDone = false;
this.isPeekingError = false;
this.isCommitingError = false;
}
private onPeekingDone() {
this.isPeeking = false;
this.isPeekingDone = true;
}
private onPeekingError() {
this.isPeeking = false;
this.isPeekingError = true;
}
private initProperties() {
this.isFileReading = DEFAULT_PROPERTIES.isFileReading;
this.isFileRead = DEFAULT_PROPERTIES.isFileRead;
this.isPeeking = DEFAULT_PROPERTIES.isPeeking;
this.isPeekingDone = DEFAULT_PROPERTIES.isPeekingDone;
this.isPeekingError = DEFAULT_PROPERTIES.isPeekingError;
this.isCommiting = DEFAULT_PROPERTIES.isCommiting;
this.isCommitingDone = DEFAULT_PROPERTIES.isCommitingDone;
this.isCommitingError = DEFAULT_PROPERTIES.isCommitingError;
this.file = DEFAULT_PROPERTIES.file;
this.fileSource = DEFAULT_PROPERTIES.fileSource;
this.documentPath = DEFAULT_PROPERTIES.documentPath;
this.documentFormat = DEFAULT_PROPERTIES.documentFormat;
}
}

View File

@ -1,15 +1,32 @@
.separator {
display: block;
margin: 10px 0;
background: #eee;
height: 1px;
width: 100%;
@value color-grey, color-green, color-black, color-error from 'accent-webapp/styles/variables/colors';
@value font-monospace from 'accent-webapp/styles/variables/fonts';
.commit-file {
:global(.textInput) {
@extend %textInput;
margin-bottom: 8px;
padding: 5px;
outline: 0;
border: 1px solid lighten(color-grey, 20%);
box-shadow: inset 0 2px 6px rgba(color-black, 0.05);
background: #fff;
font-family: font-monospace;
font-size: 12px;
}
:global(.ember-power-select-trigger) {
padding: 6px 10px;
border: 1px solid rgba(color-black, 0.1);
color: rgba(color-black, 0.8);
color: var(--color-black-opacity-70);
background: #fafafa;
}
}
.textHelper {
margin-bottom: 3px;
width: 80%;
color: $color-grey;
color: color-grey;
font-size: 12px;
}
@ -18,30 +35,18 @@
margin-bottom: 7px;
padding: 2px 4px 3px;
border-radius: 4px;
background: lighten($color-green, 48%);
background: lighten(color-green, 48%);
background: var(--color-primary-opacity-10);
color: $color-green;
color: color-green;
color: var(--color-primary);
font-size: 11px;
&.documentHelper--new {
background: lighten($color-green, 48%);
color: $color-green;
background: lighten(color-green, 48%);
color: color-green;
}
}
.textInput {
@extend %textInput;
margin-bottom: 8px;
padding: 5px;
outline: 0;
border: 1px solid lighten($color-grey, 20%);
box-shadow: inset 0 2px 6px rgba($color-black, 0.05);
background: #fff;
font-family: $font-monospace;
font-size: 12px;
}
.options {
display: flex;
padding: 0;
@ -84,7 +89,7 @@
width: 15px;
height: 15px;
margin-right: 10px;
stroke: $color-black;
stroke: color-black;
stroke: var(--color-black);
}
@ -92,7 +97,7 @@
display: flex;
align-items: center;
margin: 0 0 6px;
color: $color-black;
color: color-black;
color: var(--color-black);
}
@ -101,16 +106,16 @@
margin: 0 0 10px;
font-size: 14px;
font-weight: 300;
color: rgba($color-black, 0.7);
color: rgba(color-black, 0.7);
color: var(--color-black-opacity-70);
}
.fileButton {
margin-top: 7px;
padding: 9px 30px;
margin-top: 7px !important;
padding: 9px 30px !important;
.button-icon {
margin-right: 10px;
margin-right: 10px !important;
}
}
@ -134,7 +139,7 @@
margin-bottom: 5px;
font-size: 13px;
font-weight: bold;
color: $color-black;
color: color-black;
color: var(--color-black);
}
@ -142,13 +147,13 @@
.instructions-text {
margin-bottom: 18px;
font-size: 13px;
color: rgba($color-black, 0.5);
color: rgba(color-black, 0.5);
color: var(--color-black);
em {
font-style: normal;
font-weight: bold;
color: rgba($color-green, 0.8);
color: rgba(color-green, 0.8);
color: var(--color-primary);
}
}
@ -164,12 +169,12 @@
}
.peekButton {
margin-top: 7px;
margin-top: 7px !important;
}
.errorMessage {
margin: 10px 0;
color: $color-error;
color: color-error;
font-size: 13px;
font-weight: bold;
}
@ -178,14 +183,6 @@
@extend %textInput;
padding: 6px 10px;
font-size: 12px;
font-family: $font-monospace;
font-family: font-monospace;
color: #444;
}
.ember-power-select-trigger {
padding: 6px 10px;
border: 1px solid rgba($color-black, 0.1);
color: rgba($color-black, 0.8);
color: var(--color-black-opacity-70);
background: #fafafa;
}

View File

@ -1,185 +1,185 @@
<div>
{{#if isPeekingError}}
<div class="errorMessage">
<div local-class="commit-file">
{{#if this.isPeekingError}}
<div local-class="errorMessage">
{{t "components.commit_file.peek_error"}}
</div>
{{/if}}
{{#if isCommitingError}}
<div class="errorMessage">
{{#if this.isCommitingError}}
<div local-class="errorMessage">
{{t "components.commit_file.commit_error"}}
</div>
{{/if}}
{{#if isMerge}}
{{#if file}}
<div class="options">
<div class="option option--borderless">
<p class="textHelper">
{{#if this.isMerge}}
{{#if this.file}}
<div local-class="options">
<div local-class="option option--borderless">
<p local-class="textHelper">
{{t "components.commit_file.language"}}
:
</p>
<AccSelect
@searchEnabled={{false}}
@selected={{revisionValue}}
@options={{mappedRevisions}}
@onchange={{action "onSelectRevision"}}
@selected={{this.revisionValue}}
@options={{this.mappedRevisions}}
@onchange={{fn this.onSelectRevision}}
/>
</div>
<div class="option option--borderless">
<p class="textHelper">
<div local-class="option option--borderless">
<p local-class="textHelper">
{{t "components.commit_file.commit_type"}}
:
</p>
<AccSelect
@searchEnabled={{false}}
@selected={{mergeType}}
@options={{mappedMergeTypes}}
@onchange={{action "onSelectMergeType"}}
@selected={{this.mergeType}}
@options={{this.mappedMergeTypes}}
@onchange={{fn this.onSelectMergeType}}
/>
</div>
</div>
{{/if}}
{{/if}}
{{#if isSync}}
{{#if file}}
<div class="options">
<div class="option option--borderless">
<p class="textHelper">
{{#if this.isSync}}
{{#if this.file}}
<div local-class="options">
<div local-class="option option--borderless">
<p local-class="textHelper">
{{t "components.commit_file.commit_type"}}
:
</p>
<AccSelect
@searchEnabled={{false}}
@selected={{syncType}}
@options={{mappedSyncTypes}}
@onchange={{action "onSelectSyncType"}}
@selected={{this.syncType}}
@options={{this.mappedSyncTypes}}
@onchange={{fn this.onSelectSyncType}}
/>
</div>
<div class="option option--borderless"></div>
<div local-class="option option--borderless"></div>
</div>
{{/if}}
{{/if}}
{{#if file}}
<div>
{{#unless document}}
<div class="option">
<p class="textHelper">
{{#unless this.documents}}
<div local-class="option">
<p local-class="textHelper">
{{t "components.commit_file.file_source"}}
</p>
<p>
{{#if existingDocumentPath}}
<span class="documentHelper">
{{#if this.existingDocumentPath}}
<span local-class="documentHelper">
{{t "components.commit_file.existing_document_warning"}}
</span>
{{else}}
<span class="documentHelper documentHelper--new">
<span local-class="documentHelper documentHelper--new">
{{t "components.commit_file.new_document_warning"}}
</span>
{{/if}}
</p>
<Input @value={{documentPath}} class="fileSourceName" />
<Input @value={{this.documentPath}} local-class="fileSourceName" />
</div>
{{/unless}}
<div class="option">
<p class="textHelper">
<div local-class="option">
<p local-class="textHelper">
{{t "components.commit_file.document_format"}}
</p>
<AccSelect
@searchEnabled={{false}}
@selected={{documentFormatValue}}
@options={{documentFormatOptions}}
@onchange={{action (mut documentFormat) value="value"}}
@selected={{this.documentFormatValue}}
@options={{this.documentFormatOptions}}
@onchange={{fn this.onSelectDocumentFormat}}
/>
</div>
{{#if (get permissions peekAction)}}
<div class="option">
<p class="textHelper">
{{#if (get @permissions @peekAction)}}
<div local-class="option">
<p local-class="textHelper">
{{t "components.commit_file.peek_help"}}
</p>
<AsyncButton
@onClick={{action "peek"}}
@loading={{isPeeking}}
class="button
button--filled button--blue peekButton"
@onClick={{fn this.peek}}
@loading={{this.isPeeking}}
class="button button--filled button--blue"
local-class="peekButton"
>
{{t "components.commit_file.peek_button"}}
</AsyncButton>
</div>
{{/if}}
{{#if (get permissions commitAction)}}
<div class="actions">
{{#if (get @permissions @commitAction)}}
<div local-class="actions">
<AsyncButton
@onClick={{action "fileCancel"}}
@onClick={{fn this.fileCancel}}
class="button button--filled button--white"
>
{{t "components.commit_file.cancel_button"}}
</AsyncButton>
{{#if canCommit}}
{{#if @canCommit}}
<AsyncButton
@onClick={{action "commit"}}
@loading={{isCommiting}}
@onClick={{fn this.commit}}
@loading={{this.isCommiting}}
class="button button--filled"
>
{{commitButtonText}}
{{@commitButtonText}}
</AsyncButton>
{{else}}
<AsyncButton class="button button--filled button--disabled">
{{commitButtonText}}
{{@commitButtonText}}
</AsyncButton>
{{/if}}
</div>
{{/if}}
</div>
{{else}}
<div class="emptyFile">
<div class="emptyFile-upload">
<div local-class="emptyFile">
<div local-class="emptyFile-upload">
<FileInput
@name="file-input"
@id="file-input"
@onChange={{action "fileChange"}}
class="fileInput"
name="file-input"
id="file-input"
@onChange={{this.fileChange}}
local-class="fileInput"
/>
<strong class="fileInputTitle">
{{inline-svg "/assets/folder.svg" class="fileInputIcon"}}
<strong local-class="fileInputTitle">
{{inline-svg "/assets/folder.svg" local-class="fileInputIcon"}}
{{t "components.commit_file.upload_title"}}
</strong>
<p class="fileInputHelper">
<p local-class="fileInputHelper">
{{t "components.commit_file.upload_help"}}
</p>
<label for="file-input" class="button button--filled fileButton">
{{inline-svg "/assets/import.svg" class="button-icon"}}
<label for="file-input" class="button button--filled" local-class="fileButton">
{{inline-svg "/assets/import.svg" class="button-icon" local-class="button-icon"}}
{{t "components.commit_file.file_input_button"}}
</label>
</div>
<div>
<a class="instructions-title" rel="noopener noreferrer" href="https://www.accent.reviews/guides/glossary.html#sync" target="_blank">
<a local-class="instructions-title" rel="noopener noreferrer" href="https://www.accent.reviews/guides/glossary.html#sync" target="_blank">
{{t "components.commit_file.instructions.sync.title"}}
</a>
<p class="instructions-text">
<p local-class="instructions-text">
{{{t "components.commit_file.instructions.sync.text"}}}
</p>
<a class="instructions-title" rel="noopener noreferrer" href="https://www.accent.reviews/guides/glossary.html#add-translations" target="_blank">
<a local-class="instructions-title" rel="noopener noreferrer" href="https://www.accent.reviews/guides/glossary.html#add-translations" target="_blank">
{{t "components.commit_file.instructions.merge.title"}}
</a>
<p class="instructions-text">
<p local-class="instructions-text">
{{{t "components.commit_file.instructions.merge.text"}}}
</p>
<h3 class="instructions-title">
<h3 local-class="instructions-title">
{{t "components.commit_file.instructions.mistakes.title"}}
</h3>
<ul class="instructions-list">
<li class="instructions-list-item">{{t "components.commit_file.instructions.mistakes.item_1"}}</li>
<li class="instructions-list-item">{{t "components.commit_file.instructions.mistakes.item_2"}}</li>
<li class="instructions-list-item">{{t "components.commit_file.instructions.mistakes.item_3"}}</li>
<ul local-class="instructions-list">
<li local-class="instructions-list-item">{{t "components.commit_file.instructions.mistakes.item_1"}}</li>
<li local-class="instructions-list-item">{{t "components.commit_file.instructions.mistakes.item_2"}}</li>
<li local-class="instructions-list-item">{{t "components.commit_file.instructions.mistakes.item_3"}}</li>
</ul>
</div>
</div>

View File

@ -1,65 +0,0 @@
import {computed} from '@ember/object';
import {empty, reads} from '@ember/object/computed';
import Component from '@ember/component';
import parsedKeyProperty from 'accent-webapp/computed-macros/parsed-key';
// Attributes:
// project: Object <project>
// permissions: Ember Object containing <permission>
// conflict: Object <conflict>
// revision: Object <revision> (optional)
// onCorrect: Function
export default Component.extend({
classNameBindings: ['active', 'resolved', 'error:errored', 'fullscreen'],
emptyPreviousText: empty('conflict.conflictedText'),
textInput: reads('conflict.correctedText'),
samePreviousText: computed(
'conflict.{conflictedText,correctedText}',
function() {
return this.conflict.conflictedText === this.conflict.correctedText;
}
),
loading: false,
error: false,
resolved: false,
active: false,
conflictKey: parsedKeyProperty('conflict.key'),
actions: {
correct() {
this._onLoading();
this.onCorrect(this.conflict, this.textInput)
.then(this._onCorrectSuccess.bind(this))
.catch(this._onError.bind(this));
},
inputBlur() {
this.set('active', false);
},
inputFocus() {
this.set('active', true);
}
},
_onLoading() {
this.setProperties({error: false, loading: true});
},
_onError() {
this.setProperties({error: true, loading: false});
},
_onCorrectSuccess() {
this.setProperties({resolved: true, loading: false});
},
_onUncorrectSuccess() {
this.setProperties({resolved: false, loading: false});
}
});

View File

@ -0,0 +1,80 @@
import {action} from '@ember/object';
import {empty} from '@ember/object/computed';
import Component from '@glimmer/component';
import parsedKeyProperty from 'accent-webapp/computed-macros/parsed-key';
import {tracked} from '@glimmer/tracking';
interface Args {
fullscreen: boolean;
revision: any;
permissions: Record<string, true>;
project: any;
conflict: any;
onCorrect: (conflict: any, textInput: string) => Promise<void>;
}
export default class ConflictItem extends Component<Args> {
@empty('args.conflict.conflictedText')
emptyPreviousText: boolean;
@tracked
textInput = this.args.conflict.correctedText;
@tracked
loading = false;
@tracked
error = false;
@tracked
resolved = false;
@tracked
active = false;
conflictKey = parsedKeyProperty(this.args.conflict.key);
get samePreviousText() {
return (
this.args.conflict.conflictedText === this.args.conflict.correctedText
);
}
@action
async correct() {
this.onLoading();
try {
await this.args.onCorrect(this.args.conflict, this.textInput);
this.onCorrectSuccess();
} catch (error) {
this.onError();
}
}
@action
inputBlur() {
this.active = false;
}
@action
inputFocus() {
this.active = true;
}
private onLoading() {
this.error = false;
this.loading = true;
}
private onError() {
this.error = true;
this.loading = false;
}
private onCorrectSuccess() {
this.resolved = true;
this.loading = false;
}
}

Some files were not shown because too many files have changed in this diff Show More