Set `tabWidth: 2` in Prettier config (#2438)

* chore: Update Prettier config, setting tabWidth:2

* style: Apply updated Prettier styles

If you need to rebase work past this style change, do as follows:

0. Consider this to be commit `commitA`, replacing that with its id in the following.
1. To make sure mistakes aren't fatal, assign a second branch to your current work.
2. Rebase your branch on the commit immediately before this one, commitA~
3. Run the following command at the root of the repo:

    git rebase --strategy-option=theirs \
      --exec 'npx prettier --write . && git add -u && git commit --amend --no-edit' \
      commitA

That will take a short while esp. if you have multiple commits,
as it runs Prettier on everything for every commit.
If you've deleted files, the rebase may drop down to interactive mode
and have you `git rm` as appropriate, then `git rebase --continue`.

You should end up with just your changes in your branch,
prettily formatted. To validate that,
apply the same Prettier config change to your original branch,
reformat the files with `npm run prettier`,
and then compare the results with the rebased branch.

* chore: Clean up lint configs
This commit is contained in:
Eemeli Aro 2022-03-03 02:46:35 -06:00 коммит произвёл GitHub
Родитель 9fc9430cc7
Коммит 9faea69c20
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
528 изменённых файлов: 30406 добавлений и 31367 удалений

Просмотреть файл

@ -1,16 +1,23 @@
**/*.bundle.js
**/*.js.map
**/dist/**
**/build/**
vendor/**
coverage/**
static/*
**/*.min.js
**/js/lib/**/*.js
**/app/error_pages/**/*.js
**/*blockrain*js
assets/*
**/node_modules/**
docs/
.vscode/
tag-admin/dist/
translate/dist/
coverage/
docs/_build/
docs/venv/
package-lock.json
specs/
# Jinja templates
pontoon/base/templates/js/pontoon.js
translate/public/translate.html
**/templates/**/*.html
# Vendored code
error_pages/css/blockrain.css
error_pages/js/
pontoon/base/static/css/boilerplate.css
pontoon/base/static/css/fontawesome-all.css
pontoon/base/static/css/jquery-ui.css
pontoon/base/static/css/nprogress.css
pontoon/base/static/js/lib/
pontoon/in_context/static/

Просмотреть файл

@ -1,78 +1,78 @@
/* eslint-env node */
module.exports = {
"extends": [
"eslint:recommended",
"plugin:react/recommended"
extends: ['eslint:recommended', 'plugin:react/recommended'],
env: {
es6: true,
browser: true,
jest: true,
},
parser: '@babel/eslint-parser',
parserOptions: {
ecmaVersion: 2017,
ecmaFeatures: {
jsx: true,
experimentalObjectRestSpread: true,
},
sourceType: 'module',
babelOptions: {
presets: ['@babel/preset-react'],
},
requireConfigFile: false,
},
globals: {
gettext: false,
ngettext: false,
interpolate: false,
l: false,
expect: false,
test: false,
browser: false,
jest: false,
Promise: false,
Set: false,
URLSearchParameters: false,
FormData: false,
require: false,
shortcut: false,
sorttable: false,
$: false,
Pontoon: false,
jQuery: false,
Clipboard: false,
Chart: false,
NProgress: false,
diff_match_patch: false,
Highcharts: false,
Sideshow: false,
editor: false,
DIFF_INSERT: false,
DIFF_EQUAL: false,
DIFF_DELETE: false,
ga: false,
process: false,
generalShortcutsHandler: true,
traversalShortcutsHandler: true,
editorShortcutsHandler: true,
},
plugins: ['react'],
rules: {
'react/display-name': 0,
'react/prefer-es6-class': 1,
'react/prefer-stateless-function': 0,
'react/prop-types': 0,
'react/jsx-key': 0,
'react/jsx-uses-react': 1,
'react/jsx-uses-vars': 1,
'no-unused-vars': [
'error',
{ vars: 'all', args: 'after-used', ignoreRestSiblings: true },
],
env: {
es6: true,
browser: true,
jest: true,
'no-console': 1,
},
settings: {
react: {
version: 'detect',
},
parser: "@babel/eslint-parser",
parserOptions: {
ecmaVersion: 2017,
ecmaFeatures: {
jsx: true,
experimentalObjectRestSpread: true
},
sourceType: 'module',
babelOptions: {
presets: ['@babel/preset-react'],
},
requireConfigFile: false,
},
globals: {
gettext: false,
ngettext: false,
interpolate: false,
l: false,
expect: false,
test: false,
browser: false,
jest: false,
Promise: false,
Set: false,
URLSearchParameters: false,
FormData: false,
require: false,
shortcut: false,
sorttable: false,
$: false,
Pontoon: false,
jQuery: false,
Clipboard: false,
Chart: false,
NProgress: false,
diff_match_patch: false,
Highcharts: false,
Sideshow: false,
editor: false,
DIFF_INSERT: false,
DIFF_EQUAL: false,
DIFF_DELETE: false,
ga: false,
process: false,
generalShortcutsHandler: true,
traversalShortcutsHandler: true,
editorShortcutsHandler: true,
},
plugins: [
'react',
],
rules: {
'react/display-name': 0,
'react/prefer-es6-class': 1,
'react/prefer-stateless-function': 0,
"react/prop-types": 0,
"react/jsx-key": 0,
"react/jsx-uses-react": 1,
'react/jsx-uses-vars': 1,
"no-unused-vars": ["error", { "vars": "all", "args": "after-used", "ignoreRestSiblings": true }],
"no-console": 1,
},
settings: {
'react': {
'version': 'detect'
}
}
},
};

42
.github/actions/check-tsc/index.js поставляемый
Просмотреть файл

@ -3,27 +3,27 @@ const { promisify } = require('util');
const { exec } = require('child_process');
async function run() {
console.log('::group::tsc');
let errors = '0';
const run = process.env['INPUT_RUN'] || 'npm run types --pretty ';
const cwd = process.env['INPUT_WORKING-DIRECTORY'] || 'translate';
let stdout, stderr;
try {
({ stdout, stderr } = await asyncExec(run, {
cwd,
}));
} catch (failed_proc) {
({ stdout, stderr } = failed_proc);
}
console.log(stdout);
console.log(stderr);
const m = /Found ([0-9]+) errors\./.exec(stdout);
if (m) {
errors = m[1];
}
console.log('::endgroup::');
console.log(`\nFound ${errors} errors.\n`);
console.log(`::set-output name=errors::${errors}`);
console.log('::group::tsc');
let errors = '0';
const run = process.env['INPUT_RUN'] || 'npm run types --pretty ';
const cwd = process.env['INPUT_WORKING-DIRECTORY'] || 'translate';
let stdout, stderr;
try {
({ stdout, stderr } = await asyncExec(run, {
cwd,
}));
} catch (failed_proc) {
({ stdout, stderr } = failed_proc);
}
console.log(stdout);
console.log(stderr);
const m = /Found ([0-9]+) errors\./.exec(stdout);
if (m) {
errors = m[1];
}
console.log('::endgroup::');
console.log(`\nFound ${errors} errors.\n`);
console.log(`::set-output name=errors::${errors}`);
}
const asyncExec = promisify(exec);

Просмотреть файл

@ -1,21 +1,23 @@
# minified code
*.min.js
*.min.css
translate/dist
tag-admin/dist
.vscode/
tag-admin/dist/
translate/dist/
coverage/
docs/_build/
docs/venv/
package-lock.json
specs/
# libraries
**/base/static/js/lib*
**/base/static/css/boilerplate.css
**/base/static/css/fontawesome-all.css
**/base/static/css/jquery-ui.css
**/base/static/css/nprogress.css
**/base/templates/js/pontoon.js
**/in_context/static/css/agency.css
**/in_context/static/js/agency.js
# Jinja templates
pontoon/base/templates/js/pontoon.js
translate/public/translate.html
**/templates/**/*.html
# Prevent VSCode to reformat these files if "Format On Save" enabled
*.html
*.yml
**/package.json*
# Vendored code
error_pages/css/blockrain.css
error_pages/js/
pontoon/base/static/css/boilerplate.css
pontoon/base/static/css/fontawesome-all.css
pontoon/base/static/css/jquery-ui.css
pontoon/base/static/css/nprogress.css
pontoon/base/static/js/lib/
pontoon/in_context/static/

Просмотреть файл

@ -1,4 +1,4 @@
schedule: "every week on monday"
schedule: 'every week on monday'
search: False
update: insecure
requirements:

Просмотреть файл

@ -1,10 +1,11 @@
# Community Participation Guidelines
This repository is governed by Mozilla's code of conduct and etiquette guidelines.
This repository is governed by Mozilla's code of conduct and etiquette guidelines.
For more details, please read the
[Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/).
[Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/).
## How to Report
For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page.
<!--

Просмотреть файл

@ -122,13 +122,13 @@ pyupgrade:
"${DC}" run --rm server pyupgrade --exit-zero-even-if-changed --py38-plus *.py `find pontoon -name \*.py`
check-pyupgrade:
"${DC}" run --rm webapp pyupgrade --py38-plus *.py `find pontoon -name \*.py`
"${DC}" run --rm server pyupgrade --py38-plus *.py `find pontoon -name \*.py`
black:
"${DC}" run --rm server black pontoon/
check-black:
"${DC}" run --rm webapp black --check pontoon
"${DC}" run --rm server black --check pontoon
dropdb:
"${DC}" down --volumes postgresql

Просмотреть файл

@ -7,7 +7,6 @@ uses version-control systems for storing translations.
[📚 **Documentation**](https://mozilla-pontoon.readthedocs.io/)
## Installing Pontoon
If you are looking to host your own instance of Pontoon, there are several ways to do so.
@ -27,7 +26,6 @@ testing for example, see our
[Developer Setup using Docker](https://mozilla-pontoon.readthedocs.io/en/latest/dev/setup.html).
Please note that you should **not** deploy a production instance with Docker.
## Contributing to Pontoon
Do you want to help us make Pontoon better? We are very glad!
@ -39,14 +37,14 @@ database, run tests, and send your contribution.
If you want to go further, you can:
* Check out development roadmap on the [wiki](https://wiki.mozilla.org/Pontoon)
* Report an [issue](https://github.com/mozilla/pontoon/issues/new)
* Check [existing issues](https://github.com/mozilla/pontoon/issues)
* See Mozilla's Pontoon servers:
* [Staging](https://mozilla-pontoon-staging.herokuapp.com/)
* [Production](https://pontoon.mozilla.org/)
* For discussing Pontoon's development, get in touch with us on [chat.mozilla.org](https://chat.mozilla.org/#/room/#pontoon:mozilla.org)
* For feedback, support, and 3rd party deployments, check out [Discourse](https://discourse.mozilla.org/c/pontoon/)
- Check out development roadmap on the [wiki](https://wiki.mozilla.org/Pontoon)
- Report an [issue](https://github.com/mozilla/pontoon/issues/new)
- Check [existing issues](https://github.com/mozilla/pontoon/issues)
- See Mozilla's Pontoon servers:
- [Staging](https://mozilla-pontoon-staging.herokuapp.com/)
- [Production](https://pontoon.mozilla.org/)
- For discussing Pontoon's development, get in touch with us on [chat.mozilla.org](https://chat.mozilla.org/#/room/#pontoon:mozilla.org)
- For feedback, support, and 3rd party deployments, check out [Discourse](https://discourse.mozilla.org/c/pontoon/)
## License
@ -54,11 +52,10 @@ This software is licensed under the
[New BSD License](https://creativecommons.org/licenses/BSD/). For more
information, read [LICENSE](https://github.com/mozilla/pontoon/blob/master/LICENSE).
## Screenshots
![](docs/img/screenshots/teams-dashboard.png)
*Teams dashboard*
_Teams dashboard_
![](docs/img/screenshots/translation-app.png)
*Translation app*
_Translation app_

143
app.json
Просмотреть файл

@ -1,45 +1,52 @@
{
"name": "pontoon",
"description": "In-place localization tool.",
"keywords": ["l10n", "localization", "mozilla", "collaboration", "python", "django"],
"website": "https://pontoon.mozilla.org",
"logo": "https://pontoon.mozilla.org/static/img/logo.svg",
"success_url": "/",
"image": "heroku/python",
"addons": [
{
"plan": "newrelic:wayne"
},
{
"plan": "raygun:cr-free"
},
{
"plan": "papertrail:choklad"
},
{
"plan": "cloudamqp",
"as": "RABBITMQ"
},
{
"plan": "memcachier",
"as": "MEMCACHE"
},
{
"plan": "heroku-postgresql",
"options": {
"version": "11"
}
}
],
"env": {
"SITE_URL": {
"description": "Base URL of the site. Has to be https://{app-name}.herokuapp.com.",
"required": true
},
"ADMIN_EMAIL": {
"value": "pontoon@example.com",
"description": "Email address for the ``ADMINS`` setting."
},
"name": "pontoon",
"description": "In-place localization tool.",
"keywords": [
"l10n",
"localization",
"mozilla",
"collaboration",
"python",
"django"
],
"website": "https://pontoon.mozilla.org",
"logo": "https://pontoon.mozilla.org/static/img/logo.svg",
"success_url": "/",
"image": "heroku/python",
"addons": [
{
"plan": "newrelic:wayne"
},
{
"plan": "raygun:cr-free"
},
{
"plan": "papertrail:choklad"
},
{
"plan": "cloudamqp",
"as": "RABBITMQ"
},
{
"plan": "memcachier",
"as": "MEMCACHE"
},
{
"plan": "heroku-postgresql",
"options": {
"version": "11"
}
}
],
"env": {
"SITE_URL": {
"description": "Base URL of the site. Has to be https://{app-name}.herokuapp.com.",
"required": true
},
"ADMIN_EMAIL": {
"value": "pontoon@example.com",
"description": "Email address for the ``ADMINS`` setting."
},
"ADMIN_NAME": {
"value": "pontoon-admin",
"description": "Name for the ``ADMINS`` setting."
@ -93,7 +100,7 @@
"SVN_LD_LIBRARY_PATH": {
"description": "Path to prepend to ``LD_LIBRARY_PATH`` when running SVN. This is necessary on Heroku because the Python buildpack alters the path in a way that breaks the built-in SVN command.",
"value": "/usr/lib/x86_64-linux-gnu/"
},
},
"TZ": {
"description": "Timezone for the dynos that will run the app. Pontoon operates in UTC",
"value": "UTC"
@ -114,30 +121,30 @@
"description": "Optional. Default committer's email used when committing translations to version control system.",
"value": "pontoon@example.com"
}
},
"buildpacks": [
{
"url": "https://github.com/dmathieu/heroku-buildpack-submodules#0caf30af7737bf1bc32b7aafc009f19af3e603c1"
},
{
"url": "https://github.com/Osmose/heroku-buildpack-ssh"
},
{
"url": "https://github.com/mozilla/heroku-buildpack-apt.git#v0.1"
},
{
"url": "heroku/nodejs"
},
{
"url": "heroku/python"
}
],
"scripts": {
"postdeploy": "./bin/heroku_postdeploy && ./manage.py heroku_deploy_setup"
},
"formation": {
"worker": {
"quantity": 1
}
}
},
"buildpacks": [
{
"url": "https://github.com/dmathieu/heroku-buildpack-submodules#0caf30af7737bf1bc32b7aafc009f19af3e603c1"
},
{
"url": "https://github.com/Osmose/heroku-buildpack-ssh"
},
{
"url": "https://github.com/mozilla/heroku-buildpack-apt.git#v0.1"
},
{
"url": "heroku/nodejs"
},
{
"url": "heroku/python"
}
],
"scripts": {
"postdeploy": "./bin/heroku_postdeploy && ./manage.py heroku_deploy_setup"
},
"formation": {
"worker": {
"quantity": 1
}
}
}

Просмотреть файл

@ -1,9 +1,4 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": [
"@babel/plugin-transform-runtime"
]
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": ["@babel/plugin-transform-runtime"]
}

Просмотреть файл

@ -1,37 +1,34 @@
{
"name": "Pontoon",
"description": "Pontoon is a translation management system used and developed by the Mozilla localization community.",
"repository": {
"url": "https://github.com/mozilla/pontoon",
"license": "BSD-2-Clause"
},
"participate": {
"home": "https://wiki.mozilla.org/Webdev/GetInvolved/pontoon.mozilla.org",
"docs": "https://mozilla-pontoon.readthedocs.io/",
"chat": "https://chat.mozilla.org/#/room/#pontoon:mozilla.org",
"chat-contacts": [
"mathjazz",
"eemeli"
],
"mailing-list": "https://discourse.mozilla.org/c/pontoon"
},
"bugs": {
"list": "https://github.com/mozilla/pontoon/issues",
"report": "https://github.com/mozilla/pontoon/issues/new",
"mentored": "https://github.com/mozilla/pontoon/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22"
},
"urls": {
"prod": "https://pontoon.mozilla.org",
"dev": "https://mozilla-pontoon-staging.herokuapp.com"
},
"keywords": [
"python",
"django",
"html5",
"react",
"jquery",
"javascript",
"css",
"postgres"
]
"name": "Pontoon",
"description": "Pontoon is a translation management system used and developed by the Mozilla localization community.",
"repository": {
"url": "https://github.com/mozilla/pontoon",
"license": "BSD-2-Clause"
},
"participate": {
"home": "https://wiki.mozilla.org/Webdev/GetInvolved/pontoon.mozilla.org",
"docs": "https://mozilla-pontoon.readthedocs.io/",
"chat": "https://chat.mozilla.org/#/room/#pontoon:mozilla.org",
"chat-contacts": ["mathjazz", "eemeli"],
"mailing-list": "https://discourse.mozilla.org/c/pontoon"
},
"bugs": {
"list": "https://github.com/mozilla/pontoon/issues",
"report": "https://github.com/mozilla/pontoon/issues/new",
"mentored": "https://github.com/mozilla/pontoon/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22"
},
"urls": {
"prod": "https://pontoon.mozilla.org",
"dev": "https://mozilla-pontoon-staging.herokuapp.com"
},
"keywords": [
"python",
"django",
"html5",
"react",
"jquery",
"javascript",
"css",
"postgres"
]
}

Просмотреть файл

@ -1,7 +1,7 @@
# docker-compose for Pontoon development.
#
# Note: Requires docker-compose 1.10+.
version: "2.3"
version: '2.3'
services:
server:
build:
@ -14,7 +14,7 @@ services:
depends_on:
- postgresql
ports:
- "8000:8000"
- '8000:8000'
volumes:
- ./pontoon:/app/pontoon
- ./requirements:/app/requirements

Просмотреть файл

@ -32,11 +32,11 @@ data:
worker_processes 3;
error_log /var/log/nginx/error.log warn;
pid /tmp/nginx.pid;
events {
worker_connections 1024;
}
http {
server {
listen 8080 ssl http2 default_server;
@ -121,20 +121,20 @@ spec:
memory: 1Gi
cpu: 0.3
volumeMounts:
- name: vol-nginx
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
readOnly: true
- name: vol-cert
mountPath: /etc/nginx/cert
readOnly: true
- name: vol-nginx
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
readOnly: true
- name: vol-cert
mountPath: /etc/nginx/cert
readOnly: true
volumes:
- name: vol-nginx
configMap:
name: cfg-nginx
- name: vol-cert
secret:
secretName: sec-cert
- name: vol-nginx
configMap:
name: cfg-nginx
- name: vol-cert
secret:
secretName: sec-cert
---
apiVersion: apps/v1
kind: Deployment
@ -152,43 +152,43 @@ spec:
app: pontoon
spec:
containers:
- name: con-pontoon
imagePullPolicy: Always
image: <pontoon-prod image>
ports:
- protocol: TCP
containerPort: 3000
- protocol: TCP
containerPort: 8000
resources:
limits:
memory: 3Gi
cpu: 0.5
env:
- name: SECRET_KEY
value: "<the pontoon secret key>"
- name: DJANGO_LOGIN
value: "true"
- name: DJANGO_DEV
value: "false"
- name: DJANGO_DEBUG
value: "true"
- name: CI
value: "true"
- name: DATABASE_URL
value: "<pontoon database URL>"
- name: ALLOWED_HOSTS
value: "<comma seperated list of allowd hosts>"
- name: SITE_URL
value: "http://127.0.0.10"
- name: SYNC_INTERVAL
value: "30"
- name: KNOWN_HOSTS
value: "<base64 encoded file content of .ssh/known_hosts>"
- name: SSH_KEY
value: "<base64 encoded ssh private key from .ssh/id_rsa>"
- name: con-pontoon
imagePullPolicy: Always
image: <pontoon-prod image>
ports:
- protocol: TCP
containerPort: 3000
- protocol: TCP
containerPort: 8000
resources:
limits:
memory: 3Gi
cpu: 0.5
env:
- name: SECRET_KEY
value: '<the pontoon secret key>'
- name: DJANGO_LOGIN
value: 'true'
- name: DJANGO_DEV
value: 'false'
- name: DJANGO_DEBUG
value: 'true'
- name: CI
value: 'true'
- name: DATABASE_URL
value: '<pontoon database URL>'
- name: ALLOWED_HOSTS
value: '<comma seperated list of allowd hosts>'
- name: SITE_URL
value: 'http://127.0.0.10'
- name: SYNC_INTERVAL
value: '30'
- name: KNOWN_HOSTS
value: '<base64 encoded file content of .ssh/known_hosts>'
- name: SSH_KEY
value: '<base64 encoded ssh private key from .ssh/id_rsa>'
imagePullSecrets:
- name: sec-dockerhub
- name: sec-dockerhub
---
kind: Service
apiVersion: v1
@ -202,7 +202,7 @@ spec:
ports:
- name: https-web
protocol: TCP
#the port is not really used but mandatory
#the port is not really used but mandatory
port: 2049
targetPort: 8080
nodePort: <your assigned node port!>
@ -216,11 +216,11 @@ spec:
selector:
app: pontoon
ports:
- name: http-deprecated
protocol: TCP
port: 3000
targetPort: 3000
- name: http-ui
protocol: TCP
port: 8000
targetPort: 8000
- name: http-deprecated
protocol: TCP
port: 3000
targetPort: 3000
- name: http-ui
protocol: TCP
port: 8000
targetPort: 8000

Просмотреть файл

@ -1,120 +1,119 @@
# Pontoon
##### (Guide for building and deploying pontoon in a dev environment)
### Build the container under windows:
###### get code and prepare the build
- git checkout: C:\Projekte\pontoon\latest: https://github.com/mozilla/pontoon.git
- three files need to be replaced by those in this folder
###### build
- open a powershell in checkout folder and switch to wsl using the `wsl` command
- in WSL: connect windows local docker: `export DOCKER_HOST="tcp://localhost:2375"`
- run the build: `make build`
### The database setup:
For a first time setup we recommend starting the container, or at least the database migration script (python manage.py migrate) with a higher privilege user such as the "postgres" superuser. Then for all consecutive runs use the dedicated pontoon user which creation is described below.
- create a dedicated database
```
-- meant to be executed on the mandatory postgress database "postgres"
CREATE DATABASE pontoon;
```
- create a poonton database user and roll. Assign the user to its roll
```
-- meant to be executed on the mandatory postgress database "postgres"
CREATE USER "pontoon" WITH PASSWORD "h29xlKIN4nrTGyFLsKf1";
CREATE ROLE "pontoon-all";
GRANT "pontoon-all" TO "pontoon";
```
- additionally: add the database "postgres" superuser to this roll so he can see the tables easily in SQL-Clients such as pgAdmin.
```
-- meant to be executed on the mandatory postgress database "postgres"
GRANT "pontoon-all" TO "postgres";
```
- in case the "postgres" superuser or another higher privilege user was used to initially execute the migration script as mentioned above make sure to change the owner of each table within the poonton database:
```
-- meant to be executed on the postgress database "pontoon"
DO $$
DECLARE
tables CURSOR FOR
SELECT tablename
FROM pg_tables
WHERE tablename NOT LIKE 'pg_%' AND tablename NOT LIKE 'sql_%'
ORDER BY tablename;
BEGIN
FOR table_record IN tables LOOP
EXECUTE format('ALTER TABLE %s OWNER TO "pontoon-all"', table_record.tablename);
-- RAISE NOTICE 'Tablename: %', table_record.tablename;
END LOOP;
END$$;
```
### The container:
###### Test the container locally and init the database
- tag the container for easier usage:
- for testing run the container like this:
```
docker run -d -p 8000:8000 -p 3000:3000 -e DJANGO_LOGIN=true -e DJANGO_DEBUG=false -e DJANGO_DEV=false -e ALLOWED_HOSTS=127.0.0.1 -e CI=true -e DATABASE_URL=postgres://<postgres-user>:<password>@<db-ip-or-hostname>:5432/<pontoon-db-name> -e SECRET_KEY=a3cafccbafe39db54f2723f8a6f804c34753679a0f197b5b33050d784129d570 -e SITE_URL=http://127.0.0.1:8000 --name pontoon corp/imagename:pontoon-prod-31.01.20
```
The SSH_KEY and KNOWN_HOSTS environment variables are both base64 encoded, but may be omitted here and set/created manually (without base64 encoding) inside the running container. But for the prod environment they need to be passed to guarantee unattended deployment.
If the environment variable SYNC_INTERVAL is defined a shell script will call sync_projects using this interval in minutes.
- get a bash into the container: `docker exec -it pontoon bash`
useful commands:
- check open ports (8000 and 3000 should be open): `ss -lntu`
- check running processes: `ps -A`
- show simple startup log of server_run.sh: `cat /app/server_run.log`
##### first time only, with newly created database:
create an admin user: `python ./manage.py createsuperuser --user=<username> --email=<yourEmail@address>`
##### for each project:
- create project using the web ui, see: https://mozilla-l10n.github.io/documentation/tools/pontoon/adding_new_project.html
- further **administration** via Django: http://127.0.0.1:8000/__a/__
#### NOTES:
- don't try to run pontoon in a subfolder domain, like apigee does. (e.g. https://mydomain.com/pontoon/). It seems only to support running on domain level: **https://mydomain.com/~~pontoon/~~**
you may use subdomains --> https://pontoon.mydomain.com/
### deployment on k8s:
For a k8s deplyoment example yaml see the k8s-pontoon-example.yaml in this folder
#### TODO:
- "_Pontoon sends email when users request projects to be enabled for teams, or teams to be added to projects_"
**e-mails are not yet tested in the image**
---
### Useful commands
- for git access you need to create the _SSH_KEY_: `ssh-keygen -t rsa -b 4096 -C "<yourEmail@address>"`
- find the public key to be entered in guthub: `cat /root/.ssh/id_rsa.pub`
- the private key to be base64 encoded as _SSH_KEY_: `cat /root/.ssh/id_rsa`
- after the first git sync you also geht the _known_hosts_ file:
- to be base64 encoded as _KNOWN_HOSTS_: `cat /root/.ssh/known_hosts`
* first manually sync a single project by reading locales from git source code only: `python manage.py sync_projects --projects=<projectname> --no-commit`
* syncing all projects with writing changes to the soruce code `python manage.py sync_projects`.
This is done by shell script every 30 minutes (evn var SYNC_INTERVAL)
* to see if the sync works look at: http://127.0.0.1:8000/__sync/log/__
# Pontoon
##### (Guide for building and deploying pontoon in a dev environment)
### Build the container under windows:
###### get code and prepare the build
- git checkout: C:\Projekte\pontoon\latest: https://github.com/mozilla/pontoon.git
- three files need to be replaced by those in this folder
###### build
- open a powershell in checkout folder and switch to wsl using the `wsl` command
- in WSL: connect windows local docker: `export DOCKER_HOST="tcp://localhost:2375"`
- run the build: `make build`
### The database setup:
For a first time setup we recommend starting the container, or at least the database migration script (python manage.py migrate) with a higher privilege user such as the "postgres" superuser. Then for all consecutive runs use the dedicated pontoon user which creation is described below.
- create a dedicated database
```
-- meant to be executed on the mandatory postgress database "postgres"
CREATE DATABASE pontoon;
```
- create a poonton database user and roll. Assign the user to its roll
```
-- meant to be executed on the mandatory postgress database "postgres"
CREATE USER "pontoon" WITH PASSWORD "h29xlKIN4nrTGyFLsKf1";
CREATE ROLE "pontoon-all";
GRANT "pontoon-all" TO "pontoon";
```
- additionally: add the database "postgres" superuser to this roll so he can see the tables easily in SQL-Clients such as pgAdmin.
```
-- meant to be executed on the mandatory postgress database "postgres"
GRANT "pontoon-all" TO "postgres";
```
- in case the "postgres" superuser or another higher privilege user was used to initially execute the migration script as mentioned above make sure to change the owner of each table within the poonton database:
```
-- meant to be executed on the postgress database "pontoon"
DO $$
DECLARE
tables CURSOR FOR
SELECT tablename
FROM pg_tables
WHERE tablename NOT LIKE 'pg_%' AND tablename NOT LIKE 'sql_%'
ORDER BY tablename;
BEGIN
FOR table_record IN tables LOOP
EXECUTE format('ALTER TABLE %s OWNER TO "pontoon-all"', table_record.tablename);
-- RAISE NOTICE 'Tablename: %', table_record.tablename;
END LOOP;
END$$;
```
### The container:
###### Test the container locally and init the database
- tag the container for easier usage:
- for testing run the container like this:
```
docker run -d -p 8000:8000 -p 3000:3000 -e DJANGO_LOGIN=true -e DJANGO_DEBUG=false -e DJANGO_DEV=false -e ALLOWED_HOSTS=127.0.0.1 -e CI=true -e DATABASE_URL=postgres://<postgres-user>:<password>@<db-ip-or-hostname>:5432/<pontoon-db-name> -e SECRET_KEY=a3cafccbafe39db54f2723f8a6f804c34753679a0f197b5b33050d784129d570 -e SITE_URL=http://127.0.0.1:8000 --name pontoon corp/imagename:pontoon-prod-31.01.20
```
The SSH_KEY and KNOWN_HOSTS environment variables are both base64 encoded, but may be omitted here and set/created manually (without base64 encoding) inside the running container. But for the prod environment they need to be passed to guarantee unattended deployment.
If the environment variable SYNC_INTERVAL is defined a shell script will call sync_projects using this interval in minutes.
- get a bash into the container: `docker exec -it pontoon bash`
useful commands:
- check open ports (8000 and 3000 should be open): `ss -lntu`
- check running processes: `ps -A`
- show simple startup log of server_run.sh: `cat /app/server_run.log`
##### first time only, with newly created database:
create an admin user: `python ./manage.py createsuperuser --user=<username> --email=<yourEmail@address>`
##### for each project:
- create project using the web ui, see: https://mozilla-l10n.github.io/documentation/tools/pontoon/adding_new_project.html
- further **administration** via Django: http://127.0.0.1:8000/__a/__
#### NOTES:
- don't try to run pontoon in a subfolder domain, like apigee does. (e.g. https://mydomain.com/pontoon/). It seems only to support running on domain level: **https://mydomain.com/~~pontoon/~~**
you may use subdomains --> https://pontoon.mydomain.com/
### deployment on k8s:
For a k8s deplyoment example yaml see the k8s-pontoon-example.yaml in this folder
#### TODO:
- "_Pontoon sends email when users request projects to be enabled for teams, or teams to be added to projects_"
**e-mails are not yet tested in the image**
---
### Useful commands
- for git access you need to create the _SSH_KEY_: `ssh-keygen -t rsa -b 4096 -C "<yourEmail@address>"`
- find the public key to be entered in guthub: `cat /root/.ssh/id_rsa.pub`
- the private key to be base64 encoded as _SSH_KEY_: `cat /root/.ssh/id_rsa`
- after the first git sync you also geht the _known_hosts_ file:
- to be base64 encoded as _KNOWN_HOSTS_: `cat /root/.ssh/known_hosts`
* first manually sync a single project by reading locales from git source code only: `python manage.py sync_projects --projects=<projectname> --no-commit`
* syncing all projects with writing changes to the soruce code `python manage.py sync_projects`.
This is done by shell script every 30 minutes (evn var SYNC_INTERVAL)
* to see if the sync works look at: http://127.0.0.1:8000/__sync/log/__

Просмотреть файл

@ -1,4 +1,5 @@
# Pontoon Error Pages
[Custom Error Pages](https://devcenter.heroku.com/articles/error-pages#customize-pages) for Pontoon deployment on Heroku, featuring Tetris via [blockrain.js](http://aerolab.github.io/blockrain.js/)!
Must be hosted outside the main application, which will be down when these pages are displayed.

Просмотреть файл

@ -1,54 +1,63 @@
<!DOCTYPE html>
<html>
<head>
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,400i,700" rel="stylesheet">
<link href='https://fonts.googleapis.com/css?family=Play:400,700' rel='stylesheet' type='text/css'>
<head>
<link
href="https://fonts.googleapis.com/css?family=Open+Sans:400,400i,700"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css?family=Play:400,700"
rel="stylesheet"
type="text/css"
/>
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/blockrain.css">
<link rel="stylesheet" href="css/style.css" />
<link rel="stylesheet" href="css/blockrain.css" />
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
<title>Application Error</title>
</head>
<title>Application Error</title>
</head>
<body>
<header>
<h1>Application Error</h1>
<h2>Please check back later. Or play some Tetris.</h2>
</header>
<body>
<header>
<h1>Application Error</h1>
<h2>Please check back later. Or play some Tetris.</h2>
</header>
<section id="examples">
<article id="example-slider">
<div class="example">
<div class="instructions">
Use only arrows
<div class="keyboard">
<div class="key key-up">&#9650;</div>
<div class="key key-left">&#9668;</div>
<div class="key key-down">&#9660;</div>
<div class="key key-right">&#9658;</div>
<section id="examples">
<article id="example-slider">
<div class="example">
<div class="instructions">
Use only arrows
<div class="keyboard">
<div class="key key-up">&#9650;</div>
<div class="key key-left">&#9668;</div>
<div class="key key-down">&#9660;</div>
<div class="key key-right">&#9658;</div>
</div>
</div>
<div class="game" id="tetris-demo"></div>
</div>
<div class="game" id="tetris-demo"></div>
</div>
</article>
</article>
<footer>
<a id="credits" href="https://github.com/Aerolab/blockrain.js">Powered by Blockrain.js</a>
</footer>
</section>
<footer>
<a id="credits" href="https://github.com/Aerolab/blockrain.js"
>Powered by Blockrain.js</a
>
</footer>
</section>
<script src="js/jquery-1.11.1.min.js"></script>
<script src="js/blockrain.jquery.libs.js"></script>
<script src="js/blockrain.jquery.src.js"></script>
<script src="js/blockrain.jquery.themes.js"></script>
<script src="js/jquery-1.11.1.min.js"></script>
<script src="js/blockrain.jquery.libs.js"></script>
<script src="js/blockrain.jquery.src.js"></script>
<script src="js/blockrain.jquery.themes.js"></script>
<script>
var $demo = $('#tetris-demo').blockrain({
theme: 'pontoon',
playText: ''
});
</script>
</body>
<script>
var $demo = $('#tetris-demo').blockrain({
theme: 'pontoon',
playText: '',
});
</script>
</body>
</html>

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -1,54 +1,63 @@
<!DOCTYPE html>
<html>
<head>
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,400i,700" rel="stylesheet">
<link href='https://fonts.googleapis.com/css?family=Play:400,700' rel='stylesheet' type='text/css'>
<head>
<link
href="https://fonts.googleapis.com/css?family=Open+Sans:400,400i,700"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css?family=Play:400,700"
rel="stylesheet"
type="text/css"
/>
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/blockrain.css">
<link rel="stylesheet" href="css/style.css" />
<link rel="stylesheet" href="css/blockrain.css" />
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
<title>Offline for maintenance</title>
</head>
<title>Offline for maintenance</title>
</head>
<body>
<header>
<h1>Offline for maintenance</h1>
<h2>Please check back later. Or play some Tetris.</h2>
</header>
<body>
<header>
<h1>Offline for maintenance</h1>
<h2>Please check back later. Or play some Tetris.</h2>
</header>
<section id="examples">
<article id="example-slider">
<div class="example">
<div class="instructions">
Use only arrows
<div class="keyboard">
<div class="key key-up">&#9650;</div>
<div class="key key-left">&#9668;</div>
<div class="key key-down">&#9660;</div>
<div class="key key-right">&#9658;</div>
<section id="examples">
<article id="example-slider">
<div class="example">
<div class="instructions">
Use only arrows
<div class="keyboard">
<div class="key key-up">&#9650;</div>
<div class="key key-left">&#9668;</div>
<div class="key key-down">&#9660;</div>
<div class="key key-right">&#9658;</div>
</div>
</div>
<div class="game" id="tetris-demo"></div>
</div>
<div class="game" id="tetris-demo"></div>
</div>
</article>
</article>
<footer>
<a id="credits" href="https://github.com/Aerolab/blockrain.js">Powered by Blockrain.js</a>
</footer>
</section>
<footer>
<a id="credits" href="https://github.com/Aerolab/blockrain.js"
>Powered by Blockrain.js</a
>
</footer>
</section>
<script src="js/jquery-1.11.1.min.js"></script>
<script src="js/blockrain.jquery.libs.js"></script>
<script src="js/blockrain.jquery.src.js"></script>
<script src="js/blockrain.jquery.themes.js"></script>
<script src="js/jquery-1.11.1.min.js"></script>
<script src="js/blockrain.jquery.libs.js"></script>
<script src="js/blockrain.jquery.src.js"></script>
<script src="js/blockrain.jquery.themes.js"></script>
<script>
var $demo = $('#tetris-demo').blockrain({
theme: 'pontoon',
playText: ''
});
</script>
</body>
<script>
var $demo = $('#tetris-demo').blockrain({
theme: 'pontoon',
playText: '',
});
</script>
</body>
</html>

Просмотреть файл

@ -20,9 +20,9 @@
"build": "npm run build --workspaces --if-present",
"build:prod": "npm run build:prod --workspaces --if-present",
"heroku-postbuild": "echo Build is taken care of in ./bin/post_compile",
"prettier": "prettier --write '**/translate/**/*.{js,ts,tsx,css}' '**/pontoon/**/*.{js,css}' '**/tag-admin/**/*.{js,css}'",
"check-prettier": "prettier --check '**/translate/**/*.{js,ts,tsx,css}' '**/pontoon/**/*.{js,css}' '**/tag-admin/**/*.{js,css}'",
"eslint": "eslint 'translate/**/*.{js,ts,tsx}' 'pontoon/**/*.js' 'tag-admin/**/*.js'"
"prettier": "prettier --write .",
"check-prettier": "prettier --check .",
"eslint": "eslint ."
},
"workspaces": [
"translate",
@ -92,7 +92,7 @@
"prettier": {
"endOfLine": "lf",
"trailingComma": "all",
"tabWidth": 4,
"tabWidth": 2,
"jsxSingleQuote": true,
"singleQuote": true
},

Просмотреть файл

@ -1,3 +1,3 @@
.add {
float: right;
float: right;
}

Просмотреть файл

@ -1,80 +1,80 @@
body {
overflow: auto;
position: relative;
overflow: auto;
position: relative;
}
.select {
padding-left: 0;
text-align: left;
padding-left: 0;
text-align: left;
}
select {
margin-top: 4px;
margin-top: 4px;
}
h1 {
color: #ebebeb;
font-size: 48px;
letter-spacing: -1px;
margin-bottom: 40px;
position: relative;
color: #ebebeb;
font-size: 48px;
letter-spacing: -1px;
margin-bottom: 40px;
position: relative;
}
h1 aside {
bottom: 8px;
position: absolute;
right: 0;
bottom: 8px;
position: absolute;
right: 0;
}
h1 a {
color: #7bc876;
font-size: 14px;
font-weight: 300;
letter-spacing: 0;
margin-left: 20px;
color: #7bc876;
font-size: 14px;
font-weight: 300;
letter-spacing: 0;
margin-left: 20px;
}
body > form {
color: #aaaaaa;
font-size: 14px;
padding: 40px 20px 100px;
color: #aaaaaa;
font-size: 14px;
padding: 40px 20px 100px;
}
form > div {
margin: 20px 0;
text-align: right;
margin: 20px 0;
text-align: right;
}
form > div.controls {
margin-top: 70px;
margin-top: 70px;
}
form > section > div {
margin-top: 20px;
margin-top: 20px;
}
form > div.inline.delete,
form > section > div.inline.delete {
opacity: 0.3;
opacity: 0.3;
}
h3 {
font-size: 30px;
margin-top: 70px;
font-size: 30px;
margin-top: 70px;
}
h4 {
color: #ebebeb;
font-size: 18px;
font-style: italic;
font-weight: 300;
margin-top: 40px;
color: #ebebeb;
font-size: 18px;
font-style: italic;
font-weight: 300;
margin-top: 40px;
}
label {
display: block;
padding-bottom: 3px;
text-align: left;
display: block;
padding-bottom: 3px;
text-align: left;
}
input[type='text'],
@ -82,477 +82,477 @@ input[type='password'],
input[type='url'],
input[type='number'],
textarea {
display: block;
height: 24px;
margin-left: -1px;
width: 975px;
display: block;
height: 24px;
margin-left: -1px;
width: 975px;
}
.half label {
float: left;
width: 490px;
float: left;
width: 490px;
}
.half input {
margin-right: 5px;
width: 482px;
margin-right: 5px;
width: 482px;
}
.half input:last-child {
margin-right: 0;
width: 483px;
margin-right: 0;
width: 483px;
}
.half .for-name .errorlist {
float: left;
float: left;
}
.half .for-slug .errorlist {
float: right;
float: right;
}
.half .errorlist li {
margin: 0;
margin: 0;
}
form .button {
background: #888888;
border: none;
display: block;
float: right;
font-size: 14px;
height: 31px;
width: 31px;
background: #888888;
border: none;
display: block;
float: right;
font-size: 14px;
height: 31px;
width: 31px;
}
.button.delete-inline {
color: #333333;
font-size: 20px;
padding: 4px;
color: #333333;
font-size: 20px;
padding: 4px;
}
#admin-strings-form .controls .button,
#admin-form .controls .button {
border-radius: 3px;
font-weight: 400;
text-transform: uppercase;
width: 150px;
border-radius: 3px;
font-weight: 400;
text-transform: uppercase;
width: 150px;
}
#admin-strings-form .controls .add-inline {
float: left;
margin-left: 0;
float: left;
margin-left: 0;
}
#admin-strings-form .strings-list .entity textarea:nth-child(3) {
margin-bottom: 20px;
margin-bottom: 20px;
}
.controls .button.sync,
.controls .button.sync:hover {
background: #333941;
color: #aaa;
float: left;
background: #333941;
color: #aaa;
float: left;
}
.controls .button.pretranslate,
.controls .button.pretranslate:hover {
background: #333941;
color: #aaa;
float: right;
background: #333941;
color: #aaa;
float: right;
}
form a:link,
form a:visited {
color: #7bc876;
float: right;
text-transform: uppercase;
color: #7bc876;
float: right;
text-transform: uppercase;
}
#admin-form a.add-inline,
#admin-form a.add-repo {
font-size: 14px;
font-style: normal;
letter-spacing: 0;
margin-top: 17px;
font-size: 14px;
font-style: normal;
letter-spacing: 0;
margin-top: 17px;
}
.controls a {
margin: 5px 9px;
margin: 5px 9px;
}
.controls .checkbox {
float: left;
margin: -1px 20px 0 0;
text-transform: uppercase;
float: left;
margin: -1px 20px 0 0;
text-transform: uppercase;
}
.double-list-selector .locale.select .menu {
border-bottom: 1px solid #5e6475;
margin: 2px 0 -4px -1px;
padding: 10px 0;
width: 295px;
border-bottom: 1px solid #5e6475;
margin: 2px 0 -4px -1px;
padding: 10px 0;
width: 295px;
}
.double-list-selector .locale.select .menu ul li {
padding-left: 20px;
padding-right: 20px;
position: relative;
width: 100%;
padding-left: 20px;
padding-right: 20px;
position: relative;
width: 100%;
-moz-box-sizing: border-box;
box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.double-list-selector .locale.select.selected label {
text-align: center;
text-align: center;
}
.double-list-selector .locale.select.selected label .left {
float: left;
float: left;
}
.double-list-selector .locale.select.readonly label {
text-align: right;
text-align: right;
}
.double-list-selector .locale.select.readonly label a {
float: left;
float: left;
}
.locale.select {
float: left;
width: auto;
float: left;
width: auto;
}
form .locale.select.selected {
float: none;
margin-right: 49px;
float: none;
margin-right: 49px;
}
.locale.select.readonly {
float: right;
float: right;
}
.locale.select .menu ul {
height: 170px;
margin-bottom: 0;
height: 170px;
margin-bottom: 0;
}
.locale.select .menu ul li span.code {
float: right;
width: auto;
float: right;
width: auto;
}
#id_locales,
#id_locales_readonly {
display: none;
display: none;
}
.can-be-requested {
float: left;
width: 295px;
float: left;
width: 295px;
}
.copy-locales {
float: left;
margin-left: 48px;
width: 295px;
float: left;
margin-left: 48px;
width: 295px;
}
.deadline {
float: left;
width: 150px;
float: left;
width: 150px;
}
.priority {
float: left;
margin-left: 10px;
width: 165px;
float: left;
margin-left: 10px;
width: 165px;
}
.contact {
float: right;
width: 300px;
float: right;
width: 300px;
}
.copy-locales select,
.priority select,
.contact select {
width: 100%;
width: 100%;
}
.deadline input {
width: 144px;
width: 144px;
}
.checkbox label {
float: left;
padding: 6px 0 0 15px;
text-transform: uppercase;
float: left;
padding: 6px 0 0 15px;
text-transform: uppercase;
}
.data-source,
.new-strings,
.visibility {
position: relative;
position: relative;
}
.data-source #id_data_source,
.new-strings .manage-strings,
.visibility #id_visibility {
position: absolute;
right: 0;
position: absolute;
right: 0;
}
.data-source #id_data_source,
.visibility #id_visibility {
top: 5px;
top: 5px;
}
.new-strings .manage-strings {
top: -3px;
top: -3px;
}
.repository .delete-wrapper label {
display: inline;
display: inline;
}
.repository .delete-wrapper input[type='checkbox'] {
float: right;
margin: 2px;
float: right;
margin: 2px;
}
.checkbox label input {
float: left;
margin-left: -18px;
margin-top: 1px;
float: left;
margin-left: -18px;
margin-top: 1px;
}
.repository .type-wrapper {
float: left;
margin-right: 6px;
float: left;
margin-right: 6px;
}
.repository .details-wrapper {
float: left;
width: 552px;
float: left;
width: 552px;
}
.repository.git .details-wrapper {
width: 768px;
width: 768px;
}
.repository .details-wrapper input {
width: 908px;
width: 908px;
}
.repository.git .details-wrapper input {
width: 763px;
width: 763px;
}
.repository .branch-wrapper {
display: none;
float: left;
margin-left: 6px;
width: 138px;
display: none;
float: left;
margin-left: 6px;
width: 138px;
}
.repository.git .branch-wrapper {
display: block;
display: block;
}
.repository .website-wrapper,
.repository .prefix-wrapper,
.repository .repository-toolbar {
margin-top: 20px;
margin-top: 20px;
}
.repository .repository-toolbar > section {
display: inline-block;
float: left;
display: inline-block;
float: left;
}
.repository .repository-toolbar > section.delete-wrapper {
float: right;
float: right;
}
.inline input[id*='url'] {
float: left;
width: 607px;
float: left;
width: 607px;
}
.branch-wrapper input[type='text'] {
width: 134px;
width: 134px;
}
.edit section .bottom {
font-size: 12px;
text-transform: uppercase;
font-size: 12px;
text-transform: uppercase;
}
.inline[data-count],
.entity[data-count],
.repository-empty {
display: none;
display: none;
}
.inline input[id*='name']::-webkit-calendar-picker-indicator {
display: none; /* remove default arrow */
display: none; /* remove default arrow */
}
input#id_configuration_file {
margin-bottom: 5px;
margin-bottom: 5px;
}
label[for='id_configuration_file'] a {
float: none;
text-transform: none;
float: none;
text-transform: none;
}
.externalresource .arrow::after {
content: '▾';
color: #000;
float: left;
margin: 6px 0 0 -20px;
pointer-events: none;
content: '▾';
color: #000;
float: left;
margin: 6px 0 0 -20px;
pointer-events: none;
}
.inline label[for*='name'] {
float: left;
width: 325px;
float: left;
width: 325px;
}
.inline input[id*='name'] {
float: left;
width: 321px;
float: left;
width: 321px;
}
.inline input[id$='DELETE'],
.inline input[id$='obsolete'] {
display: none;
display: none;
}
.inline label[for*='url'] {
float: left;
width: 649px;
float: left;
width: 649px;
}
.inline input[id*='name'],
.inline label[for*='name'] {
margin-right: 6px;
margin-right: 6px;
}
textarea {
height: 60px;
height: 60px;
}
textarea.strings-source {
height: 300px;
height: 300px;
}
.subtitle {
font-size: 12px;
margin-top: 4px;
text-transform: uppercase;
font-size: 12px;
margin-top: 4px;
text-transform: uppercase;
}
.errorlist {
color: #f36;
display: inline-block;
font-size: 12px;
line-height: 14px; /* Strange, but needed to keep controls in line when error occures */
list-style: none;
margin-left: 0;
margin-top: 2px;
text-transform: uppercase;
color: #f36;
display: inline-block;
font-size: 12px;
line-height: 14px; /* Strange, but needed to keep controls in line when error occures */
list-style: none;
margin-left: 0;
margin-top: 2px;
text-transform: uppercase;
}
.errorlist li {
display: inline;
margin-left: 5px;
display: inline;
margin-left: 5px;
}
.locales .errorlist {
margin-top: 10px;
text-align: center;
width: 100%;
margin-top: 10px;
text-align: center;
width: 100%;
}
.notification {
display: block;
margin-left: auto;
margin-right: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
.new-strings {
display: none;
display: none;
}
.manage-strings a {
margin-left: 20px;
margin-left: 20px;
}
/* Code specific to the manage strings page */
.strings-list .entity {
margin-bottom: 30px;
margin-bottom: 30px;
}
/* Code specific to the manage tags widget */
.tag.inline label[for*='name'] {
float: left;
width: 330px;
float: left;
width: 330px;
}
.tag.inline input[id*='name'] {
float: left;
width: 324px;
margin-right: 6px;
float: left;
width: 324px;
margin-right: 6px;
}
.tag.inline label[for*='slug'] {
float: left;
width: 331px;
float: left;
width: 331px;
}
.tag.inline input[id*='slug'] {
float: left;
width: 321px;
margin-right: 6px;
float: left;
width: 321px;
margin-right: 6px;
}
.tag.inline label[for*='priority'] {
float: left;
width: 311px;
float: left;
width: 311px;
}
.tag.inline input[id*='priority'] {
float: left;
width: 270px;
margin-right: 6px;
float: left;
width: 270px;
margin-right: 6px;
}
.tag.inline .form-errors {
clear: both;
clear: both;
}
.tag.inline .form-errors .name-errors {
float: left;
width: 324px;
margin-right: 5px;
min-height: 1px;
float: left;
width: 324px;
margin-right: 5px;
min-height: 1px;
}
.tag.inline .form-errors .slug-errors {
float: left;
width: 321px;
float: left;
width: 321px;
}
.tag.inline .form-errors .priority-errors {
float: left;
width: 311px;
float: left;
width: 311px;
}
.tag.inline select[name^='tag_set-'][name$='-priority'] {
width: 276px;
margin-top: 0;
width: 276px;
margin-top: 0;
}

Просмотреть файл

@ -1,283 +1,283 @@
$(function () {
// Before submitting the form
$('#admin-form').submit(function () {
// Update locales
var locales = [
{
list: 'selected',
input: $('#id_locales'),
},
{
list: 'readonly',
input: $('#id_locales_readonly'),
},
];
// Before submitting the form
$('#admin-form').submit(function () {
// Update locales
var locales = [
{
list: 'selected',
input: $('#id_locales'),
},
{
list: 'readonly',
input: $('#id_locales_readonly'),
},
];
locales.forEach(function (type) {
var ids = $('.admin-team-selector .locale.' + type.list)
.find('li[data-id]')
.map(function () {
return $(this).data('id');
})
.get();
type.input.val(ids);
});
// Update form action
var slug = $('#id_slug').val();
if (slug.length > 0) {
slug += '/';
}
$('#admin-form').attr(
'action',
$('#admin-form').attr('action').split('/projects/')[0] +
'/projects/' +
slug,
);
});
// Submit form with Enter (keyCode === 13)
$('html')
.unbind('keydown.pontoon')
.bind('keydown.pontoon', function (e) {
if (
$('input[type=text]:focus').length > 0 ||
$('input[type=url]:focus').length > 0
) {
var key = e.keyCode || e.which;
if (key === 13) {
// A short delay to allow digest of autocomplete before submit
setTimeout(function () {
$('#admin-form').submit();
}, 1);
return false;
}
}
});
// Submit form with button
$('.save').click(function (e) {
e.preventDefault();
$('#admin-form').submit();
});
// Manually Sync project
$('.sync').click(function (e) {
e.preventDefault();
var button = $(this),
title = button.html();
if (button.is('.in-progress')) {
return;
}
button.addClass('in-progress').html('Syncing...');
$.ajax({
url: '/admin/projects/' + $('#id_slug').val() + '/sync/',
locales.forEach(function (type) {
var ids = $('.admin-team-selector .locale.' + type.list)
.find('li[data-id]')
.map(function () {
return $(this).data('id');
})
.success(function () {
button.html('Started');
})
.error(function () {
button.html('Whoops!');
})
.complete(function () {
setTimeout(function () {
button.removeClass('in-progress').html(title);
}, 2000);
});
.get();
type.input.val(ids);
});
// Manually Pretranslate project
$('.pretranslate').click(function (e) {
e.preventDefault();
var button = $(this),
title = button.html();
if (button.is('.in-progress')) {
return;
}
button.addClass('in-progress').html('Pretranslating...');
$.ajax({
url: '/admin/projects/' + $('#id_slug').val() + '/pretranslate/',
})
.success(function () {
button.html('Started');
})
.error(function () {
button.html('Whoops!');
})
.complete(function () {
setTimeout(function () {
button.removeClass('in-progress').html(title);
}, 2000);
});
});
// Suggest slugified name for new projects
$('#id_name').blur(function () {
if ($('input[name=pk]').length > 0 || !$('#id_name').val()) {
return;
}
$('#id_slug').attr('placeholder', 'Retrieving...');
$.ajax({
url: '/admin/get-slug/',
data: {
name: $('#id_name').val(),
},
success: function (data) {
var value = data === 'error' ? '' : data;
$('#id_slug').val(value);
},
error: function () {
$('#id_slug').attr('placeholder', '');
},
});
});
$('body').on('blur', '[id^=id_tag_set-][id$=-name]', function () {
var target = $('input#' + $(this).attr('id').replace('-name', '-slug'));
var $this = this;
if (target.val() || !$(this).val()) {
return;
}
target.attr('placeholder', 'Retrieving...');
$.ajax({
url: '/admin/get-slug/',
data: {
name: $($this).val(),
},
success: function (data) {
var value = data === 'error' ? '' : data;
target.val(value);
target.attr('placeholder', '');
},
error: function () {
target.attr('placeholder', '');
},
});
});
// Copy locales from another project
$('#copy-locales option').on('click', function () {
var projectLocales = [];
try {
projectLocales = JSON.parse($(this).val());
} catch (error) {
// No project selected
return;
}
$('.readonly .move-all').click();
$('.selected .move-all.left').click();
$(projectLocales).each(function (i, id) {
$('.locale.select:first')
.find('[data-id=' + id + ']')
.click();
});
});
// Show new strings input or link when source type is "database".
function displayNewStringsInput(input) {
if (input.val() === 'database') {
$('.new-strings').show();
$('.manage-strings').show();
// For now, we also hide the entire Repositories section. We might
// want to revisit that behavior later.
$('.repositories').hide();
} else {
$('.new-strings').hide();
$('.manage-strings').hide();
$('.repositories').show();
}
// Update form action
var slug = $('#id_slug').val();
if (slug.length > 0) {
slug += '/';
}
var dataSourceInput = $('#id_data_source');
dataSourceInput.on('change', function () {
displayNewStringsInput(dataSourceInput);
$('#admin-form').attr(
'action',
$('#admin-form').attr('action').split('/projects/')[0] +
'/projects/' +
slug,
);
});
// Submit form with Enter (keyCode === 13)
$('html')
.unbind('keydown.pontoon')
.bind('keydown.pontoon', function (e) {
if (
$('input[type=text]:focus').length > 0 ||
$('input[type=url]:focus').length > 0
) {
var key = e.keyCode || e.which;
if (key === 13) {
// A short delay to allow digest of autocomplete before submit
setTimeout(function () {
$('#admin-form').submit();
}, 1);
return false;
}
}
});
// Submit form with button
$('.save').click(function (e) {
e.preventDefault();
$('#admin-form').submit();
});
// Manually Sync project
$('.sync').click(function (e) {
e.preventDefault();
var button = $(this),
title = button.html();
if (button.is('.in-progress')) {
return;
}
button.addClass('in-progress').html('Syncing...');
$.ajax({
url: '/admin/projects/' + $('#id_slug').val() + '/sync/',
})
.success(function () {
button.html('Started');
})
.error(function () {
button.html('Whoops!');
})
.complete(function () {
setTimeout(function () {
button.removeClass('in-progress').html(title);
}, 2000);
});
});
// Manually Pretranslate project
$('.pretranslate').click(function (e) {
e.preventDefault();
var button = $(this),
title = button.html();
if (button.is('.in-progress')) {
return;
}
button.addClass('in-progress').html('Pretranslating...');
$.ajax({
url: '/admin/projects/' + $('#id_slug').val() + '/pretranslate/',
})
.success(function () {
button.html('Started');
})
.error(function () {
button.html('Whoops!');
})
.complete(function () {
setTimeout(function () {
button.removeClass('in-progress').html(title);
}, 2000);
});
});
// Suggest slugified name for new projects
$('#id_name').blur(function () {
if ($('input[name=pk]').length > 0 || !$('#id_name').val()) {
return;
}
$('#id_slug').attr('placeholder', 'Retrieving...');
$.ajax({
url: '/admin/get-slug/',
data: {
name: $('#id_name').val(),
},
success: function (data) {
var value = data === 'error' ? '' : data;
$('#id_slug').val(value);
},
error: function () {
$('#id_slug').attr('placeholder', '');
},
});
});
$('body').on('blur', '[id^=id_tag_set-][id$=-name]', function () {
var target = $('input#' + $(this).attr('id').replace('-name', '-slug'));
var $this = this;
if (target.val() || !$(this).val()) {
return;
}
target.attr('placeholder', 'Retrieving...');
$.ajax({
url: '/admin/get-slug/',
data: {
name: $($this).val(),
},
success: function (data) {
var value = data === 'error' ? '' : data;
target.val(value);
target.attr('placeholder', '');
},
error: function () {
target.attr('placeholder', '');
},
});
});
// Copy locales from another project
$('#copy-locales option').on('click', function () {
var projectLocales = [];
try {
projectLocales = JSON.parse($(this).val());
} catch (error) {
// No project selected
return;
}
$('.readonly .move-all').click();
$('.selected .move-all.left').click();
$(projectLocales).each(function (i, id) {
$('.locale.select:first')
.find('[data-id=' + id + ']')
.click();
});
});
// Show new strings input or link when source type is "database".
function displayNewStringsInput(input) {
if (input.val() === 'database') {
$('.new-strings').show();
$('.manage-strings').show();
// For now, we also hide the entire Repositories section. We might
// want to revisit that behavior later.
$('.repositories').hide();
} else {
$('.new-strings').hide();
$('.manage-strings').hide();
$('.repositories').show();
}
}
var dataSourceInput = $('#id_data_source');
dataSourceInput.on('change', function () {
displayNewStringsInput(dataSourceInput);
});
displayNewStringsInput(dataSourceInput);
// Suggest public repository website URL
$('body').on('blur', '.repo input', function () {
var val = $(this)
.val()
.replace(/\.git$/, '')
.replace('git@github.com:', 'https://github.com/')
.replace('ssh://', 'https://');
// Suggest public repository website URL
$('body').on('blur', '.repo input', function () {
var val = $(this)
.val()
.replace(/\.git$/, '')
.replace('git@github.com:', 'https://github.com/')
.replace('ssh://', 'https://');
$(this).parents('.repository').find('.website-wrapper input').val(val);
});
$(this).parents('.repository').find('.website-wrapper input').val(val);
});
// Delete inline form item (e.g. subpage or external resource)
$('body').on('click.pontoon', '.delete-inline', function (e) {
e.preventDefault();
$(this).parent().toggleClass('delete');
$(this).next().prop('checked', !$(this).next().prop('checked'));
});
$('.inline [checked]').click().prev().click();
// Delete inline form item (e.g. subpage or external resource)
$('body').on('click.pontoon', '.delete-inline', function (e) {
e.preventDefault();
$(this).parent().toggleClass('delete');
$(this).next().prop('checked', !$(this).next().prop('checked'));
});
$('.inline [checked]').click().prev().click();
// Add inline form item (e.g. subpage or external resource)
var count = {
subpage: $('.subpage:last').data('count'),
externalresource: $('.externalresource:last').data('count'),
entity: $('.entity:last').data('count'),
tag: $('.tag:last').data('count'),
};
$('.add-inline').click(function (e) {
e.preventDefault();
// Add inline form item (e.g. subpage or external resource)
var count = {
subpage: $('.subpage:last').data('count'),
externalresource: $('.externalresource:last').data('count'),
entity: $('.entity:last').data('count'),
tag: $('.tag:last').data('count'),
};
$('.add-inline').click(function (e) {
e.preventDefault();
var type = $(this).data('type');
var form = $('.' + type + ':last')
.html()
.replace(/__prefix__/g, count[type]);
var type = $(this).data('type');
var form = $('.' + type + ':last')
.html()
.replace(/__prefix__/g, count[type]);
$('.' + type + ':last').before(
'<div class="' + type + ' inline clearfix">' + form + '</div>',
);
count[type]++;
$('.' + type + ':last').before(
'<div class="' + type + ' inline clearfix">' + form + '</div>',
);
count[type]++;
// These two forms of selectors cover all the cases for django-generated forms we use.
$('#id_' + type + '_set-TOTAL_FORMS').val(count[type]);
$('#id_form-TOTAL_FORMS').val(count[type]);
});
// These two forms of selectors cover all the cases for django-generated forms we use.
$('#id_' + type + '_set-TOTAL_FORMS').val(count[type]);
$('#id_form-TOTAL_FORMS').val(count[type]);
});
// Toggle branch input
function toggleBranchInput(element) {
$(element)
.parents('.repository')
.toggleClass('git', $(element).val() === 'git');
}
// On select change
$('body').on('change', '.repository .type-wrapper select', function () {
toggleBranchInput(this);
});
// On page load
$('.repository .type-wrapper select').each(function () {
toggleBranchInput(this);
});
// Toggle branch input
function toggleBranchInput(element) {
$(element)
.parents('.repository')
.toggleClass('git', $(element).val() === 'git');
}
// On select change
$('body').on('change', '.repository .type-wrapper select', function () {
toggleBranchInput(this);
});
// On page load
$('.repository .type-wrapper select').each(function () {
toggleBranchInput(this);
});
// Add repo
var $totalForms = $('#id_repositories-TOTAL_FORMS');
$('.add-repo').click(function (e) {
e.preventDefault();
var count = parseInt($totalForms.val(), 10);
// Add repo
var $totalForms = $('#id_repositories-TOTAL_FORMS');
$('.add-repo').click(function (e) {
e.preventDefault();
var count = parseInt($totalForms.val(), 10);
var $emptyForm = $('.repository-empty');
var form = $emptyForm.html().replace(/__prefix__/g, count);
$('.repository:last').after(
'<div class="repository clearfix">' + form + '</div>',
);
var $emptyForm = $('.repository-empty');
var form = $emptyForm.html().replace(/__prefix__/g, count);
$('.repository:last').after(
'<div class="repository clearfix">' + form + '</div>',
);
toggleBranchInput($('.repository:last').find('.type-wrapper select'));
toggleBranchInput($('.repository:last').find('.type-wrapper select'));
$totalForms.val(count + 1);
});
$totalForms.val(count + 1);
});
});

Просмотреть файл

@ -1,12 +1,11 @@
# GraphQL API
Pontoon exposes some of its data via a public API endpoint. The API is
[GraphQL](http://graphql.org/)-based and available at ``/graphql``.
[GraphQL](http://graphql.org/)-based and available at `/graphql`.
## Production Deployments
When run in production (``DEV is False``) the API returns ``application/json``
When run in production (`DEV is False`) the API returns `application/json`
responses to GET and POST requests. In case of GET requests, any whitespace in
the query must be escaped.
@ -22,14 +21,13 @@ An example POST requests may look like this:
$ curl -X POST -d "query={ projects { name } }" https://example.com/graphql
```
## Local Development
In a local development setup (``DEV is True``) the endpoint has two modes of
In a local development setup (`DEV is True`) the endpoint has two modes of
operation: a JSON one and an HTML one.
When a request is sent, without any headers, with ``Accept: application/json`` or
if it explicitly contains a ``raw`` query argument, the endpoint will behave like
When a request is sent, without any headers, with `Accept: application/json` or
if it explicitly contains a `raw` query argument, the endpoint will behave like
a production one, returning JSON responses.
The following query in the CLI will return a JSON response:
@ -38,22 +36,21 @@ The following query in the CLI will return a JSON response:
$ curl --globoff http://localhost:8000/graphql?query={projects{name}}
```
If however a request is sent with ``Accept: text/html`` such as is the case when
If however a request is sent with `Accept: text/html` such as is the case when
accessing the endpoint in a browser, a GUI query editor and explorer,
[GraphiQL](https://github.com/graphql/graphiql), will be served::
http://localhost:8000/graphql?query={projects{name}}
To preview the JSON response in the browser, pass in the ``raw`` query argument::
To preview the JSON response in the browser, pass in the `raw` query argument::
http://localhost:8000/graphql?query={projects{name}}&raw
## Query IDE
The [GraphiQL](https://github.com/graphql/graphiql) query IDE is available at
``http://localhost:8000/graphql`` when running Pontoon locally and the URL is
accessed with the ``Accept: text/html`` header, e.g. using a browser.
`http://localhost:8000/graphql` when running Pontoon locally and the URL is
accessed with the `Accept: text/html` header, e.g. using a browser.
It offers a query editor with:
@ -62,4 +59,4 @@ It offers a query editor with:
- real-time error reporting,
- results folding,
- autogenerated docs on shapes and their fields,
- [introspection](http://docs.graphene-python.org/projects/django/en/latest/debug/) via the ``__debug``.
- [introspection](http://docs.graphene-python.org/projects/django/en/latest/debug/) via the `__debug`.

Просмотреть файл

@ -1,25 +1,25 @@
.double-list-selector .select .menu ul li .arrow {
display: none;
position: absolute;
top: 4px;
right: 2px;
display: none;
position: absolute;
top: 4px;
right: 2px;
}
.double-list-selector .select .menu ul li.hover .arrow {
display: inline-block;
display: inline-block;
}
.double-list-selector .select:first-child .menu ul li .arrow:after,
.double-list-selector .select:nth-child(2) .menu ul li .arrow:after {
content: '';
content: '';
}
.double-list-selector .select:nth-child(2) .menu ul li.left .arrow:after,
.double-list-selector .select:last-child .menu ul li .arrow:after {
content: '';
content: '';
}
.double-list-selector .select:nth-child(2) .menu ul li.left .arrow,
.double-list-selector .select:last-child .menu ul li .arrow {
left: 2px;
left: 2px;
}

Просмотреть файл

@ -1,48 +1,48 @@
.download-selector {
float: right;
float: right;
}
.download-selector .selector {
padding-right: 0;
padding-top: 0;
padding-right: 0;
padding-top: 0;
}
.download-selector .selector .fa {
float: right;
background: #3f4752;
border-radius: 3px;
box-sizing: border-box;
color: #aaa;
font-size: 16px;
height: 32px;
width: 32px;
padding-top: 8px;
text-align: center;
float: right;
background: #3f4752;
border-radius: 3px;
box-sizing: border-box;
color: #aaa;
font-size: 16px;
height: 32px;
width: 32px;
padding-top: 8px;
text-align: center;
}
.download-selector .selector .fa:hover {
background: #272a2f;
background: #272a2f;
}
.download-selector.opened .selector .fa {
background: #272a2f;
border-radius: 2px 2px 0 0;
background: #272a2f;
border-radius: 2px 2px 0 0;
}
.download-selector.opened .menu {
display: block;
top: 32px;
right: 0;
bottom: auto;
width: 220px;
display: block;
top: 32px;
right: 0;
bottom: auto;
width: 220px;
}
.download-selector.opened .menu li {
padding-bottom: 0;
padding-top: 0;
padding-bottom: 0;
padding-top: 0;
}
.download-selector.opened .menu li a {
display: block;
line-height: 22px;
display: block;
line-height: 22px;
}

Просмотреть файл

@ -1,46 +1,46 @@
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: url('../fonts/OpenSans-Regular.eot');
src: url('../fonts/OpenSans-Regular.eot') format('embedded-opentype'),
url('../fonts/OpenSans-Regular.woff') format('woff'),
url('../fonts/OpenSans-Regular.ttf') format('truetype');
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: url('../fonts/OpenSans-Regular.eot');
src: url('../fonts/OpenSans-Regular.eot') format('embedded-opentype'),
url('../fonts/OpenSans-Regular.woff') format('woff'),
url('../fonts/OpenSans-Regular.ttf') format('truetype');
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
src: url('../fonts/OpenSans-Light.eot');
src: url('../fonts/OpenSans-Light.eot') format('embedded-opentype'),
url('../fonts/OpenSans-Light.woff') format('woff'),
url('../fonts/OpenSans-Light.ttf') format('truetype');
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
src: url('../fonts/OpenSans-Light.eot');
src: url('../fonts/OpenSans-Light.eot') format('embedded-opentype'),
url('../fonts/OpenSans-Light.woff') format('woff'),
url('../fonts/OpenSans-Light.ttf') format('truetype');
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
src: url('../fonts/OpenSans-Bold.eot');
src: url('../fonts/OpenSans-Bold.eot') format('embedded-opentype'),
url('../fonts/OpenSans-Bold.woff') format('woff'),
url('../fonts/OpenSans-Bold.ttf') format('truetype');
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
src: url('../fonts/OpenSans-Bold.eot');
src: url('../fonts/OpenSans-Bold.eot') format('embedded-opentype'),
url('../fonts/OpenSans-Bold.woff') format('woff'),
url('../fonts/OpenSans-Bold.ttf') format('truetype');
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
src: url('../fonts/OpenSans-Italic.eot');
src: url('../fonts/OpenSans-Italic.eot') format('embedded-opentype'),
url('../fonts/OpenSans-Italic.woff') format('woff'),
url('../fonts/OpenSans-Italic.ttf') format('truetype');
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
src: url('../fonts/OpenSans-Italic.eot');
src: url('../fonts/OpenSans-Italic.eot') format('embedded-opentype'),
url('../fonts/OpenSans-Italic.woff') format('woff'),
url('../fonts/OpenSans-Italic.ttf') format('truetype');
}
@font-face {
font-family: 'Ubuntu Regular';
font-style: italic;
font-weight: 400;
src: url('../fonts/Ubuntu-RI.ttf');
font-family: 'Ubuntu Regular';
font-style: italic;
font-weight: 400;
src: url('../fonts/Ubuntu-RI.ttf');
}

Просмотреть файл

@ -1,192 +1,192 @@
ul {
list-style: none;
margin: 0;
list-style: none;
margin: 0;
}
#heading:not(.simple) {
padding: 30px 0;
padding: 30px 0;
}
#heading:not(.simple) h1 {
padding-bottom: 30px;
padding-bottom: 30px;
}
#heading h1 a {
color: #ebebeb;
font-weight: bold;
font-size: 32px;
color: #ebebeb;
font-weight: bold;
font-size: 32px;
}
#heading h1 .small {
color: #7bc876;
font-weight: 300;
padding-left: 5px;
color: #7bc876;
font-weight: 300;
padding-left: 5px;
}
#heading .details,
#heading .legend {
font-weight: 300;
font-weight: 300;
}
#heading .details li,
#heading .legend li {
line-height: 24px;
line-height: 24px;
}
#heading .details {
float: left;
width: 360px;
float: left;
width: 360px;
}
#heading .details .title {
color: #aaaaaa;
padding-right: 5px;
position: relative;
text-transform: uppercase;
color: #aaaaaa;
padding-right: 5px;
position: relative;
text-transform: uppercase;
}
#heading .details .title sup {
position: absolute;
top: -5px;
position: absolute;
top: -5px;
}
#heading .details .value {
color: #aaa;
float: right;
color: #aaa;
float: right;
}
#heading .details .value a {
color: #ffffff;
color: #ffffff;
}
#heading .details .value a:hover {
color: #7bc876;
color: #7bc876;
}
#heading .details .priority .value {
margin-top: 6px;
margin-top: 6px;
}
#heading .details .value.overflow {
max-width: 275px;
overflow: hidden;
padding-left: 1px; /* Needed to avoid cutting off overlay due to overflow: hidden; */
padding-right: 1px; /* Needed to avoid cutting off overlay due to overflow: hidden; */
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 275px;
overflow: hidden;
padding-left: 1px; /* Needed to avoid cutting off overlay due to overflow: hidden; */
padding-right: 1px; /* Needed to avoid cutting off overlay due to overflow: hidden; */
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
}
#heading .details .resources .value.overflow {
max-width: 280px;
white-space: normal;
max-width: 280px;
white-space: normal;
}
#heading .progress {
float: left;
margin: -10px 65px 0;
position: relative;
text-align: center;
float: left;
margin: -10px 65px 0;
position: relative;
text-align: center;
}
#heading .progress .number {
display: none;
font-size: 50px;
font-weight: bold;
left: 0;
right: 0;
top: 25px;
margin: 0;
position: absolute;
display: none;
font-size: 50px;
font-weight: bold;
left: 0;
right: 0;
top: 25px;
margin: 0;
position: absolute;
}
#heading .progress .number:after {
content: '%';
color: #888888;
display: block;
font-size: 20px;
font-weight: 100;
line-height: 10px;
content: '%';
color: #888888;
display: block;
font-size: 20px;
font-weight: 100;
line-height: 10px;
}
#heading .legend {
float: left;
width: 190px;
float: left;
width: 190px;
}
#heading .legend li {
color: #aaaaaa;
padding-left: 25px;
position: relative;
text-align: left;
text-transform: uppercase;
color: #aaaaaa;
padding-left: 25px;
position: relative;
text-align: left;
text-transform: uppercase;
}
#heading .legend li a {
color: #ffffff;
color: #ffffff;
}
#heading .legend li:hover a {
color: #7bc876;
color: #7bc876;
}
#heading .legend li .status.fa {
left: 0;
top: 4px;
left: 0;
top: 4px;
}
#heading .legend li span.value {
color: #aaa;
float: right;
color: #aaa;
float: right;
}
#heading .legend li a span.value {
color: #ffffff;
color: #ffffff;
}
#heading .legend li:hover a span.value {
color: #7bc876;
color: #7bc876;
}
#heading .non-plottable {
float: right;
float: right;
}
#heading .non-plottable p {
color: #aaa;
font-weight: 300;
line-height: 24px;
text-align: right;
text-transform: uppercase;
color: #aaa;
font-weight: 300;
line-height: 24px;
text-align: right;
text-transform: uppercase;
}
#heading .non-plottable a p {
color: #ffffff;
color: #ffffff;
}
#heading .non-plottable a:hover p {
color: #7bc876;
color: #7bc876;
}
#heading .non-plottable p.value {
font-size: 20px;
font-size: 20px;
}
#heading .non-plottable .all {
padding-bottom: 21px;
padding-bottom: 21px;
}
#heading .non-plottable .unreviewed .status.fa {
left: auto;
position: relative;
top: -1px;
left: auto;
position: relative;
top: -1px;
}
#heading .non-plottable .unreviewed .status.fa:before {
color: #4d5967;
font-size: 18px;
color: #4d5967;
font-size: 18px;
}
#heading .non-plottable .unreviewed.pending .status.fa:before {
color: #4fc4f6;
color: #4fc4f6;
}

Просмотреть файл

@ -1,43 +1,43 @@
.pontoon-hovered {
outline: 1px dashed !important;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-o-user-select: none;
user-select: none;
outline: 1px dashed !important;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-o-user-select: none;
user-select: none;
}
.pontoon-editable-toolbar {
background-color: #ebebeb;
position: absolute;
top: 0;
left: 0;
z-index: 999999999;
display: none;
border-top: 1px dashed #000000;
border-left: 1px dashed #000000;
border-right: 1px dashed #000000;
background-color: #ebebeb;
position: absolute;
top: 0;
left: 0;
z-index: 999999999;
display: none;
border-top: 1px dashed #000000;
border-left: 1px dashed #000000;
border-right: 1px dashed #000000;
}
.pontoon-editable-toolbar.bottom {
border-top: none;
border-bottom: 1px dashed #000000;
border-top: none;
border-bottom: 1px dashed #000000;
}
.pontoon-editable-toolbar a {
background: transparent none 0 0 no-repeat;
display: block;
width: 16px;
height: 16px;
float: left;
margin: 2px;
background: transparent none 0 0 no-repeat;
display: block;
width: 16px;
height: 16px;
float: left;
margin: 2px;
}
.pontoon-editable-toolbar .edit {
background-image: url('../img/edit.png');
background-image: url('../img/edit.png');
}
.pontoon-editable-toolbar .cancel {
background-image: url('../img/cancel.png');
display: none;
background-image: url('../img/cancel.png');
display: none;
}

Просмотреть файл

@ -1,54 +1,54 @@
.menu.left-column {
float: left;
width: 300px;
float: left;
width: 300px;
}
.menu.left-column ul {
max-height: none;
max-height: none;
}
.menu.left-column li.selected {
background: #3f4752;
background: #3f4752;
}
.menu.left-column li.selected a {
color: #ffffff;
color: #ffffff;
}
.menu.left-column li {
padding: 0;
padding: 0;
}
.menu.left-column li a {
display: block;
font-size: 15px;
padding: 5px;
display: block;
font-size: 15px;
padding: 5px;
}
.menu.left-column .count {
background: #333941;
float: right;
background: #333941;
float: right;
}
.menu.right-column {
background: #333941;
float: right;
width: 640px;
background: #333941;
float: right;
width: 640px;
}
.menu.right-column > section {
display: none;
display: none;
}
.menu.right-column > section.selected {
display: block;
display: block;
}
.menu.right-column li.no .title {
font-size: 22px;
margin-top: 10px;
font-size: 22px;
margin-top: 10px;
}
.menu.right-column li.no .description {
font-size: 14px;
font-size: 14px;
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -1,388 +1,388 @@
table.table {
text-align: center;
text-align: center;
}
table.table.project-list.hidden {
display: none;
display: none;
}
.table td {
padding: 0 10px;
vertical-align: middle;
padding: 0 10px;
vertical-align: middle;
}
/* Used to designate from main style, e.g. deadline not set, no latest activity, project not ready... */
.table td .not,
.table td .not-ready {
font-style: italic;
font-style: italic;
}
.table tbody tr:hover {
background: #333941;
background: #333941;
}
.table th:first-child {
text-align: left;
text-align: left;
}
.table th.name {
width: 220px;
width: 220px;
}
.table th.resource {
width: 420px;
width: 420px;
}
.table th.resource.with-deadline:not(.with-priority) {
width: 310px;
width: 310px;
}
.table th.resource.with-priority:not(.with-deadline) {
width: 330px;
width: 330px;
}
.table th.resource.with-deadline.with-priority {
width: 220px;
width: 220px;
}
.table th.tag {
width: 330px;
width: 330px;
}
.table th.population,
.table th.deadline {
width: 90px;
width: 90px;
}
.table th.code,
.table th.priority {
width: 70px;
width: 70px;
}
.table th.latest-activity,
.table th.all-strings {
width: 140px;
width: 140px;
}
.table th.unreviewed-status {
position: relative;
width: 16px;
position: relative;
width: 16px;
}
.table-sort th.unreviewed-status i {
margin: -13px 0 0 13px;
margin: -13px 0 0 13px;
}
.table .unreviewed-status span {
position: absolute;
font-size: 18px;
margin: -15px 0 0 -5px;
position: absolute;
font-size: 18px;
margin: -15px 0 0 -5px;
}
.table .progress {
text-align: left;
text-align: left;
}
.table h4 {
overflow: hidden;
padding-left: 1px; /* Needed to avoid cutting off text due to overflow: hidden; */
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
padding-left: 1px; /* Needed to avoid cutting off text due to overflow: hidden; */
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
.table .name h4 {
width: 219px;
width: 219px;
}
.table .resource h4 {
width: 419px;
width: 419px;
}
.table .resource.with-deadline:not(.with-priority) h4 {
width: 309px;
width: 309px;
}
.table .resource.with-priority:not(.with-deadline) h4 {
width: 329px;
width: 329px;
}
.table .resource.with-deadline.with-priority h4 {
width: 219px;
width: 219px;
}
.table h4 a {
font-size: 15px;
line-height: 47px;
padding: 12px 0 11px;
font-size: 15px;
line-height: 47px;
padding: 12px 0 11px;
}
.table a {
color: #ebebeb;
color: #ebebeb;
}
.table a:hover {
color: #7bc876;
color: #7bc876;
}
/* Selector must also match heading-info */
.deadline time.approaching {
color: #ffa10f;
color: #ffa10f;
}
/* Selector must also match heading-info */
.deadline time.overdue {
color: #f36;
color: #f36;
}
.table td.code div {
width: 70px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 70px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.table td.code a {
color: #7bc876;
line-height: 47px;
padding: 15px 5px 14px;
color: #7bc876;
line-height: 47px;
padding: 15px 5px 14px;
}
.table td.priority {
padding-left: 15px;
padding-left: 15px;
}
.table td.priority .fa {
margin-left: -1px;
font-size: 12px;
margin-left: -1px;
font-size: 12px;
}
.table .latest-activity .latest {
display: block;
position: relative;
width: 140px;
display: block;
position: relative;
width: 140px;
-moz-box-sizing: border-box;
box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.table .latest-activity time {
white-space: nowrap;
white-space: nowrap;
}
.table .latest-activity time:hover {
color: #ebebeb;
color: #ebebeb;
}
.table .latest-activity .tooltip {
display: block;
background: #1c1e21;
border-radius: 10px;
bottom: 30px;
color: #ebebeb;
left: -100px;
padding: 10px;
position: absolute;
text-align: left;
width: 320px;
z-index: 20;
display: block;
background: #1c1e21;
border-radius: 10px;
bottom: 30px;
color: #ebebeb;
left: -100px;
padding: 10px;
position: absolute;
text-align: left;
width: 320px;
z-index: 20;
}
.table .latest-activity .tooltip:after {
content: '';
position: absolute;
border: 10px solid;
border-color: #1c1e21 transparent transparent transparent;
bottom: -20px;
left: 160px; /* Must be (tooltip width + tooltip padding + bottom) / 2 */
clip: rect(0 20px 10px 0);
-webkit-clip-path: polygon(0 0, 100% 0, 100% 50%, 0 50%);
clip-path: polygon(0 0, 100% 0, 100% 50%, 0 50%);
content: '';
position: absolute;
border: 10px solid;
border-color: #1c1e21 transparent transparent transparent;
bottom: -20px;
left: 160px; /* Must be (tooltip width + tooltip padding + bottom) / 2 */
clip: rect(0 20px 10px 0);
-webkit-clip-path: polygon(0 0, 100% 0, 100% 50%, 0 50%);
clip-path: polygon(0 0, 100% 0, 100% 50%, 0 50%);
}
.table .latest-activity .tooltip .quote {
color: #7bc876;
font-size: 30px;
color: #7bc876;
font-size: 30px;
}
.table .latest-activity .tooltip .translation {
display: block;
margin: -20px 0 0 40px;
overflow-wrap: break-word;
display: block;
margin: -20px 0 0 40px;
overflow-wrap: break-word;
}
.table .latest-activity .tooltip footer {
color: #888888;
font-style: italic;
height: 48px;
margin-top: 10px;
position: relative;
color: #888888;
font-style: italic;
height: 48px;
margin-top: 10px;
position: relative;
}
.table .latest-activity .tooltip footer .wrapper {
bottom: 0;
position: absolute;
right: 0;
bottom: 0;
position: absolute;
right: 0;
}
.table .latest-activity .tooltip footer .translation-details {
display: inline-block;
padding-top: 8px;
text-align: right;
display: inline-block;
padding-top: 8px;
text-align: right;
}
.table .latest-activity .tooltip footer .translation-action {
overflow-y: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 264px;
overflow-y: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 264px;
}
.table .latest-activity .tooltip footer .translation-action a {
color: #7bc876;
color: #7bc876;
}
.table .latest-activity .tooltip footer img {
display: inline-block;
margin-left: 8px;
display: inline-block;
margin-left: 8px;
}
.table .progress .chart-wrapper {
position: relative;
position: relative;
}
.table .progress .chart-wrapper .chart {
display: table;
font-size: 0;
height: 3px;
margin-top: 2px;
table-layout: fixed;
width: 295px;
display: table;
font-size: 0;
height: 3px;
margin-top: 2px;
table-layout: fixed;
width: 295px;
}
.table .progress .chart-wrapper .chart span {
display: table-cell;
height: 100%;
display: table-cell;
height: 100%;
}
.table .progress .chart-wrapper .percent {
position: absolute;
right: 25px;
top: -7px;
position: absolute;
right: 25px;
top: -7px;
}
.table .progress .chart-wrapper .unreviewed-status {
color: #3f4752;
font-size: 18px;
position: absolute;
right: 0;
top: -9px;
color: #3f4752;
font-size: 18px;
position: absolute;
right: 0;
top: -9px;
}
.table .progress .chart-wrapper .unreviewed-status.pending {
color: #4fc4f6;
color: #4fc4f6;
}
.table .progress .chart-wrapper .translated {
background: #7bc876;
background: #7bc876;
}
.table .progress .chart-wrapper .fuzzy {
background: #fed271;
background: #fed271;
}
.table .progress .chart-wrapper .warnings {
background: #ffa10f;
background: #ffa10f;
}
.table .progress .chart-wrapper .errors {
background: #f36;
background: #f36;
}
.table .progress .chart-wrapper .missing {
background: #5f7285;
background: #5f7285;
}
.table tr:hover .progress .chart-wrapper {
display: none;
display: none;
}
.table .progress .legend {
display: none;
margin-bottom: -9px;
display: none;
margin-bottom: -9px;
}
.table tr:hover .progress .legend {
display: block;
display: block;
}
.table .progress .legend ul {
font-size: 0;
margin-top: -1px;
font-size: 0;
margin-top: -1px;
}
.table .progress .legend li {
display: inline-block;
padding: 0;
text-align: center;
width: 51px;
display: inline-block;
padding: 0;
text-align: center;
width: 51px;
}
.table .progress .legend li:last-child {
width: 54px;
width: 54px;
}
.table .progress .legend li a {
color: #aaaaaa;
display: block;
margin-top: -5px;
color: #aaaaaa;
display: block;
margin-top: -5px;
}
.table .progress .legend li .title {
font-size: 10px;
font-weight: 400;
line-height: 10px;
text-transform: uppercase;
font-size: 10px;
font-weight: 400;
line-height: 10px;
text-transform: uppercase;
}
.table .progress .legend li.translated .title {
color: #7bc876;
color: #7bc876;
}
.table .progress .legend li.fuzzy .title {
color: #fed271;
color: #fed271;
}
.table .progress .legend li.warnings .title {
color: #ffa10f;
color: #ffa10f;
}
.table .progress .legend li.errors .title {
color: #f36;
color: #f36;
}
.table .progress .legend li.missing .title {
color: #7c8b9c;
color: #7c8b9c;
}
.table .progress .legend li.unreviewed .title {
color: #4fc4f6;
color: #4fc4f6;
}
.table .progress .legend li.all .title {
color: #aaaaaa;
color: #aaaaaa;
}
.table .progress .legend li .value {
font-size: 15px;
line-height: 22px;
font-size: 15px;
line-height: 22px;
}
.table .progress .legend li a:hover .title {
color: #ebebeb;
color: #ebebeb;
}
.table .progress .legend li a:hover .value {
color: #ebebeb;
font-weight: 700;
color: #ebebeb;
font-weight: 700;
}

Просмотреть файл

@ -1,22 +1,22 @@
#main .container {
color: #aaaaaa;
font-size: 16px;
font-weight: 300;
color: #aaaaaa;
font-size: 16px;
font-weight: 300;
}
#main a:link,
#main a:visited {
color: #7bc876;
color: #7bc876;
}
#main p,
#main dd {
margin-bottom: 40px;
line-height: 1.5em;
margin-bottom: 40px;
line-height: 1.5em;
}
#main dt {
color: #ebebeb;
font-size: 28px;
padding-bottom: 10px;
color: #ebebeb;
font-size: 28px;
padding-bottom: 10px;
}

Просмотреть файл

@ -1,73 +1,73 @@
/* A 3-column selector to select two lists */
$(function () {
function getTarget(item) {
var list = $(item).parents('.select');
var target = list.siblings('.select:nth-child(2)');
function getTarget(item) {
var list = $(item).parents('.select');
var target = list.siblings('.select:nth-child(2)');
if (list.is('.select:nth-child(2)')) {
if ($(item).is('.left') || list.siblings().length === 1) {
target = list.siblings('.select:first-child');
} else {
target = list.siblings('.select:last-child');
}
}
return target;
if (list.is('.select:nth-child(2)')) {
if ($(item).is('.left') || list.siblings().length === 1) {
target = list.siblings('.select:first-child');
} else {
target = list.siblings('.select:last-child');
}
}
function setArrow(element, event) {
var x = event.pageX - element.offset().left;
return target;
}
if (element.outerWidth() / 2 > x) {
element.addClass('left');
} else {
element.removeClass('left');
}
function setArrow(element, event) {
var x = event.pageX - element.offset().left;
if (element.outerWidth() / 2 > x) {
element.addClass('left');
} else {
element.removeClass('left');
}
}
// Set translators arrow direction
$('body')
.on(
'mouseenter',
'.double-list-selector .select:nth-child(2) li',
function (e) {
setArrow($(this), e);
},
)
.on(
'mousemove',
'.double-list-selector .select:nth-child(2) li',
function (e) {
setArrow($(this), e);
},
);
// Move items between lists
var mainSelector = '.double-list-selector';
var itemSelector = mainSelector + ' .select li';
var allSelector = mainSelector + ' .move-all';
$('body').on('click', [itemSelector, allSelector].join(', '), function (e) {
e.preventDefault();
var target = getTarget(this);
var ul = target.find('ul');
var clone = null;
// Move selected item
if ($(this).is('li')) {
clone = $(this).remove();
}
// Move all items in the list
else {
clone = $(this)
.parents('.select')
.find('li:visible:not(".no-match")')
.remove();
}
// Set translators arrow direction
$('body')
.on(
'mouseenter',
'.double-list-selector .select:nth-child(2) li',
function (e) {
setArrow($(this), e);
},
)
.on(
'mousemove',
'.double-list-selector .select:nth-child(2) li',
function (e) {
setArrow($(this), e);
},
);
ul.append(clone.removeClass('hover'));
ul.scrollTop(ul[0].scrollHeight);
// Move items between lists
var mainSelector = '.double-list-selector';
var itemSelector = mainSelector + ' .select li';
var allSelector = mainSelector + ' .move-all';
$('body').on('click', [itemSelector, allSelector].join(', '), function (e) {
e.preventDefault();
var target = getTarget(this);
var ul = target.find('ul');
var clone = null;
// Move selected item
if ($(this).is('li')) {
clone = $(this).remove();
}
// Move all items in the list
else {
clone = $(this)
.parents('.select')
.find('li:visible:not(".no-match")')
.remove();
}
ul.append(clone.removeClass('hover'));
ul.scrollTop(ul[0].scrollHeight);
$('.double-list-selector .select:first-child').trigger('input').focus();
});
$('.double-list-selector .select:first-child').trigger('input').focus();
});
});

Просмотреть файл

@ -1,466 +1,457 @@
/* Must be available immediately */
// Add case insensitive :contains-like selector to jQuery (search)
$.expr[':'].containsi = function (a, i, m) {
return (
(a.textContent || a.innerText || '')
.toUpperCase()
.indexOf(m[3].toUpperCase()) >= 0
);
return (
(a.textContent || a.innerText || '')
.toUpperCase()
.indexOf(m[3].toUpperCase()) >= 0
);
};
/* Public functions used across different files */
var Pontoon = (function (my) {
return $.extend(true, my, {
/*
* Bind NProgress (slim progress bar on top of the page) to each AJAX request
*/
NProgressBind: function () {
NProgress.configure({ showSpinner: false });
$(document)
.bind('ajaxStart.nprogress', function () {
NProgress.start();
})
.bind('ajaxStop.nprogress', function () {
NProgress.done();
});
return $.extend(true, my, {
/*
* Bind NProgress (slim progress bar on top of the page) to each AJAX request
*/
NProgressBind: function () {
NProgress.configure({ showSpinner: false });
$(document)
.bind('ajaxStart.nprogress', function () {
NProgress.start();
})
.bind('ajaxStop.nprogress', function () {
NProgress.done();
});
},
/*
* Unbind NProgress
*/
NProgressUnbind: function () {
$(document).unbind('.nprogress');
},
/*
* Mark all notifications as read and update UI accordingly
*/
markAllNotificationsAsRead: function () {
this.NProgressUnbind();
$.ajax({
url: '/notifications/mark-all-as-read/',
success: function () {
$('#notifications.unread .button .badge').hide();
var unreadNotifications = $(
'.notifications .menu ul.notification-list li.notification-item[data-unread="true"]',
);
unreadNotifications.animate(
{ backgroundColor: 'transparent' },
1000,
function () {
// Remove inline style and unread mark to make hover work again
unreadNotifications.removeAttr('style').removeAttr('data-unread');
},
);
},
});
/*
* Unbind NProgress
*/
NProgressUnbind: function () {
$(document).unbind('.nprogress');
this.NProgressBind();
},
/*
* Log UX action
*/
logUxAction: function (action_type, experiment, data) {
this.NProgressUnbind();
$.ajax({
url: '/log-ux-action/',
type: 'POST',
data: {
csrfmiddlewaretoken: $('body').data('csrf'),
action_type,
experiment,
data: JSON.stringify(data),
},
});
/*
* Mark all notifications as read and update UI accordingly
*/
markAllNotificationsAsRead: function () {
this.NProgressUnbind();
this.NProgressBind();
},
$.ajax({
url: '/notifications/mark-all-as-read/',
success: function () {
$('#notifications.unread .button .badge').hide();
var unreadNotifications = $(
'.notifications .menu ul.notification-list li.notification-item[data-unread="true"]',
);
unreadNotifications.animate(
{ backgroundColor: 'transparent' },
1000,
function () {
// Remove inline style and unread mark to make hover work again
unreadNotifications
.removeAttr('style')
.removeAttr('data-unread');
},
);
},
});
this.NProgressBind();
/*
* Close notification
*/
closeNotification: function () {
$('.notification').animate(
{
top: '-60px',
},
/*
* Log UX action
*/
logUxAction: function (action_type, experiment, data) {
this.NProgressUnbind();
$.ajax({
url: '/log-ux-action/',
type: 'POST',
data: {
csrfmiddlewaretoken: $('body').data('csrf'),
action_type,
experiment,
data: JSON.stringify(data),
},
});
this.NProgressBind();
{
duration: 200,
},
/*
* Close notification
*/
closeNotification: function () {
$('.notification').animate(
{
top: '-60px',
},
{
duration: 200,
},
function () {
$(this).addClass('hide').empty();
},
);
function () {
$(this).addClass('hide').empty();
},
);
},
/*
* Remove loader
*
* text End of operation text (e.g. Done!)
* type Notification type (e.g. error)
* duration How long should the notification remain open (default: 2000 ms)
*/
endLoader: function (text, type, duration) {
if (text) {
$('.notification')
.html('<li class="' + (type || '') + '">' + text + '</li>')
.removeClass('hide')
.animate(
{
top: 0,
},
{
duration: 200,
},
);
}
/*
* Remove loader
*
* text End of operation text (e.g. Done!)
* type Notification type (e.g. error)
* duration How long should the notification remain open (default: 2000 ms)
*/
endLoader: function (text, type, duration) {
if (text) {
$('.notification')
.html('<li class="' + (type || '') + '">' + text + '</li>')
.removeClass('hide')
.animate(
{
top: 0,
},
{
duration: 200,
},
);
}
if (Pontoon.notificationTimeout) {
clearTimeout(Pontoon.notificationTimeout);
}
Pontoon.notificationTimeout = setTimeout(function () {
Pontoon.closeNotification();
}, duration || 2000);
},
if (Pontoon.notificationTimeout) {
clearTimeout(Pontoon.notificationTimeout);
}
Pontoon.notificationTimeout = setTimeout(function () {
Pontoon.closeNotification();
}, duration || 2000);
},
/*
* Do not render HTML tags
*
* string String that has to be displayed as is instead of rendered
*/
doNotRender: function (string) {
return $('<div/>').text(string).html();
},
});
/*
* Do not render HTML tags
*
* string String that has to be displayed as is instead of rendered
*/
doNotRender: function (string) {
return $('<div/>').text(string).html();
},
});
})(Pontoon || {});
/* Main code */
$(function () {
/*
* If Google Analytics is enabled, the translate frontend will send additional about Ajax calls.
*
* To send an event to GA, We pass following informations:
* event category - hardcoded 'ajax' string.
* event action - hardcoded 'request' string.
* event label - contains url that was called by $.ajax() call.
*
* GA Analytics enriches every event with additional information like e.g. browser, resolution, country etc.
*/
$(document).ajaxComplete(function (event, jqXHR, settings) {
if (typeof ga !== 'function') {
return;
}
/*
* If Google Analytics is enabled, the translate frontend will send additional about Ajax calls.
*
* To send an event to GA, We pass following informations:
* event category - hardcoded 'ajax' string.
* event action - hardcoded 'request' string.
* event label - contains url that was called by $.ajax() call.
*
* GA Analytics enriches every event with additional information like e.g. browser, resolution, country etc.
*/
$(document).ajaxComplete(function (event, jqXHR, settings) {
if (typeof ga !== 'function') {
return;
}
ga('send', 'event', 'ajax', 'request', settings.url);
});
ga('send', 'event', 'ajax', 'request', settings.url);
});
/*
* Display Pontoon Add-On Promotion, if:
*
* - Promotion not dismissed
* - Add-On not installed
* - Page loaded on Firefox or Chrome (add-on not available for other browsers)
*/
setTimeout(function () {
var dismissed = !$('#addon-promotion').length;
var installed = window.PontoonAddon && window.PontoonAddon.installed;
if (!dismissed && !installed) {
var isFirefox = navigator.userAgent.indexOf('Firefox') !== -1;
var isChrome = navigator.userAgent.indexOf('Chrome') !== -1;
var downloadHref = '';
if (isFirefox) {
downloadHref =
'https://addons.mozilla.org/firefox/addon/pontoon-tools/';
}
if (isChrome) {
downloadHref =
'https://chrome.google.com/webstore/detail/pontoon-add-on/gnbfbnpjncpghhjmmhklfhcglbopagbb';
}
if (downloadHref) {
$('#addon-promotion').find('.get').attr('href', downloadHref);
$('body').addClass('addon-promotion-active');
}
}
// window.PontoonAddon is made available by the Pontoon Add-On,
// but not immediatelly after the DOM is ready
}, 1000);
/*
* Display Pontoon Add-On Promotion, if:
*
* - Promotion not dismissed
* - Add-On not installed
* - Page loaded on Firefox or Chrome (add-on not available for other browsers)
*/
setTimeout(function () {
var dismissed = !$('#addon-promotion').length;
var installed = window.PontoonAddon && window.PontoonAddon.installed;
if (!dismissed && !installed) {
var isFirefox = navigator.userAgent.indexOf('Firefox') !== -1;
var isChrome = navigator.userAgent.indexOf('Chrome') !== -1;
var downloadHref = '';
if (isFirefox) {
downloadHref =
'https://addons.mozilla.org/firefox/addon/pontoon-tools/';
}
if (isChrome) {
downloadHref =
'https://chrome.google.com/webstore/detail/pontoon-add-on/gnbfbnpjncpghhjmmhklfhcglbopagbb';
}
if (downloadHref) {
$('#addon-promotion').find('.get').attr('href', downloadHref);
$('body').addClass('addon-promotion-active');
}
}
// window.PontoonAddon is made available by the Pontoon Add-On,
// but not immediatelly after the DOM is ready
}, 1000);
// Dismiss Add-On Promotion
$('#addon-promotion .dismiss').click(function () {
Pontoon.NProgressUnbind();
// Dismiss Add-On Promotion
$('#addon-promotion .dismiss').click(function () {
Pontoon.NProgressUnbind();
$.ajax({
url: '/dismiss-addon-promotion/',
success: function () {
$('body').removeClass('addon-promotion-active');
},
});
Pontoon.NProgressBind();
});
// Hide Add-On Promotion if Add-On installed while active
window.addEventListener('message', (event) => {
// only allow messages from authorized senders (extension content script, or Pontoon itself)
if (event.origin !== window.origin || event.source !== window) {
return;
}
let data;
switch (typeof event.data) {
case 'object':
data = event.data;
break;
case 'string':
// backward compatibility
// TODO: remove some reasonable time after https://github.com/MikkCZ/pontoon-addon/pull/155 is released
// and convert this switch into a condition
try {
data = JSON.parse(event.data);
} catch (_) {
return;
}
break;
}
if (data && data._type === 'PontoonAddonInfo' && data.value) {
if (data.value.installed === true) {
$('body').removeClass('addon-promotion-active');
}
}
$.ajax({
url: '/dismiss-addon-promotion/',
success: function () {
$('body').removeClass('addon-promotion-active');
},
});
Pontoon.NProgressBind();
});
// Log display of the unread notification icon
if ($('#notifications').is('.unread')) {
Pontoon.logUxAction(
'Render: Unread notifications icon',
'Notifications 1.0',
{
pathname: window.location.pathname,
},
);
// Hide Add-On Promotion if Add-On installed while active
window.addEventListener('message', (event) => {
// only allow messages from authorized senders (extension content script, or Pontoon itself)
if (event.origin !== window.origin || event.source !== window) {
return;
}
// Log clicks on the notifications icon
$('#notifications .button').click(function () {
if ($('#notifications').is('.opened')) {
return;
let data;
switch (typeof event.data) {
case 'object':
data = event.data;
break;
case 'string':
// backward compatibility
// TODO: remove some reasonable time after https://github.com/MikkCZ/pontoon-addon/pull/155 is released
// and convert this switch into a condition
try {
data = JSON.parse(event.data);
} catch (_) {
return;
}
Pontoon.logUxAction('Click: Notifications icon', 'Notifications 1.0', {
pathname: window.location.pathname,
unread: $('#notifications').is('.unread'),
});
});
// Display any notifications
var notifications = $('.notification li');
if (notifications.length) {
Pontoon.endLoader(notifications.text());
break;
}
// Close notification on click
$('body > header').on('click', '.notification', function () {
Pontoon.closeNotification();
});
// Mark notifications as read when notification menu opens
$('#notifications.unread .button').click(function () {
Pontoon.markAllNotificationsAsRead();
});
function getRedirectUrl() {
return window.location.pathname + window.location.search;
if (data && data._type === 'PontoonAddonInfo' && data.value) {
if (data.value.installed === true) {
$('body').removeClass('addon-promotion-active');
}
}
});
// Sign in button action
$('#fxa-sign-in, #standalone-signin a, #sidebar-signin').on(
'click',
function () {
var $this = $(this);
var loginUrl = $this.prop('href'),
startSign = loginUrl.match(/\?/) ? '&' : '?';
$this.prop(
'href',
loginUrl + startSign + 'next=' + getRedirectUrl(),
);
},
Pontoon.NProgressBind();
// Log display of the unread notification icon
if ($('#notifications').is('.unread')) {
Pontoon.logUxAction(
'Render: Unread notifications icon',
'Notifications 1.0',
{
pathname: window.location.pathname,
},
);
}
// Sign out button action
$('.sign-out a, #sign-out a').on('click', function (ev) {
var $this = $(this),
$form = $this.find('form');
// Log clicks on the notifications icon
$('#notifications .button').click(function () {
if ($('#notifications').is('.opened')) {
return;
}
ev.preventDefault();
$form.prop('action', $this.prop('href') + '?next=' + getRedirectUrl());
$form.submit();
Pontoon.logUxAction('Click: Notifications icon', 'Notifications 1.0', {
pathname: window.location.pathname,
unread: $('#notifications').is('.unread'),
});
});
// Display any notifications
var notifications = $('.notification li');
if (notifications.length) {
Pontoon.endLoader(notifications.text());
}
// Close notification on click
$('body > header').on('click', '.notification', function () {
Pontoon.closeNotification();
});
// Mark notifications as read when notification menu opens
$('#notifications.unread .button').click(function () {
Pontoon.markAllNotificationsAsRead();
});
function getRedirectUrl() {
return window.location.pathname + window.location.search;
}
// Sign in button action
$('#fxa-sign-in, #standalone-signin a, #sidebar-signin').on(
'click',
function () {
var $this = $(this);
var loginUrl = $this.prop('href'),
startSign = loginUrl.match(/\?/) ? '&' : '?';
$this.prop('href', loginUrl + startSign + 'next=' + getRedirectUrl());
},
);
// Sign out button action
$('.sign-out a, #sign-out a').on('click', function (ev) {
var $this = $(this),
$form = $this.find('form');
ev.preventDefault();
$form.prop('action', $this.prop('href') + '?next=' + getRedirectUrl());
$form.submit();
});
// Show/hide menu on click
$('body').on('click', '.selector', function (e) {
if (!$(this).siblings('.menu').is(':visible')) {
e.stopPropagation();
$('.menu:not(".permanent")').hide();
$('.select').removeClass('opened');
$(this)
.siblings('.menu')
.show()
.end()
.parents('.select')
.addClass('opened');
$('.menu:not(".permanent"):visible input[type=search]')
.focus()
.trigger('input');
}
});
// Hide menus on click outside
$('body').bind('click.main', function () {
$('.menu:not(".permanent")').hide();
$('.select').removeClass('opened');
$('.menu:not(".permanent") li').removeClass('hover');
});
// Menu hover
$('body')
.on('mouseenter', '.menu li', function () {
// Ignore on nested menus
if ($(this).parents('li').length) {
return false;
}
$('.menu li.hover').removeClass('hover');
$(this).toggleClass('hover');
})
.on('mouseleave', '.menu li', function () {
// Ignore on nested menus
if ($(this).parents('li').length) {
return false;
}
$('.menu li.hover').removeClass('hover');
});
// Show/hide menu on click
$('body').on('click', '.selector', function (e) {
if (!$(this).siblings('.menu').is(':visible')) {
e.stopPropagation();
$('.menu:not(".permanent")').hide();
$('.select').removeClass('opened');
$(this)
.siblings('.menu')
.show()
.end()
.parents('.select')
.addClass('opened');
$('.menu:not(".permanent"):visible input[type=search]')
.focus()
.trigger('input');
}
// Menu search
$('body')
.on('click', '.menu input[type=search]', function (e) {
e.stopPropagation();
})
.on('input.search', '.menu input[type=search]', function (e) {
// Tab
if (e.which === 9) {
return;
}
var ul = $(this).parent().siblings('ul'),
val = $(this).val(),
// Only search a limited set if defined
limited = ul.find('li.limited').length > 0 ? '.limited' : '';
ul.find('li' + limited)
.show()
.end()
.find('li' + limited + ':not(":containsi(\'' + val + '\')")')
.hide();
if (ul.find('li:not(".no-match"):visible').length === 0) {
ul.find('.no-match').show();
} else {
ul.find('.no-match').hide();
}
})
.on('keydown.search', '.menu input[type=search]', function (e) {
// Prevent form submission on Enter
if (e.which === 13) {
return false;
}
});
// Hide menus on click outside
$('body').bind('click.main', function () {
$('.menu:not(".permanent")').hide();
$('.select').removeClass('opened');
$('.menu:not(".permanent") li').removeClass('hover');
});
// General keyboard shortcuts
generalShortcutsHandler = function (e) {
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
// Menu hover
$('body')
.on('mouseenter', '.menu li', function () {
// Ignore on nested menus
if ($(this).parents('li').length) {
return false;
}
function moveMenu(type) {
var options =
type === 'up' ? ['first', 'last', -1] : ['last', 'first', 1];
var items = menu.find('li:visible:not(.horizontal-separator, :has(li))');
var element = null;
$('.menu li.hover').removeClass('hover');
$(this).toggleClass('hover');
})
.on('mouseleave', '.menu li', function () {
// Ignore on nested menus
if ($(this).parents('li').length) {
return false;
}
if (
hovered.length === 0 ||
menu.find('li:not(:has(li)):visible:' + options[0]).is('.hover')
) {
menu.find('li.hover').removeClass('hover');
element = items[options[1]]();
} else {
var current = menu.find('li.hover'),
next = items.index(current) + options[2];
$('.menu li.hover').removeClass('hover');
current.removeClass('hover');
element = $(items.get(next));
}
if (element) {
const behavior = mediaQuery.matches ? 'auto' : 'smooth';
element.addClass('hover');
element[0].scrollIntoView({
behavior: behavior,
block: 'nearest',
});
}
}
// Menu search
$('body')
.on('click', '.menu input[type=search]', function (e) {
e.stopPropagation();
})
.on('input.search', '.menu input[type=search]', function (e) {
// Tab
if (e.which === 9) {
return;
}
var key = e.which;
var ul = $(this).parent().siblings('ul'),
val = $(this).val(),
// Only search a limited set if defined
limited = ul.find('li.limited').length > 0 ? '.limited' : '';
if ($('.menu:not(".permanent")').is(':visible')) {
var menu = $('.menu:not(".permanent"):visible'),
hovered = menu.find('li.hover');
ul.find('li' + limited)
.show()
.end()
.find('li' + limited + ':not(":containsi(\'' + val + '\')")')
.hide();
// Skip for the tabs
if (menu.is('.tabs')) {
return;
}
if (ul.find('li:not(".no-match"):visible').length === 0) {
ul.find('.no-match').show();
} else {
ul.find('.no-match').hide();
}
})
.on('keydown.search', '.menu input[type=search]', function (e) {
// Prevent form submission on Enter
if (e.which === 13) {
return false;
}
});
// Up arrow
if (key === 38) {
moveMenu('up');
return false;
}
// General keyboard shortcuts
generalShortcutsHandler = function (e) {
const mediaQuery = window.matchMedia(
'(prefers-reduced-motion: reduce)',
);
// Down arrow
if (key === 40) {
moveMenu('down');
return false;
}
function moveMenu(type) {
var options =
type === 'up' ? ['first', 'last', -1] : ['last', 'first', 1];
var items = menu.find(
'li:visible:not(.horizontal-separator, :has(li))',
);
var element = null;
if (
hovered.length === 0 ||
menu.find('li:not(:has(li)):visible:' + options[0]).is('.hover')
) {
menu.find('li.hover').removeClass('hover');
element = items[options[1]]();
} else {
var current = menu.find('li.hover'),
next = items.index(current) + options[2];
current.removeClass('hover');
element = $(items.get(next));
}
if (element) {
const behavior = mediaQuery.matches ? 'auto' : 'smooth';
element.addClass('hover');
element[0].scrollIntoView({
behavior: behavior,
block: 'nearest',
});
}
// Enter: confirm
if (key === 13) {
var a = hovered.find('a');
if (a.length > 0) {
a.click();
} else {
hovered.click();
}
return false;
}
var key = e.which;
if ($('.menu:not(".permanent")').is(':visible')) {
var menu = $('.menu:not(".permanent"):visible'),
hovered = menu.find('li.hover');
// Skip for the tabs
if (menu.is('.tabs')) {
return;
}
// Up arrow
if (key === 38) {
moveMenu('up');
return false;
}
// Down arrow
if (key === 40) {
moveMenu('down');
return false;
}
// Enter: confirm
if (key === 13) {
var a = hovered.find('a');
if (a.length > 0) {
a.click();
} else {
hovered.click();
}
return false;
}
// Escape: close
if (key === 27) {
$('body').click();
return false;
}
}
};
$('html').on('keydown', generalShortcutsHandler);
// Escape: close
if (key === 27) {
$('body').click();
return false;
}
}
};
$('html').on('keydown', generalShortcutsHandler);
});

Просмотреть файл

@ -2,77 +2,73 @@
* Draw progress indicator and value
*/
$(function () {
$('canvas.chart').each(function () {
// Get data
var stats = {},
progress = $(this).parents('.progress');
$('canvas.chart').each(function () {
// Get data
var stats = {},
progress = $(this).parents('.progress');
progress
.siblings('.legend')
.find('li')
.each(function () {
stats[$(this).attr('class')] = $(this)
.find('.value')
.data('value');
});
progress
.siblings('.legend')
.find('li')
.each(function () {
stats[$(this).attr('class')] = $(this).find('.value').data('value');
});
stats.all = progress
.siblings('.non-plottable')
.find('.all .value')
.data('value');
stats.all = progress
.siblings('.non-plottable')
.find('.all .value')
.data('value');
var fraction = {
translated: stats.all ? stats.translated / stats.all : 0,
fuzzy: stats.all ? stats.fuzzy / stats.all : 0,
warnings: stats.all ? stats.warnings / stats.all : 0,
errors: stats.all ? stats.errors / stats.all : 0,
missing: stats.all
? stats.missing / stats.all
: 1 /* Draw "empty" progress if no projects enabled */,
},
number = Math.floor(
(fraction.translated + fraction.warnings) * 100,
);
var fraction = {
translated: stats.all ? stats.translated / stats.all : 0,
fuzzy: stats.all ? stats.fuzzy / stats.all : 0,
warnings: stats.all ? stats.warnings / stats.all : 0,
errors: stats.all ? stats.errors / stats.all : 0,
missing: stats.all
? stats.missing / stats.all
: 1 /* Draw "empty" progress if no projects enabled */,
},
number = Math.floor((fraction.translated + fraction.warnings) * 100);
// Update graph
var canvas = this,
context = canvas.getContext('2d');
// Update graph
var canvas = this,
context = canvas.getContext('2d');
// Set up canvas to be HiDPI display ready
var dpr = window.devicePixelRatio || 1;
canvas.style.width = canvas.width + 'px';
canvas.style.height = canvas.height + 'px';
canvas.width = canvas.width * dpr;
canvas.height = canvas.height * dpr;
// Set up canvas to be HiDPI display ready
var dpr = window.devicePixelRatio || 1;
canvas.style.width = canvas.width + 'px';
canvas.style.height = canvas.height + 'px';
canvas.width = canvas.width * dpr;
canvas.height = canvas.height * dpr;
// Clear old canvas content to avoid aliasing
context.clearRect(0, 0, canvas.width, canvas.height);
context.lineWidth = 3 * dpr;
// Clear old canvas content to avoid aliasing
context.clearRect(0, 0, canvas.width, canvas.height);
context.lineWidth = 3 * dpr;
var x = canvas.width / 2,
y = canvas.height / 2,
radius = (canvas.width - context.lineWidth) / 2,
end = -0.5;
var x = canvas.width / 2,
y = canvas.height / 2,
radius = (canvas.width - context.lineWidth) / 2,
end = -0.5;
progress
.siblings('.legend')
.find('li')
.each(function () {
var length = fraction[$(this).attr('class')] * 2,
start = end,
color = window
.getComputedStyle($(this).find('.status')[0], ':before')
.getPropertyValue('color');
progress
.siblings('.legend')
.find('li')
.each(function () {
var length = fraction[$(this).attr('class')] * 2,
start = end,
color = window
.getComputedStyle($(this).find('.status')[0], ':before')
.getPropertyValue('color');
end = start + length;
end = start + length;
context.beginPath();
context.arc(x, y, radius, start * Math.PI, end * Math.PI);
context.strokeStyle = color;
context.stroke();
});
context.beginPath();
context.arc(x, y, radius, start * Math.PI, end * Math.PI);
context.strokeStyle = color;
context.stroke();
});
// Update number
progress.find('.number').html(number).show();
});
// Update number
progress.find('.number').html(number).show();
});
});

Просмотреть файл

@ -1,14 +1,14 @@
/* Sidebar with left column acting as menu and right column as panel do display content */
$(function () {
$('body').on('click', '.menu.left-column > ul > li > a', function (e) {
e.preventDefault();
$('body').on('click', '.menu.left-column > ul > li > a', function (e) {
e.preventDefault();
$(this)
.parents('li')
.addClass('selected')
.siblings()
.removeClass('selected');
$(this)
.parents('li')
.addClass('selected')
.siblings()
.removeClass('selected');
$($(this).data('target')).show().siblings().hide();
});
$($(this).data('target')).show().siblings().hide();
});
});

Просмотреть файл

@ -1,279 +1,257 @@
/* Must be available immediately */
// Add case insensitive :contains-like selector to jQuery (search & filter)
$.expr[':'].containsi = function (a, i, m) {
return (
(a.textContent || a.innerText || '')
.toUpperCase()
.indexOf(m[3].toUpperCase()) >= 0
);
return (
(a.textContent || a.innerText || '')
.toUpperCase()
.indexOf(m[3].toUpperCase()) >= 0
);
};
/* Latest activity tooltip */
var date_formatter = new Intl.DateTimeFormat('en-GB', {
day: 'numeric',
month: 'long',
year: 'numeric',
}),
time_formatter = new Intl.DateTimeFormat('en-GB', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
}),
timer = null,
delay = 500;
day: 'numeric',
month: 'long',
year: 'numeric',
}),
time_formatter = new Intl.DateTimeFormat('en-GB', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
}),
timer = null,
delay = 500;
$('body')
.on('mouseenter', '.latest-activity .latest time', function () {
var $element = $(this);
.on('mouseenter', '.latest-activity .latest time', function () {
var $element = $(this);
timer = setTimeout(function () {
var translation = Pontoon.doNotRender($element.data('translation')),
avatar = $element.data('user-avatar'),
action = $element.data('action'),
name = $element.data('user-name'),
link = $element.data('user-link'),
date = date_formatter.format(
new Date($element.attr('datetime')),
),
time = time_formatter.format(
new Date($element.attr('datetime')),
);
timer = setTimeout(function () {
var translation = Pontoon.doNotRender($element.data('translation')),
avatar = $element.data('user-avatar'),
action = $element.data('action'),
name = $element.data('user-name'),
link = $element.data('user-link'),
date = date_formatter.format(new Date($element.attr('datetime'))),
time = time_formatter.format(new Date($element.attr('datetime')));
$element.after(
'<aside class="tooltip">' +
'<span class="quote fa fa-2x fa-quote-right"></span>' +
'<p class="translation">' +
translation +
'</p>' +
'<footer class="clearfix">' +
'<div class="wrapper">' +
'<div class="translation-details">' +
'<p class="translation-action">' +
action +
' <a href="' +
link +
'">' +
name +
'</a></p>' +
'<p class="translation-time">on ' +
date +
' at ' +
time +
'</p>' +
'</div>' +
(avatar
? '<img class="rounded" height="44" width="44" src="' +
avatar +
'">'
: '') +
'</div>' +
'</footer>' +
'</aside>',
);
}, delay);
})
.on('mouseleave', 'td.latest-activity', function () {
$('.latest-activity .latest .tooltip').remove();
clearTimeout(timer);
});
$element.after(
'<aside class="tooltip">' +
'<span class="quote fa fa-2x fa-quote-right"></span>' +
'<p class="translation">' +
translation +
'</p>' +
'<footer class="clearfix">' +
'<div class="wrapper">' +
'<div class="translation-details">' +
'<p class="translation-action">' +
action +
' <a href="' +
link +
'">' +
name +
'</a></p>' +
'<p class="translation-time">on ' +
date +
' at ' +
time +
'</p>' +
'</div>' +
(avatar
? '<img class="rounded" height="44" width="44" src="' +
avatar +
'">'
: '') +
'</div>' +
'</footer>' +
'</aside>',
);
}, delay);
})
.on('mouseleave', 'td.latest-activity', function () {
$('.latest-activity .latest .tooltip').remove();
clearTimeout(timer);
});
/* Public functions used across different files */
var Pontoon = (function (my) {
return $.extend(true, my, {
table: {
/*
* Filter table
*
* TODO: remove old search code from main.js
*/
filter: (function () {
$('body').on(
'input.filter',
'input.table-filter',
function (e) {
if (e.which === 9) {
return;
}
return $.extend(true, my, {
table: {
/*
* Filter table
*
* TODO: remove old search code from main.js
*/
filter: (function () {
$('body').on('input.filter', 'input.table-filter', function (e) {
if (e.which === 9) {
return;
}
// Filter input field
var field = $(this),
// Selector of the element containing a list of items to filter
list = $(this).data('list') || '.table-sort tbody',
// Selector of the list item element, relative to list
item = $(this).data('item') || 'tr',
// Selector of the list item element's child to match filter query against
filter = $(this).data('filter') || 'td:first-child';
// Filter input field
var field = $(this),
// Selector of the element containing a list of items to filter
list = $(this).data('list') || '.table-sort tbody',
// Selector of the list item element, relative to list
item = $(this).data('item') || 'tr',
// Selector of the list item element's child to match filter query against
filter = $(this).data('filter') || 'td:first-child';
$(list)
.find(item + '.limited')
.hide()
.end()
.find(
item +
'.limited ' +
filter +
':containsi("' +
$(field).val() +
'")',
)
.parents(item)
.show();
},
);
})(),
$(list)
.find(item + '.limited')
.hide()
.end()
.find(
item +
'.limited ' +
filter +
':containsi("' +
$(field).val() +
'")',
)
.parents(item)
.show();
});
})(),
/*
* Sort table
*/
sort: (function () {
$('body').on('click', 'table.table-sort th', function () {
function getProgress(el) {
var legend = $(el).find('.progress .legend'),
all = legend.find('.all .value').data('value') || 0,
translated =
legend
.find('.translated .value')
.data('value') / all || 0,
fuzzy =
legend.find('.fuzzy .value').data('value') /
all || 0;
/*
* Sort table
*/
sort: (function () {
$('body').on('click', 'table.table-sort th', function () {
function getProgress(el) {
var legend = $(el).find('.progress .legend'),
all = legend.find('.all .value').data('value') || 0,
translated =
legend.find('.translated .value').data('value') / all || 0,
fuzzy = legend.find('.fuzzy .value').data('value') / all || 0;
if ($(el).find('.progress .not-ready').length) {
return 'not-ready';
}
if ($(el).find('.progress .not-ready').length) {
return 'not-ready';
}
return {
translated: translated,
fuzzy: fuzzy,
};
}
return {
translated: translated,
fuzzy: fuzzy,
};
}
function getUnreviewed(el) {
return parseInt(
$(el)
.find('.progress .legend .unreviewed .value')
.data('value') || 0,
);
}
function getUnreviewed(el) {
return parseInt(
$(el)
.find('.progress .legend .unreviewed .value')
.data('value') || 0,
);
}
function getTime(el) {
var date =
$(el)
.find('td:eq(' + index + ')')
.find('time')
.attr('datetime') || 0;
return new Date(date).getTime();
}
function getTime(el) {
var date =
$(el)
.find('td:eq(' + index + ')')
.find('time')
.attr('datetime') || 0;
return new Date(date).getTime();
}
function getPriority(el) {
return $(el).find('.priority .fa-star.active').length;
}
function getPriority(el) {
return $(el).find('.priority .fa-star.active').length;
}
function getEnabled(el) {
return $(el).find('.check.enabled').length;
}
function getEnabled(el) {
return $(el).find('.check.enabled').length;
}
function getNumber(el) {
return parseInt(
$(el).find('span').text().replace(/,/g, ''),
);
}
function getNumber(el) {
return parseInt($(el).find('span').text().replace(/,/g, ''));
}
function getString(el) {
return $(el)
.find('td:eq(' + index + ')')
.text();
}
function getString(el) {
return $(el)
.find('td:eq(' + index + ')')
.text();
}
var node = $(this),
index = node.index(),
table = node.parents('.table-sort'),
list = table.find('tbody'),
items = list.find('tr'),
dir = node.hasClass('asc') ? -1 : 1,
cls = node.hasClass('asc') ? 'desc' : 'asc';
var node = $(this),
index = node.index(),
table = node.parents('.table-sort'),
list = table.find('tbody'),
items = list.find('tr'),
dir = node.hasClass('asc') ? -1 : 1,
cls = node.hasClass('asc') ? 'desc' : 'asc';
// Default value for rows which don't have a timestamp
if (node.is('.deadline')) {
var defaultTime = new Date(0).getTime();
}
// Default value for rows which don't have a timestamp
if (node.is('.deadline')) {
var defaultTime = new Date(0).getTime();
}
$(table).find('th').removeClass('asc desc');
node.addClass(cls);
$(table).find('th').removeClass('asc desc');
node.addClass(cls);
items.sort(function (a, b) {
// Sort by translated, then by fuzzy percentage
if (node.is('.progress')) {
var chartA = getProgress(a),
chartB = getProgress(b);
items.sort(function (a, b) {
// Sort by translated, then by fuzzy percentage
if (node.is('.progress')) {
var chartA = getProgress(a),
chartB = getProgress(b);
if (chartA === 'not-ready') {
if (chartB === 'not-ready') {
return 0;
} else {
return -1 * dir;
}
}
if (chartB === 'not-ready') {
return 1 * dir;
}
if (chartA === 'not-ready') {
if (chartB === 'not-ready') {
return 0;
} else {
return -1 * dir;
}
}
if (chartB === 'not-ready') {
return 1 * dir;
}
return (
(chartA.translated - chartB.translated) * dir ||
(chartA.fuzzy - chartB.fuzzy) * dir
);
return (
(chartA.translated - chartB.translated) * dir ||
(chartA.fuzzy - chartB.fuzzy) * dir
);
// Sort by unreviewed state
} else if (node.is('.unreviewed-status')) {
return (getUnreviewed(b) - getUnreviewed(a)) * dir;
// Sort by unreviewed state
} else if (node.is('.unreviewed-status')) {
return (getUnreviewed(b) - getUnreviewed(a)) * dir;
// Sort by deadline
} else if (node.is('.deadline')) {
var timeA = getTime(a),
timeB = getTime(b);
// Sort by deadline
} else if (node.is('.deadline')) {
var timeA = getTime(a),
timeB = getTime(b);
if (
timeA === defaultTime &&
timeB === defaultTime
) {
return (
getString(a).localeCompare(getString(b)) *
dir
);
} else if (timeA === defaultTime) {
return 1 * dir;
} else if (timeB === defaultTime) {
return -1 * dir;
}
return (timeA - timeB) * dir;
if (timeA === defaultTime && timeB === defaultTime) {
return getString(a).localeCompare(getString(b)) * dir;
} else if (timeA === defaultTime) {
return 1 * dir;
} else if (timeB === defaultTime) {
return -1 * dir;
}
return (timeA - timeB) * dir;
// Sort by last activity
} else if (node.is('.latest-activity')) {
return (getTime(b) - getTime(a)) * dir;
// Sort by last activity
} else if (node.is('.latest-activity')) {
return (getTime(b) - getTime(a)) * dir;
// Sort by priority
} else if (node.is('.priority')) {
return (getPriority(b) - getPriority(a)) * dir;
// Sort by priority
} else if (node.is('.priority')) {
return (getPriority(b) - getPriority(a)) * dir;
// Sort by enabled state
} else if (node.is('.check')) {
return (getEnabled(a) - getEnabled(b)) * dir;
// Sort by enabled state
} else if (node.is('.check')) {
return (getEnabled(a) - getEnabled(b)) * dir;
// Sort by number of speakers
} else if (node.is('.population')) {
return (getNumber(a) - getNumber(b)) * dir;
// Sort by number of speakers
} else if (node.is('.population')) {
return (getNumber(a) - getNumber(b)) * dir;
// Sort by alphabetical order
} else {
return (
getString(a).localeCompare(getString(b)) * dir
);
}
});
// Sort by alphabetical order
} else {
return getString(a).localeCompare(getString(b)) * dir;
}
});
list.append(items);
});
})(),
},
});
list.append(items);
});
})(),
},
});
})(Pontoon || {});

Просмотреть файл

@ -2,109 +2,109 @@
* Manage tab content as single-page application
*/
$(function () {
var urlSplit = $('#server').data('url-split'),
container = $('#main .container'),
inProgress = false;
var urlSplit = $('#server').data('url-split'),
container = $('#main .container'),
inProgress = false;
// Page load
// Page load
loadTabContent(window.location.pathname + window.location.search);
// History
window.onpopstate = function () {
loadTabContent(window.location.pathname + window.location.search);
};
// History
window.onpopstate = function () {
loadTabContent(window.location.pathname + window.location.search);
};
// Menu
$('body').on(
'click',
'#middle .links a, #main .contributors .links a',
function (e) {
// Keep default middle-, control- and command-click behaviour (open in new tab)
if (e.which === 2 || e.metaKey || e.ctrlKey) {
return;
}
// Menu
$('body').on(
'click',
'#middle .links a, #main .contributors .links a',
function (e) {
// Keep default middle-, control- and command-click behaviour (open in new tab)
if (e.which === 2 || e.metaKey || e.ctrlKey) {
return;
}
// Filtered teams are only supported by the Teams tab, so we need to drop them
// when switching to other tabs and update stats in the heading section by
// reloading the page
if (new URLSearchParams(window.location.search).get('teams')) {
return;
}
// Filtered teams are only supported by the Teams tab, so we need to drop them
// when switching to other tabs and update stats in the heading section by
// reloading the page
if (new URLSearchParams(window.location.search).get('teams')) {
return;
}
e.preventDefault();
e.preventDefault();
var url = $(this).attr('href');
loadTabContent(url);
window.history.pushState({}, '', url);
},
);
var url = $(this).attr('href');
loadTabContent(url);
window.history.pushState({}, '', url);
function showTabMessage(text) {
var message = $('<p>', {
class: 'no-results',
html: text,
});
container.append(message);
}
function updateTabCount(tab, count) {
tab.find('span').remove();
if (count > 0) {
$('<span>', {
class: 'count',
html: count,
}).appendTo(tab);
}
}
function loadTabContent(path) {
if (inProgress) {
inProgress.abort();
}
var url = '/' + path.split('/' + urlSplit + '/')[1],
tab = $('#middle .links a[href="' + path.split('?')[0] + '"]');
// Update menu
$('#middle .links li').removeClass('active');
tab.parents('li').addClass('active');
container.empty();
if (url !== '/bugs/') {
inProgress = $.ajax({
url: '/' + urlSplit + '/ajax' + url,
success: function (data) {
container.append(data);
if (url.startsWith('/contributors/')) {
var count = $('table > tbody > tr').length;
updateTabCount(tab, count);
}
if (url.startsWith('/insights/')) {
Pontoon.insights.initialize();
}
if (url === '/') {
$('.controls input').focus();
}
},
);
function showTabMessage(text) {
var message = $('<p>', {
class: 'no-results',
html: text,
});
container.append(message);
}
function updateTabCount(tab, count) {
tab.find('span').remove();
if (count > 0) {
$('<span>', {
class: 'count',
html: count,
}).appendTo(tab);
}
}
function loadTabContent(path) {
if (inProgress) {
inProgress.abort();
}
var url = '/' + path.split('/' + urlSplit + '/')[1],
tab = $('#middle .links a[href="' + path.split('?')[0] + '"]');
// Update menu
$('#middle .links li').removeClass('active');
tab.parents('li').addClass('active');
container.empty();
if (url !== '/bugs/') {
inProgress = $.ajax({
url: '/' + urlSplit + '/ajax' + url,
success: function (data) {
container.append(data);
if (url.startsWith('/contributors/')) {
var count = $('table > tbody > tr').length;
updateTabCount(tab, count);
}
if (url.startsWith('/insights/')) {
Pontoon.insights.initialize();
}
if (url === '/') {
$('.controls input').focus();
}
},
error: function (error) {
if (error.status === 0 && error.statusText !== 'abort') {
showTabMessage('Oops, something went wrong.');
}
},
});
} else {
inProgress = Pontoon.bugzilla.getLocaleBugs(
$('#server').data('locale'),
container,
tab,
updateTabCount,
showTabMessage,
);
}
error: function (error) {
if (error.status === 0 && error.statusText !== 'abort') {
showTabMessage('Oops, something went wrong.');
}
},
});
} else {
inProgress = Pontoon.bugzilla.getLocaleBugs(
$('#server').data('locale'),
container,
tab,
updateTabCount,
showTabMessage,
);
}
}
});

Просмотреть файл

@ -1,79 +1,79 @@
#heading,
#middle {
text-align: center;
text-align: center;
}
a.avatar {
display: block;
position: relative;
display: block;
position: relative;
}
a.avatar .desc {
bottom: 0;
color: #ffffff;
display: none;
font-size: 16px;
font-weight: bold;
height: 20px;
left: 0;
letter-spacing: 0;
margin: auto;
position: absolute;
right: 0;
top: 0;
bottom: 0;
color: #ffffff;
display: none;
font-size: 16px;
font-weight: bold;
height: 20px;
left: 0;
letter-spacing: 0;
margin: auto;
position: absolute;
right: 0;
top: 0;
}
a.avatar:hover .desc {
display: block;
display: block;
}
a.avatar:hover .desc ~ img {
opacity: 0.3;
opacity: 0.3;
}
#username {
color: #ebebeb;
font-size: 48px;
letter-spacing: normal;
padding: 8px 0 20px;
text-transform: none;
color: #ebebeb;
font-size: 48px;
letter-spacing: normal;
padding: 8px 0 20px;
text-transform: none;
}
.info {
list-style: none;
margin: 0;
display: inline-block;
text-align: left;
list-style: none;
margin: 0;
display: inline-block;
text-align: left;
}
.info li {
color: #aaaaaa;
display: table;
font-size: 16px;
font-weight: 300;
overflow: hidden;
padding: 4px 0;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 980px;
color: #aaaaaa;
display: table;
font-size: 16px;
font-weight: 300;
overflow: hidden;
padding: 4px 0;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 980px;
}
.info a {
color: #7bc876;
color: #7bc876;
}
.info .fa {
margin-right: 6px;
margin-right: 6px;
}
#timeline-loader {
display: none;
margin: 0 auto;
height: 50px;
width: 50px;
display: none;
margin: 0 auto;
height: 50px;
width: 50px;
}
#timeline-loader div {
color: #7bc876;
font-size: 48px;
color: #7bc876;
font-size: 48px;
}

Просмотреть файл

@ -1,94 +1,94 @@
#heading .banner .title {
color: #7bc876;
color: #7bc876;
}
#heading .legend {
width: 360px;
width: 360px;
}
#heading .legend li,
#heading .legend li.unreviewed {
padding-left: 0;
padding-left: 0;
}
#heading .legend li.banner {
padding: 0;
padding: 0;
}
body.top-contributors #heading .legend li .status.fa {
display: none;
display: none;
}
.controls .submenu .links {
text-align: right;
text-align: right;
}
.contributor {
display: inline-block;
position: relative;
width: 580px;
display: inline-block;
position: relative;
width: 580px;
}
.contributor img {
margin-right: 5px;
margin-right: 5px;
}
.contributor p {
overflow: hidden;
padding-left: 2px;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 520px;
overflow: hidden;
padding-left: 2px;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 520px;
-moz-box-sizing: border-box;
box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.contributor p.name {
display: inline-block;
font-size: 1.5em;
height: 1.5em;
display: inline-block;
font-size: 1.5em;
height: 1.5em;
}
.contributor p.role {
color: #ebebeb;
left: 67px;
position: absolute;
top: 42px;
color: #ebebeb;
left: 67px;
position: absolute;
top: 42px;
}
th:last-child,
.stats .details {
text-align: center;
width: 360px;
text-align: center;
width: 360px;
}
th:last-child {
position: relative;
position: relative;
}
th:last-child sup {
top: 7px;
position: absolute;
top: 7px;
position: absolute;
}
.stats .details div {
border: none;
border: none;
}
.stats .details div.approved {
color: #7bc876;
color: #7bc876;
}
.stats .details div.fuzzy {
color: #fed271;
color: #fed271;
}
.stats .details div.unreviewed {
color: #4fc4f6;
color: #4fc4f6;
}
.stats .details div p {
font-size: 20px;
padding: 3px 0 0;
font-size: 20px;
padding: 3px 0 0;
}

Просмотреть файл

@ -1,14 +1,14 @@
.right-column ul {
max-height: none;
max-height: none;
}
/* No notifications */
#main.no .left-column {
display: none;
display: none;
}
#main.no .right-column {
background: transparent;
float: none;
width: auto;
background: transparent;
float: none;
width: auto;
}

Просмотреть файл

@ -1,202 +1,202 @@
#middle .container {
padding: 1em 0;
padding: 1em 0;
}
#middle .container p {
line-height: 18px;
line-height: 18px;
}
#middle .container div {
width: 244px;
width: 244px;
}
#middle .container div:last-child {
width: 245px;
width: 245px;
}
#main {
padding: 5em 0;
padding: 5em 0;
}
#main .container {
margin: 0 auto;
position: relative;
text-align: left;
margin: 0 auto;
position: relative;
text-align: left;
}
#main .container:before {
background: #aaaaaa;
content: '';
height: calc(100% - 8em);
left: 50%;
margin-left: -1px;
position: absolute;
top: 0;
width: 2px;
background: #aaaaaa;
content: '';
height: calc(100% - 8em);
left: 50%;
margin-left: -1px;
position: absolute;
top: 0;
width: 2px;
}
#main .container > div {
position: relative;
margin: 8em 0 0;
position: relative;
margin: 8em 0 0;
}
#main .container > div:first-child {
margin: 0;
margin: 0;
}
#main .container > div:nth-last-child(2) {
padding-bottom: 8em;
padding-bottom: 8em;
}
#main .tick {
background: #272a2f;
border: 4px solid #aaaaaa;
position: absolute;
left: 50%;
width: 16px;
height: 16px;
border-radius: 50%;
margin-left: -12px;
visibility: hidden;
background: #272a2f;
border: 4px solid #aaaaaa;
position: absolute;
left: 50%;
width: 16px;
height: 16px;
border-radius: 50%;
margin-left: -12px;
visibility: hidden;
}
#main .content {
border: 2px solid #aaaaaa;
border-radius: 8px;
position: relative;
padding: 1.5em;
top: -19px;
visibility: hidden;
width: 39%;
border: 2px solid #aaaaaa;
border-radius: 8px;
position: relative;
padding: 1.5em;
top: -19px;
visibility: hidden;
width: 39%;
}
#main .content:before {
content: '';
position: absolute;
top: 24px;
left: 100%;
border: 6px solid transparent;
border-left-color: #aaaaaa;
content: '';
position: absolute;
top: 24px;
left: 100%;
border: 6px solid transparent;
border-left-color: #aaaaaa;
}
#main .content:after {
content: '';
display: table;
clear: both;
content: '';
display: table;
clear: both;
}
#main .content h4 {
}
#main .content h4 > div {
display: inline-block;
display: inline-block;
}
#main .content h4 .day {
font-size: 36px;
font-size: 36px;
}
#main .content h4 .weekday {
text-transform: uppercase;
text-transform: uppercase;
}
#main .content h4 .month {
font-weight: 300;
font-weight: 300;
}
#main .content h2 {
color: #7bc876;
font-size: 20px;
color: #7bc876;
font-size: 20px;
}
#main .content h2 div {
font-size: 0.7em;
font-size: 0.7em;
}
#main .content .quote {
margin-top: 1em;
margin-top: 1em;
}
#main .content .quote.fa-quote-right {
text-align: right;
width: 100%;
text-align: right;
width: 100%;
}
#main .content p {
color: #aaaaaa;
font-size: 16px;
line-height: 1.6;
margin: -1em 0 0 2em;
text-align: start;
word-wrap: break-word;
color: #aaaaaa;
font-size: 16px;
line-height: 1.6;
margin: -1em 0 0 2em;
text-align: start;
word-wrap: break-word;
}
#main .content p[dir='rtl'] {
margin-left: 0;
margin-right: 2em;
margin-left: 0;
margin-right: 2em;
}
#main .content p[data-script='Arabic'] {
font-size: 18px;
font-size: 18px;
}
#main .label {
padding: 0.5em 0;
position: absolute;
width: 100%;
left: 122%;
top: 0;
padding: 0.5em 0;
position: absolute;
width: 100%;
left: 122%;
top: 0;
}
#main .label figure {
display: inline-block;
text-align: center;
display: inline-block;
text-align: center;
}
#main .label .icon {
color: #7bc876;
color: #7bc876;
}
#main .label figcaption {
margin: 4px 2px 0 0;
margin: 4px 2px 0 0;
}
#main .label a,
#main .label span {
color: #ebebeb;
display: inline-block;
font-size: 1.4em;
font-weight: 400;
margin-top: 12px;
padding: 0 10px;
vertical-align: top;
color: #ebebeb;
display: inline-block;
font-size: 1.4em;
font-weight: 400;
margin-top: 12px;
padding: 0 10px;
vertical-align: top;
}
#main .label span {
color: #888888;
color: #888888;
}
#main .container > div:nth-child(even) .content,
#main .container > div:nth-child(even) .label figure {
float: right;
float: right;
}
#main .container > div:nth-child(even) .content:before {
top: 24px;
left: auto;
right: 100%;
border-color: transparent;
border-right-color: #aaaaaa;
top: 24px;
left: auto;
right: 100%;
border-color: transparent;
border-right-color: #aaaaaa;
}
#main .container > div:nth-child(even) .content .read-more {
float: right;
float: right;
}
#main .container > div:nth-child(even) .content .label {
left: auto;
right: 122%;
text-align: right;
left: auto;
right: 122%;
text-align: right;
}
/*
@ -207,114 +207,114 @@
*/
#main .tick.bounce-in {
visibility: visible;
-webkit-animation: bounce-1 0.6s;
animation: bounce-1 0.6s;
visibility: visible;
-webkit-animation: bounce-1 0.6s;
animation: bounce-1 0.6s;
}
@-webkit-keyframes bounce-1 {
0% {
opacity: 0;
-webkit-transform: scale(0.5);
}
0% {
opacity: 0;
-webkit-transform: scale(0.5);
}
60% {
opacity: 1;
-webkit-transform: scale(1.5);
}
60% {
opacity: 1;
-webkit-transform: scale(1.5);
}
100% {
-webkit-transform: scale(1);
}
100% {
-webkit-transform: scale(1);
}
}
@keyframes bounce-1 {
0% {
opacity: 0;
transform: scale(0.5);
}
0% {
opacity: 0;
transform: scale(0.5);
}
60% {
opacity: 1;
transform: scale(1.5);
}
60% {
opacity: 1;
transform: scale(1.5);
}
100% {
transform: scale(1);
}
100% {
transform: scale(1);
}
}
#main .content.bounce-in {
visibility: visible;
-webkit-animation: bounce-2 0.6s;
animation: bounce-2 0.6s;
visibility: visible;
-webkit-animation: bounce-2 0.6s;
animation: bounce-2 0.6s;
}
#main .container > div:nth-child(even) .content.bounce-in {
-webkit-animation: bounce-2-inverse 0.6s;
animation: bounce-2-inverse 0.6s;
-webkit-animation: bounce-2-inverse 0.6s;
animation: bounce-2-inverse 0.6s;
}
@-webkit-keyframes bounce-2 {
0% {
opacity: 0;
-webkit-transform: translateX(-100px);
}
0% {
opacity: 0;
-webkit-transform: translateX(-100px);
}
60% {
opacity: 1;
-webkit-transform: translateX(20px);
}
60% {
opacity: 1;
-webkit-transform: translateX(20px);
}
100% {
-webkit-transform: translateX(0);
}
100% {
-webkit-transform: translateX(0);
}
}
@keyframes bounce-2 {
0% {
opacity: 0;
transform: translateX(-100px);
}
0% {
opacity: 0;
transform: translateX(-100px);
}
60% {
opacity: 1;
transform: translateX(20px);
}
60% {
opacity: 1;
transform: translateX(20px);
}
100% {
transform: translateX(0);
}
100% {
transform: translateX(0);
}
}
@-webkit-keyframes bounce-2-inverse {
0% {
opacity: 0;
-webkit-transform: translateX(100px);
}
0% {
opacity: 0;
-webkit-transform: translateX(100px);
}
60% {
opacity: 1;
-webkit-transform: translateX(-20px);
}
60% {
opacity: 1;
-webkit-transform: translateX(-20px);
}
100% {
-webkit-transform: translateX(0);
}
100% {
-webkit-transform: translateX(0);
}
}
@keyframes bounce-2-inverse {
0% {
opacity: 0;
transform: translateX(100px);
}
0% {
opacity: 0;
transform: translateX(100px);
}
60% {
opacity: 1;
transform: translateX(-20px);
}
60% {
opacity: 1;
transform: translateX(-20px);
}
100% {
transform: translateX(0);
}
100% {
transform: translateX(0);
}
}

Просмотреть файл

@ -1,114 +1,114 @@
#locale-settings {
margin-top: 30px;
margin-top: 30px;
}
#preferred-locale {
margin-top: 10px;
margin-top: 10px;
}
#locale-settings .label {
color: #aaa;
display: inline-block;
font-size: 16px;
font-weight: 300;
margin: 6px 10px 0 0;
text-align: right;
width: 280px;
vertical-align: top;
color: #aaa;
display: inline-block;
font-size: 16px;
font-weight: 300;
margin: 6px 10px 0 0;
text-align: right;
width: 280px;
vertical-align: top;
}
#locale-settings .locale-selector {
display: inline-block;
display: inline-block;
}
#locale-settings .locale-selector .locale.select {
width: 280px;
width: 280px;
}
#locale-settings .locale-selector .locale.select .button {
background: #272a2f;
color: #aaaaaa;
font-size: 16px;
font-weight: 400;
height: 36px;
margin: 0;
padding: 8px 12px;
width: 100%;
background: #272a2f;
color: #aaaaaa;
font-size: 16px;
font-weight: 400;
height: 36px;
margin: 0;
padding: 8px 12px;
width: 100%;
}
#locale-settings .locale-selector .locale.select .menu {
background: #272a2f;
border: 1px solid #333941;
border-top: none;
top: 36px;
left: -1px;
width: 282px;
z-index: 30;
background: #272a2f;
border: 1px solid #333941;
border-top: none;
top: 36px;
left: -1px;
width: 282px;
z-index: 30;
}
#main form {
margin: 0 auto;
margin: 0 auto;
}
#main form section {
margin: 0 auto 70px;
margin: 0 auto 70px;
}
#main form section h3 {
margin-bottom: 20px;
margin-bottom: 20px;
}
#main .controls .cancel {
float: none;
margin: 9px;
float: none;
margin: 9px;
}
#profile-form {
display: block;
position: relative;
text-align: left;
width: 620px;
display: block;
position: relative;
text-align: left;
width: 620px;
}
#profile-form .field {
text-align: left;
text-align: left;
}
#profile-form .field:not(:last-child) {
margin-bottom: 20px;
margin-bottom: 20px;
}
#profile-form .field input {
color: #ffffff;
background: #333941;
border: 1px solid #4d5967;
border-radius: 3px;
float: none;
width: 290px;
padding: 4px;
color: #ffffff;
background: #333941;
border: 1px solid #4d5967;
border-radius: 3px;
float: none;
width: 290px;
padding: 4px;
-moz-box-sizing: border-box;
box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
#profile-form button {
margin-top: 10px;
margin-top: 10px;
}
#profile-form .help {
color: #888888;
font-style: italic;
margin-top: 5px;
color: #888888;
font-style: italic;
margin-top: 5px;
}
.errorlist {
color: #f36;
list-style: none;
margin: 0;
margin-top: 5px;
text-align: left;
color: #f36;
list-style: none;
margin: 0;
margin-top: 5px;
text-align: left;
}
.check-list {
cursor: pointer;
cursor: pointer;
}

Просмотреть файл

@ -1,79 +1,79 @@
$(function () {
function loadNextEvents(cb) {
var currentPage = $timeline.data('page'),
nextPage = parseInt(currentPage, 10) + 1,
// Determines if client should request new timeline events.
finalized = parseInt($timeline.data('finalized'), 10);
function loadNextEvents(cb) {
var currentPage = $timeline.data('page'),
nextPage = parseInt(currentPage, 10) + 1,
// Determines if client should request new timeline events.
finalized = parseInt($timeline.data('finalized'), 10);
if (finalized || $timelineLoader.is(':visible')) {
return;
if (finalized || $timelineLoader.is(':visible')) {
return;
}
$timelineLoader.show();
$.get(timelineUrl, { page: nextPage }).then(
function (timelineContents) {
$('#main > .container').append(timelineContents);
$timelineLoader.hide();
$timeline.data('page', nextPage);
cb();
},
function (response) {
$timeline.data('page', nextPage);
if (response.status === 404) {
$timeline.data('finalized', 1);
cb();
} else {
Pontoon.endLoader("Couldn't load the timeline.");
}
$timelineLoader.hide();
},
);
}
$timelineLoader.show();
// Show/animate timeline blocks inside viewport
function animate() {
var $blocks = $('#main > .container > div');
$.get(timelineUrl, { page: nextPage }).then(
function (timelineContents) {
$('#main > .container').append(timelineContents);
$timelineLoader.hide();
$timeline.data('page', nextPage);
cb();
},
function (response) {
$timeline.data('page', nextPage);
if (response.status === 404) {
$timeline.data('finalized', 1);
cb();
} else {
Pontoon.endLoader("Couldn't load the timeline.");
}
$timelineLoader.hide();
},
);
}
$blocks.each(function () {
var block_bottom = $(this).offset().top + $(this).outerHeight(),
window_bottom = $(window).scrollTop() + $(window).height(),
blockSelf = this;
// Show/animate timeline blocks inside viewport
function animate() {
var $blocks = $('#main > .container > div');
// Animation of event that's displayed on the user timeline.
function showEvent() {
$(this)
.find('.tick, .content')
.css('visibility', 'visible')
.addClass(function () {
return $blocks.length > 1 ? 'bounce-in' : '';
});
}
$blocks.each(function () {
var block_bottom = $(this).offset().top + $(this).outerHeight(),
window_bottom = $(window).scrollTop() + $(window).height(),
blockSelf = this;
// Animation of event that's displayed on the user timeline.
function showEvent() {
$(this)
.find('.tick, .content')
.css('visibility', 'visible')
.addClass(function () {
return $blocks.length > 1 ? 'bounce-in' : '';
});
}
if (block_bottom <= window_bottom) {
if ($blocks.index($(this)) === $blocks.length - 1) {
loadNextEvents(function () {
showEvent.apply(blockSelf);
});
} else {
showEvent.apply(blockSelf);
}
}
});
}
var $timelineLoader = $('#timeline-loader'),
$timeline = $('#main'),
timelineUrl = $timeline.data('url');
// The first page of events.
loadNextEvents(function () {
$(window).scroll();
if (block_bottom <= window_bottom) {
if ($blocks.index($(this)) === $blocks.length - 1) {
loadNextEvents(function () {
showEvent.apply(blockSelf);
});
} else {
showEvent.apply(blockSelf);
}
}
});
}
$(window).on('scroll', animate);
var $timelineLoader = $('#timeline-loader'),
$timeline = $('#main'),
timelineUrl = $timeline.data('url');
if ($('.notification li').length) {
Pontoon.endLoader();
}
// The first page of events.
loadNextEvents(function () {
$(window).scroll();
});
$(window).on('scroll', animate);
if ($('.notification li').length) {
Pontoon.endLoader();
}
});

Просмотреть файл

@ -1,42 +1,40 @@
$(function () {
window.history.replaceState(
{},
document.title,
document.location.href.split('?')[0],
);
window.history.replaceState(
{},
document.title,
document.location.href.split('?')[0],
);
// Filter notifications
$('.left-column a').on('click', function () {
var notifications = $(this).data('notifications');
// Filter notifications
$('.left-column a').on('click', function () {
var notifications = $(this).data('notifications');
// Show all notifications
if (!notifications) {
$(
'.right-column .notification-item, .right-column .horizontal-separator',
).show();
$('.right-column .horizontal-separator').show();
// Show all notifications
if (!notifications) {
$(
'.right-column .notification-item, .right-column .horizontal-separator',
).show();
$('.right-column .horizontal-separator').show();
// Show project notifications
} else {
$('.right-column .notification-item').each(function () {
var isProjectNotification =
$.inArray($(this).data('id'), notifications) > -1;
$(this).toggle(isProjectNotification);
$(this)
.next('.horizontal-separator')
.toggle(isProjectNotification);
});
// Show project notifications
} else {
$('.right-column .notification-item').each(function () {
var isProjectNotification =
$.inArray($(this).data('id'), notifications) > -1;
$(this).toggle(isProjectNotification);
$(this).next('.horizontal-separator').toggle(isProjectNotification);
});
$('.right-column .notification-item:visible:last')
.next('.horizontal-separator')
.hide();
}
});
// Mark all notifications as read
if ($('.right-column li.notification-item[data-unread="true"]').length) {
setTimeout(function () {
Pontoon.markAllNotificationsAsRead();
}, 1000);
$('.right-column .notification-item:visible:last')
.next('.horizontal-separator')
.hide();
}
});
// Mark all notifications as read
if ($('.right-column li.notification-item[data-unread="true"]').length) {
setTimeout(function () {
Pontoon.markAllNotificationsAsRead();
}, 1000);
}
});

Просмотреть файл

@ -1,82 +1,82 @@
$(function () {
// Toggle user profile attribute
$('.check-box').click(function () {
var self = $(this);
// Toggle user profile attribute
$('.check-box').click(function () {
var self = $(this);
$.ajax({
url: '/api/v1/user/' + $('#server').data('username') + '/',
type: 'POST',
data: {
csrfmiddlewaretoken: $('body').data('csrf'),
attribute: self.data('attribute'),
value: !self.is('.enabled'),
},
success: function () {
self.toggleClass('enabled');
var is_enabled = self.is('.enabled');
var status = is_enabled ? 'enabled' : 'disabled';
$.ajax({
url: '/api/v1/user/' + $('#server').data('username') + '/',
type: 'POST',
data: {
csrfmiddlewaretoken: $('body').data('csrf'),
attribute: self.data('attribute'),
value: !self.is('.enabled'),
},
success: function () {
self.toggleClass('enabled');
var is_enabled = self.is('.enabled');
var status = is_enabled ? 'enabled' : 'disabled';
Pontoon.endLoader(self.text() + ' ' + status + '.');
},
error: function (request) {
if (request.responseText === 'error') {
Pontoon.endLoader('Oops, something went wrong.', 'error');
} else {
Pontoon.endLoader(request.responseText, 'error');
}
},
});
Pontoon.endLoader(self.text() + ' ' + status + '.');
},
error: function (request) {
if (request.responseText === 'error') {
Pontoon.endLoader('Oops, something went wrong.', 'error');
} else {
Pontoon.endLoader(request.responseText, 'error');
}
},
});
});
// Save custom homepage
$('#homepage .locale .menu li:not(".no-match")').click(function () {
var custom_homepage = $(this).find('.language').data('code');
// Save custom homepage
$('#homepage .locale .menu li:not(".no-match")').click(function () {
var custom_homepage = $(this).find('.language').data('code');
$.ajax({
url: '/save-custom-homepage/',
type: 'POST',
data: {
csrfmiddlewaretoken: $('body').data('csrf'),
custom_homepage: custom_homepage,
},
success: function (data) {
if (data === 'ok') {
Pontoon.endLoader('Custom homepage saved.');
}
},
error: function (request) {
if (request.responseText === 'error') {
Pontoon.endLoader('Oops, something went wrong.', 'error');
} else {
Pontoon.endLoader(request.responseText, 'error');
}
},
});
$.ajax({
url: '/save-custom-homepage/',
type: 'POST',
data: {
csrfmiddlewaretoken: $('body').data('csrf'),
custom_homepage: custom_homepage,
},
success: function (data) {
if (data === 'ok') {
Pontoon.endLoader('Custom homepage saved.');
}
},
error: function (request) {
if (request.responseText === 'error') {
Pontoon.endLoader('Oops, something went wrong.', 'error');
} else {
Pontoon.endLoader(request.responseText, 'error');
}
},
});
});
// Save preferred source locale
$('#preferred-locale .locale .menu li:not(".no-match")').click(function () {
var preferred_source_locale = $(this).find('.language').data('code');
// Save preferred source locale
$('#preferred-locale .locale .menu li:not(".no-match")').click(function () {
var preferred_source_locale = $(this).find('.language').data('code');
$.ajax({
url: '/save-preferred-source-locale/',
type: 'POST',
data: {
csrfmiddlewaretoken: $('body').data('csrf'),
preferred_source_locale: preferred_source_locale,
},
success: function (data) {
if (data === 'ok') {
Pontoon.endLoader('Preferred source locale saved.');
}
},
error: function (request) {
if (request.responseText === 'error') {
Pontoon.endLoader('Oops, something went wrong.', 'error');
} else {
Pontoon.endLoader(request.responseText, 'error');
}
},
});
$.ajax({
url: '/save-preferred-source-locale/',
type: 'POST',
data: {
csrfmiddlewaretoken: $('body').data('csrf'),
preferred_source_locale: preferred_source_locale,
},
success: function (data) {
if (data === 'ok') {
Pontoon.endLoader('Preferred source locale saved.');
}
},
error: function (request) {
if (request.responseText === 'error') {
Pontoon.endLoader('Oops, something went wrong.', 'error');
} else {
Pontoon.endLoader(request.responseText, 'error');
}
},
});
});
});

Просмотреть файл

@ -1,409 +1,409 @@
/* General styles */
:root {
--content-width: 980px;
--header-height: 60px;
--content-width: 980px;
--header-height: 60px;
}
body > header {
background: transparent;
border-color: transparent;
position: fixed;
width: 100%;
z-index: 10;
background: transparent;
border-color: transparent;
position: fixed;
width: 100%;
z-index: 10;
}
body > header.menu-opened {
border-color: #333941;
border-color: #333941;
}
#main {
font-size: 16px;
font-weight: 300;
padding: 0;
font-size: 16px;
font-weight: 300;
padding: 0;
}
#main p {
color: #dddddd;
line-height: 1.5rem;
color: #dddddd;
line-height: 1.5rem;
}
#main .section {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
width: 100vw;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
width: 100vw;
background-image: url(../img/background.svg);
background-attachment: fixed;
background-size: cover;
background-image: url(../img/background.svg);
background-attachment: fixed;
background-size: cover;
}
.section {
color: #f4f4f4;
color: #f4f4f4;
}
.section .container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
overflow: hidden;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
overflow: hidden;
}
.section .container.with-footer {
flex: 1;
flex: 1;
}
.section .content-wrapper {
padding-right: 60px;
padding-right: 60px;
}
h2 {
color: #ffffff;
font-weight: 700;
font-size: 36px;
letter-spacing: -1px;
line-height: 50px;
padding-bottom: 16px;
text-transform: none;
color: #ffffff;
font-weight: 700;
font-size: 36px;
letter-spacing: -1px;
line-height: 50px;
padding-bottom: 16px;
text-transform: none;
}
#main .button {
background-color: #ffffff;
color: #000000;
display: flex;
border-radius: 2px;
width: 240px;
height: 40px;
justify-content: center;
align-items: center;
text-align: center;
font-weight: 400;
background-color: #ffffff;
color: #000000;
display: flex;
border-radius: 2px;
width: 240px;
height: 40px;
justify-content: center;
align-items: center;
text-align: center;
font-weight: 400;
}
#main .button.primary {
background-color: #7bc876;
background-color: #7bc876;
}
.flex {
display: flex;
display: flex;
}
.flex-direction-col {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
}
.flex-col-2 {
flex-basis: 50%;
flex-basis: 50%;
}
.flex-col-3 {
flex-basis: 33.3333333%;
flex-basis: 33.3333333%;
}
/* Side Navigation */
nav#sections {
position: fixed;
left: 17px;
top: 50%;
opacity: 1;
transform: translate(0, -50%);
position: fixed;
left: 17px;
top: 50%;
opacity: 1;
transform: translate(0, -50%);
}
nav#sections ul {
margin: 0;
padding: 0;
margin: 0;
padding: 0;
}
nav#sections ul li {
display: block;
width: 14px;
height: 13px;
margin: 7px;
position: relative;
display: block;
width: 14px;
height: 13px;
margin: 7px;
position: relative;
}
nav#sections ul li a {
display: block;
position: relative;
z-index: 1;
width: 100%;
height: 100%;
cursor: pointer;
text-decoration: none;
display: block;
position: relative;
z-index: 1;
width: 100%;
height: 100%;
cursor: pointer;
text-decoration: none;
}
nav#sections ul li a.active span,
nav#sections ul li:hover a.active span {
height: 12px;
width: 12px;
margin: -6px 0 0 -6px;
border-radius: 100%;
background-color: #7bc876;
height: 12px;
width: 12px;
margin: -6px 0 0 -6px;
border-radius: 100%;
background-color: #7bc876;
}
nav#sections ul li a span {
border-radius: 50%;
position: absolute;
z-index: 1;
height: 4px;
width: 4px;
border: 0;
background-color: #888888;
left: 50%;
top: 50%;
margin: -2px 0 0 -2px;
transition: all 0.1s ease-in-out;
border-radius: 50%;
position: absolute;
z-index: 1;
height: 4px;
width: 4px;
border: 0;
background-color: #888888;
left: 50%;
top: 50%;
margin: -2px 0 0 -2px;
transition: all 0.1s ease-in-out;
}
nav#sections ul li:hover a span {
width: 10px;
height: 10px;
margin: -5px 0px 0px -5px;
width: 10px;
height: 10px;
margin: -5px 0px 0px -5px;
}
/* Edit Homepage button */
#edit-homepage {
position: fixed;
left: 0;
right: 0;
margin: 0 auto;
width: var(--content-width);
text-align: right;
position: fixed;
left: 0;
right: 0;
margin: 0 auto;
width: var(--content-width);
text-align: right;
}
#edit-homepage .select {
top: calc(var(--header-height) + 10px);
transition: top 0.3s;
top: calc(var(--header-height) + 10px);
transition: top 0.3s;
}
body.addon-promotion-active #edit-homepage .select {
top: calc(var(--header-height) + 54px);
top: calc(var(--header-height) + 54px);
}
#edit-homepage .button {
background: #7bc876;
color: #000;
background: #7bc876;
color: #000;
}
#edit-homepage .fa {
margin-right: 7px;
margin-right: 7px;
}
/* Section-1 */
#section-1 .container {
flex-direction: column;
flex: 1;
align-items: start;
flex-direction: column;
flex: 1;
align-items: start;
}
#section-1 h1 {
font-size: 64px;
margin-bottom: 10px;
font-size: 64px;
margin-bottom: 10px;
}
#section-1 p {
font-size: 22px;
line-height: 36px;
margin-bottom: 60px;
width: 900px;
font-size: 22px;
line-height: 36px;
margin-bottom: 60px;
width: 900px;
}
#section-1 .flex {
align-items: center;
align-items: center;
}
#section-1 .flex span {
padding: 0 20px;
padding: 0 20px;
}
/* Scroll for more animation */
#section-1 .footer {
text-align: center;
width: var(--content-width);
text-align: center;
width: var(--content-width);
}
#section-1 .scroll {
display: block;
position: relative;
font-size: 12px;
height: 90px;
letter-spacing: 3px;
text-transform: uppercase;
display: block;
position: relative;
font-size: 12px;
height: 90px;
letter-spacing: 3px;
text-transform: uppercase;
}
#section-1 .scroll::after {
content: '';
border-right: 2px solid #7bc876;
border-bottom: 2px solid #7bc876;
width: 30px;
height: 30px;
position: absolute;
margin: auto;
right: 0;
left: 0;
animation: 3s jump infinite ease;
transform: rotate(45deg);
content: '';
border-right: 2px solid #7bc876;
border-bottom: 2px solid #7bc876;
width: 30px;
height: 30px;
position: absolute;
margin: auto;
right: 0;
left: 0;
animation: 3s jump infinite ease;
transform: rotate(45deg);
}
@keyframes jump {
0%,
100% {
top: 20px;
}
50% {
top: 40px;
}
0%,
100% {
top: 20px;
}
50% {
top: 40px;
}
}
/* Section-2 */
/* Section-3 */
#section-3 .flex {
justify-content: center;
justify-content: center;
}
#section-3 .timeline {
margin-top: 40px;
width: 2px;
height: 290px;
background-color: #7bc876;
margin-top: 40px;
width: 2px;
height: 290px;
background-color: #7bc876;
}
#section-3 ol {
counter-reset: item;
list-style: none;
margin-left: -28px;
counter-reset: item;
list-style: none;
margin-left: -28px;
}
#section-3 ol .flex {
align-items: center;
align-items: center;
}
#section-3 li {
color: #dddddd;
counter-increment: item;
margin: 40px 0;
color: #dddddd;
counter-increment: item;
margin: 40px 0;
}
#section-3 li:before {
background-color: #272a2f;
border: 2px solid #7bc876;
border-radius: 100%;
color: #ffffff;
content: counter(item);
display: inline-block;
font-size: 20px;
font-weight: 600;
height: 39px;
margin-right: 20px;
padding-top: 11px;
text-align: center;
width: 50px;
background-color: #272a2f;
border: 2px solid #7bc876;
border-radius: 100%;
color: #ffffff;
content: counter(item);
display: inline-block;
font-size: 20px;
font-weight: 600;
height: 39px;
margin-right: 20px;
padding-top: 11px;
text-align: center;
width: 50px;
}
#section-3 .checkmark {
background-color: #7bc876;
border-radius: 100%;
width: 54px;
height: 39px;
font-size: 24px;
margin-left: -74px;
margin-right: 20px;
text-align: center;
padding-top: 15px;
background-color: #7bc876;
border-radius: 100%;
width: 54px;
height: 39px;
font-size: 24px;
margin-left: -74px;
margin-right: 20px;
text-align: center;
padding-top: 15px;
}
/* Section-4 */
#section-4 .flex {
margin: 20px 0;
margin: 20px 0;
}
#section-4 .box {
border: 2px solid #4d5967;
border-radius: 3px;
margin-left: 20px;
padding: 10px;
padding-top: 18px;
text-align: center;
width: 186px;
border: 2px solid #4d5967;
border-radius: 3px;
margin-left: 20px;
padding: 10px;
padding-top: 18px;
text-align: center;
width: 186px;
}
#section-4 .box .box-image {
color: #7bc876;
font-size: 35px;
margin-bottom: 10px;
color: #7bc876;
font-size: 35px;
margin-bottom: 10px;
}
#section-4 .box p {
font-size: 12px;
text-transform: uppercase;
font-size: 12px;
text-transform: uppercase;
}
/* Section-5 */
#section-5 p {
padding-bottom: 30px;
padding-bottom: 30px;
}
#section-5 .image-wrapper {
text-align: right;
text-align: right;
}
#section-5 img {
border: 2px solid #4d5967;
border-radius: 3px;
width: 436px;
border: 2px solid #4d5967;
border-radius: 3px;
width: 436px;
}
/* Section-6 */
#section-6 .container {
flex-direction: column;
flex: 1;
flex-direction: column;
flex: 1;
}
#section-6 .content-wrapper {
flex: 1;
align-items: center;
justify-content: center;
padding: 0;
flex: 1;
align-items: center;
justify-content: center;
padding: 0;
}
#section-6 p {
padding-bottom: 10px;
padding-bottom: 10px;
}
#section-6 .button.primary {
margin-bottom: 30px;
margin-bottom: 30px;
}
#section-6 .footer {
height: 60px;
width: var(--content-width);
align-items: center;
font-size: 14px;
height: 60px;
width: var(--content-width);
align-items: center;
font-size: 14px;
}
#section-6 .footer .mozilla .logo {
width: 80px;
width: 80px;
}
#section-6 .footer .github {
text-align: center;
text-align: center;
}
#section-6 .footer .contact {
text-align: right;
text-align: right;
}
#section-6 .footer a {
color: #ffffff;
color: #ffffff;
}
#section-6 .footer a:hover {
color: #7bc876;
color: #7bc876;
}

Просмотреть файл

@ -1,4 +1,4 @@
form .aligned div.help {
margin-left: 0;
padding-left: 0;
margin-left: 0;
padding-left: 0;
}

Просмотреть файл

@ -1,160 +1,158 @@
const Sections = {
init(rootSel) {
this.root = document.querySelector(rootSel);
this.nav = null;
this.sections = this.root.querySelectorAll('[id^=section-]');
this.activeSectionIdx = 0;
this.mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
init(rootSel) {
this.root = document.querySelector(rootSel);
this.nav = null;
this.sections = this.root.querySelectorAll('[id^=section-]');
this.activeSectionIdx = 0;
this.mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
this._initKeyboard();
this._initWheel();
this._render();
},
this._initKeyboard();
this._initWheel();
this._render();
},
goToNext() {
this.goTo(
Math.min(this.activeSectionIdx + 1, this.sections.length - 1),
);
},
goToNext() {
this.goTo(Math.min(this.activeSectionIdx + 1, this.sections.length - 1));
},
goToPrev() {
this.goTo(Math.max(this.activeSectionIdx - 1, 0));
},
goToPrev() {
this.goTo(Math.max(this.activeSectionIdx - 1, 0));
},
goTo(idx) {
if (idx !== this.activeSectionIdx) {
this.activeSectionIdx = idx;
this.navigate();
goTo(idx) {
if (idx !== this.activeSectionIdx) {
this.activeSectionIdx = idx;
this.navigate();
}
},
navigate() {
requestAnimationFrame(() => {
const section = this.sections[this.activeSectionIdx];
const behavior = this.mediaQuery.matches ? 'auto' : 'smooth';
section.scrollIntoView({
behavior: behavior,
block: 'nearest',
});
this._render();
});
},
_initKeyboard() {
document.addEventListener('keydown', (e) => {
if (e.keyCode === 38) {
this.goToPrev();
}
if (e.keyCode === 40) {
this.goToNext();
}
});
},
_initWheel() {
let samples = [];
let lastScroll = new Date().getTime();
// Disable scrollbars
document.body.style.overflow = 'hidden';
const avg = (numbers, count) => {
let sum = 0;
for (let i = 0; i < count && i < numbers.length; i++) {
const idx = numbers.length - i - 1;
sum += numbers[idx];
}
return Math.ceil(sum / count);
};
document.addEventListener(
'wheel',
(e) => {
if (samples.length >= 50) {
samples.shift();
}
},
samples.push(Math.abs(e.deltaY));
navigate() {
requestAnimationFrame(() => {
const section = this.sections[this.activeSectionIdx];
const behavior = this.mediaQuery.matches ? 'auto' : 'smooth';
section.scrollIntoView({
behavior: behavior,
block: 'nearest',
});
this._render();
});
},
_initKeyboard() {
document.addEventListener('keydown', (e) => {
if (e.keyCode === 38) {
this.goToPrev();
}
if (e.keyCode === 40) {
this.goToNext();
}
});
},
_initWheel() {
let samples = [];
let lastScroll = new Date().getTime();
// Disable scrollbars
document.body.style.overflow = 'hidden';
const avg = (numbers, count) => {
let sum = 0;
for (let i = 0; i < count && i < numbers.length; i++) {
const idx = numbers.length - i - 1;
sum += numbers[idx];
}
return Math.ceil(sum / count);
};
document.addEventListener(
'wheel',
(e) => {
if (samples.length >= 50) {
samples.shift();
}
samples.push(Math.abs(e.deltaY));
const now = new Date().getTime();
const elapsed = now - lastScroll;
// Too fast!
if (elapsed < 550) {
return;
}
// Higher recent sample values mean scroll now happens faster
const isAccelerating = avg(samples, 10) >= avg(samples, 50);
if (!isAccelerating) {
return;
}
// Record the current scroll and restart measuring the next time
lastScroll = new Date().getTime();
samples = [];
if (e.deltaY < 0) {
this.goToPrev();
} else if (0 < e.deltaY) {
this.goToNext();
}
},
{ passive: true },
);
},
_renderNav() {
const nav = document.createElement('nav');
nav.id = 'sections';
const ul = document.createElement('ul');
this.sections.forEach((section, idx) => {
const li = document.createElement('li');
const a = document.createElement('a');
const span = document.createElement('span');
a.className = 'js-section-nav';
a.onclick = (e) => {
e.preventDefault();
this.goTo(idx);
};
a.appendChild(span);
li.appendChild(a);
ul.appendChild(li);
});
nav.appendChild(ul);
this.root.appendChild(nav);
return nav;
},
_render() {
if (!this.nav) {
this.nav = this._renderNav();
const now = new Date().getTime();
const elapsed = now - lastScroll;
// Too fast!
if (elapsed < 550) {
return;
}
const navElements = this.nav.querySelectorAll('.js-section-nav');
navElements.forEach((el, idx) =>
el.classList.toggle('active', idx === this.activeSectionIdx),
);
},
// Higher recent sample values mean scroll now happens faster
const isAccelerating = avg(samples, 10) >= avg(samples, 50);
if (!isAccelerating) {
return;
}
// Record the current scroll and restart measuring the next time
lastScroll = new Date().getTime();
samples = [];
if (e.deltaY < 0) {
this.goToPrev();
} else if (0 < e.deltaY) {
this.goToNext();
}
},
{ passive: true },
);
},
_renderNav() {
const nav = document.createElement('nav');
nav.id = 'sections';
const ul = document.createElement('ul');
this.sections.forEach((section, idx) => {
const li = document.createElement('li');
const a = document.createElement('a');
const span = document.createElement('span');
a.className = 'js-section-nav';
a.onclick = (e) => {
e.preventDefault();
this.goTo(idx);
};
a.appendChild(span);
li.appendChild(a);
ul.appendChild(li);
});
nav.appendChild(ul);
this.root.appendChild(nav);
return nav;
},
_render() {
if (!this.nav) {
this.nav = this._renderNav();
}
const navElements = this.nav.querySelectorAll('.js-section-nav');
navElements.forEach((el, idx) =>
el.classList.toggle('active', idx === this.activeSectionIdx),
);
},
};
$(function () {
Sections.init('#main');
Sections.init('#main');
// Scroll from Section 1 to Section 2
$('#section-1 .footer .scroll').on('click', function (e) {
e.preventDefault();
Sections.goToNext();
});
// Scroll from Section 1 to Section 2
$('#section-1 .footer .scroll').on('click', function (e) {
e.preventDefault();
Sections.goToNext();
});
// Show/hide header border on menu open/close
$('body > header').on('click', '.selector', function () {
if (!$(this).siblings('.menu').is(':visible')) {
$('body > header').addClass('menu-opened');
}
});
$('body').bind('click.main', function () {
$('body > header').removeClass('menu-opened');
});
// Show/hide header border on menu open/close
$('body > header').on('click', '.selector', function () {
if (!$(this).siblings('.menu').is(':visible')) {
$('body > header').addClass('menu-opened');
}
});
$('body').bind('click.main', function () {
$('body > header').removeClass('menu-opened');
});
});

Просмотреть файл

@ -1,288 +1,288 @@
#insights .half {
width: 470px;
float: left;
width: 470px;
float: left;
}
#insights .half:last-child {
float: right;
float: right;
}
#insights .block {
border-radius: 6px;
background: #333941;
margin-bottom: 40px;
overflow: hidden;
padding: 30px;
border-radius: 6px;
background: #333941;
margin-bottom: 40px;
overflow: hidden;
padding: 30px;
}
#insights h3 {
color: #ebebeb;
font-size: 20px;
font-style: normal;
font-weight: bold;
height: 25px;
letter-spacing: normal;
margin-bottom: 30px;
color: #ebebeb;
font-size: 20px;
font-style: normal;
font-weight: bold;
height: 25px;
letter-spacing: normal;
margin-bottom: 30px;
}
#insights .controls .period-selector {
float: right;
font-size: 0;
margin-right: 10px;
float: right;
font-size: 0;
margin-right: 10px;
}
#insights .controls .period-selector li {
display: inline-block;
display: inline-block;
}
#insights .controls .period-selector li .selector {
font-size: 12px;
text-transform: uppercase;
width: 32px;
font-size: 12px;
text-transform: uppercase;
width: 32px;
}
#insights .controls .period-selector li .selector.active,
#insights .controls .period-selector li .selector:hover {
background: #7bc876;
color: #272a2f;
background: #7bc876;
color: #272a2f;
}
#insights .controls .selector {
background: #3f4752;
color: #aaa;
cursor: pointer;
width: 24px;
height: 24px;
text-align: center;
border-radius: 3px;
margin-left: 5px;
padding-top: 5px;
box-sizing: border-box;
background: #3f4752;
color: #aaa;
cursor: pointer;
width: 24px;
height: 24px;
text-align: center;
border-radius: 3px;
margin-left: 5px;
padding-top: 5px;
box-sizing: border-box;
}
#insights .active-users-chart,
#insights #unreviewed-suggestions-lifespan-chart {
height: 160px;
height: 160px;
}
#insights .active-users-chart {
float: left;
margin-right: 40px;
position: relative;
text-align: center;
float: left;
margin-right: 40px;
position: relative;
text-align: center;
}
#insights .active-users-chart:last-child {
margin-right: 0;
margin-right: 0;
}
#insights .active-users-chart h4 {
font-size: 14px;
font-weight: bold;
margin: 10px auto 0;
width: 100px;
font-size: 14px;
font-weight: bold;
margin: 10px auto 0;
width: 100px;
}
#insights .active-users-chart .active-wrapper {
left: 0;
right: 0;
top: 15px;
position: absolute;
left: 0;
right: 0;
top: 15px;
position: absolute;
}
#insights .active-users-chart .active {
border-bottom: 2px solid #888888;
display: inline-block;
font-size: 40px;
font-weight: bold;
line-height: 48px;
border-bottom: 2px solid #888888;
display: inline-block;
font-size: 40px;
font-weight: bold;
line-height: 48px;
}
#insights .active-users-chart .total {
color: #888;
font-size: 16px;
left: 0;
right: 0;
top: 68px;
position: absolute;
color: #888;
font-size: 16px;
left: 0;
right: 0;
top: 68px;
position: absolute;
}
#insights figure {
margin-bottom: 40px;
margin-bottom: 40px;
}
#insights .suggestions-age .block {
position: relative;
position: relative;
}
#insights .suggestions-age-items {
width: 940px;
height: 180px;
display: inline-block;
overflow: hidden;
transition: margin 0.4s ease-in-out;
width: 940px;
height: 180px;
display: inline-block;
overflow: hidden;
transition: margin 0.4s ease-in-out;
}
#insights .suggestions-age-items .suggestions-age-item {
float: left;
padding-right: 60px;
float: left;
padding-right: 60px;
}
#insights .suggestions-age nav {
text-align: center;
text-align: center;
}
#insights .suggestions-age nav ul li {
cursor: pointer;
display: inline-block;
margin: 15px 15px 0;
cursor: pointer;
display: inline-block;
margin: 15px 15px 0;
}
#insights .suggestions-age nav ul li .icon {
background-color: #272a2f;
background-color: #272a2f;
}
#insights .suggestions-age nav ul li .label {
color: #4d5967;
color: #4d5967;
}
#insights .suggestions-age nav ul li.active .icon {
background-color: #4fc4f6;
background-color: #4fc4f6;
}
#insights .suggestions-age nav ul li.active .label {
color: #ffffff;
color: #ffffff;
}
/* Info tooltip */
#insights h3 .fa {
float: right;
font-size: 14px;
float: right;
font-size: 14px;
}
#insights h3 .fa.active,
#insights h3 .fa:hover {
background: #272a2f;
background: #272a2f;
}
#insights h3 .tooltip {
background: #000000dd;
position: absolute;
display: none;
margin-top: 10px;
padding: 10px;
z-index: 1;
font-size: 14px;
font-weight: normal;
line-height: 1.5em;
border-radius: 3px;
right: 0;
max-width: 570px;
background: #000000dd;
position: absolute;
display: none;
margin-top: 10px;
padding: 10px;
z-index: 1;
font-size: 14px;
font-weight: normal;
line-height: 1.5em;
border-radius: 3px;
right: 0;
max-width: 570px;
}
#insights h3 .tooltip ul {
margin-top: 15px;
margin-left: 15px;
margin-top: 15px;
margin-left: 15px;
}
#insights h3 .tooltip li {
list-style-type: disc;
list-style-type: disc;
}
#insights h3 .tooltip li:not(:last-child) {
padding-bottom: 5px;
padding-bottom: 5px;
}
/* Active users info tooltip */
#insights h3 .tooltip li::marker {
color: #7bc876;
color: #7bc876;
}
/* Active users info tooltip */
#insights h3 .tooltip li.current-month::marker {
color: #4fc4f6;
color: #4fc4f6;
}
#insights h3 .tooltip li.twelve-month-average::marker {
color: #385465;
color: #385465;
}
/* Translation activity info tooltip */
#insights h3 .tooltip li.human-translations::marker {
color: #4f7256;
color: #4f7256;
}
#insights h3 .tooltip li.machinery-translations::marker {
color: #41554c;
color: #41554c;
}
#insights h3 .tooltip li.new-source-strings::marker {
color: #272a2f;
color: #272a2f;
}
#insights h3 .tooltip li.completion::marker {
color: #7bc876;
color: #7bc876;
}
/* Review activity info tooltip */
#insights h3 .tooltip li.peer-approved::marker {
color: #3e7089;
color: #3e7089;
}
#insights h3 .tooltip li.self-approved::marker {
color: #385465;
color: #385465;
}
#insights h3 .tooltip li.rejected::marker {
color: #843650;
color: #843650;
}
#insights h3 .tooltip li.new-suggestions::marker {
color: #272a2f;
color: #272a2f;
}
#insights h3 .tooltip li.unreviewed::marker {
color: #4fc4f6;
color: #4fc4f6;
}
/* Custom chart legend */
#insights .legend {
text-align: center;
text-align: center;
}
#insights .legend li {
display: inline-block;
font-size: 12px;
margin: 15px;
margin-bottom: 5px;
display: inline-block;
font-size: 12px;
margin: 15px;
margin-bottom: 5px;
}
#insights .legend li .icon,
#insights nav li .icon {
display: inline-block;
border-radius: 50%;
margin-right: 8px;
height: 12px;
width: 12px;
display: inline-block;
border-radius: 50%;
margin-right: 8px;
height: 12px;
width: 12px;
}
#insights .legend li .label,
#insights nav li .label {
cursor: pointer;
font-weight: bold;
vertical-align: text-top;
cursor: pointer;
font-weight: bold;
vertical-align: text-top;
}
#insights .legend li.disabled .label {
color: #4d5967;
color: #4d5967;
}
#insights .legend li.disabled .label:hover {
color: #fff;
color: #fff;
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -1,32 +1,32 @@
.controls {
margin-bottom: 40px;
margin-bottom: 40px;
}
.locale-selector {
float: right;
width: auto;
float: right;
width: auto;
}
.controls > .search-wrapper .fa-spin,
.controls > .search-wrapper.loading .fa-search {
display: none;
display: none;
}
.controls > .search-wrapper.loading .fa-spin {
display: block;
display: block;
}
.clipboard-success {
float: left;
color: #7bc876;
float: left;
color: #7bc876;
}
#helpers .machinery li {
padding-left: 5px;
padding-right: 5px;
padding-left: 5px;
padding-right: 5px;
}
#helpers .machinery li:hover {
background: #333941;
cursor: pointer;
background: #333941;
cursor: pointer;
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -1,85 +1,85 @@
.manual-notifications .right-column #compose {
padding: 20px;
padding: 20px;
-moz-box-sizing: border-box;
box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.manual-notifications #compose h3 {
color: #ebebeb;
font-size: 22px;
letter-spacing: 0;
color: #ebebeb;
font-size: 22px;
letter-spacing: 0;
}
.manual-notifications #compose h3 .stress {
color: #7bc876;
color: #7bc876;
}
.manual-notifications #compose .toolbar {
padding: 10px 0 0;
padding: 10px 0 0;
}
.manual-notifications #compose .controls {
margin: 0;
text-align: right;
margin: 0;
text-align: right;
}
.manual-notifications #compose .errors {
float: right;
text-align: right;
float: right;
text-align: right;
}
.manual-notifications #compose .errors p {
color: #f36;
text-transform: uppercase;
visibility: hidden;
color: #f36;
text-transform: uppercase;
visibility: hidden;
}
.manual-notifications #compose .locale-selector {
margin: 20px 0 40px;
margin: 20px 0 40px;
}
.manual-notifications #compose .locale-selector .locale.select .menu {
width: 285px; /* must be same as .shortcuts */
width: 285px; /* must be same as .shortcuts */
}
.manual-notifications #compose .locale-selector .shortcuts {
float: left;
font-size: 14px;
width: 285px; /* must be same as .menu */
float: left;
font-size: 14px;
width: 285px; /* must be same as .menu */
}
.manual-notifications #compose .locale-selector .shortcuts .complete {
float: left;
float: left;
}
.manual-notifications #compose .locale-selector .shortcuts .incomplete {
float: right;
float: right;
}
.manual-notifications #compose .message-wrapper .subtitle {
color: #aaa;
float: left;
text-transform: uppercase;
color: #aaa;
float: left;
text-transform: uppercase;
}
.manual-notifications #compose textarea {
background: #272a2f;
color: #ebebeb;
font-size: 14px;
font-weight: 300;
height: 150px;
padding: 10px;
width: 100%;
background: #272a2f;
color: #ebebeb;
font-size: 14px;
font-weight: 300;
height: 150px;
padding: 10px;
width: 100%;
-moz-box-sizing: border-box;
box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.manual-notifications #sent li.no {
padding-top: 14px;
padding-top: 14px;
}
.manual-notifications #sent li.no .icon {
color: #272a2f;
color: #272a2f;
}

Просмотреть файл

@ -1,71 +1,67 @@
$(function () {
var container = $('#main .container');
var container = $('#main .container');
function isValidForm($form, locales, message) {
$form.find('.errors p').css('visibility', 'hidden');
function isValidForm($form, locales, message) {
$form.find('.errors p').css('visibility', 'hidden');
if (!locales) {
$form
.find('.locale-selector .errors p')
.css('visibility', 'visible');
}
if (!message) {
$form
.find('.message-wrapper .errors p')
.css('visibility', 'visible');
}
return locales && message;
if (!locales) {
$form.find('.locale-selector .errors p').css('visibility', 'visible');
}
// Send notification
container.on('click', '#send-notification .send', function (e) {
e.preventDefault();
var $form = $('#send-notification');
if (!message) {
$form.find('.message-wrapper .errors p').css('visibility', 'visible');
}
// Validate form
var locales = $form.find('[name=selected_locales]').val(),
message = $form.find('[name=message]').val();
return locales && message;
}
if (!isValidForm($form, locales, message)) {
return;
// Send notification
container.on('click', '#send-notification .send', function (e) {
e.preventDefault();
var $form = $('#send-notification');
// Validate form
var locales = $form.find('[name=selected_locales]').val(),
message = $form.find('[name=message]').val();
if (!isValidForm($form, locales, message)) {
return;
}
// Submit form
$.ajax({
url: $form.prop('action'),
type: $form.prop('method'),
data: $form.serialize(),
success: function (data) {
if (data.selected_locales || data.message) {
isValidForm($form, !data.selected_locales, !data.message);
return false;
}
// Submit form
$.ajax({
url: $form.prop('action'),
type: $form.prop('method'),
data: $form.serialize(),
success: function (data) {
if (data.selected_locales || data.message) {
isValidForm($form, !data.selected_locales, !data.message);
return false;
}
Pontoon.endLoader('Notification sent.');
container.empty().append(data);
},
error: function () {
Pontoon.endLoader('Oops, something went wrong.', 'error');
},
});
Pontoon.endLoader('Notification sent.');
container.empty().append(data);
},
error: function () {
Pontoon.endLoader('Oops, something went wrong.', 'error');
},
});
});
// Recipient shortcuts
container.on('click', '.locale-selector .shortcuts a', function (e) {
e.preventDefault();
// Recipient shortcuts
container.on('click', '.locale-selector .shortcuts a', function (e) {
e.preventDefault();
var locales = $(this).data('ids').reverse(),
$localeSelector = $(this).parents('.locale-selector');
var locales = $(this).data('ids').reverse(),
$localeSelector = $(this).parents('.locale-selector');
$localeSelector.find('.selected .move-all').click();
$localeSelector.find('.selected .move-all').click();
$(locales).each(function (i, id) {
$localeSelector
.find('.locale.select:first')
.find('[data-id=' + id + ']')
.click();
});
$(locales).each(function (i, id) {
$localeSelector
.find('.locale.select:first')
.find('[data-id=' + id + ']')
.click();
});
});
});

Просмотреть файл

@ -1,8 +1,8 @@
export class NotImplementedError extends Error {
constructor(...args) {
super(...args);
Error.captureStackTrace(this, NotImplementedError);
}
constructor(...args) {
super(...args);
Error.captureStackTrace(this, NotImplementedError);
}
}
NotImplementedError.prototype = Error.prototype;

Просмотреть файл

@ -9,7 +9,6 @@ directly, as well as to write its own changes back.
This document describes that sync process in detail.
## Triggering a Sync
Pontoon is assumed to run a sync once an hour, although this is configurable.
@ -18,7 +17,6 @@ disabled within the admin interface and schedules a sync task for each one.
Sync tasks are executed in parallel, using [Celery](http://www.celeryproject.org/)
to manage the worker queue.
## Syncing a Project
Syncing an individual project is split into two tasks. The first one is syncing
@ -46,7 +44,6 @@ The second step is syncing translations:
changes, no commit is made.
- Clean up leftover information in the database.
## Comparing Entities
The heart of the syncing process is comparing an entity stored in Pontoon's
@ -70,7 +67,6 @@ The actual comparison logic goes something like this:
![](./sync-process-diagram.png)
## Executing Changes
Entity comparison produces a Changeset, which is used to make the necessary
@ -80,24 +76,24 @@ Changesets can perform 4 different operations on an entity:
**Update Pontoon from VCS**
&emsp;Add a translation from VCS to Pontoon if necessary. Existing translations
that match the VCS translation are re-used, and all non-matching translations
are marked as unapproved.
&emsp;Add a translation from VCS to Pontoon if necessary. Existing translations
that match the VCS translation are re-used, and all non-matching translations
are marked as unapproved.
**Update VCS from Pontoon**
&emsp;Add a translation from Pontoon to VCS, overwriting the existing translation
if it exists.
&emsp;Add a translation from Pontoon to VCS, overwriting the existing translation
if it exists.
**Create New Entity in Pontoon**
&emsp;Create a new entity in the Pontoon database, including the VCS translation if
it is present.
&emsp;Create a new entity in the Pontoon database, including the VCS translation if
it is present.
**Obsolete Pontoon Entity**
&emsp;Mark an entity in the database as obsolete, due to it not existing in VCS.
The entity will no longer appear on the website.
&emsp;Mark an entity in the database as obsolete, due to it not existing in VCS.
The entity will no longer appear on the website.
When possible, Changesets perform database operations in bulk in order to speed
up the syncing process.

Просмотреть файл

@ -1,141 +1,141 @@
/** @listing */
.log-list {
text-align: left;
width: 100%;
text-align: left;
width: 100%;
}
.log-list .log-list-column {
font-weight: bold;
padding: 0.4em;
text-transform: uppercase;
font-weight: bold;
padding: 0.4em;
text-transform: uppercase;
}
.sync-log .start-time,
.sync-log .start-date,
.sync-log .duration {
padding: 0.4em;
font-size: 14px;
padding: 0.4em;
font-size: 14px;
}
.sync-log .start-time a {
color: #7bc876;
color: #7bc876;
}
/** @details */
td {
padding: 5px 10px;
padding: 5px 10px;
}
.sync-details {
display: flex;
flex-direction: row;
justify-content: center;
list-style-type: none;
margin: 0;
padding: 1em;
text-align: center;
display: flex;
flex-direction: row;
justify-content: center;
list-style-type: none;
margin: 0;
padding: 1em;
text-align: center;
}
.sync-details .detail-item {
flex: 1;
margin: 0 1px;
flex: 1;
margin: 0 1px;
}
.sync-details .detail-label {
border: none;
color: #aaaaaa;
font-size: 12px;
margin: 0.5em 0;
text-transform: uppercase;
border: none;
color: #aaaaaa;
font-size: 12px;
margin: 0.5em 0;
text-transform: uppercase;
}
.sync-details .detail-value {
border: none;
font-size: 16px;
border: none;
font-size: 16px;
}
.command-details {
background: #333941;
background: #333941;
}
.command-details .detail-item {
border-top: 4px solid;
flex: 0 0 325px;
border-top: 4px solid;
flex: 0 0 325px;
}
.command-details .start-time {
border-color: #7bc876;
border-color: #7bc876;
}
.command-details .end-time {
border-color: #4fc4f6;
border-color: #4fc4f6;
}
.command-details .duration {
border-color: #fed271;
border-color: #fed271;
}
.project-details .start-time .detail-label,
.repository-details .start-time .detail-label {
color: #7bc876;
color: #7bc876;
}
.project-details .end-time .detail-label,
.repository-details .end-time .detail-label {
color: #4fc4f6;
color: #4fc4f6;
}
.project-details .duration .detail-label,
.repository-details .duration .detail-label {
color: #fed271;
color: #fed271;
}
.project {
border: 1px solid #5c6172;
border-radius: 5px;
margin-bottom: 1em;
padding: 1em;
border: 1px solid #5c6172;
border-radius: 5px;
margin-bottom: 1em;
padding: 1em;
}
.project .project-name {
color: #aaaaaa;
font-size: 36px;
text-align: left;
text-transform: none;
color: #aaaaaa;
font-size: 36px;
text-align: left;
text-transform: none;
}
.repository-logs {
list-style-type: none;
width: 100%;
list-style-type: none;
width: 100%;
}
.repository-logs .repository-logs-column {
color: #aaaaaa;
padding: 1em;
text-align: left;
text-transform: uppercase;
color: #aaaaaa;
padding: 1em;
text-align: left;
text-transform: uppercase;
}
.repository .repository-url {
vertical-align: middle;
vertical-align: middle;
}
.repository:nth-child(even) {
background: #333941;
background: #333941;
}
/** @pagination */
.pagination {
font-size: 14px;
padding: 3em 0;
text-align: center;
text-transform: uppercase;
font-size: 14px;
padding: 3em 0;
text-align: center;
text-transform: uppercase;
}
.pagination .previous {
float: left;
float: left;
}
.pagination .next {
float: right;
float: right;
}

Просмотреть файл

@ -1,38 +1,38 @@
#info-wrapper .edit-info .fa {
padding-right: 5px;
padding-right: 5px;
}
#info-wrapper .read-write-info textarea {
background: #333941;
border: 1px solid #4d5967;
border-radius: 3px;
color: #ffffff;
font-weight: 300;
margin-bottom: 10px;
padding: 5px;
width: 100%;
background: #333941;
border: 1px solid #4d5967;
border-radius: 3px;
color: #ffffff;
font-weight: 300;
margin-bottom: 10px;
padding: 5px;
width: 100%;
-moz-box-sizing: border-box;
box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
#info-wrapper .read-write-info .toolbar .subtitle {
color: #aaa;
float: left;
text-transform: uppercase;
color: #aaa;
float: left;
text-transform: uppercase;
}
#info-wrapper .controls {
text-align: right;
text-align: right;
}
#info-wrapper .controls .cancel {
display: none;
color: #7bc876;
margin: 9px;
text-transform: uppercase;
display: none;
color: #7bc876;
margin: 9px;
text-transform: uppercase;
}
#info-wrapper .controls .save {
display: none;
display: none;
}

Просмотреть файл

@ -2,70 +2,70 @@
* All styles are related to forms visible for the contributor.
*/
form.user-locales-settings {
width: 620px;
width: 620px;
}
form.user-locales-settings div {
text-align: right;
text-align: right;
}
/**
* CSS classes related to the select locale widget.
*/
form .locale.select.selected {
float: right;
float: right;
}
form .locale.select .menu {
background: transparent;
border-bottom: 1px solid #5e6475;
margin: 2px 0 -4px -1px;
overflow: auto;
padding: 10px 0;
width: 295px;
background: transparent;
border-bottom: 1px solid #5e6475;
margin: 2px 0 -4px -1px;
overflow: auto;
padding: 10px 0;
width: 295px;
}
form > div {
margin: 20px 0;
margin: 20px 0;
}
form .locale.select .menu ul {
height: 170px;
margin-bottom: 0;
height: 170px;
margin-bottom: 0;
}
form .locale.select .menu ul li span.code {
float: right;
width: auto;
float: right;
width: auto;
}
form .locale.select {
float: left;
width: auto;
float: left;
width: auto;
}
form .locale li {
cursor: pointer;
cursor: pointer;
}
form .locale .sortable li {
cursor: grab;
cursor: -webkit-grab;
cursor: grab;
cursor: -webkit-grab;
}
.select {
text-align: left;
text-align: left;
}
label {
display: block;
padding-bottom: 3px;
text-align: left;
display: block;
padding-bottom: 3px;
text-align: left;
}
form a:link,
form a:visited {
color: #7bc876;
float: right;
text-transform: uppercase;
color: #7bc876;
float: right;
text-transform: uppercase;
}

Просмотреть файл

@ -1,185 +1,185 @@
.request-toggle {
float: right;
float: right;
}
.request-toggle:before {
margin-right: 2px;
margin-right: 2px;
}
.request-toggle.back:after {
margin-left: 2px;
margin-left: 2px;
}
/* Bug 1468997 */
.request-team {
display: none;
display: none;
}
.request-team:before {
content: 'Request new team';
content: 'Request new team';
}
.request-team.back:after {
content: 'Back to enabled teams';
content: 'Back to enabled teams';
}
.request-projects:before {
content: 'Request more projects';
content: 'Request more projects';
}
.request-projects.back:after {
content: 'Back to enabled projects';
content: 'Back to enabled projects';
}
.request-teams:before {
content: 'Request new language';
content: 'Request new language';
}
.request-teams.back:after {
content: 'Back to enabled languages';
content: 'Back to enabled languages';
}
.request-toggle:after,
.request-toggle.back:before {
content: '';
content: '';
}
#request-team-form {
margin: 0 auto;
display: none;
margin: 0 auto;
display: none;
}
#request-item-note {
margin: 25px 0 5px;
display: none;
margin: 25px 0 5px;
display: none;
}
#request-item-note p {
font-style: italic;
color: #aaaaaa;
text-align: center;
display: block;
font-style: italic;
color: #aaaaaa;
text-align: center;
display: block;
}
#request-item {
display: none;
background: #7bc876;
border: none;
border-radius: 3px;
margin-top: 15px;
padding: 10px;
text-transform: uppercase;
width: 100%;
display: none;
background: #7bc876;
border: none;
border-radius: 3px;
margin-top: 15px;
padding: 10px;
text-transform: uppercase;
width: 100%;
}
#request-item.confirmed {
background: #fed271;
background: #fed271;
}
#team-form {
display: flex;
position: relative;
text-align: left;
margin: 0 auto;
justify-content: space-between;
display: flex;
position: relative;
text-align: left;
margin: 0 auto;
justify-content: space-between;
}
#team-form .field {
display: inline-block;
margin-top: 10px;
text-align: left;
display: inline-block;
margin-top: 10px;
text-align: left;
}
#team-form .field input {
color: #ffffff;
background: #333941;
border: 1px solid #4d5967;
border-radius: 3px;
float: none;
width: 480px;
padding: 4px;
box-shadow: none;
color: #ffffff;
background: #333941;
border: 1px solid #4d5967;
border-radius: 3px;
float: none;
width: 480px;
padding: 4px;
box-shadow: none;
-moz-box-sizing: border-box;
box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
#team-form .field label {
display: block;
padding-bottom: 3px;
text-align: left;
display: block;
padding-bottom: 3px;
text-align: left;
}
.item-list tbody tr:not(.limited) {
display: none;
display: none;
}
.items.request .item-list .all-strings {
display: table-cell;
display: table-cell;
}
.items.request .item-list th.check {
text-align: right;
width: auto;
text-align: right;
width: auto;
}
.item-list .check,
.item-list .radio,
.item-list .all-strings {
display: none;
display: none;
}
.items.request .item-list .check,
.items.request .item-list .radio {
cursor: pointer;
display: block;
cursor: pointer;
display: block;
}
.item-list td.check,
.item-list td.radio {
color: #3f4752;
float: right;
font-size: 16px;
height: 47px;
text-align: right;
width: 100%;
color: #3f4752;
float: right;
font-size: 16px;
height: 47px;
text-align: right;
width: 100%;
-moz-box-sizing: border-box;
box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.item-list td.radio {
color: #4d5967;
color: #4d5967;
}
.item-list td.check:before {
content: '';
display: block;
margin-top: 16px;
content: '';
display: block;
margin-top: 16px;
}
.item-list td.check:hover:before,
.item-list td.check.enabled:before {
content: '';
color: #7bc876;
content: '';
color: #7bc876;
}
.item-list td.check.enabled:before {
content: '';
content: '';
}
.item-list td.radio:before {
display: block;
margin-top: 16px;
display: block;
margin-top: 16px;
}
.item-list td.radio:hover:before,
.item-list td.radio.enabled:before {
color: #7bc876;
color: #7bc876;
}
.items.request .item-list .latest-activity,
.items.request .item-list .progress,
.items.request .item-list .unreviewed-status {
display: none;
display: none;
}

Просмотреть файл

@ -1,221 +1,221 @@
.buglist {
display: none;
table-layout: fixed;
display: none;
table-layout: fixed;
}
.buglist .id {
width: 80px;
width: 80px;
}
.buglist .summary {
width: 430px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 430px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.buglist .last-changed {
text-align: left;
width: 130px;
text-align: left;
width: 130px;
}
.buglist .assigned-to {
width: 260px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
width: 260px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
}
.buglist tbody .id a {
color: #7bc876;
color: #7bc876;
}
.buglist th {
cursor: pointer;
cursor: pointer;
}
.buglist th:hover {
color: #ebebeb;
color: #ebebeb;
}
.buglist th i {
margin-left: 5px;
position: absolute;
margin-left: 5px;
position: absolute;
}
.buglist th.asc i:after {
content: '';
display: inline-block;
margin-top: 5px;
content: '';
display: inline-block;
margin-top: 5px;
}
.buglist th.desc i:after {
content: '';
display: inline-block;
margin-top: -5px;
content: '';
display: inline-block;
margin-top: -5px;
}
.controls.no-projects {
text-align: right;
text-align: right;
}
.controls.no-projects > .request-toggle.button {
float: none;
float: none;
}
.controls.no-projects > .search-wrapper {
display: none;
display: none;
}
#permissions-form .selector-wrapper {
white-space: nowrap;
white-space: nowrap;
}
#permissions-form h3 {
padding-bottom: 30px;
padding-bottom: 30px;
}
#permissions-form h3 .remove-project {
bottom: 25px;
font-size: 13px;
font-style: normal;
letter-spacing: 0;
position: absolute;
right: 0;
bottom: 25px;
font-size: 13px;
font-style: normal;
letter-spacing: 0;
position: absolute;
right: 0;
}
#permissions-form h3 .remove-project:hover {
background: #f36;
background: #f36;
}
#permissions-form h3 .remove-project .fa {
float: left;
font-size: 13px;
margin-top: 2px;
padding-right: 5px;
float: left;
font-size: 13px;
margin-top: 2px;
padding-right: 5px;
}
#permissions-form .permissions-groups {
margin-bottom: 80px;
margin-bottom: 80px;
}
#permissions-form .user.select {
display: inline-block;
vertical-align: top;
width: 300px;
display: inline-block;
vertical-align: top;
width: 300px;
}
#permissions-form .user.select.translators {
text-align: center;
text-align: center;
}
#permissions-form .user.select.managers {
text-align: right;
text-align: right;
}
#permissions-form .user.select.translators,
#permissions-form .user.select.managers {
margin-left: 40px;
margin-left: 40px;
}
#permissions-form .user.select label {
display: block;
display: block;
}
#permissions-form .user.select label,
#permissions-form .user.select label a {
color: #aaaaaa;
font-size: 13px;
font-weight: bold;
text-align: center;
text-transform: uppercase;
color: #aaaaaa;
font-size: 13px;
font-weight: bold;
text-align: center;
text-transform: uppercase;
}
#permissions-form .user.select label a {
display: inline-block;
display: inline-block;
}
#permissions-form .user.select.translators label:hover,
#permissions-form .user.select.managers label:hover,
#permissions-form .user.select label a.active,
#permissions-form .user.select label a:hover {
color: #ebebeb;
color: #ebebeb;
}
#permissions-form .user.select .menu {
border-bottom: 1px solid #5e6475;
overflow: auto;
padding: 10px 0;
border-bottom: 1px solid #5e6475;
overflow: auto;
padding: 10px 0;
}
#permissions-form .user.select .menu input[type='search'] {
width: 100%;
width: 100%;
}
#permissions-form .user.select .menu ul {
height: 168px;
margin-bottom: 0;
white-space: normal;
height: 168px;
margin-bottom: 0;
white-space: normal;
}
#permissions-form .user.select .menu ul li {
cursor: pointer;
line-height: 17px;
position: relative;
width: 100%;
cursor: pointer;
line-height: 17px;
position: relative;
width: 100%;
-moz-box-sizing: border-box;
box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
#permissions-form .user.select.available .menu ul li:not(.contributor) {
display: none;
display: none;
}
#permissions-form .user.select .intro {
color: #aaa;
font-size: 13px;
font-style: italic;
font-weight: 300;
margin: 10px 0;
text-align: center;
white-space: normal;
color: #aaa;
font-size: 13px;
font-style: italic;
font-weight: 300;
margin: 10px 0;
text-align: center;
white-space: normal;
}
#permissions-form .button.save {
float: right;
float: right;
}
#permissions-form #project-selector {
float: right;
float: right;
}
#permissions-form #project-selector .button {
margin-right: 10px;
padding: 6px 12px;
margin-right: 10px;
padding: 6px 12px;
}
#permissions-form #project-selector .button .icon {
float: right;
margin-left: 5px;
float: right;
margin-left: 5px;
}
#permissions-form #project-selector .menu {
background: #333941;
bottom: 28px;
width: 287px;
background: #333941;
bottom: 28px;
width: 287px;
}
#permissions-form #project-selector .menu .search-wrapper input {
width: 100%;
width: 100%;
}
#permissions-form #project-selector .menu li {
color: #cccccc;
cursor: pointer;
color: #cccccc;
cursor: pointer;
}
#project-selector .menu li:not(.limited) {
display: none;
display: none;
}

Просмотреть файл

@ -1,53 +1,53 @@
.locale-selector .locale.select .button.breadcrumbs {
background: #333941;
border-radius: 2px;
box-sizing: border-box;
font-size: 14px;
height: 28px;
margin: 0;
padding: 6px 12px;
text-align: left;
text-transform: uppercase;
width: 240px;
background: #333941;
border-radius: 2px;
box-sizing: border-box;
font-size: 14px;
height: 28px;
margin: 0;
padding: 6px 12px;
text-align: left;
text-transform: uppercase;
width: 240px;
}
.locale-selector .locale.select .button:before,
.locale-selector .locale.select .button:after {
display: none;
display: none;
}
.locale-selector .locale.select.opened .button {
border-radius: 2px 2px 0 0;
border-radius: 2px 2px 0 0;
}
.locale-selector .locale.select .button .code {
text-transform: none;
text-transform: none;
}
.locale-selector .locale.select .menu {
background: #333941;
top: 28px;
display: none;
position: absolute;
right: 0;
width: 240px;
background: #333941;
top: 28px;
display: none;
position: absolute;
right: 0;
width: 240px;
}
.locale-selector .locale.select .menu ul {
max-height: 214px;
max-height: 214px;
}
.locale-selector .locale.select .menu li {
color: #cccccc;
cursor: pointer;
text-align: left;
color: #cccccc;
cursor: pointer;
text-align: left;
}
.locale-selector .locale.select .code {
float: right;
float: right;
}
.locale-selector .locale.select .menu ul li span.code {
float: right;
width: auto;
float: right;
width: auto;
}

Просмотреть файл

@ -1,177 +1,164 @@
var Pontoon = (function (my) {
return $.extend(true, my, {
bugzilla: {
/*
* Retrieve bugs for the given locale and update bug count and tab content
* using the provided elements and callbacks.
*
* Heavily inspired by the similar functionality available in Elmo.
*
* Source: https://github.com/mozilla/elmo/blob/master/apps/bugsy/static/bugsy/js/bugcount.js
* Authors: Pike, peterbe, adngdb
*/
getLocaleBugs: function (
locale,
container,
tab,
countCallback,
errorCallback,
) {
return $.ajax({
url: 'https://bugzilla.mozilla.org/rest/bug',
data: {
'field0-0-0': 'component',
'type0-0-0': 'regexp',
'value0-0-0': '^' + locale + ' / ',
'field0-0-1': 'cf_locale',
'type0-0-1': 'regexp',
'value0-0-1': '^' + locale + ' / ',
resolution: '---',
include_fields:
'id,summary,last_change_time,assigned_to',
},
success: function (data) {
if (data.bugs.length) {
data.bugs.sort(function (l, r) {
return l.last_change_time < r.last_change_time
? 1
: -1;
});
return $.extend(true, my, {
bugzilla: {
/*
* Retrieve bugs for the given locale and update bug count and tab content
* using the provided elements and callbacks.
*
* Heavily inspired by the similar functionality available in Elmo.
*
* Source: https://github.com/mozilla/elmo/blob/master/apps/bugsy/static/bugsy/js/bugcount.js
* Authors: Pike, peterbe, adngdb
*/
getLocaleBugs: function (
locale,
container,
tab,
countCallback,
errorCallback,
) {
return $.ajax({
url: 'https://bugzilla.mozilla.org/rest/bug',
data: {
'field0-0-0': 'component',
'type0-0-0': 'regexp',
'value0-0-0': '^' + locale + ' / ',
'field0-0-1': 'cf_locale',
'type0-0-1': 'regexp',
'value0-0-1': '^' + locale + ' / ',
resolution: '---',
include_fields: 'id,summary,last_change_time,assigned_to',
},
success: function (data) {
if (data.bugs.length) {
data.bugs.sort(function (l, r) {
return l.last_change_time < r.last_change_time ? 1 : -1;
});
var tbody = $('<tbody>'),
formatter = new Intl.DateTimeFormat('en-GB', {
day: 'numeric',
month: 'long',
year: 'numeric',
});
$.each(data.bugs, function (i, bug) {
// Prevent malicious bug summary from executin JS code
var summary = Pontoon.doNotRender(bug.summary);
var tr = $('<tr>', {
title: summary,
});
$('<td>', {
class: 'id',
html:
'<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=' +
bug.id +
'">' +
bug.id +
'</a>',
}).appendTo(tr);
$('<td>', {
class: 'summary',
html: summary,
}).appendTo(tr);
$('<td>', {
class: 'last-changed',
datetime: bug.last_change_time,
html: formatter.format(
new Date(bug.last_change_time),
),
}).appendTo(tr);
$('<td>', {
class: 'assigned-to',
html: bug.assigned_to,
}).appendTo(tr);
tbody.append(tr);
});
var table = $('<table>', {
class: 'buglist striped',
html:
'<thead>' +
'<tr>' +
'<th class="id">ID<i class="fa"></i></th>' +
'<th class="summary">Summary<i class="fa"></i></th>' +
'<th class="last-changed desc">Last Changed<i class="fa"></i></th>' +
'<th class="assigned-to">Assigned To<i class="fa"></i></th>' +
'</tr>' +
'</thead>',
}).append(tbody);
container.append(table.show());
var count = data.bugs.length;
countCallback(tab, count);
} else {
errorCallback('Zarro Boogs Found.');
}
},
error: function (error) {
if (
error.status === 0 &&
error.statusText !== 'abort'
) {
errorCallback(
'Oops, something went wrong. We were unable to load the bugs. Please try again later.',
);
}
},
var tbody = $('<tbody>'),
formatter = new Intl.DateTimeFormat('en-GB', {
day: 'numeric',
month: 'long',
year: 'numeric',
});
},
/*
* Sort Bug Table
*/
sort: (function () {
$('body').on('click', 'table.buglist th', function () {
function getString(el) {
return $(el)
.find('td:eq(' + index + ')')
.text();
}
$.each(data.bugs, function (i, bug) {
// Prevent malicious bug summary from executin JS code
var summary = Pontoon.doNotRender(bug.summary);
function getNumber(el) {
return parseInt(
$(el).find('.id').text().replace(/,/g, ''),
);
}
function getTime(el) {
var date =
$(el).find('.last-changed').attr('datetime') || 0;
return new Date(date).getTime();
}
var node = $(this),
index = node.index(),
table = node.parents('.buglist'),
list = table.find('tbody'),
items = list.find('tr'),
dir = node.hasClass('desc') ? -1 : 1,
cls = node.hasClass('desc') ? 'asc' : 'desc';
$(table).find('th').removeClass('asc desc');
node.addClass(cls);
items.sort(function (a, b) {
// Sort by bugzilla ID
if (node.is('.id')) {
return (getNumber(a) - getNumber(b)) * dir;
// Sort by last changed
} else if (node.is('.last-changed')) {
return (getTime(b) - getTime(a)) * dir;
// Sort by alphabetical order
} else {
return (
getString(a).localeCompare(getString(b)) * dir
);
}
});
list.append(items);
var tr = $('<tr>', {
title: summary,
});
})(),
},
});
$('<td>', {
class: 'id',
html:
'<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=' +
bug.id +
'">' +
bug.id +
'</a>',
}).appendTo(tr);
$('<td>', {
class: 'summary',
html: summary,
}).appendTo(tr);
$('<td>', {
class: 'last-changed',
datetime: bug.last_change_time,
html: formatter.format(new Date(bug.last_change_time)),
}).appendTo(tr);
$('<td>', {
class: 'assigned-to',
html: bug.assigned_to,
}).appendTo(tr);
tbody.append(tr);
});
var table = $('<table>', {
class: 'buglist striped',
html:
'<thead>' +
'<tr>' +
'<th class="id">ID<i class="fa"></i></th>' +
'<th class="summary">Summary<i class="fa"></i></th>' +
'<th class="last-changed desc">Last Changed<i class="fa"></i></th>' +
'<th class="assigned-to">Assigned To<i class="fa"></i></th>' +
'</tr>' +
'</thead>',
}).append(tbody);
container.append(table.show());
var count = data.bugs.length;
countCallback(tab, count);
} else {
errorCallback('Zarro Boogs Found.');
}
},
error: function (error) {
if (error.status === 0 && error.statusText !== 'abort') {
errorCallback(
'Oops, something went wrong. We were unable to load the bugs. Please try again later.',
);
}
},
});
},
/*
* Sort Bug Table
*/
sort: (function () {
$('body').on('click', 'table.buglist th', function () {
function getString(el) {
return $(el)
.find('td:eq(' + index + ')')
.text();
}
function getNumber(el) {
return parseInt($(el).find('.id').text().replace(/,/g, ''));
}
function getTime(el) {
var date = $(el).find('.last-changed').attr('datetime') || 0;
return new Date(date).getTime();
}
var node = $(this),
index = node.index(),
table = node.parents('.buglist'),
list = table.find('tbody'),
items = list.find('tr'),
dir = node.hasClass('desc') ? -1 : 1,
cls = node.hasClass('desc') ? 'asc' : 'desc';
$(table).find('th').removeClass('asc desc');
node.addClass(cls);
items.sort(function (a, b) {
// Sort by bugzilla ID
if (node.is('.id')) {
return (getNumber(a) - getNumber(b)) * dir;
// Sort by last changed
} else if (node.is('.last-changed')) {
return (getTime(b) - getTime(a)) * dir;
// Sort by alphabetical order
} else {
return getString(a).localeCompare(getString(b)) * dir;
}
});
list.append(items);
});
})(),
},
});
})(Pontoon || {});

Просмотреть файл

@ -1,55 +1,55 @@
$(function () {
var container = $('#main .container');
var container = $('#main .container');
function toggleWidgets() {
function toggleWidgets() {
container
.find('.controls > *')
.toggle()
.end()
.find('.read-only-info')
.toggle()
.end()
.find('.read-write-info')
.toggleClass('hidden');
}
container.on('click', '#info-wrapper .edit-info', function (e) {
e.preventDefault();
var content = container.find('.info').html();
var textArea = container
.find('.read-write-info textarea')
.val($.trim(content));
toggleWidgets();
textArea.focus();
});
container.on('click', '#info-wrapper .cancel', function (e) {
e.preventDefault();
toggleWidgets();
});
container.on('click', '#info-wrapper .save', function (e) {
e.preventDefault();
var textArea = container.find('.read-write-info textarea');
$.ajax({
url: textArea.parent().data('url'),
type: 'POST',
data: {
csrfmiddlewaretoken: $('body').data('csrf'),
team_info: textArea.val(),
},
success: function (data) {
container
.find('.controls > *')
.toggle()
.end()
.find('.read-only-info')
.toggle()
.end()
.find('.read-write-info')
.toggleClass('hidden');
}
container.on('click', '#info-wrapper .edit-info', function (e) {
e.preventDefault();
var content = container.find('.info').html();
var textArea = container
.find('.read-write-info textarea')
.val($.trim(content));
.find('.info')
.html(data)
.toggle(data !== '');
container.find('.no-results').toggle(data === '');
toggleWidgets();
textArea.focus();
});
container.on('click', '#info-wrapper .cancel', function (e) {
e.preventDefault();
toggleWidgets();
});
container.on('click', '#info-wrapper .save', function (e) {
e.preventDefault();
var textArea = container.find('.read-write-info textarea');
$.ajax({
url: textArea.parent().data('url'),
type: 'POST',
data: {
csrfmiddlewaretoken: $('body').data('csrf'),
team_info: textArea.val(),
},
success: function (data) {
container
.find('.info')
.html(data)
.toggle(data !== '');
container.find('.no-results').toggle(data === '');
toggleWidgets();
Pontoon.endLoader('Team info saved.');
},
error: function (request) {
Pontoon.endLoader(request.responseText, 'error');
},
});
Pontoon.endLoader('Team info saved.');
},
error: function (request) {
Pontoon.endLoader(request.responseText, 'error');
},
});
});
});

Просмотреть файл

@ -1,58 +1,58 @@
// Contains behaviours of widgets that are shared between admin and end-user interface.
$(function () {
/**
* Function keeps track of inputs that contain information about the order of selected locales.
*/
function updateSelectedLocales() {
var $selectedList = $('.multiple-team-selector .locale.selected'),
$selectedLocalesField = $selectedList.find('input[type=hidden]'),
selectedLocales = $selectedList
.find('li[data-id]')
.map(function () {
return $(this).data('id');
})
.get();
/**
* Function keeps track of inputs that contain information about the order of selected locales.
*/
function updateSelectedLocales() {
var $selectedList = $('.multiple-team-selector .locale.selected'),
$selectedLocalesField = $selectedList.find('input[type=hidden]'),
selectedLocales = $selectedList
.find('li[data-id]')
.map(function () {
return $(this).data('id');
})
.get();
$selectedLocalesField.val(selectedLocales.join());
}
$selectedLocalesField.val(selectedLocales.join());
}
// Choose locales
$('body').on(
'click',
'.multiple-team-selector .locale.select li',
function () {
var ls = $(this).parents('.locale.select'),
target = ls.siblings('.locale.select').find('ul'),
item = $(this).remove();
// Choose locales
$('body').on(
'click',
'.multiple-team-selector .locale.select li',
function () {
var ls = $(this).parents('.locale.select'),
target = ls.siblings('.locale.select').find('ul'),
item = $(this).remove();
target.append(item);
target.scrollTop(target[0].scrollHeight);
updateSelectedLocales();
},
);
target.append(item);
target.scrollTop(target[0].scrollHeight);
updateSelectedLocales();
},
);
// Choose/remove all locales
$('body').on('click', '.multiple-team-selector .move-all', function (e) {
e.preventDefault();
var ls = $(this).parents('.locale.select'),
target = ls.siblings('.locale.select').find('ul'),
items = ls.find('li:visible:not(".no-match")').remove();
// Choose/remove all locales
$('body').on('click', '.multiple-team-selector .move-all', function (e) {
e.preventDefault();
var ls = $(this).parents('.locale.select'),
target = ls.siblings('.locale.select').find('ul'),
items = ls.find('li:visible:not(".no-match")').remove();
target.append(items);
target.scrollTop(target[0].scrollHeight);
updateSelectedLocales();
target.append(items);
target.scrollTop(target[0].scrollHeight);
updateSelectedLocales();
});
if ($.ui && $.ui.sortable) {
$('.multiple-team-selector .locale.select .sortable').sortable({
axis: 'y',
containment: 'parent',
update: updateSelectedLocales,
tolerance: 'pointer',
});
}
if ($.ui && $.ui.sortable) {
$('.multiple-team-selector .locale.select .sortable').sortable({
axis: 'y',
containment: 'parent',
update: updateSelectedLocales,
tolerance: 'pointer',
});
}
$('body').on('submit', '.form.user-locales-settings', function () {
updateSelectedLocales();
});
$('body').on('submit', '.form.user-locales-settings', function () {
updateSelectedLocales();
});
});

Просмотреть файл

@ -1,163 +1,155 @@
$(function () {
var container = $('#main .container');
var container = $('#main .container');
function inputHidden(name, value, cssClass) {
return $(
'<input class="' +
(cssClass || '') +
'" type="hidden" name="' +
name +
'" value="' +
value +
'">',
);
function inputHidden(name, value, cssClass) {
return $(
'<input class="' +
(cssClass || '') +
'" type="hidden" name="' +
name +
'" value="' +
value +
'">',
);
}
container.on('click', '#permissions-form .save', function (e) {
e.preventDefault();
var $form = $('#permissions-form');
// Remove stale permissions items (bug 1416890)
$('input.permissions-form-item').remove();
// Before submitting the form, update translators and managers
$.each(['translators', 'managers'], function (i, value) {
var data = $form.find('.user.' + value + ' li');
data.each(function () {
var itemId = $(this).data('id');
if ($(this).parents('.general').length > 0) {
$form.append(
inputHidden('general-' + value, itemId, 'permissions-form-item'),
);
} else {
// We have to retrieve an index of parent project locale form
var localeProjectIndex = $(this)
.parents('.project-locale')
.data('index');
$form.append(
inputHidden(
'project-locale-' + localeProjectIndex + '-translators',
itemId,
'permissions-form-item',
),
);
}
});
});
$.ajax({
url: $('#permissions-form').prop('action'),
type: $('#permissions-form').prop('method'),
data: $('#permissions-form').serialize(),
success: function () {
Pontoon.endLoader('Permissions saved.');
},
error: function () {
Pontoon.endLoader('Oops, something went wrong.', 'error');
},
});
});
// Switch available users
container.on('click', '.user.available label a', function (e) {
e.preventDefault();
$(this).addClass('active').siblings('a').removeClass('active');
var available = $(this).parents('.user.available');
available.find('li').show();
if ($(this).is('.contributors')) {
available.find('li:not(".contributor")').hide();
}
container.on('click', '#permissions-form .save', function (e) {
e.preventDefault();
var $form = $('#permissions-form');
available.find('.search-wrapper input').trigger('input').focus();
});
// Remove stale permissions items (bug 1416890)
$('input.permissions-form-item').remove();
// While in contributors tab, search contributors only
// Has to be attached to body, like the input.search event in main.js
$('body').on(
'input.search',
'.user.available .menu input[type=search]',
function () {
var available = $(this).parents('.user.available');
// Before submitting the form, update translators and managers
$.each(['translators', 'managers'], function (i, value) {
var data = $form.find('.user.' + value + ' li');
data.each(function () {
var itemId = $(this).data('id');
if (available.find('label a.contributors').is('.active')) {
available.find('li:not(".contributor")').hide();
}
},
);
if ($(this).parents('.general').length > 0) {
$form.append(
inputHidden(
'general-' + value,
itemId,
'permissions-form-item',
),
);
} else {
// We have to retrieve an index of parent project locale form
var localeProjectIndex = $(this)
.parents('.project-locale')
.data('index');
$form.append(
inputHidden(
'project-locale-' +
localeProjectIndex +
'-translators',
itemId,
'permissions-form-item',
),
);
}
});
});
// Focus project selector search field
container.on('click', '#project-selector .selector', function () {
$('#project-selector .search-wrapper input').focus();
});
$.ajax({
url: $('#permissions-form').prop('action'),
type: $('#permissions-form').prop('method'),
data: $('#permissions-form').serialize(),
success: function () {
Pontoon.endLoader('Permissions saved.');
},
error: function () {
Pontoon.endLoader('Oops, something went wrong.', 'error');
},
});
});
// Add project
container.on('click', '#project-selector .menu li', function () {
var slug = $(this).data('slug'),
$permsForm = $(".project-locale[data-slug='" + slug + "']");
// Switch available users
container.on('click', '.user.available label a', function (e) {
e.preventDefault();
$('.project-locale:last').after($permsForm.removeClass('hidden'));
$(this).addClass('active').siblings('a').removeClass('active');
var available = $(this).parents('.user.available');
available.find('li').show();
if ($(this).is('.contributors')) {
available.find('li:not(".contributor")').hide();
}
available.find('.search-wrapper input').trigger('input').focus();
});
// While in contributors tab, search contributors only
// Has to be attached to body, like the input.search event in main.js
$('body').on(
'input.search',
'.user.available .menu input[type=search]',
function () {
var available = $(this).parents('.user.available');
if (available.find('label a.contributors').is('.active')) {
available.find('li:not(".contributor")').hide();
}
},
$permsForm.append(
inputHidden(
'project-locale-' +
$permsForm.data('index') +
'-has_custom_translators',
1,
),
);
// Focus project selector search field
container.on('click', '#project-selector .selector', function () {
$('#project-selector .search-wrapper input').focus();
});
// Add project
container.on('click', '#project-selector .menu li', function () {
var slug = $(this).data('slug'),
$permsForm = $(".project-locale[data-slug='" + slug + "']");
$('.project-locale:last').after($permsForm.removeClass('hidden'));
$permsForm.append(
inputHidden(
'project-locale-' +
$permsForm.data('index') +
'-has_custom_translators',
1,
),
);
// Update menu (must be above Copying Translators)
$(this).addClass('hidden').removeClass('limited').removeAttr('style');
if ($('#project-selector .menu li:not(".hidden")').length === 0) {
$('#project-selector').addClass('hidden');
}
// Copy Translators from the General section
// Reverse selector order to keep presentation order (prepend)
$(
$('.permissions-groups.general .translators li').get().reverse(),
).each(function () {
$permsForm
.find(
'.user.available li[data-id="' + $(this).data('id') + '"]',
)
.click();
});
// Scroll to the right project locale
$('html, body').animate(
{
scrollTop: $permsForm.offset().top,
},
500,
);
});
// Remove project
container.on('click', '.remove-project', function (e) {
var $permsForm = $(this).parents('.project-locale');
e.preventDefault();
$('#project-selector').removeClass('hidden');
$("#project-selector li[data-slug='" + $permsForm.data('slug') + "']")
.removeClass('hidden')
.addClass('limited');
$permsForm.find('input[name$=has_custom_translators]').remove();
$permsForm.addClass('hidden');
$permsForm.find('.select.translators li').each(function () {
$permsForm.find('.select.available ul').append($(this).remove());
});
// Update menu (must be above Copying Translators)
$(this).addClass('hidden').removeClass('limited').removeAttr('style');
if ($('#project-selector .menu li:not(".hidden")').length === 0) {
$('#project-selector').addClass('hidden');
}
// Copy Translators from the General section
// Reverse selector order to keep presentation order (prepend)
$($('.permissions-groups.general .translators li').get().reverse()).each(
function () {
$permsForm
.find('.user.available li[data-id="' + $(this).data('id') + '"]')
.click();
},
);
// Scroll to the right project locale
$('html, body').animate(
{
scrollTop: $permsForm.offset().top,
},
500,
);
});
// Remove project
container.on('click', '.remove-project', function (e) {
var $permsForm = $(this).parents('.project-locale');
e.preventDefault();
$('#project-selector').removeClass('hidden');
$("#project-selector li[data-slug='" + $permsForm.data('slug') + "']")
.removeClass('hidden')
.addClass('limited');
$permsForm.find('input[name$=has_custom_translators]').remove();
$permsForm.addClass('hidden');
$permsForm.find('.select.translators li').each(function () {
$permsForm.find('.select.available ul').append($(this).remove());
});
});
});

Просмотреть файл

@ -1,270 +1,243 @@
var Pontoon = (function (my) {
return $.extend(true, my, {
requestItem: {
/*
* Toggle available projects/teams and request div
*
* show Show enabled projects/teams?
*/
toggleItem: function (show, type) {
// Toggle
$('.controls .request-toggle')
.toggleClass('back', !show)
.find('span')
.toggleClass('fa-chevron-right', show)
.toggleClass('fa-chevron-left', !show);
return $.extend(true, my, {
requestItem: {
/*
* Toggle available projects/teams and request div
*
* show Show enabled projects/teams?
*/
toggleItem: function (show, type) {
// Toggle
$('.controls .request-toggle')
.toggleClass('back', !show)
.find('span')
.toggleClass('fa-chevron-right', show)
.toggleClass('fa-chevron-left', !show);
if (type === 'locale-projects') {
var localeProjects = $('#server').data('locale-projects');
if (type === 'locale-projects') {
var localeProjects = $('#server').data('locale-projects');
// Hide all projects
$('.items')
.toggleClass('request', !show)
.find('tbody tr')
.toggleClass('limited', !show)
.toggle(!show);
// Hide all projects
$('.items')
.toggleClass('request', !show)
.find('tbody tr')
.toggleClass('limited', !show)
.toggle(!show);
// Show requested projects
$(localeProjects).each(function () {
$('.items')
.find('td[data-slug="' + this + '"]')
.parent()
.toggleClass('limited', show)
.toggle(show);
});
// Show requested projects
$(localeProjects).each(function () {
$('.items')
.find('td[data-slug="' + this + '"]')
.parent()
.toggleClass('limited', show)
.toggle(show);
});
// Toggle table & search box, show no results message based on project visibility
var noProject = $('.project-list tr.limited').length === 0;
$('.project-list').toggleClass('hidden', noProject);
$('menu.controls').toggleClass('no-projects', noProject);
$('.no-results').toggle();
// Toggle table & search box, show no results message based on project visibility
var noProject = $('.project-list tr.limited').length === 0;
$('.project-list').toggleClass('hidden', noProject);
$('menu.controls').toggleClass('no-projects', noProject);
$('.no-results').toggle();
Pontoon.requestItem.toggleButton(!show, 'locale-projects');
} else if (type === 'team') {
// Hide all teams and the search bar
$('.team-list').toggle(show);
$('.search-wrapper').toggle(show);
Pontoon.requestItem.toggleButton(!show, 'locale-projects');
} else if (type === 'team') {
// Hide all teams and the search bar
$('.team-list').toggle(show);
$('.search-wrapper').toggle(show);
// Show team form
$('#request-team-form').toggle(!show);
Pontoon.requestItem.toggleButton(!show, 'team');
}
// Show team form
$('#request-team-form').toggle(!show);
Pontoon.requestItem.toggleButton(!show, 'team');
}
$('.controls input[type=search]:visible').trigger('input');
},
$('.controls input[type=search]:visible').trigger('input');
},
toggleButton: function (condition, type) {
condition = condition || true;
var show = condition;
toggleButton: function (condition, type) {
condition = condition || true;
var show = condition;
if (type === 'locale-projects') {
show =
condition && $('.items td.enabled:visible').length > 0;
} else if (type === 'team') {
show =
condition &&
$.trim($('#request-team-form #id_name').val()) !== '' &&
$.trim($('#request-team-form #id_code').val()) !== '';
}
if (type === 'locale-projects') {
show = condition && $('.items td.enabled:visible').length > 0;
} else if (type === 'team') {
show =
condition &&
$.trim($('#request-team-form #id_name').val()) !== '' &&
$.trim($('#request-team-form #id_code').val()) !== '';
}
$('#request-item-note').toggle(show);
$('#request-item').toggle(show);
},
$('#request-item-note').toggle(show);
$('#request-item').toggle(show);
},
requestProjects: function (locale, projects, type) {
$.ajax({
url: '/' + locale + '/request/',
type: 'POST',
data: {
csrfmiddlewaretoken: $('body').data('csrf'),
projects: projects,
},
success: function () {
Pontoon.endLoader(
'New ' + type + ' request sent.',
'',
5000,
);
},
error: function () {
Pontoon.endLoader(
'Oops, something went wrong.',
'error',
);
},
complete: function () {
$('.items td.check').removeClass('enabled');
$('.items td.radio.enabled').toggleClass(
'far fa fa-circle fa-dot-circle enabled',
);
Pontoon.requestItem.toggleItem(true, 'locale-projects');
window.scrollTo(0, 0);
},
});
},
requestProjects: function (locale, projects, type) {
$.ajax({
url: '/' + locale + '/request/',
type: 'POST',
data: {
csrfmiddlewaretoken: $('body').data('csrf'),
projects: projects,
},
success: function () {
Pontoon.endLoader('New ' + type + ' request sent.', '', 5000);
},
error: function () {
Pontoon.endLoader('Oops, something went wrong.', 'error');
},
complete: function () {
$('.items td.check').removeClass('enabled');
$('.items td.radio.enabled').toggleClass(
'far fa fa-circle fa-dot-circle enabled',
);
Pontoon.requestItem.toggleItem(true, 'locale-projects');
window.scrollTo(0, 0);
},
});
},
requestTeam: function (name, code) {
$.ajax({
url: '/teams/request/',
type: 'POST',
data: {
csrfmiddlewaretoken: $('body').data('csrf'),
name: name,
code: code,
},
success: function () {
Pontoon.endLoader('New team request sent.', '', 5000);
},
error: function (res) {
if (res.status === 409) {
Pontoon.endLoader(res.responseText, 'error');
} else {
Pontoon.endLoader(
'Oops, something went wrong.',
'error',
);
}
},
complete: function () {
$('#request-team-form #id_name').val('');
$('#request-team-form #id_code').val('');
Pontoon.requestItem.toggleButton(true, 'team');
window.scrollTo(0, 0);
},
});
},
},
});
requestTeam: function (name, code) {
$.ajax({
url: '/teams/request/',
type: 'POST',
data: {
csrfmiddlewaretoken: $('body').data('csrf'),
name: name,
code: code,
},
success: function () {
Pontoon.endLoader('New team request sent.', '', 5000);
},
error: function (res) {
if (res.status === 409) {
Pontoon.endLoader(res.responseText, 'error');
} else {
Pontoon.endLoader('Oops, something went wrong.', 'error');
}
},
complete: function () {
$('#request-team-form #id_name').val('');
$('#request-team-form #id_code').val('');
Pontoon.requestItem.toggleButton(true, 'team');
window.scrollTo(0, 0);
},
});
},
},
});
})(Pontoon || {});
$(function () {
var container = $('#main .container');
var type = $('#server').data('locale-projects')
? 'locale-projects'
: 'team';
var container = $('#main .container');
var type = $('#server').data('locale-projects') ? 'locale-projects' : 'team';
// Switch between available projects/teams and projects/team to request
container.on('click', '.controls .request-toggle', function (e) {
// Switch between available projects/teams and projects/team to request
container.on('click', '.controls .request-toggle', function (e) {
e.stopPropagation();
e.preventDefault();
Pontoon.requestItem.toggleItem($(this).is('.back'), type);
});
// Select projects
container.on('click', '.items td.check', function (e) {
if ($('.controls .request-toggle').is('.back:visible')) {
e.stopPropagation();
$(this).toggleClass('enabled');
Pontoon.requestItem.toggleButton(true, (type = 'locale-projects'));
}
});
// Radio button hover behavior
container.on(
{
mouseenter: function () {
$(this).toggleClass('fa-circle fa-dot-circle');
},
mouseleave: function () {
$(this).toggleClass('fa-circle fa-dot-circle');
},
},
'.items td.radio:not(.enabled)',
);
// Select team
container.on('click', '.items td.radio', function (e) {
if ($('.controls .request-toggle').is('.back:visible')) {
e.stopPropagation();
$(this)
.add('.items td.radio.enabled')
.toggleClass('fa far fa-circle fa-dot-circle enabled');
if ($(this).hasClass('enabled')) {
$(this).toggleClass('fa-circle fa-dot-circle');
}
Pontoon.requestItem.toggleButton(true, (type = 'locale-projects'));
}
});
// Prevent openning project page from the request panel
var menu = container.find('.project .menu');
menu.find('a').click(function (e) {
if (menu.find('.search-wrapper > a').is('.back:visible')) {
e.preventDefault();
}
});
// Enter team details
container.on(
'change keyup click',
'#request-team-form input[type=text]',
function (e) {
if ($('.controls .request-toggle').is('.back:visible')) {
e.stopPropagation();
e.preventDefault();
Pontoon.requestItem.toggleButton(true, (type = 'team'));
}
},
);
Pontoon.requestItem.toggleItem($(this).is('.back'), type);
});
// Request projects/team
container.on('click', '#request-item', function (e) {
e.preventDefault();
e.stopPropagation();
// Select projects
container.on('click', '.items td.check', function (e) {
if ($('.controls .request-toggle').is('.back:visible')) {
e.stopPropagation();
var locale = '';
$(this).toggleClass('enabled');
Pontoon.requestItem.toggleButton(true, (type = 'locale-projects'));
}
});
if ($(this).is('.confirmed')) {
// Requesting from team page
if (type === 'locale-projects' && $('body').hasClass('locale')) {
var projects = $('.items td.check.enabled')
.map(function (val, element) {
return $(element).siblings('.name').data('slug');
})
.get();
locale = $('#server').data('locale') || Pontoon.getSelectedLocale();
// Radio button hover behavior
container.on(
{
mouseenter: function () {
$(this).toggleClass('fa-circle fa-dot-circle');
},
mouseleave: function () {
$(this).toggleClass('fa-circle fa-dot-circle');
},
},
'.items td.radio:not(.enabled)',
);
Pontoon.requestItem.requestProjects(locale, projects, 'projects');
// Select team
container.on('click', '.items td.radio', function (e) {
if ($('.controls .request-toggle').is('.back:visible')) {
e.stopPropagation();
$(this).removeClass('confirmed').html('Request new projects');
}
$(this)
.add('.items td.radio.enabled')
.toggleClass('fa far fa-circle fa-dot-circle enabled');
// Requesting from project page
else if (type === 'locale-projects' && $('body').hasClass('project')) {
var project = $('#server').data('project');
locale = $('.items td.radio.enabled').siblings('.name').data('slug');
if ($(this).hasClass('enabled')) {
$(this).toggleClass('fa-circle fa-dot-circle');
}
Pontoon.requestItem.requestProjects(locale, [project], 'language');
Pontoon.requestItem.toggleButton(true, (type = 'locale-projects'));
}
});
$(this).removeClass('confirmed').html('Request new language');
} else if (type === 'team') {
locale = $.trim($('#request-team-form #id_name').val());
var code = $.trim($('#request-team-form #id_code').val());
// Prevent openning project page from the request panel
var menu = container.find('.project .menu');
menu.find('a').click(function (e) {
if (menu.find('.search-wrapper > a').is('.back:visible')) {
e.preventDefault();
}
});
Pontoon.requestItem.requestTeam(locale, code);
// Enter team details
container.on(
'change keyup click',
'#request-team-form input[type=text]',
function (e) {
if ($('.controls .request-toggle').is('.back:visible')) {
e.stopPropagation();
Pontoon.requestItem.toggleButton(true, (type = 'team'));
}
},
);
// Request projects/team
container.on('click', '#request-item', function (e) {
e.preventDefault();
e.stopPropagation();
var locale = '';
if ($(this).is('.confirmed')) {
// Requesting from team page
if (type === 'locale-projects' && $('body').hasClass('locale')) {
var projects = $('.items td.check.enabled')
.map(function (val, element) {
return $(element).siblings('.name').data('slug');
})
.get();
locale =
$('#server').data('locale') || Pontoon.getSelectedLocale();
Pontoon.requestItem.requestProjects(
locale,
projects,
'projects',
);
$(this).removeClass('confirmed').html('Request new projects');
}
// Requesting from project page
else if (
type === 'locale-projects' &&
$('body').hasClass('project')
) {
var project = $('#server').data('project');
locale = $('.items td.radio.enabled')
.siblings('.name')
.data('slug');
Pontoon.requestItem.requestProjects(
locale,
[project],
'language',
);
$(this).removeClass('confirmed').html('Request new language');
} else if (type === 'team') {
locale = $.trim($('#request-team-form #id_name').val());
var code = $.trim($('#request-team-form #id_code').val());
Pontoon.requestItem.requestTeam(locale, code);
$(this).removeClass('confirmed').html('Request new team');
}
} else {
$(this).addClass('confirmed').html('Are you sure?');
}
});
$(this).removeClass('confirmed').html('Request new team');
}
} else {
$(this).addClass('confirmed').html('Are you sure?');
}
});
});

Просмотреть файл

@ -1,8 +1,8 @@
$(function () {
$('.locale-selector .locale .menu li:not(".no-match")').click(function () {
$(this)
.parents('.locale-selector')
.find('.locale .selector')
.html($(this).html());
});
$('.locale-selector .locale .menu li:not(".no-match")').click(function () {
$(this)
.parents('.locale-selector')
.find('.locale .selector')
.html($(this).html());
});
});

Просмотреть файл

@ -2,14 +2,14 @@
/** @type {import('@jest/types').Config.InitialOptions} */
module.exports = {
testEnvironment: 'jsdom',
testURL: 'https://nowhere.com/at/all',
moduleNameMapper: {
'\\.(css|less)$': 'identity-obj-proxy',
},
setupFiles: ['./src/setupTests.js'],
transform: {
'\\.[jt]sx?$': ['babel-jest', { configFile: '../babel.config.json' }],
},
collectCoverage: true,
testEnvironment: 'jsdom',
testURL: 'https://nowhere.com/at/all',
moduleNameMapper: {
'\\.(css|less)$': 'identity-obj-proxy',
},
setupFiles: ['./src/setupTests.js'],
transform: {
'\\.[jt]sx?$': ['babel-jest', { configFile: '../babel.config.json' }],
},
collectCoverage: true,
};

Просмотреть файл

@ -9,26 +9,26 @@ import css from 'rollup-plugin-css-only';
/** @type {import('rollup').RollupOptions} */
const config = {
input: 'src/index.js',
output: { file: 'dist/tag_admin.js' },
input: 'src/index.js',
output: { file: 'dist/tag_admin.js' },
treeshake: 'recommended',
treeshake: 'recommended',
plugins: [
replace({
preventAssignment: true,
'process.env.NODE_ENV': JSON.stringify(
process.env.BUILD ?? 'development',
),
}),
resolve(),
babel({
babelHelpers: 'runtime',
configFile: path.resolve('../babel.config.json'),
}),
commonjs(),
css({ output: 'tag_admin.css' }),
],
plugins: [
replace({
preventAssignment: true,
'process.env.NODE_ENV': JSON.stringify(
process.env.BUILD ?? 'development',
),
}),
resolve(),
babel({
babelHelpers: 'runtime',
configFile: path.resolve('../babel.config.json'),
}),
commonjs(),
css({ output: 'tag_admin.css' }),
],
};
export default config;

Просмотреть файл

@ -3,20 +3,20 @@ import React, { useState } from 'react';
import { TagResourceManager } from './manager.js';
export function TagResourcesButton(props) {
const [open, setOpen] = useState(false);
const message = open
? 'Hide the resource manager for this tag'
: 'Manage resources for this tag';
const [open, setOpen] = useState(false);
const message = open
? 'Hide the resource manager for this tag'
: 'Manage resources for this tag';
const toggle = (ev) => {
ev.preventDefault();
setOpen((open) => !open);
};
const toggle = (ev) => {
ev.preventDefault();
setOpen((open) => !open);
};
return (
<div>
<button onClick={toggle}>{message}</button>
{open ? <TagResourceManager {...props} /> : null}
</div>
);
return (
<div>
<button onClick={toggle}>{message}</button>
{open ? <TagResourceManager {...props} /> : null}
</div>
);
}

Просмотреть файл

@ -4,33 +4,33 @@ import React from 'react';
import { TagResourcesButton } from './button.js';
test('TagResourcesButton renders only button initially', () => {
const button = shallow(
<TagResourcesButton tag='foo' api='bar' project='baz' />,
);
expect(button.html()).toBe(
'<div><button>Manage resources for this tag</button></div>',
);
const button = shallow(
<TagResourcesButton tag='foo' api='bar' project='baz' />,
);
expect(button.html()).toBe(
'<div><button>Manage resources for this tag</button></div>',
);
});
test('TagResourcesButton shows TagResourceManager when clicked', () => {
const button = shallow(
<TagResourcesButton tag='foo' api='bar' project='baz' />,
);
const button = shallow(
<TagResourcesButton tag='foo' api='bar' project='baz' />,
);
const preventDefault = jest.fn();
button.find('button').simulate('click', { preventDefault });
expect(button.html()).toMatch('<div class="tag-resource-widget">');
expect(preventDefault).toHaveBeenCalled();
const preventDefault = jest.fn();
button.find('button').simulate('click', { preventDefault });
expect(button.html()).toMatch('<div class="tag-resource-widget">');
expect(preventDefault).toHaveBeenCalled();
});
test('TagResourcesButton renders only Button when clicked twice', () => {
const button = shallow(
<TagResourcesButton tag='foo' api='bar' project='baz' />,
);
const button = shallow(
<TagResourcesButton tag='foo' api='bar' project='baz' />,
);
button.find('button').simulate('click', { preventDefault: () => {} });
expect(button.html()).toMatch('Hide the resource manager for this tag');
button.find('button').simulate('click', { preventDefault: () => {} });
expect(button.html()).toMatch('Hide the resource manager for this tag');
button.find('button').simulate('click', { preventDefault: () => {} });
expect(button.html()).toMatch('Manage resources for this tag');
button.find('button').simulate('click', { preventDefault: () => {} });
expect(button.html()).toMatch('Manage resources for this tag');
});

Просмотреть файл

@ -4,11 +4,11 @@ import ReactDOM from 'react-dom';
import { TagResourcesButton } from './button.js';
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.js-tag-resources').forEach((node) => {
const { api, project, tag } = node.dataset;
ReactDOM.render(
<TagResourcesButton api={api} project={project} tag={tag} />,
node,
);
});
document.querySelectorAll('.js-tag-resources').forEach((node) => {
const { api, project, tag } = node.dataset;
ReactDOM.render(
<TagResourcesButton api={api} project={project} tag={tag} />,
node,
);
});
});

Просмотреть файл

@ -8,39 +8,39 @@ import { ErrorList } from './widgets/error-list.js';
import './tag-resources.css';
export function TagResourceManager({ api }) {
const [data, setData] = useState([]);
const [errors, setErrors] = useState({});
const [type, setType] = useState('assoc');
const [search, setSearch] = useState('');
const [data, setData] = useState([]);
const [errors, setErrors] = useState({});
const [type, setType] = useState('assoc');
const [search, setSearch] = useState('');
const handleChange = useCallback(
async (params) => {
const response = await post(api, params);
const json = await response.json();
if (response.status === 200) {
setData(json.data || []);
setErrors({});
} else {
setErrors(json.errors || {});
}
},
[api],
);
const handleChange = useCallback(
async (params) => {
const response = await post(api, params);
const json = await response.json();
if (response.status === 200) {
setData(json.data || []);
setErrors({});
} else {
setErrors(json.errors || {});
}
},
[api],
);
useEffect(() => {
handleChange({ search, type });
}, [search, type]);
useEffect(() => {
handleChange({ search, type });
}, [search, type]);
const message = type === 'assoc' ? 'Unlink resources' : 'Link resources';
return (
<div className='tag-resource-widget'>
<TagResourceSearch onSearch={setSearch} onType={setType} />
<ErrorList errors={errors} />
<CheckboxTable
data={data}
onSubmit={({ data }) => handleChange({ data, search, type })}
submitMessage={message}
/>
</div>
);
const message = type === 'assoc' ? 'Unlink resources' : 'Link resources';
return (
<div className='tag-resource-widget'>
<TagResourceSearch onSearch={setSearch} onType={setType} />
<ErrorList errors={errors} />
<CheckboxTable
data={data}
onSubmit={({ data }) => handleChange({ data, search, type })}
submitMessage={message}
/>
</div>
);
}

Просмотреть файл

@ -8,79 +8,79 @@ import { TagResourceManager } from './manager.js';
const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0));
test('TagResourceManager search', async () => {
document.querySelector = jest.fn(() => ({ value: '73' }));
window.fetch = jest.fn(async () => ({
status: 200,
json: async () => ({ data: [] }),
}));
document.querySelector = jest.fn(() => ({ value: '73' }));
window.fetch = jest.fn(async () => ({
status: 200,
json: async () => ({ data: [] }),
}));
const manager = mount(<TagResourceManager api='x' />);
const manager = mount(<TagResourceManager api='x' />);
// Enter 'FOO' in the search input
manager
.find('input.search-tag-resources')
.simulate('change', { target: { value: 'FOO' } });
await act(async () => {
await flushPromises();
});
// Enter 'FOO' in the search input
manager
.find('input.search-tag-resources')
.simulate('change', { target: { value: 'FOO' } });
await act(async () => {
await flushPromises();
});
const { calls } = window.fetch.mock;
expect(calls).toHaveLength(2);
const { calls } = window.fetch.mock;
expect(calls).toHaveLength(2);
expect(Array.from(calls[0][1].body.entries())).toMatchObject([
['search', ''],
['type', 'assoc'],
['csrfmiddlewaretoken', '73'],
]);
expect(Array.from(calls[0][1].body.entries())).toMatchObject([
['search', ''],
['type', 'assoc'],
['csrfmiddlewaretoken', '73'],
]);
expect(Array.from(calls[1][1].body.entries())).toMatchObject([
['search', 'FOO'],
['type', 'assoc'],
['csrfmiddlewaretoken', '73'],
]);
expect(Array.from(calls[1][1].body.entries())).toMatchObject([
['search', 'FOO'],
['type', 'assoc'],
['csrfmiddlewaretoken', '73'],
]);
});
test('TagResourceManager checkboxing', async () => {
document.querySelector = jest.fn(() => ({ value: '73' }));
window.fetch = jest.fn(async () => ({
status: 200,
json: async () => ({ data: [['foo'], ['bar']] }),
}));
document.querySelector = jest.fn(() => ({ value: '73' }));
window.fetch = jest.fn(async () => ({
status: 200,
json: async () => ({ data: [['foo'], ['bar']] }),
}));
const manager = mount(<TagResourceManager api='y' />);
const manager = mount(<TagResourceManager api='y' />);
await act(async () => {
await flushPromises();
});
manager.update();
await act(async () => {
await flushPromises();
});
manager.update();
// Check the 'foo' checkbox
manager
.find('input[name="foo"]')
.simulate('change', { target: { name: 'foo', checked: true } });
// Check the 'foo' checkbox
manager
.find('input[name="foo"]')
.simulate('change', { target: { name: 'foo', checked: true } });
// Click on 'Unlink resources'
manager
.find('button.tag-resources-associate')
.simulate('click', { preventDefault: () => {} });
// Click on 'Unlink resources'
manager
.find('button.tag-resources-associate')
.simulate('click', { preventDefault: () => {} });
await act(async () => {
await flushPromises();
});
await act(async () => {
await flushPromises();
});
const { calls } = window.fetch.mock;
expect(calls).toHaveLength(2);
const { calls } = window.fetch.mock;
expect(calls).toHaveLength(2);
expect(Array.from(calls[0][1].body.entries())).toMatchObject([
['search', ''],
['type', 'assoc'],
['csrfmiddlewaretoken', '73'],
]);
expect(Array.from(calls[0][1].body.entries())).toMatchObject([
['search', ''],
['type', 'assoc'],
['csrfmiddlewaretoken', '73'],
]);
expect(Array.from(calls[1][1].body.entries())).toMatchObject([
['data', 'foo'],
['search', ''],
['type', 'assoc'],
['csrfmiddlewaretoken', '73'],
]);
expect(Array.from(calls[1][1].body.entries())).toMatchObject([
['data', 'foo'],
['search', ''],
['type', 'assoc'],
['csrfmiddlewaretoken', '73'],
]);
});

Просмотреть файл

@ -1,44 +1,44 @@
import React from 'react';
export const TagResourceSearch = ({ onSearch, onType }) => (
<div
className='container'
style={{ content: '', display: 'table', width: '100%' }}
>
<div
className='container'
style={{ content: '', display: 'table', width: '100%' }}
style={{
float: 'left',
boxSizing: 'border-box',
width: '60%',
}}
>
<div
style={{
float: 'left',
boxSizing: 'border-box',
width: '60%',
}}
>
<div className='column'>
<input
type='text'
className='search-tag-resources'
name='search'
onChange={(ev) => onSearch(ev.target.value)}
placeholder='Search for resources'
/>
</div>
</div>
<div
style={{
float: 'left',
boxSizing: 'border-box',
width: '40%',
}}
>
<div className='column'>
<select
className='search-tag-resource-type'
name='type'
onChange={(ev) => onType(ev.target.value)}
>
<option value='assoc'>Linked</option>
<option value='nonassoc'>Not linked</option>
</select>
</div>
</div>
<div className='column'>
<input
type='text'
className='search-tag-resources'
name='search'
onChange={(ev) => onSearch(ev.target.value)}
placeholder='Search for resources'
/>
</div>
</div>
<div
style={{
float: 'left',
boxSizing: 'border-box',
width: '40%',
}}
>
<div className='column'>
<select
className='search-tag-resource-type'
name='type'
onChange={(ev) => onType(ev.target.value)}
>
<option value='assoc'>Linked</option>
<option value='nonassoc'>Not linked</option>
</select>
</div>
</div>
</div>
);

Просмотреть файл

@ -4,32 +4,30 @@ import React from 'react';
import { TagResourceSearch } from './search.js';
test('TagResourceSearch renders search input', () => {
const search = mount(<TagResourceSearch />);
const input = search.find('input.search-tag-resources');
const search = mount(<TagResourceSearch />);
const input = search.find('input.search-tag-resources');
expect(input).toHaveLength(1);
expect(input.html()).toMatch('placeholder="Search for resources"');
expect(input).toHaveLength(1);
expect(input.html()).toMatch('placeholder="Search for resources"');
});
test('TagResourceSearch renders select', () => {
const search = mount(<TagResourceSearch />);
const options = search.find('select.search-tag-resource-type option');
const search = mount(<TagResourceSearch />);
const options = search.find('select.search-tag-resource-type option');
expect(options).toHaveLength(2);
expect(options.at(0).html()).toMatch('"assoc"');
expect(options.at(1).html()).toMatch('"nonassoc"');
expect(options).toHaveLength(2);
expect(options.at(0).html()).toMatch('"assoc"');
expect(options.at(1).html()).toMatch('"nonassoc"');
});
test('TagResourceSearch onChange', async () => {
const search = jest.fn();
const type = jest.fn();
const wrapper = mount(
<TagResourceSearch onSearch={search} onType={type} />,
);
const search = jest.fn();
const type = jest.fn();
const wrapper = mount(<TagResourceSearch onSearch={search} onType={type} />);
wrapper.find('input').simulate('change', { target: { value: 'FOO' } });
wrapper.find('select').simulate('change', { target: { value: 'BAR' } });
wrapper.find('input').simulate('change', { target: { value: 'FOO' } });
wrapper.find('select').simulate('change', { target: { value: 'BAR' } });
expect(search.mock.calls).toEqual([['FOO']]);
expect(type.mock.calls).toEqual([['BAR']]);
expect(search.mock.calls).toEqual([['FOO']]);
expect(type.mock.calls).toEqual([['BAR']]);
});

Просмотреть файл

@ -1,49 +1,49 @@
.tag-resources {
text-align: left;
text-align: left;
}
.tag-resources button {
margin: 0.2em auto 1em auto;
display: block;
margin: 0.2em auto 1em auto;
display: block;
}
.tag-resources .rt-table {
color: white;
color: white;
}
.tag-resource-widget {
background: #999;
margin-bottom: 2em;
padding-bottom: 0.3em;
background: #999;
margin-bottom: 2em;
padding-bottom: 0.3em;
}
.tag-resources .rt-table input {
margin: auto;
display: block;
margin: auto;
display: block;
}
.tag-resources .ReactTable .pagination-bottom .-pageInfo {
color: #333;
color: #333;
}
.tag-resources .rt-th {
text-align: left;
text-align: left;
}
button.tag-resources-associate {
margin-top: 0.5em;
margin-top: 0.5em;
}
.tag-resources select.search-tag-resource-type {
width: 90%;
display: block;
margin: 1em auto;
float: none;
width: 90%;
display: block;
margin: 1em auto;
float: none;
}
.tag-resources input.search-tag-resources {
width: 90%;
display: block;
margin: 1em auto;
float: none;
width: 90%;
display: block;
margin: 1em auto;
float: none;
}

Просмотреть файл

@ -1,45 +1,45 @@
function asFormData(data) {
/**
* Mangle a data object to FormData
*
* If any of the object values are arrayish, it will append k=v[n]
* for each item n of v
*/
const formData = new FormData();
if (data) {
for (const [k, v] of Object.entries(data)) {
if (Array.isArray(v)) {
for (const item of v) formData.append(k, item);
} else {
formData.append(k, v);
}
}
/**
* Mangle a data object to FormData
*
* If any of the object values are arrayish, it will append k=v[n]
* for each item n of v
*/
const formData = new FormData();
if (data) {
for (const [k, v] of Object.entries(data)) {
if (Array.isArray(v)) {
for (const item of v) formData.append(k, item);
} else {
formData.append(k, v);
}
}
return formData;
}
return formData;
}
function getLocation() {
let hostname = window.location.hostname;
const protocol = window.location.protocol;
const port = window.location.port;
if (port) {
hostname += ':' + port;
}
const origin = '//' + hostname;
const domain = protocol + origin;
return { origin, domain, port };
let hostname = window.location.hostname;
const protocol = window.location.protocol;
const port = window.location.port;
if (port) {
hostname += ':' + port;
}
const origin = '//' + hostname;
const domain = protocol + origin;
return { origin, domain, port };
}
function parseURL(url, port) {
const parser = document.createElement('a');
parser.href = url;
let parsedURL = parser.protocol + '//' + parser.hostname;
if (parser.port !== '') {
parsedURL += ':' + parser.port;
} else if (!/^(\/\/|http:|https:).*/.test(url) && port) {
parsedURL += ':' + port;
}
return parsedURL;
const parser = document.createElement('a');
parser.href = url;
let parsedURL = parser.protocol + '//' + parser.hostname;
if (parser.port !== '') {
parsedURL += ':' + parser.port;
} else if (!/^(\/\/|http:|https:).*/.test(url) && port) {
parsedURL += ':' + port;
}
return parsedURL;
}
/**
@ -49,13 +49,13 @@ function parseURL(url, port) {
* regared as same-origin
*/
export function isSameOrigin(url) {
const { origin, domain, port } = getLocation();
const parsedURL = parseURL(url, port);
return (
parsedURL === domain ||
parsedURL === origin ||
!/^(\/\/|http:|https:).*/.test(parsedURL)
);
const { origin, domain, port } = getLocation();
const parsedURL = parseURL(url, port);
return (
parsedURL === domain ||
parsedURL === origin ||
!/^(\/\/|http:|https:).*/.test(parsedURL)
);
}
/**
@ -66,24 +66,22 @@ export function isSameOrigin(url) {
* to prevent leaking of csrf data
*/
export function post(url, data) {
// this is a bit sketchy but the only afaict way due to session_csrf
const csrf = document.querySelector(
'input[name=csrfmiddlewaretoken]',
).value;
// this is a bit sketchy but the only afaict way due to session_csrf
const csrf = document.querySelector('input[name=csrfmiddlewaretoken]').value;
const init = {
body: asFormData(data),
headers: new Headers({
Accept: 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': csrf,
}),
method: 'POST',
};
if (isSameOrigin(url)) {
init.body.append('csrfmiddlewaretoken', csrf);
init.credentials = 'same-origin';
}
const init = {
body: asFormData(data),
headers: new Headers({
Accept: 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': csrf,
}),
method: 'POST',
};
if (isSameOrigin(url)) {
init.body.append('csrfmiddlewaretoken', csrf);
init.credentials = 'same-origin';
}
return window.fetch(url, init);
return window.fetch(url, init);
}

Просмотреть файл

@ -1,67 +1,67 @@
import { isSameOrigin, post } from './http-post.js';
test('isSameOrigin', () => {
// default url is set to https://nowhere.com/at/all
expect(isSameOrigin('https://somwhere.else/all/together')).toBe(false);
expect(isSameOrigin('https://somwhere.else/')).toBe(false);
expect(isSameOrigin('http://somwhere.else/')).toBe(false);
expect(isSameOrigin('//somwhere.else/')).toBe(false);
expect(isSameOrigin('http://nowhere.com/')).toBe(false);
expect(isSameOrigin('http://nowhere.com/like/this')).toBe(false);
expect(isSameOrigin('https://nowhere.org/and/this')).toBe(false);
expect(isSameOrigin('https://nowhere.org')).toBe(false);
// default url is set to https://nowhere.com/at/all
expect(isSameOrigin('https://somwhere.else/all/together')).toBe(false);
expect(isSameOrigin('https://somwhere.else/')).toBe(false);
expect(isSameOrigin('http://somwhere.else/')).toBe(false);
expect(isSameOrigin('//somwhere.else/')).toBe(false);
expect(isSameOrigin('http://nowhere.com/')).toBe(false);
expect(isSameOrigin('http://nowhere.com/like/this')).toBe(false);
expect(isSameOrigin('https://nowhere.org/and/this')).toBe(false);
expect(isSameOrigin('https://nowhere.org')).toBe(false);
expect(isSameOrigin('https://nowhere.com/but/this')).toBe(true);
expect(isSameOrigin('https://nowhere.com/and/this')).toBe(true);
expect(isSameOrigin('https://nowhere.com/')).toBe(true);
expect(isSameOrigin('https://nowhere.com')).toBe(true);
expect(isSameOrigin('//nowhere.com/or/this')).toBe(true);
expect(isSameOrigin('/else/this')).toBe(true);
expect(isSameOrigin('even/this')).toBe(true);
expect(isSameOrigin('https://nowhere.com/but/this')).toBe(true);
expect(isSameOrigin('https://nowhere.com/and/this')).toBe(true);
expect(isSameOrigin('https://nowhere.com/')).toBe(true);
expect(isSameOrigin('https://nowhere.com')).toBe(true);
expect(isSameOrigin('//nowhere.com/or/this')).toBe(true);
expect(isSameOrigin('/else/this')).toBe(true);
expect(isSameOrigin('even/this')).toBe(true);
Object.defineProperty(window, 'location', {
writable: true,
value: {
href: 'https://nowhere.com:2323/some/where',
protocol: 'https:',
hostname: 'nowhere.com',
host: 'nowhere.com:2323',
port: '2323',
},
});
Object.defineProperty(window, 'location', {
writable: true,
value: {
href: 'https://nowhere.com:2323/some/where',
protocol: 'https:',
hostname: 'nowhere.com',
host: 'nowhere.com:2323',
port: '2323',
},
});
expect(isSameOrigin('https://nowhere.com/but/this')).toBe(false);
expect(isSameOrigin('https://nowhere.com/and/this')).toBe(false);
expect(isSameOrigin('https://nowhere.com/')).toBe(false);
expect(isSameOrigin('https://nowhere.com')).toBe(false);
expect(isSameOrigin('//nowhere.com/or/this')).toBe(false);
expect(isSameOrigin('https://nowhere.com:2323')).toBe(true);
expect(isSameOrigin('//nowhere.com:2323/or/this')).toBe(true);
expect(isSameOrigin('/still/this')).toBe(true);
expect(isSameOrigin('and/even/this')).toBe(true);
expect(isSameOrigin('https://nowhere.com/but/this')).toBe(false);
expect(isSameOrigin('https://nowhere.com/and/this')).toBe(false);
expect(isSameOrigin('https://nowhere.com/')).toBe(false);
expect(isSameOrigin('https://nowhere.com')).toBe(false);
expect(isSameOrigin('//nowhere.com/or/this')).toBe(false);
expect(isSameOrigin('https://nowhere.com:2323')).toBe(true);
expect(isSameOrigin('//nowhere.com:2323/or/this')).toBe(true);
expect(isSameOrigin('/still/this')).toBe(true);
expect(isSameOrigin('and/even/this')).toBe(true);
});
test('http post', () => {
window.fetch = jest.fn(() => 73);
document.querySelector = jest.fn(() => ({ value: '37' }));
expect(post('foo', { bar: 11 })).toBe(73);
window.fetch = jest.fn(() => 73);
document.querySelector = jest.fn(() => ({ value: '37' }));
expect(post('foo', { bar: 11 })).toBe(73);
const { calls } = window.fetch.mock;
expect(calls).toMatchObject([
['foo', { credentials: 'same-origin', method: 'POST' }],
]);
expect(document.querySelector.mock.calls).toEqual([
['input[name=csrfmiddlewaretoken]'],
]);
const { calls } = window.fetch.mock;
expect(calls).toMatchObject([
['foo', { credentials: 'same-origin', method: 'POST' }],
]);
expect(document.querySelector.mock.calls).toEqual([
['input[name=csrfmiddlewaretoken]'],
]);
const { body, headers } = calls[0][1];
expect(Array.from(body.entries())).toMatchObject([
['bar', '11'],
['csrfmiddlewaretoken', '37'],
]);
expect(Array.from(headers.entries())).toMatchObject([
['accept', 'application/json'],
['x-csrftoken', '37'],
['x-requested-with', 'XMLHttpRequest'],
]);
const { body, headers } = calls[0][1];
expect(Array.from(body.entries())).toMatchObject([
['bar', '11'],
['csrfmiddlewaretoken', '37'],
]);
expect(Array.from(headers.entries())).toMatchObject([
['accept', 'application/json'],
['x-csrftoken', '37'],
['x-requested-with', 'XMLHttpRequest'],
]);
});

Просмотреть файл

@ -7,103 +7,98 @@ import { Checkbox } from './checkbox.js';
// Returns a copy of the `checked` set with only resource paths that are in `visible`
const prune = (checked, visible) =>
new Set([...checked].filter((v) => visible.includes(v)));
new Set([...checked].filter((v) => visible.includes(v)));
export function CheckboxTable({ data, onSubmit, submitMessage }) {
const visible = useRef([]);
const [checked, setChecked] = useState(new Set());
const clearChecked = () => setChecked(new Set());
const pruneChecked = () =>
setChecked((checked) => prune(checked, visible.current));
const visible = useRef([]);
const [checked, setChecked] = useState(new Set());
const clearChecked = () => setChecked(new Set());
const pruneChecked = () =>
setChecked((checked) => prune(checked, visible.current));
useEffect(() => {
visible.current.length = 0;
clearChecked();
}, [data]);
useEffect(() => {
visible.current.length = 0;
clearChecked();
}, [data]);
const selectAll = useCallback(() => {
setChecked((checked) => {
if (checked.size > 0) return new Set();
else return new Set([...visible.current.filter(Boolean)]);
});
}, []);
const selectAll = useCallback(() => {
setChecked((checked) => {
if (checked.size > 0) return new Set();
else return new Set([...visible.current.filter(Boolean)]);
});
}, []);
const selectOne = useCallback(({ target }) => {
setChecked((checked) => {
const next = new Set(checked);
if (target.checked) next.add(target.name);
else next.delete(target.name);
return next;
});
}, []);
const selectOne = useCallback(({ target }) => {
setChecked((checked) => {
const next = new Set(checked);
if (target.checked) next.add(target.name);
else next.delete(target.name);
return next;
});
}, []);
const Header = () => {
const pruned = prune(checked, visible.current);
// some rows can be empty strings if there are more visible rows than resources
const some = pruned.size > 0;
const all =
some && pruned.size === visible.current.filter(Boolean).length;
return (
<Checkbox
checked={all}
indeterminate={some && !all}
onChange={selectAll}
/>
);
};
const Cell = (item) => {
const name = item.original[0];
visible.current.length = item.pageSize;
visible.current[item.viewIndex] = name;
return (
<Checkbox
checked={checked.has(name)}
name={name}
onChange={selectOne}
/>
);
};
const columns = [
{ Header, Cell, sortable: false, width: 45 },
{
Header: 'Resource',
id: 'type',
Cell: (item) => <span>{item.original[0]}</span>,
},
];
const handleSubmit = async (evt) => {
// after emitting handleSubmit to parent with list of currently
// checked, clears the checkboxes
evt.preventDefault();
await onSubmit({ data: [...checked] });
clearChecked();
};
const Header = () => {
const pruned = prune(checked, visible.current);
// some rows can be empty strings if there are more visible rows than resources
const some = pruned.size > 0;
const all = some && pruned.size === visible.current.filter(Boolean).length;
return (
<div>
<ReactTable
defaultPageSize={5}
className='-striped -highlight'
data={data}
onPageChange={() => {
visible.current.length = 0;
clearChecked();
}}
onPageSizeChange={(pageSize) => {
visible.current.length = pageSize;
pruneChecked();
}}
onSortedChange={pruneChecked}
columns={columns}
/>
<button className='tag-resources-associate' onClick={handleSubmit}>
{submitMessage}
</button>
</div>
<Checkbox
checked={all}
indeterminate={some && !all}
onChange={selectAll}
/>
);
};
const Cell = (item) => {
const name = item.original[0];
visible.current.length = item.pageSize;
visible.current[item.viewIndex] = name;
return (
<Checkbox checked={checked.has(name)} name={name} onChange={selectOne} />
);
};
const columns = [
{ Header, Cell, sortable: false, width: 45 },
{
Header: 'Resource',
id: 'type',
Cell: (item) => <span>{item.original[0]}</span>,
},
];
const handleSubmit = async (evt) => {
// after emitting handleSubmit to parent with list of currently
// checked, clears the checkboxes
evt.preventDefault();
await onSubmit({ data: [...checked] });
clearChecked();
};
return (
<div>
<ReactTable
defaultPageSize={5}
className='-striped -highlight'
data={data}
onPageChange={() => {
visible.current.length = 0;
clearChecked();
}}
onPageSizeChange={(pageSize) => {
visible.current.length = pageSize;
pruneChecked();
}}
onSortedChange={pruneChecked}
columns={columns}
/>
<button className='tag-resources-associate' onClick={handleSubmit}>
{submitMessage}
</button>
</div>
);
}

Просмотреть файл

@ -5,200 +5,194 @@ import { act } from 'react-dom/test-utils';
import { CheckboxTable } from './checkbox-table.js';
test('CheckboxTable render', () => {
const table = shallow(<CheckboxTable />);
expect(table.text()).toBe('<ReactTable />');
const table = shallow(<CheckboxTable />);
expect(table.text()).toBe('<ReactTable />');
});
test('CheckboxTable render checkboxes', () => {
const table = mount(<CheckboxTable data={[]} />);
expect(table.find('input[type="checkbox"]')).toHaveLength(1);
const table = mount(<CheckboxTable data={[]} />);
expect(table.find('input[type="checkbox"]')).toHaveLength(1);
table.setProps({ data: [['foo'], ['bar']] });
table.update();
expect(table.find('input[type="checkbox"]')).toHaveLength(3);
table.setProps({ data: [['foo'], ['bar']] });
table.update();
expect(table.find('input[type="checkbox"]')).toHaveLength(3);
expect(table.find('input[name="foo"]').getDOMNode().checked).toBe(false);
expect(table.find('input[name="bar"]').getDOMNode().checked).toBe(false);
expect(table.find('input[name="foo"]').getDOMNode().checked).toBe(false);
expect(table.find('input[name="bar"]').getDOMNode().checked).toBe(false);
});
test('CheckboxTable select checkboxes', () => {
const table = mount(<CheckboxTable data={[['foo'], ['bar']]} />);
const inputs = table.find('input[type="checkbox"]');
expect(inputs).toHaveLength(3);
let checkbox = inputs.at(0).getDOMNode();
const table = mount(<CheckboxTable data={[['foo'], ['bar']]} />);
const inputs = table.find('input[type="checkbox"]');
expect(inputs).toHaveLength(3);
let checkbox = inputs.at(0).getDOMNode();
// Start with no selections
expect(checkbox.checked).toBe(false);
expect(checkbox.indeterminate).toBe(false);
// Start with no selections
expect(checkbox.checked).toBe(false);
expect(checkbox.indeterminate).toBe(false);
// Select 'foo'
table
.find('input[name="foo"]')
.simulate('change', { target: { name: 'foo', checked: true } });
checkbox = table.find('input[type="checkbox"]').at(0).getDOMNode();
expect(checkbox.checked).toBe(false);
expect(checkbox.indeterminate).toBe(true);
// Select 'foo'
table
.find('input[name="foo"]')
.simulate('change', { target: { name: 'foo', checked: true } });
checkbox = table.find('input[type="checkbox"]').at(0).getDOMNode();
expect(checkbox.checked).toBe(false);
expect(checkbox.indeterminate).toBe(true);
// Select also 'bar'
table
.find('input[name="bar"]')
.simulate('change', { target: { name: 'bar', checked: true } });
checkbox = table.find('input[type="checkbox"]').at(0).getDOMNode();
expect(checkbox.checked).toBe(true);
expect(checkbox.indeterminate).toBe(false);
// Select also 'bar'
table
.find('input[name="bar"]')
.simulate('change', { target: { name: 'bar', checked: true } });
checkbox = table.find('input[type="checkbox"]').at(0).getDOMNode();
expect(checkbox.checked).toBe(true);
expect(checkbox.indeterminate).toBe(false);
// Unselect all
inputs.at(0).simulate('change', {});
expect(table.find('input[name="foo"]').getDOMNode().checked).toBe(false);
expect(table.find('input[name="bar"]').getDOMNode().checked).toBe(false);
// Unselect all
inputs.at(0).simulate('change', {});
expect(table.find('input[name="foo"]').getDOMNode().checked).toBe(false);
expect(table.find('input[name="bar"]').getDOMNode().checked).toBe(false);
// Select all
inputs.at(0).simulate('change', {});
expect(table.find('input[name="foo"]').getDOMNode().checked).toBe(true);
expect(table.find('input[name="bar"]').getDOMNode().checked).toBe(true);
// Select all
inputs.at(0).simulate('change', {});
expect(table.find('input[name="foo"]').getDOMNode().checked).toBe(true);
expect(table.find('input[name="bar"]').getDOMNode().checked).toBe(true);
// Unselect 'foo'
table
.find('input[name="foo"]')
.simulate('change', { target: { name: 'foo', checked: false } });
checkbox = table.find('input[type="checkbox"]').at(0).getDOMNode();
expect(checkbox.checked).toBe(false);
expect(checkbox.indeterminate).toBe(true);
// Unselect 'foo'
table
.find('input[name="foo"]')
.simulate('change', { target: { name: 'foo', checked: false } });
checkbox = table.find('input[type="checkbox"]').at(0).getDOMNode();
expect(checkbox.checked).toBe(false);
expect(checkbox.indeterminate).toBe(true);
});
test('CheckboxTable sort change', () => {
// Tests what happens when the table sort is changed.
// The expectation is that any items in state.checked are
// removed if they are no longer visible
// Tests what happens when the table sort is changed.
// The expectation is that any items in state.checked are
// removed if they are no longer visible
const table = mount(
<CheckboxTable
data={[['1'], ['2'], ['3'], ['4'], ['5'], ['6'], ['7']]}
/>,
);
const table = mount(
<CheckboxTable data={[['1'], ['2'], ['3'], ['4'], ['5'], ['6'], ['7']]} />,
);
// Select '2' and '3'
table
.find('input[name="2"]')
.simulate('change', { target: { name: '2', checked: true } });
table
.find('input[name="3"]')
.simulate('change', { target: { name: '3', checked: true } });
// Select '2' and '3'
table
.find('input[name="2"]')
.simulate('change', { target: { name: '2', checked: true } });
table
.find('input[name="3"]')
.simulate('change', { target: { name: '3', checked: true } });
// Click twice to use descending sort, which sets checkboxes 7..3 visible
const th = table.find('.rt-th.-cursor-pointer');
th.simulate('click', {});
th.simulate('click', {});
// Click twice to use descending sort, which sets checkboxes 7..3 visible
const th = table.find('.rt-th.-cursor-pointer');
th.simulate('click', {});
th.simulate('click', {});
expect(table.find('input[name="2"]')).toHaveLength(0);
expect(table.find('input[name="3"]').getDOMNode().checked).toBe(true);
expect(table.find('input[name="2"]')).toHaveLength(0);
expect(table.find('input[name="3"]').getDOMNode().checked).toBe(true);
// Switch back to ascending sort
th.simulate('click', {});
// Switch back to ascending sort
th.simulate('click', {});
expect(table.find('input[name="2"]').getDOMNode().checked).toBe(false);
expect(table.find('input[name="3"]').getDOMNode().checked).toBe(true);
expect(table.find('input[name="2"]').getDOMNode().checked).toBe(false);
expect(table.find('input[name="3"]').getDOMNode().checked).toBe(true);
});
test('CheckboxTable page change', () => {
const table = mount(
<CheckboxTable
data={[['1'], ['2'], ['3'], ['4'], ['5'], ['6'], ['7']]}
/>,
);
const table = mount(
<CheckboxTable data={[['1'], ['2'], ['3'], ['4'], ['5'], ['6'], ['7']]} />,
);
expect(table.find('input[name="2"]')).toHaveLength(1);
expect(table.find('input[name="6"]')).toHaveLength(0);
expect(table.find('input[name="2"]')).toHaveLength(1);
expect(table.find('input[name="6"]')).toHaveLength(0);
// Click 'Next' button
const buttons = table.find('button.-btn');
expect(buttons).toHaveLength(2);
buttons.at(1).simulate('click', {});
// Click 'Next' button
const buttons = table.find('button.-btn');
expect(buttons).toHaveLength(2);
buttons.at(1).simulate('click', {});
expect(table.find('input[name="2"]')).toHaveLength(0);
expect(table.find('input[name="6"]')).toHaveLength(1);
expect(table.find('input[name="2"]')).toHaveLength(0);
expect(table.find('input[name="6"]')).toHaveLength(1);
});
test('CheckboxTable resize', () => {
const table = mount(
<CheckboxTable
data={[['1'], ['2'], ['3'], ['4'], ['5'], ['6'], ['7']]}
/>,
);
const table = mount(
<CheckboxTable data={[['1'], ['2'], ['3'], ['4'], ['5'], ['6'], ['7']]} />,
);
// Resize to 10 rows
const select = table.find('.-pageSizeOptions select');
select.simulate('change', { target: { value: '10' } });
// Resize to 10 rows
const select = table.find('.-pageSizeOptions select');
select.simulate('change', { target: { value: '10' } });
// Select '2' and '6'
table
.find('input[name="2"]')
.simulate('change', { target: { name: '2', checked: true } });
table
.find('input[name="6"]')
.simulate('change', { target: { name: '6', checked: true } });
// Select '2' and '6'
table
.find('input[name="2"]')
.simulate('change', { target: { name: '2', checked: true } });
table
.find('input[name="6"]')
.simulate('change', { target: { name: '6', checked: true } });
// Resize to 5 rows
select.simulate('change', { target: { value: '5' } });
// Resize to 5 rows
select.simulate('change', { target: { value: '5' } });
expect(table.find('input[name="2"]').getDOMNode().checked).toBe(true);
expect(table.find('input[name="6"]')).toHaveLength(0);
expect(table.find('input[name="2"]').getDOMNode().checked).toBe(true);
expect(table.find('input[name="6"]')).toHaveLength(0);
// Resize to 10 rows
select.simulate('change', { target: { value: '10' } });
// Resize to 10 rows
select.simulate('change', { target: { value: '10' } });
expect(table.find('input[name="2"]').getDOMNode().checked).toBe(true);
expect(table.find('input[name="6"]').getDOMNode().checked).toBe(false);
expect(table.find('input[name="2"]').getDOMNode().checked).toBe(true);
expect(table.find('input[name="6"]').getDOMNode().checked).toBe(false);
});
test('CheckboxTable submit', async () => {
const spy = jest.fn();
const table = mount(
<CheckboxTable
data={[['1'], ['2'], ['3'], ['4'], ['5'], ['6'], ['7']]}
onSubmit={spy}
/>,
);
const spy = jest.fn();
const table = mount(
<CheckboxTable
data={[['1'], ['2'], ['3'], ['4'], ['5'], ['6'], ['7']]}
onSubmit={spy}
/>,
);
// Select '2' and '3'
table
.find('input[name="2"]')
.simulate('change', { target: { name: '2', checked: true } });
table
.find('input[name="3"]')
.simulate('change', { target: { name: '3', checked: true } });
// Select '2' and '3'
table
.find('input[name="2"]')
.simulate('change', { target: { name: '2', checked: true } });
table
.find('input[name="3"]')
.simulate('change', { target: { name: '3', checked: true } });
// Submit changes
table
.find('button.tag-resources-associate')
.simulate('click', { preventDefault: () => {} });
await act(() => new Promise((resolve) => setTimeout(resolve, 0)));
table.update();
// Submit changes
table
.find('button.tag-resources-associate')
.simulate('click', { preventDefault: () => {} });
await act(() => new Promise((resolve) => setTimeout(resolve, 0)));
table.update();
expect(spy.mock.calls).toEqual([[{ data: ['2', '3'] }]]);
expect(table.find('input[name="2"]').getDOMNode().checked).toBe(false);
expect(table.find('input[name="3"]').getDOMNode().checked).toBe(false);
expect(spy.mock.calls).toEqual([[{ data: ['2', '3'] }]]);
expect(table.find('input[name="2"]').getDOMNode().checked).toBe(false);
expect(table.find('input[name="3"]').getDOMNode().checked).toBe(false);
});
test('CheckboxTable props change', () => {
const data = [['1'], ['2'], ['3'], ['4'], ['5'], ['6'], ['7']];
const table = mount(<CheckboxTable data={data} />);
const data = [['1'], ['2'], ['3'], ['4'], ['5'], ['6'], ['7']];
const table = mount(<CheckboxTable data={data} />);
// Select '2' and '3'
table
.find('input[name="2"]')
.simulate('change', { target: { name: '2', checked: true } });
table
.find('input[name="3"]')
.simulate('change', { target: { name: '3', checked: true } });
// Select '2' and '3'
table
.find('input[name="2"]')
.simulate('change', { target: { name: '2', checked: true } });
table
.find('input[name="3"]')
.simulate('change', { target: { name: '3', checked: true } });
expect(table.find('input[name="2"]').getDOMNode().checked).toBe(true);
expect(table.find('input[name="3"]').getDOMNode().checked).toBe(true);
expect(table.find('input[name="2"]').getDOMNode().checked).toBe(true);
expect(table.find('input[name="3"]').getDOMNode().checked).toBe(true);
// Re-setting props clears checkboxes
table.setProps({ data: [...data] });
table.update();
// Re-setting props clears checkboxes
table.setProps({ data: [...data] });
table.update();
expect(table.find('input[name="2"]').getDOMNode().checked).toBe(false);
expect(table.find('input[name="3"]').getDOMNode().checked).toBe(false);
expect(table.find('input[name="2"]').getDOMNode().checked).toBe(false);
expect(table.find('input[name="3"]').getDOMNode().checked).toBe(false);
});

Просмотреть файл

@ -2,11 +2,11 @@ import React, { useEffect, useRef } from 'react';
/** A checkbox which you can set `indeterminate` on */
export function Checkbox({ indeterminate, ...props }) {
const ref = useRef();
const ref = useRef();
useEffect(() => {
if (ref.current) ref.current.indeterminate = !!indeterminate;
}, [indeterminate]);
useEffect(() => {
if (ref.current) ref.current.indeterminate = !!indeterminate;
}, [indeterminate]);
return <input {...props} type='checkbox' ref={ref} />;
return <input {...props} type='checkbox' ref={ref} />;
}

Просмотреть файл

@ -4,19 +4,19 @@ import React from 'react';
import { Checkbox } from './checkbox.js';
test('Checkbox render', () => {
const checkbox = mount(<Checkbox id='x' />);
expect(checkbox.find('input[type="checkbox"]#x')).toHaveLength(1);
const checkbox = mount(<Checkbox id='x' />);
expect(checkbox.find('input[type="checkbox"]#x')).toHaveLength(1);
});
test('Checkbox indeterminate', () => {
const checkbox = mount(<Checkbox indeterminate />);
expect(checkbox.getDOMNode().indeterminate).toBe(true);
const checkbox = mount(<Checkbox indeterminate />);
expect(checkbox.getDOMNode().indeterminate).toBe(true);
});
test('Checkbox switch', () => {
const checkbox = mount(<Checkbox indeterminate={0} />);
expect(checkbox.getDOMNode().indeterminate).toBe(false);
const checkbox = mount(<Checkbox indeterminate={0} />);
expect(checkbox.getDOMNode().indeterminate).toBe(false);
checkbox.setProps({ indeterminate: 1 });
expect(checkbox.getDOMNode().indeterminate).toBe(true);
checkbox.setProps({ indeterminate: 1 });
expect(checkbox.getDOMNode().indeterminate).toBe(true);
});

Просмотреть файл

@ -1,10 +1,10 @@
ul.errors {
padding: 0.3em;
margin: 0;
list-style: none;
background: #ccc;
padding: 0.3em;
margin: 0;
list-style: none;
background: #ccc;
}
ul.errors li.error {
color: #f36;
color: #f36;
}

Просмотреть файл

@ -3,14 +3,14 @@ import React from 'react';
import './error-list.css';
export function ErrorList({ errors }) {
const entries = Object.entries(errors);
return entries.length === 0 ? null : (
<ul className='errors'>
{entries.map(([name, error], index) => (
<li className='error' key={index}>
{name}: {error}
</li>
))}
</ul>
);
const entries = Object.entries(errors);
return entries.length === 0 ? null : (
<ul className='errors'>
{entries.map(([name, error], index) => (
<li className='error' key={index}>
{name}: {error}
</li>
))}
</ul>
);
}

Просмотреть файл

@ -4,14 +4,14 @@ import React from 'react';
import { ErrorList } from './error-list.js';
test('ErrorList render', () => {
const errors = mount(<ErrorList errors={{}} />);
expect(errors.text()).toBe('');
expect(errors.find('ul').length).toBe(0);
const errors = mount(<ErrorList errors={{}} />);
expect(errors.text()).toBe('');
expect(errors.find('ul').length).toBe(0);
errors.setProps({ errors: { foo: 'Did a foo', bar: 'Bars happen' } });
expect(errors.find('ul.errors')).toHaveLength(1);
expect(errors.find('li.error')).toHaveLength(2);
expect(errors.find('li.error').at(1).html()).toBe(
'<li class="error">bar: Bars happen</li>',
);
errors.setProps({ errors: { foo: 'Did a foo', bar: 'Bars happen' } });
expect(errors.find('ul.errors')).toHaveLength(1);
expect(errors.find('li.error')).toHaveLength(2);
expect(errors.find('li.error').at(1).html()).toBe(
'<li class="error">bar: Bars happen</li>',
);
});

Просмотреть файл

@ -1,22 +1,23 @@
/* global module */
/* eslint-env node */
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
env: {
browser: true,
jest: true,
},
rules: {
'prefer-const': 0,
'no-var': 0,
'@typescript-eslint/ban-ts-comment': 0,
'@typescript-eslint/ban-types': 0,
'@typescript-eslint/explicit-module-boundary-types': 0,
'@typescript-eslint/no-empty-function': 0,
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/no-inferrable-types': 0,
'@typescript-eslint/prefer-as-const': 0,
},
root: true,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
env: {
browser: true,
jest: true,
},
rules: {
'prefer-const': 0,
'no-var': 0,
'@typescript-eslint/ban-ts-comment': 0,
'@typescript-eslint/ban-types': 0,
'@typescript-eslint/explicit-module-boundary-types': 0,
'@typescript-eslint/no-empty-function': 0,
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/no-inferrable-types': 0,
'@typescript-eslint/prefer-as-const': 0,
},
};

Просмотреть файл

@ -45,7 +45,6 @@
</tr>
</table>
## Code architecture
### Where code goes
@ -74,6 +73,7 @@ Of course, more can be added if needed. For example, modules with a high number
To import code from further away than the parent directory,
use paths starting with `~` to refer to the root of the `src/` directory.
For example:
```js
import { SearchBox } from '~/modules/search';
```
@ -111,7 +111,6 @@ As far as we know, it is not possible to make that work in Chrome or Edge. This
If you can't turn on websockets, you will see errors in the console (that's not very impacting) and you'll have to reload your Django server regularly, because polling requests don't close, and after so many web page reloads, the Django process won't be able to accept new requests.
## Dependencies
We manage our JavaScript dependencies with `npm`.
@ -134,7 +133,6 @@ You might want to remove the `translate/node_modules` folder after you've run th
(and the `package.json` and `package-lock.json` files have been updated)
and before rebuilding the image, to reduce the size of the docker context.
## Type checking
Our code uses TypeScript for type-checking the production code. Tests are not type-checked in general, which allows for smaller test fixtures. Visit the [TypeScript documentation](https://www.typescriptlang.org/docs) to learn more about TypeScript.
@ -143,7 +141,6 @@ To check for TypeScript errors locally, run:
$ make types
## Testing
Tests are run using [`jest`](https://facebook.github.io/jest/). We use [`enzyme`](http://airbnb.io/enzyme/docs/api/) for mounting React components and [`sinon`](http://sinonjs.org/) for mocking.
@ -158,7 +155,7 @@ Tests are put in files called `fileToTest.test.js` in the same directory as the
```javascript
describe('<Component>', () => {
// test suite here
// test suite here
});
```
@ -166,13 +163,12 @@ Individual tests follow `mocha`'s descriptive syntax. Try to be as explicit as p
```javascript
it('does something specific', () => {
// unit test here
// unit test here
});
```
We use `jest`'s [`expect`](https://facebook.github.io/jest/docs/en/expect.html) assertion tool.
## Localization
The user interface is localized using [Fluent](https://projectfluent.org/) and the library `fluent-react`. Fluent allows to move the complexity of handling translated content from the developer to the translator. Thus, when using it, you should care only about the English version, and trust that the localizers will know what to do with your string in their language.
@ -185,13 +181,15 @@ That would give:
```js
class Editor extends React.Component {
render() {
return <div>
<Localized id="entitydetails-Editor--button-update">
<button>Update</button>
</Localized>
</div>;
}
render() {
return (
<div>
<Localized id='entitydetails-Editor--button-update'>
<button>Update</button>
</Localized>
</div>
);
}
}
```
@ -206,7 +204,7 @@ Those files use the FTL format. In its simplest form, a string in such a file (c
### Semantic identifiers
Fluent uses the concept of a *social contract* between developer and localizers. This contract is established by the selection of a unique identifier, called `l10n-id`, which carries a promise of being used in a particular place to carry a particular meaning.
Fluent uses the concept of a _social contract_ between developer and localizers. This contract is established by the selection of a unique identifier, called `l10n-id`, which carries a promise of being used in a particular place to carry a particular meaning.
You should consider the `l10n-id` as a variable name. If the meaning of the content changes, then you should also change the ID. This will notify localizers that the content is different from before and that a new translation is needed. However, if you make minor changes (fix a typo, make a change that keeps the same meaning) you should instead keep the same ID.
@ -220,11 +218,10 @@ In Fluent, the developer is not to be bothered with inner logic and complexity t
In order to easily verify that a string is effectively localized, you can turn on pseudo-localization. To do that, add `pseudolocalization=accented` or `pseudolocalization=bidi` to the URL, then refresh the page.
Pseudo-localization turns every supported string into a different version of itself. We support two modes: "accented" (transforms "Accented English" into "Ȧȧƈƈḗḗƞŧḗḗḓ Ḗḗƞɠŀīīşħ") and "bidi" (transforms "Reversed English" into "ᴚǝʌǝɹsǝp Ǝuƃʅı"). Because only strings that are actually localized (they exist in our reference en-US FTL file and they are properly set in a `<Localized>` component) get that transformation, it is easy to spot which strings are *not* properly localized in the interface.
Pseudo-localization turns every supported string into a different version of itself. We support two modes: "accented" (transforms "Accented English" into "Ȧȧƈƈḗḗƞŧḗḗḓ Ḗḗƞɠŀīīşħ") and "bidi" (transforms "Reversed English" into "ᴚǝʌǝɹsǝp Ǝuƃʅı"). Because only strings that are actually localized (they exist in our reference en-US FTL file and they are properly set in a `<Localized>` component) get that transformation, it is easy to spot which strings are _not_ properly localized in the interface.
You can read [more about pseudo-localization on Wikipedia](https://en.wikipedia.org/wiki/Pseudolocalization).
## Development resources
### Integration between Django and React

Просмотреть файл

@ -2,39 +2,39 @@
/** @type {import('@jest/types').Config.InitialOptions} */
module.exports = {
verbose: true,
roots: ['<rootDir>/src'],
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'],
coveragePathIgnorePatterns: ['<rootDir>/node_modules/'],
testMatch: [
'<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
'<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}',
],
testEnvironment: 'jsdom',
preset: 'ts-jest',
globals: {
'ts-jest': {
isolatedModules: true,
},
verbose: true,
roots: ['<rootDir>/src'],
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'],
coveragePathIgnorePatterns: ['<rootDir>/node_modules/'],
testMatch: [
'<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
'<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}',
],
testEnvironment: 'jsdom',
preset: 'ts-jest',
globals: {
'ts-jest': {
isolatedModules: true,
},
transform: {
'\\.jsx?$': ['babel-jest', { configFile: '../babel.config.json' }],
'\\.tsx?$': 'ts-jest',
},
transformIgnorePatterns: [
'[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$',
],
resetMocks: true,
moduleNameMapper: {
'.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$':
'identity-obj-proxy',
'\\.svg$': '<rootDir>/__mocks__/svg.js',
'~(.*)$': '<rootDir>/src/$1',
},
watchPlugins: [
'jest-watch-typeahead/filename',
'jest-watch-typeahead/testname',
],
testTimeout: 10000, // optional
},
transform: {
'\\.jsx?$': ['babel-jest', { configFile: '../babel.config.json' }],
'\\.tsx?$': 'ts-jest',
},
transformIgnorePatterns: [
'[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$',
],
resetMocks: true,
moduleNameMapper: {
'.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$':
'identity-obj-proxy',
'\\.svg$': '<rootDir>/__mocks__/svg.js',
'~(.*)$': '<rootDir>/src/$1',
},
watchPlugins: [
'jest-watch-typeahead/filename',
'jest-watch-typeahead/testname',
],
testTimeout: 10000, // optional
};

Просмотреть файл

@ -47,9 +47,6 @@
"start": "rollup -c --watch",
"build": "rollup -c",
"build:prod": "rollup -c --environment BUILD:production",
"lint": "eslint 'src/**/*.{js,ts,tsx}'",
"prettier": "prettier --write src/",
"check-prettier": "prettier --check src/",
"test": "jest",
"types": "tsc --noEmit"
}

Просмотреть файл

@ -9,38 +9,38 @@ import css from 'rollup-plugin-css-only';
/** @type {import('rollup').RollupOptions} */
const config = {
input: 'src/index.tsx',
output: { file: 'dist/translate.js' },
input: 'src/index.tsx',
output: { file: 'dist/translate.js' },
treeshake: 'recommended',
treeshake: 'recommended',
plugins: [
json(),
typescript(),
replace({
preventAssignment: true,
'process.env.NODE_ENV': JSON.stringify(
process.env.BUILD ?? 'development',
),
}),
resolve(),
commonjs(),
css({ output: 'translate.css' }),
],
plugins: [
json(),
typescript(),
replace({
preventAssignment: true,
'process.env.NODE_ENV': JSON.stringify(
process.env.BUILD ?? 'development',
),
}),
resolve(),
commonjs(),
css({ output: 'translate.css' }),
],
onwarn(warning, warn) {
// https://github.com/reduxjs/redux-toolkit/issues/1466
if (warning.id?.includes('@reduxjs/toolkit')) {
switch (warning.code) {
case 'SOURCEMAP_ERROR':
return;
case 'THIS_IS_UNDEFINED':
if (warning.frame?.includes('this && this')) return;
}
}
onwarn(warning, warn) {
// https://github.com/reduxjs/redux-toolkit/issues/1466
if (warning.id?.includes('@reduxjs/toolkit')) {
switch (warning.code) {
case 'SOURCEMAP_ERROR':
return;
case 'THIS_IS_UNDEFINED':
if (warning.frame?.includes('this && this')) return;
}
}
warn(warning);
},
warn(warning);
},
};
export default config;

Просмотреть файл

@ -1,64 +1,64 @@
#app {
display: flex;
flex-direction: column;
flex-flow: column;
height: 100%;
display: flex;
flex-direction: column;
flex-flow: column;
height: 100%;
}
#app > header {
background: #272a2f;
border-bottom: 1px solid #333941;
box-sizing: border-box;
height: 60px;
min-width: 700px;
position: relative;
background: #272a2f;
border-bottom: 1px solid #333941;
box-sizing: border-box;
height: 60px;
min-width: 700px;
position: relative;
}
#app > .main-content {
display: flex;
flex: 1;
justify-content: space-between;
overflow: auto;
display: flex;
flex: 1;
justify-content: space-between;
overflow: auto;
}
#app > .main-content > .panel-content,
#app > .main-content > .panel-list {
height: 100%;
position: relative;
height: 100%;
position: relative;
}
#app > .main-content > .panel-list {
width: 25%;
width: 25%;
}
#app > .main-content > .panel-content {
border-left: 1px solid #5e6475;
box-sizing: border-box;
width: 75%;
border-left: 1px solid #5e6475;
box-sizing: border-box;
width: 75%;
}
/* NProgress: A nanoscopic progress bar. */
#nprogress {
pointer-events: none;
pointer-events: none;
}
#nprogress .bar {
background: #7bc876;
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: #7bc876;
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 2px;
}
#nprogress .peg {
display: block;
position: absolute;
right: 0px;
width: 100px;
height: 100%;
box-shadow: 0 0 10px #7bc876, 0 0 5px #7bc876;
opacity: 1;
transform: rotate(3deg) translate(0px, -4px);
display: block;
position: absolute;
right: 0px;
width: 100px;
height: 100%;
box-shadow: 0 0 10px #7bc876, 0 0 5px #7bc876;
opacity: 1;
transform: rotate(3deg) translate(0px, -4px);
}

Просмотреть файл

@ -8,13 +8,13 @@
// import store from '~/store';
describe('<App>', () => {
it('renders without crashing', () => {
// Commented out because there's a network call that I can't figure out
// how to mock yet.
// store.dispatch = sinon.fake();
//
// const div = document.createElement('div');
// ReactDOM.render(shallow(<App store={ store } />), div);
// ReactDOM.unmountComponentAtNode(div);
});
it('renders without crashing', () => {
// Commented out because there's a network call that I can't figure out
// how to mock yet.
// store.dispatch = sinon.fake();
//
// const div = document.createElement('div');
// ReactDOM.render(shallow(<App store={ store } />), div);
// ReactDOM.unmountComponentAtNode(div);
});
});

Просмотреть файл

@ -34,119 +34,114 @@ import type { Stats } from '~/core/stats';
import { AppDispatch, RootState } from '~/store';
type Props = {
batchactions: BatchActionsState;
l10n: L10nState;
locale: LocaleState;
notification: notification.NotificationState;
parameters: NavigationParams;
project: ProjectState;
stats: Stats;
batchactions: BatchActionsState;
l10n: L10nState;
locale: LocaleState;
notification: notification.NotificationState;
parameters: NavigationParams;
project: ProjectState;
stats: Stats;
};
type InternalProps = Props & {
dispatch: AppDispatch;
dispatch: AppDispatch;
};
/**
* Main entry point to the application. Will render the structure of the page.
*/
class App extends React.Component<InternalProps> {
componentDidMount() {
const { parameters } = this.props;
componentDidMount() {
const { parameters } = this.props;
this.props.dispatch(locale.actions.get(parameters.locale));
this.props.dispatch(project.actions.get(parameters.project));
this.props.dispatch(user.actions.getUsers());
this.props.dispatch(locale.actions.get(parameters.locale));
this.props.dispatch(project.actions.get(parameters.project));
this.props.dispatch(user.actions.getUsers());
// Load resources, unless we're in the All Projects view
if (parameters.project !== 'all-projects') {
this.props.dispatch(
resource.actions.get(parameters.locale, parameters.project),
);
}
// Load resources, unless we're in the All Projects view
if (parameters.project !== 'all-projects') {
this.props.dispatch(
resource.actions.get(parameters.locale, parameters.project),
);
}
}
componentDidUpdate(prevProps: InternalProps) {
// If there's a notification in the DOM, passed by django, show it.
// Note that we only show it once, and only when the UI has already
// been rendered, to make sure users do see it.
if (
!this.props.l10n.fetching &&
!this.props.locale.fetching &&
(prevProps.l10n.fetching || prevProps.locale.fetching)
) {
let notifications = [];
const rootElt = document.getElementById('root');
if (rootElt) {
notifications = JSON.parse(rootElt.dataset.notifications);
}
componentDidUpdate(prevProps: InternalProps) {
// If there's a notification in the DOM, passed by django, show it.
// Note that we only show it once, and only when the UI has already
// been rendered, to make sure users do see it.
if (
!this.props.l10n.fetching &&
!this.props.locale.fetching &&
(prevProps.l10n.fetching || prevProps.locale.fetching)
) {
let notifications = [];
const rootElt = document.getElementById('root');
if (rootElt) {
notifications = JSON.parse(rootElt.dataset.notifications);
}
if (notifications.length) {
// Our notification system only supports showing one notification
// for the moment, so we only add the first notification here.
const notif = notifications[0];
this.props.dispatch(
notification.actions.addRaw(notif.content, notif.type),
);
}
}
}
render() {
const state = this.props;
if (state.l10n.fetching || state.locale.fetching) {
return <WaveLoader />;
}
return (
<div id='app'>
<AddonPromotion />
<header>
<Navigation />
<ResourceProgress
stats={state.stats}
parameters={state.parameters}
/>
<ProjectInfo
projectSlug={state.parameters.project}
project={state.project}
/>
<notification.NotificationPanel
notification={state.notification}
/>
<UserControls />
</header>
<section className='main-content'>
<section className='panel-list'>
<SearchBox />
<EntitiesList />
</section>
<section className='panel-content'>
{state.batchactions.entities.length === 0 ? (
<EntityDetails />
) : (
<BatchActions />
)}
</section>
</section>
<Lightbox />
<InteractiveTour />
</div>
if (notifications.length) {
// Our notification system only supports showing one notification
// for the moment, so we only add the first notification here.
const notif = notifications[0];
this.props.dispatch(
notification.actions.addRaw(notif.content, notif.type),
);
}
}
}
render() {
const state = this.props;
if (state.l10n.fetching || state.locale.fetching) {
return <WaveLoader />;
}
return (
<div id='app'>
<AddonPromotion />
<header>
<Navigation />
<ResourceProgress stats={state.stats} parameters={state.parameters} />
<ProjectInfo
projectSlug={state.parameters.project}
project={state.project}
/>
<notification.NotificationPanel notification={state.notification} />
<UserControls />
</header>
<section className='main-content'>
<section className='panel-list'>
<SearchBox />
<EntitiesList />
</section>
<section className='panel-content'>
{state.batchactions.entities.length === 0 ? (
<EntityDetails />
) : (
<BatchActions />
)}
</section>
</section>
<Lightbox />
<InteractiveTour />
</div>
);
}
}
const mapStateToProps = (state: RootState): Props => {
return {
batchactions: state[batchactions.NAME],
l10n: state[l10n.NAME],
locale: state[locale.NAME],
notification: state[notification.NAME],
parameters: navigation.selectors.getNavigationParams(state),
project: state[project.NAME],
stats: state[stats.NAME],
};
return {
batchactions: state[batchactions.NAME],
l10n: state[l10n.NAME],
locale: state[locale.NAME],
notification: state[notification.NAME],
parameters: navigation.selectors.getNavigationParams(state),
project: state[project.NAME],
stats: state[stats.NAME],
};
};
export default connect(mapStateToProps)(App);

Просмотреть файл

@ -1,112 +1,108 @@
export default class APIBase {
abortController: AbortController;
signal: AbortSignal;
abortController: AbortController;
signal: AbortSignal;
constructor() {
// Create a controller to abort fetch requests.
this.abortController = new AbortController();
this.signal = this.abortController.signal;
constructor() {
// Create a controller to abort fetch requests.
this.abortController = new AbortController();
this.signal = this.abortController.signal;
}
abort() {
// Abort the previously started requests.
this.abortController.abort();
// Now create a new controller for the next round of requests.
this.abortController = new AbortController();
this.signal = this.abortController.signal;
}
getCSRFToken(): string {
let csrfToken = '';
const rootElt = document.getElementById('root');
if (rootElt) {
csrfToken = rootElt.dataset.csrfToken;
}
return csrfToken;
}
abort() {
// Abort the previously started requests.
this.abortController.abort();
getFullURL(url: string): URL {
return new URL(url, window.location.origin);
}
// Now create a new controller for the next round of requests.
this.abortController = new AbortController();
this.signal = this.abortController.signal;
}
toCamelCase: (s: string) => string = (s: string) => {
return s.replace(/([-_][a-z])/gi, ($1) => {
return $1.toUpperCase().replace('-', '').replace('_', '');
});
};
getCSRFToken(): string {
let csrfToken = '';
const rootElt = document.getElementById('root');
if (rootElt) {
csrfToken = rootElt.dataset.csrfToken;
}
return csrfToken;
}
isObject: (obj: any) => boolean = function (obj: any) {
return (
obj === Object(obj) && !Array.isArray(obj) && typeof obj !== 'function'
);
};
getFullURL(url: string): URL {
return new URL(url, window.location.origin);
}
async fetch(
url: string,
method: string,
payload: URLSearchParams | FormData | null,
headers: Headers,
): Promise<any> {
const fullUrl = this.getFullURL(url);
toCamelCase: (s: string) => string = (s: string) => {
return s.replace(/([-_][a-z])/gi, ($1) => {
return $1.toUpperCase().replace('-', '').replace('_', '');
});
const requestParams = {
method: method,
credentials: 'same-origin' as RequestCredentials,
headers: headers,
// This signal is used to cancel requests with the `abort()` method.
signal: this.signal,
body: payload,
};
isObject: (obj: any) => boolean = function (obj: any) {
return (
obj === Object(obj) &&
!Array.isArray(obj) &&
typeof obj !== 'function'
);
};
async fetch(
url: string,
method: string,
payload: URLSearchParams | FormData | null,
headers: Headers,
): Promise<any> {
const fullUrl = this.getFullURL(url);
const requestParams = {
method: method,
credentials: 'same-origin' as RequestCredentials,
headers: headers,
// This signal is used to cancel requests with the `abort()` method.
signal: this.signal,
body: payload,
};
if (payload !== null && method === 'GET') {
requestParams.body = null;
fullUrl.search = payload.toString();
}
let response;
try {
response = await fetch(fullUrl.toString(), requestParams);
} catch (e) {
// Swallow Abort errors because we trigger them ourselves.
if (e.name === 'AbortError') {
return {};
}
throw e;
}
try {
return await response.json();
} catch (e) {
// Catch non-JSON responses
/* eslint-disable no-console */
console.error('The response content is not JSON-compatible');
console.error(`URL: ${url} - Method: ${method}`);
console.error(e);
return {};
}
if (payload !== null && method === 'GET') {
requestParams.body = null;
fullUrl.search = payload.toString();
}
keysToCamelCase(results: any): any {
if (this.isObject(results)) {
const newObj: any = {};
Object.keys(results).forEach((key) => {
newObj[this.toCamelCase(key)] = this.keysToCamelCase(
results[key],
);
});
return newObj;
} else if (Array.isArray(results)) {
return results.map((i) => {
return this.keysToCamelCase(i);
});
}
return results;
let response;
try {
response = await fetch(fullUrl.toString(), requestParams);
} catch (e) {
// Swallow Abort errors because we trigger them ourselves.
if (e.name === 'AbortError') {
return {};
}
throw e;
}
try {
return await response.json();
} catch (e) {
// Catch non-JSON responses
/* eslint-disable no-console */
console.error('The response content is not JSON-compatible');
console.error(`URL: ${url} - Method: ${method}`);
console.error(e);
return {};
}
}
keysToCamelCase(results: any): any {
if (this.isObject(results)) {
const newObj: any = {};
Object.keys(results).forEach((key) => {
newObj[this.toCamelCase(key)] = this.keysToCamelCase(results[key]);
});
return newObj;
} else if (Array.isArray(results)) {
return results.map((i) => {
return this.keysToCamelCase(i);
});
}
return results;
}
}

Просмотреть файл

@ -1,45 +1,45 @@
import APIBase from './base';
export default class CommentAPI extends APIBase {
add(
entity: number,
locale: string,
comment: string,
translation: number,
): Promise<any> {
const payload = new URLSearchParams();
payload.append('entity', entity.toString());
payload.append('locale', locale);
payload.append('comment', comment);
if (translation) {
payload.append('translation', translation.toString());
}
const headers = new Headers();
const csrfToken = this.getCSRFToken();
headers.append('X-Requested-With', 'XMLHttpRequest');
headers.append('X-CSRFToken', csrfToken);
return this.fetch('/add-comment/', 'POST', payload, headers);
add(
entity: number,
locale: string,
comment: string,
translation: number,
): Promise<any> {
const payload = new URLSearchParams();
payload.append('entity', entity.toString());
payload.append('locale', locale);
payload.append('comment', comment);
if (translation) {
payload.append('translation', translation.toString());
}
_updateComment(url: string, commentId: number): Promise<any> {
const payload = new URLSearchParams();
payload.append('comment_id', commentId.toString());
const headers = new Headers();
const csrfToken = this.getCSRFToken();
headers.append('X-Requested-With', 'XMLHttpRequest');
headers.append('X-CSRFToken', csrfToken);
const headers = new Headers();
const csrfToken = this.getCSRFToken();
headers.append('X-Requested-With', 'XMLHttpRequest');
headers.append('X-CSRFToken', csrfToken);
return this.fetch('/add-comment/', 'POST', payload, headers);
}
return this.fetch(url, 'POST', payload, headers);
}
_updateComment(url: string, commentId: number): Promise<any> {
const payload = new URLSearchParams();
payload.append('comment_id', commentId.toString());
pinComment(commentId: number): Promise<any> {
return this._updateComment('/pin-comment/', commentId);
}
const headers = new Headers();
const csrfToken = this.getCSRFToken();
headers.append('X-Requested-With', 'XMLHttpRequest');
headers.append('X-CSRFToken', csrfToken);
unpinComment(commentId: number): Promise<any> {
return this._updateComment('/unpin-comment/', commentId);
}
return this.fetch(url, 'POST', payload, headers);
}
pinComment(commentId: number): Promise<any> {
return this._updateComment('/pin-comment/', commentId);
}
unpinComment(commentId: number): Promise<any> {
return this._updateComment('/unpin-comment/', commentId);
}
}

Просмотреть файл

@ -3,215 +3,210 @@ import APIBase from './base';
import type { OtherLocaleTranslations } from './types';
export default class EntityAPI extends APIBase {
async batchEdit(
action: string,
locale: string,
entities: Array<number>,
find: string | undefined,
replace: string | undefined,
): Promise<any> {
const payload = new FormData();
async batchEdit(
action: string,
locale: string,
entities: Array<number>,
find: string | undefined,
replace: string | undefined,
): Promise<any> {
const payload = new FormData();
const csrfToken = this.getCSRFToken();
payload.append('csrfmiddlewaretoken', csrfToken);
const csrfToken = this.getCSRFToken();
payload.append('csrfmiddlewaretoken', csrfToken);
payload.append('action', action);
payload.append('locale', locale);
payload.append('entities', entities.join(','));
payload.append('action', action);
payload.append('locale', locale);
payload.append('entities', entities.join(','));
if (find) {
payload.append('find', find);
}
if (replace) {
payload.append('replace', replace);
}
const headers = new Headers();
headers.append('X-Requested-With', 'XMLHttpRequest');
return await this.fetch(
'/batch-edit-translations/',
'POST',
payload,
headers,
);
if (find) {
payload.append('find', find);
}
/**
* Return a list of entities for a project and locale.
*
* Pass in a `resource` to restrict the list to a specific path.
* If the `exclude` array has values, those entities will be excluded from
* the query. Use this to query for the next set of entities.
*/
async getEntities(
locale: string,
project: string,
resource: string,
entityIds: Array<number> | null | undefined,
exclude: Array<number>,
entity?: string | null | undefined,
search?: string | null | undefined,
status?: string | null | undefined,
extra?: string | null | undefined,
tag?: string | null | undefined,
author?: string | null | undefined,
time?: string | null | undefined,
pkOnly?: boolean | null | undefined,
): Promise<Record<string, any>> {
const payload = new FormData();
payload.append('locale', locale);
payload.append('project', project);
if (resource !== 'all-resources') {
payload.append('paths[]', resource);
}
if (entityIds && entityIds.length) {
payload.append('entity_ids', entityIds.join(','));
}
if (exclude.length) {
payload.append('exclude_entities', exclude.join(','));
}
if (entity) {
payload.append('entity', entity);
}
if (search) {
payload.append('search', search);
}
if (status) {
payload.append('status', status);
}
if (extra) {
payload.append('extra', extra);
}
if (tag) {
payload.append('tag', tag);
}
if (author) {
payload.append('author', author);
}
if (time) {
payload.append('time', time);
}
if (pkOnly) {
payload.append('pk_only', 'true');
}
const headers = new Headers();
headers.append('X-Requested-With', 'XMLHttpRequest');
return await this.fetch('/get-entities/', 'POST', payload, headers);
if (replace) {
payload.append('replace', replace);
}
async getHistory(
entity: number,
locale: string,
pluralForm: number = -1,
): Promise<any> {
const payload = new URLSearchParams();
payload.append('entity', entity.toString());
payload.append('locale', locale);
payload.append('plural_form', pluralForm.toString());
const headers = new Headers();
headers.append('X-Requested-With', 'XMLHttpRequest');
const headers = new Headers();
headers.append('X-Requested-With', 'XMLHttpRequest');
return await this.fetch(
'/batch-edit-translations/',
'POST',
payload,
headers,
);
}
const results = await this.fetch(
'/get-history/',
'GET',
payload,
headers,
);
/**
* Return a list of entities for a project and locale.
*
* Pass in a `resource` to restrict the list to a specific path.
* If the `exclude` array has values, those entities will be excluded from
* the query. Use this to query for the next set of entities.
*/
async getEntities(
locale: string,
project: string,
resource: string,
entityIds: Array<number> | null | undefined,
exclude: Array<number>,
entity?: string | null | undefined,
search?: string | null | undefined,
status?: string | null | undefined,
extra?: string | null | undefined,
tag?: string | null | undefined,
author?: string | null | undefined,
time?: string | null | undefined,
pkOnly?: boolean | null | undefined,
): Promise<Record<string, any>> {
const payload = new FormData();
payload.append('locale', locale);
payload.append('project', project);
return this.keysToCamelCase(results);
if (resource !== 'all-resources') {
payload.append('paths[]', resource);
}
async getSiblingEntities(entity: number, locale: string): Promise<any> {
const payload = new URLSearchParams();
payload.append('entity', entity.toString());
payload.append('locale', locale);
const headers = new Headers();
headers.append('X-Requested-With', 'XMLHttpRequest');
const results = await this.fetch(
'/get-sibling-entities/',
'GET',
payload,
headers,
);
return this.keysToCamelCase(results);
if (entityIds && entityIds.length) {
payload.append('entity_ids', entityIds.join(','));
}
async getOtherLocales(
entity: number,
locale: string,
): Promise<OtherLocaleTranslations> {
const payload = new URLSearchParams();
payload.append('entity', entity.toString());
payload.append('locale', locale);
const headers = new Headers();
headers.append('X-Requested-With', 'XMLHttpRequest');
const results = await this.fetch(
'/other-locales/',
'GET',
payload,
headers,
);
if (results.status === false) {
return [];
}
return results as OtherLocaleTranslations;
if (exclude.length) {
payload.append('exclude_entities', exclude.join(','));
}
async getTeamComments(entity: number, locale: string): Promise<any> {
const payload = new URLSearchParams();
payload.append('entity', entity.toString());
payload.append('locale', locale);
const headers = new Headers();
headers.append('X-Requested-With', 'XMLHttpRequest');
const results = await this.fetch(
'/get-team-comments/',
'GET',
payload,
headers,
);
return this.keysToCamelCase(results);
if (entity) {
payload.append('entity', entity);
}
async getTerms(sourceString: string, locale: string): Promise<any> {
const payload = new URLSearchParams();
payload.append('source_string', sourceString);
payload.append('locale', locale);
const headers = new Headers();
headers.append('X-Requested-With', 'XMLHttpRequest');
const results = await this.fetch(
'/terminology/get-terms/',
'GET',
payload,
headers,
);
return this.keysToCamelCase(results);
if (search) {
payload.append('search', search);
}
if (status) {
payload.append('status', status);
}
if (extra) {
payload.append('extra', extra);
}
if (tag) {
payload.append('tag', tag);
}
if (author) {
payload.append('author', author);
}
if (time) {
payload.append('time', time);
}
if (pkOnly) {
payload.append('pk_only', 'true');
}
const headers = new Headers();
headers.append('X-Requested-With', 'XMLHttpRequest');
return await this.fetch('/get-entities/', 'POST', payload, headers);
}
async getHistory(
entity: number,
locale: string,
pluralForm: number = -1,
): Promise<any> {
const payload = new URLSearchParams();
payload.append('entity', entity.toString());
payload.append('locale', locale);
payload.append('plural_form', pluralForm.toString());
const headers = new Headers();
headers.append('X-Requested-With', 'XMLHttpRequest');
const results = await this.fetch('/get-history/', 'GET', payload, headers);
return this.keysToCamelCase(results);
}
async getSiblingEntities(entity: number, locale: string): Promise<any> {
const payload = new URLSearchParams();
payload.append('entity', entity.toString());
payload.append('locale', locale);
const headers = new Headers();
headers.append('X-Requested-With', 'XMLHttpRequest');
const results = await this.fetch(
'/get-sibling-entities/',
'GET',
payload,
headers,
);
return this.keysToCamelCase(results);
}
async getOtherLocales(
entity: number,
locale: string,
): Promise<OtherLocaleTranslations> {
const payload = new URLSearchParams();
payload.append('entity', entity.toString());
payload.append('locale', locale);
const headers = new Headers();
headers.append('X-Requested-With', 'XMLHttpRequest');
const results = await this.fetch(
'/other-locales/',
'GET',
payload,
headers,
);
if (results.status === false) {
return [];
}
return results as OtherLocaleTranslations;
}
async getTeamComments(entity: number, locale: string): Promise<any> {
const payload = new URLSearchParams();
payload.append('entity', entity.toString());
payload.append('locale', locale);
const headers = new Headers();
headers.append('X-Requested-With', 'XMLHttpRequest');
const results = await this.fetch(
'/get-team-comments/',
'GET',
payload,
headers,
);
return this.keysToCamelCase(results);
}
async getTerms(sourceString: string, locale: string): Promise<any> {
const payload = new URLSearchParams();
payload.append('source_string', sourceString);
payload.append('locale', locale);
const headers = new Headers();
headers.append('X-Requested-With', 'XMLHttpRequest');
const results = await this.fetch(
'/terminology/get-terms/',
'GET',
payload,
headers,
);
return this.keysToCamelCase(results);
}
}

Просмотреть файл

@ -1,18 +1,18 @@
import APIBase from './base';
export default class FilterAPI extends APIBase {
/**
* Return data needed for filtering strings.
*/
async get(locale: string, project: string, resource: string): Promise<any> {
const headers = new Headers();
headers.append('X-Requested-With', 'XMLHttpRequest');
/**
* Return data needed for filtering strings.
*/
async get(locale: string, project: string, resource: string): Promise<any> {
const headers = new Headers();
headers.append('X-Requested-With', 'XMLHttpRequest');
return await this.fetch(
`/${locale}/${project}/${resource}/authors-and-time-range/`,
'GET',
null,
headers,
);
}
return await this.fetch(
`/${locale}/${project}/${resource}/authors-and-time-range/`,
'GET',
null,
headers,
);
}
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше