Merge branch 'opensupports:master' into master

This commit is contained in:
Luri Darmawan 2022-01-14 10:30:13 +07:00 committed by GitHub
commit 03bf0821a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
486 changed files with 57238 additions and 5766 deletions

190
.circleci/config.yml Normal file
View File

@ -0,0 +1,190 @@
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:
docker:
- image: 'cimg/base:edge'
steps:
- checkout
- php/install-php:
version: '7.0'
- php/install-composer
- run:
name: Install php extensions
command: |
sudo add-apt-repository ppa:ondrej/php
sudo apt update
sudo apt install php7.0-imap -y
sudo apt install php7.0-xml -y
sudo apt install zip unzip php7.0-zip php7.0-mbstring -y
- 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
View File

@ -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

67
Makefile Normal file
View 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

View File

@ -1,6 +1,6 @@
![OpenSupports](http://www.opensupports.com/logo.png)
[![Build Status](https://travis-ci.org/opensupports/opensupports.svg?branch=master)](https://travis-ci.org/opensupports/opensupports) v4.6.1
[![Build Status](https://travis-ci.org/opensupports/opensupports.svg?branch=master)](https://travis-ci.org/opensupports/opensupports) v4.11.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/)
@ -65,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

19
SECURITY.md Normal file
View File

@ -0,0 +1,19 @@
# Security Policy
This document is intended to provide a guide to properly disclosure security issues found in our open source software.
## Reporting a Vulnerability
If you find a vulnerability or potential security issue in OpenSupports. Please contact us at contact@opensupports.com
We will acknowledge your email within 48 hours, and will send a more detailed response within 48 hours indicating the next steps in handling your report. After the initial reply to your report, we will endeavor to keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
## Disclosure Policy
When we receive a security bug report, we will assign it to a
primary handler. This person will coordinate the fix and release process,
involving the following steps:
* Confirm the problem and determine the affected versions.
* Audit code to find any potential similar problems.
* Prepare fixes for all releases still under maintenance. These fixes will be
released as fast as possible in a new OpenSupports version.

View File

@ -4,7 +4,6 @@ npm run build
rm build/index.html
echo "2/3 Creating api folder..."
cd ../server
composer install
echo -n > config.php
mkdir files2
mv files/.htaccess files2

View File

@ -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/'
};

View File

@ -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);
});

View File

@ -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: '/'
});
});

View File

@ -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))
});

View File

@ -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);
});

View File

@ -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))
});

View File

@ -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/'));
});

View File

@ -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));
});

View File

@ -1,10 +0,0 @@
'use strict';
var gulp = require('gulp');
//var config = require('../config');
gulp.task('deploy', ['prod'], function() {
// Deploy to hosting environment
});

View File

@ -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);
});

View File

@ -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 })));
});

View File

@ -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);
});

View File

@ -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 })));
});

View File

@ -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);
});

View File

@ -1,10 +0,0 @@
'use strict';
var gulp = require('gulp');
//var config = require('../config');
gulp.task('test', function() {
// Run all tests
});

View File

@ -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']);
});

View File

@ -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);
}
};

View File

@ -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));
};

View File

@ -1,5 +0,0 @@
'use strict';
global.isProd = false;
require('./gulp');

37261
client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "OpenSupports",
"version": "4.2.0",
"version": "4.11.0",
"author": "Ivan Diaz <contact@opensupports.com>",
"description": "Open source ticket system made with PHP and ReactJS",
"repository": {
@ -15,7 +15,7 @@
"scripts": {
"start": "webpack-dev-server",
"start-fixtures": "webpack-dev-server --env.FIXTURES=1",
"build": "rimraf build && NODE_ENV=production webpack -p --devtool none",
"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": {
@ -30,7 +30,7 @@
"axios-mock-adapter": "^1.15.0",
"babel-loader": "^8.0.6",
"babel-plugin-add-module-exports": "^1.0.2",
"browser-sync": "^2.7.13",
"browser-sync": "^2.27.5",
"chai": "^3.5.0",
"copy-webpack-plugin": "^5.0.3",
"css-loader": "^3.0.0",
@ -67,18 +67,23 @@
"webpack-import-glob": "^2.0.0"
},
"dependencies": {
"axios": "^0.18.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": "^4.17.15",
"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",
"quill-magic-url": "^4.1.3",
"random-string": "^0.2.0",
"react": "^15.4.2",
"react-chartjs-2": "^2.10.0",
"react-document-title": "^1.0.2",
"react-dom": "^15.4.2",
"react-google-recaptcha": "^0.5.2",

View File

@ -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());
// });
// });
// });

View File

@ -0,0 +1,15 @@
export default {
showLoginForm() {
return {
type: 'SHOW_LOGIN_FORM',
payload: true
};
},
hideLoginForm() {
return {
type: 'HIDE_LOGIN_FORM',
payload: false
};
}
};

View 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}
}
},
};

View File

@ -22,6 +22,7 @@ export default {
.then(() => {
if(result.data.staff) {
store.dispatch(AdminDataActions.retrieveCustomResponses());
store.dispatch(AdminDataActions.retrieveStaffMembers());
}
});
@ -53,11 +54,11 @@ 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;
})

View File

@ -35,7 +35,6 @@ describe('TicketList component', function () {
id: 1,
name: 'Sales Support'
},
priority: 'low',
author: {
id: 3,
name: 'Francisco Villegas'
@ -58,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);
@ -69,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 () {
@ -98,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({
@ -135,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 () {

View File

@ -18,7 +18,6 @@ class ActivityRow extends React.Component {
'CREATE_TICKET',
'RE_OPEN',
'DEPARTMENT_CHANGED',
'PRIORITY_CHANGED',
'EDIT_TITLE',
'EDIT_COMMENT',
@ -59,7 +58,6 @@ class ActivityRow extends React.Component {
'CREATE_TICKET',
'RE_OPEN',
'DEPARTMENT_CHANGED',
'PRIORITY_CHANGED',
'COMMENT_EDITED',
'EDIT_TITLE',
'EDIT_COMMENT',
@ -115,7 +113,6 @@ class ActivityRow extends React.Component {
'CREATE_TICKET': 'ticket',
'RE_OPEN': 'unlock-alt',
'DEPARTMENT_CHANGED': 'exchange',
'PRIORITY_CHANGED': 'exclamation',
'EDIT_TITLE': 'edit',
'EDIT_COMMENT': 'edit',

View File

@ -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;

View File

@ -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;
}

View File

@ -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>
);

View File

@ -2,7 +2,11 @@
width: 800px;
&__cancel-button {
float: right;
float: left;
margin-top: 15px;
}
&__submit-button {
float: right;
}
}

View File

@ -36,11 +36,13 @@ class ArticlesList extends React.Component {
}
render() {
if(this.props.errored) {
return <Message type="error">{i18n('ERROR_RETRIEVING_ARTICLES')}</Message>;
const { errored, loading } = this.props;
if(errored) {
return <Message showCloseButton={false} 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);

View File

@ -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;
}
}
}

View File

@ -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';

View File

@ -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,12 +8,14 @@ import Modal from 'core-components/modal';
class ModalContainer extends React.Component {
static openModal(content, noPadding, outsideClick=false) {
static openModal(
content,
options={noPadding: false, outsideClick: false, closeButton: {showCloseButton: false, whiteColor: false}}
) {
store.dispatch(
ModalActions.openModal({
content,
noPadding,
outsideClick
options
})
);
}
@ -50,9 +51,16 @@ class ModalContainer extends React.Component {
}
renderModal() {
const {content, noPadding, outsideClick} = this.props.modal;
const { content, options } = this.props.modal;
const { noPadding, outsideClick, closeButton } = options;
return (
<Modal content={content} noPadding={noPadding} outsideClick={outsideClick} onOutsideClick={this.closeModal.bind(this)}/>
<Modal
content={content}
noPadding={noPadding}
outsideClick={outsideClick}
onOutsideClick={this.closeModal.bind(this)}
closeButton={closeButton} />
);
}
@ -71,4 +79,4 @@ export default connect((store) => {
return {
modal: store.modal
};
})(ModalContainer);
})(ModalContainer);

View File

@ -24,10 +24,21 @@ class PasswordRecovery extends React.Component {
renderLogo: false
};
state = {
showRecoverSentMessage: true
}
componentDidUpdate(prevProps) {
if (!prevProps.recoverSent && this.props.recoverSent) {
this.setState({showRecoverSentMessage : true});
}
}
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">
@ -63,22 +74,29 @@ class PasswordRecovery extends React.Component {
}
renderRecoverStatus() {
let status = null;
if (this.props.recoverSent) {
status = (
<Message className="password-recovery__message" type="info" leftAligned>
{i18n('RECOVER_SENT')}
</Message>
);
}
return status;
return (
this.props.recoverSent ?
<Message
showMessage={this.state.showRecoverSentMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showRecoverSentMessage")}
className="password-recovery__message"
type="info"
leftAligned>
{i18n('RECOVER_SENT')}
</Message> :
null
);
}
focusEmail() {
this.refs.email.focus();
}
onCloseMessage(showMessage) {
this.setState({
[showMessage]: false
});
}
}
export default PasswordRecovery;

View File

@ -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%;
}
}

View File

@ -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;
}
}
}
}
}

View File

@ -1,10 +1,8 @@
import React from 'react';
import i18n from 'lib-app/i18n';
import ModalContainer from 'app-components/modal-container';
import Button from 'core-components/button';
import Icon from 'core-components/icon';
import Message from 'core-components/message';
@ -17,9 +15,8 @@ class PopupMessage extends React.Component {
static open(props) {
ModalContainer.openModal(
<PopupMessage {...props}/>,
true,
true
<PopupMessage {...props} />,
{noPadding: true, outsideClick: true, closeButton: {showCloseButton: false, whiteColor: false}}
);
}
@ -30,17 +27,22 @@ class PopupMessage extends React.Component {
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)}/>
<Message {...this.props} showCloseButton={false} className="popup-message__message" />
<Button
className="popup-message__close-button"
iconName="times"
type="clean"
ref="closeButton"
onClick={this.closeModal.bind(this)} />
</div>
);
}
closeModal() {
if (this.context.closeModal) {
this.context.closeModal();
}
const { closeModal } = this.context;
closeModal && closeModal();
}
}
export default PopupMessage;
export default PopupMessage;

View 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;

View 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;
}
}

View File

@ -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;

View File

@ -1,191 +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 className="stats__disable-box">
<div className="stats__disable-box-message">
{i18n('CURRENTLY_UNAVAILABLE')}
</div>
</div>
</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;

View File

@ -1,95 +0,0 @@
@import '../scss/vars';
.stats {
pointer-events: none;
position: relative;
&__disable-box {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
background-color: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
&-message {
color: white;
font-size: $font-size--bg;
}
}
&__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;
}
}
}
}
}

View File

@ -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,12 +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 TextEditor from 'core-components/text-editor';
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([
@ -22,7 +24,6 @@ class TicketEvent extends React.Component {
'CLOSE',
'RE_OPEN',
'DEPARTMENT_CHANGED',
'PRIORITY_CHANGED'
]),
author: React.PropTypes.object,
content: React.PropTypes.string,
@ -30,7 +31,9 @@ class TicketEvent extends React.Component {
private: React.PropTypes.string,
edited: React.PropTypes.bool,
edit: React.PropTypes.bool,
onToggleEdit: React.PropTypes.func
onToggleEdit: React.PropTypes.func,
isLastComment: React.PropTypes.bool,
isTicketClosed: React.PropTypes.bool
};
state = {
@ -85,39 +88,64 @@ 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 = this.props.author;
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>
{!this.props.edit ? this.renderContent() : this.renderEditField()}
{this.renderFooter(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() {
const { content, author, userId, userStaff, isLastComment, isTicketClosed } = this.props;
const { id, staff } = author;
return (
<div className="ticket-event__comment-content">
<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 className="ticket-event__comment-content ql-editor">
<div dangerouslySetInnerHTML={{__html: content}}></div>
{(id == userId && staff == userStaff && isLastComment && !isTicketClosed) ? this.renderEditIcon() : null }
</div>
)
}
@ -206,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">
@ -271,7 +288,6 @@ class TicketEvent extends React.Component {
'CLOSE': true,
'RE_OPEN': true,
'DEPARTMENT_CHANGED': true,
'PRIORITY_CHANGED': true
};
const classes = {
'row': true,
@ -282,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,
};
@ -297,7 +312,6 @@ class TicketEvent extends React.Component {
'CLOSE': 'lock',
'RE_OPEN': 'unlock-alt',
'DEPARTMENT_CHANGED': 'exchange',
'PRIORITY_CHANGED': 'exclamation'
};
const iconSize = {
'COMMENT': '2x',
@ -306,7 +320,6 @@ class TicketEvent extends React.Component {
'CLOSE': 'lg',
'RE_OPEN': 'lg',
'DEPARTMENT_CHANGED': 'lg',
'PRIORITY_CHANGED': 'lg'
};
return {
@ -324,4 +337,7 @@ class TicketEvent extends React.Component {
}
}
export default TicketEvent;
export default connect((store) => {
return { level: store.session.userLevel };
})(TicketEvent);

View File

@ -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,9 +94,10 @@
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 {
@ -212,12 +213,6 @@
}
}
&_priority {
.ticket-event__icon {
padding-left: 11px;
padding-top: 5px;
}
}
&_private {
.ticket-event__comment-pointer {
border-right-color: $light-yellow;

View File

@ -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;

View File

@ -36,7 +36,6 @@
&__status,
&__owner,
&__priority,
&__comments {
display: inline-block;
width: 50%;

View File

@ -1,6 +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';
@ -12,7 +13,9 @@ import Button from 'core-components/button';
import Tooltip from 'core-components/tooltip';
import Checkbox from 'core-components/checkbox';
import Tag from 'core-components/tag';
import Icon from 'core-components/icon';
import Message from 'core-components/message';
import history from 'lib-app/history';
class TicketList extends React.Component {
static propTypes = {
@ -28,7 +31,7 @@ class TicketList extends React.Component {
]),
closedTicketsShown: React.PropTypes.bool,
onClosedTicketsShownChange: React.PropTypes.func,
onDepartmentChange: React.PropTypes.func,
onDepartmentChange: React.PropTypes.func
};
static defaultProps = {
@ -46,12 +49,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 === 'primary' ? this.renderMessage() : null}
{(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>
@ -60,7 +69,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() {
@ -72,42 +89,59 @@ class TicketList extends React.Component {
}
renderMessage() {
switch (this.getQueryVariable('message')) {
switch (queryString.parse(window.location.search)["message"]) {
case 'success':
return <Message className="create-ticket-form__message" type="success">{i18n('TICKET_SENT')}</Message>
return (
<Message
onCloseMessage={this.onCloseMessage}
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>;
return (
<Message
onCloseMessage={this.onCloseMessage}
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 && this.props.departments[event.index - 1].id;
const departmentId = event.index && departments[event.index - 1].id;
this.setState({
selectedDepartment: departmentId
});
if(this.props.onDepartmentChange) {
this.props.onDepartmentChange(departmentId || null);
}
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
};
}
@ -121,22 +155,10 @@ class TicketList extends React.Component {
return departments;
}
getQueryVariable(variable) {
let query = window.location.search.substring(1);
let vars = query.split("&");
for (let i=0; i < vars.length; i++) {
let pair = vars[i].split("=");
if(pair[0] == variable) {
return pair[1];
}
}
return false;
}
getTableHeaders() {
if (this.props.type == 'primary' ) {
const { type } = this.props;
if(type == 'primary' ) {
return [
{
key: 'number',
@ -155,11 +177,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',
@ -171,11 +196,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'),
@ -188,113 +208,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));
return this.getTickets().map(this.getTicketTableObject.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;
getTicketTableObject(ticket) {
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) ? i18n("YESTERDAY_AT") : i18n("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: (
<div>
<Button className="ticket-list__title-link" type="clean" route={{to: this.props.ticketPath + ticket.ticketNumber}}>
{closed ? <Icon size="sm" name="lock" /> : null}
<Button className="ticket-list__title-link" type="clean" route={{to: this.props.ticketPath + ticketNumber}}>
{titleText}
</Button>
{(ticket.tags || []).map((tagName,index) => {
{(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;
}
}
}
onCloseMessage() {
history.push(window.location.pathname);
}
}
export default connect((store) => {

View File

@ -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;
}
}

View File

@ -0,0 +1,390 @@
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;
this.onChangeFormState({...formState, tags: [...formState.tags, tag]});
}
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) {
selectedDepartments = selectedDepartmentsId.map(
(departmentId) => this.getDepartmentsItems().find(_department => (_department.id === departmentId))
);
}
return selectedDepartments;
}
getSelectedStaffs(selectedStaffsId) {
let selectedStaffs = [];
if(selectedStaffsId !== undefined) {
selectedStaffs = selectedStaffsId.map(
(staffId) => this.getStaffList().find(_staff => (_staff.id === staffId))
);
}
return selectedStaffs;
}
getSelectedTagsName(selectedTagsId) {
let selectedTagsName = [];
if(selectedTagsId !== undefined) {
selectedTagsName = selectedTagsId.map(
(tagId) => (this.getTags().find(_tag => (_tag.id === tagId)) || {}).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;
this.onChangeFormState({...formState, tags: formState.tags.filter(item => item !== tag)});
}
tagsNametoTagsId(selectedTagsName) {
let selectedTagsId = [];
if (selectedTagsName != undefined) {
selectedTagsId = selectedTagsName.map(
(tagName) => (this.getTags().find(_tag => (_tag.name === tagName)) || {}).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);

View 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;
}
}
}

View File

@ -1,12 +1,13 @@
import React from 'react';
import _ from 'lodash';
import API from 'lib-app/api-call';
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 {
@ -18,66 +19,52 @@ class TicketQueryList extends React.Component {
loading: true
};
componentDidMount() {
this.getTickets();
}
componentDidUpdate(prevProps) {
if (this.props.customList.title !== prevProps.customList.title) {
this.getTickets();
}
}
render() {
return (
<div>
{(this.state.error) ? <Message type="error">{i18n('ERROR_RETRIEVING_TICKETS')}</Message> : <TicketList {...this.getTicketListProps()}/>}
</div>
this.state.error ?
<Message showCloseButton={false} type="error">{i18n('ERROR_RETRIEVING_TICKETS')}</Message> :
<TicketList {...this.getTicketListProps()} />
);
}
getTickets() {
this.setState({
loading:true
})
API.call({
path: '/ticket/search',
data: {
page : this.state.page,
...this.props.customList.filters
}
}).then((result) => {
this.setState({
tickets: result.data.tickets,
page: result.data.page,
pages: result.data.pages,
error: null,
loading: false
})
}).catch((result) => this.setState({
loading: false,
error: result.message
}));
}
onPageChange(event) {
this.setState({page: event.target.value}, () => this.getTickets());
const {
dispatch,
filters
} = this.props;
dispatch(searchFiltersActions.changePage({
...filters,
page: event.target.value
}));
}
getTicketListProps () {
const {page,pages,loading,tickets} = this.state;
const {
filters,
onChangeOrderBy,
userId,
ticketQueryListState
} = this.props;
const page = {
...ticketQueryListState,
...queryString.parse(window.location.search)
}.page*1;
return {
userId: this.props.userId,
userId: userId,
ticketPath: '/admin/panel/tickets/view-ticket/',
tickets,
page,
pages,
loading,
tickets: ticketQueryListState.tickets,
page: page,
pages: ticketQueryListState.pages,
loading: ticketQueryListState.loading,
type: 'secondary',
showDepartmentDropdown: false,
closedTicketsShown: false,
onPageChange:this.onPageChange.bind(this)
onPageChange:this.onPageChange.bind(this),
orderBy: filters.orderBy ? JSON.parse(filters.orderBy) : filters.orderBy,
showOrderArrows: true,
onChangeOrderBy: onChangeOrderBy,
};
}
@ -85,6 +72,8 @@ class TicketQueryList extends React.Component {
export default connect((store) => {
return {
userId: store.session.userId
userId: store.session.userId*1,
filters: store.searchFilters.listConfig.filters,
ticketQueryListState: store.searchFilters.ticketQueryListState
};
})(TicketQueryList);

View File

@ -9,6 +9,7 @@ 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';
@ -24,6 +25,7 @@ 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 = {
@ -64,55 +66,89 @@ class TicketViewer extends React.Component {
editTitle: false,
newTitle: this.props.ticket.title,
editTitleError: false,
editTitleLoading: false,
editStatus: false,
editTags: false,
editOwner: false,
editDepartment: false,
showTicketCommentErrorMessage: true
};
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;
const { editTitle, loading, edit, editId } = this.state;
const { closed, author, content, date, edited, file, events} = ticket;
const showResponseField = (!closed && (editable || !assignmentAllowed));
const lastComment = events.map(
(event, index) => {
return {...event, index}}
).filter(
(event) => event.type === "COMMENT"
).at(-1);
const eventsWithModifiedComments = events.map(
(event, index) => {
return {...event, isLastComment: lastComment && index === lastComment.index && event.type === "COMMENT"};
}
);
return (
<div className="ticket-viewer">
{this.state.editTitle ? this.renderEditableTitle() : this.renderTitleHeader()}
{this.props.editable ? this.renderEditableHeaders() : this.renderHeaders()}
{editTitle ? this.renderEditableTitle() : this.renderTitleHeader()}
{editable ? this.renderEditableHeaders() : this.renderHeaders()}
<div className="ticket-viewer__content">
<TicketEvent
loading={this.state.loading}
loading={loading}
type="COMMENT"
author={ticket.author}
content={this.props.userStaff ? MentionsParser.parse(ticket.content) : ticket.content}
userStaff={this.props.userStaff}
userId={this.props.userId}
date={ticket.date}
isLastComment={!events.filter(event => event.type === "COMMENT").length}
author={author}
isTicketClosed={closed}
content={userStaff ? MentionsParser.parse(content) : content}
userStaff={userStaff}
userId={userId}
date={date}
onEdit={this.onEdit.bind(this,0)}
edited={ticket.edited}
file={ticket.file}
edit={this.state.edit && this.state.editId == 0}
edited={edited}
file={file}
edit={edit && editId == 0}
onToggleEdit={this.onToggleEdit.bind(this, 0)}
allowAttachments={this.props.allowAttachments}
/>
allowAttachments={allowAttachments} />
</div>
<div className="ticket-viewer__comments">
{ticket.events && ticket.events.map(this.renderTicketEvent.bind(this))}
{eventsWithModifiedComments && eventsWithModifiedComments.map(this.renderTicketEvent.bind(this, closed))}
</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}
{showResponseField ? this.renderResponseField() : this.renderReopenCloseButtons()}
</div>
);
}
renderReopenCloseButtons() {
return(
<div className="ticket-viewer__reopen-close-buttons">
{this.renderReopenTicketButton()}
{this.showDeleteButton() ? this.renderDeleteTicketButton() : null}
</div>
)
}
renderTitleHeader() {
const {ticket, userStaff, userId} = this.props;
const {ticketNumber, title, author, editedTitle, language} = ticket;
return(
<div className="ticket-viewer__header row">
<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}/>
<Icon name={(language === 'en') ? 'us' : language} />
</span>
{((author.id == userId && author.staff == userStaff) || userStaff) ? this.renderEditTitleOption() : null}
{editedTitle ? this.renderEditedTitleText() : null }
@ -120,12 +156,246 @@ class TicketViewer extends React.Component {
)
}
renderEditedTitleText(){
renderEditableTitle(){
return(
<div className="ticket-viewer__edited-title-text"> {i18n('TITLE_EDITED')} </div>
<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
}
]
};
return (
<div className="ticket-viewer__headers">
<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="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="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 ? this.renderReopenTicketButton() : this.renderCloseTicketButton()}
</div>
);
}
renderReopenTicketButton() {
return (
<Button type='secondary' size="medium" onClick={this.onReopenClick.bind(this)}>
{i18n('RE_OPEN')}
</Button>
);
}
renderHeaders() {
const ticket = this.props.ticket;
return (
<div className="ticket-viewer__headers">
<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="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="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>
);
}
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')
);
return TAGS;
}
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 (
<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">
@ -134,176 +404,52 @@ class TicketViewer extends React.Component {
)
}
renderEditableTitle(){
renderEditOption(option) {
return(
<div className="ticket-viewer__header row">
<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>
<Button type='secondary' size="extra-small" onClick={this.changeTitle.bind(this)}>
{i18n('EDIT_TITLE')}
</Button>
</div>
<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>
)
}
renderEditableHeaders() {
const ticket = this.props.ticket;
const departments = this.getDepartmentsForTransfer();
const priorities = {
'low': 0,
'medium': 1,
'high': 2
};
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('TAGS')}</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>
<div className="col-md-4">{ticket.author.name}</div>
<div className="col-md-4">
<TagSelector
items={this.props.tags}
values={this.props.ticket.tags}
onRemoveClick={this.removeTag.bind(this)}
onTagSelected={this.addTag.bind(this)}
loading={this.state.tagSelectorLoading}/>
</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>
<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>
</div>
</div>
);
renderCancelButton(option) {
return <Button type='link' size="medium" onClick={() => this.setState({["edit"+option]: false})}>{i18n('CLOSE')}</Button>
}
renderHeaders() {
const ticket = this.props.ticket;
const priorities = {
'low': 'LOW',
'medium': 'MEDIUM',
'high': 'HIGH'
};
renderTicketEvent(isTicketClosed, ticketEventObject, index) {
const { userStaff, ticket, userId, allowAttachments } = this.props;
const { edit, editId } = this.state;
const { content, author, id} = ticketEventObject;
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('TAGS')}</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="col-md-4">{ticket.tags.length ? ticket.tags.map((tagName,index) => {
let tag = _.find(this.props.tags, {name:tagName});
return <Tag name={tag && tag.name} color={tag && tag.color} key={index} />
}) : i18n('NONE')}</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>
<div className="col-md-4">
{this.renderOwnerNode()}
</div>
<div className="col-md-4">
{i18n((this.props.ticket.closed) ? 'CLOSED' : 'OPENED')}
</div>
</div>
</div>
);
}
renderOwnerNode() {
let ownerNode = null;
if (this.props.assignmentAllowed) {
ownerNode = this.renderAssignStaffList();
} else {
ownerNode = (this.props.ticket.owner) ? this.props.ticket.owner.name : i18n('NONE')
if(userStaff && typeof content === 'string') {
ticketEventObject.content = MentionsParser.parse(content);
}
return ownerNode;
}
renderAssignStaffList() {
const items = this.getStaffAssignmentItems();
const ownerId = this.props.ticket.owner && this.props.ticket.owner.id;
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)}
/>
);
}
renderTicketEvent(options, index) {
if (this.props.userStaff && typeof options.content === 'string') {
options.content = MentionsParser.parse(options.content);
}
return (
<TicketEvent
{...options}
author={(!_.isEmpty(options.author)) ? options.author : this.props.ticket.author}
userStaff={this.props.userStaff}
userId={this.props.userId}
onEdit={this.onEdit.bind(this, options.id)}
edit={this.state.edit && this.state.editId == options.id}
onToggleEdit={this.onToggleEdit.bind(this, options.id)}
{...ticketEventObject}
isLastComment={ticketEventObject.isLastComment}
author={(!_.isEmpty(author)) ? author : ticket.author}
userStaff={userStaff}
isTicketClosed={isTicketClosed}
userId={userId}
onEdit={this.onEdit.bind(this, id)}
edit={edit && editId == id}
onToggleEdit={this.onToggleEdit.bind(this, id)}
key={index}
allowAttachments={this.props.allowAttachments}
/>
allowAttachments={allowAttachments} />
);
}
renderResponseField() {
const { allowAttachments } = this.props;
return (
<div className="ticket-viewer__response">
<Form {...this.getCommentFormProps()}>
@ -315,13 +461,16 @@ 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}
<div className="ticket-viewer__response-buttons">
<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}
<FormField name="content" validation="TEXT_AREA" required field="textarea" fieldProps={{allowImages: allowAttachments}} />
<div className="ticket-viewer__response-container">
<div className="ticket-viewer__response-buttons">
{allowAttachments ? <FormField name="file" field="file" /> : null}
<SubmitButton type="secondary">{i18n('RESPOND_TICKET')}</SubmitButton>
</div>
<div className="ticket-viewer__buttons-column">
<div className="ticket-viewer__buttons-row">
{this.renderCloseTicketButton()}
</div>
</div>
</div>
</div>
@ -331,10 +480,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
@ -347,7 +508,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>
);
}
@ -356,10 +517,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>
);
@ -369,11 +530,26 @@ class TicketViewer extends React.Component {
}
renderCommentError() {
const { showTicketCommentErrorMessage } = this.state;
return (
<Message className="ticket-viewer__message" type="error">{i18n('TICKET_COMMENT_ERROR')}</Message>
<Message
showMessage={showTicketCommentErrorMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showTicketCommentErrorMessage")}
className="ticket-viewer__message"
type="error">
{i18n('TICKET_COMMENT_ERROR')}
</Message>
);
}
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),
@ -397,11 +573,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) {
@ -415,27 +587,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));
}
@ -446,6 +630,9 @@ class TicketViewer extends React.Component {
}
changeTitle(){
this.setState({
editTitleLoading: true
});
API.call({
path: '/ticket/edit-title',
data: {
@ -455,18 +642,20 @@ class TicketViewer extends React.Component {
}).then(() => {
this.setState({
editTitle: false,
editTitleError: false
editTitleError: false,
editTitleLoading: false
});
this.onTicketModification();
}).catch((result) => {
this.setState({
editTitleError: i18n(result.message)
editTitleError: i18n(result.message),
editTitleLoading: false
})
});
}
reopenTicket() {
API.call({
return API.call({
path: '/ticket/re-open',
data: {
ticketNumber: this.props.ticket.ticketNumber
@ -475,7 +664,7 @@ class TicketViewer extends React.Component {
}
closeTicket() {
API.call({
return API.call({
path: '/ticket/close',
data: {
ticketNumber: this.props.ticket.ticketNumber
@ -484,41 +673,31 @@ class TicketViewer extends React.Component {
}
deleteTicket() {
API.call({
return API.call({
path: '/ticket/delete',
data: {
ticketNumber: this.props.ticket.ticketNumber
}
}).then((result) => {
this.onTicketModification(result);
history.push('/admin/panel/tickets/my-tickets/');
history.push(history.goBack());
});
}
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));
}
changePriority(index) {
const priorities = [
'low',
'medium',
'high'
];
API.call({
path: '/ticket/change-priority',
data: {
ticketNumber: this.props.ticket.ticketNumber,
priority: priorities[index]
}
}).then(this.onTicketModification.bind(this));
}).then((_.some(userDepartments, {id: departmentId}) || (userId === (ticket.author.id*1))) ? this.onTicketModification.bind(this) : history.goBack());
}
addTag(tag) {
@ -587,16 +766,16 @@ class TicketViewer extends React.Component {
})
}
onEdit(ticketeventid,{content}) {
onEdit(ticketeventid, {content}) {
this.setState({
loading: true
});
const data = {};
if(ticketeventid){
data.ticketEventId = ticketeventid
}else{
data.ticketNumber = this.props.ticket.ticketNumber
if(ticketeventid) {
data.ticketEventId = ticketeventid;
} else {
data.ticketNumber = this.props.ticket.ticketNumber;
}
API.call({
@ -605,7 +784,7 @@ class TicketViewer extends React.Component {
data,
TextEditor.getContentFormData(content)
)
}).then(this.onEditCommentSuccess.bind(this), this.onFailCommentFail.bind(this));
}).then(this.onEditCommentSuccess.bind(this), this.onEditCommentFail.bind(this));
}
onEditCommentSuccess() {
@ -619,10 +798,11 @@ class TicketViewer extends React.Component {
this.onTicketModification();
}
onFailCommentFail() {
onEditCommentFail() {
this.setState({
loading: false,
commentError: true
commentError: true,
showTicketCommentErrorMessage: true
});
}
@ -635,7 +815,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));
}
@ -654,66 +834,90 @@ class TicketViewer extends React.Component {
onCommentFail() {
this.setState({
loading: false,
commentError: true
commentError: true,
showTicketCommentErrorMessage: true
});
}
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(_.some(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) && _.some(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)){
return true;
}
const { ticket, userLevel, userId, userStaff } = this.props;
const { owner, author, closed } = ticket || {};
const { staff, id } = author || {};
if(!owner) {
if(userLevel === 3) return true;
if(userId == id*1) {
return (userStaff && staff && closed);
}
}
return false;
}
onCloseMessage(showMessage) {
this.setState({
[showMessage]: 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,
tags: store.config['tags']
userLevel: store.session.userLevel*1,
tags: store.config['tags'].map((tag) => {return {...tag, id: tag.id*1}})
};
})(TicketViewer);

View File

@ -1,7 +1,6 @@
@import "../scss/vars";
.ticket-viewer {
&__header {
background-color: $primary-blue;
border-top-right-radius: 4px;
@ -12,7 +11,7 @@
display: flex;
align-items:center;
justify-content:center;
position: relative;
&:hover {
.ticket-viewer__edit-title-icon {
color: $grey;
@ -20,56 +19,140 @@
}
}
&__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;
position: absolute;
left: 0;
bottom: 0;
}
&__edited-title-text {
font-style: italic;
font-size: 14px;
margin-left: 10px;
margin: 0 10px;
}
&__edit-title-icon {
position: absolute;
color: #414A59;
&__edit-icon {
right: 12px;
margin: 0 10px;
color: $light-grey;
&:hover {
cursor:pointer;
}
}
&___input-edit-title {
color: black;
align-items:center;
justify-content: center;
margin-bottom: 6px;
margin-right: 6px;
&__edit-title-icon {
color: $primary-blue;
right: 12px;
margin: 0 10px;
.input__text {
height: 25px;
}
&: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 {
@ -88,7 +171,15 @@
position: relative;
}
&__reopen-close-buttons {
width: 230px;
display: flex;
align-content: left;
justify-content: space-between;
}
&__response {
width: 100%;
margin-top: 20px;
position: relative;
@ -126,14 +217,55 @@
}
}
&-container {
display: flex;
position: relative;
flex-direction: row-reverse;
}
&-buttons {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
align-items: flex-end;
width: 50%;
min-width: 50%;
}
}
&__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;
}
}
}
}

View File

@ -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>
);

View File

@ -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;

View File

@ -166,7 +166,7 @@ class TopicViewer extends React.Component {
}
onDeleteClick() {
API.call({
return API.call({
path: '/article/delete-topic',
data: {
topicId: this.props.id

View File

@ -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('/');
}

View File

@ -32,7 +32,6 @@ 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';
@ -62,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}>
@ -99,7 +98,7 @@ 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} />
@ -113,7 +112,6 @@ 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} />

View File

@ -1,5 +1,5 @@
export default {
dispatch: stub(),
dispatch: stub().returns(new Promise(r => r())),
getState: stub().returns({
config: {},
session: {},

View File

@ -16,6 +16,9 @@ import Message from 'core-components/message';
import Widget from 'core-components/widget';
import WidgetTransition from 'core-components/widget-transition';
import Captcha from 'app/main/captcha';
const MAX_FREE_LOGIN_ATTEMPTS = 3;
class AdminLoginPage extends React.Component {
state = {
@ -24,11 +27,14 @@ class AdminLoginPage extends React.Component {
recoverFormErrors: {},
recoverSent: false,
loadingLogin: false,
loadingRecover: false
loadingRecover: false,
showRecoverSentMessage: true,
showEmailOrPassordErrorMessage: true
};
componentDidUpdate(prevProps) {
if (!prevProps.session.failed && this.props.session.failed) {
this.setState({showEmailOrPassordErrorMessage : true});
this.refs.loginForm.refs.password.focus();
}
}
@ -49,11 +55,33 @@ 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>
{this.props.session.loginAttempts > MAX_FREE_LOGIN_ATTEMPTS ? this.renderLoginCaptcha() : null}
<div className="admin-login-page__login-form-container__login-form__submit-button">
<SubmitButton>{i18n('LOG_IN')}</SubmitButton>
</div>
</Form>
</div>
{this.renderRecoverStatus()}
@ -66,46 +94,57 @@ class AdminLoginPage extends React.Component {
);
}
renderLoginCaptcha() {
return(
<div className={`main-home-page__${this.props.sitekey ? "captcha" : "no-captcha"}`}>
<Captcha ref="captcha" />
</div>
)
}
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>
);
}
renderRecoverStatus() {
let status = null;
const { showRecoverSentMessage, recoverSent } = this.state;
if (this.state.recoverSent) {
status = (
<Message className="admin-login-page__message" type="info" leftAligned>
{i18n('RECOVER_SENT')}
</Message>
);
}
return status;
return (
recoverSent ?
<Message
showMessage={showRecoverSentMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showRecoverSentMessage")}
className="admin-login-page__message"
type="info"
leftAligned>
{i18n('RECOVER_SENT')}
</Message> :
null
);
}
renderErrorStatus() {
let status = null;
if (this.props.session.failed) {
status = (
<Message className="admin-login-page__error" type="error">
{i18n('EMAIL_OR_PASSWORD')}
</Message>
);
}
return status;
return (
this.props.session.failed ?
<Message
showMessage={this.state.showEmailOrPassordErrorMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showEmailOrPassordErrorMessage")}
className="admin-login-page__error"
type="error">
{i18n('EMAIL_OR_PASSWORD')}
</Message> :
null
);
}
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 +153,14 @@ 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)
};
}
@ -184,7 +225,8 @@ class AdminLoginPage extends React.Component {
onRecoverPasswordSent() {
this.setState({
loadingRecover: false,
recoverSent: true
recoverSent: true,
showRecoverSentMessage: true
});
}
@ -198,10 +240,17 @@ class AdminLoginPage extends React.Component {
this.refs.recoverForm.refs.email.focus();
}.bind(this));
}
onCloseMessage(showMessage) {
this.setState({
[showMessage]: false
});
}
}
export default connect((store) => {
return {
session: store.session
session: store.session,
sitekey: store.config.reCaptchaKey
};
})(AdminLoginPage);

View File

@ -19,12 +19,22 @@
margin-bottom: 30px;
}
&__login-form {
&__login-form-container {
margin: 0 auto;
display: inline-block;
&__login-form__fields {
padding: 10px 0;
}
}
&__error {
margin-top: 30px;
}
&__captcha {
margin: 10px auto 20px;
height: 78px;
width: 304px;
}
}

View File

@ -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>

View File

@ -4,4 +4,10 @@
&__header {
margin-bottom: 20px;
}
@media screen and (max-width: 415px) {
.admin-panel-layout__content__widget {
padding: 20px 5px;
}
}
}

View File

@ -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 + this.props.location.search;
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() {
@ -93,11 +113,11 @@ class AdminPanelMenu extends React.Component {
getCustomlists() {
if(window.customTicketList){
return window.customTicketList.map((item, index) => {
return {
name: item.title,
path: '/admin/panel/tickets/search-tickets?custom=' + index,
level: 1
}
return {
name: item.title,
path: `/admin/panel/tickets/search-tickets?custom=${index}&page=${INITIAL_PAGE}&useInitialValues=true`,
level: 1,
}
})
} else {
return [];
@ -114,15 +134,15 @@ class AdminPanelMenu extends React.Component {
icon: 'tachometer',
level: 1,
items: this.getItemsByFilteredByLevel([
/*{
name: i18n('STATISTICS'),
path: '/admin/panel/stats',
level: 1
},*/
{
name: i18n('LAST_ACTIVITY'),
path: '/admin/panel/activity',
level: 1
},
{
name: i18n('STATISTICS'),
path: '/admin/panel/stats',
level: 1
}
])
},
@ -143,9 +163,9 @@ 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'),
@ -155,7 +175,7 @@ class AdminPanelMenu extends React.Component {
...customLists
])
},
this.props.config['user-system-enabled'] ? {
{
groupName: i18n('USERS'),
path: '/admin/panel/users',
icon: 'user',
@ -177,7 +197,7 @@ class AdminPanelMenu extends React.Component {
level: 1
}
])
} : null,
},
{
groupName: i18n('ARTICLES'),
path: '/admin/panel/articles',
@ -249,6 +269,7 @@ class AdminPanelMenu extends React.Component {
export default connect((store) => {
return {
level: store.session.userLevel,
config: store.config
config: store.config,
searchFilters: store.searchFilters,
};
})(AdminPanelMenu);

View File

@ -3,4 +3,11 @@
&__list {
padding: 0 50px;
}
}
@media screen and (max-width: 415px) {
.admin-panel-list-articles__list {
padding: 0;
}
}
}

View File

@ -66,12 +66,12 @@ 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}/>
@ -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

View File

@ -5,7 +5,11 @@
}
&__edit-buttons {
text-align: left;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 200px;
margin-bottom: 20px;
}

View File

@ -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';

View File

@ -1,25 +1,182 @@
import React from 'react';
import { connect } from 'react-redux';
import API from 'lib-app/api-call';
import i18n from 'lib-app/i18n';
// import Stats from 'app-components/stats';
import statsUtils from 'lib-app/stats-utils';
import Header from 'core-components/header';
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: statsUtils.getInitialDateRange(),
departments: [],
owners: [],
tags: []
},
ticketData: {}
};
componentDidMount() {
statsUtils.retrieveStats({
rawForm: this.state.rawForm,
tags: this.props.tags
}).then(({data}) => {
this.setState({ticketData: data, loading: false});
}).catch((error) => {
if (showLogs) console.error('ERROR: ', error);
});
}
render() {
// return (
// <div className="admin-panel-stats">
// <Header title={i18n('STATISTICS')} description={i18n('STATISTICS_DESCRIPTION')}/>
// <Stats type="general"/>
// </div>
// );
const { loading, rawForm, ticketData } = this.state;
return (
<div className="admin-panel-stats">
<Header title={i18n('STATISTICS')} description={i18n('STATISTICS_DESCRIPTION')}/>
<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> :
statsUtils.renderStatistics({showStatCards: true, showStatsByHours: true, showStatsByDays: true, ticketData})
}
</div>
)
}
clearFormValues(event) {
event.preventDefault();
this.setState({
rawForm: {
dateRange: statsUtils.getInitialDateRange(),
departments: [],
owners: [],
tags: []
}
});
}
getTagItems() {
return this.props.tags.map((tag) => {
return {
id: JSON.parse(tag.id),
name: tag.name,
color : tag.color
}
});
}
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'),
}
});
}
onFormChange(newFormState) {
this.setState({rawForm: newFormState});
}
onFormSubmit() {
statsUtils.retrieveStats({
rawForm: this.state.rawForm,
tags: this.props.tags
}).then(({data}) => {
this.setState({ticketData: data, loading: false});
}).catch((error) => {
if (showLogs) console.error('ERROR: ', error);
});
}
}
export default AdminPanelStats;
export default connect((store) => {
return {
tags: store.config.tags,
departments: store.config.departments,
staffList: store.adminData.staffMembers
};
})(AdminPanelStats);

View File

@ -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;
}
}

View File

@ -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,11 @@ class AdminPanelAdvancedSettings extends React.Component {
messageTitle: null,
messageType: '',
messageContent: '',
keyName: '',
keyCode: '',
selectedAPIKey: -1,
APIKeys: []
APIKeys: [],
error: '',
showMessage: true,
showAPIKeyMessage: true
};
componentDidMount() {
@ -36,24 +37,33 @@ class AdminPanelAdvancedSettings extends React.Component {
}
render() {
const { config } = this.props;
const { messageType, error, selectedAPIKey, showAPIKeyMessage } = 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 +75,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>
@ -80,12 +90,21 @@ class AdminPanelAdvancedSettings extends React.Component {
<span className="separator" />
</div>
<div className="col-md-12 admin-panel-advanced-settings__api-keys">
<div className="col-md-12 admin-panel-advanced-settings__api-keys-title">{i18n('REGISTRATION_API_KEYS')}</div>
<div className="col-md-12 admin-panel-advanced-settings__api-keys-title">{i18n('API_KEYS')}</div>
<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
showMessage={showAPIKeyMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showAPIKeyMessage")}
type="error">
{i18n(error)}
</Message> :
((selectedAPIKey === -1) ? this.renderNoKey() : this.renderKey())
}
</div>
</div>
</div>
@ -94,8 +113,17 @@ class AdminPanelAdvancedSettings extends React.Component {
}
renderMessage() {
const { messageType, messageTitle, messageContent, showMessage } = this.state;
return (
<Message type={this.state.messageType} title={this.state.messageTitle}>{this.state.messageContent}</Message>
<Message
showMessage={showMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showMessage")}
className="admin-panel-advanced-settings__message"
type={messageType}
title={messageTitle}>
{messageContent}
</Message>
);
}
@ -108,14 +136,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>
@ -125,7 +170,7 @@ class AdminPanelAdvancedSettings extends React.Component {
getListingProps() {
return {
title: i18n('REGISTRATION_API_KEYS'),
title: i18n('API_KEYS'),
enableAddNew: true,
items: this.state.APIKeys.map((item) => {
return {
@ -134,7 +179,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 +187,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, type: 'REGISTRATION'}
data: {
name,
canCreateUsers: userPermission*1,
canCreateTickets: createTicketPermission*1,
canCheckTickets: checkTicketPermission*1,
shouldReturnTicketNumber: ticketNumberPermission*1
}
}).then(this.getAllKeys.bind(this));
}
@ -161,15 +239,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));
});
@ -177,22 +260,25 @@ class AdminPanelAdvancedSettings extends React.Component {
onRetrieveSuccess(result) {
this.setState({
APIKeys: result.data.filter(key => key['type'] === 'REGISTRATION'),
selectedAPIKey: -1
APIKeys: result.data,
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,26 +286,30 @@ 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')
showMessage: true,
messageContent: config['mandatory-login'] ? i18n('MANDATORY_LOGIN_DISABLED') : i18n('MANDATORY_LOGIN_ENABLED')
});
this.props.dispatch(ConfigActions.updateData());
}).catch(() => this.setState({messageType: 'error', messageTitle: null, messageContent: i18n('ERROR_UPDATING_SETTINGS')}));
dispatch(ConfigActions.updateData());
}).catch(() => this.setState({messageType: 'error', showMessage: true, 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
}
}).then(() => {
this.setState({
messageType: 'success',
showMessage: true,
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());
}).catch(() => this.setState({messageType: 'error', messageTitle: null, messageContent: i18n('ERROR_UPDATING_SETTINGS')}));
dispatch(ConfigActions.updateData());
}).catch(() => this.setState({messageType: 'error', showMessage: true, messageTitle: null, messageContent: i18n('ERROR_UPDATING_SETTINGS')}));
}
onImportCSV(event) {
@ -227,7 +317,7 @@ class AdminPanelAdvancedSettings extends React.Component {
}
onAreYouSureCSVOk(file, password) {
API.call({
return API.call({
path: '/system/csv-import',
dataAsForm: true,
data: {
@ -236,7 +326,8 @@ class AdminPanelAdvancedSettings extends React.Component {
}
})
.then((result) => this.setState({
messageType: 'success',
messageType: 'success',
showMessage: true,
messageTitle: i18n('SUCCESS_IMPORTING_CSV_DESCRIPTION'),
messageContent: (result.data.length) ? (
<div>
@ -247,7 +338,7 @@ class AdminPanelAdvancedSettings extends React.Component {
</div>
) : null
}))
.catch(() => this.setState({messageType: 'error', messageTitle: null, messageContent: i18n('INVALID_FILE')}));
.catch(() => this.setState({messageType: 'error', showMessage: true, messageTitle: null, messageContent: i18n('INVALID_FILE')}));
}
onBackupDatabase() {
@ -270,17 +361,22 @@ class AdminPanelAdvancedSettings extends React.Component {
}
onAreYouSureDeleteAllUsersOk(password) {
API.call({
return API.call({
path: '/system/delete-all-users',
data: {
password: password
}
}).then(() => this.setState({messageType: 'success', messageTitle: null, messageContent: i18n('SUCCESS_DELETING_ALL_USERS')}
)).catch(() => this.setState({messageType: 'error', messageTitle: null, messageContent: i18n('ERROR_DELETING_ALL_USERS')}));
}).then(() => this.setState({messageType: 'success', showMessage: true, messageTitle: null, messageContent: i18n('SUCCESS_DELETING_ALL_USERS')}
)).catch(() => this.setState({messageType: 'error', showMessage: true, messageTitle: null, messageContent: i18n('ERROR_DELETING_ALL_USERS')}));
}
onCloseMessage(showMessage) {
this.setState({
[showMessage]: false
});
}
}
export default connect((store) => {
return {
config: store.config

View File

@ -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;
}
}
}
}
}

View File

@ -12,7 +12,8 @@ import ColorSelector from 'core-components/color-selector';
class AdminPanelCustomTagsModal extends React.Component {
static contextTypes = {
closeModal: React.PropTypes.func
closeModal: React.PropTypes.func,
createTag: React.PropTypes.bool
};
static propTypes = {
@ -27,57 +28,50 @@ class AdminPanelCustomTagsModal extends React.Component {
render() {
return (
this.props.createTag ? this.renderCreateTagContent() : this.renderEditTagContent()
this.renderTagContentPopUp(this.props.createTag)
);
}
renderEditTagContent() {
return (
<div className='admin-panel-custom-tags-modal'>
<Header title={i18n('EDIT_CUSTOM_TAG')} description={i18n('DESCRIPTION_EDIT_CUSTOM_TAG')} />
<Form
values={this.state.form}
onChange={this.onFormChange.bind(this)}
onSubmit={this.onSubmitEditTag.bind(this)}
errors={this.state.errors}
onValidateErrors={errors => this.setState({errors})}
loading={this.state.loading}>
<FormField name="name" label={i18n('NAME')} fieldProps={{size: 'large'}}/>
<FormField name="color" label={i18n('COLOR')} decorator={ColorSelector} />
<div className='admin-panel-custom-tags-modal__actions'>
<SubmitButton type="secondary" size="small">
{i18n('SAVE')}
</SubmitButton>
<Button onClick={this.onDiscardClick.bind(this)} size="small">
{i18n('CANCEL')}
</Button>
</div>
</Form>
</div>
);
}
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);
}
renderCreateTagContent() {
return (
<div className='admin-panel-custom-tags-modal'>
<Header title={i18n('ADD_CUSTOM_TAG')} description={i18n('DESCRIPTION_ADD_CUSTOM_TAG')} />
<Header title={title} description={description} />
<Form
values={this.state.form}
values={form}
onChange={this.onFormChange.bind(this)}
onSubmit={this.onSubmitNewTag.bind(this)}
errors={this.state.errors}
onSubmit={submitFunction}
errors={errors}
onValidateErrors={errors => this.setState({errors})}
loading={this.state.loading}>
<FormField name="name" label={i18n('NAME')} fieldProps={{size: 'large'}} required />
<FormField name="color" label={i18n('COLOR')} decorator={ColorSelector} />
<div className='admin-panel-custom-tags-modal__actions'>
<SubmitButton type="secondary" size="small">
{i18n('SAVE')}
</SubmitButton>
<Button onClick={this.onDiscardClick.bind(this)} size="small">
{i18n('CANCEL')}
</Button>
</div>
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>
);
@ -103,10 +97,7 @@ class AdminPanelCustomTagsModal extends React.Component {
}
}).then(() => {
this.context.closeModal();
this.setState({
loading: false,
errors: {}
});
if(this.props.onTagChange) {
this.props.onTagChange();
}
@ -121,7 +112,7 @@ class AdminPanelCustomTagsModal extends React.Component {
});
}
onSubmitNewTag(form) {
this.setState({
loading: true
@ -135,10 +126,6 @@ class AdminPanelCustomTagsModal extends React.Component {
}
}).then(() => {
this.context.closeModal();
this.setState({
loading: false,
errors: {}
});
if(this.props.onTagCreated) {
this.props.onTagCreated();

View File

@ -1,7 +1,7 @@
import React from 'react';
import {connect} from 'react-redux';
import AdminPanelCustomTagsModal from 'app/admin/panel/tickets/admin-panel-custom-tags-modal';
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';
@ -44,7 +44,7 @@ class AdminPanelCustomTags extends React.Component {
<div className="admin-panel-custom-tags__content">
<div>
<Button onClick={this.openTagModal.bind(this)} type="secondary">
{i18n('ADD_CUSTOM_TAG')}<Icon className="admin-panel-custom-tags__add-button-icon" name="plus"/>
<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">
@ -57,7 +57,14 @@ class AdminPanelCustomTags extends React.Component {
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 />
<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>
)
}
@ -70,17 +77,17 @@ class AdminPanelCustomTags extends React.Component {
openEditTagModal(tagId,tagName,tagColor, event) {
ModalContainer.openModal(
<AdminPanelCustomTagsModal defaultValues={{name: tagName , color: tagColor}} id={tagId} onTagChange={this.retrieveCustomTags.bind(this)}/>
<AdminPanelCustomTagsModal defaultValues={{name: tagName , color: tagColor}} id={tagId} onTagChange={this.retrieveCustomTags.bind(this)} />
);
}
onDeleteClick(tagId, event) {
event.preventDefault();
AreYouSure.openModal(i18n('WILL_DELETE_CUSTOM_RESPONSE'), this.deleteCustomTag.bind(this, tagId));
AreYouSure.openModal(i18n('WILL_DELETE_CUSTOM_TAG'), this.deleteCustomTag.bind(this, tagId));
}
deleteCustomTag(tagId) {
API.call({
return API.call({
path: '/ticket/delete-tag',
data: {
tagId,
@ -97,6 +104,6 @@ class AdminPanelCustomTags extends React.Component {
export default connect((store) => {
return {
tags: store.config['tags']
tags: store.config['tags'].map((tag) => {return {...tag, id: tag.id*1}})
};
})(AdminPanelCustomTags);

View File

@ -77,10 +77,10 @@ class AdminPanelEmailSettings extends React.Component {
return (
<div>
{this.renderEmailSettings()}
<Header title={i18n('EMAIL_TEMPLATES')} description={i18n('EMAIL_TEMPLATES_DESCRIPTION')}/>
<Header title={i18n('EMAIL_TEMPLATES')} description={i18n('EMAIL_TEMPLATES_DESCRIPTION')} />
<div className="row">
<div className="col-md-3">
<Listing {...this.getListingProps()}/>
<Listing {...this.getListingProps()} />
</div>
{(this.state.selectedIndex !== -1) ? this.renderForm() : null}
</div>
@ -91,7 +91,7 @@ class AdminPanelEmailSettings extends React.Component {
renderLoading() {
return (
<div className="admin-panel-email-settings__loading">
<Loading backgrounded size="large"/>
<Loading backgrounded size="large" />
</div>
);
}
@ -99,27 +99,31 @@ class AdminPanelEmailSettings extends React.Component {
renderEmailSettings() {
return (
<div>
<Header title={i18n('EMAIL_SETTINGS')} description={i18n('EMAIL_SETTINGS_DESCRIPTION')}/>
<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}>
<FormField 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 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 values={{headerImage: this.state.headerImage}}
<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'}}/>
fieldProps={{size: 'large'}} />
<SubmitButton className="admin-panel-email-settings__image-header-submit" type="secondary"
size="small">{i18n('SAVE')}</SubmitButton>
</div>
@ -127,39 +131,45 @@ class AdminPanelEmailSettings extends React.Component {
<div className="admin-panel-email-settings__servers">
<div className="admin-panel-email-settings__box">
<Header title={i18n('SMTP_SERVER')} description={i18n('SMTP_SERVER_DESCRIPTION')}/>
<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'}}/>
<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 className="admin-panel-email-settings__submit" type="secondary"
size="small">{i18n('SAVE')}</SubmitButton>
<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')}/>
<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'}}/>
<FormField name="imap-token" label={i18n('IMAP_TOKEN')} infoMessage={i18n('IMAP_TOKEN_DESCRIPTION')} fieldProps={{size: 'large', icon: 'refresh', onIconClick: this.generateImapToken.bind(this)}}/>
<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 className="admin-panel-email-settings__submit" type="secondary"
size="small">{i18n('SAVE')}</SubmitButton>
<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">
<Message showCloseButton={false} className="admin-panel-email-settings__imap-message" type="info">
{i18n('IMAP_POLLING_DESCRIPTION', {url: `${apiRoot}/system/email-polling`})}
</Message>
</div>
@ -169,48 +179,57 @@ class AdminPanelEmailSettings extends React.Component {
}
renderForm() {
const {
form,
language,
selectedIndex,
edited
} = this.state;
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)}
<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'}}/>
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'}}/>
{(this.state.form.text2) ?
fieldProps={{className: 'admin-panel-email-settings__text-area'}} />
{(form.text2 || 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}
{(this.state.form.text3) ?
fieldProps={{className: 'admin-panel-email-settings__text-area'}} /> : null}
{(form.text3 || 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}
fieldProps={{className: 'admin-panel-email-settings__text-area'}} /> : null}
<div className="admin-panel-email-settings__actions">
<div className="admin-panel-email-settings__save-button">
<SubmitButton key="submit-email-template" type="secondary" size="small" onClick={e => {
e.preventDefault();
this.onFormSubmit(this.state.form);
}}>{i18n('SAVE')}</SubmitButton>
</div>
<div className="admin-panel-email-settings__optional-buttons">
{(this.state.edited) ? this.renderDiscardButton() : null}
<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>
@ -438,7 +457,7 @@ class AdminPanelEmailSettings extends React.Component {
recoverEmailTemplate() {
const {selectedIndex, language, templates} = this.state;
API.call({
return API.call({
path: '/system/recover-mail-template',
data: {
template: templates[selectedIndex],
@ -454,7 +473,7 @@ class AdminPanelEmailSettings extends React.Component {
loadingForm: true,
});
API.call({
return API.call({
path: '/system/get-mail-template',
data: {template: this.state.templates[index], language}
}).then((result) => this.setState({
@ -482,20 +501,20 @@ class AdminPanelEmailSettings extends React.Component {
path: '/system/get-settings',
data: {allSettings: 1}
}).then(result => this.setState({
headerImage: result.data['mail-template-header-image'],
headerImage: result.data['mail-template-header-image'] || '',
emailForm: {
['server-email']: result.data['server-email'],
['server-email']: result.data['server-email'] || '',
},
smtpForm: {
['smtp-host']: result.data['smtp-host'],
['smtp-user']: result.data['smtp-user'],
['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-host']: result.data['imap-host'] || '',
['imap-user']: result.data['imap-user'] || '',
['imap-pass']: 'HIDDEN',
['imap-token']: result.data['imap-token'],
['imap-token']: result.data['imap-token'] || '',
},
}));
}

View File

@ -1,6 +1,14 @@
@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%;
@ -9,12 +17,12 @@
&__save-button {
display: inline-block;
float: left;
float: right;
}
&__optional-buttons {
display: inline-block;
float: right;
float: left;
}
&__discard-button {
@ -23,11 +31,11 @@
&__recover-button {
display: inline-block;
margin-left: 10px;
margin: 0 10px;
}
&__image-container,
&__email-form {
&__email-container {
background-color: $very-light-grey;
display: flex;
justify-content: space-between;
@ -62,3 +70,24 @@
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%;
}
}
}

View File

@ -26,7 +26,8 @@ class AdminPanelSystemPreferences extends React.Component {
message: null,
values: {
maintenance: false,
}
},
showMessage: true
};
componentDidMount() {
@ -52,13 +53,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">
@ -99,12 +99,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>
@ -114,11 +114,29 @@ class AdminPanelSystemPreferences extends React.Component {
}
renderMessage() {
switch (this.state.message) {
const { message, showMessage } = this.state;
switch (message) {
case 'success':
return <Message className="admin-panel-system-preferences__message" type="success">{i18n('SETTINGS_UPDATED')}</Message>;
return (
<Message
showMessage={showMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showMessage")}
className="admin-panel-system-preferences__message"
type="success">
{i18n('SETTINGS_UPDATED')}
</Message>
);
case 'fail':
return <Message className="admin-panel-system-preferences__message" type="error">{i18n('ERROR_UPDATING_SETTINGS')}</Message>;
return (
<Message
showMessage={showMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showMessage")}
className="admin-panel-system-preferences__message"
type="error">
{i18n('ERROR_UPDATING_SETTINGS')}
</Message>
);
default:
return null;
}
@ -127,13 +145,14 @@ class AdminPanelSystemPreferences extends React.Component {
onFormChange(form) {
const { language, supportedLanguages, allowedLanguages } = form;
const languageIndex = _.indexOf(languageKeys, language);
const updatedSupportedLanguages = _.filter(supportedLanguages, (supportedIndex) => _.includes(allowedLanguages, supportedIndex));
this.setState({
values: _.extend({}, form, {
language: _.includes(supportedLanguages, languageIndex) ? language : languageKeys[supportedLanguages[0]],
supportedLanguages: _.filter(supportedLanguages, (supportedIndex) => _.includes(allowedLanguages, supportedIndex))
}),
message: null
values: _.extend({}, form, {
language: _.includes(updatedSupportedLanguages, languageIndex) ? language : languageKeys[updatedSupportedLanguages[0]],
supportedLanguages: updatedSupportedLanguages
}),
message: null
});
}
@ -149,21 +168,21 @@ class AdminPanelSystemPreferences extends React.Component {
'url': form['url'],
'title': form['title'],
'layout': form['layout'] ? 'full-width' : 'boxed',
'time-zone': form['time-zone'],
'maintenance-mode': form['maintenance-mode'] * 1,
'allow-attachments': form['allow-attachments'] * 1,
'max-size': form['max-size'],
'allowedLanguages': JSON.stringify(form.allowedLanguages.map(index => languageKeys[index])),
'supportedLanguages': JSON.stringify(form.supportedLanguages.map(index => languageKeys[index]))
}
}).then(this.onSubmitSuccess.bind(this)).catch(() => this.setState({loading: false, message: 'fail'}));
}).then(this.onSubmitSuccess.bind(this)).catch(() => this.setState({loading: false, message: 'fail', showMessage: true}));
}
onSubmitSuccess() {
this.recoverSettings();
this.setState({
message: 'success',
loading: false
loading: false,
showMessage: true
});
}
@ -190,7 +209,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'],
'maintenance-mode': !!(result.data['maintenance-mode'] * 1),
'allow-attachments': !!(result.data['allow-attachments'] * 1),
'max-size': result.data['max-size'],
@ -204,7 +222,8 @@ class AdminPanelSystemPreferences extends React.Component {
onRecoverSettingsFail() {
this.setState({
message: 'error'
message: 'error',
showMessage: true
});
}
@ -213,6 +232,12 @@ class AdminPanelSystemPreferences extends React.Component {
this.setState({loading: true});
this.recoverSettings();
}
onCloseMessage(showMessage) {
this.setState({
[showMessage]: false
});
}
}
export default AdminPanelSystemPreferences;

View File

@ -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;
}
}
}
}
}

View File

@ -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,120 @@ class AdminPanelDepartments extends React.Component {
formLoading: false,
selectedIndex: -1,
selectedDropDownIndex: 0,
edited: false,
editedAddDepartmentForm: false,
editedDefaultDepartmentForm: false,
errorMessage: null,
showErrorMessage: true,
showSuccessMessage: true,
showDefaultDepartmentErrorMessage: true,
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, showErrorMessage } = 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 showMessage={showErrorMessage} onCloseMessage={this.onCloseMessage.bind(this, "showErrorMessage")} 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, showSuccessMessage, showDefaultDepartmentErrorMessage } = this.state;
return (
<div className="admin-panel-departments__default-departments-container">
<span className="separator" />
{(defaultDepartmentError !== null) ?
((!defaultDepartmentError) ?
<Message showMessage={showSuccessMessage} onCloseMessage={this.onCloseMessage.bind(this, "showSuccessMessage")} type="success">
{i18n('SETTINGS_UPDATED')}
</Message> :
<Message showMessage={showDefaultDepartmentErrorMessage} onCloseMessage={this.onCloseMessage.bind(this, "showDefaultDepartmentErrorMessage")} 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 +172,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 +212,87 @@ 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
};
}
onCloseMessage(showMessage) {
this.setState({
[showMessage]: false
});
}
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, showSuccessMessage: true});
}).catch(result => this.setState({formLoading: false, defaultDepartmentError: result.message, showDefaultDepartmentErrorMessage: true}));
}
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, showErrorMessage: true, defaultDepartmentError: null}));
} else {
API.call({
path: '/system/add-department',
@ -167,9 +301,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(result => {
this.onItemChange.bind(this, -1);
this.setState({formLoading: false, errorMessage: result.message, showErrorMessage: true, defaultDepartmentError: null});
});
}
}
@ -186,49 +324,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, showErrorMessage: true, 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 +383,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);

View File

@ -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;
}
}
}
}
}

View File

@ -14,7 +14,6 @@ import ModalContainer from 'app-components/modal-container';
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';
@ -48,7 +47,7 @@ class AdminPanelStaffMembers extends React.Component {
<div className="admin-panel-staff-members__wrapper">
<DepartmentDropdown {...this.getDepartmentDropdownProps()} className="admin-panel-staff-members__dropdown" />
<Button onClick={this.onInviteStaff.bind(this)} size="medium" type="secondary" className="admin-panel-staff-members__button">
<Icon name="user-plus" className=""/> {i18n('INVITE_STAFF')}
<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})} />}

View File

@ -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;
}
}
}
}

View File

@ -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() {

View File

@ -51,12 +51,14 @@ class InviteStaffModal extends React.Component {
</div>
</div>
</div>
<SubmitButton type="secondary" size="small">
{i18n('SAVE')}
</SubmitButton>
<Button type="clean" onClick={this.onCancelClick.bind(this)}>
{i18n('CANCEL')}
</Button>
<div className="invite-staff-modal__buttons-container">
<Button type="clean" onClick={this.onCancelClick.bind(this)}>
{i18n('CANCEL')}
</Button>
<SubmitButton type="secondary" size="small">
{i18n('SAVE')}
</SubmitButton>
</div>
</Form>
</div>
);

View File

@ -20,4 +20,12 @@
font-size: $font-size--md;
text-align: center;
}
}
&__buttons-container {
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
width: 50%;
}
}

View File

@ -1,13 +1,15 @@
import React from 'react';
import _ from 'lodash';
import classNames from 'classnames';
import {connect} from 'react-redux';
import AdminDataActions from 'actions/admin-data-actions';
import i18n from 'lib-app/i18n';
import API from 'lib-app/api-call';
import SessionStore from 'lib-app/session-store';
import TicketList from 'app-components/ticket-list';
import AreYouSure from 'app-components/are-you-sure';
// import Stats from 'app-components/stats';
import Form from 'core-components/form';
import FormField from 'core-components/form-field';
@ -16,62 +18,105 @@ import Message from 'core-components/message';
import Button from 'core-components/button';
import Icon from 'core-components/icon';
import Loading from 'core-components/loading';
import statsUtils from 'lib-app/stats-utils';
const INITIAL_API_VALUE = {
page: 1,
closed: 0,
departments: undefined,
};
class StaffEditor extends React.Component {
static propTypes = {
myAccount: React.PropTypes.bool,
staffId: React.PropTypes.number,
userId: React.PropTypes.number,
email: React.PropTypes.string.isRequired,
name: React.PropTypes.string.isRequired,
profilePic: React.PropTypes.string.isRequired,
level: React.PropTypes.number.isRequired,
tickets: React.PropTypes.array.isRequired,
departments: React.PropTypes.array.isRequired,
sendEmailOnNewTicket: React.PropTypes.bool,
onChange: React.PropTypes.func,
onDelete: React.PropTypes.func
};
static defaultProps = {
tickets: []
};
state = {
email: this.props.email,
level: this.props.level - 1,
message: null,
loadingPicture: false,
tickets: [],
page: 1,
pages: 0,
department: undefined,
departments: this.getUserDepartments(),
sendEmailOnNewTicket: this.props.sendEmailOnNewTicket
closedTicketsShown: false,
sendEmailOnNewTicket: this.props.sendEmailOnNewTicket,
loadingReInviteStaff: false,
reInviteStaff: "",
loadingStats: true,
showMessage: true,
showReInviteStaffMessage: true,
rawForm: {
dateRange: statsUtils.getInitialDateRange(),
departments: [],
owners: [],
tags: []
},
ticketData: {}
};
componentDidMount() {
const departmentsAssigned = SessionStore.getDepartments().filter((_department, index) => this.state.departments.includes(index));
const departmentsAssignedId = departmentsAssigned.map(department => department.id);
this.retrieveStaffMembers();
this.retrieveTicketsAssigned(INITIAL_API_VALUE);
statsUtils.retrieveStats({
rawForm: this.state.rawForm,
departments: departmentsAssignedId
}).then(({data}) => {
this.setState({
ticketData: data,
loadingStats: false
});
}).catch((error) => {
if (showLogs) console.error('ERROR: ', error);
});
}
render() {
const { name, level, profilePic, myAccount, staffId, staffList, userId } = this.props;
const { message, tickets, loadingPicture, email } = this.state;
const myData = _.filter(staffList, {id: `${staffId}`})[0];
return (
<div className="staff-editor">
{(this.state.message) ? this.renderMessage() : null}
{message ? this.renderMessage() : null}
<div className="row">
<div className="col-md-4">
<div className="staff-editor__card">
<div className="staff-editor__card-data">
<div className="staff-editor__card-name">
{this.props.name}
{name}
</div>
<div className="staff-editor__card-info">
<div className="staff-editor__card-badge">
<span className="staff-editor__card-badge-blue">
{this.props.level}
{level}
</span>
<span className="staff-editor__card-badge-text">{i18n('STAFF_LEVEL')}</span>
</div>
<div className="staff-editor__card-badge">
<span className="staff-editor__card-badge-green">
{_.filter(this.props.tickets, {closed: false}).length}
{myData.assignedTickets}
</span>
<span className="staff-editor__card-badge-text">{i18n('ASSIGNED')}</span>
</div>
<div className="staff-editor__card-badge">
<span className="staff-editor__card-badge-red">
{_.filter(this.props.tickets, {closed: true}).length}
{myData.closedTickets}
</span>
<span className="staff-editor__card-badge-text">{i18n('CLOSED')}</span>
</div>
@ -79,25 +124,25 @@ class StaffEditor extends React.Component {
</div>
<label className={this.getPictureWrapperClass()}>
<div className="staff-editor__card-pic-background"></div>
<img className="staff-editor__card-pic" src={(this.props.profilePic) ? API.getFileLink(this.props.profilePic) : (API.getURL() + '/images/profile.png')} />
{(this.state.loadingPicture) ? <Loading className="staff-editor__card-pic-loading" size="large"/> : <Icon className="staff-editor__card-pic-icon" name="upload" size="4x"/>}
<input className="staff-editor__image-uploader" type="file" multiple={false} accept="image/x-png,image/gif,image/jpeg" onChange={this.onProfilePicChange.bind(this)}/>
<img className="staff-editor__card-pic" src={(profilePic) ? API.getFileLink(profilePic) : (API.getURL() + '/images/profile.png')} />
{(loadingPicture) ? <Loading className="staff-editor__card-pic-loading" size="large" /> : <Icon className="staff-editor__card-pic-icon" name="upload" size="4x" />}
<input className="staff-editor__image-uploader" type="file" multiple={false} accept="image/x-png,image/gif,image/jpeg" onChange={this.onProfilePicChange.bind(this)} />
</label>
</div>
</div>
<div className="col-md-8">
<div className="staff-editor__form">
<Form className="staff-editor__update-email" values={{email: this.state.email}} onChange={form => this.setState({email: form.email})} onSubmit={this.onSubmit.bind(this, 'EMAIL')}>
<FormField name="email" validation="EMAIL" required label={i18n('EMAIL')} fieldProps={{size: 'large'}}/>
<Form className="staff-editor__update-email" values={{email: email}} onChange={form => this.setState({email: form.email})} onSubmit={this.onSubmit.bind(this, 'EMAIL')}>
<FormField name="email" validation="EMAIL" required label={i18n('EMAIL')} fieldProps={{size: 'large'}} />
<SubmitButton size="medium" className="staff-editor__submit-button">{i18n('UPDATE_EMAIL')}</SubmitButton>
</Form>
<span className="separator staff-editor__separator" />
<Form className="staff-editor__update-password" onSubmit={this.onSubmit.bind(this, 'PASSWORD')}>
<FormField name="password" validation="PASSWORD" required label={i18n('PASSWORD')} fieldProps={{size: 'large', password: true}}/>
<FormField name="rpassword" validation="REPEAT_PASSWORD" required label={i18n('REPEAT_PASSWORD')} fieldProps={{size: 'large', password: true}}/>
<FormField name="password" validation="PASSWORD" required label={i18n('PASSWORD')} fieldProps={{size: 'large', password: true}} />
<FormField name="rpassword" validation="REPEAT_PASSWORD" required label={i18n('REPEAT_PASSWORD')} fieldProps={{size: 'large', password: true}} />
<SubmitButton size="medium" className="staff-editor__submit-button">{i18n('UPDATE_PASSWORD')}</SubmitButton>
</Form>
{(this.props.myAccount) ? this.renderSendEmailOnNewTicketForm() : this.renderLevelForm()}
{(myAccount) ? this.renderSendEmailOnNewTicketForm() : this.renderLevelForm()}
<span className="separator staff-editor__separator" />
</div>
</div>
@ -106,54 +151,122 @@ class StaffEditor extends React.Component {
<div className="col-md-4">
<div className="staff-editor__departments">
<div className="staff-editor__departments-title">{i18n('DEPARTMENTS')}</div>
{(!this.props.myAccount) ? this.renderDepartmentsForm() : this.renderDepartmentsInfo()}
{(myAccount && (level !== 3)) ? this.renderDepartmentsInfo() : this.renderDepartmentsForm()}
</div>
</div>
<div className="col-md-8">
<div className="staff-editor__activity">
<div className="staff-editor__activity-title">{i18n('ACTIVITY')}</div>
{myData.lastLogin ? null : this.renderReInviteStaffButton()}
{this.renderReInviteStaffMessage()}
{this.renderStaffStats()}
</div>
</div>
</div>
{(this.props.tickets) ? this.renderTickets() : null}
{(!this.props.myAccount) ? this.renderDelete() : null}
{(tickets) ? this.renderTickets() : null}
{((!myAccount) && (userId !== staffId)) ? this.renderDelete() : null}
</div>
);
}
renderMessage() {
let messageType = (this.state.message === 'FAIL') ? 'error' : 'success';
let message = null;
renderReInviteStaffButton () {
const inviteStaffButtonContent = <div><Icon name="user-plus" /> {i18n('INVITE_STAFF')}</div>;
switch (this.state.message) {
return (
<div className="staff-editor__staff-invitation-content">
{i18n('USER_UNLOGGED_IN')}
<Button onClick={this.onReInviteStaffButton.bind(this)} size="medium" type="secondary" className="staff-editor__staff-invitation-button" disabled={this.state.loadingReInviteStaff}>
{this.state.loadingReInviteStaff ? <Loading /> : inviteStaffButtonContent}
</Button>
</div>
);
}
renderReInviteStaffMessage() {
const { reInviteStaff, showReInviteStaffMessage } = this.state;
if (reInviteStaff) {
return (
<Message
showMessage={showReInviteStaffMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showReInviteStaffMessage")}
className="staff-editor__staff-invitation-message"
type={reInviteStaff}
leftAligned>
{(reInviteStaff === "success") ? i18n('RESEND_STAFF_INVITATION_SUCCESS') : i18n('RESEND_STAFF_INVITATION_FAIL')}
</Message>
);
} else {
return null;
}
}
onReInviteStaffButton() {
this.setState({
loadingReInviteStaff: true
})
API.call({
path: '/staff/resend-invite-staff',
data: {
email: this.props.email
}
}).then(() => {
this.setState({
loadingReInviteStaff: false,
reInviteStaff: 'success',
showReInviteStaffMessage: true
})
}).catch(() => {
this.setState({
loadingReInviteStaff: false,
reInviteStaff: 'error',
showReInviteStaffMessage: true
})
})
}
renderMessage() {
const { message, showMessage } = this.state;
const messageType = (message === 'FAIL') ? 'error' : 'success';
let _message = null;
switch (message) {
case 'EMAIL':
message = 'EMAIL_CHANGED';
_message = 'EMAIL_CHANGED';
break;
case 'PASSWORD':
message = 'PASSWORD_CHANGED';
_message = 'PASSWORD_CHANGED';
break;
case 'LEVEL':
message = 'LEVEL_UPDATED';
_message = 'LEVEL_UPDATED';
break;
case 'DEPARTMENTS':
message = 'DEPARTMENTS_UPDATED';
_message = 'DEPARTMENTS_UPDATED';
break;
case 'SEND_EMAIL_ON_NEW_TICKET':
message = 'STAFF_UPDATED';
_message = 'STAFF_UPDATED';
break;
case 'FAIL':
message = 'FAILED_EDIT_STAFF';
_message = 'FAILED_EDIT_STAFF';
break;
}
return <Message className="staff-editor__message" type={messageType}>{i18n(message)}</Message>;
return (
<Message
showMessage={showMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showMessage")}
className="staff-editor__message"
type={messageType}>
{i18n(_message)}
</Message>
);
}
renderSendEmailOnNewTicketForm() {
return (
<div>
<span className="separator staff-editor__separator"/>
<span className="separator staff-editor__separator" />
<Form className="staff-editor__update-email-setting" values={{sendEmailOnNewTicket: this.state.sendEmailOnNewTicket}} onChange={form => this.setState({sendEmailOnNewTicket: form.sendEmailOnNewTicket})} onSubmit={this.onSubmit.bind(this, 'SEND_EMAIL_ON_NEW_TICKET')}>
<FormField name="sendEmailOnNewTicket" label={i18n('SEND_EMAIL_ON_NEW_TICKET')} field="checkbox" fieldProps={{size: 'large'}} />
<SubmitButton size="medium" className="staff-editor__submit-button">{i18n('UPDATE')}</SubmitButton>
@ -165,7 +278,7 @@ class StaffEditor extends React.Component {
renderLevelForm() {
return (
<div>
<span className="separator staff-editor__separator"/>
<span className="separator staff-editor__separator" />
<Form className="staff-editor__update-level" values={{level: this.state.level}} onChange={form => this.setState({level: form.level})} onSubmit={this.onSubmit.bind(this, 'LEVEL')}>
<FormField name="level" label={i18n('LEVEL')} field="select" infoMessage={this.getStaffLevelInfo()} fieldProps={{
items: [{content: i18n('LEVEL_1')}, {content: i18n('LEVEL_2')}, {content: i18n('LEVEL_3')}],
@ -186,30 +299,38 @@ class StaffEditor extends React.Component {
);
}
renderDepartmentsInfo() {
const { departments } = this.state;
const departmentsAssigned = this.getDepartments().filter((_department, index) => departments.includes(index))
return (
<Form values={{departments: this.state.departments}}>
<FormField name="departments" field="checkbox-group" fieldProps={{items: this.getDepartments()}} />
<Form values={{departments: Array.from({length: departmentsAssigned.length}, (value, index) => index)}}>
<FormField name="departments" field="checkbox-group" fieldProps={{items: departmentsAssigned}} />
</Form>
);
}
renderStaffStats() {
// return (
// <Stats staffId={this.props.staffId} type="staff"/>
// );
const { loadingStats, ticketData } = this.state;
return null;
return (
<div className="admin-panel-stats">
{
loadingStats ?
<div className="admin-panel-stats__loading"><Loading backgrounded size="large" /></div> :
statsUtils.renderStatistics({showStatCards: true, showStatsByHours: true, ticketData})
}
</div>
)
}
renderTickets() {
return (
<div>
<span className="separator"/>
<span className="separator" />
<div className="staff-editor__tickets">
<div className="staff-editor__tickets-title">{i18n('TICKETS')}</div>
<TicketList {...this.getTicketListProps()}/>
<div className="staff-editor__tickets-title">{i18n('TICKETS_ASSIGNED')}</div>
<TicketList {...this.getTicketListProps()} />
</div>
</div>
);
@ -218,7 +339,7 @@ class StaffEditor extends React.Component {
renderDelete() {
return (
<div>
<span className="separator"/>
<span className="separator" />
<div className="staff-editor__delete">
<div className="staff-editor__delete-title">
{i18n('DELETE_STAFF_MEMBER')}
@ -241,12 +362,21 @@ class StaffEditor extends React.Component {
}
getTicketListProps() {
const { staffId, departments } = this.props;
const { tickets, page, pages, closedTicketsShown } = this.state;
return {
type: 'secondary',
userId: this.props.staffId,
tickets: this.props.tickets,
departments: this.props.departments,
ticketPath: '/admin/panel/tickets/view-ticket/'
userId: staffId,
tickets,
departments,
closedTicketsShown,
ticketPath: '/admin/panel/tickets/view-ticket/',
page,
pages,
onPageChange: this.onPageChange.bind(this),
onDepartmentChange: this.onDepartmentChange.bind(this),
onClosedTicketsShownChange: this.onClosedTicketsShownChange.bind(this)
};
}
@ -265,8 +395,8 @@ class StaffEditor extends React.Component {
getDepartments() {
return SessionStore.getDepartments().map(department => {
if(department.private * 1){
return <span> {department.name} <Icon name='user-secret'/> </span>
if(department.private*1){
return <span> {department.name} <Icon name='user-secret' /> </span>
} else {
return department.name;
}
@ -293,6 +423,9 @@ class StaffEditor extends React.Component {
}
onSubmit(eventType, form) {
this.setState({loadingStats: true});
const { myAccount, staffId, onChange } = this.props;
let departments;
if(form.departments) {
@ -304,39 +437,55 @@ class StaffEditor extends React.Component {
API.call({
path: '/staff/edit',
data: {
staffId: (!this.props.myAccount) ? this.props.staffId : null,
staffId: (!myAccount) ? staffId : null,
sendEmailOnNewTicket: (eventType === 'SEND_EMAIL_ON_NEW_TICKET') ? form.sendEmailOnNewTicket * 1 : null,
email: (eventType === 'EMAIL') ? form.email : null,
password: (eventType === 'PASSWORD') ? form.password : null,
level: (form.level !== undefined && eventType == 'LEVEL') ? form.level + 1 : null,
departments: (eventType === 'DEPARTMENTS') ? (departments && JSON.stringify(departments)) : null,
level: ((form.level !== undefined) && (eventType == 'LEVEL')) ? form.level + 1 : null,
departments: (eventType === 'DEPARTMENTS') ? (departments && JSON.stringify(departments)) : null
}
}).then(() => {
window.scrollTo(0,0);
this.setState({message: eventType});
this.retrieveStaffMembers();
window.scrollTo(0,250);
this.setState({message: eventType, showMessage: true});
if(this.props.onChange) {
this.props.onChange();
}
const departmentsAssigned = SessionStore.getDepartments().filter((_department, index) => this.state.departments.includes(index));
const departmentsAssignedId = departmentsAssigned.map(department => department.id);
statsUtils.retrieveStats({
rawForm: this.state.rawForm,
departments: departmentsAssignedId
}).then(({data}) => {
this.setState({ticketData: data, loadingStats: false});
}).catch((error) => {
if (showLogs) console.error('ERROR: ', error);
this.setState({loadingStats: false});
});
onChange && onChange();
}).catch(() => {
window.scrollTo(0,0);
this.setState({message: 'FAIL'});
window.scrollTo(0,250);
this.setState({message: 'FAIL', loadingStats: false, showMessage: true});
});
}
onDeleteClick() {
API.call({
const { staffId, onDelete } = this.props;
return API.call({
path: '/staff/delete',
data: {
staffId: this.props.staffId
staffId: staffId
}
}).then(this.props.onDelete).catch(() => {
}).then(onDelete).catch(() => {
window.scrollTo(0,0);
this.setState({message: 'FAIL'});
this.setState({message: 'FAIL', showMessage: true});
});
}
onProfilePicChange(event) {
const { myAcount, staffId, onChange } = this.props;
this.setState({
loadingPicture: true
});
@ -345,7 +494,7 @@ class StaffEditor extends React.Component {
path: '/staff/edit',
dataAsForm: true,
data: {
staffId: (!this.props.myAcount) ? this.props.staffId : null,
staffId: (!myAcount) ? staffId : null,
file: event.target.files[0]
}
}).then(() => {
@ -353,14 +502,88 @@ class StaffEditor extends React.Component {
loadingPicture: false
});
if(this.props.onChange) {
this.props.onChange();
}
onChange && onChange();
}).catch(() => {
window.scrollTo(0,0);
this.setState({message: 'FAIL', loadingPicture: false});
this.setState({message: 'FAIL', loadingPicture: false, showMessage: true});
});
}
retrieveTicketsAssigned({page, department, closed}) {
API.call({
path: '/ticket/search',
data: {
page,
departments: department,
closed,
owners: `[${this.props.staffId}]`
}
}).then((result) => {
const data = result.data;
this.setState({
tickets: data.tickets,
page: data.page,
pages: data.pages
});
});
}
onPageChange(event) {
this.setState({
page: event.target.value
});
this.retrieveTicketsAssigned({page: event.target.value});
}
onDepartmentChange(department) {
const { closedTicketsShown } = this.state;
this.setState({
department
});
this.retrieveTicketsAssigned(this.prepareFiltersForAPI({
newClosedFilter: closedTicketsShown,
newDepartmentFilter: department
}));
}
onClosedTicketsShownChange() {
const { department, closedTicketsShown } = this.state;
const newClosedValue = !closedTicketsShown;
this.setState({
closedTicketsShown: newClosedValue
});
this.retrieveTicketsAssigned(this.prepareFiltersForAPI({
newClosedFilter: newClosedValue,
newDepartmentFilter: department
}));
}
retrieveStaffMembers() {
this.props.dispatch(AdminDataActions.retrieveStaffMembers());
}
prepareFiltersForAPI({newClosedFilter, newDepartmentFilter}) {
return {
closed: newClosedFilter ? undefined : 0,
department: newDepartmentFilter ? `[${newDepartmentFilter}]` : undefined
}
}
onCloseMessage(showMessage) {
this.setState({
[showMessage]: false
});
}
}
export default StaffEditor;
export default connect((store) => {
return {
staffList: store.adminData.staffMembers
};
})(StaffEditor);

View File

@ -213,11 +213,23 @@
}
&__activity {
&-title {
margin-bottom: 10px;
text-align: left;
}
}
}
&__staff {
&-invitation-content {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
}
&-invitation-button {
min-width: 180px;
}
}
}

View File

@ -1,102 +0,0 @@
import React from 'react';
import {connect} from 'react-redux';
import i18n from 'lib-app/i18n';
import AdminDataAction from 'actions/admin-data-actions';
import TicketList from 'app-components/ticket-list';
import Header from 'core-components/header';
import SearchBox from 'core-components/search-box';
import Message from 'core-components/message';
class AdminPanelAllTickets extends React.Component {
static defaultProps = {
userId: 0,
departments: [],
tickets: []
};
state = {
page: 1,
query: '',
closedTicketsShown: 0
};
componentDidMount() {
this.updateTicketList();
}
render() {
const noDepartments = !this.props.departments.length;
return (
<div className="admin-panel-all-tickets">
<Header title={i18n('ALL_TICKETS')} description={i18n('ALL_TICKETS_DESCRIPTION')} />
{(noDepartments) ? <Message className="admin-panel-all-tickets__department-warning" type="warning">{i18n('NO_DEPARTMENT_ASSIGNED')}</Message> : null}
<div className="admin-panel-all-tickets__search-box">
<SearchBox onSearch={this.onSearch.bind(this)} />
</div>
{(this.props.error) ? <Message type="error">{i18n('ERROR_RETRIEVING_TICKETS')}</Message> : <TicketList {...this.getTicketListProps()}/>}
</div>
);
}
updateTicketList() {
this.props.dispatch(AdminDataAction.retrieveAllTickets(
this.state.page,
this.state.query,
this.state.closedTicketsShown * 1
));
}
getTicketListProps() {
return {
userId: this.props.userId,
showDepartmentDropdown: false,
departments: this.props.departments,
tickets: this.props.tickets,
type: 'secondary',
loading: this.props.loading,
ticketPath: '/admin/panel/tickets/view-ticket/',
onPageChange: this.onPageChange.bind(this),
page: this.state.page,
pages: this.props.pages,
closedTicketsShown: this.state.closedTicketsShown,
onClosedTicketsShownChange: this.onClosedTicketsShownChange.bind(this)
};
}
onClosedTicketsShownChange() {
this.setState(function(state) {
return {
closedTicketsShown: !state.closedTicketsShown
};
}, () => {
this.updateTicketList();
});
}
onSearch(query) {
this.setState({query, page: 1}, () => {
this.updateTicketList();
});
}
onPageChange(event) {
this.setState({page: event.target.value}, () => {
this.updateTicketList();
});
}
}
export default connect((store) => {
return {
userId: store.session.userId,
departments: store.session.userDepartments,
tickets: store.adminData.allTickets,
pages: store.adminData.allTicketsPages,
loading: !store.adminData.allTicketsLoaded,
error: store.adminData.allTicketsError
};
})(AdminPanelAllTickets);

View File

@ -1,11 +0,0 @@
.admin-panel-all-tickets {
&__search-box {
padding: 0 50px;
margin-bottom: 30px;
}
&__department-warning {
margin-bottom: 20px;
}
}

View File

@ -14,6 +14,7 @@ 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 Message from 'core-components/message';
import Form from 'core-components/form';
import FormField from 'core-components/form-field';
import SubmitButton from 'core-components/submit-button';
@ -30,6 +31,7 @@ class AdminPanelCustomResponses extends React.Component {
formLoading: false,
selectedIndex: -1,
errors: {},
error:'',
originalForm: {
title: '',
content: TextEditor.createEmpty(),
@ -39,7 +41,8 @@ class AdminPanelCustomResponses extends React.Component {
title: '',
content: TextEditor.createEmpty(),
language: this.props.language
}
},
showErrorMessage: true
};
componentDidMount() {
@ -61,7 +64,7 @@ class AdminPanelCustomResponses extends React.Component {
return (
<div className="row">
<div className="col-md-3">
<Listing {...this.getListingProps()}/>
<Listing {...this.getListingProps()} />
</div>
{this.state.showForm ? this.renderForm() : null}
</div>
@ -71,7 +74,7 @@ class AdminPanelCustomResponses extends React.Component {
renderLoading() {
return (
<div className="admin-panel-custom-responses__loading">
<Loading backgrounded size="large"/>
<Loading backgrounded size="large" />
</div>
);
}
@ -82,33 +85,46 @@ class AdminPanelCustomResponses extends React.Component {
<Form {...this.getFormProps()}>
<div className="row">
<div className="col-md-7">
<FormField label={i18n('TITLE')} name="title" validation="TITLE" required fieldProps={{size: 'large'}}/>
<FormField label={i18n('TITLE')} name="title" validation="TITLE" required fieldProps={{size: 'large'}} />
</div>
<div className="col-md-5">
<FormField label={i18n('LANGUAGE')} name="language" field="input" decorator={LanguageSelector} fieldProps={{size: 'medium'}} />
</div>
</div>
<FormField label={i18n('CONTENT')} name="content" validation="TEXT_AREA" required field="textarea" />
<FormField label={i18n('CONTENT')} name="content" validation="TEXT_AREA" required field="textarea" />
<div className="admin-panel-custom-responses__actions">
{(this.state.selectedIndex !== -1) ? this.renderOptionalButtons() : null}
<div className="admin-panel-custom-responses__save-button">
<SubmitButton type="secondary" size="small">{i18n('SAVE')}</SubmitButton>
</div>
{(this.state.selectedIndex !== -1) ? this.renderOptionalButtons() : null}
</div>
{this.state.error ? this.renderErrorMessage() : null}
</Form>
</div>
);
}
renderErrorMessage() {
const { showErrorMessage, error } = this.state;
return(
<Message
showMessage={showErrorMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showErrorMessage")}
className="admin-panel-custom-responses__message"
type="error">
{i18n(error)}
</Message>
)
}
renderOptionalButtons() {
return (
<div className="admin-panel-custom-responses__optional-buttons">
<div className="admin-panel-custom-responses__discard-button">
{this.isEdited() ? <Button onClick={this.onDiscardChangesClick.bind(this)}>{i18n('DISCARD_CHANGES')}</Button> : null}
</div>
<div className="admin-panel-custom-responses__delete-button">
<Button onClick={this.onDeleteClick.bind(this)}>{i18n('DELETE')}</Button>
</div>
<div className="admin-panel-custom-responses__discard-button">
{this.isEdited() ? <Button onClick={this.onDiscardChangesClick.bind(this)}>{i18n('DISCARD_CHANGES')}</Button> : null}
</div>
</div>
);
}
@ -125,10 +141,12 @@ class AdminPanelCustomResponses extends React.Component {
}
getFormProps() {
const { form, errors, formLoading } = this.state;
return {
values: this.state.form,
errors: this.state.errors,
loading: this.state.formLoading,
values: form,
errors,
loading: formLoading,
onClick: () => this.setState({formClicked: true}),
onChange: (form) => this.setState({form}),
onValidateErrors: (errors) => {this.setState({errors})},
@ -143,7 +161,7 @@ class AdminPanelCustomResponses extends React.Component {
<span>
{item.name}
<span className="admin-panel-custom-responses__item-flag">
<Icon name={(item.language != 'en') ? item.language : 'us'}/>
<Icon name={(item.language != 'en') ? item.language : 'us'} />
</span>
</span>
)
@ -161,13 +179,15 @@ class AdminPanelCustomResponses extends React.Component {
onFormSubmit(form) {
const {items, allowedLanguages} = this.props;
const { selectedIndex } = this.state;
this.setState({formLoading: true});
if(this.state.selectedIndex !== -1) {
if(selectedIndex !== -1) {
API.call({
path: '/ticket/edit-custom-response',
data: {
id: items[this.state.selectedIndex].id,
id: items[selectedIndex].id,
name: form.title,
content: form.content,
language: _.includes(allowedLanguages, form.language) ? form.language : allowedLanguages[0]
@ -186,9 +206,13 @@ class AdminPanelCustomResponses extends React.Component {
language: _.includes(allowedLanguages, form.language) ? form.language : allowedLanguages[0]
}
}).then(() => {
this.setState({error: ''});
this.retrieveCustomResponses();
this.onItemChange(-1);
}).catch(this.onItemChange.bind(this, -1));
}).catch((e) => {
this.onItemChange.bind(this, -1)
this.setState({error: e.message, formLoading:false, showErrorMessage: true});
});
}
}
@ -203,7 +227,9 @@ class AdminPanelCustomResponses extends React.Component {
}
deleteCustomResponse() {
API.call({
this.updateForm(this.state.selectedIndex)
return API.call({
path: '/ticket/delete-custom-response',
data: {
id: this.props.items[this.state.selectedIndex].id
@ -215,11 +241,14 @@ class AdminPanelCustomResponses extends React.Component {
}
updateForm(index) {
const { items, language } = this.props;
const item = items[index];
let form = _.clone(this.state.form);
form.title = (this.props.items[index] && this.props.items[index].name) || '';
form.content = TextEditor.getEditorStateFromHTML((this.props.items[index] && this.props.items[index].content) || '');
form.language = (this.props.items[index] && this.props.items[index].language) || this.props.language;
form.title = (item && item.name) || '';
form.content = TextEditor.getEditorStateFromHTML((item && item.content) || '');
form.language = (item && item.language) || language;
this.setState({
formClicked: false,
@ -227,7 +256,7 @@ class AdminPanelCustomResponses extends React.Component {
selectedIndex: index,
formLoading: false,
originalForm: form,
form: form,
form,
errors: {}
});
}
@ -237,12 +266,22 @@ class AdminPanelCustomResponses extends React.Component {
}
isEdited() {
return this.state.form.title && this.state.formClicked && (
this.state.form.title != this.state.originalForm.title ||
this.state.form.content != this.state.originalForm.content ||
this.state.form.language != this.state.originalForm.language
const { form, formClicked, originalForm } = this.state;
return (
form.title && formClicked && (
form.title != originalForm.title ||
form.content != originalForm.content ||
form.language != originalForm.language
)
);
}
onCloseMessage(showMessage) {
this.setState({
[showMessage]: false
});
}
}
export default connect((store) => {

View File

@ -7,25 +7,30 @@
float: right;
}
&__actions {
text-align: left;
&__message {
margin-top: 10px;
text-align: center;
}
&__actions,
&__optional-buttons {
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
width: 100%;
}
&__discard-button,
&__delete-button {
padding: 0 10px;
}
&__save-button {
display: inline-block;
margin-right: 30px;
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
width: 100%;
}
&__optional-buttons {
display: inline;
}
&__discard-button {
display: inline-block;
}
&__delete-button {
display: inline-block;
float: right;
}
}
}

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