mirror of
https://github.com/opensupports/opensupports.git
synced 2025-08-16 15:28:35 +02:00
Merge branch 'master' into master
This commit is contained in:
commit
9f17dea87c
184
.circleci/config.yml
Normal file
184
.circleci/config.yml
Normal file
@ -0,0 +1,184 @@
|
||||
version: 2.1
|
||||
orbs:
|
||||
php: circleci/php@1.0.2
|
||||
node: circleci/node@1.1.6
|
||||
aws-cli: circleci/aws-cli@1.0.0
|
||||
jobs:
|
||||
install_composer_packages:
|
||||
executor: php/default
|
||||
steps:
|
||||
- checkout
|
||||
- php/install-composer
|
||||
|
||||
- run:
|
||||
name: Install php extensions
|
||||
command: |
|
||||
sudo add-apt-repository ppa:ondrej/php
|
||||
sudo apt install php7.4-imap
|
||||
|
||||
- php/install-packages:
|
||||
app-dir: server/
|
||||
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- .
|
||||
|
||||
install_node_packages:
|
||||
docker:
|
||||
- image: circleci/node:11.15.0-stretch
|
||||
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: .
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- node-cache-{{ checksum "client/package.json" }}
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: |
|
||||
sudo npm install -g npm@6.7.0
|
||||
sudo npm install -g mocha@6.2.0
|
||||
cd client && npm install
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- client/node_modules
|
||||
key: node-cache-{{ checksum "client/package.json" }}
|
||||
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- .
|
||||
|
||||
deploy_staging_files:
|
||||
docker:
|
||||
- image: circleci/node:11.15.0-stretch
|
||||
|
||||
environment:
|
||||
- GIT_COMMIT_DESC: git log --format=oneline -n 1 $CIRCLE_SHA1
|
||||
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: .
|
||||
|
||||
- deploy:
|
||||
name: Deploy staging files
|
||||
command: |
|
||||
if [ ! "$CIRCLE_BRANCH" = "master" ]; then exit 0; fi
|
||||
if [[ "$GIT_COMMIT_DESC" = Release* ]]; then exit 0; fi
|
||||
sudo apt update
|
||||
sudo apt install -y lftp
|
||||
make deploy-staging-files
|
||||
make deploy-staging-population
|
||||
|
||||
add_release_commit:
|
||||
docker:
|
||||
- image: circleci/node:11.15.0-stretch
|
||||
|
||||
parameters:
|
||||
version:
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: .
|
||||
|
||||
- add_ssh_keys:
|
||||
fingerprints:
|
||||
- "45:1e:cf:38:3f:9f:97:87:5b:b8:fd:e1:6c:71:11:41"
|
||||
|
||||
- run:
|
||||
name: Commit new version
|
||||
command: |
|
||||
export VERSION=<< parameters.version >>
|
||||
cd version_upgrades/release_script
|
||||
npm i
|
||||
npm run modify-files
|
||||
cd ../..
|
||||
ssh-keyscan -H github.com >> ~/.ssh/known_hosts
|
||||
git config --global user.email "ivan@opensupports.com"
|
||||
git config --global user.name "CircleCI-BOT"
|
||||
git add .
|
||||
git commit -m "Release $VERSION"
|
||||
git checkout -b release-${VERSION}
|
||||
git push origin release-${VERSION}
|
||||
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- .
|
||||
|
||||
add_release_tag:
|
||||
docker:
|
||||
- image: circleci/node:11.15.0-stretch
|
||||
|
||||
parameters:
|
||||
version:
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: .
|
||||
|
||||
- add_ssh_keys:
|
||||
fingerprints:
|
||||
- "45:1e:cf:38:3f:9f:97:87:5b:b8:fd:e1:6c:71:11:41"
|
||||
|
||||
- run:
|
||||
name: Add Release tag
|
||||
command: |
|
||||
export VERSION=<< parameters.version >>
|
||||
sudo apt-get update
|
||||
sudo apt-get install lftp
|
||||
make build-release-bundles
|
||||
make upload-bundles
|
||||
# make push-prerelease-tag
|
||||
make populate-staging-release
|
||||
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- .
|
||||
|
||||
parameters:
|
||||
version:
|
||||
type: string
|
||||
default: ""
|
||||
run_build:
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
workflows:
|
||||
build:
|
||||
when:
|
||||
and:
|
||||
- equal: [ master, << pipeline.git.branch >> ]
|
||||
- << pipeline.parameters.run_build >>
|
||||
jobs:
|
||||
- install_composer_packages
|
||||
- install_node_packages:
|
||||
requires:
|
||||
- install_composer_packages
|
||||
- deploy_staging_files:
|
||||
requires:
|
||||
- install_node_packages
|
||||
release:
|
||||
when: << pipeline.parameters.version >>
|
||||
jobs:
|
||||
- install_composer_packages
|
||||
- install_node_packages:
|
||||
requires:
|
||||
- install_composer_packages
|
||||
- add_release_commit:
|
||||
version: << pipeline.parameters.version >>
|
||||
requires:
|
||||
- install_node_packages
|
||||
- add_release_tag:
|
||||
version: << pipeline.parameters.version >>
|
||||
requires:
|
||||
- add_release_commit
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,8 +1,6 @@
|
||||
.idea
|
||||
.jshintrc
|
||||
client/package-lock.json
|
||||
tests/Gemfile.lock
|
||||
server/composer.lock
|
||||
.DS_Store
|
||||
server/vendor
|
||||
server/files/
|
||||
!server/files/.gitkeep
|
||||
@ -11,3 +9,4 @@ server/.dbdata
|
||||
server/.fakemail
|
||||
server/apidoc
|
||||
dist/
|
||||
.env
|
||||
|
@ -10,15 +10,16 @@ services:
|
||||
- mysql
|
||||
|
||||
before_install:
|
||||
- rvm use 2.2 --install --binary --fuzzy
|
||||
- rvm use 2.3 --install --binary --fuzzy
|
||||
- ruby --version
|
||||
- mysql -e 'CREATE DATABASE development;'
|
||||
- nvm install 6.14.4
|
||||
- npm install -g npm@6.1.0
|
||||
- npm install -g mocha
|
||||
- npm install -g mocha@6.2.0
|
||||
- cd client
|
||||
- npm install
|
||||
- cd ../tests
|
||||
- gem install bundler
|
||||
- bundle install
|
||||
- gem install bacon
|
||||
- cd ../server
|
||||
|
67
Makefile
Normal file
67
Makefile
Normal file
@ -0,0 +1,67 @@
|
||||
#!make
|
||||
-include .env
|
||||
|
||||
deploy-staging-files:
|
||||
./build.sh
|
||||
mv dist/opensupports_dev.zip .
|
||||
make upload-bundles
|
||||
|
||||
deploy-staging-population:
|
||||
curl --request POST \
|
||||
--url https://circleci.com/api/v2/project/github/opensupports/staging-population/pipeline \
|
||||
--header 'Circle-Token: ${CIRCLE_API_USER_TOKEN}' \
|
||||
--header 'content-type: application/json' \
|
||||
--data '{"branch":"master","parameters":{"server_to_deploy": "dev1"}}'
|
||||
curl --request POST \
|
||||
--url https://circleci.com/api/v2/project/github/opensupports/staging-population/pipeline \
|
||||
--header 'Circle-Token: ${CIRCLE_API_USER_TOKEN}' \
|
||||
--header 'content-type: application/json' \
|
||||
--data '{"branch":"master","parameters":{"server_to_deploy": "dev2"}}'
|
||||
curl --request POST \
|
||||
--url https://circleci.com/api/v2/project/github/opensupports/staging-population/pipeline \
|
||||
--header 'Circle-Token: ${CIRCLE_API_USER_TOKEN}' \
|
||||
--header 'content-type: application/json' \
|
||||
--data '{"branch":"master","parameters":{"server_to_deploy": "dev3"}}'
|
||||
|
||||
build-release-bundles:
|
||||
$(eval UPGRADE_ZIP="opensupports_v$(VERSION)_update.zip")
|
||||
./build.sh
|
||||
mv dist/opensupports_dev.zip .
|
||||
cp opensupports_dev.zip ${UPGRADE_ZIP} && \
|
||||
mv opensupports_dev.zip opensupports_v${VERSION}.zip && \
|
||||
zip -d ${UPGRADE_ZIP} "api/config.php" && \
|
||||
(( \
|
||||
zip -r ${UPGRADE_ZIP} "version_upgrades/${VERSION}" && \
|
||||
zip -r ${UPGRADE_ZIP} "version_upgrades/mysql_connect.php" \
|
||||
) || true)
|
||||
|
||||
upload-bundles:
|
||||
for file in *.zip ; do \
|
||||
lftp -c "open -u $(FTP_USER),$(FTP_PASSWORD) $(FTP_HOST); set ssl:verify-certificate no; put -O /files/ $${file}"; \
|
||||
done
|
||||
|
||||
push-prerelease-tag:
|
||||
echo -e "Release v${VERSION}\n====\n" > log.txt && \
|
||||
git log $(git describe --tags --abbrev=0 @^)..@ --pretty=format:'%s' >> log.txt
|
||||
# ./version_upgrades/release_script/node_modules/.bin/github-release upload \
|
||||
# --owner opensupports \
|
||||
# --repo opensupports \
|
||||
# --draft true\
|
||||
# --tag "v$(VERSION)" \
|
||||
# --release-name "Release v$(VERSION)" \
|
||||
# --body "$(<log.txt)" \
|
||||
# opensupports_v${VERSION}.zip opensupports_v${VERSION}_update.zip
|
||||
|
||||
populate-staging-release:
|
||||
curl --request POST \
|
||||
--url https://circleci.com/api/v2/project/github/opensupports/staging-population/pipeline \
|
||||
--header 'Circle-Token: ${CIRCLE_API_USER_TOKEN}' \
|
||||
--header 'content-type: application/json' \
|
||||
--data '{"branch":"master","parameters":{"server_to_deploy": "westeros", "version_to_deploy": "${VERSION}"}}'
|
||||
curl --request POST \
|
||||
--url https://circleci.com/api/v2/project/github/opensupports/staging-population/pipeline \
|
||||
--header 'Circle-Token: ${CIRCLE_API_USER_TOKEN}' \
|
||||
--header 'content-type: application/json' \
|
||||
--data '{"branch":"master","parameters":{"server_to_deploy": "senate", "version_to_deploy": "${VERSION}_update"}}'
|
||||
|
||||
deploy-staging-release: build-release-bundles upload-bundles populate-staging-release
|
23
README.md
23
README.md
@ -1,6 +1,6 @@
|
||||

|
||||
|
||||
[](https://travis-ci.org/opensupports/opensupports) v4.3.2
|
||||
[](https://travis-ci.org/opensupports/opensupports) v4.9.0
|
||||
|
||||
OpenSupports is an open source ticket system built primarily with PHP and ReactJS.
|
||||
Please, visit our website for more information: [http://www.opensupports.com/](http://www.opensupports.com/)
|
||||
@ -20,28 +20,25 @@ Here is a guide of how to set up the development environment in OpenSupports.
|
||||
- `curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -`
|
||||
- `sudo apt-get install -y nodejs`
|
||||
4. Install npm: `sudo apt-get install npm`
|
||||
5. Install gulp: `sudo npm install -g gulp`
|
||||
6. Go to client: `cd opensupports/client`
|
||||
7. Install dependencies: `npm install`
|
||||
8. Rebuild node-sass: `npm rebuild node-sass`
|
||||
9. Run: `gulp dev`
|
||||
5. Go to client: `cd opensupports/client`
|
||||
6. Install dependencies: `npm install`
|
||||
7. Rebuild node-sass: `npm rebuild node-sass`
|
||||
8. Run: `npm start` (PHP server api it must be running at :8080)
|
||||
10. Go to the main app: `http://localhost:3000/app` or to the component demo `http://localhost:3000/demo`
|
||||
11. Your browser will automatically be opened and directed to the browser-sync proxy address.
|
||||
12. Use `gulp dev --api` to disable fixtures and use the real PHP server api (it must be running at :8080).
|
||||
12. Use `npm start-fixtures` to enable fixtures and not require php server to be running.
|
||||
|
||||
Now that `gulp dev` is running, the server is up as well and serving files from the `/build` directory. Any changes in the `/src` directory will be automatically processed by Gulp and the changes will be injected to any open browsers pointed at the proxy address.
|
||||
|
||||
OpenSupport uses by default the port 3000, but this port could already be used. If this is the case, you can modify this in the file: `client/gulp/config.js`.
|
||||
OpenSupport uses by default the port 3000, but this port could already be used. If this is the case, you can modify this in the file: `client/webpack.config.js`.
|
||||
|
||||
##### Production Task
|
||||
|
||||
Just as there is a `gulp dev` task for development, there is also a `gulp prod` task for putting the project into a production-ready state. This will run each of the tasks, while also adding the image minification task discussed above.
|
||||
Just as there is a task for development, there is also a `npm build` task for putting the project into a production-ready state. This will run each of the tasks, while also adding the image minification task discussed above and the result store in `dist/` folder.
|
||||
|
||||
**Reminder:** Notice there is `index.html` and `index.php`. The first one searches the backend server where `config.js` says it, the second one uses `/api` to find the server. If you want to run OpenSupports in a single server, then use `index.php`.
|
||||
|
||||
#### Frontend Unit Testing
|
||||
1. Do the steps described before.
|
||||
2. Install mocha: `sudo npm install -g mocha`
|
||||
2. Install mocha: `npm install -g mocha@6.2.0`
|
||||
3. Run `npm test` to run the tests.
|
||||
|
||||
### Getting up and running BACK-END (server folder)
|
||||
@ -68,7 +65,7 @@ Once you've installed dependencies for frontend and backend, you can run `./buil
|
||||
##### BACKEND API RUBY TESTING
|
||||
|
||||
1. Go to tests folder: `cd opensupports/tests`
|
||||
2. Run `make install` to install ruby and its the required dependencies
|
||||
2. Run `make build` to install ruby container and its required dependencies
|
||||
|
||||
- `make run` for running tests (database will be cleared)
|
||||
- `make clear` for clearing database
|
||||
|
32
build.sh
32
build.sh
@ -1,39 +1,37 @@
|
||||
echo "1/3 Building frontend..."
|
||||
cd client
|
||||
gulp prod --api
|
||||
npm run build
|
||||
rm build/index.html
|
||||
echo "2/3 Creating api folder..."
|
||||
cd ../server
|
||||
echo -n > config.php
|
||||
mkdir files2
|
||||
mv files/.htaccess files2
|
||||
rm -rf files/
|
||||
mv files2 files
|
||||
cd ..
|
||||
mkdir api
|
||||
cp server/index.php api
|
||||
cp server/.htaccess api
|
||||
cp server/composer.json api
|
||||
cp server/composer.lock api
|
||||
cp -R server/controllers api
|
||||
cp -R server/data api
|
||||
cp -R server/libs api
|
||||
cp -R server/models api
|
||||
cp -R server/vendor api
|
||||
cp -R server/files api
|
||||
echo -n > api/config.php
|
||||
mv server/index.php api
|
||||
mv server/.htaccess api
|
||||
mv server/composer.json api
|
||||
mv server/composer.lock api
|
||||
mv server/controllers api
|
||||
mv server/data api
|
||||
mv server/libs api
|
||||
mv server/models api
|
||||
mv server/vendor api
|
||||
mv server/files api
|
||||
cp server/config.php api
|
||||
chmod -R 755 .
|
||||
cp client/src/index.php client/build
|
||||
echo "3/3 Generating zip..."
|
||||
cd client/build
|
||||
zip opensupports_dev.zip index.php
|
||||
zip -u opensupports_dev.zip .htaccess
|
||||
zip -u opensupports_dev.zip css/main.css
|
||||
zip -u opensupports_dev.zip js/main.js
|
||||
zip -ur opensupports_dev.zip fonts
|
||||
zip -u opensupports_dev.zip bundle.js
|
||||
zip -ur opensupports_dev.zip images
|
||||
mv opensupports_dev.zip ../..
|
||||
cd ../..
|
||||
zip -ur opensupports_dev.zip api
|
||||
rm -rf dist
|
||||
mkdir dist
|
||||
mv opensupports_dev.zip dist
|
||||
rm -rf api
|
||||
|
@ -1,3 +1,4 @@
|
||||
{
|
||||
"optional": ["es7.classProperties"]
|
||||
}
|
||||
"presets": ["@babel/preset-env", "@babel/preset-react"],
|
||||
"plugins": ["@babel/plugin-proposal-class-properties", "add-module-exports"]
|
||||
}
|
||||
|
@ -1,31 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
|
||||
'serverport': 3006,
|
||||
|
||||
'scripts': {
|
||||
'src': './src/*.js',
|
||||
'dest': './build/js/'
|
||||
},
|
||||
|
||||
'images': {
|
||||
'src': './src/assets/images/**/*.{jpeg,jpg,png}',
|
||||
'dest': './build/images/'
|
||||
},
|
||||
|
||||
'styles': {
|
||||
'src': './src/**/*.scss',
|
||||
'dest': './build/css/'
|
||||
},
|
||||
|
||||
'fonts': {
|
||||
'src': './src/scss/font_awesome/fonts/*',
|
||||
'dest': './build/fonts/'
|
||||
},
|
||||
|
||||
'sourceDir': './src/',
|
||||
|
||||
'buildDir': './build/'
|
||||
|
||||
};
|
@ -1,11 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs');
|
||||
var onlyScripts = require('./util/script-filter');
|
||||
var tasks = fs.readdirSync('./gulp/tasks/').filter(onlyScripts);
|
||||
|
||||
process.env.NODE_PATH = './src';
|
||||
|
||||
tasks.forEach(function(task) {
|
||||
require('./tasks/' + task);
|
||||
});
|
@ -1,14 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var config = require('../config');
|
||||
var browserSync = require('browser-sync');
|
||||
var gulp = require('gulp');
|
||||
|
||||
gulp.task('browserSync', function() {
|
||||
|
||||
browserSync({
|
||||
proxy: 'localhost:' + config.serverport,
|
||||
startPath: '/'
|
||||
});
|
||||
|
||||
});
|
@ -1,78 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
var gulpif = require('gulp-if');
|
||||
var gutil = require('gulp-util');
|
||||
var source = require('vinyl-source-stream');
|
||||
var streamify = require('gulp-streamify');
|
||||
var sourcemaps = require('gulp-sourcemaps');
|
||||
var rename = require('gulp-rename');
|
||||
var watchify = require('watchify');
|
||||
var browserify = require('browserify');
|
||||
var babelify = require('babelify');
|
||||
var uglify = require('gulp-uglify');
|
||||
var browserSync = require('browser-sync');
|
||||
var debowerify = require('debowerify');
|
||||
var handleErrors = require('../util/handle-errors');
|
||||
var config = require('../config');
|
||||
var util = require('gulp-util');
|
||||
|
||||
// Based on: http://blog.avisi.nl/2014/04/25/how-to-keep-a-fast-build-with-browserify-and-reactjs/
|
||||
function buildScript(file, watch) {
|
||||
|
||||
var bundler = browserify({
|
||||
entries: [config.sourceDir + file],
|
||||
debug: !global.isProd,
|
||||
insertGlobalVars: {
|
||||
isProd: function () {
|
||||
return (global.isProd) ? "'enabled'" : "'disabled'";
|
||||
},
|
||||
noFixtures: function() {
|
||||
return (util.env['api']) ? "'enabled'" : "'disabled'";
|
||||
}
|
||||
},
|
||||
cache: {},
|
||||
packageCache: {},
|
||||
fullPaths: false
|
||||
});
|
||||
|
||||
if ( watch ) {
|
||||
bundler = watchify(bundler);
|
||||
bundler.on('update', rebundle);
|
||||
}
|
||||
|
||||
bundler.transform(babelify, {'optional': ['es7.classProperties']});
|
||||
bundler.transform(debowerify);
|
||||
|
||||
function rebundle() {
|
||||
var stream = bundler.bundle();
|
||||
|
||||
gutil.log('Rebundle...');
|
||||
|
||||
return stream.on('error', handleErrors)
|
||||
.pipe(source(file))
|
||||
.pipe(gulpif(global.isProd, streamify(uglify())))
|
||||
.pipe(streamify(rename({
|
||||
basename: 'main'
|
||||
})))
|
||||
.pipe(gulpif(!global.isProd, sourcemaps.write('./')))
|
||||
.pipe(gulp.dest(config.scripts.dest))
|
||||
.pipe(gulpif(browserSync.active, browserSync.reload({ stream: true, once: true })));
|
||||
}
|
||||
|
||||
return rebundle();
|
||||
|
||||
}
|
||||
|
||||
gulp.task('browserify', function() {
|
||||
|
||||
// Only run watchify if NOT production
|
||||
return buildScript('index.js', !global.isProd);
|
||||
|
||||
});
|
||||
|
||||
gulp.task('config', function() {
|
||||
|
||||
return gulp.src(config.sourceDir + 'config.js')
|
||||
.pipe(gulp.dest(config.scripts.dest))
|
||||
});
|
@ -1,11 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var config = require('../config');
|
||||
var gulp = require('gulp');
|
||||
var del = require('del');
|
||||
|
||||
gulp.task('clean', function(cb) {
|
||||
|
||||
del([config.buildDir], cb);
|
||||
|
||||
});
|
@ -1,10 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
var config = require('../config');
|
||||
|
||||
gulp.task('copyFonts', function() {
|
||||
|
||||
return gulp.src(config.fonts.src)
|
||||
.pipe(gulp.dest(config.fonts.dest))
|
||||
});
|
@ -1,11 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
|
||||
gulp.task('copyIcons', function() {
|
||||
|
||||
// Copy icons from root directory to build/
|
||||
return gulp.src(['./*.png', './favicon.ico'])
|
||||
.pipe(gulp.dest('build/'));
|
||||
|
||||
});
|
@ -1,12 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
var config = require('../config');
|
||||
|
||||
gulp.task('copyIndex', function() {
|
||||
|
||||
gulp.src(config.sourceDir + 'index.html').pipe(gulp.dest(config.buildDir));
|
||||
gulp.src(config.sourceDir + 'index.php').pipe(gulp.dest(config.buildDir));
|
||||
gulp.src(config.sourceDir + '.htaccess').pipe(gulp.dest(config.buildDir));
|
||||
|
||||
});
|
@ -1,10 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
//var config = require('../config');
|
||||
|
||||
gulp.task('deploy', ['prod'], function() {
|
||||
|
||||
// Deploy to hosting environment
|
||||
|
||||
});
|
@ -1,15 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
var runSequence = require('run-sequence');
|
||||
|
||||
gulp.task('dev', ['clean'], function(callback) {
|
||||
|
||||
callback = callback || function() {};
|
||||
|
||||
global.isProd = false;
|
||||
|
||||
// Run all tasks once
|
||||
return runSequence(['sass', 'imagemin', 'browserify', 'copyFonts', 'copyIndex', 'copyIcons', 'config'], 'watch', callback);
|
||||
|
||||
});
|
@ -1,17 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
var gulpif = require('gulp-if');
|
||||
var imagemin = require('gulp-imagemin');
|
||||
var browserSync = require('browser-sync');
|
||||
var config = require('../config');
|
||||
|
||||
gulp.task('imagemin', function() {
|
||||
|
||||
// Run imagemin task on all images
|
||||
return gulp.src(config.images.src)
|
||||
.pipe(gulpif(global.isProd, imagemin()))
|
||||
.pipe(gulp.dest(config.images.dest))
|
||||
.pipe(gulpif(browserSync.active, browserSync.reload({ stream: true, once: true })));
|
||||
|
||||
});
|
@ -1,15 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
var runSequence = require('run-sequence');
|
||||
|
||||
gulp.task('prod', ['clean'], function(callback) {
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
callback = callback || function() {};
|
||||
|
||||
global.isProd = true;
|
||||
|
||||
runSequence(['sass', 'imagemin', 'browserify', 'copyFonts', 'copyIndex', 'copyIcons'], callback);
|
||||
|
||||
});
|
@ -1,29 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
var sass = require('gulp-sass');
|
||||
var gulpif = require('gulp-if');
|
||||
var browserSync = require('browser-sync');
|
||||
var autoprefixer = require('gulp-autoprefixer');
|
||||
var bulkSass = require('gulp-sass-bulk-import');
|
||||
var plumber = require('gulp-plumber');
|
||||
var handleErrors = require('../util/handle-errors');
|
||||
var config = require('../config');
|
||||
|
||||
gulp.task('sass', function () {
|
||||
|
||||
return gulp.src(config.styles.src)
|
||||
.pipe(bulkSass())
|
||||
.pipe(plumber())
|
||||
.pipe(sass({
|
||||
sourceComments: global.isProd ? 'none' : 'map',
|
||||
sourceMap: 'sass',
|
||||
outputStyle: global.isProd ? 'compressed' : 'nested'
|
||||
}))
|
||||
.on('error', handleErrors)
|
||||
.pipe(autoprefixer("last 2 versions", "> 1%", "ie 8"))
|
||||
.on('error', handleErrors)
|
||||
.pipe(gulp.dest(config.styles.dest))
|
||||
.pipe(gulpif(browserSync.active, browserSync.reload({ stream: true })));
|
||||
|
||||
});
|
@ -1,44 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var config = require('../config');
|
||||
var http = require('http');
|
||||
var express = require('express');
|
||||
var gulp = require('gulp');
|
||||
var gutil = require('gulp-util');
|
||||
var morgan = require('morgan');
|
||||
var proxy = require('express-http-proxy');
|
||||
|
||||
gulp.task('server', function() {
|
||||
|
||||
var server = express();
|
||||
|
||||
// log all requests to the console
|
||||
server.use(morgan('dev'));
|
||||
server.use(express.static(config.buildDir));
|
||||
|
||||
// Proxy php server api
|
||||
server.use('/api', proxy('http://localhost:8080', {
|
||||
forwardPath: function(req, res) {
|
||||
return require('url').parse(req.url).path;
|
||||
}
|
||||
}));
|
||||
|
||||
// Serve index.html for all routes to leave routing up to react-router
|
||||
server.all('/*', function(req, res) {
|
||||
res.sendFile('index.html', { root: 'build' });
|
||||
});
|
||||
|
||||
// Start webserver if not already running
|
||||
var s = http.createServer(server);
|
||||
s.on('error', function(err){
|
||||
if(err.code === 'EADDRINUSE'){
|
||||
gutil.log('Development server is already started at port ' + config.serverport);
|
||||
}
|
||||
else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
s.listen(config.serverport);
|
||||
|
||||
});
|
@ -1,10 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
//var config = require('../config');
|
||||
|
||||
gulp.task('test', function() {
|
||||
|
||||
// Run all tests
|
||||
|
||||
});
|
@ -1,13 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
var config = require('../config');
|
||||
|
||||
gulp.task('watch', ['browserSync', 'server'], function() {
|
||||
|
||||
// Scripts are automatically watched by Watchify inside Browserify task
|
||||
gulp.watch(config.styles.src, ['sass']);
|
||||
gulp.watch(config.images.src, ['imagemin']);
|
||||
gulp.watch(config.sourceDir + 'index.html', ['copyIndex']);
|
||||
|
||||
});
|
@ -1,27 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var notify = require('gulp-notify');
|
||||
|
||||
module.exports = function(error) {
|
||||
|
||||
if( !global.isProd ) {
|
||||
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
|
||||
// Send error to notification center with gulp-notify
|
||||
notify.onError({
|
||||
title: 'Compile Error',
|
||||
message: '<%= error.message %>'
|
||||
}).apply(this, args);
|
||||
|
||||
// Keep gulp from hanging on this task
|
||||
this.emit('end');
|
||||
|
||||
} else {
|
||||
// Log the error and stop the process
|
||||
// to prevent broken code from building
|
||||
console.log(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
};
|
@ -1,9 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var path = require('path');
|
||||
|
||||
// Filters out non .coffee and .js files. Prevents
|
||||
// accidental inclusion of possible hidden files
|
||||
module.exports = function(name) {
|
||||
return /(\.(js|coffee)$)/i.test(path.extname(name));
|
||||
};
|
@ -1,5 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
global.isProd = false;
|
||||
|
||||
require('./gulp');
|
21684
client/package-lock.json
generated
Normal file
21684
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "OpenSupports",
|
||||
"version": "4.2.0",
|
||||
"version": "4.9.0",
|
||||
"author": "Ivan Diaz <contact@opensupports.com>",
|
||||
"description": "Open source ticket system made with PHP and ReactJS",
|
||||
"repository": {
|
||||
@ -13,65 +13,78 @@
|
||||
"npm": "^2.1.x"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "export NODE_PATH=src && mocha src/lib-test/preprocessor.js --compilers js:babel-core/register --recursive src/**/**/__tests__/*-test.js"
|
||||
"start": "webpack-dev-server",
|
||||
"start-fixtures": "webpack-dev-server --env.FIXTURES=1",
|
||||
"build": "./node_modules/.bin/rimraf build && NODE_ENV=production ./node_modules/.bin/webpack -p --devtool none",
|
||||
"test": "export NODE_PATH=src && mocha src/lib-test/preprocessor.js --require @babel/register --recursive src/**/**/__tests__/*-test.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.5.5",
|
||||
"@babel/core": "^7.5.5",
|
||||
"@babel/node": "^7.5.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.4.4",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.5.0",
|
||||
"@babel/preset-env": "^7.5.5",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@babel/register": "^7.5.5",
|
||||
"axios-mock-adapter": "^1.15.0",
|
||||
"babel-core": "^5.8.22",
|
||||
"babel-plugin-transform-class-properties": "^6.11.5",
|
||||
"babel-register": "^6.7.2",
|
||||
"babelify": "^6.1.x",
|
||||
"browser-sync": "^2.7.13",
|
||||
"browserify": "^10.2.6",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-add-module-exports": "^1.0.2",
|
||||
"browser-sync": "^2.27.5",
|
||||
"chai": "^3.5.0",
|
||||
"copy-webpack-plugin": "^5.0.3",
|
||||
"css-loader": "^3.0.0",
|
||||
"debowerify": "^1.3.1",
|
||||
"del": "^1.2.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-loader": "^2.1.2",
|
||||
"express": "^4.13.1",
|
||||
"express-http-proxy": "^0.6.0",
|
||||
"gulp": "^3.9.0",
|
||||
"gulp-autoprefixer": "^2.3.1",
|
||||
"gulp-connect-php": "0.0.5",
|
||||
"gulp-if": "^1.2.5",
|
||||
"gulp-imagemin": "^2.3.0",
|
||||
"gulp-notify": "^2.2.0",
|
||||
"gulp-plumber": "^1.2.0",
|
||||
"gulp-rename": "^1.2.2",
|
||||
"gulp-sass": "^4.0.1",
|
||||
"gulp-sass-bulk-import": "^1.0.1",
|
||||
"gulp-sourcemaps": "^2.6.4",
|
||||
"gulp-streamify": "0.0.5",
|
||||
"gulp-uglify": "^2.1.2",
|
||||
"gulp-util": "^3.0.6",
|
||||
"file-loader": "^4.0.0",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"humps": "^0.6.0",
|
||||
"jsdom": "^8.4.1",
|
||||
"mocha": "^6.2.0",
|
||||
"morgan": "^1.6.1",
|
||||
"node-sass": "^4.12.0",
|
||||
"nodemon": "^1.19.1",
|
||||
"path": "^0.12.7",
|
||||
"proxyquire": "^1.7.4",
|
||||
"react-addons-test-utils": "^15.0.1",
|
||||
"rimraf": "^2.6.3",
|
||||
"run-sequence": "^1.1.1",
|
||||
"sass-loader": "^7.1.0",
|
||||
"sinon": "^1.17.3",
|
||||
"sinon-chai": "^2.8.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"stylelint-webpack-plugin": "^0.10.5",
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"watchify": "^3.2.x"
|
||||
"watchify": "^3.2.x",
|
||||
"webpack": "^4.34.0",
|
||||
"webpack-bundle-analyzer": "^3.3.2",
|
||||
"webpack-cli": "^3.3.4",
|
||||
"webpack-dev-server": "^3.7.1",
|
||||
"webpack-import-glob": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"app-module-path": "^1.0.3",
|
||||
"axios": "^0.18.0",
|
||||
"chart.js": "^2.4.0",
|
||||
"axios": "^0.21.1",
|
||||
"chart.js": "^2.9.3",
|
||||
"classnames": "^2.2.5",
|
||||
"history": "^3.0.0",
|
||||
"html-to-text": "^4.0.0",
|
||||
"keycode": "^2.1.4",
|
||||
"localStorage": "^1.0.3",
|
||||
"lodash": "^3.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"messageformat": "^0.2.2",
|
||||
"moment": "^2.27.0",
|
||||
"qs": "^6.5.2",
|
||||
"query-string": "^6.12.1",
|
||||
"quill-image-resize-module-react": "^3.0.0",
|
||||
"random-string": "^0.2.0",
|
||||
"react": "^15.4.2",
|
||||
"react-chartjs-2": "^2.0.0",
|
||||
"react-chartjs-2": "^2.10.0",
|
||||
"react-document-title": "^1.0.2",
|
||||
"react-dom": "^15.4.2",
|
||||
"react-draft-wysiwyg": "^1.12.13",
|
||||
"react-google-recaptcha": "^0.5.2",
|
||||
"react-motion": "^0.4.7",
|
||||
"react-quill": "^1.3.1",
|
||||
|
@ -1,164 +1,160 @@
|
||||
const sessionStoreMock = require('lib-app/__mocks__/session-store-mock');
|
||||
const APICallMock = require('lib-app/__mocks__/api-call-mock');
|
||||
const storeMock = {
|
||||
dispatch: stub().returns({
|
||||
then: stub()
|
||||
})
|
||||
};
|
||||
// const sessionStoreMock = require('lib-app/__mocks__/session-store-mock');
|
||||
// const APICallMock = require('lib-app/__mocks__/api-call-mock');
|
||||
// const storeMock = require('app/__mocks__/store-mock');
|
||||
|
||||
const SessionActions = requireUnit('actions/session-actions', {
|
||||
'lib-app/api-call': APICallMock,
|
||||
'lib-app/session-store': sessionStoreMock,
|
||||
'app/store': storeMock
|
||||
});
|
||||
// const SessionActions = requireUnit('actions/session-actions', {
|
||||
// 'lib-app/api-call': APICallMock,
|
||||
// 'lib-app/session-store': sessionStoreMock,
|
||||
// 'app/store': storeMock
|
||||
// });
|
||||
|
||||
describe('Session Actions,', function () {
|
||||
// describe('Session Actions,', function () {
|
||||
|
||||
describe('login action', function () {
|
||||
it('should return LOGIN with with a result promise', function () {
|
||||
APICallMock.call.returns({
|
||||
then: function (resolve) {
|
||||
resolve({
|
||||
data: {
|
||||
userId: 14,
|
||||
token: 'SOME_TOKEN'
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// describe('login action', function () {
|
||||
// it('should return LOGIN with with a result promise', function () {
|
||||
// APICallMock.call.returns({
|
||||
// then: function (resolve) {
|
||||
// resolve({
|
||||
// data: {
|
||||
// userId: 14,
|
||||
// token: 'SOME_TOKEN'
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
let loginData = {
|
||||
email: 'SOME_EMAIL',
|
||||
password: 'SOME_PASSWORD',
|
||||
remember: false
|
||||
};
|
||||
// let loginData = {
|
||||
// email: 'SOME_EMAIL',
|
||||
// password: 'SOME_PASSWORD',
|
||||
// remember: false
|
||||
// };
|
||||
|
||||
expect(SessionActions.login(loginData).type).to.equal('LOGIN');
|
||||
expect(storeMock.dispatch).to.have.been.calledWithMatch({type: 'USER_DATA'});
|
||||
expect(APICallMock.call).to.have.been.calledWith({
|
||||
path: '/user/get',
|
||||
data: {
|
||||
csrf_userid: 14,
|
||||
csrf_token: 'SOME_TOKEN'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
// expect(SessionActions.login(loginData).type).to.equal('LOGIN');
|
||||
// expect(storeMock.dispatch).to.have.been.calledWithMatch({type: 'USER_DATA'});
|
||||
// expect(APICallMock.call).to.have.been.calledWith({
|
||||
// path: '/user/get',
|
||||
// data: {
|
||||
// csrf_userid: 14,
|
||||
// csrf_token: 'SOME_TOKEN'
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
describe('autoLogin action', function () {
|
||||
it('should return LOGIN_AUTO with remember data from sessionStore', function () {
|
||||
APICallMock.call.returns({
|
||||
then: function (resolve) {
|
||||
resolve({
|
||||
data: {
|
||||
userId: 14
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
sessionStoreMock.getRememberData.returns({
|
||||
token: 'SOME_TOKEN',
|
||||
userId: 'SOME_ID',
|
||||
expiration: 'SOME_EXPIRATION'
|
||||
});
|
||||
// describe('autoLogin action', function () {
|
||||
// it('should return LOGIN_AUTO with remember data from sessionStore', function () {
|
||||
// APICallMock.call.returns({
|
||||
// then: function (resolve) {
|
||||
// resolve({
|
||||
// data: {
|
||||
// userId: 14
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// sessionStoreMock.getRememberData.returns({
|
||||
// token: 'SOME_TOKEN',
|
||||
// userId: 'SOME_ID',
|
||||
// expiration: 'SOME_EXPIRATION'
|
||||
// });
|
||||
|
||||
expect(SessionActions.autoLogin().type).to.equal('LOGIN_AUTO');
|
||||
expect(storeMock.dispatch).to.have.been.calledWithMatch({type: 'USER_DATA'});
|
||||
expect(APICallMock.call).to.have.been.calledWith({
|
||||
path: '/user/login',
|
||||
data: {
|
||||
rememberToken: 'SOME_TOKEN',
|
||||
userId: 'SOME_ID',
|
||||
remember: 1,
|
||||
isAutomatic: 1
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
// expect(SessionActions.autoLogin().type).to.equal('LOGIN_AUTO');
|
||||
// expect(storeMock.dispatch).to.have.been.calledWithMatch({type: 'USER_DATA'});
|
||||
// expect(APICallMock.call).to.have.been.calledWith({
|
||||
// path: '/user/login',
|
||||
// data: {
|
||||
// rememberToken: 'SOME_TOKEN',
|
||||
// userId: 'SOME_ID',
|
||||
// remember: 1,
|
||||
// isAutomatic: 1
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
describe('logout action', function () {
|
||||
it('should return LOGOUT and call /user/logout', function () {
|
||||
APICallMock.call.returns('API_RESULT');
|
||||
APICallMock.call.reset();
|
||||
// describe('logout action', function () {
|
||||
// it('should return LOGOUT and call /user/logout', function () {
|
||||
// APICallMock.call.returns('API_RESULT');
|
||||
// APICallMock.call.reset();
|
||||
|
||||
expect(SessionActions.logout()).to.deep.equal({
|
||||
type: 'LOGOUT',
|
||||
payload: 'API_RESULT'
|
||||
});
|
||||
expect(APICallMock.call).to.have.been.calledWith({
|
||||
path: '/user/logout',
|
||||
data: {}
|
||||
});
|
||||
});
|
||||
});
|
||||
// expect(SessionActions.logout()).to.deep.equal({
|
||||
// type: 'LOGOUT',
|
||||
// payload: 'API_RESULT'
|
||||
// });
|
||||
// expect(APICallMock.call).to.have.been.calledWith({
|
||||
// path: '/user/logout',
|
||||
// data: {}
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
describe('initSession action', function () {
|
||||
beforeEach(function () {
|
||||
APICallMock.call.returns({
|
||||
then: function (resolve) {
|
||||
resolve({
|
||||
data: {
|
||||
sessionActive: false
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
APICallMock.call.reset();
|
||||
storeMock.dispatch.reset();
|
||||
sessionStoreMock.isLoggedIn.returns(true)
|
||||
});
|
||||
// describe('initSession action', function () {
|
||||
// beforeEach(function () {
|
||||
// APICallMock.call.returns({
|
||||
// then: function (resolve) {
|
||||
// resolve({
|
||||
// data: {
|
||||
// sessionActive: false
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// APICallMock.call.reset();
|
||||
// storeMock.dispatch.reset();
|
||||
// sessionStoreMock.isLoggedIn.returns(true)
|
||||
// });
|
||||
|
||||
after(function () {
|
||||
APICallMock.call.returns(new Promise(function (resolve) {
|
||||
resolve({
|
||||
data: {
|
||||
sessionActive: true
|
||||
}
|
||||
});
|
||||
}));
|
||||
});
|
||||
// after(function () {
|
||||
// APICallMock.call.returns(new Promise(function (resolve) {
|
||||
// resolve({
|
||||
// data: {
|
||||
// sessionActive: true
|
||||
// }
|
||||
// });
|
||||
// }));
|
||||
// });
|
||||
|
||||
it('should return CHECK_SESSION and dispatch SESSION_CHECKED if session is active', function () {
|
||||
APICallMock.call.returns({
|
||||
then: function (resolve) {
|
||||
resolve({
|
||||
data: {
|
||||
sessionActive: true
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// it('should return CHECK_SESSION and dispatch SESSION_CHECKED if session is active', function () {
|
||||
// APICallMock.call.returns({
|
||||
// then: function (resolve) {
|
||||
// resolve({
|
||||
// data: {
|
||||
// sessionActive: true
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
expect(SessionActions.initSession().type).to.equal('CHECK_SESSION');
|
||||
expect(storeMock.dispatch).to.have.been.calledWith({type: 'SESSION_CHECKED'});
|
||||
expect(APICallMock.call).to.have.been.calledWith({
|
||||
path: '/user/check-session',
|
||||
data: {}
|
||||
});
|
||||
});
|
||||
// expect(SessionActions.initSession().type).to.equal('CHECK_SESSION');
|
||||
// expect(storeMock.dispatch).to.have.been.calledWith({type: 'SESSION_CHECKED'});
|
||||
// expect(APICallMock.call).to.have.been.calledWith({
|
||||
// path: '/user/check-session',
|
||||
// data: {}
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should return CHECK_SESSION and dispatch LOGOUT_FULFILLED if session is not active and no remember data', function () {
|
||||
sessionStoreMock.isRememberDataExpired.returns(true);
|
||||
// it('should return CHECK_SESSION and dispatch LOGOUT_FULFILLED if session is not active and no remember data', function () {
|
||||
// sessionStoreMock.isRememberDataExpired.returns(true);
|
||||
|
||||
expect(SessionActions.initSession().type).to.equal('CHECK_SESSION');
|
||||
expect(storeMock.dispatch).to.have.been.calledWith({type: 'LOGOUT_FULFILLED'});
|
||||
expect(APICallMock.call).to.have.been.calledWith({
|
||||
path: '/user/check-session',
|
||||
data: {}
|
||||
});
|
||||
});
|
||||
// expect(SessionActions.initSession().type).to.equal('CHECK_SESSION');
|
||||
// expect(storeMock.dispatch).to.have.been.calledWith({type: 'LOGOUT_FULFILLED'});
|
||||
// expect(APICallMock.call).to.have.been.calledWith({
|
||||
// path: '/user/check-session',
|
||||
// data: {}
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should return CHECK_SESSION and dispatch LOGIN_AUTO if session is not active but remember data exists', function () {
|
||||
sessionStoreMock.isRememberDataExpired.returns(false);
|
||||
// it('should return CHECK_SESSION and dispatch LOGIN_AUTO if session is not active but remember data exists', function () {
|
||||
// sessionStoreMock.isRememberDataExpired.returns(false);
|
||||
|
||||
expect(SessionActions.initSession().type).to.equal('CHECK_SESSION');
|
||||
expect(storeMock.dispatch).to.not.have.been.calledWith({type: 'LOGOUT_FULFILLED'});
|
||||
expect(APICallMock.call).to.have.been.calledWith({
|
||||
path: '/user/check-session',
|
||||
data: {}
|
||||
});
|
||||
// expect(SessionActions.initSession().type).to.equal('CHECK_SESSION');
|
||||
// expect(storeMock.dispatch).to.not.have.been.calledWith({type: 'LOGOUT_FULFILLED'});
|
||||
// expect(APICallMock.call).to.have.been.calledWith({
|
||||
// path: '/user/check-session',
|
||||
// data: {}
|
||||
// });
|
||||
|
||||
expect(storeMock.dispatch).to.have.been.calledWith(SessionActions.autoLogin());
|
||||
});
|
||||
});
|
||||
});
|
||||
// expect(storeMock.dispatch).to.have.been.calledWith(SessionActions.autoLogin());
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
@ -12,22 +12,22 @@ export default {
|
||||
};
|
||||
},
|
||||
|
||||
retrieveMyTickets(page, closed = 0) {
|
||||
retrieveMyTickets(page, closed = 0, departmentId = 0) {
|
||||
return {
|
||||
type: 'MY_TICKETS',
|
||||
payload: API.call({
|
||||
path: '/staff/get-tickets',
|
||||
data: {page, closed}
|
||||
data: {page, closed, departmentId}
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
retrieveNewTickets(page = 1) {
|
||||
retrieveNewTickets(page = 1, departmentId = 0) {
|
||||
return {
|
||||
type: 'NEW_TICKETS',
|
||||
payload: API.call({
|
||||
path: '/staff/get-new-tickets',
|
||||
data: {page}
|
||||
data: {page, departmentId}
|
||||
})
|
||||
};
|
||||
},
|
||||
|
15
client/src/actions/login-form-actions.js
Normal file
15
client/src/actions/login-form-actions.js
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
export default {
|
||||
showLoginForm() {
|
||||
return {
|
||||
type: 'SHOW_LOGIN_FORM',
|
||||
payload: true
|
||||
};
|
||||
},
|
||||
hideLoginForm() {
|
||||
return {
|
||||
type: 'HIDE_LOGIN_FORM',
|
||||
payload: false
|
||||
};
|
||||
}
|
||||
};
|
80
client/src/actions/search-filters-actions.js
Normal file
80
client/src/actions/search-filters-actions.js
Normal file
@ -0,0 +1,80 @@
|
||||
import API from 'lib-app/api-call';
|
||||
import searchTicketsUtils from 'lib-app/search-tickets-utils';
|
||||
import history from 'lib-app/history';
|
||||
|
||||
export default {
|
||||
setLoadingInTrue() {
|
||||
return {
|
||||
type: 'SEARCH_FILTERS_SET_LOADING_IN_TRUE',
|
||||
payload: {}
|
||||
}
|
||||
},
|
||||
retrieveSearchTickets(ticketQueryListState, filters = {}) {
|
||||
return {
|
||||
type: 'SEARCH_TICKETS',
|
||||
payload: API.call({
|
||||
path: '/ticket/search',
|
||||
data: {
|
||||
...filters,
|
||||
page: ticketQueryListState.page
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
changeForm(form) {
|
||||
return {
|
||||
type: 'SEARCH_FILTERS_CHANGE_FORM',
|
||||
payload: form
|
||||
}
|
||||
},
|
||||
changeFilters(listConfig) {
|
||||
const filtersForAPI = searchTicketsUtils.prepareFiltersForAPI(listConfig.filters);
|
||||
|
||||
return {
|
||||
type: 'SEARCH_FILTERS_CHANGE_FILTERS',
|
||||
payload: {...listConfig, filtersForAPI}
|
||||
}
|
||||
},
|
||||
setDefaultFormValues() {
|
||||
return {
|
||||
type: 'SEARCH_FILTERS_SET_DEFAULT_FORM_VALUES',
|
||||
payload: {}
|
||||
}
|
||||
},
|
||||
changeShowFilters(showFilters) {
|
||||
return {
|
||||
type: 'SEARCH_FILTERS_CHANGE_SHOW_FILTERS',
|
||||
payload: showFilters
|
||||
}
|
||||
},
|
||||
changePage(filtersWithPage) {
|
||||
const filtersForAPI = searchTicketsUtils.prepareFiltersForAPI(filtersWithPage);
|
||||
const currentPath = window.location.pathname;
|
||||
const urlQuery = searchTicketsUtils.getFiltersForURL({
|
||||
filters: filtersForAPI,
|
||||
shouldRemoveCustomParam: false,
|
||||
shouldRemoveUseInitialValuesParam: true
|
||||
});
|
||||
urlQuery && history.push(`${currentPath}${urlQuery}`);
|
||||
|
||||
return {
|
||||
type: 'SEARCH_FILTERS_CHANGE_PAGE',
|
||||
payload: {...filtersWithPage, ...filtersForAPI}
|
||||
}
|
||||
},
|
||||
changeOrderBy(filtersWithOrderBy) {
|
||||
const filtersForAPI = searchTicketsUtils.prepareFiltersForAPI(filtersWithOrderBy);
|
||||
const currentPath = window.location.pathname;
|
||||
const urlQuery = searchTicketsUtils.getFiltersForURL({
|
||||
filters: filtersForAPI,
|
||||
shouldRemoveCustomParam: false,
|
||||
shouldRemoveUseInitialValuesParam: true
|
||||
});
|
||||
urlQuery && history.push(`${currentPath}${urlQuery}`);
|
||||
|
||||
return {
|
||||
type: 'SEARCH_FILTERS_CHANGE_ORDER_BY',
|
||||
payload: {...filtersWithOrderBy, ...filtersForAPI}
|
||||
}
|
||||
},
|
||||
};
|
@ -2,6 +2,7 @@ import _ from 'lodash';
|
||||
|
||||
import API from 'lib-app/api-call';
|
||||
import AdminDataActions from 'actions/admin-data-actions';
|
||||
import ConfigActions from 'actions/config-actions';
|
||||
import sessionStore from 'lib-app/session-store';
|
||||
import store from 'app/store';
|
||||
|
||||
@ -16,11 +17,14 @@ export default {
|
||||
path: '/user/login',
|
||||
data: _.extend(loginData, {remember: loginData.remember * 1})
|
||||
}).then((result) => {
|
||||
store.dispatch(this.getUserData(result.data.userId, result.data.token, result.data.staff)).then(() => {
|
||||
if(result.data.staff) {
|
||||
store.dispatch(AdminDataActions.retrieveCustomResponses());
|
||||
}
|
||||
});
|
||||
store.dispatch(this.getUserData(result.data.userId, result.data.token, result.data.staff))
|
||||
.then(() => store.dispatch(ConfigActions.updateData()))
|
||||
.then(() => {
|
||||
if(result.data.staff) {
|
||||
store.dispatch(AdminDataActions.retrieveCustomResponses());
|
||||
store.dispatch(AdminDataActions.retrieveStaffMembers());
|
||||
}
|
||||
});
|
||||
|
||||
resolve(result);
|
||||
}).catch((result) => {
|
||||
@ -50,12 +54,12 @@ export default {
|
||||
data: {
|
||||
userId: rememberData.userId,
|
||||
rememberToken: rememberData.token,
|
||||
staff: rememberData.isStaff,
|
||||
remember: 1,
|
||||
isAutomatic: 1
|
||||
}
|
||||
}).then((result) => {
|
||||
store.dispatch(this.getUserData(result.data.userId, result.data.token));
|
||||
|
||||
store.dispatch(this.getUserData(result.data.userId, result.data.token, result.data.staff));
|
||||
|
||||
return result;
|
||||
})
|
||||
};
|
||||
|
@ -12,7 +12,14 @@ const TicketList = requireUnit('app-components/ticket-list', {
|
||||
'core-components/button': Button,
|
||||
'core-components/tooltip': Tooltip,
|
||||
'app-components/department-dropdown': DepartmentDropdown,
|
||||
'lib-app/i18n': i18n
|
||||
'lib-app/i18n': i18n,
|
||||
'react-redux': {
|
||||
connect: function() {
|
||||
return function(param) {
|
||||
return param;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
describe('TicketList component', function () {
|
||||
@ -28,11 +35,11 @@ describe('TicketList component', function () {
|
||||
id: 1,
|
||||
name: 'Sales Support'
|
||||
},
|
||||
priority: 'low',
|
||||
author: {
|
||||
id: 3,
|
||||
name: 'Francisco Villegas'
|
||||
}
|
||||
},
|
||||
tags: []
|
||||
};
|
||||
let list = _.range(5).map(() => ticket);
|
||||
|
||||
@ -50,7 +57,7 @@ describe('TicketList component', function () {
|
||||
|
||||
function renderTicketList(props = {}) {
|
||||
ticketList = TestUtils.renderIntoDocument(
|
||||
<TicketList tickets={tickets} {...props}/>
|
||||
<TicketList tickets={tickets} {...props}></TicketList>
|
||||
);
|
||||
|
||||
table = TestUtils.scryRenderedComponentsWithType(ticketList, Table);
|
||||
@ -61,28 +68,28 @@ describe('TicketList component', function () {
|
||||
renderTicketList();
|
||||
expect(table[0].props.loading).to.equal(false);
|
||||
expect(table[0].props.pageSize).to.equal(10);
|
||||
expect(table[0].props.headers).to.deep.equal([
|
||||
expect(table[0].props.headers[0]).to.deep.equal(
|
||||
{
|
||||
key: 'number',
|
||||
value: i18n('NUMBER'),
|
||||
className: 'ticket-list__number col-md-1'
|
||||
},
|
||||
});
|
||||
expect(table[0].props.headers[1]).to.deep.equal(
|
||||
{
|
||||
key: 'title',
|
||||
value: i18n('TITLE'),
|
||||
className: 'ticket-list__title col-md-6'
|
||||
},
|
||||
});
|
||||
expect(table[0].props.headers[2]).to.deep.equal(
|
||||
{
|
||||
key: 'department',
|
||||
value: i18n('DEPARTMENT'),
|
||||
className: 'ticket-list__department col-md-3'
|
||||
},
|
||||
{
|
||||
key: 'date',
|
||||
value: i18n('DATE'),
|
||||
className: 'ticket-list__date col-md-2'
|
||||
}
|
||||
]);
|
||||
});
|
||||
expect(table[0].props.headers[3].key).to.equal('date');
|
||||
expect(table[0].props.headers[3].value.props.children[0]).to.equal(i18n('DATE'));
|
||||
expect(table[0].props.headers[3].value.props.children[1]).to.equal(null);
|
||||
expect(table[0].props.headers[3].className).to.equal('ticket-list__date col-md-2');
|
||||
});
|
||||
|
||||
it('should pass loading to Table', function () {
|
||||
@ -90,31 +97,6 @@ describe('TicketList component', function () {
|
||||
expect(table[0].props.loading).to.equal(true);
|
||||
});
|
||||
|
||||
it('should pass correct compare function to Table', function () {
|
||||
let minCompare = table[0].props.comp;
|
||||
|
||||
let row1 = {
|
||||
closed: false,
|
||||
unread: false,
|
||||
date: '20160405'
|
||||
};
|
||||
let row2 = {
|
||||
closed: false,
|
||||
unread: false,
|
||||
date: '20160406'
|
||||
};
|
||||
expect(minCompare(row1, row2)).to.equal(1);
|
||||
|
||||
row1.unread = true;
|
||||
expect(minCompare(row1, row2)).to.equal(-1);
|
||||
|
||||
row2.unread = true;
|
||||
expect(minCompare(row1, row2)).to.equal(1);
|
||||
|
||||
row2.date = '20160401';
|
||||
expect(minCompare(row1, row2)).to.equal(-1);
|
||||
});
|
||||
|
||||
describe('when using secondary type', function () {
|
||||
beforeEach(function () {
|
||||
renderTicketList({
|
||||
@ -127,38 +109,34 @@ describe('TicketList component', function () {
|
||||
});
|
||||
|
||||
it('should pass correct props to Table', function () {
|
||||
expect(table[0].props.headers).to.deep.equal([
|
||||
expect(table[0].props.headers[0]).to.deep.equal(
|
||||
{
|
||||
key: 'number',
|
||||
value: i18n('NUMBER'),
|
||||
className: 'ticket-list__number col-md-1'
|
||||
},
|
||||
});
|
||||
expect(table[0].props.headers[1]).to.deep.equal(
|
||||
{
|
||||
key: 'title',
|
||||
value: i18n('TITLE'),
|
||||
className: 'ticket-list__title col-md-4'
|
||||
},
|
||||
{
|
||||
key: 'priority',
|
||||
value: i18n('PRIORITY'),
|
||||
className: 'ticket-list__priority col-md-1'
|
||||
},
|
||||
});
|
||||
expect(table[0].props.headers[2]).to.deep.equal(
|
||||
{
|
||||
key: 'department',
|
||||
value: i18n('DEPARTMENT'),
|
||||
className: 'ticket-list__department col-md-2'
|
||||
},
|
||||
});
|
||||
expect(table[0].props.headers[3]).to.deep.equal(
|
||||
{
|
||||
key: 'author',
|
||||
value: i18n('AUTHOR'),
|
||||
className: 'ticket-list__author col-md-2'
|
||||
},
|
||||
{
|
||||
key: 'date',
|
||||
value: i18n('DATE'),
|
||||
className: 'ticket-list__date col-md-2'
|
||||
}
|
||||
]);
|
||||
});
|
||||
expect(table[0].props.headers[4].key).to.equal('date');
|
||||
expect(table[0].props.headers[4].value.props.children[0]).to.equal(i18n('DATE'));
|
||||
expect(table[0].props.headers[4].value.props.children[1]).to.equal(null);
|
||||
expect(table[0].props.headers[4].className).to.equal('ticket-list__date col-md-2');
|
||||
});
|
||||
|
||||
it('should pass correct props to dropdown', function () {
|
||||
|
@ -18,10 +18,12 @@ class ActivityRow extends React.Component {
|
||||
'CREATE_TICKET',
|
||||
'RE_OPEN',
|
||||
'DEPARTMENT_CHANGED',
|
||||
'PRIORITY_CHANGED',
|
||||
'EDIT_TITLE',
|
||||
'EDIT_COMMENT',
|
||||
|
||||
'EDIT_SETTINGS',
|
||||
'SIGNUP',
|
||||
'INVITE',
|
||||
'ADD_TOPIC',
|
||||
'ADD_ARTICLE',
|
||||
'DELETE_TOPIC',
|
||||
@ -56,16 +58,16 @@ class ActivityRow extends React.Component {
|
||||
'CREATE_TICKET',
|
||||
'RE_OPEN',
|
||||
'DEPARTMENT_CHANGED',
|
||||
'PRIORITY_CHANGED'
|
||||
'COMMENT_EDITED',
|
||||
'EDIT_TITLE',
|
||||
'EDIT_COMMENT',
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="activity-row">
|
||||
<Icon {...this.getIconProps()} className="activity-row__icon"/>
|
||||
<span>
|
||||
<Link className="activity-row__name-link" to={this.getNameLinkDestination()}>
|
||||
{this.props.author.name}
|
||||
</Link>
|
||||
{this.renderAuthorName()}
|
||||
</span>
|
||||
<span className="activity-row__message"> {i18n('ACTIVITY_' + this.props.type)} </span>
|
||||
{_.includes(ticketRelatedTypes, this.props.type) ? this.renderTicketNumber() : this.props.to}
|
||||
@ -74,6 +76,18 @@ class ActivityRow extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderAuthorName() {
|
||||
let name = this.props.author.name;
|
||||
|
||||
if (this.props.author.id) {
|
||||
name = <Link className="activity-row__name-link" to={this.getNameLinkDestination()}>
|
||||
{this.props.author.name}
|
||||
</Link>;
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
renderTicketNumber() {
|
||||
let ticketNumber = (this.props.mode === 'staff') ? this.props.ticketNumber : this.props.to;
|
||||
|
||||
@ -99,10 +113,12 @@ class ActivityRow extends React.Component {
|
||||
'CREATE_TICKET': 'ticket',
|
||||
'RE_OPEN': 'unlock-alt',
|
||||
'DEPARTMENT_CHANGED': 'exchange',
|
||||
'PRIORITY_CHANGED': 'exclamation',
|
||||
'EDIT_TITLE': 'edit',
|
||||
'EDIT_COMMENT': 'edit',
|
||||
|
||||
'EDIT_SETTINGS': 'wrench',
|
||||
'SIGNUP': 'user-plus',
|
||||
'INVITE': 'user-plus',
|
||||
'ADD_TOPIC': 'book',
|
||||
'ADD_ARTICLE': 'book',
|
||||
'DELETE_TOPIC': 'book',
|
||||
|
@ -5,8 +5,7 @@ import ModalContainer from 'app-components/modal-container';
|
||||
|
||||
import Button from 'core-components/button';
|
||||
import Input from 'core-components/input';
|
||||
import Icon from 'core-components/icon';
|
||||
|
||||
import Loading from 'core-components/loading'
|
||||
|
||||
class AreYouSure extends React.Component {
|
||||
static propTypes = {
|
||||
@ -24,13 +23,14 @@ class AreYouSure extends React.Component {
|
||||
};
|
||||
|
||||
state = {
|
||||
loading: false,
|
||||
password: ''
|
||||
};
|
||||
|
||||
static openModal(description, onYes, type = 'default') {
|
||||
ModalContainer.openModal(
|
||||
<AreYouSure description={description} onYes={onYes} type={type}/>,
|
||||
true
|
||||
<AreYouSure description={description} onYes={onYes} type={type} />,
|
||||
{noPadding: true, closeButton: {showCloseButton: true, whiteColor: true}}
|
||||
);
|
||||
}
|
||||
|
||||
@ -39,28 +39,34 @@ class AreYouSure extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading } = this.state;
|
||||
const { description, type } = this.props;
|
||||
|
||||
return (
|
||||
<div className="are-you-sure" role="dialog" aria-labelledby="are-you-sure__header" aria-describedby="are-you-sure__description">
|
||||
<div className="are-you-sure__header" id="are-you-sure__header">
|
||||
{i18n('ARE_YOU_SURE')}
|
||||
</div>
|
||||
<span className="are-you-sure__close-icon" onClick={this.onNo.bind(this)}>
|
||||
<Icon name="times" size="2x"/>
|
||||
</span>
|
||||
<div className="are-you-sure__description" id="are-you-sure__description">
|
||||
{this.props.description || (this.props.type === 'secure' && i18n('PLEASE_CONFIRM_PASSWORD'))}
|
||||
{description || (type === 'secure' && i18n('PLEASE_CONFIRM_PASSWORD'))}
|
||||
</div>
|
||||
{(this.props.type === 'secure') ? this.renderPassword() : null}
|
||||
{(type === 'secure') ? this.renderPassword() : null}
|
||||
<span className="separator" />
|
||||
<div className="are-you-sure__buttons">
|
||||
<div className="are-you-sure__no-button">
|
||||
<Button type="link" size="auto" onClick={this.onNo.bind(this)} tabIndex="2">
|
||||
<Button disabled={loading} type="link" size="auto" onClick={this.onNo.bind(this)} tabIndex="2">
|
||||
{i18n('CANCEL')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="are-you-sure__yes-button">
|
||||
<Button type="secondary" size="small" onClick={this.onYes.bind(this)} ref="yesButton" tabIndex="2">
|
||||
{i18n('YES')}
|
||||
<Button
|
||||
type="secondary"
|
||||
size="small"
|
||||
onClick={this.onYes.bind(this)}
|
||||
ref="yesButton"
|
||||
tabIndex="2"
|
||||
disabled={loading}>
|
||||
{loading ? <Loading /> : i18n('YES')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -69,8 +75,20 @@ class AreYouSure extends React.Component {
|
||||
}
|
||||
|
||||
renderPassword() {
|
||||
const { password, loading } = this.state;
|
||||
|
||||
return (
|
||||
<Input className="are-you-sure__password" password placeholder="password" name="password" ref="password" size="medium" value={this.state.password} onChange={this.onPasswordChange.bind(this)} onKeyDown={this.onInputKeyDown.bind(this)}/>
|
||||
<Input
|
||||
className="are-you-sure__password"
|
||||
password
|
||||
placeholder="password"
|
||||
name="password"
|
||||
ref="password"
|
||||
size="medium"
|
||||
value={password}
|
||||
onChange={this.onPasswordChange.bind(this)}
|
||||
onKeyDown={this.onInputKeyDown.bind(this)}
|
||||
disabled={loading} />
|
||||
);
|
||||
}
|
||||
|
||||
@ -87,28 +105,59 @@ class AreYouSure extends React.Component {
|
||||
}
|
||||
|
||||
onYes() {
|
||||
if (this.props.type === 'secure' && !this.state.password) {
|
||||
const { password } = this.state;
|
||||
const { type, onYes } = this.props;
|
||||
|
||||
if(type === 'secure' && !password) {
|
||||
this.refs.password.focus()
|
||||
}
|
||||
|
||||
if (this.props.type === 'default' || this.state.password) {
|
||||
this.closeModal();
|
||||
|
||||
if (this.props.onYes) {
|
||||
this.props.onYes(this.state.password);
|
||||
if(type === 'default' || password) {
|
||||
if(onYes) {
|
||||
const result = onYes(password);
|
||||
if(this.isPromise(result)) {
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
result
|
||||
.then(() => {
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
this.closeModal();
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
this.closeModal();
|
||||
})
|
||||
} else {
|
||||
this.closeModal();
|
||||
}
|
||||
} else {
|
||||
this.closeModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isPromise(object) {
|
||||
if(Promise && Promise.resolve) {
|
||||
return Promise.resolve(object) == object;
|
||||
} else {
|
||||
throw "Promise not supported in your environment"
|
||||
}
|
||||
}
|
||||
|
||||
onNo() {
|
||||
this.closeModal();
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
if (this.context.closeModal) {
|
||||
this.context.closeModal();
|
||||
}
|
||||
const { closeModal } = this.context;
|
||||
|
||||
closeModal && closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
export default AreYouSure;
|
||||
export default AreYouSure;
|
||||
|
@ -24,12 +24,16 @@
|
||||
&__buttons {
|
||||
margin-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__yes-button,
|
||||
&__no-button {
|
||||
display: inline-block;
|
||||
display: block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
|
@ -32,11 +32,11 @@ class ArticleAddModal extends React.Component {
|
||||
<Form onSubmit={this.onAddNewArticleFormSubmit.bind(this)} loading={this.state.loading}>
|
||||
<FormField name="title" label={i18n('TITLE')} field="input" fieldProps={{size: 'large'}} validation="TITLE" required/>
|
||||
<FormField name="content" label={i18n('CONTENT')} field="textarea" validation="TEXT_AREA" required fieldProps={{allowImages: this.props.allowAttachments}}/>
|
||||
<SubmitButton type="secondary">{i18n('ADD_ARTICLE')}</SubmitButton>
|
||||
<Button className="article-add-modal__cancel-button" type="link" onClick={(event) => {
|
||||
event.preventDefault();
|
||||
ModalContainer.closeModal();
|
||||
}}>{i18n('CANCEL')}</Button>
|
||||
<SubmitButton className="article-add-modal__submit-button" type="secondary">{i18n('ADD_ARTICLE')}</SubmitButton>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
@ -2,7 +2,11 @@
|
||||
width: 800px;
|
||||
|
||||
&__cancel-button {
|
||||
float: right;
|
||||
float: left;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
&__submit-button {
|
||||
float: right;
|
||||
}
|
||||
}
|
@ -36,11 +36,13 @@ class ArticlesList extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
if(this.props.errored) {
|
||||
const { errored, loading } = this.props;
|
||||
|
||||
if(errored) {
|
||||
return <Message type="error">{i18n('ERROR_RETRIEVING_ARTICLES')}</Message>;
|
||||
}
|
||||
|
||||
return (this.props.loading) ? <Loading /> : this.renderContent();
|
||||
return loading ? <Loading /> : this.renderContent();
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
@ -53,17 +55,19 @@ class ArticlesList extends React.Component {
|
||||
}
|
||||
|
||||
renderTopics() {
|
||||
const { topics, editable, articlePath } = this.props;
|
||||
|
||||
return (
|
||||
<div className="articles-list__topics">
|
||||
{this.props.topics.map((topic, index) => {
|
||||
{topics.map((topic, index) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
<TopicViewer
|
||||
{...topic}
|
||||
id={topic.id * 1}
|
||||
editable={this.props.editable}
|
||||
editable={editable}
|
||||
onChange={this.retrieveArticles.bind(this)}
|
||||
articlePath={this.props.articlePath} />
|
||||
articlePath={articlePath} />
|
||||
<span className="separator" />
|
||||
</div>
|
||||
);
|
||||
@ -76,7 +80,7 @@ class ArticlesList extends React.Component {
|
||||
return (
|
||||
<div className="articles-list__add-topic-button">
|
||||
<Button onClick={() => ModalContainer.openModal(<TopicEditModal addForm onChange={this.retrieveArticles.bind(this)} />)} type="secondary" className="articles-list__add">
|
||||
<Icon name="plus-circle" size="2x" className="articles-list__add-icon"/> {i18n('ADD_TOPIC')}
|
||||
<Icon name="plus" className="articles-list__add-icon" /> {i18n('ADD_TOPIC')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@ -88,9 +92,11 @@ class ArticlesList extends React.Component {
|
||||
}
|
||||
|
||||
export default connect((store) => {
|
||||
const { topics, errored, loading } = store.articles;
|
||||
|
||||
return {
|
||||
topics: store.articles.topics,
|
||||
errored: store.articles.errored,
|
||||
loading: store.articles.loading
|
||||
topics: topics.map((topic) => {return {...topic, private: topic.private === "1"}}),
|
||||
errored,
|
||||
loading
|
||||
};
|
||||
})(ArticlesList);
|
||||
|
@ -1,14 +1,14 @@
|
||||
@import "../scss/vars";
|
||||
|
||||
.articles-list {
|
||||
|
||||
&__add {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__add-icon {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
margin-top: -4px;
|
||||
&__add-topic-button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,4 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import DropDown from 'core-components/drop-down';
|
||||
import Icon from 'core-components/icon';
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import keyCode from 'keycode';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import store from 'app/store';
|
||||
import ModalActions from 'actions/modal-actions';
|
||||
@ -9,11 +8,14 @@ import Modal from 'core-components/modal';
|
||||
|
||||
class ModalContainer extends React.Component {
|
||||
|
||||
static openModal(content, noPadding) {
|
||||
static openModal(
|
||||
content,
|
||||
options={noPadding: false, outsideClick: false, closeButton: {showCloseButton: false, whiteColor: false}}
|
||||
) {
|
||||
store.dispatch(
|
||||
ModalActions.openModal({
|
||||
content,
|
||||
noPadding
|
||||
options
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -49,8 +51,16 @@ class ModalContainer extends React.Component {
|
||||
}
|
||||
|
||||
renderModal() {
|
||||
const { content, options } = this.props.modal;
|
||||
const { noPadding, outsideClick, closeButton } = options;
|
||||
|
||||
return (
|
||||
<Modal content={this.props.modal.content} noPadding={this.props.modal.noPadding}/>
|
||||
<Modal
|
||||
content={content}
|
||||
noPadding={noPadding}
|
||||
outsideClick={outsideClick}
|
||||
onOutsideClick={this.closeModal.bind(this)}
|
||||
closeButton={closeButton} />
|
||||
);
|
||||
}
|
||||
|
||||
@ -69,4 +79,4 @@ export default connect((store) => {
|
||||
return {
|
||||
modal: store.modal
|
||||
};
|
||||
})(ModalContainer);
|
||||
})(ModalContainer);
|
||||
|
@ -25,9 +25,14 @@ class PasswordRecovery extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { renderLogo, formProps, onBackToLoginClick } = this.props;
|
||||
const {
|
||||
renderLogo,
|
||||
formProps,
|
||||
onBackToLoginClick,
|
||||
style
|
||||
} = this.props;
|
||||
return (
|
||||
<Widget {...this.props} className={this.getClass()} title={!renderLogo && i18n('RECOVER_PASSWORD')}>
|
||||
<Widget style={style} className={this.getClass()} title={!renderLogo ? i18n('RECOVER_PASSWORD') : ''}>
|
||||
{this.renderLogo()}
|
||||
<Form {...formProps}>
|
||||
<div className="password-recovery__inputs">
|
||||
|
@ -24,3 +24,15 @@
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 320px) {
|
||||
.widget {
|
||||
margin-left: -12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.password-recovery__content {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
@ -61,4 +61,28 @@
|
||||
&__pagination {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 415px) {
|
||||
.people-list {
|
||||
&__item {
|
||||
height: unset;
|
||||
padding: unset;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
|
||||
&-profile-pic-wrapper {
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
&-block {
|
||||
width: unset;
|
||||
display: unset;
|
||||
padding: 15px 10px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
48
client/src/app-components/popup-message.js
Normal file
48
client/src/app-components/popup-message.js
Normal file
@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
|
||||
import ModalContainer from 'app-components/modal-container';
|
||||
|
||||
import Button from 'core-components/button';
|
||||
import Message from 'core-components/message';
|
||||
|
||||
|
||||
class PopupMessage extends React.Component {
|
||||
static propTypes = Message.propTypes;
|
||||
|
||||
static contextTypes = {
|
||||
closeModal: React.PropTypes.func
|
||||
};
|
||||
|
||||
static open(props) {
|
||||
ModalContainer.openModal(
|
||||
<PopupMessage {...props} />,
|
||||
{noPadding: true, outsideClick: true}
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.refs.closeButton && this.refs.closeButton.focus();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="popup-message">
|
||||
<Message {...this.props} className="popup-message__message" />
|
||||
<Button
|
||||
className="popup-message__close-button"
|
||||
iconName="times"
|
||||
type="clean"
|
||||
ref="closeButton"
|
||||
onClick={this.closeModal.bind(this)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
const { closeModal } = this.context;
|
||||
|
||||
closeModal && closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
export default PopupMessage;
|
17
client/src/app-components/popup-message.scss
Normal file
17
client/src/app-components/popup-message.scss
Normal file
@ -0,0 +1,17 @@
|
||||
@import "../scss/vars";
|
||||
|
||||
.popup-message {
|
||||
min-width: 500px;
|
||||
position: relative;
|
||||
|
||||
&__close-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
color: $dark-grey;
|
||||
&:focus {
|
||||
outline: none;
|
||||
color: $grey;
|
||||
}
|
||||
}
|
||||
}
|
42
client/src/app-components/stat-card.js
Normal file
42
client/src/app-components/stat-card.js
Normal file
@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
|
||||
import Tooltip from 'core-components/tooltip';
|
||||
|
||||
class StatCard extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
label: React.PropTypes.string,
|
||||
description: React.PropTypes.string,
|
||||
value: React.PropTypes.number,
|
||||
isPercentage: React.PropTypes.bool
|
||||
};
|
||||
|
||||
state = {
|
||||
showTooltip: false
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
label,
|
||||
description,
|
||||
value,
|
||||
isPercentage
|
||||
} = this.props;
|
||||
|
||||
const displayValue = isNaN(value) ? "-" : (isPercentage ? value.toFixed(2) : value);
|
||||
return (
|
||||
<Tooltip content={description} show={this.state.showTooltip} >
|
||||
<div className="stat-card" onMouseEnter={() => this.setState({showTooltip: true})} onMouseLeave={() => this.setState({showTooltip: false})}>
|
||||
<div className="stat-card__wrapper">
|
||||
{label}
|
||||
<div className="stat-card__container">
|
||||
{displayValue}{isPercentage && !isNaN(value) ? "%" : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default StatCard;
|
22
client/src/app-components/stat-card.scss
Normal file
22
client/src/app-components/stat-card.scss
Normal file
@ -0,0 +1,22 @@
|
||||
@import "../scss/vars";
|
||||
|
||||
.stat-card {
|
||||
margin: 0 4px;
|
||||
|
||||
&__wrapper {
|
||||
padding: 10px;
|
||||
min-width: 160px;
|
||||
margin: 8px auto;
|
||||
transition: 0.3s;
|
||||
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
|
||||
text-align: center;
|
||||
&:hover {
|
||||
box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
|
||||
}
|
||||
}
|
||||
|
||||
&__container {
|
||||
font-size: $font-size--lg;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Line} from 'react-chartjs-2';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
|
||||
class StatsChart extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
strokes: React.PropTypes.arrayOf(React.PropTypes.shape({
|
||||
name: React.PropTypes.string,
|
||||
values: React.PropTypes.arrayOf(React.PropTypes.shape({
|
||||
date: React.PropTypes.string,
|
||||
value: React.PropTypes.number
|
||||
}))
|
||||
})),
|
||||
period: React.PropTypes.number
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Line data={this.getChartData()} options={this.getChartOptions()} width={800} height={400} redraw/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getChartData() {
|
||||
let labels = this.getLabels();
|
||||
|
||||
let color = {
|
||||
'CLOSE': 'rgba(150, 20, 20, 0.6)',
|
||||
'CREATE_TICKET': 'rgba(20, 150, 20, 0.6)',
|
||||
'SIGNUP': 'rgba(20, 20, 150, 0.6)',
|
||||
'COMMENT': 'rgba(20, 200, 200, 0.6)',
|
||||
'ASSIGN': 'rgba(20, 150, 20, 0.6)'
|
||||
};
|
||||
|
||||
let strokes = this.props.strokes.slice();
|
||||
let datasets = strokes.map((stroke, index) => {
|
||||
return {
|
||||
label: i18n('CHART_' + stroke.name),
|
||||
data: stroke.values.map((val) => val.value),
|
||||
fill: false,
|
||||
borderWidth: this.getBorderWidth(),
|
||||
borderColor: color[stroke.name],
|
||||
pointBorderColor: color[stroke.name],
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 3,
|
||||
lineTension: 0.2,
|
||||
pointHoverBackgroundColor: color[stroke.name],
|
||||
hitRadius: this.hitRadius(index)
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
labels: labels,
|
||||
datasets: datasets
|
||||
};
|
||||
}
|
||||
|
||||
getBorderWidth() {
|
||||
return (this.props.period <= 90) ? 3 : 2;
|
||||
}
|
||||
|
||||
getLabels() {
|
||||
let months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
let labels = [];
|
||||
|
||||
if (!this.props.strokes.length) {
|
||||
labels = Array.from('x'.repeat(this.props.period));
|
||||
}
|
||||
else {
|
||||
labels = this.props.strokes[0].values.map((item) => {
|
||||
let idx = item.date.slice(4, 6) - 1;
|
||||
|
||||
return item.date.slice(6, 8) + ' ' + months[idx];
|
||||
});
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
hitRadius(index) {
|
||||
if (this.props.period <= 7) return 20;
|
||||
if (this.props.period <= 30) return 15;
|
||||
if (this.props.period <= 90) return 10;
|
||||
return 3;
|
||||
}
|
||||
|
||||
getChartOptions() {
|
||||
return {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default StatsChart;
|
@ -1,186 +0,0 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import API from 'lib-app/api-call';
|
||||
|
||||
import DropDown from 'core-components/drop-down';
|
||||
import ToggleList from 'core-components/toggle-list';
|
||||
|
||||
import StatsChart from 'app-components/stats-chart';
|
||||
|
||||
const generalStrokes = ['CREATE_TICKET', 'CLOSE', 'SIGNUP', 'COMMENT'];
|
||||
const staffStrokes = ['ASSIGN', 'CLOSE'];
|
||||
const ID = {
|
||||
'CREATE_TICKET': 0,
|
||||
'ASSIGN': 0,
|
||||
'CLOSE': 1,
|
||||
'SIGNUP': 2,
|
||||
'COMMENT': 3
|
||||
};
|
||||
|
||||
const statsPeriod = {
|
||||
'WEEK': 7,
|
||||
'MONTH': 30
|
||||
};
|
||||
|
||||
class Stats extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
type: React.PropTypes.string,
|
||||
staffId: React.PropTypes.number
|
||||
};
|
||||
|
||||
state = {
|
||||
stats: this.getDefaultStats(),
|
||||
strokes: this.getStrokes().map((name) => {
|
||||
return {
|
||||
name: name,
|
||||
values: []
|
||||
}
|
||||
}),
|
||||
showed: [0],
|
||||
period: 0
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.retrieve('WEEK');
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={this.getClass()}>
|
||||
<DropDown {...this.getDropDownProps()}/>
|
||||
<ToggleList {...this.getToggleListProps()} />
|
||||
<StatsChart {...this.getStatsChartProps()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getClass() {
|
||||
let classes = {
|
||||
'stats': true,
|
||||
'stats_staff': this.props.type === 'staff'
|
||||
};
|
||||
|
||||
return classNames(classes);
|
||||
}
|
||||
|
||||
getToggleListProps() {
|
||||
return {
|
||||
values: this.state.showed,
|
||||
className: 'stats__toggle-list',
|
||||
onChange: this.onToggleListChange.bind(this),
|
||||
type: this.props.type === 'general' ? 'default' : 'small',
|
||||
items: this.getStrokes().map((name) => {
|
||||
return {
|
||||
className: 'stats__toggle-list_' + name,
|
||||
content: (
|
||||
<div className="stats__toggle-list-item">
|
||||
<div className="stats__toggle-list-item-value">{this.state.stats[name]}</div>
|
||||
<div className="stats__toggle-list-item-name">{i18n('CHART_' + name)}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
onToggleListChange(event) {
|
||||
this.setState({
|
||||
showed: event.target.value
|
||||
});
|
||||
}
|
||||
|
||||
getDropDownProps() {
|
||||
return {
|
||||
items: Object.keys(statsPeriod).map(key => 'LAST_' + statsPeriod[key] + '_DAYS').map((name) => {
|
||||
return {
|
||||
content: i18n(name),
|
||||
icon: ''
|
||||
};
|
||||
}),
|
||||
onChange: this.onDropDownChange.bind(this),
|
||||
className: 'stats__dropdown'
|
||||
}
|
||||
}
|
||||
|
||||
onDropDownChange(event) {
|
||||
this.retrieve(Object.keys(statsPeriod)[event.index]);
|
||||
}
|
||||
|
||||
getStatsChartProps() {
|
||||
let showed = this.getShowedArray();
|
||||
|
||||
return {
|
||||
period: this.state.period,
|
||||
strokes: _.filter(this.state.strokes, (s, i) => showed[i])
|
||||
};
|
||||
}
|
||||
|
||||
retrieve(periodName) {
|
||||
API.call({
|
||||
path: '/system/get-stats',
|
||||
data: this.getApiCallData(periodName)
|
||||
}).then(this.onRetrieveSuccess.bind(this));
|
||||
}
|
||||
|
||||
onRetrieveSuccess(result) {
|
||||
let newStats = this.getDefaultStats();
|
||||
|
||||
let newStrokes = this.getStrokes().map((name) => {
|
||||
return {
|
||||
name: name,
|
||||
values: []
|
||||
};
|
||||
});
|
||||
|
||||
let realPeriod = result.data.length / this.getStrokes().length;
|
||||
|
||||
result.data.reverse().map((item) => {
|
||||
newStats[item.type] += item.value * 1;
|
||||
|
||||
newStrokes[ ID[item.type] ].values.push({
|
||||
date: item.date,
|
||||
value: item.value * 1
|
||||
});
|
||||
});
|
||||
|
||||
this.setState({stats: newStats, strokes: newStrokes, period: realPeriod});
|
||||
}
|
||||
|
||||
getShowedArray() {
|
||||
let showed = this.getStrokes().map(() => false);
|
||||
|
||||
for (let i = 0; i < showed.length; i++) {
|
||||
showed[this.state.showed[i]] = true;
|
||||
}
|
||||
|
||||
return showed;
|
||||
}
|
||||
|
||||
getStrokes() {
|
||||
return this.props.type === 'general' ? generalStrokes : staffStrokes;
|
||||
}
|
||||
|
||||
getDefaultStats() {
|
||||
return this.props.type === 'general' ?
|
||||
{
|
||||
'CREATE_TICKET': 0,
|
||||
'CLOSE': 0,
|
||||
'SIGNUP': 0,
|
||||
'COMMENT': 0
|
||||
} :
|
||||
{
|
||||
'ASSIGN': 0,
|
||||
'CLOSE': 0
|
||||
};
|
||||
}
|
||||
|
||||
getApiCallData(periodName) {
|
||||
return this.props.type === 'general' ? {period: periodName} : {period: periodName, staffId: this.props.staffId};
|
||||
}
|
||||
}
|
||||
|
||||
export default Stats;
|
@ -1,76 +0,0 @@
|
||||
@import '../scss/vars';
|
||||
|
||||
.stats {
|
||||
|
||||
&__dropdown {
|
||||
margin-left: auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
&__toggle-list {
|
||||
margin-bottom: 20px;
|
||||
user-select: none;
|
||||
|
||||
&-item {
|
||||
|
||||
&-value {
|
||||
font-size: $font-size--lg;
|
||||
line-height: 80px;
|
||||
}
|
||||
|
||||
&-name {
|
||||
font-size: $font-size--md;
|
||||
line-height: 20px;
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
&_CREATE_TICKET.toggle-list__selected {
|
||||
box-shadow: inset 0 -5px 0px 0px rgba(20, 150, 20, 0.6);
|
||||
}
|
||||
|
||||
&_CLOSE.toggle-list__selected {
|
||||
box-shadow: inset 0 -5px 0px 0px rgba(150, 20, 20, 0.6);
|
||||
}
|
||||
|
||||
&_SIGNUP.toggle-list__selected {
|
||||
box-shadow: inset 0 -5px 0px 0px rgba(20, 20, 150, 0.6);
|
||||
}
|
||||
|
||||
&_COMMENT.toggle-list__selected {
|
||||
box-shadow: inset 0 -5px 0px 0px rgba(20, 200, 200, 0.6);
|
||||
}
|
||||
|
||||
&_ASSIGN.toggle-list__selected {
|
||||
box-shadow: inset 0 -5px 0px 0px rgba(20, 150, 20, 0.6);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&_staff {
|
||||
.stats__dropdown {
|
||||
margin-left: auto;
|
||||
margin-bottom: 20px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.stats__toggle-list {
|
||||
margin-bottom: 20px;
|
||||
float: right;
|
||||
|
||||
&-item {
|
||||
|
||||
&-value {
|
||||
font-size: $font-size--md;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
&-name {
|
||||
font-size: $font-size--sm;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import API from 'lib-app/api-call';
|
||||
@ -7,7 +8,13 @@ import API from 'lib-app/api-call';
|
||||
import DateTransformer from 'lib-core/date-transformer';
|
||||
import Icon from 'core-components/icon';
|
||||
import Tooltip from 'core-components/tooltip';
|
||||
import Button from 'core-components/button';
|
||||
import SubmitButton from 'core-components/submit-button';
|
||||
import Form from 'core-components/form';
|
||||
import FormField from 'core-components/form-field';
|
||||
|
||||
const VIEW_USER_PATH = "/admin/panel/users/view-user/";
|
||||
const VIEW_STAFF_PATH = "/admin/panel/staff/view-staff/";
|
||||
class TicketEvent extends React.Component {
|
||||
static propTypes = {
|
||||
type: React.PropTypes.oneOf([
|
||||
@ -17,12 +24,18 @@ class TicketEvent extends React.Component {
|
||||
'CLOSE',
|
||||
'RE_OPEN',
|
||||
'DEPARTMENT_CHANGED',
|
||||
'PRIORITY_CHANGED'
|
||||
]),
|
||||
author: React.PropTypes.object,
|
||||
content: React.PropTypes.string,
|
||||
date: React.PropTypes.string,
|
||||
private: React.PropTypes.string,
|
||||
edited: React.PropTypes.bool,
|
||||
edit: React.PropTypes.bool,
|
||||
onToggleEdit: React.PropTypes.func
|
||||
};
|
||||
|
||||
state = {
|
||||
content: this.props.content
|
||||
};
|
||||
|
||||
render() {
|
||||
@ -73,30 +86,87 @@ class TicketEvent extends React.Component {
|
||||
'CLOSE': this.renderClosed.bind(this),
|
||||
'RE_OPEN': this.renderReOpened.bind(this),
|
||||
'DEPARTMENT_CHANGED': this.renderDepartmentChange.bind(this),
|
||||
'PRIORITY_CHANGED': this.renderPriorityChange.bind(this)
|
||||
};
|
||||
|
||||
return renders[this.props.type]();
|
||||
}
|
||||
|
||||
renderComment() {
|
||||
const {
|
||||
author,
|
||||
date,
|
||||
edit,
|
||||
file
|
||||
} = this.props;
|
||||
const customFields = (author && author.customfields) || [];
|
||||
|
||||
return (
|
||||
<div className="ticket-event__comment">
|
||||
<span className="ticket-event__comment-pointer" />
|
||||
<div className="ticket-event__comment-author">
|
||||
<span className="ticket-event__comment-author-name">{this.props.author.name}</span>
|
||||
{this.renderCommentAuthor()}
|
||||
<span className="ticket-event__comment-badge-container">
|
||||
<span className="ticket-event__comment-badge">{i18n((this.props.author.staff) ? 'STAFF' : 'CUSTOMER')}</span>
|
||||
<span className="ticket-event__comment-badge">{i18n((author.staff) ? 'STAFF' : 'CUSTOMER')}</span>
|
||||
</span>
|
||||
{customFields.map(this.renderCustomFieldValue.bind(this))}
|
||||
{(this.props.private*1) ? this.renderPrivateBadge() : null}
|
||||
</div>
|
||||
<div className="ticket-event__comment-date">{DateTransformer.transformToString(this.props.date)}</div>
|
||||
<div className="ticket-event__comment-content" dangerouslySetInnerHTML={{__html: this.props.content}}></div>
|
||||
{this.renderFileRow(this.props.file)}
|
||||
<div className="ticket-event__comment-date">{DateTransformer.transformToString(date)}</div>
|
||||
{!edit ? this.renderContent() : this.renderEditField()}
|
||||
{this.renderFooter(file)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderCommentAuthor() {
|
||||
const {
|
||||
author,
|
||||
level
|
||||
} = this.props;
|
||||
const commentAutorClass = "ticket-event__comment-author-name";
|
||||
let commentAuthor;
|
||||
|
||||
if(level === "3") {
|
||||
commentAuthor = (
|
||||
<a className={commentAutorClass} href={((author.staff) ? VIEW_STAFF_PATH : VIEW_USER_PATH)+author.id}>
|
||||
{author.name}
|
||||
</a>
|
||||
);
|
||||
} else if(level && !author.staff) {
|
||||
commentAuthor = <a className={commentAutorClass} href={VIEW_USER_PATH+author.id}>{author.name}</a>;
|
||||
} else {
|
||||
commentAuthor = <span className={commentAutorClass}>{author.name}</span>;
|
||||
}
|
||||
|
||||
return commentAuthor;
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
return (
|
||||
<div className="ticket-event__comment-content ql-editor">
|
||||
<div dangerouslySetInnerHTML={{__html: this.props.content}}></div>
|
||||
{((this.props.author.id == this.props.userId && this.props.author.staff == this.props.userStaff) || this.props.userStaff) ? this.renderEditIcon() : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
renderEditIcon() {
|
||||
return (
|
||||
<div className="ticket-event__comment-content__edit" >
|
||||
<Icon name="pencil" onClick={this.props.onToggleEdit} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
renderEditField() {
|
||||
return (
|
||||
<Form loading={this.props.loading} values={{content:this.state.content}} onChange={(form) => {this.setState({content:form.content})}} onSubmit={this.props.onEdit}>
|
||||
<FormField name="content" required field="textarea" validation="TEXT_AREA" fieldProps={{allowImages: this.props.allowAttachments}}/>
|
||||
<div className="ticket-event__submit-edited-comment" >
|
||||
<SubmitButton type="secondary" >{i18n('SUBMIT')}</SubmitButton>
|
||||
<Button size="medium" onClick={this.props.onToggleEdit}>{i18n('CLOSE')}</Button>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
renderAssignment() {
|
||||
let assignedTo = this.props.content;
|
||||
let authorName = this.props.author.name;
|
||||
@ -164,17 +234,6 @@ class TicketEvent extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderPriorityChange() {
|
||||
return (
|
||||
<div className="ticket-event__circled">
|
||||
<span className="ticket-event__circled-author">{this.props.author.name}</span>
|
||||
<span className="ticket-event__circled-text"> {i18n('ACTIVITY_PRIORITY_CHANGED_THIS')}</span>
|
||||
<span className="ticket-event__circled-indication"> {this.props.content}</span>
|
||||
<span className="ticket-event__circled-date"> {i18n('DATE_PREFIX')} {DateTransformer.transformToString(this.props.date)}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderPrivateBadge() {
|
||||
return (
|
||||
<span className="ticket-event__comment-badge-container">
|
||||
@ -185,8 +244,9 @@ class TicketEvent extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderFileRow(file) {
|
||||
renderFooter(file) {
|
||||
let node = null;
|
||||
let edited = null;
|
||||
|
||||
if (file) {
|
||||
node = <span> {this.getFileLink(file)} <Icon name="paperclip" /> </span>;
|
||||
@ -194,11 +254,30 @@ class TicketEvent extends React.Component {
|
||||
node = i18n('NO_ATTACHMENT');
|
||||
}
|
||||
|
||||
if (this.props.edited && this.props.type === 'COMMENT') {
|
||||
edited = i18n('COMMENT_EDITED');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ticket-event__file">
|
||||
{node}
|
||||
<div className="ticket-event__items">
|
||||
<div className="ticket-event__edited">
|
||||
{edited}
|
||||
</div>
|
||||
<div className="ticket-event__file">
|
||||
{node}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
renderCustomFieldValue(customField) {
|
||||
return (
|
||||
<span className="ticket-event__comment-badge-container">
|
||||
<span className="ticket-event__comment-badge">
|
||||
{customField.customfield}: <span className="ticket-event__comment-badge-value">{customField.value}</span>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
getClass() {
|
||||
@ -209,7 +288,6 @@ class TicketEvent extends React.Component {
|
||||
'CLOSE': true,
|
||||
'RE_OPEN': true,
|
||||
'DEPARTMENT_CHANGED': true,
|
||||
'PRIORITY_CHANGED': true
|
||||
};
|
||||
const classes = {
|
||||
'row': true,
|
||||
@ -220,7 +298,6 @@ class TicketEvent extends React.Component {
|
||||
'ticket-event_close': this.props.type === 'CLOSE',
|
||||
'ticket-event_reopen': this.props.type === 'RE_OPEN',
|
||||
'ticket-event_department': this.props.type === 'DEPARTMENT_CHANGED',
|
||||
'ticket-event_priority': this.props.type === 'PRIORITY_CHANGED',
|
||||
'ticket-event_private': this.props.private*1,
|
||||
};
|
||||
|
||||
@ -235,7 +312,6 @@ class TicketEvent extends React.Component {
|
||||
'CLOSE': 'lock',
|
||||
'RE_OPEN': 'unlock-alt',
|
||||
'DEPARTMENT_CHANGED': 'exchange',
|
||||
'PRIORITY_CHANGED': 'exclamation'
|
||||
};
|
||||
const iconSize = {
|
||||
'COMMENT': '2x',
|
||||
@ -244,7 +320,6 @@ class TicketEvent extends React.Component {
|
||||
'CLOSE': 'lg',
|
||||
'RE_OPEN': 'lg',
|
||||
'DEPARTMENT_CHANGED': 'lg',
|
||||
'PRIORITY_CHANGED': 'lg'
|
||||
};
|
||||
|
||||
return {
|
||||
@ -262,4 +337,7 @@ class TicketEvent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default TicketEvent;
|
||||
export default connect((store) => {
|
||||
return { level: store.session.userLevel };
|
||||
})(TicketEvent);
|
||||
|
||||
|
@ -84,7 +84,7 @@
|
||||
border: 2px solid $light-grey;
|
||||
border-bottom: none;
|
||||
padding: 12px;
|
||||
font-size: 10.6px;
|
||||
font-size: 12.5px;
|
||||
font-family: helvetica;
|
||||
background-color: $light-grey;
|
||||
|
||||
@ -94,21 +94,56 @@
|
||||
background-color: white;
|
||||
border: 2px solid $very-light-grey;
|
||||
border-top: none;
|
||||
padding: 20px 10px;
|
||||
padding: 28px 10px;
|
||||
text-align: left;
|
||||
|
||||
position:relative;
|
||||
overflow-y: initial;
|
||||
|
||||
&:hover {
|
||||
.ticket-event__comment-content__edit {
|
||||
color: grey;
|
||||
cursor:pointer;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
max-width:100%;
|
||||
}
|
||||
|
||||
&__edit {
|
||||
position:absolute;
|
||||
top: 3px;
|
||||
right: 9px;
|
||||
align-self: right;
|
||||
color:white;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__submit-edited-comment {
|
||||
display:flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
&__items {
|
||||
background-color: $very-light-grey;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&__file {
|
||||
background-color: $very-light-grey;
|
||||
cursor: pointer;
|
||||
text-align: right;
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
&__edited{
|
||||
font-style: italic;
|
||||
}
|
||||
&__comment-badge-value {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
&_staff {
|
||||
@ -178,12 +213,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
&_priority {
|
||||
.ticket-event__icon {
|
||||
padding-left: 11px;
|
||||
padding-top: 5px;
|
||||
}
|
||||
}
|
||||
&_private {
|
||||
.ticket-event__comment-pointer {
|
||||
border-right-color: $light-yellow;
|
||||
|
@ -35,14 +35,6 @@ class TicketInfo extends React.Component {
|
||||
{(this.props.ticket.closed) ? 'closed' : 'open'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ticket-info__properties__priority">
|
||||
<span className="ticket-info__properties__label">
|
||||
{i18n('PRIORITY')}:
|
||||
</span>
|
||||
<span className={this.getPriorityClass()}>
|
||||
{this.props.ticket.priority}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ticket-info__properties__owner">
|
||||
<span className="ticket-info__properties__label">
|
||||
{i18n('OWNED')}:
|
||||
@ -80,15 +72,6 @@ class TicketInfo extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
getPriorityClass() {
|
||||
let priorityClasses = {
|
||||
'low': 'ticket-info__properties__badge-green',
|
||||
'medium': 'ticket-info__properties__badge-blue',
|
||||
'high': 'ticket-info__properties__badge-red'
|
||||
};
|
||||
|
||||
return priorityClasses[this.props.ticket.priority];
|
||||
}
|
||||
}
|
||||
|
||||
export default TicketInfo;
|
||||
|
@ -36,7 +36,6 @@
|
||||
|
||||
&__status,
|
||||
&__owner,
|
||||
&__priority,
|
||||
&__comments {
|
||||
display: inline-block;
|
||||
width: 50%;
|
||||
|
@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import {connect} from 'react-redux';
|
||||
import queryString from 'query-string';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import DateTransformer from 'lib-core/date-transformer';
|
||||
@ -9,8 +11,10 @@ import DepartmentDropdown from 'app-components/department-dropdown';
|
||||
import Table from 'core-components/table';
|
||||
import Button from 'core-components/button';
|
||||
import Tooltip from 'core-components/tooltip';
|
||||
import Icon from 'core-components/icon';
|
||||
import Checkbox from 'core-components/checkbox';
|
||||
import Tag from 'core-components/tag';
|
||||
import Icon from 'core-components/icon';
|
||||
import Message from 'core-components/message';
|
||||
|
||||
class TicketList extends React.Component {
|
||||
static propTypes = {
|
||||
@ -25,7 +29,8 @@ class TicketList extends React.Component {
|
||||
'secondary'
|
||||
]),
|
||||
closedTicketsShown: React.PropTypes.bool,
|
||||
onClosedTicketsShownChange: React.PropTypes.func
|
||||
onClosedTicketsShownChange: React.PropTypes.func,
|
||||
onDepartmentChange: React.PropTypes.func
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -43,11 +48,18 @@ class TicketList extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { type, showDepartmentDropdown, onClosedTicketsShownChange } = this.props;
|
||||
|
||||
return (
|
||||
<div className="ticket-list">
|
||||
<div className="ticket-list__filters">
|
||||
{(this.props.type === 'secondary' && this.props.showDepartmentDropdown) ? this.renderDepartmentsDropDown() : null}
|
||||
{this.props.onClosedTicketsShownChange ? this.renderFilterCheckbox() : null}
|
||||
{(type === 'primary') ? this.renderMessage() : null}
|
||||
{
|
||||
((type === 'secondary') && showDepartmentDropdown) ?
|
||||
this.renderDepartmentsDropDown() :
|
||||
null
|
||||
}
|
||||
{onClosedTicketsShownChange ? this.renderFilterCheckbox() : null}
|
||||
</div>
|
||||
<Table {...this.getTableProps()} />
|
||||
</div>
|
||||
@ -56,7 +68,15 @@ class TicketList extends React.Component {
|
||||
|
||||
|
||||
renderFilterCheckbox() {
|
||||
return <Checkbox className="ticket-list__checkbox" label={i18n("SHOW_CLOSED_TICKETS")} value={this.props.closedTicketsShown} onChange={this.props.onClosedTicketsShownChange} wrapInLabel/>
|
||||
return (
|
||||
<Checkbox
|
||||
className="ticket-list__checkbox"
|
||||
label={i18n("SHOW_CLOSED_TICKETS")}
|
||||
value={this.props.closedTicketsShown}
|
||||
onChange={this.props.onClosedTicketsShownChange}
|
||||
wrapInLabel
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderDepartmentsDropDown() {
|
||||
@ -67,28 +87,46 @@ class TicketList extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderMessage() {
|
||||
switch (queryString.parse(window.location.search)["message"]) {
|
||||
case 'success':
|
||||
return <Message className="create-ticket-form__message" type="success">{i18n('TICKET_SENT')}</Message>
|
||||
case 'fail':
|
||||
return <Message className="create-ticket-form__message" type="error">{i18n('TICKET_SENT_ERROR')}</Message>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
getDepartmentDropdownProps() {
|
||||
const { departments, onDepartmentChange } = this.props;
|
||||
|
||||
return {
|
||||
departments: this.getDepartments(),
|
||||
onChange: (event) => {
|
||||
const departmentId = event.index && departments[event.index - 1].id;
|
||||
|
||||
this.setState({
|
||||
selectedDepartment: event.index && this.props.departments[event.index - 1].id
|
||||
selectedDepartment: departmentId
|
||||
});
|
||||
|
||||
onDepartmentChange && onDepartmentChange(departmentId || null);
|
||||
},
|
||||
size: 'medium'
|
||||
};
|
||||
}
|
||||
|
||||
getTableProps() {
|
||||
const { loading, page, pages, onPageChange } = this.props;
|
||||
|
||||
return {
|
||||
loading: this.props.loading,
|
||||
loading,
|
||||
headers: this.getTableHeaders(),
|
||||
rows: this.getTableRows(),
|
||||
pageSize: 10,
|
||||
comp: this.compareFunction,
|
||||
page: this.props.page,
|
||||
pages: this.props.pages,
|
||||
onPageChange: this.props.onPageChange
|
||||
page,
|
||||
pages,
|
||||
onPageChange
|
||||
};
|
||||
}
|
||||
|
||||
@ -103,7 +141,9 @@ class TicketList extends React.Component {
|
||||
}
|
||||
|
||||
getTableHeaders() {
|
||||
if (this.props.type == 'primary' ) {
|
||||
const { type } = this.props;
|
||||
|
||||
if(type == 'primary' ) {
|
||||
return [
|
||||
{
|
||||
key: 'number',
|
||||
@ -122,11 +162,14 @@ class TicketList extends React.Component {
|
||||
},
|
||||
{
|
||||
key: 'date',
|
||||
value: i18n('DATE'),
|
||||
value: <div>
|
||||
{i18n('DATE')}
|
||||
{this.renderSortArrow('date')}
|
||||
</div>,
|
||||
className: 'ticket-list__date col-md-2'
|
||||
}
|
||||
];
|
||||
} else if (this.props.type == 'secondary') {
|
||||
} else if(type == 'secondary') {
|
||||
return [
|
||||
{
|
||||
key: 'number',
|
||||
@ -138,11 +181,6 @@ class TicketList extends React.Component {
|
||||
value: i18n('TITLE'),
|
||||
className: 'ticket-list__title col-md-4'
|
||||
},
|
||||
{
|
||||
key: 'priority',
|
||||
value: i18n('PRIORITY'),
|
||||
className: 'ticket-list__priority col-md-1'
|
||||
},
|
||||
{
|
||||
key: 'department',
|
||||
value: i18n('DEPARTMENT'),
|
||||
@ -155,107 +193,110 @@ class TicketList extends React.Component {
|
||||
},
|
||||
{
|
||||
key: 'date',
|
||||
value: i18n('DATE'),
|
||||
value: <div>
|
||||
{i18n('DATE')}
|
||||
{this.renderSortArrow('date')}
|
||||
</div>,
|
||||
className: 'ticket-list__date col-md-2'
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
renderSortArrow(header) {
|
||||
const { orderBy, showOrderArrows, onChangeOrderBy } = this.props;
|
||||
|
||||
return (
|
||||
showOrderArrows ?
|
||||
<Icon
|
||||
name={`arrow-${this.getIconName(header, orderBy)}`}
|
||||
className="ticket-list__order-icon"
|
||||
color={this.getIconColor(header, orderBy)}
|
||||
onClick={() => onChangeOrderBy(header)} /> :
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
getIconName(header, orderBy) {
|
||||
return (orderBy && orderBy.value === header && orderBy.asc) ? "up" : "down";
|
||||
}
|
||||
|
||||
getIconColor(header, orderBy) {
|
||||
return (orderBy && orderBy.value === header) ? "gray" : "white";
|
||||
}
|
||||
|
||||
getTableRows() {
|
||||
return this.getTickets().map(this.gerTicketTableObject.bind(this));
|
||||
}
|
||||
|
||||
getTickets() {
|
||||
return (this.state.selectedDepartment) ? _.filter(this.props.tickets, (ticket) => {
|
||||
return ticket.department.id == this.state.selectedDepartment
|
||||
}) : this.props.tickets;
|
||||
const { tickets } = this.props;
|
||||
const { selectedDepartment } = this.state;
|
||||
|
||||
return (
|
||||
(selectedDepartment) ?
|
||||
_.filter(tickets, (ticket) => { return ticket.department.id == selectedDepartment}) :
|
||||
tickets
|
||||
);
|
||||
}
|
||||
|
||||
gerTicketTableObject(ticket) {
|
||||
let titleText = (this.isTicketUnread(ticket)) ? ticket.title + ' (1)' : ticket.title;
|
||||
const { date, title, ticketNumber, closed, tags, department, author } = ticket;
|
||||
const dateTodayWithOutHoursAndMinutes = DateTransformer.getDateToday();
|
||||
const ticketDateWithOutHoursAndMinutes = Math.floor(DateTransformer.UTCDateToLocalNumericDate(JSON.stringify(date*1)) / 10000);
|
||||
const stringTicketLocalDateFormat = DateTransformer.transformToString(date, false, true);
|
||||
const ticketDate = (
|
||||
((dateTodayWithOutHoursAndMinutes - ticketDateWithOutHoursAndMinutes) > 1) ?
|
||||
stringTicketLocalDateFormat :
|
||||
`${(dateTodayWithOutHoursAndMinutes - ticketDateWithOutHoursAndMinutes) ? "Yesterday" : "Today"} at ${stringTicketLocalDateFormat.slice(-5)}`
|
||||
);
|
||||
let titleText = (this.isTicketUnread(ticket)) ? title + ' (1)' : title;
|
||||
|
||||
return {
|
||||
number: (
|
||||
<Tooltip content={<TicketInfo ticket={ticket}/>} openOnHover>
|
||||
{'#' + ticket.ticketNumber}
|
||||
<Tooltip content={<TicketInfo ticket={ticket} />} openOnHover>
|
||||
{'#' + ticketNumber}
|
||||
</Tooltip>
|
||||
),
|
||||
title: (
|
||||
<Button className="ticket-list__title-link" type="clean" route={{to: this.props.ticketPath + ticket.ticketNumber}}>
|
||||
{titleText}
|
||||
</Button>
|
||||
<div>
|
||||
{closed ? <Icon size="sm" name="lock" /> : null}
|
||||
<Button className="ticket-list__title-link" type="clean" route={{to: this.props.ticketPath + ticketNumber}}>
|
||||
{titleText}
|
||||
</Button>
|
||||
{(tags || []).map((tagName,index) => {
|
||||
let tag = _.find(this.props.tags, {name:tagName});
|
||||
return <Tag size='small' name={tag && tag.name} color={tag && tag.color} key={index} />
|
||||
})}
|
||||
</div>
|
||||
|
||||
),
|
||||
priority: this.getTicketPriority(ticket.priority),
|
||||
department: ticket.department.name,
|
||||
author: ticket.author.name,
|
||||
date: DateTransformer.transformToString(ticket.date, false),
|
||||
department: department.name,
|
||||
author: author.name,
|
||||
date: ticketDate,
|
||||
unread: this.isTicketUnread(ticket),
|
||||
highlighted: this.isTicketUnread(ticket)
|
||||
};
|
||||
}
|
||||
|
||||
getTicketPriority(priority) {
|
||||
if(priority == 'high'){
|
||||
return (
|
||||
<span className="ticket-list__priority-high">{i18n('HIGH')}</span>
|
||||
);
|
||||
}
|
||||
if(priority == 'medium'){
|
||||
return (
|
||||
<span className="ticket-list__priority-medium">{i18n('MEDIUM')}</span>
|
||||
);
|
||||
}
|
||||
if(priority == 'low'){
|
||||
return (
|
||||
<span className="ticket-list__priority-low">{i18n('LOW')}</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
compareFunction(row1, row2) {
|
||||
if (row1.closed == row2.closed) {
|
||||
if (row1.unread == row2.unread) {
|
||||
let s1 = row1.date;
|
||||
let s2 = row2.date;
|
||||
|
||||
let y1 = s1.substring(0, 4);
|
||||
let y2 = s2.substring(0, 4);
|
||||
|
||||
if (y1 == y2) {
|
||||
let m1 = s1.substring(4, 6);
|
||||
let m2 = s2.substring(4, 6);
|
||||
|
||||
if (m1 == m2) {
|
||||
let d1 = s1.substring(6, 8);
|
||||
let d2 = s2.substring(6, 8);
|
||||
|
||||
if (d1 == d2) {
|
||||
return 0;
|
||||
}
|
||||
return d1 > d2 ? -1 : 1;
|
||||
}
|
||||
return m1 > m2 ? -1 : 1;
|
||||
}
|
||||
return y1 > y2 ? -1 : 1;
|
||||
}
|
||||
return row1.unread ? -1 : 1;
|
||||
}
|
||||
return row1.closed ? -1 : 1;
|
||||
}
|
||||
|
||||
isTicketUnread(ticket) {
|
||||
if(this.props.type === 'primary') {
|
||||
return ticket.unread;
|
||||
} else if(this.props.type === 'secondary') {
|
||||
if(ticket.author.id == this.props.userId && ticket.author.staff) {
|
||||
return ticket.unread;
|
||||
} else {
|
||||
return ticket.unreadStaff;
|
||||
}
|
||||
const { type, userId } = this.props;
|
||||
const { unread, author, unreadStaff } = ticket;
|
||||
|
||||
if(type === 'primary') {
|
||||
return unread;
|
||||
} else if(type === 'secondary') {
|
||||
if(author.id == userId && author.staff) {
|
||||
return unread;
|
||||
} else {
|
||||
return unreadStaff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default TicketList;
|
||||
export default connect((store) => {
|
||||
return {
|
||||
tags: store.config['tags']
|
||||
};
|
||||
})(TicketList);
|
||||
|
@ -1,6 +1,10 @@
|
||||
@import "../scss/vars";
|
||||
|
||||
.ticket-list {
|
||||
&__order-icon {
|
||||
padding-left: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&__filters {
|
||||
margin-bottom: 25px;
|
||||
@ -43,24 +47,4 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&__priority-low,
|
||||
&__priority-medium,
|
||||
&__priority-high {
|
||||
display: inline-block;
|
||||
border-radius: 10px;
|
||||
width: 70px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&__priority-low {
|
||||
background-color: $primary-green;
|
||||
}
|
||||
|
||||
&__priority-medium {
|
||||
background-color: $secondary-blue;
|
||||
}
|
||||
|
||||
&__priority-high {
|
||||
background-color: $primary-red;
|
||||
}
|
||||
}
|
||||
|
387
client/src/app-components/ticket-query-filters.js
Normal file
387
client/src/app-components/ticket-query-filters.js
Normal file
@ -0,0 +1,387 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import SearchFiltersActions from 'actions/search-filters-actions';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import API from 'lib-app/api-call';
|
||||
import history from 'lib-app/history';
|
||||
import searchTicketsUtils from 'lib-app/search-tickets-utils';
|
||||
|
||||
import Form from 'core-components/form';
|
||||
import SubmitButton from 'core-components/submit-button';
|
||||
import FormField from 'core-components/form-field';
|
||||
import Icon from 'core-components/icon';
|
||||
import Button from 'core-components/button';
|
||||
import Loading from 'core-components/loading';
|
||||
|
||||
|
||||
class TicketQueryFilters extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
filters: React.PropTypes.shape({
|
||||
query: React.PropTypes.string,
|
||||
departments: React.PropTypes.string,
|
||||
owners: React.PropTypes.string,
|
||||
tags: React.PropTypes.string,
|
||||
dateRange: React.PropTypes.string,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
formState,
|
||||
filters,
|
||||
showFilters,
|
||||
ticketQueryListState
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={"ticket-query-filters" + (showFilters ? "__open" : "") }>
|
||||
<Form
|
||||
loading={ticketQueryListState.loading}
|
||||
values={this.getFormValue(formState)}
|
||||
onChange={this.onChangeForm.bind(this)}
|
||||
onSubmit={this.onSubmitListConfig.bind(this)}>
|
||||
<div className="ticket-query-filters__search-box">
|
||||
<FormField name="query" field="search-box" fieldProps={{onSearch: this.onSubmitListConfig.bind(this)}} />
|
||||
</div>
|
||||
<div className="ticket-query-filters__first-row">
|
||||
<FormField
|
||||
label={i18n('DATE')}
|
||||
name="dateRange"
|
||||
field="date-range"
|
||||
fieldProps={{defaultValue: formState.dateRange}} />
|
||||
<FormField
|
||||
label={i18n('STATUS')}
|
||||
name="closed"
|
||||
field="select"
|
||||
fieldProps={{
|
||||
items: this.getStatusItems(),
|
||||
className: 'ticket-query-filters__status-drop-down'
|
||||
}} />
|
||||
</div>
|
||||
<div className="ticket-query-filters__second-row">
|
||||
<FormField
|
||||
label={i18n('DEPARTMENTS')}
|
||||
name="departments"
|
||||
field="autocomplete"
|
||||
fieldProps={{items: this.getDepartmentsItems()}} />
|
||||
<FormField
|
||||
label={i18n('OWNER')}
|
||||
name="owners"
|
||||
field="autocomplete"
|
||||
fieldProps={{items: this.getStaffList()}} />
|
||||
</div>
|
||||
<div className="ticket-query-filters__third-row">
|
||||
<FormField
|
||||
label={i18n('TAGS')}
|
||||
name="tags"
|
||||
field="tag-selector"
|
||||
fieldProps={{
|
||||
items: this.getTags(filters.tags),
|
||||
onRemoveClick: this.removeTag.bind(this),
|
||||
onTagSelected: this.addTag.bind(this)
|
||||
}} />
|
||||
<FormField
|
||||
label={i18n('AUTHORS')}
|
||||
name="authors"
|
||||
field="autocomplete"
|
||||
fieldProps={{
|
||||
getItemListFromQuery: this.searchAuthors.bind(this),
|
||||
comparerFunction: this.autorsComparer.bind(this)
|
||||
}} />
|
||||
</div>
|
||||
<div className="ticket-query-filters__container">
|
||||
<Button
|
||||
className="ticket-query-filters__container__button ticket-query-filters__container__clear-button"
|
||||
size= "medium"
|
||||
disabled={ticketQueryListState.loading}
|
||||
onClick={this.clearFormValues.bind(this)}>
|
||||
{ticketQueryListState.loading ? <Loading /> : i18n('CLEAR')}
|
||||
</Button>
|
||||
<SubmitButton
|
||||
className="ticket-query-filters__container__button ticket-query-filters__container__search-button"
|
||||
type="secondary"
|
||||
size= "medium">
|
||||
{i18n('SEARCH')}
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</Form>
|
||||
<span className="separator" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
searchAuthors(query, blacklist = []) {
|
||||
blacklist = blacklist.map(item => {return {isStaff: item.isStaff, id: item.id}});
|
||||
|
||||
return API.call({
|
||||
path: '/ticket/search-authors',
|
||||
data: {
|
||||
query: query,
|
||||
blackList: JSON.stringify(blacklist)
|
||||
}
|
||||
}).then(r => {
|
||||
return r.data.authors.map(author => {
|
||||
return {
|
||||
name: author.name,
|
||||
color: "gray",
|
||||
id: author.id*1,
|
||||
profilePic: author.profilePic,
|
||||
isStaff: author.isStaff * 1,
|
||||
content: author.profilePic !== undefined ? this.renderStaffOption(author) : author.name,
|
||||
contentOnSelected: author.profilePic !== undefined ? this.renderStaffSelected(author) : author.name
|
||||
}});
|
||||
});
|
||||
}
|
||||
|
||||
renderDepartmentOption(department) {
|
||||
return (
|
||||
<div className="ticket-query-filters__department-option" key={`department-option-${department.id}`}>
|
||||
{department.private*1 ?
|
||||
<Icon className="ticket-query-filters__department-option__icon" name='user-secret'/> :
|
||||
null}
|
||||
<span className="ticket-query-filters__department-option__name">{department.name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderDeparmentSelected(department) {
|
||||
return (
|
||||
<div className="ticket-query-filters__department-selected" key={`department-selected-${department.id}`}>
|
||||
{department.private*1 ?
|
||||
<Icon className="ticket-query-filters__department-selected__icon" name='user-secret'/> :
|
||||
null}
|
||||
<span className="ticket-query-filters__department-selected__name">{department.name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderStaffOption(staff) {
|
||||
return (
|
||||
<div className="ticket-query-filters__staff-option" key={`staff-option-${staff.id}`}>
|
||||
<img className="ticket-query-filters__staff-option__profile-pic" src={this.getStaffProfilePic(staff)}/>
|
||||
<span className="ticket-query-filters__staff-option__name">{staff.name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderStaffSelected(staff) {
|
||||
return (
|
||||
<div className="ticket-query-filters__staff-selected" key={`staff-selected-${staff.id}`}>
|
||||
<img className="ticket-query-filters__staff-selected__profile-pic" src={this.getStaffProfilePic(staff)}/>
|
||||
<span className="ticket-query-filters__staff-selected__name">{staff.name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
addTag(tag) {
|
||||
const { formState } = this.props;
|
||||
let selectedTagsId = formState.tags.concat(this.tagsNametoTagsId(this.getSelectedTagsName([tag])));
|
||||
|
||||
this.onChangeFormState({...formState, tags: selectedTagsId});
|
||||
}
|
||||
|
||||
autorsComparer(autorList, autorSelectedList) {
|
||||
return autorList.filter(item => !_.some(autorSelectedList, {id: item.id, isStaff: item.isStaff}));
|
||||
}
|
||||
|
||||
clearFormValues(event) {
|
||||
event.preventDefault();
|
||||
this.props.dispatch(SearchFiltersActions.setDefaultFormValues());
|
||||
}
|
||||
|
||||
getDepartmentsItems() {
|
||||
const { departments, } = this.props;
|
||||
let departmentsList = departments.map(department => {
|
||||
return {
|
||||
id: JSON.parse(department.id),
|
||||
name: department.name.toLowerCase(),
|
||||
color: 'gray',
|
||||
contentOnSelected: this.renderDeparmentSelected(department),
|
||||
content: this.renderDepartmentOption(department),
|
||||
}
|
||||
});
|
||||
|
||||
return departmentsList;
|
||||
}
|
||||
|
||||
getSelectedDepartments(selectedDepartmentsId) {
|
||||
let selectedDepartments = [];
|
||||
|
||||
if(selectedDepartmentsId !== undefined) {
|
||||
let departments = this.getDepartmentsItems();
|
||||
selectedDepartments = departments.filter(item => _.includes(selectedDepartmentsId, item.id));
|
||||
}
|
||||
|
||||
return selectedDepartments;
|
||||
}
|
||||
|
||||
getSelectedStaffs(selectedStaffsId) {
|
||||
let selectedStaffs = [];
|
||||
if(selectedStaffsId !== undefined) {
|
||||
let staffs = this.getStaffList();
|
||||
selectedStaffs = staffs.filter(staff => _.includes(selectedStaffsId, staff.id));
|
||||
}
|
||||
|
||||
return selectedStaffs;
|
||||
}
|
||||
|
||||
getSelectedTagsName(selectedTagsId) {
|
||||
let selectedTagsName = [];
|
||||
|
||||
if(selectedTagsId !== undefined) {
|
||||
let tagList = this.getTags();
|
||||
let selectedTags = tagList.filter(item => _.includes(selectedTagsId, item.id));
|
||||
selectedTagsName = selectedTags.map(tag => tag.name);
|
||||
}
|
||||
|
||||
return selectedTagsName;
|
||||
}
|
||||
|
||||
getStaffList() {
|
||||
const { staffList, } = this.props;
|
||||
let newStaffList = staffList.map(staff => {
|
||||
return {
|
||||
id: JSON.parse(staff.id),
|
||||
name: staff.name.toLowerCase(),
|
||||
color: 'gray',
|
||||
contentOnSelected: this.renderStaffSelected(staff),
|
||||
content: this.renderStaffOption(staff),
|
||||
}
|
||||
});
|
||||
|
||||
return newStaffList;
|
||||
}
|
||||
|
||||
getStaffProfilePic(staff) {
|
||||
return staff.profilePic ? API.getFileLink(staff.profilePic) : (API.getURL() + '/images/profile.png');
|
||||
}
|
||||
|
||||
getStatusItems() {
|
||||
let items = [
|
||||
{id: 0, name: 'Any', content: i18n('ANY')},
|
||||
{id: 1, name: 'Opened', content: i18n('OPENED')},
|
||||
{id: 2, name: 'Closed', content: i18n('CLOSED')},
|
||||
];
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
getTags() {
|
||||
const { tags, } = this.props;
|
||||
let newTagList = tags.map(tag => {
|
||||
return {
|
||||
id: JSON.parse(tag.id),
|
||||
name: tag.name,
|
||||
color : tag.color
|
||||
}
|
||||
});
|
||||
|
||||
return newTagList;
|
||||
}
|
||||
|
||||
onChangeFormState(formValues) {
|
||||
this.props.dispatch(SearchFiltersActions.changeForm(formValues));
|
||||
}
|
||||
|
||||
onSubmitListConfig() {
|
||||
const {
|
||||
formState,
|
||||
filters,
|
||||
formEdited,
|
||||
} = this.props;
|
||||
const listConfigWithCompleteAuthorsList = searchTicketsUtils.formValueToListConfig(
|
||||
{...formState, orderBy: filters.orderBy, page: 1},
|
||||
true
|
||||
);
|
||||
|
||||
if(formEdited && formState.dateRange.valid) {
|
||||
const filtersForAPI = searchTicketsUtils.prepareFiltersForAPI(listConfigWithCompleteAuthorsList.filters);
|
||||
const currentPath = window.location.pathname;
|
||||
const urlQuery = searchTicketsUtils.getFiltersForURL({
|
||||
filters: filtersForAPI,
|
||||
shouldRemoveCustomParam: true,
|
||||
shouldRemoveUseInitialValuesParam: true
|
||||
});
|
||||
urlQuery && history.push(`${currentPath}${urlQuery}`);
|
||||
}
|
||||
}
|
||||
|
||||
removeTag(tag) {
|
||||
const { formState } = this.props;
|
||||
let tagListName = formState.tags;
|
||||
let newTagList = tagListName.filter(item => item !== tag);
|
||||
let selectedTags = this.tagsNametoTagsId(this.getSelectedTagsName(newTagList));
|
||||
|
||||
this.onChangeFormState({...formState, tags: selectedTags});
|
||||
}
|
||||
|
||||
tagsNametoTagsId(selectedTagsName) {
|
||||
let tagList = this.getTags();
|
||||
let selectedTags = tagList.filter(item => _.includes(selectedTagsName, item.name));
|
||||
let selectedTagsId = selectedTags.map(tag => tag.id);
|
||||
|
||||
return selectedTagsId;
|
||||
}
|
||||
|
||||
onChangeForm(data) {
|
||||
const newStartDate = data.dateRange.startDate ? data.dateRange.startDate : searchTicketsUtils.getDefaultLocalStartDate();
|
||||
const newEndDate = data.dateRange.endDate ? data.dateRange.endDate : searchTicketsUtils.getDefaultlocalEndDate();
|
||||
const departmentsId = data.departments.map(department => department.id);
|
||||
const staffsId = data.owners.map(staff => staff.id);
|
||||
const tagsName = this.tagsNametoTagsId(data.tags);
|
||||
const authors = data.authors.map(({name, id, isStaff, profilePic, color}) => ({name, id: id*1, isStaff, profilePic, color}));
|
||||
|
||||
this.onChangeFormState({
|
||||
...data,
|
||||
tags: tagsName,
|
||||
owners: staffsId,
|
||||
departments: departmentsId,
|
||||
authors: authors,
|
||||
dateRange: {
|
||||
...data.dateRange,
|
||||
startDate: newStartDate,
|
||||
endDate: newEndDate
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getFormValue(form) {
|
||||
return {
|
||||
...form,
|
||||
departments: this.getSelectedDepartments(form.departments),
|
||||
owners: this.getSelectedStaffs(form.owners),
|
||||
tags: this.getSelectedTagsName(form.tags),
|
||||
authors: this.getAuthors(form.authors),
|
||||
}
|
||||
}
|
||||
|
||||
getAuthors(authors = []) {
|
||||
return authors.map(author => ({
|
||||
name: author.name,
|
||||
color: "gray",
|
||||
id: author.id*1,
|
||||
isStaff: author.isStaff*1,
|
||||
profilePic: author.profilePic,
|
||||
content: author.profilePic !== undefined ? this.renderStaffOption(author) : author.name,
|
||||
contentOnSelected: author.profilePic !== undefined ? this.renderStaffSelected(author) : author.name
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect((store) => {
|
||||
return {
|
||||
tags: store.config.tags,
|
||||
departments: store.config.departments,
|
||||
staffList: store.adminData.staffMembers,
|
||||
formState: store.searchFilters.form,
|
||||
filters: store.searchFilters.listConfig.filters,
|
||||
showFilters: store.searchFilters.showFilters,
|
||||
formEdited: store.searchFilters.formEdited,
|
||||
ticketQueryListState: store.searchFilters.ticketQueryListState,
|
||||
};
|
||||
})(TicketQueryFilters);
|
118
client/src/app-components/ticket-query-filters.scss
Normal file
118
client/src/app-components/ticket-query-filters.scss
Normal file
@ -0,0 +1,118 @@
|
||||
@import '../scss/vars';
|
||||
|
||||
.ticket-query-filters {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
overflow-y: hidden;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&__open {
|
||||
max-height: 1000px;
|
||||
opacity: 1;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
&__button {
|
||||
margin: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__department-option {
|
||||
&__name {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__department-selected {
|
||||
display: inline-block;
|
||||
border-radius: 3px;
|
||||
margin-left: 5px;
|
||||
padding: 1px;
|
||||
font-size: 13px;
|
||||
cursor: default;
|
||||
|
||||
&__icon {
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__first-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 10%;
|
||||
}
|
||||
|
||||
&__second-row,
|
||||
&__third-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&__status-drop-down > .drop-down__current-item {
|
||||
background-color: $very-light-grey;
|
||||
|
||||
&:focus {
|
||||
background-color: $medium-grey;
|
||||
}
|
||||
}
|
||||
|
||||
&__search-box {
|
||||
width: 100%;
|
||||
padding: 0 50px;
|
||||
}
|
||||
|
||||
&__staff-option {
|
||||
&__profile-pic {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__name {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__staff-selected {
|
||||
display: inline-block;
|
||||
border-radius: 3px;
|
||||
margin-left: 5px;
|
||||
padding: 1px;
|
||||
font-size: 13px;
|
||||
cursor: default;
|
||||
|
||||
&__profile-pic {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.ticket-query-filters {
|
||||
&__first-row,
|
||||
&__second-row,
|
||||
&__third-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: unset;
|
||||
}
|
||||
}
|
||||
}
|
83
client/src/app-components/ticket-query-list.js
Normal file
83
client/src/app-components/ticket-query-list.js
Normal file
@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import TicketList from 'app-components/ticket-list';
|
||||
import Message from 'core-components/message';
|
||||
import searchFiltersActions from '../actions/search-filters-actions';
|
||||
import queryString from 'query-string';
|
||||
|
||||
class TicketQueryList extends React.Component {
|
||||
|
||||
state = {
|
||||
tickets: [],
|
||||
page: 1,
|
||||
pages: 0,
|
||||
error: null,
|
||||
loading: true
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
(this.state.error) ?
|
||||
<Message type="error">{i18n('ERROR_RETRIEVING_TICKETS')}</Message> :
|
||||
<TicketList {...this.getTicketListProps()}/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onPageChange(event) {
|
||||
const {
|
||||
dispatch,
|
||||
filters
|
||||
} = this.props;
|
||||
|
||||
dispatch(searchFiltersActions.changePage({
|
||||
...filters,
|
||||
page: event.target.value
|
||||
}));
|
||||
}
|
||||
|
||||
getTicketListProps () {
|
||||
const {
|
||||
filters,
|
||||
onChangeOrderBy,
|
||||
userId,
|
||||
ticketQueryListState
|
||||
} = this.props;
|
||||
const page = {
|
||||
...ticketQueryListState,
|
||||
...queryString.parse(window.location.search)
|
||||
}.page*1;
|
||||
|
||||
return {
|
||||
userId: userId,
|
||||
ticketPath: '/admin/panel/tickets/view-ticket/',
|
||||
tickets: ticketQueryListState.tickets,
|
||||
page: page,
|
||||
pages: ticketQueryListState.pages,
|
||||
loading: ticketQueryListState.loading,
|
||||
type: 'secondary',
|
||||
showDepartmentDropdown: false,
|
||||
closedTicketsShown: false,
|
||||
onPageChange:this.onPageChange.bind(this),
|
||||
orderBy: filters.orderBy ? JSON.parse(filters.orderBy) : filters.orderBy,
|
||||
showOrderArrows: true,
|
||||
onChangeOrderBy: onChangeOrderBy,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect((store) => {
|
||||
return {
|
||||
userId: store.session.userId*1,
|
||||
filters: store.searchFilters.listConfig.filters,
|
||||
ticketQueryListState: store.searchFilters.ticketQueryListState
|
||||
};
|
||||
})(TicketQueryList);
|
@ -8,10 +8,11 @@ import i18n from 'lib-app/i18n';
|
||||
import API from 'lib-app/api-call';
|
||||
import SessionStore from 'lib-app/session-store';
|
||||
import MentionsParser from 'lib-app/mentions-parser';
|
||||
import history from 'lib-app/history';
|
||||
import searchTicketsUtils from 'lib-app/search-tickets-utils';
|
||||
|
||||
import TicketEvent from 'app-components/ticket-event';
|
||||
import AreYouSure from 'app-components/are-you-sure';
|
||||
import DateTransformer from 'lib-core/date-transformer';
|
||||
import Form from 'core-components/form';
|
||||
import FormField from 'core-components/form-field';
|
||||
import SubmitButton from 'core-components/submit-button';
|
||||
@ -22,6 +23,9 @@ import Icon from 'core-components/icon';
|
||||
import TextEditor from 'core-components/text-editor';
|
||||
import InfoTooltip from 'core-components/info-tooltip';
|
||||
import DepartmentDropdown from 'app-components/department-dropdown';
|
||||
import TagSelector from 'core-components/tag-selector';
|
||||
import Tag from 'core-components/tag';
|
||||
import Loading from 'core-components/loading';
|
||||
|
||||
class TicketViewer extends React.Component {
|
||||
static propTypes = {
|
||||
@ -36,15 +40,18 @@ class TicketViewer extends React.Component {
|
||||
userId: React.PropTypes.number,
|
||||
userStaff: React.PropTypes.bool,
|
||||
userDepartments: React.PropTypes.array,
|
||||
userLevel: React.PropTypes.number
|
||||
userLevel: React.PropTypes.number,
|
||||
tags: React.PropTypes.array
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
tags: [],
|
||||
editable: false,
|
||||
ticket: {
|
||||
author: {},
|
||||
department: {},
|
||||
comments: []
|
||||
comments: [],
|
||||
edited: null
|
||||
}
|
||||
};
|
||||
|
||||
@ -52,173 +59,362 @@ class TicketViewer extends React.Component {
|
||||
loading: false,
|
||||
commentValue: TextEditor.createEmpty(),
|
||||
commentEdited: false,
|
||||
commentPrivate: false
|
||||
commentPrivate: false,
|
||||
edit: false,
|
||||
editId: 0,
|
||||
tagSelectorLoading: false,
|
||||
editTitle: false,
|
||||
newTitle: this.props.ticket.title,
|
||||
editTitleError: false,
|
||||
editTitleLoading: false,
|
||||
editStatus: false,
|
||||
editTags: false,
|
||||
editOwner: false,
|
||||
editDepartment: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if(!this.props.staffMembersLoaded && this.props.userStaff) {
|
||||
this.props.dispatch(AdminDataActions.retrieveStaffMembers());
|
||||
const { staffMembersLoaded, userStaff, dispatch } = this.props;
|
||||
|
||||
if(!staffMembersLoaded && userStaff) {
|
||||
dispatch(AdminDataActions.retrieveStaffMembers());
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const ticket = this.props.ticket;
|
||||
const { ticket, userStaff, userId, editable, allowAttachments, assignmentAllowed } = this.props;
|
||||
|
||||
return (
|
||||
<div className="ticket-viewer">
|
||||
<div className="ticket-viewer__header row">
|
||||
<span className="ticket-viewer__number">#{ticket.ticketNumber}</span>
|
||||
<span className="ticket-viewer__title">{ticket.title}</span>
|
||||
<span className="ticket-viewer__flag">
|
||||
<Icon name={(ticket.language === 'en') ? 'us' : ticket.language}/>
|
||||
</span>
|
||||
</div>
|
||||
{this.props.editable ? this.renderEditableHeaders() : this.renderHeaders()}
|
||||
{this.state.editTitle ? this.renderEditableTitle() : this.renderTitleHeader()}
|
||||
{editable ? this.renderEditableHeaders() : this.renderHeaders()}
|
||||
<div className="ticket-viewer__content">
|
||||
<TicketEvent type="COMMENT" author={ticket.author} content={this.props.userStaff ? MentionsParser.parse(ticket.content) : ticket.content} date={ticket.date} file={ticket.file}/>
|
||||
<TicketEvent
|
||||
loading={this.state.loading}
|
||||
type="COMMENT"
|
||||
author={ticket.author}
|
||||
content={userStaff ? MentionsParser.parse(ticket.content) : ticket.content}
|
||||
userStaff={userStaff}
|
||||
userId={userId}
|
||||
date={ticket.date}
|
||||
onEdit={this.onEdit.bind(this,0)}
|
||||
edited={ticket.edited}
|
||||
file={ticket.file}
|
||||
edit={this.state.edit && this.state.editId == 0}
|
||||
onToggleEdit={this.onToggleEdit.bind(this, 0)}
|
||||
allowAttachments={allowAttachments} />
|
||||
</div>
|
||||
<div className="ticket-viewer__comments">
|
||||
{ticket.events && ticket.events.map(this.renderTicketEvent.bind(this))}
|
||||
</div>
|
||||
{(!this.props.ticket.closed && (this.props.editable || !this.props.assignmentAllowed)) ? this.renderResponseField() : (this.showDeleteButton())? <Button size="medium" onClick={this.onDeleteTicketClick.bind(this)}>{i18n('DELETE_TICKET')}</Button> : null}
|
||||
{(!ticket.closed && (editable || !assignmentAllowed)) ? this.renderResponseField() : (this.showDeleteButton()) ? this.renderDeleteTicketButton() : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderEditableHeaders() {
|
||||
const ticket = this.props.ticket;
|
||||
const departments = this.getDepartmentsForTransfer();
|
||||
renderTitleHeader() {
|
||||
const {ticket, userStaff, userId} = this.props;
|
||||
const {ticketNumber, title, author, editedTitle, language} = ticket;
|
||||
|
||||
const priorities = {
|
||||
'low': 0,
|
||||
'medium': 1,
|
||||
'high': 2
|
||||
return(
|
||||
<div className="ticket-viewer__header">
|
||||
<span className="ticket-viewer__number">#{ticketNumber}</span>
|
||||
<span className="ticket-viewer__title">{title}</span>
|
||||
<span className="ticket-viewer__flag">
|
||||
<Icon name={(language === 'en') ? 'us' : language} />
|
||||
</span>
|
||||
{((author.id == userId && author.staff == userStaff) || userStaff) ? this.renderEditTitleOption() : null}
|
||||
{editedTitle ? this.renderEditedTitleText() : null }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderEditableTitle(){
|
||||
return(
|
||||
<div className="ticket-viewer__header">
|
||||
<div className="ticket-viewer__edit-title-box">
|
||||
<FormField
|
||||
className="ticket-viewer___input-edit-title"
|
||||
error={this.state.editTitleError}
|
||||
value={this.state.newTitle}
|
||||
field='input'
|
||||
onChange={(e) => this.setState({newTitle: e.target.value})} />
|
||||
</div>
|
||||
<div className="ticket-viewer__edit-title__buttons">
|
||||
<Button disabled={this.state.editTitleLoading} type='primary' size="medium" onClick={() => this.setState({editTitle: false, newTitle: this.props.ticket.title})}>
|
||||
{this.state.editTitleLoading ? <Loading /> : <Icon name="times" />}
|
||||
</Button>
|
||||
<Button disabled={this.state.editTitleLoading} type='secondary' size="medium" onClick={this.changeTitle.bind(this)}>
|
||||
{this.state.editTitleLoading ? <Loading /> : <Icon name="check" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderEditableHeaders() {
|
||||
const { userStaff, ticket } = this.props;
|
||||
const filtersOnlyWithAuthor = {
|
||||
authors: [
|
||||
{
|
||||
id: ticket.author.id*1,
|
||||
isStaff: ticket.author.staff*1
|
||||
}
|
||||
]
|
||||
};
|
||||
const priorityList = [
|
||||
{content: i18n('LOW')},
|
||||
{content: i18n('MEDIUM')},
|
||||
{content: i18n('HIGH')}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="ticket-viewer__headers">
|
||||
<div className="ticket-viewer__info-row-header row">
|
||||
<div className="col-md-4">{i18n('DEPARTMENT')}</div>
|
||||
<div className="col-md-4">{i18n('AUTHOR')}</div>
|
||||
<div className="col-md-4">{i18n('DATE')}</div>
|
||||
</div>
|
||||
<div className="ticket-viewer__info-row-values row">
|
||||
<div className="col-md-4">
|
||||
<DepartmentDropdown className="ticket-viewer__editable-dropdown"
|
||||
departments={departments}
|
||||
selectedIndex={_.findIndex(departments, {id: this.props.ticket.department.id})}
|
||||
onChange={this.onDepartmentDropdownChanged.bind(this)} />
|
||||
<div className="ticket-viewer__info">
|
||||
<div className="ticket-viewer__info-container-editable">
|
||||
<div className="ticket-viewer__info-container-editable__column">
|
||||
<div className="ticket-viewer__info-header">{i18n('DEPARTMENT')}</div>
|
||||
<div className="ticket-viewer__info-value">
|
||||
{
|
||||
this.state.editDepartment ?
|
||||
this.renderEditDepartment() :
|
||||
ticket.department.name
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{userStaff ? this.renderEditOption("Department") : null}
|
||||
</div>
|
||||
<div className="col-md-4">{ticket.author.name}</div>
|
||||
<div className="col-md-4">{DateTransformer.transformToString(ticket.date)}</div>
|
||||
</div>
|
||||
<div className="ticket-viewer__info-row-header row">
|
||||
<div className="col-md-4">{i18n('PRIORITY')}</div>
|
||||
<div className="col-md-4">{i18n('OWNER')}</div>
|
||||
<div className="col-md-4">{i18n('STATUS')}</div>
|
||||
</div>
|
||||
<div className="ticket-viewer__info-row-values row">
|
||||
<div className="col-md-4">
|
||||
<DropDown className="ticket-viewer__editable-dropdown" items={priorityList} selectedIndex={priorities[ticket.priority]} onChange={this.onPriorityDropdownChanged.bind(this)} />
|
||||
<div className="ticket-viewer__info-container-editable">
|
||||
<div className="ticket-viewer__info-container-editable__column">
|
||||
<div className="ticket-viewer__info-header">{i18n('TAGS')}</div>
|
||||
<div className="ticket-viewer__info-value">
|
||||
{
|
||||
this.state.editTags ?
|
||||
this.renderEditTags() :
|
||||
this.renderTags()
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{userStaff ? this.renderEditOption("Tags") : null}
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
{this.renderAssignStaffList()}
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
{ticket.closed ?
|
||||
<Button type='secondary' size="extra-small" onClick={this.onReopenClick.bind(this)}>
|
||||
{i18n('RE_OPEN')}
|
||||
</Button> : i18n('OPENED')}
|
||||
<div className="ticket-viewer__info-container-editable">
|
||||
<div className="ticket-viewer__info-container-editable__column">
|
||||
<div className="ticket-viewer__info-header">{i18n('OWNER')}</div>
|
||||
<div className="ticket-viewer__info-value">
|
||||
{this.renderOwnerNode()}
|
||||
</div>
|
||||
</div>
|
||||
{userStaff ? this.renderEditOption("Owner") : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ticket-viewer__info">
|
||||
<div className="ticket-viewer__info-container-editable">
|
||||
<div className="ticket-viewer__info-container-editable__column">
|
||||
<div className="ticket-viewer__info-header">{i18n('AUTHOR')}</div>
|
||||
<div className="ticket-viewer__info-value">
|
||||
<a className="ticket-viewer__info-author-name" href={this.searchTickets(filtersOnlyWithAuthor)}>
|
||||
{ticket.author.name}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ticket-viewer__info-container-editable">
|
||||
<div className="ticket-viewer__info-container-editable__column">
|
||||
<div className="ticket-viewer__info-header">{i18n('STATUS')}</div>
|
||||
<div className="ticket-viewer__info-value">
|
||||
{this.state.editStatus ? this.renderEditStatus() : (ticket.closed ? i18n('CLOSED') : i18n('OPENED'))}
|
||||
</div>
|
||||
</div>
|
||||
{userStaff ? this.renderEditOption("Status") : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderEditTags() {
|
||||
const { tags, ticket } = this.props;
|
||||
|
||||
return (
|
||||
<div className="ticket-viewer__edit-tags">
|
||||
<TagSelector
|
||||
items={tags}
|
||||
values={ticket.tags}
|
||||
onRemoveClick={this.removeTag.bind(this)}
|
||||
onTagSelected={this.addTag.bind(this)}
|
||||
loading={this.state.tagSelectorLoading} />
|
||||
{this.renderCancelButton("Tags")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderEditStatus() {
|
||||
return (
|
||||
<div className="ticket-viewer__edit-status__buttons">
|
||||
{this.renderCancelButton("Status")}
|
||||
{this.props.ticket.closed ?
|
||||
<Button type='secondary' size="medium" onClick={this.onReopenClick.bind(this)}>
|
||||
{i18n('RE_OPEN')}
|
||||
</Button> :
|
||||
this.renderCloseTicketButton()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderHeaders() {
|
||||
const ticket = this.props.ticket;
|
||||
const priorities = {
|
||||
'low': 'LOW',
|
||||
'medium': 'MEDIUM',
|
||||
'high': 'HIGH'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ticket-viewer__headers">
|
||||
<div className="ticket-viewer__info-row-header row">
|
||||
<div className="ticket-viewer__department col-md-4">{i18n('DEPARTMENT')}</div>
|
||||
<div className="ticket-viewer__author col-md-4">{i18n('AUTHOR')}</div>
|
||||
<div className="ticket-viewer__date col-md-4">{i18n('DATE')}</div>
|
||||
</div>
|
||||
<div className="ticket-viewer__info-row-values row">
|
||||
<div className="ticket-viewer__department col-md-4">{ticket.department.name}</div>
|
||||
<div className="ticket-viewer__author col-md-4">{ticket.author.name}</div>
|
||||
<div className="ticket-viewer__date col-md-4">{DateTransformer.transformToString(ticket.date, false)}</div>
|
||||
</div>
|
||||
<div className="ticket-viewer__info-row-header row">
|
||||
<div className="ticket-viewer__department col-md-4">{i18n('PRIORITY')}</div>
|
||||
<div className="ticket-viewer__author col-md-4">{i18n('OWNER')}</div>
|
||||
<div className="ticket-viewer__date col-md-4">{i18n('STATUS')}</div>
|
||||
</div>
|
||||
<div className="ticket-viewer__info-row-values row">
|
||||
<div className="col-md-4">
|
||||
{i18n(priorities[this.props.ticket.priority || 'low'])}
|
||||
<div className="ticket-viewer__info">
|
||||
<div className="ticket-viewer__info-container">
|
||||
<div className="ticket-viewer__info-header">{i18n('DEPARTMENT')}</div>
|
||||
<div className="ticket-viewer__info-value">{ticket.department.name}</div>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
{this.renderOwnerNode()}
|
||||
<div className="ticket-viewer__info-container">
|
||||
<div className="ticket-viewer__info-header">{i18n('AUTHOR')}</div>
|
||||
<div className="ticket-viewer__info-value">{ticket.author.name}</div>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
{i18n((this.props.ticket.closed) ? 'CLOSED' : 'OPENED')}
|
||||
<div className="ticket-viewer__info-container">
|
||||
<div className="ticket-viewer__info-header">{i18n('TAGS')}</div>
|
||||
<div className="ticket-viewer__info-value">
|
||||
{this.renderTags()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ticket-viewer__info">
|
||||
<div className="ticket-viewer__info-container">
|
||||
<div className="ticket-viewer__info-header">{i18n('OWNER')}</div>
|
||||
<div className="ticket-viewer__info-value">
|
||||
{this.renderOwnerNode()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ticket-viewer__info-container">
|
||||
<div className="ticket-viewer__info-header">{i18n('STATUS')}</div>
|
||||
<div className="ticket-viewer__info-value">
|
||||
{i18n((ticket.closed) ? 'CLOSED' : 'OPENED')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderOwnerNode() {
|
||||
let ownerNode = null;
|
||||
renderTags() {
|
||||
const { ticket, tags } = this.props;
|
||||
const TAGS = (
|
||||
ticket.tags.length ?
|
||||
ticket.tags.map((tagName, index) => {
|
||||
const tag = _.find(tags, {name: tagName});
|
||||
return <Tag name={tag && tag.name} color={tag && tag.color} key={index} />
|
||||
}) :
|
||||
i18n('NONE')
|
||||
);
|
||||
|
||||
if (this.props.assignmentAllowed) {
|
||||
ownerNode = this.renderAssignStaffList();
|
||||
} else {
|
||||
ownerNode = (this.props.ticket.owner) ? this.props.ticket.owner.name : i18n('NONE')
|
||||
}
|
||||
|
||||
return ownerNode;
|
||||
return TAGS;
|
||||
}
|
||||
|
||||
renderAssignStaffList() {
|
||||
const items = this.getStaffAssignmentItems();
|
||||
const ownerId = this.props.ticket.owner && this.props.ticket.owner.id;
|
||||
renderOwnerNode() {
|
||||
const { assignmentAllowed, ticket } = this.props;
|
||||
const filtersOnlyWithOwner = ticket.owner && {owners: [ticket.owner.id*1]};
|
||||
let ownerNode = null;
|
||||
|
||||
if(assignmentAllowed && ticket.owner) {
|
||||
ownerNode = (
|
||||
<a className="ticket-viewer__info-owner-name" href={this.searchTickets(filtersOnlyWithOwner)}>
|
||||
{ticket.owner.name}
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
ownerNode = (
|
||||
<span className="ticket-viewer__info-owner-name">
|
||||
{(ticket.owner) ? ticket.owner.name : i18n('NONE')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (assignmentAllowed && this.state.editOwner) ? this.renderEditOwner() : ownerNode;
|
||||
}
|
||||
|
||||
renderEditOwner() {
|
||||
const items = this.getStaffAssignmentItems();
|
||||
const { ticket } = this.props;
|
||||
const ownerId = ticket.owner && ticket.owner.id*1;
|
||||
let selectedIndex = _.findIndex(items, {id: ownerId});
|
||||
selectedIndex = (selectedIndex !== -1) ? selectedIndex : 0;
|
||||
|
||||
return (
|
||||
<DropDown
|
||||
className="ticket-viewer__editable-dropdown" items={items}
|
||||
selectedIndex={selectedIndex}
|
||||
onChange={this.onAssignmentChange.bind(this)}
|
||||
/>
|
||||
<div className="ticket-viewer__edit-owner">
|
||||
<DropDown
|
||||
className="ticket-viewer__editable-dropdown" items={items}
|
||||
selectedIndex={selectedIndex}
|
||||
onChange={this.onAssignmentChange.bind(this)} />
|
||||
{this.renderCancelButton("Owner")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderEditDepartment() {
|
||||
const { ticket } = this.props;
|
||||
const departments = this.getDepartmentsForTransfer();
|
||||
|
||||
return (
|
||||
<div className="ticket-viewer__edit-owner">
|
||||
<DepartmentDropdown
|
||||
className="ticket-viewer__editable-dropdown"
|
||||
departments={departments}
|
||||
selectedIndex={_.findIndex(departments, {id: ticket.department.id})}
|
||||
onChange={this.onDepartmentDropdownChanged.bind(this)} />
|
||||
{this.renderCancelButton("Department")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderEditTitleOption() {
|
||||
return(
|
||||
<span className="ticket-viewer__edit-title-icon">
|
||||
<Icon name="pencil" onClick={() => this.setState({editTitle: true})} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
renderEditOption(option) {
|
||||
return(
|
||||
<span className="ticket-viewer__edit-icon">
|
||||
<Icon name="pencil" onClick={() => this.setState({["edit"+option]: true})} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
renderEditedTitleText(){
|
||||
return(
|
||||
<div className="ticket-viewer__edited-title-text"> {i18n('TITLE_EDITED')} </div>
|
||||
)
|
||||
}
|
||||
|
||||
renderCancelButton(option) {
|
||||
return <Button type='link' size="medium" onClick={() => this.setState({["edit"+option]: false})}>{i18n('CLOSE')}</Button>
|
||||
}
|
||||
|
||||
renderTicketEvent(options, index) {
|
||||
if (this.props.userStaff && typeof options.content === 'string') {
|
||||
const { userStaff, ticket, userId, allowAttachments } = this.props;
|
||||
|
||||
if(userStaff && typeof options.content === 'string') {
|
||||
options.content = MentionsParser.parse(options.content);
|
||||
}
|
||||
|
||||
return (
|
||||
<TicketEvent {...options} author={(!_.isEmpty(options.author)) ? options.author : this.props.ticket.author} key={index} />
|
||||
<TicketEvent
|
||||
{...options}
|
||||
author={(!_.isEmpty(options.author)) ? options.author : ticket.author}
|
||||
userStaff={userStaff}
|
||||
userId={userId}
|
||||
onEdit={this.onEdit.bind(this, options.id)}
|
||||
edit={this.state.edit && this.state.editId == options.id}
|
||||
onToggleEdit={this.onToggleEdit.bind(this, options.id)}
|
||||
key={index}
|
||||
allowAttachments={allowAttachments} />
|
||||
);
|
||||
}
|
||||
|
||||
renderResponseField() {
|
||||
const { allowAttachments } = this.props;
|
||||
|
||||
return (
|
||||
<div className="ticket-viewer__response">
|
||||
<Form {...this.getCommentFormProps()}>
|
||||
@ -230,13 +426,15 @@ class TicketViewer extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
<div className="ticket-viewer__response-field row">
|
||||
<FormField name="content" validation="TEXT_AREA" required field="textarea" fieldProps={{allowImages: this.props.allowAttachments}}/>
|
||||
{(this.props.allowAttachments) ? <FormField name="file" field="file"/> : null}
|
||||
<FormField name="content" validation="TEXT_AREA" required field="textarea" fieldProps={{allowImages: allowAttachments}} />
|
||||
<div className="ticket-viewer__response-buttons">
|
||||
{allowAttachments ? <FormField name="file" field="file" /> : null}
|
||||
<SubmitButton type="secondary">{i18n('RESPOND_TICKET')}</SubmitButton>
|
||||
<div>
|
||||
<Button size="medium" onClick={this.onCloseTicketClick.bind(this)}>{i18n('CLOSE_TICKET')}</Button>
|
||||
{(this.showDeleteButton())? <Button className="ticket-viewer__delete-button" size="medium" onClick={this.onDeleteTicketClick.bind(this)}>{i18n('DELETE_TICKET')}</Button> : null}
|
||||
</div>
|
||||
<div className="ticket-viewer__buttons-column">
|
||||
<div className="ticket-viewer__buttons-row">
|
||||
{(this.showDeleteButton()) ? this.renderDeleteTicketButton() : null}
|
||||
{this.renderCloseTicketButton()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -246,10 +444,22 @@ class TicketViewer extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderDeleteTicketButton() {
|
||||
return (
|
||||
<Button className="ticket-viewer__delete-button" size="medium" onClick={this.onDeleteTicketClick.bind(this)}>{i18n('DELETE_TICKET')}</Button>
|
||||
);
|
||||
}
|
||||
|
||||
renderCloseTicketButton() {
|
||||
return (
|
||||
<Button size="medium" onClick={this.onCloseTicketClick.bind(this)}>{i18n('CLOSE_TICKET')}</Button>
|
||||
);
|
||||
}
|
||||
|
||||
renderCustomResponses() {
|
||||
let customResponsesNode = null;
|
||||
|
||||
if (this.props.customResponses && this.props.editable) {
|
||||
if(this.props.customResponses && this.props.editable) {
|
||||
let customResponses = this.props.customResponses.map((customResponse) => {
|
||||
return {
|
||||
content: customResponse.name
|
||||
@ -262,7 +472,7 @@ class TicketViewer extends React.Component {
|
||||
|
||||
customResponsesNode = (
|
||||
<div className="ticket-viewer__response-custom">
|
||||
<DropDown items={customResponses} size="medium" onChange={this.onCustomResponsesChanged.bind(this)}/>
|
||||
<DropDown items={customResponses} size="medium" onChange={this.onCustomResponsesChanged.bind(this)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -271,10 +481,10 @@ class TicketViewer extends React.Component {
|
||||
}
|
||||
|
||||
renderPrivate() {
|
||||
if (this.props.userStaff) {
|
||||
if(this.props.userStaff) {
|
||||
return (
|
||||
<div className="ticket-viewer__response-private">
|
||||
<FormField label={i18n('PRIVATE')} name="private" field="checkbox"/>
|
||||
<FormField label={i18n('PRIVATE')} name="private" field="checkbox" />
|
||||
<InfoTooltip className="ticket-viewer__response-private-info" text={i18n('PRIVATE_RESPONSE_DESCRIPTION')} />
|
||||
</div>
|
||||
);
|
||||
@ -289,6 +499,13 @@ class TicketViewer extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
searchTickets(filters) {
|
||||
const SEARCH_TICKETS_PATH = '/admin/panel/tickets/search-tickets';
|
||||
const urlQuery = filters && searchTicketsUtils.getFiltersForURL({filters});
|
||||
|
||||
return urlQuery && `${SEARCH_TICKETS_PATH}${urlQuery}`;
|
||||
}
|
||||
|
||||
getCommentFormProps() {
|
||||
return {
|
||||
onSubmit: this.onSubmit.bind(this),
|
||||
@ -312,11 +529,7 @@ class TicketViewer extends React.Component {
|
||||
}
|
||||
|
||||
onDepartmentDropdownChanged(event) {
|
||||
AreYouSure.openModal(null, this.changeDepartment.bind(this, event.index));
|
||||
}
|
||||
|
||||
onPriorityDropdownChanged(event) {
|
||||
AreYouSure.openModal(null, this.changePriority.bind(this, event.index));
|
||||
AreYouSure.openModal(null, this.changeDepartment.bind(this, this.getDepartmentsForTransfer()[event.index].id));
|
||||
}
|
||||
|
||||
onAssignmentChange(event) {
|
||||
@ -330,27 +543,39 @@ class TicketViewer extends React.Component {
|
||||
let APICallPromise = new Promise(resolve => resolve());
|
||||
|
||||
if(owner) {
|
||||
APICallPromise.then(() => API.call({
|
||||
APICallPromise = APICallPromise.then(() => API.call({
|
||||
path: '/staff/un-assign-ticket',
|
||||
data: { ticketNumber }
|
||||
}));
|
||||
}
|
||||
|
||||
if(id !== 0) {
|
||||
APICallPromise.then(() => API.call({
|
||||
APICallPromise = APICallPromise.then(() => API.call({
|
||||
path: '/staff/assign-ticket',
|
||||
data: { ticketNumber, staffId: id }
|
||||
}));
|
||||
}
|
||||
|
||||
APICallPromise.then(this.onTicketModification.bind(this));
|
||||
this.setState({
|
||||
editOwner: false
|
||||
});
|
||||
|
||||
return APICallPromise.then(this.onTicketModification.bind(this));
|
||||
}
|
||||
|
||||
onReopenClick() {
|
||||
this.setState({
|
||||
editStatus: false
|
||||
});
|
||||
|
||||
AreYouSure.openModal(null, this.reopenTicket.bind(this));
|
||||
}
|
||||
|
||||
onCloseTicketClick(event) {
|
||||
this.setState({
|
||||
editStatus: false
|
||||
});
|
||||
|
||||
event.preventDefault();
|
||||
AreYouSure.openModal(null, this.closeTicket.bind(this));
|
||||
}
|
||||
@ -360,8 +585,33 @@ class TicketViewer extends React.Component {
|
||||
AreYouSure.openModal(null, this.deleteTicket.bind(this));
|
||||
}
|
||||
|
||||
reopenTicket() {
|
||||
changeTitle(){
|
||||
this.setState({
|
||||
editTitleLoading: true
|
||||
});
|
||||
API.call({
|
||||
path: '/ticket/edit-title',
|
||||
data: {
|
||||
ticketNumber: this.props.ticket.ticketNumber,
|
||||
title: this.state.newTitle
|
||||
}
|
||||
}).then(() => {
|
||||
this.setState({
|
||||
editTitle: false,
|
||||
editTitleError: false,
|
||||
editTitleLoading: false
|
||||
});
|
||||
this.onTicketModification();
|
||||
}).catch((result) => {
|
||||
this.setState({
|
||||
editTitleError: i18n(result.message),
|
||||
editTitleLoading: false
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
reopenTicket() {
|
||||
return API.call({
|
||||
path: '/ticket/re-open',
|
||||
data: {
|
||||
ticketNumber: this.props.ticket.ticketNumber
|
||||
@ -370,7 +620,7 @@ class TicketViewer extends React.Component {
|
||||
}
|
||||
|
||||
closeTicket() {
|
||||
API.call({
|
||||
return API.call({
|
||||
path: '/ticket/close',
|
||||
data: {
|
||||
ticketNumber: this.props.ticket.ticketNumber
|
||||
@ -379,38 +629,75 @@ class TicketViewer extends React.Component {
|
||||
}
|
||||
|
||||
deleteTicket() {
|
||||
API.call({
|
||||
return API.call({
|
||||
path: '/ticket/delete',
|
||||
data: {
|
||||
ticketNumber: this.props.ticket.ticketNumber
|
||||
}
|
||||
}).then(this.onTicketModification.bind(this));
|
||||
}).then((result) => {
|
||||
this.onTicketModification(result);
|
||||
history.push('/admin/panel/tickets/my-tickets/');
|
||||
});
|
||||
}
|
||||
|
||||
changeDepartment(index) {
|
||||
API.call({
|
||||
changeDepartment(departmentId) {
|
||||
const { userId, userDepartments, ticket } = this.props;
|
||||
|
||||
this.setState({
|
||||
editDepartment: false
|
||||
});
|
||||
|
||||
return API.call({
|
||||
path: '/ticket/change-department',
|
||||
data: {
|
||||
ticketNumber: this.props.ticket.ticketNumber,
|
||||
departmentId: this.getDepartmentsForTransfer()[index].id
|
||||
ticketNumber: ticket.ticketNumber,
|
||||
departmentId
|
||||
}
|
||||
}).then(this.onTicketModification.bind(this));
|
||||
}).then((_.some(userDepartments, {id: departmentId}) || (userId === (ticket.author.id*1))) ? this.onTicketModification.bind(this) : history.goBack());
|
||||
}
|
||||
|
||||
changePriority(index) {
|
||||
const priorities = [
|
||||
'low',
|
||||
'medium',
|
||||
'high'
|
||||
];
|
||||
|
||||
addTag(tag) {
|
||||
this.setState({
|
||||
tagSelectorLoading: true,
|
||||
})
|
||||
API.call({
|
||||
path: '/ticket/change-priority',
|
||||
path: '/ticket/add-tag',
|
||||
data: {
|
||||
ticketNumber: this.props.ticket.ticketNumber,
|
||||
priority: priorities[index]
|
||||
tagId: tag
|
||||
}
|
||||
}).then(this.onTicketModification.bind(this));
|
||||
})
|
||||
.then(() => {
|
||||
this.setState({
|
||||
tagSelectorLoading: false,
|
||||
});
|
||||
this.onTicketModification();
|
||||
})
|
||||
.catch(() => this.setState({
|
||||
tagSelectorLoading: false,
|
||||
}))
|
||||
}
|
||||
|
||||
removeTag(tag) {
|
||||
this.setState({
|
||||
tagSelectorLoading: true,
|
||||
});
|
||||
|
||||
API.call({
|
||||
path: '/ticket/remove-tag',
|
||||
data: {
|
||||
ticketNumber: this.props.ticket.ticketNumber,
|
||||
tagId: tag
|
||||
}
|
||||
}).then(() => {
|
||||
this.setState({
|
||||
tagSelectorLoading: false,
|
||||
});
|
||||
|
||||
this.onTicketModification();
|
||||
}).catch(() => this.setState({
|
||||
tagSelectorLoading: false,
|
||||
}))
|
||||
}
|
||||
|
||||
onCustomResponsesChanged({index}) {
|
||||
@ -428,6 +715,52 @@ class TicketViewer extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
onToggleEdit(ticketEventId){
|
||||
this.setState({
|
||||
edit: !this.state.edit,
|
||||
editId: ticketEventId
|
||||
})
|
||||
}
|
||||
|
||||
onEdit(ticketeventid,{content}) {
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
const data = {};
|
||||
|
||||
if(ticketeventid){
|
||||
data.ticketEventId = ticketeventid
|
||||
}else{
|
||||
data.ticketNumber = this.props.ticket.ticketNumber
|
||||
}
|
||||
|
||||
API.call({
|
||||
path: '/ticket/edit-comment',
|
||||
data: _.extend(
|
||||
data,
|
||||
TextEditor.getContentFormData(content)
|
||||
)
|
||||
}).then(this.onEditCommentSuccess.bind(this), this.onFailCommentFail.bind(this));
|
||||
}
|
||||
|
||||
onEditCommentSuccess() {
|
||||
this.setState({
|
||||
loading: false,
|
||||
commentError: false,
|
||||
commentEdited: false,
|
||||
edit:false
|
||||
});
|
||||
|
||||
this.onTicketModification();
|
||||
}
|
||||
|
||||
onFailCommentFail() {
|
||||
this.setState({
|
||||
loading: false,
|
||||
commentError: true
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit(formState) {
|
||||
this.setState({
|
||||
loading: true
|
||||
@ -437,7 +770,7 @@ class TicketViewer extends React.Component {
|
||||
path: '/ticket/comment',
|
||||
dataAsForm: true,
|
||||
data: _.extend({
|
||||
ticketNumber: this.props.ticket.ticketNumber
|
||||
ticketNumber: this.props.ticket.ticketNumber,
|
||||
}, formState, {private: formState.private ? 1 : 0}, TextEditor.getContentFormData(formState.content))
|
||||
}).then(this.onCommentSuccess.bind(this), this.onCommentFail.bind(this));
|
||||
}
|
||||
@ -461,60 +794,78 @@ class TicketViewer extends React.Component {
|
||||
}
|
||||
|
||||
onTicketModification() {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange();
|
||||
}
|
||||
const { onChange } = this.props;
|
||||
|
||||
onChange && onChange();
|
||||
}
|
||||
|
||||
getStaffAssignmentItems() {
|
||||
const {staffMembers, userDepartments, userId, ticket} = this.props;
|
||||
const ticketDepartmentId = ticket.department.id;
|
||||
const { userDepartments, userId, ticket } = this.props;
|
||||
let staffAssignmentItems = [
|
||||
{content: 'None', id: 0}
|
||||
{content: i18n('NONE'), contentOnSelected: i18n('NONE'), id: 0}
|
||||
];
|
||||
|
||||
if(_.any(userDepartments, {id: ticketDepartmentId})) {
|
||||
staffAssignmentItems.push({content: i18n('ASSIGN_TO_ME'), id: userId});
|
||||
if(_.some(userDepartments, {id: ticket.department.id})) {
|
||||
staffAssignmentItems.push({
|
||||
content: i18n('ASSIGN_TO_ME'),
|
||||
contentOnSelected: this.getCurrentStaff().name,
|
||||
id: userId
|
||||
});
|
||||
}
|
||||
|
||||
staffAssignmentItems = staffAssignmentItems.concat(
|
||||
_.map(
|
||||
_.filter(staffMembers, ({id, departments}) => {
|
||||
return (id != userId) && _.any(departments, {id: ticketDepartmentId});
|
||||
}),
|
||||
({id, name}) => ({content: name, id})
|
||||
this.getStaffList(),
|
||||
({id, name}) => ({content: name, contentOnSelected: name, id: id*1})
|
||||
)
|
||||
);
|
||||
|
||||
return staffAssignmentItems;
|
||||
}
|
||||
|
||||
getStaffList() {
|
||||
const { userId, staffMembers, ticket } = this.props;
|
||||
|
||||
return _.filter(staffMembers, ({id, departments}) => {
|
||||
return (id != userId) && _.some(departments, {id: ticket.department.id});
|
||||
})
|
||||
}
|
||||
|
||||
getCurrentStaff() {
|
||||
const { userId, staffMembers, ticket } = this.props;
|
||||
|
||||
return _.find(staffMembers, ({id}) => {return id == userId});
|
||||
}
|
||||
|
||||
getDepartmentsForTransfer() {
|
||||
return this.props.ticket.author.staff ? SessionStore.getDepartments() : this.getPublicDepartments();
|
||||
}
|
||||
|
||||
showDeleteButton() {
|
||||
if(!this.props.ticket.owner) {
|
||||
if(this.props.userLevel == 3) return true;
|
||||
if(this.props.userId == this.props.ticket.author.id) {
|
||||
if((this.props.userStaff && this.props.ticket.author.staff) || (!this.props.userStaff && !this.props.ticket.author.staff)){
|
||||
const { ticket, userLevel, userId, userStaff } = this.props;
|
||||
|
||||
if(!ticket.owner) {
|
||||
if(userLevel === 3) return true;
|
||||
if(userId == ticket.author.id*1) {
|
||||
if((userStaff && ticket.author.staff) || (!userStaff && !ticket.author.staff)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default connect((store) => {
|
||||
return {
|
||||
userId: store.session.userId,
|
||||
userId: store.session.userId*1,
|
||||
userStaff: store.session.staff,
|
||||
userDepartments: store.session.userDepartments,
|
||||
staffMembers: store.adminData.staffMembers,
|
||||
staffMembersLoaded: store.adminData.staffMembersLoaded,
|
||||
allowAttachments: store.config['allow-attachments'],
|
||||
userSystemEnabled: store.config['user-system-enabled'],
|
||||
userLevel: store.session.userLevel
|
||||
userLevel: store.session.userLevel*1,
|
||||
tags: store.config['tags'].map((tag) => {return {...tag, id: tag.id*1}})
|
||||
};
|
||||
})(TicketViewer);
|
||||
|
@ -1,7 +1,6 @@
|
||||
@import "../scss/vars";
|
||||
|
||||
.ticket-viewer {
|
||||
|
||||
&__header {
|
||||
background-color: $primary-blue;
|
||||
border-top-right-radius: 4px;
|
||||
@ -9,31 +8,148 @@
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
padding: 6px 0;
|
||||
display: flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
|
||||
&:hover {
|
||||
.ticket-viewer__edit-title-icon {
|
||||
color: $grey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__buttons-column {
|
||||
padding-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&__buttons-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
&__edited-title-text {
|
||||
font-style: italic;
|
||||
font-size: 14px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
&__edit-icon {
|
||||
right: 12px;
|
||||
margin: 0 10px;
|
||||
color: $light-grey;
|
||||
|
||||
&:hover {
|
||||
cursor:pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__edit-title-icon {
|
||||
color: $primary-blue;
|
||||
right: 12px;
|
||||
margin: 0 10px;
|
||||
|
||||
&:hover {
|
||||
cursor:pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__edit-status__buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
&___input-edit-title {
|
||||
color: black;
|
||||
align-items:center;
|
||||
justify-content: center;
|
||||
margin-bottom: 6px;
|
||||
margin-right: 6px;
|
||||
|
||||
.input__text {
|
||||
height: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
&__edit-title__buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
width: 160px;
|
||||
}
|
||||
&__number {
|
||||
color: white;
|
||||
margin-right: 10px;
|
||||
margin-right: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
display: inline-block;
|
||||
max-width: 375px;
|
||||
}
|
||||
|
||||
&__flag {
|
||||
margin-left: 10px;
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
&__info-row-header {
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
background-color: $light-grey;
|
||||
font-weight: bold;
|
||||
}
|
||||
padding: 0 15px 30px 15px;
|
||||
|
||||
&__info-row-values {
|
||||
background-color: $light-grey;
|
||||
color: $secondary-blue;
|
||||
padding-bottom: 10px;
|
||||
&-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
min-width: 150px;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
&-container-editable {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
min-width: 300px;
|
||||
|
||||
&:hover {
|
||||
.ticket-viewer__edit-icon {
|
||||
color: $primary-blue;
|
||||
}
|
||||
}
|
||||
|
||||
&__column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
min-width: 170px;
|
||||
}
|
||||
}
|
||||
|
||||
&-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&-value {
|
||||
color: $secondary-blue;
|
||||
padding-bottom: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__editable-dropdown {
|
||||
@ -92,12 +208,48 @@
|
||||
|
||||
&-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&__delete-button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1151px) {
|
||||
.ticket-viewer__info {
|
||||
&-container {
|
||||
width: 200px;
|
||||
min-width: unset;
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
&-container-editable {
|
||||
min-width: 250px;
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.ticket-viewer__info {
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 571px) {
|
||||
.ticket-viewer {
|
||||
&__number, &__edit-title-icon {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
&__flag {
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,15 +37,16 @@ class TopicEditModal extends React.Component {
|
||||
<FormField name="title" label={i18n('TITLE')} fieldProps={{size: 'large'}} validation="TITLE" required />
|
||||
<FormField name="icon" className="topic-edit-modal__icon" label={i18n('ICON')} decorator={IconSelector} />
|
||||
<FormField name="color" className="topic-edit-modal__color" label={i18n('COLOR')} decorator={ColorSelector} />
|
||||
<FormField className="topic-edit-modal__private" label={i18n('PRIVATE')} name="private" field="checkbox"/>
|
||||
<FormField className="topic-edit-modal__private" label={i18n('PRIVATE')} name="private" field="checkbox" />
|
||||
<InfoTooltip className="topic-edit-modal__private" text={i18n('PRIVATE_TOPIC_DESCRIPTION')} />
|
||||
|
||||
<SubmitButton className="topic-edit-modal__save-button" type="secondary" size="small">
|
||||
{i18n('SAVE')}
|
||||
</SubmitButton>
|
||||
<Button className="topic-edit-modal__discard-button" onClick={this.onDiscardClick.bind(this)} size="small">
|
||||
{i18n('CANCEL')}
|
||||
</Button>
|
||||
<div className="topic-edit-modal__buttons-container">
|
||||
<Button className="topic-edit-modal__discard-button" onClick={this.onDiscardClick.bind(this)} size="small">
|
||||
{i18n('CANCEL')}
|
||||
</Button>
|
||||
<SubmitButton className="topic-edit-modal__save-button" type="secondary" size="small">
|
||||
{i18n('SAVE')}
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
@ -13,9 +13,19 @@
|
||||
|
||||
}
|
||||
|
||||
&__discard-button {
|
||||
float: right;
|
||||
&__buttons-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 0 0 0;
|
||||
}
|
||||
|
||||
&__discard-button {
|
||||
float: left;
|
||||
}
|
||||
|
||||
&__private {
|
||||
display: inline-block;
|
||||
padding-right: 10px;
|
||||
|
@ -166,7 +166,7 @@ class TopicViewer extends React.Component {
|
||||
}
|
||||
|
||||
onDeleteClick() {
|
||||
API.call({
|
||||
return API.call({
|
||||
path: '/article/delete-topic',
|
||||
data: {
|
||||
topicId: this.props.id
|
||||
|
@ -50,7 +50,7 @@ class App extends React.Component {
|
||||
'application': true,
|
||||
'application_modal-opened': (this.props.modal.opened),
|
||||
'application_full-width': (this.props.config.layout === 'full-width' && !_.includes(this.props.location.pathname, '/admin')),
|
||||
'application_user-system': (this.props.config['user-system-enabled'])
|
||||
'application_mandatory-login': (this.props.config['mandatory-login'])
|
||||
};
|
||||
|
||||
return classNames(classes);
|
||||
@ -99,7 +99,7 @@ class App extends React.Component {
|
||||
history.push('/');
|
||||
}
|
||||
|
||||
if(props.config['user-system-enabled'] && _.includes(props.location.pathname, '/check-ticket')) {
|
||||
if(props.config['mandatory-login'] && _.includes(props.location.pathname, '/check-ticket')) {
|
||||
history.push('/');
|
||||
}
|
||||
|
||||
@ -111,7 +111,7 @@ class App extends React.Component {
|
||||
history.push('/admin');
|
||||
}
|
||||
|
||||
if(isProd && _.includes(props.location.pathname, '/components-demo')) {
|
||||
if(process.env.NODE_ENV === 'production' && _.includes(props.location.pathname, '/components-demo')) {
|
||||
history.push('/');
|
||||
}
|
||||
}
|
||||
|
@ -32,13 +32,14 @@ import AdminPanelMyAccount from 'app/admin/panel/dashboard/admin-panel-my-accoun
|
||||
|
||||
import AdminPanelMyTickets from 'app/admin/panel/tickets/admin-panel-my-tickets';
|
||||
import AdminPanelNewTickets from 'app/admin/panel/tickets/admin-panel-new-tickets';
|
||||
import AdminPanelAllTickets from 'app/admin/panel/tickets/admin-panel-all-tickets';
|
||||
import AdminPanelSearchTickets from 'app/admin/panel/tickets/admin-panel-search-tickets';
|
||||
import AdminPanelViewTicket from 'app/admin/panel/tickets/admin-panel-view-ticket';
|
||||
import AdminPanelCustomResponses from 'app/admin/panel/tickets/admin-panel-custom-responses';
|
||||
|
||||
import AdminPanelListUsers from 'app/admin/panel/users/admin-panel-list-users';
|
||||
import AdminPanelViewUser from 'app/admin/panel/users/admin-panel-view-user';
|
||||
import AdminPanelBanUsers from 'app/admin/panel/users/admin-panel-ban-users';
|
||||
import AdminPanelCustomFields from 'app/admin/panel/users/admin-panel-custom-fields';
|
||||
|
||||
import AdminPanelListArticles from 'app/admin/panel/articles/admin-panel-list-articles';
|
||||
import AdminPanelViewArticle from 'app/admin/panel/articles/admin-panel-view-article';
|
||||
@ -49,7 +50,8 @@ import AdminPanelViewStaff from 'app/admin/panel/staff/admin-panel-view-staff';
|
||||
|
||||
import AdminPanelSystemPreferences from 'app/admin/panel/settings/admin-panel-system-preferences';
|
||||
import AdminPanelAdvancedSettings from 'app/admin/panel/settings/admin-panel-advanced-settings';
|
||||
import AdminPanelEmailTemplates from 'app/admin/panel/settings/admin-panel-email-templates';
|
||||
import AdminPanelEmailSettings from 'app/admin/panel/settings/admin-panel-email-settings';
|
||||
import AdminPanelCustomTags from 'app/admin/panel/settings/admin-panel-custom-tags';
|
||||
|
||||
// INSTALLATION
|
||||
import InstallLayout from 'app/install/install-layout';
|
||||
@ -59,7 +61,7 @@ import InstallStep3Database from 'app/install/install-step-3-database';
|
||||
import InstallStep4UserSystem from 'app/install/install-step-4-user-system';
|
||||
import InstallStep5Settings from 'app/install/install-step-5-settings';
|
||||
import InstallStep6Admin from 'app/install/install-step-6-admin';
|
||||
import InstallStep7Completed from 'app/install/install-step-7-completed';
|
||||
import InstallCompleted from 'app/install/install-completed';
|
||||
|
||||
export default (
|
||||
<Router history={history}>
|
||||
@ -96,12 +98,12 @@ export default (
|
||||
<Route path="step-4" component={InstallStep4UserSystem} />
|
||||
<Route path="step-5" component={InstallStep5Settings} />
|
||||
<Route path="step-6" component={InstallStep6Admin} />
|
||||
<Route path="step-7" component={InstallStep7Completed} />
|
||||
<Route path="completed" component={InstallCompleted} />
|
||||
</Route>
|
||||
<Route path="admin">
|
||||
<IndexRoute component={AdminLoginPage} />
|
||||
<Route path="panel" component={AdminPanelLayout}>
|
||||
<IndexRedirect to="stats" />
|
||||
<IndexRedirect to="activity" />
|
||||
<Route path="stats" component={AdminPanelStats} />
|
||||
<Route path="activity" component={AdminPanelActivity} />
|
||||
<Route path="my-account" component={AdminPanelMyAccount} />
|
||||
@ -110,7 +112,7 @@ export default (
|
||||
<IndexRedirect to="my-tickets" />
|
||||
<Route path="my-tickets" component={AdminPanelMyTickets} />
|
||||
<Route path="new-tickets" component={AdminPanelNewTickets} />
|
||||
<Route path="all-tickets" component={AdminPanelAllTickets} />
|
||||
<Route path="search-tickets" component={AdminPanelSearchTickets} />
|
||||
<Route path="custom-responses" component={AdminPanelCustomResponses} />
|
||||
<Route path="view-ticket/:ticketNumber" component={AdminPanelViewTicket} />
|
||||
</Route>
|
||||
@ -120,6 +122,7 @@ export default (
|
||||
<Route path="list-users" component={AdminPanelListUsers} />
|
||||
<Route path="view-user/:userId" component={AdminPanelViewUser} />
|
||||
<Route path="ban-users" component={AdminPanelBanUsers} />
|
||||
<Route path="custom-fields" component={AdminPanelCustomFields} />
|
||||
</Route>
|
||||
|
||||
<Route path="articles">
|
||||
@ -139,7 +142,8 @@ export default (
|
||||
<IndexRedirect to="system-preferences" />
|
||||
<Route path="system-preferences" component={AdminPanelSystemPreferences} />
|
||||
<Route path="advanced-settings" component={AdminPanelAdvancedSettings} />
|
||||
<Route path="email-templates" component={AdminPanelEmailTemplates} />
|
||||
<Route path="email-settings" component={AdminPanelEmailSettings} />
|
||||
<Route path="custom-tags" component={AdminPanelCustomTags} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
|
@ -1,5 +1,5 @@
|
||||
export default {
|
||||
dispatch: stub(),
|
||||
dispatch: stub().returns(new Promise(r => r())),
|
||||
getState: stub().returns({
|
||||
config: {},
|
||||
session: {},
|
||||
|
@ -49,11 +49,32 @@ class AdminLoginPage extends React.Component {
|
||||
<div>
|
||||
<Widget className="admin-login-page__content">
|
||||
<div className="admin-login-page__image"><img width="100%" src={API.getURL() + '/images/logo.png'} alt="OpenSupports Admin Panel"/></div>
|
||||
<div className="admin-login-page__login-form">
|
||||
<Form onSubmit={this.onLoginFormSubmit.bind(this)} loading={this.props.session.pending}>
|
||||
<FormField name="email" label={i18n('EMAIL')} field="input" validation="EMAIL" fieldProps={{size:'large'}} required />
|
||||
<FormField name="password" label={i18n('PASSWORD')} field="input" fieldProps={{password:true, size:'large'}} />
|
||||
<SubmitButton>{i18n('LOG_IN')}</SubmitButton>
|
||||
<div className="admin-login-page__login-form-container">
|
||||
<Form {...this.getLoginFormProps()}>
|
||||
<div className="admin-login-page__login-form-container__login-form__fields">
|
||||
<FormField
|
||||
name="email"
|
||||
label={i18n('EMAIL')}
|
||||
className="admin-login-page__login-form-container__login-form__fields__email"
|
||||
field="input"
|
||||
validation="EMAIL"
|
||||
fieldProps={{size:'large'}}
|
||||
required />
|
||||
<FormField
|
||||
name="password"
|
||||
label={i18n('PASSWORD')}
|
||||
className="admin-login-page__login-form-container__login-form__fields__password"
|
||||
field="input"
|
||||
fieldProps={{password:true, size:'large'}} />
|
||||
<FormField
|
||||
name="remember"
|
||||
label={i18n('REMEMBER_ME')}
|
||||
className="admin-login-page__login-form-container__login-form__fields__remember"
|
||||
field="checkbox" />
|
||||
</div>
|
||||
<div className="admin-login-page__login-form-container__login-form__submit-button">
|
||||
<SubmitButton>{i18n('LOG_IN')}</SubmitButton>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
{this.renderRecoverStatus()}
|
||||
@ -68,7 +89,7 @@ class AdminLoginPage extends React.Component {
|
||||
|
||||
renderPasswordRecovery() {
|
||||
return (
|
||||
<div>
|
||||
<div className="admin-login-page__recovery-form-container">
|
||||
<PasswordRecovery recoverSent={this.state.recoverSent} formProps={this.getRecoverFormProps()} onBackToLoginClick={this.onBackToLoginClick.bind(this)} renderLogo={true}/>
|
||||
</div>
|
||||
);
|
||||
@ -105,7 +126,7 @@ class AdminLoginPage extends React.Component {
|
||||
getLoginFormProps() {
|
||||
return {
|
||||
loading: this.props.session.pending,
|
||||
className: 'admin-login-page__form',
|
||||
className: 'admin-login-page__login-form-container__login-form',
|
||||
ref: 'loginForm',
|
||||
onSubmit: this.onLoginFormSubmit.bind(this),
|
||||
errors: this.getLoginFormErrors(),
|
||||
@ -114,12 +135,17 @@ class AdminLoginPage extends React.Component {
|
||||
}
|
||||
|
||||
getRecoverFormProps() {
|
||||
const {
|
||||
loadingRecover,
|
||||
recoverFormErrors
|
||||
} = this.state;
|
||||
|
||||
return {
|
||||
loading: this.state.loadingRecover,
|
||||
className: 'admin-login-page__form',
|
||||
loading: loadingRecover,
|
||||
className: 'admin-login-page__recovery-form-container__recovery-form',
|
||||
ref: 'recoverForm',
|
||||
onSubmit: this.onForgotPasswordSubmit.bind(this),
|
||||
errors: this.state.recoverFormErrors,
|
||||
errors: recoverFormErrors,
|
||||
onValidateErrors: this.onRecoverFormErrorsValidation.bind(this)
|
||||
};
|
||||
}
|
||||
|
@ -19,9 +19,13 @@
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
&__login-form {
|
||||
&__login-form-container {
|
||||
margin: 0 auto;
|
||||
display: inline-block;
|
||||
|
||||
&__login-form__fields {
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__error {
|
||||
|
@ -29,7 +29,7 @@ class AdminPanel extends React.Component {
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-md-12 admin-panel-layout__content">
|
||||
<Widget>
|
||||
<Widget className='admin-panel-layout__content__widget'>
|
||||
{this.props.children}
|
||||
</Widget>
|
||||
</div>
|
||||
|
@ -4,4 +4,10 @@
|
||||
&__header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 415px) {
|
||||
.admin-panel-layout__content__widget {
|
||||
padding: 20px 5px;
|
||||
}
|
||||
}
|
||||
}
|
@ -2,10 +2,13 @@ import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {dispatch} from 'app/store';
|
||||
import i18n from 'lib-app/i18n';
|
||||
|
||||
import Menu from 'core-components/menu';
|
||||
import queryString from 'query-string';
|
||||
|
||||
const INITIAL_PAGE = 1;
|
||||
|
||||
|
||||
class AdminPanelMenu extends React.Component {
|
||||
static contextTypes = {
|
||||
@ -70,15 +73,32 @@ class AdminPanelMenu extends React.Component {
|
||||
|
||||
onGroupItemClick(index) {
|
||||
const group = this.getRoutes()[this.getGroupIndex()];
|
||||
const item = group.items[index];
|
||||
|
||||
this.context.router.push(group.items[index].path);
|
||||
this.context.router.push(item.path);
|
||||
item.onItemClick && item.onItemClick();
|
||||
}
|
||||
|
||||
getGroupItemIndex() {
|
||||
const { location } = this.props;
|
||||
const search = window.location.search;
|
||||
const filtersInURL = queryString.parse(search);
|
||||
const group = this.getRoutes()[this.getGroupIndex()];
|
||||
const pathname = this.props.location.pathname;
|
||||
const pathname = location.pathname + location.search;
|
||||
const SEARCH_TICKETS_PATH = '/admin/panel/tickets/search-tickets';
|
||||
|
||||
return _.findIndex(group.items, {path: pathname});
|
||||
return (
|
||||
_.findIndex(
|
||||
group.items,
|
||||
(item) => {
|
||||
if(location.pathname === SEARCH_TICKETS_PATH) {
|
||||
const customTicketsListNumber = queryString.parse(item.path.slice(SEARCH_TICKETS_PATH.length)).custom;
|
||||
return item.path.includes(SEARCH_TICKETS_PATH) && customTicketsListNumber === filtersInURL.custom;
|
||||
}
|
||||
return item.path === pathname;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getGroupIndex() {
|
||||
@ -90,8 +110,24 @@ class AdminPanelMenu extends React.Component {
|
||||
return (groupIndex === -1) ? 0 : groupIndex;
|
||||
}
|
||||
|
||||
getCustomlists() {
|
||||
if(window.customTicketList){
|
||||
return window.customTicketList.map((item, index) => {
|
||||
return {
|
||||
name: item.title,
|
||||
path: `/admin/panel/tickets/search-tickets?custom=${index}&page=${INITIAL_PAGE}&useInitialValues=true`,
|
||||
level: 1,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
getRoutes() {
|
||||
return this.getItemsByFilteredByLevel([
|
||||
const customLists = this.getCustomlists();
|
||||
|
||||
return this.getItemsByFilteredByLevel(_.without([
|
||||
{
|
||||
groupName: i18n('DASHBOARD'),
|
||||
path: '/admin/panel',
|
||||
@ -99,13 +135,13 @@ class AdminPanelMenu extends React.Component {
|
||||
level: 1,
|
||||
items: this.getItemsByFilteredByLevel([
|
||||
{
|
||||
name: i18n('STATISTICS'),
|
||||
path: '/admin/panel/stats',
|
||||
name: i18n('LAST_ACTIVITY'),
|
||||
path: '/admin/panel/activity',
|
||||
level: 1
|
||||
},
|
||||
{
|
||||
name: i18n('LAST_ACTIVITY'),
|
||||
path: '/admin/panel/activity',
|
||||
name: i18n('STATISTICS'),
|
||||
path: '/admin/panel/stats',
|
||||
level: 1
|
||||
}
|
||||
])
|
||||
@ -127,15 +163,16 @@ class AdminPanelMenu extends React.Component {
|
||||
level: 1
|
||||
},
|
||||
{
|
||||
name: i18n('ALL_TICKETS'),
|
||||
path: '/admin/panel/tickets/all-tickets',
|
||||
level: 1
|
||||
name: i18n('SEARCH_TICKETS'),
|
||||
path: `/admin/panel/tickets/search-tickets?page=${INITIAL_PAGE}&useInitialValues=true`,
|
||||
level: 1,
|
||||
},
|
||||
{
|
||||
name: i18n('CUSTOM_RESPONSES'),
|
||||
path: '/admin/panel/tickets/custom-responses',
|
||||
level: 2
|
||||
}
|
||||
},
|
||||
...customLists
|
||||
])
|
||||
},
|
||||
{
|
||||
@ -153,6 +190,11 @@ class AdminPanelMenu extends React.Component {
|
||||
name: i18n('BAN_USERS'),
|
||||
path: '/admin/panel/users/ban-users',
|
||||
level: 1
|
||||
},
|
||||
{
|
||||
name: i18n('CUSTOM_FIELDS'),
|
||||
path: '/admin/panel/users/custom-fields',
|
||||
level: 1
|
||||
}
|
||||
])
|
||||
},
|
||||
@ -170,7 +212,6 @@ class AdminPanelMenu extends React.Component {
|
||||
])
|
||||
},
|
||||
{
|
||||
|
||||
groupName: i18n('STAFF'),
|
||||
path: '/admin/panel/staff',
|
||||
icon: 'users',
|
||||
@ -206,13 +247,18 @@ class AdminPanelMenu extends React.Component {
|
||||
level: 3
|
||||
},
|
||||
{
|
||||
name: i18n('EMAIL_TEMPLATES'),
|
||||
path: '/admin/panel/settings/email-templates',
|
||||
name: i18n('EMAIL_SETTINGS'),
|
||||
path: '/admin/panel/settings/email-settings',
|
||||
level: 3
|
||||
},
|
||||
{
|
||||
name: i18n('CUSTOM_TAGS'),
|
||||
path: '/admin/panel/settings/custom-tags',
|
||||
level: 3
|
||||
}
|
||||
])
|
||||
}
|
||||
]);
|
||||
], null));
|
||||
}
|
||||
|
||||
getItemsByFilteredByLevel(items) {
|
||||
@ -222,6 +268,8 @@ class AdminPanelMenu extends React.Component {
|
||||
|
||||
export default connect((store) => {
|
||||
return {
|
||||
level: store.session.userLevel
|
||||
level: store.session.userLevel,
|
||||
config: store.config,
|
||||
searchFilters: store.searchFilters,
|
||||
};
|
||||
})(AdminPanelMenu);
|
||||
|
@ -3,4 +3,11 @@
|
||||
&__list {
|
||||
padding: 0 50px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 415px) {
|
||||
.admin-panel-list-articles__list {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -66,17 +66,17 @@ class AdminPanelViewArticle extends React.Component {
|
||||
return (
|
||||
<div className="admin-panel-view-article__content">
|
||||
<div className="admin-panel-view-article__edit-buttons">
|
||||
<Button className="admin-panel-view-article__edit-button" size="medium" onClick={this.onEditClick.bind(this, article)} type="tertiary">
|
||||
{i18n('EDIT')}
|
||||
</Button>
|
||||
<Button size="medium" onClick={this.onDeleteClick.bind(this, article)}>
|
||||
{i18n('DELETE')}
|
||||
</Button>
|
||||
<Button className="admin-panel-view-article__edit-button" size="medium" onClick={this.onEditClick.bind(this, article)} type="tertiary">
|
||||
{i18n('EDIT')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="admin-panel-view-article__article">
|
||||
<Header title={article.title}/>
|
||||
|
||||
<div className="admin-panel-view-article__article-content">
|
||||
<div className="admin-panel-view-article__article-content ql-editor">
|
||||
<div dangerouslySetInnerHTML={{__html: MentionsParser.parse(article.content)}}/>
|
||||
</div>
|
||||
<div className="admin-panel-view-article__last-edited">
|
||||
@ -91,13 +91,13 @@ class AdminPanelViewArticle extends React.Component {
|
||||
return (
|
||||
<Form values={this.state.form} onChange={(form) => this.setState({form})} onSubmit={this.onFormSubmit.bind(this)}>
|
||||
<div className="admin-panel-view-article__buttons">
|
||||
<SubmitButton className="admin-panel-view-article__button" type="secondary" size="medium">{i18n('SAVE')}</SubmitButton>
|
||||
<Button className="admin-panel-view-article__button" size="medium" onClick={this.onFormCancel.bind(this)}>
|
||||
{i18n('CANCEL')}
|
||||
</Button>
|
||||
<SubmitButton className="admin-panel-view-article__button" type="secondary" size="medium">{i18n('SAVE')}</SubmitButton>
|
||||
</div>
|
||||
<FormField name="title" label={i18n('TITLE')} />
|
||||
<FormField name="content" label={i18n('CONTENT')} field="textarea" fieldProps={{allowImages: this.props.allowAttachments}}/>
|
||||
<FormField name="content" label={i18n('CONTENT')} field="textarea" validation="TEXT_AREA" required fieldProps={{allowImages: this.props.allowAttachments}}/>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@ -153,7 +153,7 @@ class AdminPanelViewArticle extends React.Component {
|
||||
}
|
||||
|
||||
onArticleDeleted(article) {
|
||||
API.call({
|
||||
return API.call({
|
||||
path: '/article/delete',
|
||||
data: {
|
||||
articleId: article.id
|
||||
|
@ -1,7 +1,15 @@
|
||||
.admin-panel-view-article {
|
||||
|
||||
&__content {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
&__edit-buttons {
|
||||
text-align: left;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 200px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@ -19,8 +27,8 @@
|
||||
text-align: left;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
&__button {
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ class AdminPanelActivity extends React.Component {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
getMenuProps() {
|
||||
return {
|
||||
className: 'admin-panel-activity__menu',
|
||||
@ -148,4 +148,4 @@ class AdminPanelActivity extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminPanelActivity;
|
||||
export default AdminPanelActivity;
|
||||
|
@ -3,7 +3,6 @@ import {connect} from 'react-redux';
|
||||
import _ from 'lodash';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import API from 'lib-app/api-call';
|
||||
import SessionActions from 'actions/session-actions';
|
||||
|
||||
import StaffEditor from 'app/admin/panel/staff/staff-editor';
|
||||
|
@ -1,20 +1,288 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Bar, HorizontalBar } from 'react-chartjs-2';
|
||||
|
||||
import date from 'lib-app/date';
|
||||
import API from 'lib-app/api-call';
|
||||
import i18n from 'lib-app/i18n';
|
||||
import Stats from 'app-components/stats';
|
||||
import StatCard from 'app-components/stat-card';
|
||||
|
||||
import Header from 'core-components/header';
|
||||
import Tooltip from 'core-components/tooltip';
|
||||
import Form from 'core-components/form';
|
||||
import FormField from 'core-components/form-field';
|
||||
import Icon from 'core-components/icon';
|
||||
import Loading from 'core-components/loading';
|
||||
import SubmitButton from 'core-components/submit-button';
|
||||
import Button from 'core-components/button';
|
||||
|
||||
class AdminPanelStats extends React.Component {
|
||||
|
||||
state = {
|
||||
loading: true,
|
||||
rawForm: {
|
||||
dateRange: this.getInitialDateRange(),
|
||||
departments: [],
|
||||
owners: [],
|
||||
tags: []
|
||||
},
|
||||
ticketData: {}
|
||||
};
|
||||
|
||||
getInitialDateRange() {
|
||||
let firstDayOfMonth = new Date();
|
||||
firstDayOfMonth.setDate(1);
|
||||
firstDayOfMonth.setHours(0);
|
||||
firstDayOfMonth.setMinutes(0);
|
||||
let todayAtNight = new Date();
|
||||
todayAtNight.setHours(23);
|
||||
todayAtNight.setMinutes(59);
|
||||
return {
|
||||
startDate: date.getFullDate(firstDayOfMonth),
|
||||
endDate: date.getFullDate(todayAtNight)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.retrieveStats();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
loading,
|
||||
rawForm
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<div className="admin-panel-stats">
|
||||
<Header title={i18n('STATISTICS')} description={i18n('STATISTICS_DESCRIPTION')}/>
|
||||
<Stats type="general"/>
|
||||
<Header title={i18n('STATISTICS')} description={i18n('STATISTICS_DESCRIPTION')} />
|
||||
<Form className="admin-panel-stats__form" loading={loading} values={rawForm} onChange={this.onFormChange.bind(this)} onSubmit={this.onFormSubmit.bind(this)}>
|
||||
<div className="admin-panel-stats__form__container">
|
||||
<div className="admin-panel-stats__form__container__row">
|
||||
<div className="admin-panel-stats__form__container__col">
|
||||
<FormField name="dateRange" label={i18n('DATE')} field="date-range" fieldProps={{defaultValue: rawForm.dateRange}} />
|
||||
<FormField name="tags" label={i18n('TAGS')} field="tag-selector" fieldProps={{items: this.getTagItems()}} />
|
||||
</div>
|
||||
<div className="admin-panel-stats__form__container__col">
|
||||
<FormField name="departments" label={i18n('DEPARTMENTS')} field="autocomplete" fieldProps={{items: this.getDepartmentsItems()}} />
|
||||
<FormField name="owners" label={i18n('OWNER')} field="autocomplete" fieldProps={{items: this.getStaffItems()}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-panel-stats__container">
|
||||
<Button
|
||||
className="admin-panel-stats__container__button admin-panel-stats__container__clear-button"
|
||||
size= "medium"
|
||||
disabled={loading}
|
||||
onClick={this.clearFormValues.bind(this)}>
|
||||
{loading ? <Loading /> : i18n('CLEAR')}
|
||||
</Button>
|
||||
<SubmitButton
|
||||
className="admin-panel-stats__container__button admin-panel-stats__container__apply-button"
|
||||
type="secondary"
|
||||
size= "medium">
|
||||
{i18n('APPLY')}
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</Form>
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<span className="separator" />
|
||||
</div>
|
||||
</div>
|
||||
{loading ? <div className="admin-panel-stats__loading"><Loading backgrounded size="large" /></div> : this.renderStatistics()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderStatistics() {
|
||||
const primaryBlueWithTransparency = (alpha) => `rgba(32, 184, 197, ${alpha})`;
|
||||
const ticketsByHoursChartData = {
|
||||
labels: Array.from(Array(24).keys()),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Created Tickets by Hour',
|
||||
backgroundColor: primaryBlueWithTransparency(0.2),
|
||||
borderColor: primaryBlueWithTransparency(1),
|
||||
borderWidth: 1,
|
||||
hoverBackgroundColor: primaryBlueWithTransparency(0.4),
|
||||
hoverBorderColor: primaryBlueWithTransparency(1),
|
||||
data: this.state.ticketData.created_by_hour
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const primaryGreenWithTransparency = (alpha) => `rgba(130, 202, 156, ${alpha})`;
|
||||
const ticketsByWeekdayChartData = {
|
||||
labels: [
|
||||
i18n('MONDAY'),
|
||||
i18n('TUESDAY'),
|
||||
i18n('WEDNESDAY'),
|
||||
i18n('THURSDAY'),
|
||||
i18n('FRIDAY'),
|
||||
i18n('SATURDAY'),
|
||||
i18n('SUNDAY')
|
||||
],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Created Tickets by Weekday',
|
||||
backgroundColor: primaryGreenWithTransparency(0.2),
|
||||
borderColor: primaryGreenWithTransparency(1),
|
||||
borderWidth: 1,
|
||||
hoverBackgroundColor: primaryGreenWithTransparency(0.4),
|
||||
hoverBorderColor: primaryGreenWithTransparency(1),
|
||||
data: this.state.ticketData.created_by_weekday
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{this.renderStatCards()}
|
||||
<Bar
|
||||
options={this.getStatsOptions('y')}
|
||||
data={ticketsByHoursChartData}
|
||||
legend={{onClick: null}} /> {/* Weird, but if you only set the legend here, it changes that of the HorizontalBar next too*/}
|
||||
<HorizontalBar
|
||||
options={this.getStatsOptions('x')}
|
||||
data={ticketsByWeekdayChartData}
|
||||
legend={{onClick: null}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderStatCards() {
|
||||
const {created, open, closed, instant, reopened} = this.state.ticketData;
|
||||
|
||||
return (
|
||||
<div className="admin-panel-stats__card-list">
|
||||
<StatCard label={i18n('CREATED')} description={i18n('CREATED_DESCRIPTION')} value={created} isPercentage={false} />
|
||||
<StatCard label={i18n('OPEN')} description={i18n('OPEN_DESCRIPTION')} value={open} isPercentage={false} />
|
||||
<StatCard label={i18n('CLOSED')} description={i18n('CLOSED_DESCRIPTION')} value={closed} isPercentage={false} />
|
||||
<StatCard label={i18n('INSTANT')} description={i18n('INSTANT_DESCRIPTION')} value={100*instant / closed} isPercentage={true} />
|
||||
<StatCard label={i18n('REOPENED')} description={i18n('REOPENED_DESCRIPTION')} value={100*reopened / created} isPercentage={true} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
getStatsOptions(axis) {
|
||||
return {
|
||||
scales: {
|
||||
[`${axis}Axes`]: [{
|
||||
ticks: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearFormValues(event) {
|
||||
event.preventDefault();
|
||||
this.setState({
|
||||
rawForm: {
|
||||
dateRange: this.getInitialDateRange(),
|
||||
departments: [],
|
||||
owners: [],
|
||||
tags: []
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getTagItems() {
|
||||
return this.props.tags.map((tag) => {
|
||||
return {
|
||||
id: JSON.parse(tag.id),
|
||||
name: tag.name,
|
||||
color : tag.color
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getSelectedTagIds() {
|
||||
return this.props.tags.filter(tag => _.includes(this.state.rawForm.tags, tag.name)).map(tag => tag.id);
|
||||
}
|
||||
|
||||
getStaffItems() {
|
||||
const getStaffProfilePic = (staff) => {
|
||||
return staff.profilePic ? API.getFileLink(staff.profilePic) : (API.getURL() + '/images/profile.png');
|
||||
}
|
||||
|
||||
const renderStaffItem = (staff, style) => {
|
||||
return (
|
||||
<div className={`admin-panel-stats__staff-${style}`} key={`staff-${style}-${staff.id}`}>
|
||||
<img className={`admin-panel-stats__staff-${style}__profile-pic`} src={getStaffProfilePic(staff)} />
|
||||
<span className={`admin-panel-stats__staff-${style}__name`}>{staff.name}</span>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
const { staffList } = this.props;
|
||||
let newStaffList = staffList.map(staff => {
|
||||
return {
|
||||
id: JSON.parse(staff.id),
|
||||
name: staff.name.toLowerCase(),
|
||||
color: 'gray',
|
||||
contentOnSelected: renderStaffItem(staff, 'selected'),
|
||||
content: renderStaffItem(staff, 'option'),
|
||||
}
|
||||
});
|
||||
|
||||
return newStaffList;
|
||||
}
|
||||
|
||||
getDepartmentsItems() {
|
||||
const renderDepartmentItem = (department, style) => {
|
||||
return (
|
||||
<div className={`admin-panel-stats__department-${style}`} key={`department-${style}-${department.id}`}>
|
||||
{department.private*1 ? <Icon className={`admin-panel-stats__department-${style}__icon`} name='user-secret' /> : null}
|
||||
<span className={`admin-panel-stats__department-${style}__name`}>{department.name}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return this.props.departments.map(department => {
|
||||
return {
|
||||
id: JSON.parse(department.id),
|
||||
name: department.name.toLowerCase(),
|
||||
color: 'gray',
|
||||
contentOnSelected: renderDepartmentItem(department, 'selected'),
|
||||
content: renderDepartmentItem(department, 'option'),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
retrieveStats() {
|
||||
const { rawForm } = this.state;
|
||||
const { startDate, endDate } = rawForm.dateRange;
|
||||
API.call({
|
||||
path: '/system/get-stats',
|
||||
data: {
|
||||
dateRange: "[" + startDate.toString() + "," + endDate.toString() + "]",
|
||||
departments: "[" + rawForm.departments.map(department => department.id) + "]",
|
||||
owners: "[" + rawForm.owners.map(owner => owner.id) + "]",
|
||||
tags: "[" + this.getSelectedTagIds() + "]"
|
||||
}
|
||||
}).then(({data}) => {
|
||||
this.setState({ticketData: data, loading: false});
|
||||
}).catch((error) => {
|
||||
if (showLogs) console.error('ERROR: ', error);
|
||||
})
|
||||
}
|
||||
|
||||
onFormChange(newFormState) {
|
||||
this.setState({rawForm: newFormState});
|
||||
}
|
||||
|
||||
onFormSubmit() {
|
||||
this.retrieveStats();
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminPanelStats;
|
||||
export default connect((store) => {
|
||||
return {
|
||||
tags: store.config.tags,
|
||||
departments: store.config.departments,
|
||||
staffList: store.adminData.staffMembers
|
||||
};
|
||||
})(AdminPanelStats);
|
||||
|
@ -0,0 +1,110 @@
|
||||
@import "../../../../scss/vars";
|
||||
|
||||
.admin-panel-stats {
|
||||
text-align: left;
|
||||
|
||||
&__form {
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
&__col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
&__button {
|
||||
margin: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__loading {
|
||||
min-height: 361px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: $grey;
|
||||
}
|
||||
|
||||
&__card-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
&__department-option { // Duplicated from ticket-query-filters, please REMOVE
|
||||
&__name {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__department-selected { // Duplicated from ticket-query-filters, please REMOVE
|
||||
display: inline-block;
|
||||
border-radius: 3px;
|
||||
margin-left: 5px;
|
||||
padding: 1px;
|
||||
font-size: 13px;
|
||||
cursor: default;
|
||||
|
||||
&__icon {
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__staff-option { // Duplicated from ticket-query-filters, please REMOVE
|
||||
&__profile-pic {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__name {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__staff-selected { // Duplicated from ticket-query-filters, please REMOVE
|
||||
display: inline-block;
|
||||
border-radius: 3px;
|
||||
margin-left: 5px;
|
||||
padding: 1px;
|
||||
font-size: 13px;
|
||||
cursor: default;
|
||||
|
||||
&__profile-pic {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.admin-panel-stats__form__container__row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@ import {connect} from 'react-redux';
|
||||
import ConfigActions from 'actions/config-actions';
|
||||
import API from 'lib-app/api-call';
|
||||
import i18n from 'lib-app/i18n';
|
||||
import ToggleButton from 'app-components/toggle-button';
|
||||
import AreYouSure from 'app-components/are-you-sure';
|
||||
import ModalContainer from 'app-components/modal-container';
|
||||
|
||||
@ -17,6 +16,7 @@ import Listing from 'core-components/listing';
|
||||
import Form from 'core-components/form';
|
||||
import FormField from 'core-components/form-field';
|
||||
import SubmitButton from 'core-components/submit-button';
|
||||
import Checkbox from 'core-components/checkbox';
|
||||
|
||||
class AdminPanelAdvancedSettings extends React.Component {
|
||||
|
||||
@ -25,10 +25,9 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
messageTitle: null,
|
||||
messageType: '',
|
||||
messageContent: '',
|
||||
keyName: '',
|
||||
keyCode: '',
|
||||
selectedAPIKey: -1,
|
||||
APIKeys: []
|
||||
APIKeys: [],
|
||||
error: ''
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@ -36,24 +35,33 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { config } = this.props;
|
||||
const { messageType, error, selectedAPIKey } = this.state;
|
||||
|
||||
return (
|
||||
<div className="admin-panel-advanced-settings">
|
||||
<Header title={i18n('ADVANCED_SETTINGS')} description={i18n('ADVANCED_SETTINGS_DESCRIPTION')}/>
|
||||
{(this.state.messageType) ? this.renderMessage() : null}
|
||||
<Header title={i18n('ADVANCED_SETTINGS')} description={i18n('ADVANCED_SETTINGS_DESCRIPTION')} />
|
||||
{messageType ? this.renderMessage() : null}
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<div className="col-md-6">
|
||||
<div className="admin-panel-advanced-settings__user-system-enabled">
|
||||
<span className="admin-panel-advanced-settings__text">
|
||||
{i18n('ENABLE_USER_SYSTEM')} <InfoTooltip text={i18n('ENABLE_USER_SYSTEM_DESCRIPTION')} />
|
||||
</span>
|
||||
<ToggleButton className="admin-panel-advanced-settings__toggle-button" value={this.props.config['user-system-enabled']} onChange={this.onToggleButtonUserSystemChange.bind(this)}/>
|
||||
</div>
|
||||
<div className="col-md-6 admin-panel-advanced-settings__mandatory-login">
|
||||
<Checkbox
|
||||
label={i18n('ENABLE_MANDATORY_LOGIN')}
|
||||
disabled={!config['registration']}
|
||||
className="admin-panel-advanced-settings__mandatory-login__checkbox"
|
||||
value={config['mandatory-login']}
|
||||
onChange={this.onCheckboxMandatoryLoginChange.bind(this)}
|
||||
wrapInLabel />
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="admin-panel-advanced-settings__registration">
|
||||
<span className="admin-panel-advanced-settings__text">{i18n('ENABLE_USER_REGISTRATION')}</span>
|
||||
<ToggleButton className="admin-panel-advanced-settings__toggle-button" value={this.props.config['registration']} onChange={this.onToggleButtonRegistrationChange.bind(this)}/>
|
||||
<Checkbox
|
||||
label={i18n('ENABLE_USER_REGISTRATION')}
|
||||
disabled={!config['mandatory-login']}
|
||||
className="admin-panel-advanced-settings__registration__checkbox"
|
||||
value={config['registration']}
|
||||
onChange={this.onCheckboxRegistrationChange.bind(this)}
|
||||
wrapInLabel />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -65,7 +73,7 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
<div className="admin-panel-advanced-settings__text">
|
||||
{i18n('INCLUDE_USERS_VIA_CSV')} <InfoTooltip text={i18n('CSV_DESCRIPTION')} />
|
||||
</div>
|
||||
<FileUploader className="admin-panel-advanced-settings__button" text="Upload" onChange={this.onImportCSV.bind(this)}/>
|
||||
<FileUploader className="admin-panel-advanced-settings__button" text="Upload" onChange={this.onImportCSV.bind(this)} />
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<div className="admin-panel-advanced-settings__text">{i18n('BACKUP_DATABASE')}</div>
|
||||
@ -84,8 +92,8 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
<div className="col-md-4">
|
||||
<Listing {...this.getListingProps()} />
|
||||
</div>
|
||||
<div className="col-md-8">
|
||||
{(this.state.selectedAPIKey === -1) ? this.renderNoKey() : this.renderKey()}
|
||||
<div className="col-md-8 admin-panel-advanced-settings__api-keys__container">
|
||||
{error ? <Message type="error">{i18n(error)}</Message> : ((selectedAPIKey === -1) ? this.renderNoKey() : this.renderKey())}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -94,8 +102,12 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
}
|
||||
|
||||
renderMessage() {
|
||||
const { messageType, messageTitle, messageContent } = this.state;
|
||||
|
||||
return (
|
||||
<Message type={this.state.messageType} title={this.state.messageTitle}>{this.state.messageContent}</Message>
|
||||
<Message className="admin-panel-advanced-settings__message" type={messageType} title={messageTitle}>
|
||||
{messageContent}
|
||||
</Message>
|
||||
);
|
||||
}
|
||||
|
||||
@ -108,14 +120,31 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
}
|
||||
|
||||
renderKey() {
|
||||
let currentAPIKey = this.state.APIKeys[this.state.selectedAPIKey];
|
||||
const { APIKeys, selectedAPIKey } = this.state;
|
||||
const {
|
||||
name,
|
||||
token,
|
||||
canCreateTickets,
|
||||
shouldReturnTicketNumber,
|
||||
canCheckTickets,
|
||||
canCreateUser
|
||||
} = APIKeys[selectedAPIKey];
|
||||
|
||||
return (
|
||||
<div className="admin-panel-advanced-settings__api-keys-info">
|
||||
<div className="admin-panel-advanced-settings__api-keys__container-info">
|
||||
<div className="admin-panel-advanced-settings__api-keys-subtitle">{i18n('NAME_OF_KEY')}</div>
|
||||
<div className="admin-panel-advanced-settings__api-keys-data">{currentAPIKey.name}</div>
|
||||
<div className="admin-panel-advanced-settings__api-keys-data">{name}</div>
|
||||
<div className="admin-panel-advanced-settings__api-keys-subtitle">{i18n('KEY')}</div>
|
||||
<div className="admin-panel-advanced-settings__api-keys-data">{currentAPIKey.token}</div>
|
||||
<div className="admin-panel-advanced-settings__api-keys-data">{token}</div>
|
||||
<div className="admin-panel-advanced-settings__api-keys-subtitle">{i18n('PERMISSIONS')}</div>
|
||||
<div className="admin-panel-advanced-settings__api-keys__permissions">
|
||||
<FormField className="admin-panel-advanced-settings__api-keys__permissions__item" value={canCreateTickets*1} label={i18n('TICKET_CREATION_PERMISSION')} field='checkbox' />
|
||||
<FormField value={shouldReturnTicketNumber*1} label={i18n('TICKET_NUMBER_RETURN_PERMISSION')} field='checkbox' />
|
||||
</div>
|
||||
<div className="admin-panel-advanced-settings__api-keys__permissions">
|
||||
<FormField className="admin-panel-advanced-settings__api-keys__permissions__item" value={canCheckTickets*1} label={i18n('TICKET_CHECK_PERMISSION')} field='checkbox' />
|
||||
<FormField value={canCreateUser*1} label={i18n('USER_CREATION_PERMISSION')} field='checkbox' />
|
||||
</div>
|
||||
<Button className="admin-panel-advanced-settings__api-keys-button" size="medium" onClick={this.onDeleteKeyClick.bind(this)}>
|
||||
{i18n('DELETE')}
|
||||
</Button>
|
||||
@ -134,7 +163,7 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
};
|
||||
}),
|
||||
selectedIndex: this.state.selectedAPIKey,
|
||||
onChange: index => this.setState({selectedAPIKey: index}),
|
||||
onChange: index => this.setState({selectedAPIKey: index, error:''}),
|
||||
onAddClick: this.openAPIKeyModal.bind(this)
|
||||
};
|
||||
}
|
||||
@ -142,18 +171,51 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
openAPIKeyModal() {
|
||||
ModalContainer.openModal(
|
||||
<Form className="admin-panel-advanced-settings__api-keys-modal" onSubmit={this.addAPIKey.bind(this)}>
|
||||
<Header title={i18n('ADD_API_KEY')} description={i18n('ADD_API_KEY_DESCRIPTION')}/>
|
||||
<FormField name="name" label={i18n('NAME_OF_KEY')} validation="DEFAULT" required fieldProps={{size: 'large'}}/>
|
||||
<SubmitButton type="secondary">{i18n('SUBMIT')}</SubmitButton>
|
||||
</Form>
|
||||
<Header title={i18n('ADD_API_KEY')} description={i18n('ADD_API_KEY_DESCRIPTION')} />
|
||||
<FormField name="name" label={i18n('NAME_OF_KEY')} validation="DEFAULT" required fieldProps={{size: 'large'}} />
|
||||
<div className="admin-panel-advanced-settings__api-keys__permissions">
|
||||
<FormField className = "admin-panel-advanced-settings__api-keys__permissions__item" name="createTicketPermission" label={i18n('TICKET_CREATION_PERMISSION')} field='checkbox' />
|
||||
<FormField name="ticketNumberPermission" label={i18n('TICKET_NUMBER_RETURN_PERMISSION')} field='checkbox' />
|
||||
</div>
|
||||
<div className="admin-panel-advanced-settings__api-keys__permissions" >
|
||||
<FormField className = "admin-panel-advanced-settings__api-keys__permissions__item" name="checkTicketPermission" label={i18n('TICKET_CHECK_PERMISSION')} field='checkbox' />
|
||||
<FormField name="userPermission" label={i18n('USER_CREATION_PERMISSION')} field='checkbox' />
|
||||
</div>
|
||||
<div className="admin-panel-advanced-settings__api-keys__buttons-container">
|
||||
<Button
|
||||
className="admin-panel-advanced-settings__api-keys__cancel-button"
|
||||
onClick={(e) => {e.preventDefault(); ModalContainer.closeModal();}}
|
||||
type='link'
|
||||
size="medium">
|
||||
{i18n('CANCEL')}
|
||||
</Button>
|
||||
<SubmitButton className="admin-panel-advanced-settings__api-keys-modal__submit-button" type="secondary">{i18n('SUBMIT')}</SubmitButton>
|
||||
</div>
|
||||
</Form>,
|
||||
{
|
||||
closeButton: {
|
||||
showCloseButton: true
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
addAPIKey({name}) {
|
||||
addAPIKey({name,userPermission,createTicketPermission,checkTicketPermission,ticketNumberPermission}) {
|
||||
ModalContainer.closeModal();
|
||||
|
||||
this.setState({
|
||||
error: ''
|
||||
});
|
||||
|
||||
API.call({
|
||||
path: '/system/add-api-key',
|
||||
data: {name}
|
||||
data: {
|
||||
name,
|
||||
canCreateUsers: userPermission*1,
|
||||
canCreateTickets: createTicketPermission*1,
|
||||
canCheckTickets: checkTicketPermission*1,
|
||||
shouldReturnTicketNumber: ticketNumberPermission*1
|
||||
}
|
||||
}).then(this.getAllKeys.bind(this));
|
||||
}
|
||||
|
||||
@ -161,15 +223,20 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
API.call({
|
||||
path: '/system/get-api-keys',
|
||||
data: {}
|
||||
}).then(this.onRetrieveSuccess.bind(this));
|
||||
}).then(this.onRetrieveSuccess.bind(this))
|
||||
}
|
||||
|
||||
onDeleteKeyClick() {
|
||||
const {
|
||||
APIKeys,
|
||||
selectedAPIKey
|
||||
} = this.state;
|
||||
|
||||
AreYouSure.openModal(null, () => {
|
||||
API.call({
|
||||
return API.call({
|
||||
path: '/system/delete-api-key',
|
||||
data: {
|
||||
name: this.state.APIKeys[this.state.selectedAPIKey].name
|
||||
name: APIKeys[selectedAPIKey].name
|
||||
}
|
||||
}).then(this.getAllKeys.bind(this));
|
||||
});
|
||||
@ -178,21 +245,24 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
onRetrieveSuccess(result) {
|
||||
this.setState({
|
||||
APIKeys: result.data,
|
||||
selectedAPIKey: -1
|
||||
selectedAPIKey: -1,
|
||||
error: null
|
||||
});
|
||||
}
|
||||
|
||||
onToggleButtonUserSystemChange() {
|
||||
AreYouSure.openModal(null, this.onAreYouSureUserSystemOk.bind(this), 'secure');
|
||||
onCheckboxMandatoryLoginChange() {
|
||||
AreYouSure.openModal(null, this.onAreYouSureMandatoryLoginOk.bind(this), 'secure');
|
||||
}
|
||||
|
||||
onToggleButtonRegistrationChange() {
|
||||
onCheckboxRegistrationChange() {
|
||||
AreYouSure.openModal(null, this.onAreYouSureRegistrationOk.bind(this), 'secure');
|
||||
}
|
||||
|
||||
onAreYouSureUserSystemOk(password) {
|
||||
API.call({
|
||||
path: this.props.config['user-system-enabled'] ? '/system/disable-user-system' : '/system/enable-user-system',
|
||||
onAreYouSureMandatoryLoginOk(password) {
|
||||
const { config, dispatch } = this.props;
|
||||
|
||||
return API.call({
|
||||
path: config['mandatory-login'] ? '/system/disable-mandatory-login' : '/system/enable-mandatory-login',
|
||||
data: {
|
||||
password: password
|
||||
}
|
||||
@ -200,15 +270,17 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
this.setState({
|
||||
messageType: 'success',
|
||||
messageTitle: null,
|
||||
messageContent: this.props.config['user-system-enabled'] ? i18n('USER_SYSTEM_DISABLED') : i18n('USER_SYSTEM_ENABLED')
|
||||
messageContent: config['mandatory-login'] ? i18n('MANDATORY_LOGIN_DISABLED') : i18n('MANDATORY_LOGIN_ENABLED')
|
||||
});
|
||||
this.props.dispatch(ConfigActions.updateData());
|
||||
dispatch(ConfigActions.updateData());
|
||||
}).catch(() => this.setState({messageType: 'error', messageTitle: null, messageContent: i18n('ERROR_UPDATING_SETTINGS')}));
|
||||
}
|
||||
|
||||
onAreYouSureRegistrationOk(password) {
|
||||
API.call({
|
||||
path: this.props.config['registration'] ? '/system/disable-registration' : '/system/enable-registration',
|
||||
const { config, dispatch } = this.props;
|
||||
|
||||
return API.call({
|
||||
path: config['registration'] ? '/system/disable-registration' : '/system/enable-registration',
|
||||
data: {
|
||||
password: password
|
||||
}
|
||||
@ -216,9 +288,9 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
this.setState({
|
||||
messageType: 'success',
|
||||
messageTitle: null,
|
||||
messageContent: this.props.config['registration'] ? i18n('REGISTRATION_DISABLED') : i18n('REGISTRATION_ENABLED')
|
||||
messageContent: config['registration'] ? i18n('REGISTRATION_DISABLED') : i18n('REGISTRATION_ENABLED')
|
||||
});
|
||||
this.props.dispatch(ConfigActions.updateData());
|
||||
dispatch(ConfigActions.updateData());
|
||||
}).catch(() => this.setState({messageType: 'error', messageTitle: null, messageContent: i18n('ERROR_UPDATING_SETTINGS')}));
|
||||
}
|
||||
|
||||
@ -227,7 +299,7 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
}
|
||||
|
||||
onAreYouSureCSVOk(file, password) {
|
||||
API.call({
|
||||
return API.call({
|
||||
path: '/system/csv-import',
|
||||
dataAsForm: true,
|
||||
data: {
|
||||
@ -270,7 +342,7 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
}
|
||||
|
||||
onAreYouSureDeleteAllUsersOk(password) {
|
||||
API.call({
|
||||
return API.call({
|
||||
path: '/system/delete-all-users',
|
||||
data: {
|
||||
password: password
|
||||
|
@ -1,7 +1,7 @@
|
||||
@import "../../../../scss/vars";
|
||||
|
||||
.admin-panel-advanced-settings {
|
||||
&__user-system-enabled {
|
||||
&__mandatory-login {
|
||||
|
||||
}
|
||||
|
||||
@ -9,13 +9,6 @@
|
||||
|
||||
}
|
||||
|
||||
&__toggle-button {
|
||||
display: inline-block;
|
||||
margin-left: 20px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
&__text {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 20px;
|
||||
@ -28,13 +21,20 @@
|
||||
|
||||
&__api-keys {
|
||||
|
||||
&__buttons-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: $font-size--bg;
|
||||
margin-bottom: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&-info {
|
||||
&__container-info {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@ -52,8 +52,21 @@
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
&__permissions {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 20px;
|
||||
&__item {
|
||||
margin-right: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
&-modal {
|
||||
min-width: 500px;
|
||||
min-width: 400px;
|
||||
|
||||
&__submit-button {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&-none {
|
||||
@ -61,4 +74,36 @@
|
||||
font-size: $font-size--md;
|
||||
}
|
||||
}
|
||||
|
||||
&__message {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 415px) {
|
||||
.admin-panel-advanced-settings {
|
||||
&__api-keys {
|
||||
|
||||
&-button {
|
||||
margin-bottom: 30px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
&__container {
|
||||
padding: 30px 0;
|
||||
|
||||
&-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&-subtitle {
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,156 @@
|
||||
import React from 'react';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import API from 'lib-app/api-call';
|
||||
|
||||
import Button from 'core-components/button';
|
||||
import Header from 'core-components/header';
|
||||
import Form from 'core-components/form';
|
||||
import FormField from 'core-components/form-field';
|
||||
import SubmitButton from 'core-components/submit-button';
|
||||
import ColorSelector from 'core-components/color-selector';
|
||||
|
||||
class AdminPanelCustomTagsModal extends React.Component {
|
||||
static contextTypes = {
|
||||
closeModal: React.PropTypes.func,
|
||||
createTag: React.PropTypes.bool
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
defaultValues: React.PropTypes.object,
|
||||
onTagCreated: React.PropTypes.func
|
||||
};
|
||||
|
||||
state = {
|
||||
form: this.props.defaultValues || {name: '', color: '#ff6900'},
|
||||
loading: false
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
this.renderTagContentPopUp(this.props.createTag)
|
||||
);
|
||||
}
|
||||
|
||||
renderTagContentPopUp(create) {
|
||||
const {
|
||||
form,
|
||||
errors,
|
||||
loading,
|
||||
} = this.state;
|
||||
let title, description, nameRequired, submitFunction;
|
||||
|
||||
if(create) {
|
||||
title = i18n('ADD_CUSTOM_TAG');
|
||||
description = i18n('DESCRIPTION_ADD_CUSTOM_TAG');
|
||||
submitFunction = this.onSubmitNewTag.bind(this);
|
||||
nameRequired = true;
|
||||
} else {
|
||||
title = i18n('EDIT_CUSTOM_TAG');
|
||||
description = i18n('DESCRIPTION_EDIT_CUSTOM_TAG');
|
||||
nameRequired = false;
|
||||
submitFunction = this.onSubmitEditTag.bind(this);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='admin-panel-custom-tags-modal'>
|
||||
<Header title={title} description={description} />
|
||||
<Form
|
||||
values={form}
|
||||
onChange={this.onFormChange.bind(this)}
|
||||
onSubmit={submitFunction}
|
||||
errors={errors}
|
||||
onValidateErrors={errors => this.setState({errors})}
|
||||
loading={loading}>
|
||||
<FormField name="name" label={i18n('NAME')} fieldProps={{size: 'large'}} required={nameRequired} />
|
||||
<FormField name="color" label={i18n('COLOR')} decorator={ColorSelector} />
|
||||
<div className='admin-panel-custom-tags-modal__actions'>
|
||||
<Button onClick={this.onDiscardClick.bind(this)} size="small">
|
||||
{i18n('CANCEL')}
|
||||
</Button>
|
||||
<SubmitButton type="secondary" size="small">
|
||||
{i18n('SAVE')}
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onFormChange(form) {
|
||||
this.setState({
|
||||
form
|
||||
});
|
||||
}
|
||||
|
||||
onSubmitEditTag(form) {
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
|
||||
API.call({
|
||||
path: '/ticket/edit-tag',
|
||||
data: {
|
||||
tagId: this.props.id,
|
||||
name: form.name,
|
||||
color: form.color,
|
||||
}
|
||||
}).then(() => {
|
||||
this.context.closeModal();
|
||||
|
||||
if(this.props.onTagChange) {
|
||||
this.props.onTagChange();
|
||||
}
|
||||
}).catch((result) => {
|
||||
|
||||
this.setState({
|
||||
loading: false,
|
||||
errors: {
|
||||
'name': result.message
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
onSubmitNewTag(form) {
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
|
||||
API.call({
|
||||
path: '/ticket/create-tag',
|
||||
data: {
|
||||
name: form.name,
|
||||
color: form.color,
|
||||
}
|
||||
}).then(() => {
|
||||
this.context.closeModal();
|
||||
|
||||
if(this.props.onTagCreated) {
|
||||
this.props.onTagCreated();
|
||||
}
|
||||
|
||||
}).catch((result) => {
|
||||
|
||||
this.setState({
|
||||
loading: false,
|
||||
errors: {
|
||||
'name': result.message
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
onDiscardClick(event) {
|
||||
event.preventDefault();
|
||||
this.context.closeModal();
|
||||
this.setState({
|
||||
loading: false,
|
||||
errors: {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminPanelCustomTagsModal;
|
@ -0,0 +1,7 @@
|
||||
.admin-panel-custom-tags-modal {
|
||||
|
||||
&__actions{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
109
client/src/app/admin/panel/settings/admin-panel-custom-tags.js
Normal file
109
client/src/app/admin/panel/settings/admin-panel-custom-tags.js
Normal file
@ -0,0 +1,109 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import AdminPanelCustomTagsModal from 'app/admin/panel/settings/admin-panel-custom-tags-modal';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import API from 'lib-app/api-call';
|
||||
import ConfigActions from 'actions/config-actions';
|
||||
|
||||
import AreYouSure from 'app-components/are-you-sure';
|
||||
import ModalContainer from 'app-components/modal-container';
|
||||
|
||||
import Icon from 'core-components/icon';
|
||||
import Button from 'core-components/button';
|
||||
import Header from 'core-components/header';
|
||||
import Tag from 'core-components/tag';
|
||||
|
||||
class AdminPanelCustomTags extends React.Component {
|
||||
static propTypes = {
|
||||
tags: React.PropTypes.arrayOf(
|
||||
React.PropTypes.shape({
|
||||
name: React.PropTypes.string,
|
||||
color: React.PropTypes.string,
|
||||
id: React.PropTypes.number
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.retrieveCustomTags();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="admin-panel-custom-tags">
|
||||
<Header title={i18n('CUSTOM_TAGS')} description={i18n('CUSTOM_TAGS_DESCRIPTION')} />
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
return (
|
||||
<div className="admin-panel-custom-tags__content">
|
||||
<div>
|
||||
<Button onClick={this.openTagModal.bind(this)} type="secondary">
|
||||
<Icon className="admin-panel-custom-tags__add-button-icon" name="plus" /> {i18n('ADD_CUSTOM_TAG')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="admin-panel-custom-tags__tag-list">
|
||||
{this.props.tags.map(this.renderTag.bind(this))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderTag(tag, index) {
|
||||
return (
|
||||
<div key={index} className="admin-panel-custom-tags__tag-container" >
|
||||
<Tag
|
||||
color={tag.color}
|
||||
name={tag.name}
|
||||
onEditClick={this.openEditTagModal.bind(this, tag.id, tag.name, tag.color)}
|
||||
onRemoveClick={this.onDeleteClick.bind(this, tag.id)}
|
||||
size='large'
|
||||
showEditButton
|
||||
showDeleteButton />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
openTagModal() {
|
||||
ModalContainer.openModal(
|
||||
<AdminPanelCustomTagsModal onTagCreated={this.retrieveCustomTags.bind(this)} createTag />
|
||||
);
|
||||
}
|
||||
|
||||
openEditTagModal(tagId,tagName,tagColor, event) {
|
||||
ModalContainer.openModal(
|
||||
<AdminPanelCustomTagsModal defaultValues={{name: tagName , color: tagColor}} id={tagId} onTagChange={this.retrieveCustomTags.bind(this)} />
|
||||
);
|
||||
}
|
||||
|
||||
onDeleteClick(tagId, event) {
|
||||
event.preventDefault();
|
||||
AreYouSure.openModal(i18n('WILL_DELETE_CUSTOM_TAG'), this.deleteCustomTag.bind(this, tagId));
|
||||
}
|
||||
|
||||
deleteCustomTag(tagId) {
|
||||
return API.call({
|
||||
path: '/ticket/delete-tag',
|
||||
data: {
|
||||
tagId,
|
||||
}
|
||||
}).then(() => {
|
||||
this.retrieveCustomTags()
|
||||
});
|
||||
}
|
||||
|
||||
retrieveCustomTags() {
|
||||
this.props.dispatch(ConfigActions.updateData());
|
||||
}
|
||||
}
|
||||
|
||||
export default connect((store) => {
|
||||
return {
|
||||
tags: store.config['tags'].map((tag) => {return {...tag, id: tag.id*1}})
|
||||
};
|
||||
})(AdminPanelCustomTags);
|
@ -0,0 +1,18 @@
|
||||
.admin-panel-custom-tags {
|
||||
|
||||
&__content {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&__add-button-icon{
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
&__tag-list{
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
&__tag-container{
|
||||
margin-top:5px ;
|
||||
}
|
||||
}
|
@ -0,0 +1,539 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import {connect} from 'react-redux';
|
||||
import randomString from 'random-string';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import API from 'lib-app/api-call';
|
||||
|
||||
import AreYouSure from 'app-components/are-you-sure';
|
||||
import LanguageSelector from 'app-components/language-selector';
|
||||
import PopupMessage from 'app-components/popup-message';
|
||||
|
||||
import Button from 'core-components/button';
|
||||
import Header from 'core-components/header';
|
||||
import Listing from 'core-components/listing';
|
||||
import Loading from 'core-components/loading';
|
||||
import Form from 'core-components/form';
|
||||
import FormField from 'core-components/form-field';
|
||||
import SubmitButton from 'core-components/submit-button';
|
||||
import Message from 'core-components/message';
|
||||
|
||||
class AdminPanelEmailSettings extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
language: React.PropTypes.string,
|
||||
};
|
||||
|
||||
state = {
|
||||
headerImage: '',
|
||||
loadingHeaderImage: false,
|
||||
loadingList: true,
|
||||
loadingTemplate: false,
|
||||
templates: [],
|
||||
loadingForm: false,
|
||||
selectedIndex: -1,
|
||||
edited: false,
|
||||
errors: {},
|
||||
language: this.props.language,
|
||||
imapLoading: false,
|
||||
smtpLoading: false,
|
||||
form: {
|
||||
subject: '',
|
||||
text1: '',
|
||||
text2: '',
|
||||
text3: '',
|
||||
},
|
||||
emailForm: {
|
||||
['server-email']: '',
|
||||
},
|
||||
smtpForm: {
|
||||
['smtp-host']: '',
|
||||
['smtp-user']: '',
|
||||
['smtp-pass']: 'HIDDEN',
|
||||
},
|
||||
imapForm: {
|
||||
['imap-host']: '',
|
||||
['imap-user']: '',
|
||||
['imap-pass']: 'HIDDEN',
|
||||
['imap-token']: '',
|
||||
},
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.retrieveMailTemplateList();
|
||||
this.retrieveHeaderImage();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="admin-panel-email-settings">
|
||||
{(!this.state.loadingList) ? this.renderContent() : this.renderLoading()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
return (
|
||||
<div>
|
||||
{this.renderEmailSettings()}
|
||||
<Header title={i18n('EMAIL_TEMPLATES')} description={i18n('EMAIL_TEMPLATES_DESCRIPTION')} />
|
||||
<div className="row">
|
||||
<div className="col-md-3">
|
||||
<Listing {...this.getListingProps()} />
|
||||
</div>
|
||||
{(this.state.selectedIndex !== -1) ? this.renderForm() : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderLoading() {
|
||||
return (
|
||||
<div className="admin-panel-email-settings__loading">
|
||||
<Loading backgrounded size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderEmailSettings() {
|
||||
return (
|
||||
<div>
|
||||
<Header title={i18n('EMAIL_SETTINGS')} description={i18n('EMAIL_SETTINGS_DESCRIPTION')} />
|
||||
<Form className="admin-panel-email-settings__email-form"
|
||||
onSubmit={this.submitEmailAddress.bind(this)}
|
||||
onChange={emailForm => this.setState({emailForm})}
|
||||
values={this.state.emailForm}>
|
||||
<div className="admin-panel-email-settings__email-container">
|
||||
<FormField className="admin-panel-email-settings__email-server-address"
|
||||
name="server-email"
|
||||
label={i18n('EMAIL_SERVER_ADDRESS')}
|
||||
fieldProps={{size: 'large'}}
|
||||
infoMessage={i18n('EMAIL_SERVER_ADDRESS_DESCRIPTION')} />
|
||||
<SubmitButton className="admin-panel-email-settings__submit" type="secondary"
|
||||
size="small">{i18n('SAVE')}</SubmitButton>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<Form className="admin-panel-email-settings__image-form"
|
||||
values={{headerImage: this.state.headerImage}}
|
||||
onChange={form => this.setState({headerImage: form.headerImage})}
|
||||
onSubmit={this.onHeaderImageSubmit.bind(this)}>
|
||||
<div className="admin-panel-email-settings__image-container">
|
||||
<FormField className="admin-panel-email-settings__image-header-url"
|
||||
label={i18n('IMAGE_HEADER_URL')} name="headerImage" required
|
||||
infoMessage={i18n('IMAGE_HEADER_URL_DESCRIPTION')}
|
||||
fieldProps={{size: 'large'}} />
|
||||
<SubmitButton className="admin-panel-email-settings__image-header-submit" type="secondary"
|
||||
size="small">{i18n('SAVE')}</SubmitButton>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<div className="admin-panel-email-settings__servers">
|
||||
<div className="admin-panel-email-settings__box">
|
||||
<Header title={i18n('SMTP_SERVER')} description={i18n('SMTP_SERVER_DESCRIPTION')} />
|
||||
<Form onSubmit={this.submitSMTP.bind(this)} onChange={smtpForm => this.setState({smtpForm})}
|
||||
values={this.state.smtpForm} loading={this.state.smtpLoading}>
|
||||
<FormField name="smtp-host" label={i18n('SMTP_SERVER')} fieldProps={{size: 'large'}} />
|
||||
<FormField name="smtp-user" label={i18n('SMTP_USER')} fieldProps={{size: 'large'}} />
|
||||
<FormField name="smtp-pass" label={i18n('SMTP_PASSWORD')} fieldProps={{size: 'large', autoComplete: 'off'}} />
|
||||
<div className="admin-panel-email-settings__server-form-buttons">
|
||||
<SubmitButton type="tertiary" size="small" onClick={this.testSMTP.bind(this)}>
|
||||
{i18n('TEST')}
|
||||
</SubmitButton>
|
||||
<SubmitButton className="admin-panel-email-settings__submit" type="secondary" size="small">
|
||||
{i18n('SAVE')}
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<div className="admin-panel-email-settings__box">
|
||||
<Header title={i18n('IMAP_SERVER')} description={i18n('IMAP_SERVER_DESCRIPTION')} />
|
||||
<Form onSubmit={this.submitIMAP.bind(this)} onChange={imapForm => this.setState({imapForm})}
|
||||
values={this.state.imapForm} loading={this.state.imapLoading}>
|
||||
<FormField name="imap-host" label={i18n('IMAP_SERVER')} fieldProps={{size: 'large'}} />
|
||||
<FormField name="imap-user" label={i18n('IMAP_USER')} fieldProps={{size: 'large'}} />
|
||||
<FormField name="imap-pass" label={i18n('IMAP_PASSWORD')} fieldProps={{size: 'large', autoComplete: 'off'}} />
|
||||
<FormField
|
||||
name="imap-token"
|
||||
label={i18n('IMAP_TOKEN')}
|
||||
infoMessage={i18n('IMAP_TOKEN_DESCRIPTION')}
|
||||
fieldProps={{size: 'large', icon: 'refresh', onIconClick: this.generateImapToken.bind(this)}} />
|
||||
<div className="admin-panel-email-settings__server-form-buttons">
|
||||
<SubmitButton type="tertiary" size="small" onClick={this.testIMAP.bind(this)}>
|
||||
{i18n('TEST')}
|
||||
</SubmitButton>
|
||||
<SubmitButton className="admin-panel-email-settings__submit" type="secondary" size="small">
|
||||
{i18n('SAVE')}
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</Form>
|
||||
<Message className="admin-panel-email-settings__imap-message" type="info">
|
||||
{i18n('IMAP_POLLING_DESCRIPTION', {url: `${apiRoot}/system/email-polling`})}
|
||||
</Message>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderForm() {
|
||||
const {
|
||||
form,
|
||||
language,
|
||||
selectedIndex,
|
||||
edited
|
||||
} = this.state;
|
||||
return (
|
||||
<div className="col-md-9">
|
||||
<FormField label={i18n('LANGUAGE')} decorator={LanguageSelector} value={language}
|
||||
onChange={event => this.onItemChange(selectedIndex, event.target.value)}
|
||||
fieldProps={{
|
||||
type: 'allowed',
|
||||
size: 'medium'
|
||||
}} />
|
||||
<Form {...this.getFormProps()}>
|
||||
<div className="row">
|
||||
<div className="col-md-7">
|
||||
<FormField label={i18n('SUBJECT')} name="subject" validation="TITLE" required
|
||||
fieldProps={{size: 'large'}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormField key="text1" label={i18n('TEXT') + '1'} name="text1" validation="TEXT_AREA" required
|
||||
decorator={'textarea'}
|
||||
fieldProps={{className: 'admin-panel-email-settings__text-area'}} />
|
||||
{(form.text2) ?
|
||||
<FormField key="text2" label={i18n('TEXT') + '2'} name="text2" validation="TEXT_AREA" required
|
||||
decorator={'textarea'}
|
||||
fieldProps={{className: 'admin-panel-email-settings__text-area'}} /> : null}
|
||||
{(form.text3) ?
|
||||
<FormField key="text3" label={i18n('TEXT') + '3'} name="text3" validation="TEXT_AREA" required
|
||||
decorator={'textarea'}
|
||||
fieldProps={{className: 'admin-panel-email-settings__text-area'}} /> : null}
|
||||
|
||||
<div className="admin-panel-email-settings__actions">
|
||||
<div className="admin-panel-email-settings__optional-buttons">
|
||||
<div className="admin-panel-email-settings__recover-button">
|
||||
<Button onClick={this.onRecoverClick.bind(this)} size="medium">
|
||||
{i18n('RECOVER_DEFAULT')}
|
||||
</Button>
|
||||
</div>
|
||||
{edited ? this.renderDiscardButton() : null}
|
||||
</div>
|
||||
<div className="admin-panel-email-settings__save-button">
|
||||
<SubmitButton
|
||||
key="submit-email-template"
|
||||
type="secondary"
|
||||
size="small"
|
||||
onClick={(e) => {e.preventDefault(); this.onFormSubmit(form);}}>
|
||||
{i18n('SAVE')}
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderDiscardButton() {
|
||||
return (
|
||||
<div className="admin-panel-email-settings__discard-button">
|
||||
<Button onClick={this.onDiscardChangesClick.bind(this)} size="medium">
|
||||
{i18n('DISCARD_CHANGES')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getListingProps() {
|
||||
return {
|
||||
title: i18n('EMAIL_TEMPLATES'),
|
||||
items: this.getTemplateItems(),
|
||||
selectedIndex: this.state.selectedIndex,
|
||||
onChange: this.onItemChange.bind(this)
|
||||
};
|
||||
}
|
||||
|
||||
getFormProps() {
|
||||
return {
|
||||
values: this.state.form,
|
||||
errors: this.state.errors,
|
||||
loading: this.state.loadingForm,
|
||||
onChange: (form) => {
|
||||
this.setState({form, edited: true})
|
||||
},
|
||||
onValidateErrors: (errors) => {
|
||||
this.setState({errors})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
getTemplateItems() {
|
||||
return this.state.templates.map((template) => {
|
||||
return {
|
||||
content: template
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
onItemChange(index, language) {
|
||||
if (this.state.edited) {
|
||||
AreYouSure.openModal(i18n('WILL_LOSE_CHANGES'), this.retrieveEmailTemplate.bind(this, index, language || this.state.language));
|
||||
} else {
|
||||
this.retrieveEmailTemplate(index, language || this.state.language);
|
||||
}
|
||||
}
|
||||
|
||||
onHeaderImageSubmit(form) {
|
||||
this.setState({
|
||||
loadingHeaderImage: true,
|
||||
});
|
||||
|
||||
API.call({
|
||||
path: '/system/edit-settings',
|
||||
data: {
|
||||
'mail-template-header-image': form['headerImage']
|
||||
}
|
||||
}).then(() => this.setState({
|
||||
loadingHeaderImage: false,
|
||||
}))
|
||||
}
|
||||
|
||||
onFormSubmit(form) {
|
||||
const {selectedIndex, language, templates} = this.state;
|
||||
|
||||
this.setState({loadingForm: true});
|
||||
|
||||
API.call({
|
||||
path: '/system/edit-mail-template',
|
||||
data: {
|
||||
template: templates[selectedIndex],
|
||||
language,
|
||||
subject: form.subject,
|
||||
text1: form.text1,
|
||||
text2: form.text2,
|
||||
text3: form.text3,
|
||||
}
|
||||
}).then(() => {
|
||||
this.setState({loadingForm: false, edited: false});
|
||||
}).catch(response => {
|
||||
this.setState({
|
||||
loadingForm: false,
|
||||
});
|
||||
|
||||
switch (response.message) {
|
||||
case 'INVALID_SUBJECT':
|
||||
this.setState({
|
||||
errors: {subject: i18n('INVALID_SYNTAX')}
|
||||
});
|
||||
break;
|
||||
case 'INVALID_TEXT_1':
|
||||
this.setState({
|
||||
errors: {text1: i18n('INVALID_SYNTAX')}
|
||||
});
|
||||
break;
|
||||
case 'INVALID_TEXT_2':
|
||||
this.setState({
|
||||
errors: {text2: i18n('INVALID_SYNTAX')}
|
||||
});
|
||||
break;
|
||||
case 'INVALID_TEXT_3':
|
||||
this.setState({
|
||||
errors: {text3: i18n('INVALID_SYNTAX')}
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onDiscardChangesClick(event) {
|
||||
event.preventDefault();
|
||||
this.onItemChange(this.state.selectedIndex, this.state.language);
|
||||
}
|
||||
|
||||
onRecoverClick(event) {
|
||||
event.preventDefault();
|
||||
AreYouSure.openModal(i18n('WILL_RECOVER_EMAIL_TEMPLATE'), this.recoverEmailTemplate.bind(this));
|
||||
}
|
||||
|
||||
generateImapToken() {
|
||||
this.setState({
|
||||
imapForm: {
|
||||
...this.state.imapForm,
|
||||
['imap-token']: randomString({length: 20}),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
submitEmailAddress(form) {
|
||||
this.editSettings(form, 'EMAIL_SUCCESS');
|
||||
}
|
||||
|
||||
submitSMTP(form) {
|
||||
this.setState({
|
||||
smtpLoading: true
|
||||
});
|
||||
|
||||
this.editSettings(form, 'SMTP_SUCCESS')
|
||||
.then(() => this.setState({
|
||||
smtpLoading: false
|
||||
}));
|
||||
}
|
||||
|
||||
submitIMAP(form) {
|
||||
this.setState({
|
||||
imapLoading: true
|
||||
});
|
||||
|
||||
this.editSettings(form, 'IMAP_SUCCESS')
|
||||
.then(() => this.setState({
|
||||
imapLoading: false
|
||||
}));
|
||||
}
|
||||
|
||||
editSettings(form, successMessage) {
|
||||
return API.call({
|
||||
path: '/system/edit-settings',
|
||||
data: this.parsePasswordField(form)
|
||||
}).then(() => PopupMessage.open({
|
||||
title: i18n('SETTINGS_UPDATED'),
|
||||
children: i18n(successMessage),
|
||||
type: 'success'
|
||||
})).catch(response => PopupMessage.open({
|
||||
title: i18n('ERROR_UPDATING_SETTINGS'),
|
||||
children: response.message,
|
||||
type: 'error'
|
||||
}));
|
||||
}
|
||||
|
||||
testSMTP(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.setState({
|
||||
smtpLoading: true
|
||||
});
|
||||
|
||||
API.call({
|
||||
path: '/system/test-smtp',
|
||||
data: this.parsePasswordField(this.state.smtpForm)
|
||||
}).then(() => PopupMessage.open({
|
||||
title: `${i18n('SUCCESSFUL_CONNECTION')}: SMTP`,
|
||||
children: i18n('SERVER_CREDENTIALS_WORKING'),
|
||||
type: 'success',
|
||||
})).catch(response => PopupMessage.open({
|
||||
title: `${i18n('UNSUCCESSFUL_CONNECTION')}: SMTP`,
|
||||
children: `${i18n('SERVER_ERROR')}: ${response.message}`,
|
||||
type: 'error',
|
||||
})).then(() => this.setState({
|
||||
smtpLoading: false
|
||||
}));
|
||||
}
|
||||
|
||||
testIMAP(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.setState({
|
||||
imapLoading: true
|
||||
});
|
||||
|
||||
API.call({
|
||||
path: '/system/test-imap',
|
||||
data: this.parsePasswordField(this.state.imapForm)
|
||||
}).then(() => PopupMessage.open({
|
||||
title: `${i18n('SUCCESSFUL_CONNECTION')}: IMAP`,
|
||||
children: i18n('SERVER_CREDENTIALS_WORKING'),
|
||||
type: 'success',
|
||||
})).catch(response => PopupMessage.open({
|
||||
title: `${i18n('UNSUCCESSFUL_CONNECTION')}: IMAP`,
|
||||
children: `${i18n('SERVER_ERROR')}: ${response.message}`,
|
||||
type: 'error',
|
||||
})).then(() => this.setState({
|
||||
imapLoading: false
|
||||
}));
|
||||
}
|
||||
|
||||
recoverEmailTemplate() {
|
||||
const {selectedIndex, language, templates} = this.state;
|
||||
|
||||
return API.call({
|
||||
path: '/system/recover-mail-template',
|
||||
data: {
|
||||
template: templates[selectedIndex],
|
||||
language
|
||||
}
|
||||
}).then(() => {
|
||||
this.retrieveEmailTemplate(this.state.selectedIndex, language);
|
||||
});
|
||||
}
|
||||
|
||||
retrieveEmailTemplate(index, language) {
|
||||
this.setState({
|
||||
loadingForm: true,
|
||||
});
|
||||
|
||||
return API.call({
|
||||
path: '/system/get-mail-template',
|
||||
data: {template: this.state.templates[index], language}
|
||||
}).then((result) => this.setState({
|
||||
language,
|
||||
selectedIndex: index,
|
||||
edited: false,
|
||||
loadingForm: false,
|
||||
form: result.data,
|
||||
errors: {},
|
||||
}));
|
||||
}
|
||||
|
||||
retrieveMailTemplateList() {
|
||||
API.call({
|
||||
path: '/system/get-mail-template-list',
|
||||
data: {}
|
||||
}).then((result) => this.setState({
|
||||
loadingList: false,
|
||||
templates: result.data
|
||||
}));
|
||||
}
|
||||
|
||||
retrieveHeaderImage() {
|
||||
API.call({
|
||||
path: '/system/get-settings',
|
||||
data: {allSettings: 1}
|
||||
}).then(result => this.setState({
|
||||
headerImage: result.data['mail-template-header-image'] || '',
|
||||
emailForm: {
|
||||
['server-email']: result.data['server-email'] || '',
|
||||
},
|
||||
smtpForm: {
|
||||
['smtp-host']: result.data['smtp-host'] || '',
|
||||
['smtp-user']: result.data['smtp-user'] || '',
|
||||
['smtp-pass']: 'HIDDEN',
|
||||
},
|
||||
imapForm: {
|
||||
['imap-host']: result.data['imap-host'] || '',
|
||||
['imap-user']: result.data['imap-user'] || '',
|
||||
['imap-pass']: 'HIDDEN',
|
||||
['imap-token']: result.data['imap-token'] || '',
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
parsePasswordField(form) {
|
||||
let parsedForm = _.extend({}, form);
|
||||
|
||||
delete parsedForm['smtp-pass'];
|
||||
delete parsedForm['imap-pass'];
|
||||
|
||||
return _.extend(parsedForm, {
|
||||
[ form['smtp-pass'] && form['smtp-pass'] !== 'HIDDEN' ? 'smtp-pass' : null]: form['smtp-pass'],
|
||||
[ form['imap-pass'] && form['imap-pass'] !== 'HIDDEN' ? 'imap-pass' : null]: form['imap-pass'],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default connect((store) => {
|
||||
return {
|
||||
language: store.config.language,
|
||||
};
|
||||
})(AdminPanelEmailSettings);
|
@ -0,0 +1,93 @@
|
||||
@import "../../../../scss/vars";
|
||||
|
||||
.admin-panel-email-settings {
|
||||
&__loading {
|
||||
min-height: 361px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: $grey;
|
||||
}
|
||||
|
||||
&__text-area {
|
||||
width: 100%;
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
&__save-button {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
}
|
||||
|
||||
&__optional-buttons {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
}
|
||||
|
||||
&__discard-button {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&__recover-button {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
&__image-container,
|
||||
&__email-container {
|
||||
background-color: $very-light-grey;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 5px 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
&__box {
|
||||
text-align: center;
|
||||
background-color: $very-light-grey;
|
||||
padding: 30px;
|
||||
margin-bottom: 30px;
|
||||
flex-basis: 48%;
|
||||
}
|
||||
|
||||
&__servers {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
&__server-form-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__imap-message {
|
||||
margin-top: 10px;
|
||||
padding-left: 40px;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.admin-panel-email-settings {
|
||||
&__image-container > label > .input,
|
||||
&__email-container > label > .input {
|
||||
display: unset;
|
||||
}
|
||||
|
||||
&__image-container,
|
||||
&__email-container,
|
||||
&__servers {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__email-server-address,
|
||||
&__image-header-url {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,295 +0,0 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import API from 'lib-app/api-call';
|
||||
|
||||
import AreYouSure from 'app-components/are-you-sure';
|
||||
import LanguageSelector from 'app-components/language-selector';
|
||||
|
||||
import Button from 'core-components/button';
|
||||
import Header from 'core-components/header';
|
||||
import Listing from 'core-components/listing';
|
||||
import Loading from 'core-components/loading';
|
||||
import Form from 'core-components/form';
|
||||
import FormField from 'core-components/form-field';
|
||||
import SubmitButton from 'core-components/submit-button';
|
||||
|
||||
class AdminPanelEmailTemplates extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
language: React.PropTypes.string,
|
||||
};
|
||||
|
||||
state = {
|
||||
headerImage: '',
|
||||
loadingHeaderImage: false,
|
||||
loadingList: true,
|
||||
loadingTemplate: false,
|
||||
templates: [],
|
||||
loadingForm: false,
|
||||
selectedIndex: -1,
|
||||
edited: false,
|
||||
errors: {},
|
||||
language: this.props.language,
|
||||
form: {
|
||||
subject: '',
|
||||
text1: '',
|
||||
text2: '',
|
||||
text3: '',
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.retrieveMailTemplateList();
|
||||
this.retrieveHeaderImage();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="admin-panel-email-templates">
|
||||
<Header title={i18n('EMAIL_TEMPLATES')} description={i18n('EMAIL_TEMPLATES_DESCRIPTION')} />
|
||||
{(!this.state.loadingList) ? this.renderContent() : this.renderLoading()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
return (
|
||||
<div>
|
||||
<div className="row">
|
||||
<div className="col-md-3">
|
||||
<Listing {...this.getListingProps()}/>
|
||||
</div>
|
||||
{(this.state.selectedIndex != -1) ? this.renderForm() : null}
|
||||
</div>
|
||||
<Form values={{headerImage: this.state.headerImage}} onChange={form => this.setState({headerImage: form.headerImage})} onSubmit={this.onHeaderImageSubmit.bind(this)}>
|
||||
<div className="admin-panel-email-templates__image-container">
|
||||
<FormField className="admin-panel-email-templates__image-header-url" label={i18n('IMAGE_HEADER_URL')} name="headerImage" required fieldProps={{size: 'large'}} />
|
||||
<SubmitButton className="admin-panel-email-templates__image-header-submit" type="secondary" size="small">{i18n('SAVE')}</SubmitButton>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderLoading() {
|
||||
return (
|
||||
<div className="admin-panel-email-templates__loading">
|
||||
<Loading backgrounded size="large"/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderForm() {
|
||||
return (
|
||||
<div className="col-md-9">
|
||||
<FormField label={i18n('LANGUAGE')} decorator={LanguageSelector} value={this.state.language} onChange={event => this.onItemChange(this.state.selectedIndex, event.target.value)} fieldProps={{
|
||||
type: 'allowed',
|
||||
size: 'medium'
|
||||
}}/>
|
||||
<Form {...this.getFormProps()}>
|
||||
<div className="row">
|
||||
<div className="col-md-7">
|
||||
<FormField label={i18n('SUBJECT')} name="subject" validation="TITLE" required fieldProps={{size: 'large'}}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormField label={i18n('TEXT') + '1'} name="text1" validation="TEXT_AREA" required decorator={'textarea'} fieldProps={{className: 'admin-panel-email-templates__text-area'}} />
|
||||
{(this.state.form.text2) ? <FormField label={i18n('TEXT') + '2'} name="text2" validation="TEXT_AREA" required decorator={'textarea'} fieldProps={{className: 'admin-panel-email-templates__text-area'}} /> : null}
|
||||
{(this.state.form.text3) ? <FormField label={i18n('TEXT') + '3'} name="text3" validation="TEXT_AREA" required decorator={'textarea'} fieldProps={{className: 'admin-panel-email-templates__text-area'}} /> : null}
|
||||
|
||||
<div className="admin-panel-email-templates__actions">
|
||||
<div className="admin-panel-email-templates__save-button">
|
||||
<SubmitButton type="secondary" size="small">{i18n('SAVE')}</SubmitButton>
|
||||
</div>
|
||||
<div className="admin-panel-email-templates__optional-buttons">
|
||||
{(this.state.edited) ? this.renderDiscardButton() : null}
|
||||
<div className="admin-panel-email-templates__recover-button">
|
||||
<Button onClick={this.onRecoverClick.bind(this)} size="medium">
|
||||
{i18n('RECOVER_DEFAULT')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderDiscardButton() {
|
||||
return (
|
||||
<div className="admin-panel-email-templates__discard-button">
|
||||
<Button onClick={this.onDiscardChangesClick.bind(this)} size="medium">
|
||||
{i18n('DISCARD_CHANGES')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getListingProps() {
|
||||
return {
|
||||
title: i18n('EMAIL_TEMPLATES'),
|
||||
items: this.getTemplateItems(),
|
||||
selectedIndex: this.state.selectedIndex,
|
||||
onChange: this.onItemChange.bind(this)
|
||||
};
|
||||
}
|
||||
|
||||
getFormProps() {
|
||||
return {
|
||||
values: this.state.form,
|
||||
errors: this.state.errors,
|
||||
loading: this.state.loadingForm,
|
||||
onChange: (form) => {this.setState({form, edited: true})},
|
||||
onValidateErrors: (errors) => {this.setState({errors})},
|
||||
onSubmit: this.onFormSubmit.bind(this)
|
||||
}
|
||||
}
|
||||
|
||||
getTemplateItems() {
|
||||
return this.state.templates.map((template) => {
|
||||
return {
|
||||
content: template
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
onItemChange(index, language) {
|
||||
if(this.state.edited) {
|
||||
AreYouSure.openModal(i18n('WILL_LOSE_CHANGES'), this.retrieveEmailTemplate.bind(this, index, language || this.state.language));
|
||||
} else {
|
||||
this.retrieveEmailTemplate(index, language || this.state.language);
|
||||
}
|
||||
}
|
||||
|
||||
onHeaderImageSubmit(form) {
|
||||
this.setState({
|
||||
loadingHeaderImage: true,
|
||||
});
|
||||
|
||||
API.call({
|
||||
path: '/system/edit-settings',
|
||||
data: {
|
||||
'mail-template-header-image': form['headerImage']
|
||||
}
|
||||
}).then(() => this.setState({
|
||||
loadingHeaderImage: false,
|
||||
}))
|
||||
}
|
||||
|
||||
onFormSubmit(form) {
|
||||
const {selectedIndex, language, templates} = this.state;
|
||||
|
||||
this.setState({loadingForm: true});
|
||||
|
||||
API.call({
|
||||
path: '/system/edit-mail-template',
|
||||
data: {
|
||||
template: templates[selectedIndex],
|
||||
language,
|
||||
subject: form.subject,
|
||||
text1: form.text1,
|
||||
text2: form.text2,
|
||||
text3: form.text3,
|
||||
}
|
||||
}).then(() => {
|
||||
this.setState({loadingForm: false, edited: false});
|
||||
}).catch(response => {
|
||||
this.setState({
|
||||
loadingForm: false,
|
||||
});
|
||||
|
||||
switch(response.message) {
|
||||
case 'INVALID_SUBJECT':
|
||||
this.setState({
|
||||
errors: {subject: i18n('INVALID_SYNTAX')}
|
||||
});
|
||||
break;
|
||||
case 'INVALID_TEXT_1':
|
||||
this.setState({
|
||||
errors: {text1: i18n('INVALID_SYNTAX')}
|
||||
});
|
||||
break;
|
||||
case 'INVALID_TEXT_2':
|
||||
this.setState({
|
||||
errors: {text2: i18n('INVALID_SYNTAX')}
|
||||
});
|
||||
break;
|
||||
case 'INVALID_TEXT_3':
|
||||
this.setState({
|
||||
errors: {text3: i18n('INVALID_SYNTAX')}
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onDiscardChangesClick(event) {
|
||||
event.preventDefault();
|
||||
this.onItemChange(this.state.selectedIndex, this.state.language);
|
||||
}
|
||||
|
||||
onRecoverClick(event) {
|
||||
event.preventDefault();
|
||||
AreYouSure.openModal(i18n('WILL_RECOVER_EMAIL_TEMPLATE'), this.recoverEmailTemplate.bind(this));
|
||||
}
|
||||
|
||||
recoverEmailTemplate() {
|
||||
const {selectedIndex, language, templates} = this.state;
|
||||
|
||||
API.call({
|
||||
path: '/system/recover-mail-template',
|
||||
data: {
|
||||
template: templates[selectedIndex],
|
||||
language
|
||||
}
|
||||
}).then(() => {
|
||||
this.retrieveEmailTemplate(this.state.selectedIndex, language);
|
||||
});
|
||||
}
|
||||
|
||||
retrieveEmailTemplate(index, language) {
|
||||
this.setState({
|
||||
loadingForm: true,
|
||||
});
|
||||
|
||||
API.call({
|
||||
path: '/system/get-mail-template',
|
||||
data: {template: this.state.templates[index], language}
|
||||
}).then((result) => this.setState({
|
||||
language,
|
||||
selectedIndex: index,
|
||||
edited: false,
|
||||
loadingForm: false,
|
||||
form: result.data,
|
||||
errors: {},
|
||||
}));
|
||||
}
|
||||
|
||||
retrieveMailTemplateList() {
|
||||
API.call({
|
||||
path: '/system/get-mail-template-list',
|
||||
data: {}
|
||||
}).then((result) => this.setState({
|
||||
loadingList: false,
|
||||
templates: result.data
|
||||
}));
|
||||
}
|
||||
|
||||
retrieveHeaderImage() {
|
||||
API.call({
|
||||
path: '/system/get-settings',
|
||||
data: {allSettings: 1}
|
||||
}).then(result => this.setState({
|
||||
headerImage: result.data['mail-template-header-image']
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export default connect((store) => {
|
||||
return {
|
||||
language: store.config.language,
|
||||
};
|
||||
})(AdminPanelEmailTemplates);
|
@ -1,37 +0,0 @@
|
||||
@import "../../../../scss/vars";
|
||||
|
||||
.admin-panel-email-templates {
|
||||
|
||||
&__text-area {
|
||||
width: 100%;
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
&__save-button {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
}
|
||||
|
||||
&__optional-buttons {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
}
|
||||
|
||||
&__discard-button {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&__recover-button {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&__image-container {
|
||||
background-color: $very-light-grey;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 5px 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
48
client/src/app/admin/panel/settings/admin-panel-system-preferences.js
Normal file → Executable file
48
client/src/app/admin/panel/settings/admin-panel-system-preferences.js
Normal file → Executable file
@ -26,7 +26,6 @@ class AdminPanelSystemPreferences extends React.Component {
|
||||
message: null,
|
||||
values: {
|
||||
maintenance: false,
|
||||
'smtp-pass': 'HIDDEN',
|
||||
}
|
||||
};
|
||||
|
||||
@ -53,35 +52,12 @@ class AdminPanelSystemPreferences extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<FormField label={i18n('SUPPORT_CENTER_URL')} fieldProps={{size: 'large'}} name="url" validation="URL" required/>
|
||||
<FormField label={i18n('SUPPORT_CENTER_LAYOUT')} fieldProps={{size: 'large', items: [{content: i18n('BOXED')}, {content: i18n('FULL_WIDTH')}]}} field="select" name="layout" />
|
||||
<div className="col-md-6 admin-panel-system-preferences__form-fields">
|
||||
<FormField className="admin-panel-system-preferences__form-fields__input" label={i18n('SUPPORT_CENTER_URL')} fieldProps={{size: 'large'}} name="url" validation="URL" required/>
|
||||
<FormField className="admin-panel-system-preferences__form-fields__select" label={i18n('SUPPORT_CENTER_LAYOUT')} fieldProps={{size: 'large', items: [{content: i18n('BOXED')}, {content: i18n('FULL_WIDTH')}]}} field="select" name="layout" />
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<FormField label={i18n('SUPPORT_CENTER_TITLE')} fieldProps={{size: 'large'}} name="title" validation="TITLE" required/>
|
||||
<FormField label={i18n('DEFAULT_TIMEZONE')} fieldProps={{size: 'large'}} name="time-zone"/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<span className="separator" />
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<FormField label={i18n('NOREPLY_EMAIL')} fieldProps={{size: 'large'}} name="no-reply-email"/>
|
||||
<FormField label={i18n('SMTP_USER')} fieldProps={{size: 'large'}} name="smtp-user"/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="row">
|
||||
<div className="col-md-9">
|
||||
<FormField label={i18n('SMTP_SERVER')} fieldProps={{size: 'large'}} name="smtp-host"/>
|
||||
<FormField label={i18n('SMTP_PASSWORD')} fieldProps={{size: 'large'}} name="smtp-pass"/>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<FormField label={i18n('PORT')} fieldProps={{size: 'auto'}} name="smtp-port"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
@ -122,12 +98,12 @@ class AdminPanelSystemPreferences extends React.Component {
|
||||
<span className="separator" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="row admin-panel-system-preferences__container">
|
||||
<div className="col-md-4 col-md-offset-2">
|
||||
<SubmitButton type="secondary">{i18n('UPDATE_SETTINGS')}</SubmitButton>
|
||||
<Button className="admin-panel-system-preferences__container__button" onClick={this.onDiscardChangesSubmit.bind(this)}>{i18n('DISCARD_CHANGES')}</Button>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<Button onClick={this.onDiscardChangesSubmit.bind(this)}>{i18n('DISCARD_CHANGES')}</Button>
|
||||
<SubmitButton className="admin-panel-system-preferences__container__button" type="secondary">{i18n('UPDATE_SETTINGS')}</SubmitButton>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
@ -172,12 +148,6 @@ class AdminPanelSystemPreferences extends React.Component {
|
||||
'url': form['url'],
|
||||
'title': form['title'],
|
||||
'layout': form['layout'] ? 'full-width' : 'boxed',
|
||||
'time-zone': form['time-zone'],
|
||||
'no-reply-email': form['no-reply-email'],
|
||||
'smtp-host': form['smtp-host'],
|
||||
'smtp-port': form['smtp-port'],
|
||||
'smtp-user': form['smtp-user'],
|
||||
[ form['smtp-pass'] !== 'HIDDEN' ? 'smtp-pass' : null]: form['smtp-pass'],
|
||||
'maintenance-mode': form['maintenance-mode'] * 1,
|
||||
'allow-attachments': form['allow-attachments'] * 1,
|
||||
'max-size': form['max-size'],
|
||||
@ -218,12 +188,6 @@ class AdminPanelSystemPreferences extends React.Component {
|
||||
'url': result.data['url'],
|
||||
'title': result.data['title'],
|
||||
'layout': (result.data['layout'] == 'full-width') ? 1 : 0,
|
||||
'time-zone': result.data['time-zone'],
|
||||
'no-reply-email': result.data['no-reply-email'],
|
||||
'smtp-host': result.data['smtp-host'],
|
||||
'smtp-port': result.data['smtp-port'],
|
||||
'smtp-user': result.data['smtp-user'],
|
||||
'smtp-pass': 'HIDDEN',
|
||||
'maintenance-mode': !!(result.data['maintenance-mode'] * 1),
|
||||
'allow-attachments': !!(result.data['allow-attachments'] * 1),
|
||||
'max-size': result.data['max-size'],
|
||||
|
@ -51,4 +51,30 @@
|
||||
&__message {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 376px) {
|
||||
.admin-panel-system-preferences {
|
||||
&__form-fields {
|
||||
padding: 0;
|
||||
|
||||
&__input, &__select {
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 415px) {
|
||||
.admin-panel-system-preferences {
|
||||
&__container__button {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
&__languages {
|
||||
&-allowed, &-supported {
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ class AddStaffModal extends React.Component {
|
||||
return SessionStore.getDepartments().map(department => {
|
||||
if(department.private*1){
|
||||
return <spam>{department.name} <Icon name='user-secret'/> </spam>
|
||||
}else {
|
||||
} else {
|
||||
return department.name;
|
||||
}
|
||||
});
|
||||
|
@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import {connect} from 'react-redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import API from 'lib-app/api-call';
|
||||
import ConfigActions from 'actions/config-actions';
|
||||
import SessionStore from 'lib-app/session-store';
|
||||
|
||||
import AreYouSure from 'app-components/are-you-sure';
|
||||
import DepartmentDropDown from 'app-components/department-dropdown';
|
||||
import DepartmentDropdown from 'app-components/department-dropdown';
|
||||
|
||||
import InfoTooltip from 'core-components/info-tooltip';
|
||||
import Button from 'core-components/button';
|
||||
@ -16,9 +17,19 @@ import Listing from 'core-components/listing';
|
||||
import Form from 'core-components/form';
|
||||
import FormField from 'core-components/form-field';
|
||||
import SubmitButton from 'core-components/submit-button';
|
||||
import DropDown from 'core-components/drop-down';
|
||||
import Icon from 'core-components/icon';
|
||||
import Message from 'core-components/message';
|
||||
import Loading from 'core-components/loading'
|
||||
|
||||
function getPublicDepartmentList(){
|
||||
return _.filter(SessionStore.getDepartments(),item => item.private != 1)
|
||||
}
|
||||
|
||||
export const getPublicDepartmentIndexFromDepartmentId = (departmentId, departments) => {
|
||||
const departmentIndex = _.findIndex(departments, department => department.id == departmentId);
|
||||
|
||||
return (departmentIndex !== -1) ? departmentIndex : 0;
|
||||
}
|
||||
|
||||
class AdminPanelDepartments extends React.Component {
|
||||
static defaultProps = {
|
||||
@ -29,52 +40,107 @@ class AdminPanelDepartments extends React.Component {
|
||||
formLoading: false,
|
||||
selectedIndex: -1,
|
||||
selectedDropDownIndex: 0,
|
||||
edited: false,
|
||||
editedAddDepartmentForm: false,
|
||||
editedDefaultDepartmentForm: false,
|
||||
errorMessage: null,
|
||||
errors: {},
|
||||
defaultDepartmentError: null,
|
||||
form: {
|
||||
title: '',
|
||||
name: '',
|
||||
language: 'en',
|
||||
private: 0,
|
||||
}
|
||||
private: 0
|
||||
},
|
||||
defaultDepartment: this.props.defaultDepartmentId,
|
||||
defaultDepartmentLocked: this.props.defaultDepartmentLocked * 1
|
||||
};
|
||||
|
||||
render() {
|
||||
const { errorMessage, formLoading, selectedIndex } = this.state;
|
||||
|
||||
return (
|
||||
<div className="admin-panel-departments">
|
||||
<Header title={i18n('DEPARTMENTS')} description={i18n('DEPARTMENTS_DESCRIPTION')} />
|
||||
<div className="row">
|
||||
<div className="col-md-4">
|
||||
<Listing {...this.getListingProps()}/>
|
||||
<Listing {...this.getListingProps()} />
|
||||
</div>
|
||||
<div className="col-md-8">
|
||||
{(this.state.errorMessage) ? <Message type="error">{i18n(this.state.errorMessage)}</Message> : null}
|
||||
{errorMessage ? <Message type="error">{i18n(errorMessage)}</Message> : null}
|
||||
<Form {...this.getFormProps()}>
|
||||
<div>
|
||||
<FormField className="admin-panel-departments__name" label={i18n('NAME')} name="name" validation="NAME" required fieldProps={{size: 'large'}}/>
|
||||
<div className="admin-panel-departments__container">
|
||||
<FormField className="admin-panel-departments__container__name" label={i18n('NAME')} name="name" validation="NAME" required fieldProps={{size: 'large'}} />
|
||||
<div className="admin-panel-departments__private-option">
|
||||
<FormField label={i18n('PRIVATE')} name="private" field="checkbox"/>
|
||||
<InfoTooltip className="admin-panel-departments__info-tooltip" text={i18n('PRIVATE_DEPARTMENT_DESCRIPTION')} />
|
||||
<FormField label={i18n('PRIVATE')} name="private" field="checkbox" />
|
||||
<InfoTooltip className="admin-panel-departments__container__info-tooltip" text={i18n('PRIVATE_DEPARTMENT_DESCRIPTION')} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-panel-departments__buttons-container">
|
||||
{(selectedIndex !== -1 && this.props.departments.length) ? this.renderOptionalButtons() : null}
|
||||
<div className="admin-panel-departments__update-name-button__container">
|
||||
<SubmitButton
|
||||
size="medium"
|
||||
className="admin-panel-departments__update-name-button"
|
||||
type="secondary">
|
||||
{
|
||||
formLoading ?
|
||||
<Loading /> :
|
||||
i18n((selectedIndex !== -1) ? 'UPDATE_DEPARTMENT' : 'ADD_DEPARTMENT')
|
||||
}
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</div>
|
||||
<SubmitButton size="medium" className="admin-panel-departments__update-name-button" type="secondary">
|
||||
{i18n((this.state.selectedIndex !== -1) ? 'UPDATE_DEPARTMENT' : 'ADD_DEPARTMENT')}
|
||||
</SubmitButton>
|
||||
</Form>
|
||||
{(this.state.selectedIndex !== -1 && this.props.departments.length) ? this.renderOptionalButtons() : null}
|
||||
</div>
|
||||
</div>
|
||||
{this.renderDefaultDepartmentForm()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderDefaultDepartmentForm() {
|
||||
const { defaultDepartmentError, formLoading } = this.state;
|
||||
|
||||
return (
|
||||
<div className="admin-panel-departments__default-departments-container">
|
||||
<span className="separator" />
|
||||
{(defaultDepartmentError !== null) ?
|
||||
((!defaultDepartmentError) ?
|
||||
<Message type="success">{i18n('SETTINGS_UPDATED')}</Message> :
|
||||
<Message type="error">{i18n(defaultDepartmentError)}</Message>) :
|
||||
null}
|
||||
<Form {...this.getDefaultDepartmentFormProps()} className="admin-panel-departments__default-departments-container__form">
|
||||
<div className="admin-panel-departments__default-departments-container__form__fields" >
|
||||
<FormField
|
||||
className="admin-panel-departments__default-departments-container__form__fields__select"
|
||||
label={i18n('DEFAULT_DEPARTMENT')}
|
||||
name="defaultDepartment"
|
||||
field="select"
|
||||
decorator={DepartmentDropdown}
|
||||
fieldProps={{departments: getPublicDepartmentList(), size: 'medium'}} />
|
||||
<FormField className="admin-panel-departments__default-departments-container__form__fields__checkbox" label={i18n('LOCK_DEPARTMENT_DESCRIPTION')} name="locked" field="checkbox" />
|
||||
</div>
|
||||
<SubmitButton
|
||||
className="admin-panel-departments__default-departments-container__form__button"
|
||||
size="medium"
|
||||
type="secondary" >
|
||||
{formLoading ? <Loading /> : i18n('UPDATE_DEFAULT_DEPARTMENT')}
|
||||
</SubmitButton>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderOptionalButtons() {
|
||||
return (
|
||||
<div className="admin-panel-departments__optional-buttons">
|
||||
<div className="admin-panel-departments__discard-button">
|
||||
<Button onClick={this.onDiscardChangesClick.bind(this)} size="medium">{i18n('DISCARD_CHANGES')}</Button>
|
||||
</div>
|
||||
<div className="admin-panel-departments__optional-buttons-container">
|
||||
{this.props.departments.length > 1 ? this.renderDeleteButton() : null}
|
||||
{
|
||||
(this.state.editedAddDepartmentForm) ?
|
||||
<div className="admin-panel-departments__discard-button">
|
||||
<Button onClick={this.onDiscardChangesClick.bind(this)} size="medium">{i18n('DISCARD_CHANGES')}</Button>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -93,25 +159,32 @@ class AdminPanelDepartments extends React.Component {
|
||||
{i18n('WILL_DELETE_DEPARTMENT')}
|
||||
<div className="admin-panel-departments__transfer-tickets">
|
||||
<span className="admin-panel-departments__transfer-tickets-title">{i18n('TRANSFER_TICKETS_TO')}</span>
|
||||
<DepartmentDropDown className="admin-panel-departments__transfer-tickets-drop-down" departments={this.getDropDownDepartments()} onChange={(event) => this.setState({selectedDropDownIndex: event.index})} size="medium"/>
|
||||
<DepartmentDropdown
|
||||
className="admin-panel-departments__transfer-tickets-drop-down"
|
||||
departments={this.getDropDownDepartments()}
|
||||
onChange={(event) => this.setState({selectedDropDownIndex: event.index})}
|
||||
size="medium" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getListingProps() {
|
||||
const { departments, defaultDepartmentId } = this.props;
|
||||
|
||||
return {
|
||||
className: 'admin-panel-departments__list',
|
||||
title: i18n('DEPARTMENTS'),
|
||||
items: this.props.departments.map(department => {
|
||||
items: departments.map(department => {
|
||||
return {
|
||||
content: (
|
||||
<span>
|
||||
{department.name}
|
||||
{department.private*1 ? <Icon className="admin-panel-departments__private-icon" name='user-secret'/> : null }
|
||||
{department.private*1 ? <Icon className="admin-panel-departments__private-icon" name='user-secret' /> : null}
|
||||
{department.id == defaultDepartmentId ? <spam className="admin-panel-departments__default-icon"> {i18n('DEFAULT')} </spam> : null}
|
||||
{(!department.owners) ? (
|
||||
<span className="admin-panel-departments__warning">
|
||||
<InfoTooltip type="warning" text={i18n('NO_STAFF_ASSIGNED')}/>
|
||||
<InfoTooltip type="warning" text={i18n('NO_STAFF_ASSIGNED')} />
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
@ -126,39 +199,81 @@ class AdminPanelDepartments extends React.Component {
|
||||
}
|
||||
|
||||
getFormProps() {
|
||||
const { form, errors, formLoading } = this.state;
|
||||
|
||||
return {
|
||||
values: this.state.form,
|
||||
errors: this.state.errors,
|
||||
loading: this.state.formLoading,
|
||||
onChange: (form) => {this.setState({form, edited: true})},
|
||||
values: {...form, private: !!form.private},
|
||||
errors: errors,
|
||||
loading: formLoading,
|
||||
onChange: (form) => {this.setState({form, editedAddDepartmentForm: true})},
|
||||
onValidateErrors: (errors) => {this.setState({errors})},
|
||||
onSubmit: this.onFormSubmit.bind(this)
|
||||
onSubmit: this.onFormSubmit.bind(this),
|
||||
loading: formLoading,
|
||||
className: 'admin-panel-departments__form'
|
||||
};
|
||||
}
|
||||
|
||||
getDefaultDepartmentFormProps() {
|
||||
const { formLoading, defaultDepartment, defaultDepartmentLocked } = this.state;
|
||||
|
||||
return {
|
||||
values: {
|
||||
defaultDepartment: getPublicDepartmentIndexFromDepartmentId(defaultDepartment, getPublicDepartmentList()),
|
||||
locked: defaultDepartmentLocked ? true : false,
|
||||
},
|
||||
onChange: (formValue) => {
|
||||
this.setState({
|
||||
editedDefaultDepartmentForm: true,
|
||||
defaultDepartmentError: null,
|
||||
defaultDepartment: getPublicDepartmentList()[formValue.defaultDepartment].id,
|
||||
defaultDepartmentLocked: formValue.locked
|
||||
});
|
||||
},
|
||||
onSubmit: this.onDefaultDepartmentFormSubmit.bind(this),
|
||||
loading: formLoading
|
||||
};
|
||||
}
|
||||
|
||||
onItemChange(index) {
|
||||
if(this.state.edited) {
|
||||
if(this.state.editedAddDepartmentForm) {
|
||||
AreYouSure.openModal(i18n('WILL_LOSE_CHANGES'), this.updateForm.bind(this, index));
|
||||
} else {
|
||||
this.updateForm(index);
|
||||
}
|
||||
}
|
||||
|
||||
onDefaultDepartmentFormSubmit(formValue) {
|
||||
let publicDepartments = getPublicDepartmentList();
|
||||
|
||||
this.setState({formLoading: true, editedDefaultDepartmentForm: false});
|
||||
|
||||
API.call({
|
||||
path: '/system/edit-settings',
|
||||
data: {
|
||||
'default-department-id': this.getCurrentDepartment(publicDepartments, formValue.defaultDepartment).id,
|
||||
'default-is-locked': formValue.locked ? 1 : 0
|
||||
}
|
||||
}).then(() => {
|
||||
this.retrieveDepartments(true);
|
||||
this.setState({formLoading: false, errorMessage: false, defaultDepartmentError: false});
|
||||
}).catch(result => this.setState({formLoading: false, defaultDepartmentError: result.message}));
|
||||
}
|
||||
|
||||
onFormSubmit(form) {
|
||||
this.setState({formLoading: true, edited: false});
|
||||
this.setState({formLoading: true, editedAddDepartmentForm: false});
|
||||
|
||||
if(this.state.selectedIndex !== -1) {
|
||||
API.call({
|
||||
path: '/system/edit-department',
|
||||
data: {
|
||||
departmentId: this.getCurrentDepartment().id,
|
||||
departmentId: this.getCurrentDepartment(this.props.departments).id,
|
||||
name: form.name,
|
||||
private: form.private ? 1 : 0
|
||||
}
|
||||
}).then(() => {
|
||||
this.setState({formLoading: false});
|
||||
this.setState({formLoading: false, errorMessage: false, defaultDepartmentError: null});
|
||||
this.retrieveDepartments();
|
||||
}).catch(result => this.setState({formLoading: false, errorMessage: result.message}));
|
||||
}).catch(result => this.setState({formLoading: false, errorMessage: result.message, defaultDepartmentError: null}));
|
||||
} else {
|
||||
API.call({
|
||||
path: '/system/add-department',
|
||||
@ -167,9 +282,13 @@ class AdminPanelDepartments extends React.Component {
|
||||
private: form.private ? 1 : 0
|
||||
}
|
||||
}).then(() => {
|
||||
this.setState({formLoading: false,errorMessage: false, defaultDepartmentError: null});
|
||||
this.retrieveDepartments();
|
||||
this.onItemChange(-1);
|
||||
}).catch(this.onItemChange.bind(this, -1));
|
||||
}).catch(() => {
|
||||
this.onItemChange.bind(this, -1);
|
||||
this.setState({formLoading: false, defaultDepartmentError: null});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -186,49 +305,56 @@ class AdminPanelDepartments extends React.Component {
|
||||
}
|
||||
|
||||
deleteDepartment() {
|
||||
API.call({
|
||||
return API.call({
|
||||
path: '/system/delete-department',
|
||||
data: {
|
||||
departmentId: this.getCurrentDepartment().id,
|
||||
departmentId: this.getCurrentDepartment(this.props.departments).id,
|
||||
transferDepartmentId: this.getDropDownItemId()
|
||||
}
|
||||
}).then(() => {
|
||||
this.retrieveDepartments();
|
||||
this.onItemChange(-1);
|
||||
this.setState({defaultDepartmentError: null});
|
||||
})
|
||||
.catch(result => this.setState({errorMessage: result.message}));
|
||||
.catch(result => this.setState({errorMessage: result.message, defaultDepartmentError: null}));
|
||||
}
|
||||
|
||||
updateForm(index) {
|
||||
let form = _.clone(this.state.form);
|
||||
let department = this.getCurrentDepartment(index);
|
||||
let department = this.getCurrentDepartment(this.props.departments, index);
|
||||
|
||||
form.name = (department && department.name) || '';
|
||||
form.private = (department && department.private) || 0;
|
||||
|
||||
this.setState({
|
||||
selectedIndex: index,
|
||||
edited: false,
|
||||
editedAddDepartmentForm: false,
|
||||
formLoading: false,
|
||||
form,
|
||||
errorMessage: null,
|
||||
errors: {}
|
||||
errors: {},
|
||||
defaultDepartmentError: null
|
||||
});
|
||||
}
|
||||
|
||||
retrieveDepartments() {
|
||||
retrieveDepartments(fromUpdateDefaultDepartmentForm = false) {
|
||||
this.props.dispatch(ConfigActions.updateData());
|
||||
this.setState({
|
||||
edited: false
|
||||
});
|
||||
|
||||
this.setState(
|
||||
fromUpdateDefaultDepartmentForm ?
|
||||
{editedDefaultDepartmentForm: false} :
|
||||
{editedAddDepartmentForm: false}
|
||||
);
|
||||
}
|
||||
|
||||
getCurrentDepartment(index) {
|
||||
return this.props.departments[(index == undefined) ? this.state.selectedIndex : index];
|
||||
getCurrentDepartment(list, index) {
|
||||
return list[(index == undefined) ? this.state.selectedIndex : index];
|
||||
}
|
||||
|
||||
getDropDownItemId() {
|
||||
return this.props.departments.filter((department, index) => index !== this.state.selectedIndex)[this.state.selectedDropDownIndex].id;
|
||||
const { selectedIndex, selectedDropDownIndex } = this.state;
|
||||
|
||||
return this.props.departments.filter((department, index) => index !== selectedIndex)[selectedDropDownIndex].id;
|
||||
}
|
||||
|
||||
getDropDownDepartments() {
|
||||
@ -238,6 +364,8 @@ class AdminPanelDepartments extends React.Component {
|
||||
|
||||
export default connect((store) => {
|
||||
return {
|
||||
defaultDepartmentId: store.config['default-department-id']*1,
|
||||
defaultDepartmentLocked: store.config['default-is-locked']*1,
|
||||
departments: store.config.departments
|
||||
};
|
||||
})(AdminPanelDepartments);
|
||||
|
@ -7,26 +7,67 @@
|
||||
}
|
||||
|
||||
&__update-name-button {
|
||||
float: left;
|
||||
min-width: 156px;
|
||||
|
||||
&__container {
|
||||
padding: 0 0 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__name {
|
||||
display:inline-block;margin-right: 101px;
|
||||
}
|
||||
&__private-option {
|
||||
display:inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&__optional-buttons {
|
||||
float: right;
|
||||
&__buttons-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__discard-button,
|
||||
&__delete-button {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
&__container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 460px;
|
||||
|
||||
&__info-tooltip {
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&__form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&__default-departments-container {
|
||||
&__form {
|
||||
text-align: left;
|
||||
&__fields {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
align-items: flex-start;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
&__button {
|
||||
min-width: 156px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__optional-buttons-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__warning {
|
||||
@ -49,7 +90,43 @@
|
||||
&__private-icon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
&__info-tooltip {
|
||||
margin-left: 2px;
|
||||
&__default-icon {
|
||||
color: lightgrey;
|
||||
font-style: italic;
|
||||
text-transform: lowercase;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 415px) {
|
||||
.admin-panel-departments {
|
||||
&__container{
|
||||
display: unset;
|
||||
width: 100%;
|
||||
|
||||
&__name {
|
||||
margin: 30px;
|
||||
}
|
||||
|
||||
&__private-option {
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__update-name-button {
|
||||
float: unset;
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
&__default-departments-container__form__fields {
|
||||
|
||||
&__checkbox {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -11,10 +11,9 @@ import SessionStore from 'lib-app/session-store';
|
||||
import PeopleList from 'app-components/people-list';
|
||||
import ModalContainer from 'app-components/modal-container';
|
||||
|
||||
import AddStaffModal from 'app/admin/panel/staff/add-staff-modal';
|
||||
import InviteStaffModal from 'app/admin/panel/staff/invite-staff-modal';
|
||||
|
||||
import Header from 'core-components/header';
|
||||
import DropDown from 'core-components/drop-down';
|
||||
import Button from 'core-components/button';
|
||||
import Icon from 'core-components/icon';
|
||||
import Loading from 'core-components/loading';
|
||||
@ -47,8 +46,8 @@ class AdminPanelStaffMembers extends React.Component {
|
||||
<Header title={i18n('STAFF_MEMBERS')} description={i18n('STAFF_MEMBERS_DESCRIPTION')} />
|
||||
<div className="admin-panel-staff-members__wrapper">
|
||||
<DepartmentDropdown {...this.getDepartmentDropdownProps()} className="admin-panel-staff-members__dropdown" />
|
||||
<Button onClick={this.onAddNewStaff.bind(this)} size="medium" type="secondary" className="admin-panel-staff-members__button">
|
||||
<Icon name="user-plus" className=""/> {i18n('ADD_NEW_STAFF')}
|
||||
<Button onClick={this.onInviteStaff.bind(this)} size="medium" type="secondary" className="admin-panel-staff-members__button">
|
||||
<Icon name="user-plus" className="" /> {i18n('INVITE_STAFF')}
|
||||
</Button>
|
||||
</div>
|
||||
{(this.props.loading) ? <Loading backgrounded /> : <PeopleList list={this.getStaffList()} page={this.state.page} onPageSelect={(index) => this.setState({page: index+1})} />}
|
||||
@ -56,8 +55,8 @@ class AdminPanelStaffMembers extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
onAddNewStaff() {
|
||||
ModalContainer.openModal(<AddStaffModal onSuccess={this.retrieveStaffMembers.bind(this)} />);
|
||||
onInviteStaff() {
|
||||
ModalContainer.openModal(<InviteStaffModal onSuccess={this.retrieveStaffMembers.bind(this)} />);
|
||||
}
|
||||
|
||||
getDepartmentDropdownProps() {
|
||||
|
@ -26,4 +26,21 @@
|
||||
&__private {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 415px) {
|
||||
.admin-panel-staff-members {
|
||||
&__drowpdown {
|
||||
float: unset;
|
||||
}
|
||||
|
||||
&__button {
|
||||
float: unset;
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
height: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,8 +32,21 @@ class AdminPanelViewStaff extends React.Component {
|
||||
}
|
||||
|
||||
getProps() {
|
||||
return _.extend({}, this.state.userData, {
|
||||
staffId: this.props.params.staffId * 1,
|
||||
const { userData } = this.state;
|
||||
const {
|
||||
userId,
|
||||
params
|
||||
} = this.props;
|
||||
const userDataWithNumericLevel = {
|
||||
...userData,
|
||||
level: userData.level*1,
|
||||
sendEmailOnNewTicket: userData.sendEmailOnNewTicket === "1",
|
||||
myAccount: this.props.userEmail == userData.email
|
||||
};
|
||||
|
||||
return _.extend({}, userDataWithNumericLevel, {
|
||||
userId: userId*1,
|
||||
staffId: params.staffId*1,
|
||||
onDelete: this.onDelete.bind(this),
|
||||
onChange: this.retrieveStaff.bind(this)
|
||||
});
|
||||
@ -49,14 +62,18 @@ class AdminPanelViewStaff extends React.Component {
|
||||
}
|
||||
|
||||
onStaffRetrieved(result) {
|
||||
const {
|
||||
userId,
|
||||
params,
|
||||
dispatch
|
||||
} = this.props;
|
||||
|
||||
this.setState({
|
||||
loading: false,
|
||||
userData: result.data
|
||||
});
|
||||
|
||||
if(this.props.userId == this.props.params.staffId) {
|
||||
this.props.dispatch(SessionActions.getUserData(null, null, true))
|
||||
}
|
||||
if(userId == params.staffId) dispatch(SessionActions.getUserData(null, null, true));
|
||||
}
|
||||
|
||||
onDelete() {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user