mirror of
https://github.com/opensupports/opensupports.git
synced 2025-04-08 18:35:06 +02:00
Compare commits
225 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
20720ca4f9 | ||
|
40016635a8 | ||
|
68b2d2bf63 | ||
|
36c5f3264b | ||
|
82fd54ffd9 | ||
|
8c732f5dda | ||
|
b620baf5ed | ||
|
d3b47eaa73 | ||
|
90e7923aec | ||
|
0ecf88237f | ||
|
713a5b5ee1 | ||
|
b578a26225 | ||
|
93fa9e12a3 | ||
|
3720baf370 | ||
|
b1ef69c60c | ||
|
615f42a91b | ||
|
39f8a601db | ||
|
c82aaa001e | ||
|
04923b0e9d | ||
|
5e7f39df05 | ||
|
e190710252 | ||
|
a74d6ab4d7 | ||
|
a5da776d27 | ||
|
41d7aa5406 | ||
|
f2adb160be | ||
|
62bd70cc3b | ||
|
0f6c64674e | ||
|
861f7dc254 | ||
|
83b8e8094b | ||
|
81cc579d57 | ||
|
b3c8819d83 | ||
|
86cad910ec | ||
|
f890fdc2d3 | ||
|
c64eb9930b | ||
|
74870c632a | ||
|
e0738f1c88 | ||
|
545f88236d | ||
|
2e5bfa611e | ||
|
784d470ba5 | ||
|
54a5a9803d | ||
|
d5552c0f73 | ||
|
daf1db847c | ||
|
2df12aa5e3 | ||
|
c80c026617 | ||
|
9cf71dcf66 | ||
|
a264d384a1 | ||
|
d90b7d48c3 | ||
|
527843f00f | ||
|
3e3a95f518 | ||
|
639d40ddb0 | ||
|
fbee7275d5 | ||
|
7df190ea01 | ||
|
1228d593d0 | ||
|
c1a7befbed | ||
|
6e195a9109 | ||
|
bf7c1ba8f9 | ||
|
8b4b73402e | ||
|
4cf446483d | ||
|
ea6bc7f436 | ||
|
37209ef3fc | ||
|
83e75cc572 | ||
|
9291aa66a4 | ||
|
b9f5f7fcf1 | ||
|
fe1dd1bd48 | ||
|
5dd6b7acdc | ||
|
4bd8df1d5e | ||
|
8d7b178fa1 | ||
|
237801e9ed | ||
|
950439bf47 | ||
|
6cb538616d | ||
|
156b285344 | ||
|
b39e4c2a5f | ||
|
5a1b558a6d | ||
|
4b9a55b334 | ||
|
e7daf76274 | ||
|
402af565a9 | ||
|
f5e9b2602c | ||
|
f1d746f9f9 | ||
|
c0fc7933ed | ||
|
f31586874e | ||
|
d2c126aad9 | ||
|
ffd09b841f | ||
|
e23468d3be | ||
|
84f36e89dc | ||
|
9715cdc9a1 | ||
|
b39dca642b | ||
|
6f6acc925d | ||
|
d2e6dc2fbe | ||
|
0df57af11e | ||
|
8400a1caf0 | ||
|
f045c08b2e | ||
|
645e64532b | ||
|
b9f935df5f | ||
|
edddc8d2c5 | ||
|
eaeaaa647e | ||
|
d7ccff1a5a | ||
|
c5f1aa2b92 | ||
|
9ed4caf202 | ||
|
018863ab3e | ||
|
7015e21966 | ||
|
7cdb6d3603 | ||
|
1836849fa5 | ||
|
9f9e1dbd91 | ||
|
60b1b5eec5 | ||
|
7b4427d3e3 | ||
|
09150d6940 | ||
|
02cf8f0da3 | ||
|
e15bd15f07 | ||
|
c657d8291f | ||
|
b2e43430b1 | ||
|
8c17d22ab3 | ||
|
143776febe | ||
|
6536050fdd | ||
|
9a4374d371 | ||
|
b8be664809 | ||
|
e9a1a2e5be | ||
|
0f976ebde9 | ||
|
c64f1f1ea6 | ||
|
5d4fe0250b | ||
|
af15d0116d | ||
|
6ccb389492 | ||
|
ae076de88f | ||
|
59fb9eaef3 | ||
|
ffe7ef8e0b | ||
|
27e86c934c | ||
|
064f00388a | ||
|
fc93ad4c00 | ||
|
c22a1cdbb3 | ||
|
89cb4c18fd | ||
|
167d7927db | ||
|
55c89d58cc | ||
|
0bcc775944 | ||
|
e6441179c9 | ||
|
45a59e0b20 | ||
|
9f7b11413c | ||
|
e554bb64d1 | ||
|
0088332562 | ||
|
42c5dd7210 | ||
|
1a9e1a852c | ||
|
49dc1ab56c | ||
|
1ea4509e4f | ||
|
c5d6068e97 | ||
|
51eee4ed7b | ||
|
1e0e0134a3 | ||
|
bc9023b8a6 | ||
|
534bf3624a | ||
|
a7cb7f376c | ||
|
c3088406da | ||
|
ea8d0719eb | ||
|
e4a7fe8783 | ||
|
ca54d19bd9 | ||
|
5416ef4009 | ||
|
af3d95cf4d | ||
|
3dd76f214d | ||
|
16435925b6 | ||
|
ea273970d1 | ||
|
b9b21ef950 | ||
|
bb1f5d0ade | ||
|
15f765cf85 | ||
|
9feb7d6cd4 | ||
|
94926f90e6 | ||
|
b02acfdf7b | ||
|
560f231e51 | ||
|
7fb7be3860 | ||
|
b8944a3f04 | ||
|
a64c9f2255 | ||
|
994a39ad6d | ||
|
b73d6d534d | ||
|
d4cdbab203 | ||
|
80a9a958a8 | ||
|
9125944bc3 | ||
|
c00720d6a2 | ||
|
5184c31907 | ||
|
817240e0b4 | ||
|
52eae4d242 | ||
|
d3638787e6 | ||
|
68c3975ea4 | ||
|
6e6d2d83e7 | ||
|
371f111706 | ||
|
bb89956e9a | ||
|
a89a465ac9 | ||
|
9f915a3291 | ||
|
0fd23ac6b8 | ||
|
5021781d25 | ||
|
47f92569ef | ||
|
c92ecf25dc | ||
|
38136ade4d | ||
|
da534e3018 | ||
|
0518b0ac17 | ||
|
5d2e4fcb1f | ||
|
7a803dd7ff | ||
|
4077dac8c7 | ||
|
c4e211518c | ||
|
56ca30a7c3 | ||
|
fad3dce646 | ||
|
deb590ceee | ||
|
e36b984b23 | ||
|
e397d45c53 | ||
|
e4a9366b07 | ||
|
bbbc845fe7 | ||
|
a46140381b | ||
|
d72aec3976 | ||
|
1c5d156723 | ||
|
f72f2ac074 | ||
|
01a494ac23 | ||
|
4fd9db8651 | ||
|
01718cf92b | ||
|
96990c8c04 | ||
|
bb9873be4f | ||
|
4970b18c2d | ||
|
92e222f10d | ||
|
76b7e2c6e7 | ||
|
38c90e2c07 | ||
|
938e25b2fa | ||
|
1ecf619892 | ||
|
57b5ea8820 | ||
|
7866880152 | ||
|
6d938bead4 | ||
|
5b16393659 | ||
|
e3b57bd106 | ||
|
2bf6b2c23f | ||
|
384f7c93d7 | ||
|
785e2d8ac5 | ||
|
47ea47f971 | ||
|
f31d0aa377 |
190
.circleci/config.yml
Normal file
190
.circleci/config.yml
Normal 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
|
14
.github/workflows/run-tests.yml
vendored
Normal file
14
.github/workflows/run-tests.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
name: run-tests
|
||||
on: [push]
|
||||
jobs:
|
||||
run-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: cd server && make build
|
||||
- run: cd server && make run
|
||||
- run: cd server && make install-not-interactive
|
||||
- run: cd server && make setup-vendor-permissions
|
||||
- run: cd server && make test-not-interactive
|
||||
- run: cd tests && make build
|
||||
- run: cd tests && make run-not-interactive
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,8 +1,6 @@
|
||||
.idea
|
||||
.jshintrc
|
||||
client/package-lock.json
|
||||
tests/Gemfile.lock
|
||||
server/composer.lock
|
||||
.DS_Store
|
||||
server/vendor
|
||||
server/files/
|
||||
!server/files/.gitkeep
|
||||
@ -11,3 +9,4 @@ server/.dbdata
|
||||
server/.fakemail
|
||||
server/apidoc
|
||||
dist/
|
||||
.env
|
||||
|
80
DEVELOPMENT.md
Normal file
80
DEVELOPMENT.md
Normal file
@ -0,0 +1,80 @@
|
||||
# Development
|
||||
|
||||
Here is a guide of how to set up the development environment in OpenSupports.
|
||||
|
||||
## Requirements
|
||||
* PHP 5.6+
|
||||
* MySQL 4.1+
|
||||
|
||||
### Getting up and running FRONT-END (client folder)
|
||||
1. Update: `sudo apt update`
|
||||
2. Clone this repo: `git clone https://github.com/opensupports/opensupports.git`
|
||||
3. Install `nvm`: https://github.com/nvm-sh/nvm
|
||||
4. Use node version 11.15.0: `nvm install 11` followed by `nvm use 11`
|
||||
5. Go to client: `cd opensupports/client`
|
||||
6. Install dependencies: `npm install`
|
||||
7. Rebuild node-sass: `npm rebuild node-sass`
|
||||
8. Run: `npm start` (PHP server api it must be running at :8080)
|
||||
10. Go to the main app: `http://localhost:3000/app`
|
||||
11. Your browser will automatically be opened and directed to the browser-sync proxy address.
|
||||
12. Use `npm start-fixtures` to enable fixtures and not require php server to be running.
|
||||
|
||||
OpenSupport uses by default the port 3000, but this port could already be used. If this is the case, you can modify this in the file: `client/webpack.config.js`.
|
||||
|
||||
##### Production Task
|
||||
|
||||
Just as there is a task for development, there is also a `npm build` task for putting the project into a production-ready state. This will run each of the tasks, while also adding the image minification task discussed above and the result store in `dist/` folder.
|
||||
|
||||
**Reminder:** Notice there is `index.html` and `index.php`. The first one searches the backend server where `config.js` says it, the second one uses `/api` to find the server. If you want to run OpenSupports in a single server, then use `index.php`.
|
||||
|
||||
#### Frontend Unit Testing
|
||||
1. Do the steps described before.
|
||||
2. Install mocha: `npm install -g mocha@6.2.0`
|
||||
3. Run `npm test` to run the tests.
|
||||
|
||||
### Getting up and running BACK-END (server folder)
|
||||
1. Install [Docker CE](https://docs.docker.com/install/)
|
||||
2. Go to the server folder: `cd opensupports/server`
|
||||
3. Run `make build` to build the images
|
||||
4. Run `make install` to install composer dependencies
|
||||
|
||||
- `make run` runs the backend and database
|
||||
- `make stop` stop backend and database server
|
||||
- `make log` show live server logs
|
||||
- `make db` access to mysql database console
|
||||
- `make sh` access to backend docker container bash
|
||||
- `make test` run phpunit tests
|
||||
- `make doc` to build the documentation (requires `apidoc`)
|
||||
|
||||
Server api runs on `http://localhost:8080/`
|
||||
Also, there's a *phpmyadmin* instance running on `http://localhost:6060/`,
|
||||
you can access with the username `root` and empty password
|
||||
|
||||
##### Building
|
||||
Once you've installed dependencies for frontend and backend, you can run `./build.sh` and it will generate a zip file inside `dist/` ready for distribution. You can use this file to install OpenSupports on a serving following the [installation instructions](https://github.com/opensupports/opensupports/wiki/Installation)
|
||||
|
||||
##### BACKEND API RUBY TESTING
|
||||
|
||||
1. Go to tests folder: `cd opensupports/tests`
|
||||
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
|
||||
|
||||
##### BACKEND FAKE SMTP SERVER
|
||||
If you're doing development, you can use a FakeSMTP server to see the mails that are being sent.
|
||||
|
||||
1. Install Java if you don't have it yet:
|
||||
|
||||
`sudo apt-get install default-jre`
|
||||
`sudo apt-get install default-jdk`
|
||||
|
||||
2. [Download FakeSMTP](https://nilhcem.github.io/FakeSMTP/download.html)
|
||||
|
||||
3. Extract the file from the zip and run it:
|
||||
|
||||
`java -jar fakeSMTP-2.0.jar`
|
||||
|
||||
4. Set the port to 7070 and start the SMTP server.
|
||||
|
||||
5. Every time the application sends an email, it will be reflected there.
|
71
Makefile
Normal file
71
Makefile
Normal file
@ -0,0 +1,71 @@
|
||||
#!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"}}'
|
||||
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": "dev4"}}'
|
||||
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
|
100
README.md
100
README.md
@ -1,89 +1,61 @@
|
||||

|
||||
<div align="center">
|
||||
|
||||
[](https://travis-ci.org/opensupports/opensupports) v4.8.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/)
|
||||
OpenSupports is a simple and beautiful open source ticket system. <br />
|
||||
<a href="https://www.opensupports.com/"><strong>Learn more »</strong></a>
|
||||
<br />
|
||||
<p align="center">
|
||||
<a href="https://www.opensupports.com/">Website</a> •
|
||||
<a href="https://docs.opensupports.com/">Docs</a> •
|
||||
<a href="https://opensupports.com/demo/">Demo</a> •
|
||||
<a href="https://www.opensupports.com/pricing/">Official Subscription</a>
|
||||
</p>
|
||||
|
||||
## Requirements
|
||||
* PHP 5.6+
|
||||
* MySQL 4.1+
|
||||
</div>
|
||||
|
||||
## Development
|
||||
Here is a guide of how to set up the development environment in OpenSupports.
|
||||
## 🌱 About the Project
|
||||
|
||||
### Getting up and running FRONT-END (client folder)
|
||||
1. Update: `sudo apt-get update`
|
||||
2. Clone this repo: `git clone https://github.com/opensupports/opensupports.git`
|
||||
3. Install node 4.x version:
|
||||
- `sudo apt-get install curl`
|
||||
- `curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -`
|
||||
- `sudo apt-get install -y nodejs`
|
||||
4. Install npm: `sudo apt-get install npm`
|
||||
5. Go to client: `cd opensupports/client`
|
||||
6. Install dependencies: `npm install`
|
||||
7. Rebuild node-sass: `npm rebuild node-sass`
|
||||
8. Run: `npm start` (PHP server api it must be running at :8080)
|
||||
10. Go to the main app: `http://localhost:3000/app` or to the component demo `http://localhost:3000/demo`
|
||||
11. Your browser will automatically be opened and directed to the browser-sync proxy address.
|
||||
12. Use `npm start-fixtures` to enable fixtures and not require php server to be running.
|
||||
### What Customers See
|
||||
|
||||
OpenSupport uses by default the port 3000, but this port could already be used. If this is the case, you can modify this in the file: `client/webpack.config.js`.
|
||||

|
||||
|
||||
##### Production Task
|
||||
### What Staff Members See
|
||||
|
||||
Just as there is a task for development, there is also a `npm build` task for putting the project into a production-ready state. This will run each of the tasks, while also adding the image minification task discussed above and the result store in `dist/` folder.
|
||||

|
||||
|
||||
**Reminder:** Notice there is `index.html` and `index.php`. The first one searches the backend server where `config.js` says it, the second one uses `/api` to find the server. If you want to run OpenSupports in a single server, then use `index.php`.
|
||||
## 🙌🏼 Ticket System for Absolutely Everyone
|
||||
|
||||
#### Frontend Unit Testing
|
||||
1. Do the steps described before.
|
||||
2. Install mocha: `npm install -g mocha@6.2.0`
|
||||
3. Run `npm test` to run the tests.
|
||||
OpenSupports is a simple and beautiful open source ticket system.
|
||||
|
||||
### Getting up and running BACK-END (server folder)
|
||||
1. Install [Docker CE](https://docs.docker.com/install/)
|
||||
2. Go to the server folder: `cd opensupports/server`
|
||||
3. Run `make build` to build the images
|
||||
4. Run `make install` to install composer dependencies
|
||||
It is a web application that provides you with a better management of your users’ queries. They send you tickets through OpenSupports and you can handle them appropriately.
|
||||
|
||||
- `make run` runs the backend and database
|
||||
- `make stop` stop backend and database server
|
||||
- `make log` show live server logs
|
||||
- `make db` access to mysql database console
|
||||
- `make sh` access to backend docker container bash
|
||||
- `make test` run phpunit tests
|
||||
- `make doc` to build the documentation (requires `apidoc`)
|
||||
Self-hosted, or [hosted by us](https://www.opensupports.com/pricing/), API-driven, and ready to be deployed on your own domain.
|
||||
|
||||
Server api runs on `http://localhost:8080/`
|
||||
Also, there's a *phpmyadmin* instance running on `http://localhost:6060/`,
|
||||
you can access with the username `root` and empty password
|
||||
## 🧐 Stay Up-to-Date
|
||||
|
||||
##### Building
|
||||
Once you've installed dependencies for frontend and backend, you can run `./build.sh` and it will generate a zip file inside `dist/` ready for distribution. You can use this file to install OpenSupports on a serving following the [installation instructions](https://github.com/opensupports/opensupports/wiki/Installation)
|
||||
OpenSupports is growing and steadily incorporating new features. You might want to **add a star to the project** (or watch updates) to be notified about new releases.
|
||||
|
||||
##### BACKEND API RUBY TESTING
|
||||
## 💪🏼 Features
|
||||
|
||||
1. Go to tests folder: `cd opensupports/tests`
|
||||
2. Run `make build` to install ruby container and its required dependencies
|
||||
Check out our [most important features](https://opensupports.com/features) at our website.
|
||||
|
||||
- `make run` for running tests (database will be cleared)
|
||||
- `make clear` for clearing database
|
||||
Are we missing something? [Suggest an improvement](https://github.com/opensupports/opensupports/issues/new)!
|
||||
|
||||
##### BACKEND FAKE SMTP SERVER
|
||||
If you're doing development, you can use a FakeSMTP server to see the mails that are being sent.
|
||||
## 🛠 Install
|
||||
|
||||
1. Install Java if you don't have it yet:
|
||||
OpenSupports can be hosted on your own servers, or [hosted by us](https://www.opensupports.com/pricing/).
|
||||
|
||||
`sudo apt-get install default-jre`
|
||||
`sudo apt-get install default-jdk`
|
||||
There are multiple benefits to having the system hosted by its creators, including official support into any problem you might encounter.
|
||||
|
||||
2. [Download FakeSMTP](https://nilhcem.github.io/FakeSMTP/download.html)
|
||||
But in the case you prefer your development team to deal with the installation, maintenance (upgrades, backups, etc.), and integrations, we charge you nothing for it, OpenSupports is **free and open-source**!
|
||||
|
||||
3. Extract the file from the zip and run it:
|
||||
Check out our [installation guide](https://docs.opensupports.com/guides/installation/).
|
||||
|
||||
`java -jar fakeSMTP-2.0.jar`
|
||||
## 👨🏼💻 Development
|
||||
|
||||
4. Set the port to 7070 and start the SMTP server.
|
||||
Are you a programmer? You can help us to fix bugs and build OpenSupports' features!
|
||||
|
||||
5. Every time the application sends an email, it will be reflected there.
|
||||
Check out our [development guide](./DEVELOPMENT.md) to get your development environment up and running.
|
||||
|
||||
And even if you are not a programmer, you can help us by [reporting problems or suggesting improvements](https://github.com/opensupports/opensupports/issues/new), we love feedback and learn a lot from it!
|
||||
|
19
SECURITY.md
Normal file
19
SECURITY.md
Normal 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.
|
1
build.sh
1
build.sh
@ -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
|
||||
|
11
client/Makefile
Normal file
11
client/Makefile
Normal file
@ -0,0 +1,11 @@
|
||||
build:
|
||||
@docker pull node:11.15.0
|
||||
|
||||
run: stop
|
||||
@docker run --platform=linux/amd64 --network os-net --name opensupports-client -v $(PWD):/client:delegated -p 3000:3000 node:11.15.0 sh -c "cd client && npm install && npm start"
|
||||
|
||||
sh: stop
|
||||
@docker run -it --platform=linux/amd64 --network os-net --name opensupports-client -v $(PWD):/client:delegated -p 3000:3000 node:11.15.0 sh -c "bash"
|
||||
|
||||
stop:
|
||||
@docker rm -f opensupports-client || true
|
21698
client/package-lock.json
generated
Normal file
21698
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "OpenSupports",
|
||||
"version": "4.8.0",
|
||||
"version": "4.11.0",
|
||||
"author": "Ivan Diaz <contact@opensupports.com>",
|
||||
"description": "Open source ticket system made with PHP and ReactJS",
|
||||
"repository": {
|
||||
@ -9,13 +9,13 @@
|
||||
},
|
||||
"private": false,
|
||||
"engines": {
|
||||
"node": "^0.12.x",
|
||||
"npm": "^2.1.x"
|
||||
"node": "^11.15.x",
|
||||
"npm": "^6.7.x"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "webpack-dev-server",
|
||||
"start": "webpack-dev-server --display-reasons --display-error-details --history-api-fallback --progress --colors",
|
||||
"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,19 +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",
|
||||
|
@ -1,5 +1,5 @@
|
||||
export default {
|
||||
login: stub(),
|
||||
logout: stub(),
|
||||
initSession: stub()
|
||||
checkSession: stub()
|
||||
};
|
@ -88,7 +88,7 @@
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('initSession action', function () {
|
||||
// describe('checkSession action', function () {
|
||||
// beforeEach(function () {
|
||||
// APICallMock.call.returns({
|
||||
// then: function (resolve) {
|
||||
@ -125,7 +125,7 @@
|
||||
// }
|
||||
// });
|
||||
|
||||
// expect(SessionActions.initSession().type).to.equal('CHECK_SESSION');
|
||||
// expect(SessionActions.checkSession().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',
|
||||
@ -136,7 +136,7 @@
|
||||
// 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(SessionActions.checkSession().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',
|
||||
@ -147,7 +147,7 @@
|
||||
// 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(SessionActions.checkSession().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',
|
||||
|
@ -12,22 +12,22 @@ export default {
|
||||
};
|
||||
},
|
||||
|
||||
retrieveMyTickets(page, closed = 0, departmentId = 0) {
|
||||
retrieveMyTickets({page, closed = 0, departmentId = 0, pageSize = 10}) {
|
||||
return {
|
||||
type: 'MY_TICKETS',
|
||||
payload: API.call({
|
||||
path: '/staff/get-tickets',
|
||||
data: {page, closed, departmentId}
|
||||
data: {page, closed, departmentId, pageSize}
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
retrieveNewTickets(page = 1, departmentId = 0) {
|
||||
retrieveNewTickets({page, departmentId = 0, pageSize = 10}) {
|
||||
return {
|
||||
type: 'NEW_TICKETS',
|
||||
payload: API.call({
|
||||
path: '/staff/get-new-tickets',
|
||||
data: {page, departmentId}
|
||||
data: {page, departmentId, pageSize}
|
||||
})
|
||||
};
|
||||
},
|
||||
|
@ -9,14 +9,15 @@ export default {
|
||||
payload: {}
|
||||
}
|
||||
},
|
||||
retrieveSearchTickets(ticketQueryListState, filters = {}) {
|
||||
retrieveSearchTickets(ticketQueryListState, filters = {}, pageSize = 10) {
|
||||
return {
|
||||
type: 'SEARCH_TICKETS',
|
||||
payload: API.call({
|
||||
path: '/ticket/search',
|
||||
data: {
|
||||
...filters,
|
||||
page: ticketQueryListState.page
|
||||
page: ticketQueryListState.page,
|
||||
pageSize
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -28,7 +29,7 @@ export default {
|
||||
}
|
||||
},
|
||||
changeFilters(listConfig) {
|
||||
const filtersForAPI = searchTicketsUtils.prepareFiltersForAPI(listConfig.filters);
|
||||
const filtersForAPI = searchTicketsUtils.getFiltersForAPI(listConfig.filters);
|
||||
|
||||
return {
|
||||
type: 'SEARCH_FILTERS_CHANGE_FILTERS',
|
||||
@ -48,7 +49,7 @@ export default {
|
||||
}
|
||||
},
|
||||
changePage(filtersWithPage) {
|
||||
const filtersForAPI = searchTicketsUtils.prepareFiltersForAPI(filtersWithPage);
|
||||
const filtersForAPI = searchTicketsUtils.getFiltersForAPI(filtersWithPage);
|
||||
const currentPath = window.location.pathname;
|
||||
const urlQuery = searchTicketsUtils.getFiltersForURL({
|
||||
filters: filtersForAPI,
|
||||
@ -63,7 +64,7 @@ export default {
|
||||
}
|
||||
},
|
||||
changeOrderBy(filtersWithOrderBy) {
|
||||
const filtersForAPI = searchTicketsUtils.prepareFiltersForAPI(filtersWithOrderBy);
|
||||
const filtersForAPI = searchTicketsUtils.getFiltersForAPI(filtersWithOrderBy);
|
||||
const currentPath = window.location.pathname;
|
||||
const urlQuery = searchTicketsUtils.getFiltersForURL({
|
||||
filters: filtersForAPI,
|
||||
|
@ -54,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;
|
||||
})
|
||||
@ -101,7 +101,7 @@ export default {
|
||||
};
|
||||
},
|
||||
|
||||
initSession() {
|
||||
checkSession() {
|
||||
return {
|
||||
type: 'CHECK_SESSION',
|
||||
payload: new Promise((resolve, reject) => {
|
||||
|
@ -57,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);
|
||||
|
@ -5,10 +5,8 @@ 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 = {
|
||||
description: React.PropTypes.node,
|
||||
@ -31,8 +29,8 @@ class AreYouSure extends React.Component {
|
||||
|
||||
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}}
|
||||
);
|
||||
}
|
||||
|
||||
@ -42,18 +40,17 @@ 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">
|
||||
@ -68,9 +65,8 @@ class AreYouSure extends React.Component {
|
||||
onClick={this.onYes.bind(this)}
|
||||
ref="yesButton"
|
||||
tabIndex="2"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? <Loading /> : i18n('YES')}
|
||||
disabled={loading}>
|
||||
{loading ? <Loading /> : i18n('YES')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -79,10 +75,8 @@ class AreYouSure extends React.Component {
|
||||
}
|
||||
|
||||
renderPassword() {
|
||||
const {
|
||||
password,
|
||||
loading
|
||||
} = this.state;
|
||||
const { password, loading } = this.state;
|
||||
|
||||
return (
|
||||
<Input
|
||||
className="are-you-sure__password"
|
||||
@ -94,8 +88,7 @@ class AreYouSure extends React.Component {
|
||||
value={password}
|
||||
onChange={this.onPasswordChange.bind(this)}
|
||||
onKeyDown={this.onInputKeyDown.bind(this)}
|
||||
disabled={loading}
|
||||
/>
|
||||
disabled={loading} />
|
||||
);
|
||||
}
|
||||
|
||||
@ -112,13 +105,8 @@ class AreYouSure extends React.Component {
|
||||
}
|
||||
|
||||
onYes() {
|
||||
const {
|
||||
password,
|
||||
} = this.state;
|
||||
const {
|
||||
type,
|
||||
onYes
|
||||
} = this.props;
|
||||
const { password } = this.state;
|
||||
const { type, onYes } = this.props;
|
||||
|
||||
if(type === 'secure' && !password) {
|
||||
this.refs.password.focus()
|
||||
@ -166,10 +154,10 @@ class AreYouSure extends React.Component {
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
if (this.context.closeModal) {
|
||||
this.context.closeModal();
|
||||
}
|
||||
const { closeModal } = this.context;
|
||||
|
||||
closeModal && closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
export default AreYouSure;
|
||||
export default AreYouSure;
|
||||
|
@ -32,11 +32,11 @@ class ArticleAddModal extends React.Component {
|
||||
<Form onSubmit={this.onAddNewArticleFormSubmit.bind(this)} loading={this.state.loading}>
|
||||
<FormField name="title" label={i18n('TITLE')} field="input" fieldProps={{size: 'large'}} validation="TITLE" required/>
|
||||
<FormField name="content" label={i18n('CONTENT')} field="textarea" validation="TEXT_AREA" required fieldProps={{allowImages: this.props.allowAttachments}}/>
|
||||
<SubmitButton type="secondary">{i18n('ADD_ARTICLE')}</SubmitButton>
|
||||
<Button className="article-add-modal__cancel-button" type="link" onClick={(event) => {
|
||||
event.preventDefault();
|
||||
ModalContainer.closeModal();
|
||||
}}>{i18n('CANCEL')}</Button>
|
||||
<SubmitButton className="article-add-modal__submit-button" type="secondary">{i18n('ADD_ARTICLE')}</SubmitButton>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
@ -2,7 +2,11 @@
|
||||
width: 800px;
|
||||
|
||||
&__cancel-button {
|
||||
float: right;
|
||||
float: left;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
&__submit-button {
|
||||
float: right;
|
||||
}
|
||||
}
|
@ -36,11 +36,13 @@ class ArticlesList extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
if(this.props.errored) {
|
||||
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 className="articles-list__loading" backgrounded size="large"/> : 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.map((topic) => {return {...topic, private: topic.private === "1"}}),
|
||||
errored: store.articles.errored,
|
||||
loading: store.articles.loading
|
||||
topics: topics.map((topic) => {return {...topic, private: topic.private === "1"}}),
|
||||
errored,
|
||||
loading
|
||||
};
|
||||
})(ArticlesList);
|
||||
|
@ -1,14 +1,19 @@
|
||||
@import "../scss/vars";
|
||||
|
||||
.articles-list {
|
||||
|
||||
&__add {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__add-icon {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
margin-top: -4px;
|
||||
&__loading {
|
||||
min-width: 300px;
|
||||
min-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
&__add-topic-button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,4 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import DropDown from 'core-components/drop-down';
|
||||
import Icon from 'core-components/icon';
|
||||
|
@ -3,7 +3,6 @@ import {connect} from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import languageList from 'data/language-list';
|
||||
import i18n from 'lib-app/i18n';
|
||||
import DropDown from 'core-components/drop-down';
|
||||
|
||||
const languageCodes = Object.keys(languageList);
|
||||
|
@ -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);
|
||||
|
41
client/src/app-components/page-size-dropdown.js
Normal file
41
client/src/app-components/page-size-dropdown.js
Normal file
@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
|
||||
import DropDown from 'core-components/drop-down';
|
||||
import i18n from 'lib-app/i18n';
|
||||
|
||||
class PageSizeDropdown extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.number,
|
||||
onChange: React.PropTypes.func,
|
||||
pages: React.PropTypes.array
|
||||
}
|
||||
|
||||
state = {
|
||||
selectedIndex: 1
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<DropDown {...this.props} onChange={this.onChange.bind(this)} items={this.getPages()} selectedIndex={this.state.selectedIndex} />
|
||||
)
|
||||
}
|
||||
|
||||
getPages() {
|
||||
return this.props.pages.map((page) => {
|
||||
return {content: `${page} / ${i18n('TICKETS')}`}
|
||||
});
|
||||
}
|
||||
|
||||
onChange(event) {
|
||||
this.setState({
|
||||
selectedIndex: event.index
|
||||
})
|
||||
if(this.props.onChange) {
|
||||
this.props.onChange({
|
||||
pageSize: this.props.pages[event.index]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default PageSizeDropdown;
|
@ -24,13 +24,19 @@ 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,
|
||||
style
|
||||
} = this.props;
|
||||
const { renderLogo, formProps, onBackToLoginClick, style } = this.props;
|
||||
|
||||
return (
|
||||
<Widget style={style} className={this.getClass()} title={!renderLogo ? i18n('RECOVER_PASSWORD') : ''}>
|
||||
{this.renderLogo()}
|
||||
@ -68,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;
|
||||
|
@ -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;
|
||||
|
19
client/src/app-components/session-expired-modal.js
Normal file
19
client/src/app-components/session-expired-modal.js
Normal file
@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
|
||||
import i18n from "lib-app/i18n";
|
||||
|
||||
import Header from "core-components/header";
|
||||
|
||||
class SessionExpiredModal extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Header
|
||||
title={i18n("SESSION_EXPIRED")}
|
||||
description={i18n("SESSION_EXPIRED_DESCRIPTION")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SessionExpiredModal;
|
42
client/src/app-components/stat-card.js
Normal file
42
client/src/app-components/stat-card.js
Normal file
@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
|
||||
import Tooltip from 'core-components/tooltip';
|
||||
|
||||
class StatCard extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
label: React.PropTypes.string,
|
||||
description: React.PropTypes.string,
|
||||
value: React.PropTypes.number,
|
||||
isPercentage: React.PropTypes.bool
|
||||
};
|
||||
|
||||
state = {
|
||||
showTooltip: false
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
label,
|
||||
description,
|
||||
value,
|
||||
isPercentage
|
||||
} = this.props;
|
||||
|
||||
const displayValue = isNaN(value) ? "-" : (isPercentage ? value.toFixed(2) : value);
|
||||
return (
|
||||
<Tooltip content={description} show={this.state.showTooltip} >
|
||||
<div className="stat-card" onMouseEnter={() => this.setState({showTooltip: true})} onMouseLeave={() => this.setState({showTooltip: false})}>
|
||||
<div className="stat-card__wrapper">
|
||||
{label}
|
||||
<div className="stat-card__container">
|
||||
{displayValue}{isPercentage && !isNaN(value) ? "%" : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default StatCard;
|
22
client/src/app-components/stat-card.scss
Normal file
22
client/src/app-components/stat-card.scss
Normal file
@ -0,0 +1,22 @@
|
||||
@import "../scss/vars";
|
||||
|
||||
.stat-card {
|
||||
margin: 0 4px;
|
||||
|
||||
&__wrapper {
|
||||
padding: 10px;
|
||||
min-width: 160px;
|
||||
margin: 8px auto;
|
||||
transition: 0.3s;
|
||||
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
|
||||
text-align: center;
|
||||
&:hover {
|
||||
box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
|
||||
}
|
||||
}
|
||||
|
||||
&__container {
|
||||
font-size: $font-size--lg;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Line} from 'react-chartjs-2';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
|
||||
class StatsChart extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
strokes: React.PropTypes.arrayOf(React.PropTypes.shape({
|
||||
name: React.PropTypes.string,
|
||||
values: React.PropTypes.arrayOf(React.PropTypes.shape({
|
||||
date: React.PropTypes.string,
|
||||
value: React.PropTypes.number
|
||||
}))
|
||||
})),
|
||||
period: React.PropTypes.number
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Line data={this.getChartData()} options={this.getChartOptions()} width={800} height={400} redraw/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getChartData() {
|
||||
let labels = this.getLabels();
|
||||
|
||||
let color = {
|
||||
'CLOSE': 'rgba(150, 20, 20, 0.6)',
|
||||
'CREATE_TICKET': 'rgba(20, 150, 20, 0.6)',
|
||||
'SIGNUP': 'rgba(20, 20, 150, 0.6)',
|
||||
'COMMENT': 'rgba(20, 200, 200, 0.6)',
|
||||
'ASSIGN': 'rgba(20, 150, 20, 0.6)'
|
||||
};
|
||||
|
||||
let strokes = this.props.strokes.slice();
|
||||
let datasets = strokes.map((stroke, index) => {
|
||||
return {
|
||||
label: i18n('CHART_' + stroke.name),
|
||||
data: stroke.values.map((val) => val.value),
|
||||
fill: false,
|
||||
borderWidth: this.getBorderWidth(),
|
||||
borderColor: color[stroke.name],
|
||||
pointBorderColor: color[stroke.name],
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 3,
|
||||
lineTension: 0.2,
|
||||
pointHoverBackgroundColor: color[stroke.name],
|
||||
hitRadius: this.hitRadius(index)
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
labels: labels,
|
||||
datasets: datasets
|
||||
};
|
||||
}
|
||||
|
||||
getBorderWidth() {
|
||||
return (this.props.period <= 90) ? 3 : 2;
|
||||
}
|
||||
|
||||
getLabels() {
|
||||
let months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
let labels = [];
|
||||
|
||||
if (!this.props.strokes.length) {
|
||||
labels = Array.from('x'.repeat(this.props.period));
|
||||
}
|
||||
else {
|
||||
labels = this.props.strokes[0].values.map((item) => {
|
||||
let idx = item.date.slice(4, 6) - 1;
|
||||
|
||||
return item.date.slice(6, 8) + ' ' + months[idx];
|
||||
});
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
hitRadius(index) {
|
||||
if (this.props.period <= 7) return 20;
|
||||
if (this.props.period <= 30) return 15;
|
||||
if (this.props.period <= 90) return 10;
|
||||
return 3;
|
||||
}
|
||||
|
||||
getChartOptions() {
|
||||
return {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default StatsChart;
|
@ -1,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;
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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';
|
||||
@ -12,6 +13,8 @@ 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([
|
||||
@ -28,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 = {
|
||||
@ -89,32 +94,58 @@ class TicketEvent extends React.Component {
|
||||
}
|
||||
|
||||
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 ql-editor">
|
||||
<div dangerouslySetInnerHTML={{__html: this.props.content}}></div>
|
||||
{((this.props.author.id == this.props.userId && this.props.author.staff == this.props.userStaff) || this.props.userStaff) ? this.renderEditIcon() : null}
|
||||
<div className="ticket-event__comment-content ql-editor">
|
||||
<div dangerouslySetInnerHTML={{__html: content}}></div>
|
||||
{(id == userId && staff == userStaff && isLastComment && !isTicketClosed) ? this.renderEditIcon() : null }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -306,4 +337,7 @@ class TicketEvent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default TicketEvent;
|
||||
export default connect((store) => {
|
||||
return { level: store.session.userLevel };
|
||||
})(TicketEvent);
|
||||
|
||||
|
@ -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';
|
||||
@ -14,6 +15,8 @@ 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';
|
||||
import PageSizeDropdown from './page-size-dropdown';
|
||||
|
||||
class TicketList extends React.Component {
|
||||
static propTypes = {
|
||||
@ -30,6 +33,7 @@ class TicketList extends React.Component {
|
||||
closedTicketsShown: React.PropTypes.bool,
|
||||
onClosedTicketsShownChange: React.PropTypes.func,
|
||||
onDepartmentChange: React.PropTypes.func,
|
||||
showPageSizeDropdown: React.PropTypes.bool
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -39,7 +43,8 @@ class TicketList extends React.Component {
|
||||
departments: [],
|
||||
ticketPath: '/dashboard/ticket/',
|
||||
type: 'primary',
|
||||
closedTicketsShown: false
|
||||
closedTicketsShown: false,
|
||||
showPageSizeDropdown: true
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -47,23 +52,32 @@ class TicketList extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { type, showDepartmentDropdown, onClosedTicketsShownChange, showPageSizeDropdown } = this.props;
|
||||
const pages = [5, 10, 20, 50];
|
||||
|
||||
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() :
|
||||
<div className="ticket-list__main-filters">
|
||||
{(type === 'primary') ? this.renderMessage() : null}
|
||||
{
|
||||
((type === 'secondary') && showDepartmentDropdown) ?
|
||||
this.renderDepartmentsDropDown() :
|
||||
null
|
||||
}
|
||||
{onClosedTicketsShownChange ? this.renderFilterCheckbox() : null}
|
||||
</div>
|
||||
{
|
||||
showPageSizeDropdown ?
|
||||
<PageSizeDropdown className="ticket-list__page-dropdown" pages={pages} onChange={(event) => this.pageSizeChange(event)} /> :
|
||||
null
|
||||
}
|
||||
{this.props.onClosedTicketsShownChange ? this.renderFilterCheckbox() : null}
|
||||
</div>
|
||||
<Table {...this.getTableProps()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
renderFilterCheckbox() {
|
||||
return (
|
||||
<Checkbox
|
||||
@ -85,45 +99,62 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
pageSizeChange(event) {
|
||||
const { onPageSizeChange } = this.props;
|
||||
|
||||
onPageSizeChange && onPageSizeChange(event.pageSize);
|
||||
}
|
||||
|
||||
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;
|
||||
const { loading, page, pages, onPageChange } = this.props;
|
||||
|
||||
return {
|
||||
loading,
|
||||
headers: this.getTableHeaders(),
|
||||
rows: this.getTableRows(),
|
||||
pageSize: 10,
|
||||
pageSize: this.state.tickets,
|
||||
page,
|
||||
pages,
|
||||
onPageChange
|
||||
@ -140,22 +171,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',
|
||||
@ -181,7 +200,7 @@ class TicketList extends React.Component {
|
||||
className: 'ticket-list__date col-md-2'
|
||||
}
|
||||
];
|
||||
} else if (this.props.type == 'secondary') {
|
||||
} else if(type == 'secondary') {
|
||||
return [
|
||||
{
|
||||
key: 'number',
|
||||
@ -216,91 +235,99 @@ class TicketList extends React.Component {
|
||||
}
|
||||
|
||||
renderSortArrow(header) {
|
||||
const {
|
||||
orderBy,
|
||||
showOrderArrows,
|
||||
onChangeOrderBy
|
||||
} = this.props;
|
||||
let arrowIcon;
|
||||
const { orderBy, showOrderArrows, onChangeOrderBy } = this.props;
|
||||
|
||||
if(showOrderArrows) {
|
||||
arrowIcon = (
|
||||
return (
|
||||
showOrderArrows ?
|
||||
<Icon
|
||||
name={`arrow-${this.getIconName(header, orderBy)}`}
|
||||
className="ticket-list__order-icon"
|
||||
color={this.getIconColor(header, orderBy)}
|
||||
onClick={() => onChangeOrderBy(header)}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
arrowIcon = null;
|
||||
}
|
||||
|
||||
return arrowIcon;
|
||||
onClick={() => onChangeOrderBy(header)} /> :
|
||||
null
|
||||
);
|
||||
}
|
||||
getIconName(header, orderBy) {
|
||||
let name = (orderBy && orderBy.value === header && orderBy.asc) ? "up" : "down";
|
||||
|
||||
return name;
|
||||
getIconName(header, orderBy) {
|
||||
return (orderBy && orderBy.value === header && orderBy.asc) ? "up" : "down";
|
||||
}
|
||||
|
||||
getIconColor(header, orderBy) {
|
||||
let color = (orderBy && orderBy.value === header) ? "gray" : "white";
|
||||
|
||||
return color;
|
||||
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>
|
||||
{ticket.closed ? <Icon size="sm" name="lock" /> : null}
|
||||
<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>
|
||||
|
||||
),
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
||||
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) => {
|
||||
|
@ -7,16 +7,31 @@
|
||||
}
|
||||
|
||||
&__filters {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&__main-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__department-selector {
|
||||
display: inline-block;
|
||||
margin-right: 25px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__page-dropdown {
|
||||
display: inline-block;
|
||||
margin-right: 25px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__checkbox {
|
||||
display: inline-block;
|
||||
}
|
||||
@ -48,3 +63,7 @@
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.create-ticket-form__message {
|
||||
width: 100%;
|
||||
}
|
@ -8,8 +8,7 @@ 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 DateTransformer from 'lib-core/date-transformer';
|
||||
import ticketUtils from 'lib-app/ticket-utils';
|
||||
|
||||
import Form from 'core-components/form';
|
||||
import SubmitButton from 'core-components/submit-button';
|
||||
@ -19,10 +18,6 @@ import Button from 'core-components/button';
|
||||
import Loading from 'core-components/loading';
|
||||
|
||||
|
||||
const INITIAL_PAGE = 1;
|
||||
|
||||
const DEFAULT_START_DATE = 20170101;
|
||||
|
||||
class TicketQueryFilters extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
@ -40,7 +35,8 @@ class TicketQueryFilters extends React.Component {
|
||||
formState,
|
||||
filters,
|
||||
showFilters,
|
||||
ticketQueryListState
|
||||
ticketQueryListState,
|
||||
staffList
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@ -53,57 +49,52 @@ class TicketQueryFilters extends React.Component {
|
||||
<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__row">
|
||||
<div className="ticket-query-filters__row__filter">
|
||||
<span>{i18n('DATE')}</span>
|
||||
<FormField
|
||||
name="dateRange"
|
||||
field="date-range"
|
||||
fieldProps={{defaultValue: this.dateRangeToFormValue(filters.dateRange)}} />
|
||||
</div>
|
||||
<div className="ticket-query-filters__row__filter">
|
||||
<span>{i18n('STATUS')}</span>
|
||||
<FormField name="closed" field="select" fieldProps={{items: this.getStatusItems()}} />
|
||||
</div>
|
||||
<div className="ticket-query-filters__second-row">
|
||||
<FormField
|
||||
label={i18n('PERIOD')}
|
||||
name="period"
|
||||
field="select"
|
||||
fieldProps={{
|
||||
items: [{content: i18n('LAST_7_DAYS')}, {content: i18n('LAST_30_DAYS')}, {content: i18n('LAST_90_DAYS')}, {content: i18n('LAST_365_DAYS')}],
|
||||
className: 'ticket-query-filters__drop-down'
|
||||
}} />
|
||||
<FormField
|
||||
label={i18n('DEPARTMENTS')}
|
||||
name="departments"
|
||||
field="autocomplete"
|
||||
fieldProps={{items: this.getDepartmentsItems()}} />
|
||||
<FormField
|
||||
label={i18n('OWNER')}
|
||||
name="owners"
|
||||
field="autocomplete"
|
||||
fieldProps={{items: ticketUtils.getStaffList({staffList}, 'toAutocomplete')}} />
|
||||
</div>
|
||||
<div className="ticket-query-filters__row">
|
||||
<div className="ticket-query-filters__row__filter">
|
||||
<span className="ticket-query-filters__title">{i18n('DEPARTMENTS')}</span>
|
||||
<FormField
|
||||
name="departments"
|
||||
field="autocomplete"
|
||||
fieldProps={{items: this.getDepartmentsItems()}} />
|
||||
</div>
|
||||
<div className="ticket-query-filters__row__filter">
|
||||
<span className="ticket-query-filters__title">{i18n('OWNER')}</span>
|
||||
<FormField
|
||||
name="owners"
|
||||
field="autocomplete"
|
||||
fieldProps={{items: this.getStaffList()}} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="ticket-query-filters__row">
|
||||
<div className="ticket-query-filters__row__filter">
|
||||
<span className="ticket-query-filters__title">{i18n('TAGS')}</span>
|
||||
<FormField
|
||||
name="tags"
|
||||
field="tag-selector"
|
||||
fieldProps={{
|
||||
items: this.getTags(filters.tags),
|
||||
onRemoveClick: this.removeTag.bind(this),
|
||||
onTagSelected: this.addTag.bind(this)
|
||||
}} />
|
||||
</div>
|
||||
<div className="ticket-query-filters__row__filter">
|
||||
<span className="ticket-query-filters__title">{i18n('AUTHORS')}</span>
|
||||
<FormField
|
||||
name="authors"
|
||||
field="autocomplete"
|
||||
fieldProps={{
|
||||
getItemListFromQuery: this.searchAuthors.bind(this),
|
||||
comparerFunction: this.autorsComparer.bind(this)
|
||||
}} />
|
||||
</div>
|
||||
<div className="ticket-query-filters__third-row">
|
||||
<FormField
|
||||
label={i18n('STATUS')}
|
||||
name="closed"
|
||||
field="select"
|
||||
fieldProps={{
|
||||
items: this.getStatusItems(),
|
||||
className: 'ticket-query-filters__drop-down'
|
||||
}} />
|
||||
<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
|
||||
@ -111,9 +102,7 @@ class TicketQueryFilters extends React.Component {
|
||||
size= "medium"
|
||||
disabled={ticketQueryListState.loading}
|
||||
onClick={this.clearFormValues.bind(this)}>
|
||||
{ticketQueryListState.loading ?
|
||||
<Loading />
|
||||
: i18n('CLEAR')}
|
||||
{ticketQueryListState.loading ? <Loading /> : i18n('CLEAR')}
|
||||
</Button>
|
||||
<SubmitButton
|
||||
className="ticket-query-filters__container__button ticket-query-filters__container__search-button"
|
||||
@ -129,6 +118,8 @@ class TicketQueryFilters extends React.Component {
|
||||
}
|
||||
|
||||
searchAuthors(query, blacklist = []) {
|
||||
blacklist = blacklist.map(item => {return {isStaff: item.isStaff, id: item.id}});
|
||||
|
||||
return API.call({
|
||||
path: '/ticket/search-authors',
|
||||
data: {
|
||||
@ -143,8 +134,8 @@ class TicketQueryFilters extends React.Component {
|
||||
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
|
||||
content: author.profilePic !== undefined ? ticketUtils.renderStaffOption(author) : author.name,
|
||||
contentOnSelected: author.profilePic !== undefined ? ticketUtils.renderStaffSelected(author) : author.name
|
||||
}});
|
||||
});
|
||||
}
|
||||
@ -171,29 +162,9 @@ class TicketQueryFilters extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderStaffOption(staff) {
|
||||
return (
|
||||
<div className="ticket-query-filters__staff-option" key={`staff-option-${staff.id}`}>
|
||||
<img className="ticket-query-filters__staff-option__profile-pic" src={this.getStaffProfilePic(staff)}/>
|
||||
<span className="ticket-query-filters__staff-option__name">{staff.name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderStaffSelected(staff) {
|
||||
return (
|
||||
<div className="ticket-query-filters__staff-selected" key={`staff-selected-${staff.id}`}>
|
||||
<img className="ticket-query-filters__staff-selected__profile-pic" src={this.getStaffProfilePic(staff)}/>
|
||||
<span className="ticket-query-filters__staff-selected__name">{staff.name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
addTag(tag) {
|
||||
const { formState } = this.props;
|
||||
let selectedTagsId = formState.tags.concat(this.tagsNametoTagsId(this.getSelectedTagsName([tag])));
|
||||
|
||||
this.onChangeFormState({...formState, tags: selectedTagsId});
|
||||
this.onChangeFormState({...formState, tags: [...formState.tags, tag]});
|
||||
}
|
||||
|
||||
autorsComparer(autorList, autorSelectedList) {
|
||||
@ -205,16 +176,6 @@ class TicketQueryFilters extends React.Component {
|
||||
this.props.dispatch(SearchFiltersActions.setDefaultFormValues());
|
||||
}
|
||||
|
||||
dateRangeToFormValue(_dateRange) {
|
||||
const dateRange = JSON.parse(_dateRange);
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
startDate: dateRange[0]/10000,
|
||||
endDate: (dateRange[1]-2400)/10000,
|
||||
};
|
||||
}
|
||||
|
||||
getDepartmentsItems() {
|
||||
const { departments, } = this.props;
|
||||
let departmentsList = departments.map(department => {
|
||||
@ -234,18 +195,23 @@ class TicketQueryFilters extends React.Component {
|
||||
let selectedDepartments = [];
|
||||
|
||||
if(selectedDepartmentsId !== undefined) {
|
||||
let departments = this.getDepartmentsItems();
|
||||
selectedDepartments = departments.filter(item => _.includes(selectedDepartmentsId, item.id));
|
||||
selectedDepartments = selectedDepartmentsId.map(
|
||||
(departmentId) => this.getDepartmentsItems().find(_department => (_department.id === departmentId))
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
return selectedDepartments;
|
||||
}
|
||||
|
||||
getSelectedStaffs(selectedStaffsId) {
|
||||
const { staffList } = this.props;
|
||||
let selectedStaffs = [];
|
||||
|
||||
if(selectedStaffsId !== undefined) {
|
||||
let staffs = this.getStaffList();
|
||||
selectedStaffs = staffs.filter(staff => _.includes(selectedStaffsId, staff.id));
|
||||
selectedStaffs = selectedStaffsId.map(
|
||||
(staffId) => ticketUtils.getStaffList({staffList}, 'toAutocomplete').find(_staff => (_staff.id === staffId))
|
||||
);
|
||||
}
|
||||
|
||||
return selectedStaffs;
|
||||
@ -255,33 +221,14 @@ class TicketQueryFilters extends React.Component {
|
||||
let selectedTagsName = [];
|
||||
|
||||
if(selectedTagsId !== undefined) {
|
||||
let tagList = this.getTags();
|
||||
let selectedTags = tagList.filter(item => _.includes(selectedTagsId, item.id));
|
||||
selectedTagsName = selectedTags.map(tag => tag.name);
|
||||
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')},
|
||||
@ -319,8 +266,9 @@ class TicketQueryFilters extends React.Component {
|
||||
{...formState, orderBy: filters.orderBy, page: 1},
|
||||
true
|
||||
);
|
||||
|
||||
if(formEdited) {
|
||||
const filtersForAPI = searchTicketsUtils.prepareFiltersForAPI(listConfigWithCompleteAuthorsList.filters);
|
||||
const filtersForAPI = searchTicketsUtils.getFiltersForAPI(listConfigWithCompleteAuthorsList.filters);
|
||||
const currentPath = window.location.pathname;
|
||||
const urlQuery = searchTicketsUtils.getFiltersForURL({
|
||||
filters: filtersForAPI,
|
||||
@ -333,41 +281,35 @@ class TicketQueryFilters extends React.Component {
|
||||
|
||||
removeTag(tag) {
|
||||
const { formState } = this.props;
|
||||
let tagListName = formState.tags;
|
||||
let newTagList = tagListName.filter(item => item !== tag);
|
||||
let selectedTags = this.tagsNametoTagsId(this.getSelectedTagsName(newTagList));
|
||||
|
||||
this.onChangeFormState({...formState, tags: selectedTags});
|
||||
this.onChangeFormState({...formState, tags: formState.tags.filter(item => item !== tag)});
|
||||
}
|
||||
|
||||
tagsNametoTagsId(selectedTagsName) {
|
||||
let tagList = this.getTags();
|
||||
let selectedTags = tagList.filter(item => _.includes(selectedTagsName, item.name));
|
||||
let selectedTagsId = selectedTags.map(tag => tag.id);
|
||||
let selectedTagsId = [];
|
||||
|
||||
if (selectedTagsName != undefined) {
|
||||
selectedTagsId = selectedTagsName.map(
|
||||
(tagName) => (this.getTags().find(_tag => (_tag.name === tagName)) || {}).id
|
||||
);
|
||||
}
|
||||
|
||||
return selectedTagsId;
|
||||
}
|
||||
|
||||
onChangeForm(data) {
|
||||
let newStartDate = data.dateRange.startDate === "" ? DEFAULT_START_DATE : data.dateRange.startDate;
|
||||
let newEndDate = data.dateRange.endDate === "" ? DateTransformer.getDateToday() : data.dateRange.endDate;
|
||||
let departmentsId = data.departments.map(department => department.id);
|
||||
let staffsId = data.owners.map(staff => staff.id);
|
||||
let tagsName = this.tagsNametoTagsId(data.tags);
|
||||
let authors = data.authors.map(({name, id, isStaff, profilePic, color}) => ({name, id: id*1, isStaff, profilePic, color}));
|
||||
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
|
||||
}
|
||||
});
|
||||
this.onChangeFormState({
|
||||
...data,
|
||||
tags: tagsName,
|
||||
owners: staffsId,
|
||||
departments: departmentsId,
|
||||
authors: authors,
|
||||
});
|
||||
}
|
||||
|
||||
getFormValue(form) {
|
||||
@ -387,8 +329,8 @@ class TicketQueryFilters extends React.Component {
|
||||
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
|
||||
content: author.profilePic !== undefined ? ticketUtils.renderStaffOption(author) : author.name,
|
||||
contentOnSelected: author.profilePic !== undefined ? ticketUtils.renderStaffSelected(author) : author.name
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
&__button {
|
||||
margin: 0 10px;
|
||||
}
|
||||
@ -41,25 +42,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__row {
|
||||
&__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;
|
||||
}
|
||||
|
||||
&__filter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
margin-bottom: 30px;
|
||||
&__drop-down > .drop-down__current-item {
|
||||
background-color: $very-light-grey;
|
||||
|
||||
&:focus {
|
||||
background-color: $medium-grey;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&__search-box {
|
||||
width: 100%;
|
||||
padding: 0 50px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
&__staff-option {
|
||||
@ -94,3 +103,16 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ 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';
|
||||
import searchTicketsUtils from 'lib-app/search-tickets-utils';
|
||||
import history from 'lib-app/history';
|
||||
|
||||
class TicketQueryList extends React.Component {
|
||||
|
||||
@ -21,13 +23,9 @@ class TicketQueryList extends React.Component {
|
||||
|
||||
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()} />
|
||||
);
|
||||
}
|
||||
|
||||
@ -69,6 +67,7 @@ class TicketQueryList extends React.Component {
|
||||
orderBy: filters.orderBy ? JSON.parse(filters.orderBy) : filters.orderBy,
|
||||
showOrderArrows: true,
|
||||
onChangeOrderBy: onChangeOrderBy,
|
||||
showPageSizeDropdown: false
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,8 @@ 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 ticketUtils from 'lib-app/ticket-utils';
|
||||
|
||||
import TicketEvent from 'app-components/ticket-event';
|
||||
import AreYouSure from 'app-components/are-you-sure';
|
||||
@ -66,45 +68,78 @@ class TicketViewer extends React.Component {
|
||||
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;
|
||||
@ -114,7 +149,7 @@ class TicketViewer extends React.Component {
|
||||
<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 }
|
||||
@ -122,20 +157,6 @@ class TicketViewer extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
renderEditedTitleText(){
|
||||
return(
|
||||
<div className="ticket-viewer__edited-title-text"> {i18n('TITLE_EDITED')} </div>
|
||||
)
|
||||
}
|
||||
|
||||
renderEditTitleOption() {
|
||||
return(
|
||||
<span className="ticket-viewer__edit-title-icon">
|
||||
<Icon name="pencil" onClick={() => this.setState({editTitle: true})} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
renderEditableTitle(){
|
||||
return(
|
||||
<div className="ticket-viewer__header">
|
||||
@ -148,11 +169,11 @@ class TicketViewer extends React.Component {
|
||||
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 className="ticket-viewer__edit-title__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" size="large" />}
|
||||
</Button>
|
||||
<Button disabled={this.state.editTitleLoading} type='secondary' size="medium" onClick={this.changeTitle.bind(this)}>
|
||||
{this.state.editTitleLoading ? <Loading /> : <Icon name="check" />}
|
||||
<Button className="ticket-viewer__edit-title__button" disabled={this.state.editTitleLoading} type='secondary' size="medium" onClick={this.changeTitle.bind(this)}>
|
||||
{this.state.editTitleLoading ? <Loading /> : <Icon name="check" size="large" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -160,59 +181,113 @@ class TicketViewer extends React.Component {
|
||||
}
|
||||
|
||||
renderEditableHeaders() {
|
||||
const ticket = this.props.ticket;
|
||||
const departments = this.getDepartmentsForTransfer();
|
||||
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-header">{i18n('DEPARTMENT')}</div>
|
||||
<div className="ticket-viewer__info-value">
|
||||
<DepartmentDropdown
|
||||
className="ticket-viewer__editable-dropdown"
|
||||
departments={departments}
|
||||
selectedIndex={_.findIndex(departments, {id: this.props.ticket.department.id})}
|
||||
onChange={this.onDepartmentDropdownChanged.bind(this)} />
|
||||
<div className="ticket-viewer__info-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-header">{i18n('TAGS')}</div>
|
||||
<div className="ticket-viewer__info-value">
|
||||
<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 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-header">{i18n('OWNER')}</div>
|
||||
<div className="ticket-viewer__info-value">
|
||||
{this.renderAssignStaffList()}
|
||||
<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-header">{i18n('AUTHOR')}</div>
|
||||
<div className="ticket-viewer__info-value">{ticket.author.name}</div>
|
||||
<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-header">{i18n('STATUS')}</div>
|
||||
<div className="ticket-viewer__info-container-editable__column">
|
||||
<div className="ticket-viewer__info-header">{i18n('STATUS')}</div>
|
||||
<div className="ticket-viewer__info-value">
|
||||
{ticket.closed ?
|
||||
<Button type='secondary' size="extra-small" onClick={this.onReopenClick.bind(this)}>
|
||||
{i18n('RE_OPEN')}
|
||||
</Button> : i18n('OPENED')}
|
||||
{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", "CLOSE")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderEditStatus() {
|
||||
return (
|
||||
<div className="ticket-viewer__edit-status__buttons">
|
||||
{this.renderCancelButton("Status", "CANCEL")}
|
||||
{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;
|
||||
|
||||
@ -229,10 +304,9 @@ class TicketViewer extends React.Component {
|
||||
</div>
|
||||
<div className="ticket-viewer__info-container">
|
||||
<div className="ticket-viewer__info-header">{i18n('TAGS')}</div>
|
||||
<div className="ticket-viewer__info-value">{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 className="ticket-viewer__info-value">
|
||||
{this.renderTags()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ticket-viewer__info">
|
||||
@ -245,7 +319,7 @@ class TicketViewer extends React.Component {
|
||||
<div className="ticket-viewer__info-container">
|
||||
<div className="ticket-viewer__info-header">{i18n('STATUS')}</div>
|
||||
<div className="ticket-viewer__info-value">
|
||||
{i18n((this.props.ticket.closed) ? 'CLOSED' : 'OPENED')}
|
||||
{i18n((ticket.closed) ? 'CLOSED' : 'OPENED')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -253,53 +327,130 @@ class TicketViewer extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderOwnerNode() {
|
||||
let ownerNode = null;
|
||||
renderTags() {
|
||||
const { ticket, tags } = this.props;
|
||||
const TAGS = (
|
||||
ticket.tags.length ?
|
||||
ticket.tags.map((tagName, index) => {
|
||||
const tag = _.find(tags, {name: tagName});
|
||||
return <Tag name={tag && tag.name} color={tag && tag.color} key={index} />
|
||||
}) :
|
||||
i18n('NONE')
|
||||
);
|
||||
|
||||
if (this.props.assignmentAllowed) {
|
||||
ownerNode = this.renderAssignStaffList();
|
||||
} else {
|
||||
ownerNode = (this.props.ticket.owner) ? this.props.ticket.owner.name : i18n('NONE')
|
||||
}
|
||||
|
||||
return ownerNode;
|
||||
return TAGS;
|
||||
}
|
||||
|
||||
renderAssignStaffList() {
|
||||
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)}>
|
||||
{ticketUtils.renderStaffSelected(ticket.owner)}
|
||||
</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 ownerId = this.props.ticket.owner && this.props.ticket.owner.id*1;
|
||||
const { ticket } = this.props;
|
||||
const ownerId = ticket.owner && ticket.owner.id*1;
|
||||
let selectedIndex = _.findIndex(items, {id: ownerId});
|
||||
selectedIndex = (selectedIndex !== -1) ? selectedIndex : 0;
|
||||
|
||||
return (
|
||||
<DropDown
|
||||
className="ticket-viewer__editable-dropdown" items={items}
|
||||
selectedIndex={selectedIndex}
|
||||
onChange={this.onAssignmentChange.bind(this)}
|
||||
/>
|
||||
<div className="ticket-viewer__edit-owner">
|
||||
<DropDown
|
||||
className="ticket-viewer__editable-dropdown" items={items}
|
||||
selectedIndex={selectedIndex}
|
||||
onChange={this.onAssignmentChange.bind(this)} />
|
||||
{this.renderCancelButton("Owner", "CANCEL")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderTicketEvent(options, index) {
|
||||
if (this.props.userStaff && typeof options.content === 'string') {
|
||||
options.content = MentionsParser.parse(options.content);
|
||||
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", "CANCEL")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderEditTitleOption() {
|
||||
return(
|
||||
<span className="ticket-viewer__edit-title-icon">
|
||||
<Icon name="pencil" onClick={() => this.setState({editTitle: true})} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
renderEditOption(option) {
|
||||
return(
|
||||
<span className="ticket-viewer__edit-icon">
|
||||
<Icon name="pencil" onClick={() => this.setState({["edit"+option]: true})} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
renderEditedTitleText(){
|
||||
return(
|
||||
<div className="ticket-viewer__edited-title-text"> {i18n('TITLE_EDITED')} </div>
|
||||
)
|
||||
}
|
||||
|
||||
renderCancelButton(option, type) {
|
||||
return <Button type='link' size="medium" onClick={() => this.setState({["edit"+option]: false})}>{i18n(type)}</Button>
|
||||
}
|
||||
|
||||
renderTicketEvent(isTicketClosed, ticketEventObject, index) {
|
||||
const { userStaff, ticket, userId, allowAttachments } = this.props;
|
||||
const { edit, editId } = this.state;
|
||||
const { content, author, id} = ticketEventObject;
|
||||
|
||||
if(userStaff && typeof content === 'string') {
|
||||
ticketEventObject.content = MentionsParser.parse(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()}>
|
||||
@ -311,13 +462,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>
|
||||
@ -327,10 +481,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
|
||||
@ -343,7 +509,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>
|
||||
);
|
||||
}
|
||||
@ -352,10 +518,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>
|
||||
);
|
||||
@ -365,11 +531,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),
|
||||
@ -420,14 +601,26 @@ class TicketViewer extends React.Component {
|
||||
}));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
@ -488,16 +681,16 @@ class TicketViewer extends React.Component {
|
||||
}
|
||||
}).then((result) => {
|
||||
this.onTicketModification(result);
|
||||
history.push('/admin/panel/tickets/my-tickets/');
|
||||
history.push(history.goBack());
|
||||
});
|
||||
}
|
||||
|
||||
changeDepartment(departmentId) {
|
||||
const {
|
||||
userId,
|
||||
userDepartments,
|
||||
ticket
|
||||
} = this.props;
|
||||
const { userId, userDepartments, ticket } = this.props;
|
||||
|
||||
this.setState({
|
||||
editDepartment: false
|
||||
});
|
||||
|
||||
return API.call({
|
||||
path: '/ticket/change-department',
|
||||
@ -574,16 +767,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({
|
||||
@ -592,7 +785,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() {
|
||||
@ -606,10 +799,11 @@ class TicketViewer extends React.Component {
|
||||
this.onTicketModification();
|
||||
}
|
||||
|
||||
onFailCommentFail() {
|
||||
onEditCommentFail() {
|
||||
this.setState({
|
||||
loading: false,
|
||||
commentError: true
|
||||
commentError: true,
|
||||
showTicketCommentErrorMessage: true
|
||||
});
|
||||
}
|
||||
|
||||
@ -632,6 +826,7 @@ class TicketViewer extends React.Component {
|
||||
loading: false,
|
||||
commentValue: TextEditor.createEmpty(),
|
||||
commentError: false,
|
||||
commentFile: null,
|
||||
commentEdited: false
|
||||
});
|
||||
|
||||
@ -641,54 +836,61 @@ 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 { staffMembers, 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});
|
||||
}
|
||||
|
||||
staffAssignmentItems = staffAssignmentItems.concat(
|
||||
_.map(
|
||||
_.filter(staffMembers, ({id, departments}) => {
|
||||
return (id != userId) && _.some(departments, {id: ticketDepartmentId});
|
||||
}),
|
||||
({id, name}) => ({content: name, id: id*1})
|
||||
ticketUtils.getStaffList({staffList: staffMembers, ticket}, 'toDropDown').map(
|
||||
({id, content}) => ({content, id: id*1})
|
||||
)
|
||||
);
|
||||
|
||||
return staffAssignmentItems;
|
||||
}
|
||||
|
||||
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*1) {
|
||||
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) => {
|
||||
@ -700,6 +902,6 @@ export default connect((store) => {
|
||||
staffMembersLoaded: store.adminData.staffMembersLoaded,
|
||||
allowAttachments: store.config['allow-attachments'],
|
||||
userLevel: store.session.userLevel*1,
|
||||
tags: store.config['tags']
|
||||
tags: store.config['tags'].map((tag) => {return {...tag, id: tag.id*1}})
|
||||
};
|
||||
})(TicketViewer);
|
||||
|
@ -1,7 +1,6 @@
|
||||
@import "../scss/vars";
|
||||
|
||||
.ticket-viewer {
|
||||
|
||||
&__header {
|
||||
background-color: $primary-blue;
|
||||
border-top-right-radius: 4px;
|
||||
@ -20,21 +19,59 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__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: 0 10px;
|
||||
}
|
||||
|
||||
&__edit-title-icon {
|
||||
color: #414A59;
|
||||
&__edit-icon {
|
||||
right: 12px;
|
||||
margin: 0 10px;
|
||||
color: $light-grey;
|
||||
|
||||
&:hover {
|
||||
cursor:pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__edit-title-icon {
|
||||
color: $primary-blue;
|
||||
right: 12px;
|
||||
margin: 0 10px;
|
||||
|
||||
&:hover {
|
||||
cursor:pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__edit-status__buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
&___input-edit-title {
|
||||
color: black;
|
||||
align-items:center;
|
||||
@ -43,7 +80,10 @@
|
||||
margin-right: 6px;
|
||||
|
||||
.input__text {
|
||||
height: 25px;
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
padding-top: 12px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,6 +94,14 @@
|
||||
align-items: center;
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
&__edit-title__button {
|
||||
width: 50px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__number {
|
||||
color: white;
|
||||
margin-right: 30px;
|
||||
@ -74,7 +122,7 @@
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
background-color: $light-grey;
|
||||
padding: 0 15px 30px 15px;
|
||||
padding: 10px 15px 30px 15px;
|
||||
|
||||
&-container {
|
||||
display: flex;
|
||||
@ -87,10 +135,24 @@
|
||||
|
||||
&-container-editable {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
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 {
|
||||
@ -100,7 +162,7 @@
|
||||
&-value {
|
||||
color: $secondary-blue;
|
||||
padding-bottom: 10px;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,7 +182,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;
|
||||
|
||||
@ -158,15 +228,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
&-buttons {
|
||||
&-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
&-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
width: 50%;
|
||||
min-width: 50%;
|
||||
}
|
||||
|
||||
}
|
||||
&__delete-button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1151px) {
|
||||
|
@ -37,15 +37,16 @@ class TopicEditModal extends React.Component {
|
||||
<FormField name="title" label={i18n('TITLE')} fieldProps={{size: 'large'}} validation="TITLE" required />
|
||||
<FormField name="icon" className="topic-edit-modal__icon" label={i18n('ICON')} decorator={IconSelector} />
|
||||
<FormField name="color" className="topic-edit-modal__color" label={i18n('COLOR')} decorator={ColorSelector} />
|
||||
<FormField className="topic-edit-modal__private" label={i18n('PRIVATE')} name="private" field="checkbox"/>
|
||||
<FormField className="topic-edit-modal__private" label={i18n('PRIVATE')} name="private" field="checkbox" />
|
||||
<InfoTooltip className="topic-edit-modal__private" text={i18n('PRIVATE_TOPIC_DESCRIPTION')} />
|
||||
|
||||
<SubmitButton className="topic-edit-modal__save-button" type="secondary" size="small">
|
||||
{i18n('SAVE')}
|
||||
</SubmitButton>
|
||||
<Button className="topic-edit-modal__discard-button" onClick={this.onDiscardClick.bind(this)} size="small">
|
||||
{i18n('CANCEL')}
|
||||
</Button>
|
||||
<div className="topic-edit-modal__buttons-container">
|
||||
<Button className="topic-edit-modal__discard-button" onClick={this.onDiscardClick.bind(this)} size="small">
|
||||
{i18n('CANCEL')}
|
||||
</Button>
|
||||
<SubmitButton className="topic-edit-modal__save-button" type="secondary" size="small">
|
||||
{i18n('SAVE')}
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
@ -13,9 +13,19 @@
|
||||
|
||||
}
|
||||
|
||||
&__discard-button {
|
||||
float: right;
|
||||
&__buttons-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 0 0 0;
|
||||
}
|
||||
|
||||
&__discard-button {
|
||||
float: left;
|
||||
}
|
||||
|
||||
&__private {
|
||||
display: inline-block;
|
||||
padding-right: 10px;
|
||||
|
@ -124,13 +124,15 @@ class TopicViewer extends React.Component {
|
||||
}
|
||||
|
||||
renderEditModal() {
|
||||
let props = {
|
||||
topicId: this.props.id,
|
||||
onChange: this.props.onChange,
|
||||
const {id, onChange, name, icon, iconColor} = this.props;
|
||||
|
||||
const props = {
|
||||
topicId: id,
|
||||
onChange,
|
||||
defaultValues: {
|
||||
title: this.props.name,
|
||||
icon: this.props.icon,
|
||||
iconColor: this.props.iconColor,
|
||||
title: name,
|
||||
icon,
|
||||
color: iconColor,
|
||||
private: this.props.private * 1
|
||||
}
|
||||
};
|
||||
|
@ -61,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}>
|
||||
@ -98,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} />
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import _ from 'lodash';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import searchTicketsUtils from '../../../lib-app/search-tickets-utils';
|
||||
|
||||
import Menu from 'core-components/menu';
|
||||
import queryString from 'query-string';
|
||||
@ -135,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
|
||||
}
|
||||
])
|
||||
},
|
||||
|
@ -18,6 +18,7 @@ import Form from 'core-components/form';
|
||||
import FormField from 'core-components/form-field';
|
||||
import SubmitButton from 'core-components/submit-button';
|
||||
import TextEditor from 'core-components/text-editor';
|
||||
import Icon from 'core-components/icon';
|
||||
|
||||
class AdminPanelViewArticle extends React.Component {
|
||||
|
||||
@ -65,23 +66,22 @@ class AdminPanelViewArticle extends React.Component {
|
||||
renderArticlePreview(article) {
|
||||
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>
|
||||
<div className="admin-panel-view-article__header-wrapper">
|
||||
<Header title={article.title} />
|
||||
<div className="admin-panel-view-article__header-buttons">
|
||||
<span onClick={this.onEditClick.bind(this, article)}>
|
||||
<Icon className="admin-panel-view-article__edit-icon" name="pencil" />
|
||||
</span>
|
||||
<span onClick={this.onDeleteClick.bind(this, article)} >
|
||||
<Icon className="admin-panel-view-article__edit-icon" name="trash" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-panel-view-article__article">
|
||||
<Header title={article.title}/>
|
||||
|
||||
<div className="admin-panel-view-article__article-content ql-editor">
|
||||
<div dangerouslySetInnerHTML={{__html: MentionsParser.parse(article.content)}}/>
|
||||
</div>
|
||||
<div className="admin-panel-view-article__last-edited">
|
||||
{i18n('LAST_EDITED_IN', {date: DateTransformer.transformToString(article.lastEdited)})}
|
||||
</div>
|
||||
<div className="admin-panel-view-article__article-content ql-editor">
|
||||
<div dangerouslySetInnerHTML={{__html: MentionsParser.parse(article.content)}}/>
|
||||
</div>
|
||||
<div className="admin-panel-view-article__last-edited">
|
||||
{i18n('LAST_EDITED_IN', {date: DateTransformer.transformToString(article.lastEdited)})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -4,13 +4,25 @@
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
&__edit-buttons {
|
||||
text-align: left;
|
||||
margin-bottom: 20px;
|
||||
&__header-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
&__edit-button {
|
||||
margin-right: 20px;
|
||||
&__header-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 50px;
|
||||
margin-bottom: 5px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
&__edit-icon {
|
||||
color: $grey;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__last-edited {
|
||||
|
@ -2,6 +2,10 @@
|
||||
|
||||
&__menu {
|
||||
margin: 0 auto 20px auto;
|
||||
width: 300px;
|
||||
min-width: 300px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ import {connect} from 'react-redux';
|
||||
import _ from 'lodash';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import API from 'lib-app/api-call';
|
||||
import SessionActions from 'actions/session-actions';
|
||||
|
||||
import StaffEditor from 'app/admin/panel/staff/staff-editor';
|
||||
|
@ -1,25 +1,195 @@
|
||||
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 date from 'lib-app/date';
|
||||
|
||||
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: {
|
||||
period: 0,
|
||||
departments: [],
|
||||
owners: [],
|
||||
tags: []
|
||||
},
|
||||
ticketData: {}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
statsUtils.retrieveStats({
|
||||
rawForm: this.getFormWithDateRange(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="period" label={i18n('DATE')} field="select" fieldProps={{size: 'large', items: [{content: i18n('LAST_7_DAYS')}, {content: i18n('LAST_30_DAYS')}, {content: i18n('LAST_90_DAYS')}, {content: i18n('LAST_365_DAYS')}]}} />
|
||||
<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: {
|
||||
period: 0,
|
||||
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.getFormWithDateRange(this.state.rawForm),
|
||||
tags: this.props.tags
|
||||
}).then(({data}) => {
|
||||
this.setState({ticketData: data, loading: false});
|
||||
}).catch((error) => {
|
||||
if (showLogs) console.error('ERROR: ', error);
|
||||
});
|
||||
}
|
||||
|
||||
getFormWithDateRange(form) {
|
||||
const {startDate, endDate} = statsUtils.getDateRangeFromPeriod(form.period);
|
||||
|
||||
return {
|
||||
...form,
|
||||
dateRange: {
|
||||
startDate,
|
||||
endDate
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminPanelStats;
|
||||
export default connect((store) => {
|
||||
return {
|
||||
tags: store.config.tags,
|
||||
departments: store.config.departments,
|
||||
staffList: store.adminData.staffMembers
|
||||
};
|
||||
})(AdminPanelStats);
|
||||
|
@ -0,0 +1,110 @@
|
||||
@import "../../../../scss/vars";
|
||||
|
||||
.admin-panel-stats {
|
||||
text-align: left;
|
||||
|
||||
&__form {
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
&__col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
&__button {
|
||||
margin: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__loading {
|
||||
min-height: 361px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: $grey;
|
||||
}
|
||||
|
||||
&__card-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
&__department-option { // Duplicated from ticket-query-filters, please REMOVE
|
||||
&__name {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__department-selected { // Duplicated from ticket-query-filters, please REMOVE
|
||||
display: inline-block;
|
||||
border-radius: 3px;
|
||||
margin-left: 5px;
|
||||
padding: 1px;
|
||||
font-size: 13px;
|
||||
cursor: default;
|
||||
|
||||
&__icon {
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__staff-option { // Duplicated from ticket-query-filters, please REMOVE
|
||||
&__profile-pic {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__name {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__staff-selected { // Duplicated from ticket-query-filters, please REMOVE
|
||||
display: inline-block;
|
||||
border-radius: 3px;
|
||||
margin-left: 5px;
|
||||
padding: 1px;
|
||||
font-size: 13px;
|
||||
cursor: default;
|
||||
|
||||
&__profile-pic {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.admin-panel-stats__form__container__row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@ import {connect} from 'react-redux';
|
||||
import ConfigActions from 'actions/config-actions';
|
||||
import API from 'lib-app/api-call';
|
||||
import i18n from 'lib-app/i18n';
|
||||
import ToggleButton from 'app-components/toggle-button';
|
||||
import AreYouSure from 'app-components/are-you-sure';
|
||||
import ModalContainer from 'app-components/modal-container';
|
||||
|
||||
@ -26,10 +25,11 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
messageTitle: null,
|
||||
messageType: '',
|
||||
messageContent: '',
|
||||
keyName: '',
|
||||
keyCode: '',
|
||||
selectedAPIKey: -1,
|
||||
APIKeys: []
|
||||
APIKeys: [],
|
||||
error: '',
|
||||
showMessage: true,
|
||||
showAPIKeyMessage: true
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@ -38,10 +38,12 @@ 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 admin-panel-advanced-settings__mandatory-login">
|
||||
@ -51,8 +53,7 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
className="admin-panel-advanced-settings__mandatory-login__checkbox"
|
||||
value={config['mandatory-login']}
|
||||
onChange={this.onCheckboxMandatoryLoginChange.bind(this)}
|
||||
wrapInLabel
|
||||
/>
|
||||
wrapInLabel />
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="admin-panel-advanced-settings__registration">
|
||||
@ -62,8 +63,7 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
className="admin-panel-advanced-settings__registration__checkbox"
|
||||
value={config['registration']}
|
||||
onChange={this.onCheckboxRegistrationChange.bind(this)}
|
||||
wrapInLabel
|
||||
/>
|
||||
wrapInLabel />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -75,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>
|
||||
@ -90,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 admin-panel-advanced-settings__api-keys__container">
|
||||
{(this.state.selectedAPIKey === -1) ? this.renderNoKey() : this.renderKey()}
|
||||
{
|
||||
error ?
|
||||
<Message
|
||||
showMessage={showAPIKeyMessage}
|
||||
onCloseMessage={this.onCloseMessage.bind(this, "showAPIKeyMessage")}
|
||||
type="error">
|
||||
{i18n(error)}
|
||||
</Message> :
|
||||
((selectedAPIKey === -1) ? this.renderNoKey() : this.renderKey())
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -104,9 +113,16 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
}
|
||||
|
||||
renderMessage() {
|
||||
const { messageType, messageTitle, messageContent, showMessage } = this.state;
|
||||
|
||||
return (
|
||||
<Message className="admin-panel-advanced-settings__message" type={this.state.messageType} title={this.state.messageTitle}>
|
||||
{this.state.messageContent}
|
||||
<Message
|
||||
showMessage={showMessage}
|
||||
onCloseMessage={this.onCloseMessage.bind(this, "showMessage")}
|
||||
className="admin-panel-advanced-settings__message"
|
||||
type={messageType}
|
||||
title={messageTitle}>
|
||||
{messageContent}
|
||||
</Message>
|
||||
);
|
||||
}
|
||||
@ -120,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__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>
|
||||
@ -137,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 {
|
||||
@ -146,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)
|
||||
};
|
||||
}
|
||||
@ -154,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));
|
||||
}
|
||||
|
||||
@ -173,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, () => {
|
||||
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));
|
||||
});
|
||||
@ -189,8 +260,9 @@ 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
|
||||
});
|
||||
}
|
||||
|
||||
@ -203,10 +275,8 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
}
|
||||
|
||||
onAreYouSureMandatoryLoginOk(password) {
|
||||
const {
|
||||
config,
|
||||
dispatch
|
||||
} = this.props;
|
||||
const { config, dispatch } = this.props;
|
||||
|
||||
return API.call({
|
||||
path: config['mandatory-login'] ? '/system/disable-mandatory-login' : '/system/enable-mandatory-login',
|
||||
data: {
|
||||
@ -216,17 +286,16 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
this.setState({
|
||||
messageType: 'success',
|
||||
messageTitle: null,
|
||||
showMessage: true,
|
||||
messageContent: config['mandatory-login'] ? i18n('MANDATORY_LOGIN_DISABLED') : i18n('MANDATORY_LOGIN_ENABLED')
|
||||
});
|
||||
dispatch(ConfigActions.updateData());
|
||||
}).catch(() => this.setState({messageType: 'error', messageTitle: null, messageContent: i18n('ERROR_UPDATING_SETTINGS')}));
|
||||
}).catch(() => this.setState({messageType: 'error', showMessage: true, messageTitle: null, messageContent: i18n('ERROR_UPDATING_SETTINGS')}));
|
||||
}
|
||||
|
||||
onAreYouSureRegistrationOk(password) {
|
||||
const {
|
||||
config,
|
||||
dispatch
|
||||
} = this.props;
|
||||
const { config, dispatch } = this.props;
|
||||
|
||||
return API.call({
|
||||
path: config['registration'] ? '/system/disable-registration' : '/system/enable-registration',
|
||||
data: {
|
||||
@ -235,11 +304,12 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
}).then(() => {
|
||||
this.setState({
|
||||
messageType: 'success',
|
||||
showMessage: true,
|
||||
messageTitle: null,
|
||||
messageContent: config['registration'] ? i18n('REGISTRATION_DISABLED') : i18n('REGISTRATION_ENABLED')
|
||||
});
|
||||
dispatch(ConfigActions.updateData());
|
||||
}).catch(() => this.setState({messageType: 'error', messageTitle: null, messageContent: i18n('ERROR_UPDATING_SETTINGS')}));
|
||||
}).catch(() => this.setState({messageType: 'error', showMessage: true, messageTitle: null, messageContent: i18n('ERROR_UPDATING_SETTINGS')}));
|
||||
}
|
||||
|
||||
onImportCSV(event) {
|
||||
@ -251,23 +321,31 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
path: '/system/csv-import',
|
||||
dataAsForm: true,
|
||||
data: {
|
||||
file: file,
|
||||
password: password
|
||||
file,
|
||||
password
|
||||
}
|
||||
})
|
||||
.then((result) => this.setState({
|
||||
messageType: 'success',
|
||||
messageType: 'success',
|
||||
showMessage: true,
|
||||
messageTitle: i18n('SUCCESS_IMPORTING_CSV_DESCRIPTION'),
|
||||
messageContent: (result.data.length) ? (
|
||||
<div>
|
||||
{i18n('ERRORS_FOUND')}
|
||||
<ul>
|
||||
{result.data.map((error) => <li>{error}</li>)}
|
||||
{result.data.map((error, index) => <li key={`csv-file__key-${index}`} >{error}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
) : null
|
||||
}))
|
||||
.catch(() => this.setState({messageType: 'error', messageTitle: null, messageContent: i18n('INVALID_FILE')}));
|
||||
.catch((error) => {
|
||||
this.setState({
|
||||
messageType: 'error',
|
||||
showMessage: true,
|
||||
messageTitle: null,
|
||||
messageContent: i18n(error.message)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
onBackupDatabase() {
|
||||
@ -295,12 +373,17 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
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
|
||||
|
@ -21,6 +21,13 @@
|
||||
|
||||
&__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;
|
||||
@ -45,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 {
|
||||
|
@ -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'>
|
||||
<SubmitButton type="secondary" size="small">
|
||||
{i18n('SAVE')}
|
||||
</SubmitButton>
|
||||
<Button onClick={this.onDiscardClick.bind(this)} size="small">
|
||||
{i18n('CANCEL')}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
@ -118,7 +112,7 @@ class AdminPanelCustomTagsModal extends React.Component {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
onSubmitNewTag(form) {
|
||||
this.setState({
|
||||
loading: true
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
&__actions{
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
@ -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">
|
||||
@ -77,13 +77,13 @@ 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) {
|
||||
|
@ -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'}}/>
|
||||
infoMessage={i18n('IMAGE_HEADER_DESCRIPTION')}
|
||||
fieldProps={{size: 'large'}} />
|
||||
<SubmitButton className="admin-panel-email-settings__image-header-submit" type="secondary"
|
||||
size="small">{i18n('SAVE')}</SubmitButton>
|
||||
</div>
|
||||
@ -127,43 +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-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)}}/>
|
||||
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>
|
||||
@ -173,48 +179,75 @@ class AdminPanelEmailSettings extends React.Component {
|
||||
}
|
||||
|
||||
renderForm() {
|
||||
const { form, language, selectedIndex, edited } = this.state;
|
||||
const { template, text2, text3} = form;
|
||||
|
||||
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',
|
||||
type: 'supported',
|
||||
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'}}/>
|
||||
<FormField
|
||||
fieldProps={{size: 'large'}}
|
||||
label={i18n('SUBJECT')}
|
||||
name="subject"
|
||||
validation="TITLE"
|
||||
required />
|
||||
</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) ?
|
||||
<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) ?
|
||||
<FormField key="text3" label={i18n('TEXT') + '3'} name="text3" validation="TEXT_AREA" required
|
||||
decorator={'textarea'}
|
||||
fieldProps={{className: 'admin-panel-email-settings__text-area'}}/> : null}
|
||||
<FormField
|
||||
fieldProps={{className: 'admin-panel-email-settings__text-area'}}
|
||||
label={i18n('TEXT') + '1'}
|
||||
key="text1"
|
||||
name="text1"
|
||||
validation="TEXT_AREA"
|
||||
required
|
||||
decorator={'textarea'} />
|
||||
{
|
||||
(text2 || text2 === "") ?
|
||||
<FormField
|
||||
fieldProps={{className: 'admin-panel-email-settings__text-area'}}
|
||||
label={i18n('TEXT') + '2'}
|
||||
key="text2"
|
||||
name="text2"
|
||||
validation="TEXT_AREA"
|
||||
required
|
||||
decorator={'textarea'} /> :
|
||||
null
|
||||
}
|
||||
{
|
||||
((text3 || text3 === "") && (template !== "USER_PASSWORD" && template !== "USER_EMAIL")) ?
|
||||
<FormField
|
||||
fieldProps={{className: 'admin-panel-email-settings__text-area'}}
|
||||
label={i18n('TEXT') + '3'}
|
||||
key="text3"
|
||||
name="text3"
|
||||
validation={(template !== "USER_PASSWORD" && template !== "USER_EMAIL") ? "TEXT_AREA" : ""}
|
||||
required={(template !== "USER_PASSWORD" && template !== "USER_EMAIL")}
|
||||
decorator={'textarea'} /> :
|
||||
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">
|
||||
{i18n('SAVE')}
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
@ -242,16 +275,19 @@ class AdminPanelEmailSettings extends React.Component {
|
||||
}
|
||||
|
||||
getFormProps() {
|
||||
const { form, errors, loadingForm } = this.state;
|
||||
|
||||
return {
|
||||
values: this.state.form,
|
||||
errors: this.state.errors,
|
||||
loading: this.state.loadingForm,
|
||||
values: form,
|
||||
errors,
|
||||
loading: loadingForm,
|
||||
onChange: (form) => {
|
||||
this.setState({form, edited: true})
|
||||
},
|
||||
onValidateErrors: (errors) => {
|
||||
this.setState({errors})
|
||||
},
|
||||
onSubmit: this.onFormSubmit.bind(this, form)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,12 +17,12 @@
|
||||
|
||||
&__save-button {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
float: right;
|
||||
}
|
||||
|
||||
&__optional-buttons {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
float: left;
|
||||
}
|
||||
|
||||
&__discard-button {
|
||||
@ -31,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;
|
||||
@ -70,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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,8 @@ class AdminPanelSystemPreferences extends React.Component {
|
||||
message: null,
|
||||
values: {
|
||||
maintenance: false,
|
||||
}
|
||||
},
|
||||
showMessage: true
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@ -58,7 +59,6 @@ class AdminPanelSystemPreferences extends React.Component {
|
||||
</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">
|
||||
@ -101,10 +101,10 @@ class AdminPanelSystemPreferences extends React.Component {
|
||||
</div>
|
||||
<div className="row admin-panel-system-preferences__container">
|
||||
<div className="col-md-4 col-md-offset-2">
|
||||
<SubmitButton className="admin-panel-system-preferences__container__button" 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 className="admin-panel-system-preferences__container__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;
|
||||
|
@ -1,6 +1,6 @@
|
||||
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';
|
||||
@ -25,12 +25,12 @@ function getPublicDepartmentList(){
|
||||
return _.filter(SessionStore.getDepartments(),item => item.private != 1)
|
||||
}
|
||||
|
||||
export const getPublicDepartmentIndexFromDepartmentId = (departmentId) => {
|
||||
const departments = getPublicDepartmentList();
|
||||
const departmentIndex = _.findIndex(departments, department => department.id == departmentId );
|
||||
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 = {
|
||||
items: []
|
||||
@ -40,55 +40,65 @@ 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: {
|
||||
name: '',
|
||||
language: 'en',
|
||||
private: 0,
|
||||
private: 0
|
||||
},
|
||||
defaultDepartment: this.props.defaultDepartmentId,
|
||||
defaultDepartmentLocked: this.props.defaultDepartmentLocked * 1,
|
||||
defaultDepartmentLocked: this.props.defaultDepartmentLocked * 1
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
errorMessage,
|
||||
formLoading,
|
||||
selectedIndex
|
||||
} = this.state;
|
||||
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">
|
||||
{(errorMessage) ? <Message type="error">{i18n(errorMessage)}</Message> : null}
|
||||
{
|
||||
errorMessage ?
|
||||
<Message showMessage={showErrorMessage} onCloseMessage={this.onCloseMessage.bind(this, "showErrorMessage")} type="error">
|
||||
{i18n(errorMessage)}
|
||||
</Message> :
|
||||
null
|
||||
}
|
||||
<Form {...this.getFormProps()}>
|
||||
<div className="admin-panel-departments__container">
|
||||
<FormField className="admin-panel-departments__container__name" label={i18n('NAME')} name="name" validation="NAME" required fieldProps={{size: 'large'}}/>
|
||||
<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"/>
|
||||
<FormField label={i18n('PRIVATE')} name="private" field="checkbox" />
|
||||
<InfoTooltip className="admin-panel-departments__container__info-tooltip" text={i18n('PRIVATE_DEPARTMENT_DESCRIPTION')} />
|
||||
</div>
|
||||
</div>
|
||||
<SubmitButton
|
||||
size="medium"
|
||||
className="admin-panel-departments__update-name-button"
|
||||
type="secondary">
|
||||
{formLoading ?
|
||||
<Loading /> :
|
||||
i18n((selectedIndex !== -1) ?
|
||||
'UPDATE_DEPARTMENT' :
|
||||
'ADD_DEPARTMENT')}
|
||||
</SubmitButton>
|
||||
<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>
|
||||
</Form>
|
||||
{(selectedIndex !== -1 && this.props.departments.length) ? this.renderOptionalButtons() : null}
|
||||
</div>
|
||||
</div>
|
||||
{this.renderDefaultDepartmentForm()}
|
||||
@ -97,18 +107,19 @@ class AdminPanelDepartments extends React.Component {
|
||||
}
|
||||
|
||||
renderDefaultDepartmentForm() {
|
||||
const {
|
||||
defaultDepartmentError,
|
||||
formLoading
|
||||
} = this.state
|
||||
const { defaultDepartmentError, formLoading, showSuccessMessage, showDefaultDepartmentErrorMessage } = this.state;
|
||||
|
||||
return (
|
||||
<div className="admin-panel-departments__default-departments-container">
|
||||
<span className="separator" />
|
||||
{(defaultDepartmentError !== null) ?
|
||||
((!defaultDepartmentError) ?
|
||||
<Message type="success">{i18n('SETTINGS_UPDATED')}</Message> :
|
||||
<Message type="error">{i18n(defaultDepartmentError)}</Message>) :
|
||||
<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" >
|
||||
@ -118,9 +129,8 @@ class AdminPanelDepartments extends React.Component {
|
||||
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"/>
|
||||
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"
|
||||
@ -135,11 +145,15 @@ class AdminPanelDepartments extends React.Component {
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@ -169,10 +183,7 @@ class AdminPanelDepartments extends React.Component {
|
||||
}
|
||||
|
||||
getListingProps() {
|
||||
const {
|
||||
departments,
|
||||
defaultDepartmentId
|
||||
} = this.props;
|
||||
const { departments, defaultDepartmentId } = this.props;
|
||||
|
||||
return {
|
||||
className: 'admin-panel-departments__list',
|
||||
@ -182,11 +193,11 @@ class AdminPanelDepartments extends React.Component {
|
||||
content: (
|
||||
<span>
|
||||
{department.name}
|
||||
{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.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>
|
||||
@ -201,17 +212,13 @@ class AdminPanelDepartments extends React.Component {
|
||||
}
|
||||
|
||||
getFormProps() {
|
||||
const {
|
||||
form,
|
||||
errors,
|
||||
formLoading,
|
||||
} = this.state;
|
||||
const { form, errors, formLoading } = this.state;
|
||||
|
||||
return {
|
||||
values: {...form, private: !!form.private},
|
||||
errors: errors,
|
||||
loading: formLoading,
|
||||
onChange: (form) => {this.setState({form, edited: true})},
|
||||
onChange: (form) => {this.setState({form, editedAddDepartmentForm: true})},
|
||||
onValidateErrors: (errors) => {this.setState({errors})},
|
||||
onSubmit: this.onFormSubmit.bind(this),
|
||||
loading: formLoading,
|
||||
@ -220,23 +227,19 @@ class AdminPanelDepartments extends React.Component {
|
||||
}
|
||||
|
||||
getDefaultDepartmentFormProps() {
|
||||
const {
|
||||
formLoading,
|
||||
defaultDepartment,
|
||||
defaultDepartmentLocked
|
||||
} = this.state;
|
||||
const { formLoading, defaultDepartment, defaultDepartmentLocked } = this.state;
|
||||
|
||||
return {
|
||||
values: {
|
||||
defaultDepartment: getPublicDepartmentIndexFromDepartmentId(defaultDepartment),
|
||||
defaultDepartment: getPublicDepartmentIndexFromDepartmentId(defaultDepartment, getPublicDepartmentList()),
|
||||
locked: defaultDepartmentLocked ? true : false,
|
||||
},
|
||||
onChange: (formValue) => {
|
||||
this.setState({
|
||||
edited: true,
|
||||
editedDefaultDepartmentForm: true,
|
||||
defaultDepartmentError: null,
|
||||
defaultDepartment: getPublicDepartmentList()[formValue.defaultDepartment].id,
|
||||
defaultDepartmentLocked: formValue.locked,
|
||||
defaultDepartmentLocked: formValue.locked
|
||||
});
|
||||
},
|
||||
onSubmit: this.onDefaultDepartmentFormSubmit.bind(this),
|
||||
@ -244,8 +247,14 @@ class AdminPanelDepartments extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
@ -255,7 +264,7 @@ class AdminPanelDepartments extends React.Component {
|
||||
onDefaultDepartmentFormSubmit(formValue) {
|
||||
let publicDepartments = getPublicDepartmentList();
|
||||
|
||||
this.setState({formLoading: true, edited: false});
|
||||
this.setState({formLoading: true, editedDefaultDepartmentForm: false});
|
||||
|
||||
API.call({
|
||||
path: '/system/edit-settings',
|
||||
@ -264,13 +273,13 @@ class AdminPanelDepartments extends React.Component {
|
||||
'default-is-locked': formValue.locked ? 1 : 0
|
||||
}
|
||||
}).then(() => {
|
||||
this.retrieveDepartments();
|
||||
this.setState({formLoading: false, errorMessage: false, defaultDepartmentError: false});
|
||||
}).catch(result => this.setState({formLoading: false, defaultDepartmentError: result.message}));
|
||||
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({
|
||||
@ -283,7 +292,7 @@ class AdminPanelDepartments extends React.Component {
|
||||
}).then(() => {
|
||||
this.setState({formLoading: false, errorMessage: false, defaultDepartmentError: null});
|
||||
this.retrieveDepartments();
|
||||
}).catch(result => this.setState({formLoading: false, errorMessage: result.message, defaultDepartmentError: null}));
|
||||
}).catch(result => this.setState({formLoading: false, errorMessage: result.message, showErrorMessage: true, defaultDepartmentError: null}));
|
||||
} else {
|
||||
API.call({
|
||||
path: '/system/add-department',
|
||||
@ -295,9 +304,9 @@ class AdminPanelDepartments extends React.Component {
|
||||
this.setState({formLoading: false,errorMessage: false, defaultDepartmentError: null});
|
||||
this.retrieveDepartments();
|
||||
this.onItemChange(-1);
|
||||
}).catch(() => {
|
||||
this.onItemChange.bind(this, -1)
|
||||
this.setState({formLoading: false, defaultDepartmentError: null});
|
||||
}).catch(result => {
|
||||
this.onItemChange.bind(this, -1);
|
||||
this.setState({formLoading: false, errorMessage: result.message, showErrorMessage: true, defaultDepartmentError: null});
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -326,19 +335,19 @@ class AdminPanelDepartments extends React.Component {
|
||||
this.onItemChange(-1);
|
||||
this.setState({defaultDepartmentError: null});
|
||||
})
|
||||
.catch(result => this.setState({errorMessage: result.message, defaultDepartmentError: null}));
|
||||
.catch(result => this.setState({errorMessage: result.message, showErrorMessage: true, defaultDepartmentError: null}));
|
||||
}
|
||||
|
||||
updateForm(index) {
|
||||
let form = _.clone(this.state.form);
|
||||
let department = this.getCurrentDepartment(this.props.departments,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,
|
||||
@ -347,11 +356,14 @@ class AdminPanelDepartments extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
retrieveDepartments() {
|
||||
retrieveDepartments(fromUpdateDefaultDepartmentForm = false) {
|
||||
this.props.dispatch(ConfigActions.updateData());
|
||||
this.setState({
|
||||
edited: false
|
||||
});
|
||||
|
||||
this.setState(
|
||||
fromUpdateDefaultDepartmentForm ?
|
||||
{editedDefaultDepartmentForm: false} :
|
||||
{editedAddDepartmentForm: false}
|
||||
);
|
||||
}
|
||||
|
||||
getCurrentDepartment(list, index) {
|
||||
@ -359,10 +371,7 @@ class AdminPanelDepartments extends React.Component {
|
||||
}
|
||||
|
||||
getDropDownItemId() {
|
||||
const {
|
||||
selectedIndex,
|
||||
selectedDropDownIndex
|
||||
} = this.state;
|
||||
const { selectedIndex, selectedDropDownIndex } = this.state;
|
||||
|
||||
return this.props.departments.filter((department, index) => index !== selectedIndex)[selectedDropDownIndex].id;
|
||||
}
|
||||
|
@ -8,6 +8,23 @@
|
||||
|
||||
&__update-name-button {
|
||||
min-width: 156px;
|
||||
|
||||
&__container {
|
||||
padding: 0 0 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__buttons-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__discard-button,
|
||||
&__delete-button {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
&__container{
|
||||
@ -46,14 +63,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__optional-buttons {
|
||||
float: right;
|
||||
}
|
||||
|
||||
&__discard-button,
|
||||
&__delete-button {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
&__optional-buttons-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__warning {
|
||||
|
@ -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,10 +47,10 @@ 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})} />}
|
||||
{(this.props.loading) ? <Loading className="admin-panel-staff-members__loading" backgrounded size="large"/> : <PeopleList list={this.getStaffList()} page={this.state.page} onPageSelect={(index) => this.setState({page: index+1})} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -27,6 +27,11 @@
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
&__loading {
|
||||
min-width: 300px;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 415px) {
|
||||
.admin-panel-staff-members {
|
||||
&__drowpdown {
|
||||
|
@ -33,14 +33,20 @@ class AdminPanelViewStaff extends React.Component {
|
||||
|
||||
getProps() {
|
||||
const { userData } = this.state;
|
||||
const {
|
||||
userId,
|
||||
params
|
||||
} = this.props;
|
||||
const userDataWithNumericLevel = {
|
||||
...userData,
|
||||
level: userData.level*1,
|
||||
sendEmailOnNewTicket: userData.sendEmailOnNewTicket === "1"
|
||||
sendEmailOnNewTicket: userData.sendEmailOnNewTicket === "1",
|
||||
myAccount: this.props.userEmail == userData.email
|
||||
};
|
||||
|
||||
return _.extend({}, userDataWithNumericLevel, {
|
||||
staffId: this.props.params.staffId * 1,
|
||||
userId: userId*1,
|
||||
staffId: params.staffId*1,
|
||||
onDelete: this.onDelete.bind(this),
|
||||
onChange: this.retrieveStaff.bind(this)
|
||||
});
|
||||
@ -56,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() {
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
@ -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,103 @@ 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: {
|
||||
departments: [],
|
||||
owners: [{id: this.props.staffId}],
|
||||
tags: []
|
||||
},
|
||||
ticketData: {},
|
||||
ticketListLoading: false
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.retrieveStaffMembers();
|
||||
this.retrieveTicketsAssigned(INITIAL_API_VALUE);
|
||||
statsUtils.retrieveStats({
|
||||
rawForm: this.state.rawForm
|
||||
}).then(({data}) => {
|
||||
this.setState({
|
||||
ticketData: data,
|
||||
loadingStats: false
|
||||
});
|
||||
}).catch((error) => {
|
||||
if (showLogs) console.error('ERROR: ', error);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
console.log('State: ', this.state.rawForm);
|
||||
|
||||
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 +122,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 +149,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 +276,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 +297,41 @@ 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 ?
|
||||
<Loading className="admin-panel-stats__loading" backgrounded size="large" /> :
|
||||
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>
|
||||
{this.state.ticketListLoading ?
|
||||
<Loading className="staff-editor__ticketlist-loading" backgrounded size="large"/> :
|
||||
<TicketList {...this.getTicketListProps()} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -218,7 +340,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 +363,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 +396,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 +424,12 @@ class StaffEditor extends React.Component {
|
||||
}
|
||||
|
||||
onSubmit(eventType, form) {
|
||||
this.setState({
|
||||
loadingStats: true,
|
||||
ticketListLoading: true
|
||||
});
|
||||
|
||||
const { myAccount, staffId, onChange } = this.props;
|
||||
let departments;
|
||||
|
||||
if(form.departments) {
|
||||
@ -304,39 +441,56 @@ 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,
|
||||
ticketListLoading: false
|
||||
});
|
||||
|
||||
if(this.props.onChange) {
|
||||
this.props.onChange();
|
||||
}
|
||||
statsUtils.retrieveStats({
|
||||
rawForm: this.state.rawForm
|
||||
}).then(({data}) => {
|
||||
this.setState({ticketData: data, loadingStats: false});
|
||||
}).catch((error) => {
|
||||
if (showLogs) console.error('ERROR: ', error);
|
||||
this.setState({loadingStats: false});
|
||||
});
|
||||
|
||||
this.retrieveTicketsAssigned({page: 1});
|
||||
onChange && onChange();
|
||||
}).catch(() => {
|
||||
window.scrollTo(0,0);
|
||||
this.setState({message: 'FAIL'});
|
||||
window.scrollTo(0,250);
|
||||
this.setState({message: 'FAIL', loadingStats: false, showMessage: true});
|
||||
});
|
||||
}
|
||||
|
||||
onDeleteClick() {
|
||||
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 +499,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 +507,89 @@ class StaffEditor extends React.Component {
|
||||
loadingPicture: false
|
||||
});
|
||||
|
||||
if(this.props.onChange) {
|
||||
this.props.onChange();
|
||||
}
|
||||
this.retrieveStaffMembers();
|
||||
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);
|
||||
|
@ -174,6 +174,15 @@
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
&__ticketlist-loading {
|
||||
min-height: 361px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: $grey;
|
||||
}
|
||||
|
||||
&__separator {
|
||||
margin: 3px 0;
|
||||
}
|
||||
@ -213,11 +222,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,6 +227,8 @@ class AdminPanelCustomResponses extends React.Component {
|
||||
}
|
||||
|
||||
deleteCustomResponse() {
|
||||
this.updateForm(this.state.selectedIndex)
|
||||
|
||||
return API.call({
|
||||
path: '/ticket/delete-custom-response',
|
||||
data: {
|
||||
@ -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) => {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,20 +26,21 @@ class AdminPanelMyTickets extends React.Component {
|
||||
state = {
|
||||
closedTicketsShown: false,
|
||||
departmentId: null,
|
||||
pageSize: 10
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.retrieveMyTickets();
|
||||
this.retrieveMyTickets({});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="admin-panel-my-tickets">
|
||||
<Header title={i18n('MY_TICKETS')} description={i18n('MY_TICKETS_DESCRIPTION')} />
|
||||
{(this.props.error) ? <Message type="error">{i18n('ERROR_RETRIEVING_TICKETS')}</Message> : <TicketList {...this.getProps()}/>}
|
||||
{(this.props.error) ? <Message showCloseButton={false} type="error">{i18n('ERROR_RETRIEVING_TICKETS')}</Message> : <TicketList {...this.getProps()} />}
|
||||
<div style={{textAlign: 'right', marginTop: 10}}>
|
||||
<Button onClick={this.onCreateTicket.bind(this)} type="secondary" size="medium">
|
||||
<Icon size="sm" name="plus"/> {i18n('CREATE_TICKET')}
|
||||
<Icon size="sm" name="plus" /> {i18n('CREATE_TICKET')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -47,21 +48,35 @@ class AdminPanelMyTickets extends React.Component {
|
||||
}
|
||||
|
||||
getProps() {
|
||||
const { closedTicketsShown } = this.state;
|
||||
const {
|
||||
userId,
|
||||
departments,
|
||||
tickets,
|
||||
loading,
|
||||
pages,
|
||||
page
|
||||
} = this.props;
|
||||
|
||||
return {
|
||||
userId: this.props.userId,
|
||||
departments: this.props.departments,
|
||||
tickets: this.props.tickets,
|
||||
userId,
|
||||
departments ,
|
||||
tickets,
|
||||
type: 'secondary',
|
||||
loading: this.props.loading,
|
||||
loading,
|
||||
ticketPath: '/admin/panel/tickets/view-ticket/',
|
||||
closedTicketsShown: this.state.closedTicketsShown,
|
||||
closedTicketsShown,
|
||||
onClosedTicketsShownChange: this.onClosedTicketsShownChange.bind(this),
|
||||
pages: this.props.pages,
|
||||
page: this.props.page,
|
||||
onPageChange: event => this.retrieveMyTickets(event.target.value),
|
||||
pages,
|
||||
page,
|
||||
onPageChange: event => this.retrieveMyTickets({page: event.target.value}),
|
||||
onDepartmentChange: departmentId => {
|
||||
this.setState({departmentId});
|
||||
this.retrieveMyTickets(1, this.state.closedTicketsShown, departmentId);
|
||||
this.setState({departmentId})
|
||||
this.retrieveMyTickets({page: 1, departmentId});
|
||||
},
|
||||
onPageSizeChange: pageSize => {
|
||||
this.setState({pageSize});
|
||||
this.retrieveMyTickets({page: 1, pageSize});
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -71,15 +86,18 @@ class AdminPanelMyTickets extends React.Component {
|
||||
return {
|
||||
closedTicketsShown: !state.closedTicketsShown
|
||||
};
|
||||
}, () => this.retrieveMyTickets());
|
||||
}, () => this.retrieveMyTickets({}));
|
||||
}
|
||||
|
||||
onCreateTicket() {
|
||||
ModalContainer.openModal(
|
||||
<div>
|
||||
<CreateTicketForm isStaff={true} onSuccess={this.onCreateTicketSuccess.bind(this)} />
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<Button onClick={ModalContainer.closeModal} type="link">{i18n('CLOSE')}</Button>
|
||||
<div className="admin-panel-my-tickets__create-ticket-form-container">
|
||||
<CreateTicketForm
|
||||
className="admin-panel-my-tickets__create-ticket-form"
|
||||
isStaff={true}
|
||||
onSuccess={this.onCreateTicketSuccess.bind(this)} />
|
||||
<div className="admin-panel-my-tickets__close-button-container">
|
||||
<Button className="admin-panel-my-tickets__close-button" onClick={ModalContainer.closeModal} type="link">{i18n('CLOSE')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -87,11 +105,11 @@ class AdminPanelMyTickets extends React.Component {
|
||||
|
||||
onCreateTicketSuccess() {
|
||||
ModalContainer.closeModal();
|
||||
this.retrieveMyTickets();
|
||||
this.retrieveMyTickets({});
|
||||
}
|
||||
|
||||
retrieveMyTickets(page = this.props.page, closed = this.state.closedTicketsShown, departmentId = this.state.departmentId) {
|
||||
this.props.dispatch(AdminDataAction.retrieveMyTickets(page, closed * 1, departmentId));
|
||||
retrieveMyTickets({page = this.props.page, closed = this.state.closedTicketsShown, departmentId = this.state.departmentId, pageSize = this.state.pageSize}) {
|
||||
this.props.dispatch(AdminDataAction.retrieveMyTickets({page, closed: closed * 1, departmentId, pageSize}));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,12 @@
|
||||
.admin-panel-my-tickets {
|
||||
&__close-button-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
bottom: 35px;
|
||||
}
|
||||
|
||||
}
|
@ -20,10 +20,11 @@ class AdminPanelNewTickets extends React.Component {
|
||||
|
||||
state = {
|
||||
departmentId: null,
|
||||
pageSize: 10
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.retrieveNewTickets();
|
||||
this.retrieveNewTickets({});
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -31,8 +32,8 @@ class AdminPanelNewTickets extends React.Component {
|
||||
return (
|
||||
<div className="admin-panel-new-tickets">
|
||||
<Header title={i18n('NEW_TICKETS')} description={i18n('NEW_TICKETS_DESCRIPTION')} />
|
||||
{(noDepartments) ? <Message className="admin-panel-new-tickets__department-warning" type="warning">{i18n('NO_DEPARTMENT_ASSIGNED')}</Message> : null}
|
||||
{(this.props.error) ? <Message type="error">{i18n('ERROR_RETRIEVING_TICKETS')}</Message> : <TicketList {...this.getProps()}/>}
|
||||
{(noDepartments) ? <Message showCloseButton={false} className="admin-panel-new-tickets__department-warning" type="warning">{i18n('NO_DEPARTMENT_ASSIGNED')}</Message> : null}
|
||||
{(this.props.error) ? <Message showCloseButton={false} type="error">{i18n('ERROR_RETRIEVING_TICKETS')}</Message> : <TicketList {...this.getProps()} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -47,16 +48,20 @@ class AdminPanelNewTickets extends React.Component {
|
||||
ticketPath: '/admin/panel/tickets/view-ticket/',
|
||||
page: this.props.page,
|
||||
pages: this.props.pages,
|
||||
onPageChange: event => this.retrieveNewTickets(event.target.value),
|
||||
onPageChange: event => this.retrieveNewTickets({page: event.target.value}),
|
||||
onDepartmentChange: departmentId => {
|
||||
this.setState({departmentId});
|
||||
this.retrieveNewTickets(1, departmentId);
|
||||
this.retrieveNewTickets({page: 1, departmentId});
|
||||
},
|
||||
onPageSizeChange: pageSize => {
|
||||
this.setState({pageSize});
|
||||
this.retrieveNewTickets({page: 1, pageSize});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
retrieveNewTickets(page = this.props.page, departmentId = this.state.departmentId) {
|
||||
this.props.dispatch(AdminDataAction.retrieveNewTickets(page, departmentId));
|
||||
retrieveNewTickets({page = this.props.page, departmentId = this.state.departmentId, pageSize = this.state.pageSize }) {
|
||||
this.props.dispatch(AdminDataAction.retrieveNewTickets({page, departmentId, pageSize}));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ export function updateSearchTicketsFromURL() {
|
||||
const currentSearchParams = queryString.parse(currentSearch);
|
||||
const showFilters = (currentSearch !== SEARCH_TICKETS_INITIAL_QUERY) && currentSearchParams.custom;
|
||||
|
||||
if((showFilters !== undefined) && currentSearchParams.useInitialValues) store.dispatch(searchFiltersActions.changeShowFilters(!showFilters));
|
||||
if(showFilters !== undefined && currentSearchParams.useInitialValues) store.dispatch(searchFiltersActions.changeShowFilters(!showFilters));
|
||||
|
||||
store.dispatch(searchFiltersActions.changeFilters(listConfig));
|
||||
store.dispatch(searchFiltersActions.retrieveSearchTickets(
|
||||
@ -38,10 +38,10 @@ export function updateSearchTicketsFromURL() {
|
||||
...store.getState().searchFilters.ticketQueryListState,
|
||||
page: (currentSearchParams.page || INITIAL_PAGE)*1
|
||||
},
|
||||
searchTicketsUtils.prepareFiltersForAPI(listConfig.filters)
|
||||
searchTicketsUtils.getFiltersForAPI(listConfig.filters),
|
||||
currentSearchParams.pageSize
|
||||
));
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,7 +50,8 @@ updateSearchTicketsFromURL();
|
||||
class AdminPanelSearchTickets extends React.Component {
|
||||
|
||||
render() {
|
||||
const { listConfig } = this.props;
|
||||
const { listConfig, error } = this.props;
|
||||
|
||||
return (
|
||||
<div className="admin-panel-search-tickets">
|
||||
<div className="admin-panel-search-tickets__container">
|
||||
@ -68,10 +69,9 @@ class AdminPanelSearchTickets extends React.Component {
|
||||
</div>
|
||||
<TicketQueryFilters />
|
||||
{
|
||||
(this.props.error) ?
|
||||
<Message type="error">{i18n('ERROR_RETRIEVING_TICKETS')}</Message> :
|
||||
<TicketQueryList
|
||||
onChangeOrderBy={this.onChangeOrderBy.bind(this)} />
|
||||
error ?
|
||||
<Message showCloseButton={false} type="error">{i18n('ERROR_RETRIEVING_TICKETS')}</Message> :
|
||||
<TicketQueryList onChangeOrderBy={this.onChangeOrderBy.bind(this)} />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
@ -21,7 +21,9 @@ class AdminPanelBanUsers extends React.Component {
|
||||
listError: false,
|
||||
addBanStatus: 'none',
|
||||
emails: [],
|
||||
filteredEmails: []
|
||||
filteredEmails: [],
|
||||
showMessage: true,
|
||||
showListErrorMessage: true
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@ -29,10 +31,18 @@ class AdminPanelBanUsers extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { listError, showListErrorMessage } = this.state;
|
||||
|
||||
return (
|
||||
<div className="admin-panel-ban-users row">
|
||||
<Header title={i18n('BAN_USERS')} description={i18n('BAN_USERS_DESCRIPTION')} />
|
||||
{(this.state.listError) ? <Message type="error">{i18n('ERROR_RETRIEVING_BAN_LIST')}</Message> : this.renderContent()}
|
||||
{
|
||||
listError ?
|
||||
<Message showMessage={showListErrorMessage} onCloseMessage={this.onCloseMessage.bind(this, "showListErrorMessage")} type="error">
|
||||
{i18n('ERROR_RETRIEVING_BAN_LIST')}
|
||||
</Message> :
|
||||
this.renderContent()
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -59,11 +69,29 @@ class AdminPanelBanUsers extends React.Component {
|
||||
}
|
||||
|
||||
renderMessage() {
|
||||
switch (this.state.addBanStatus) {
|
||||
const { addBanStatus, showMessage } = this.state;
|
||||
|
||||
switch (addBanStatus) {
|
||||
case 'success':
|
||||
return <Message className="admin-panel-ban-users__form-message" type="success">{i18n('EMAIL_BANNED_SUCCESSFULLY')}</Message>;
|
||||
return (
|
||||
<Message
|
||||
showMessage={showMessage}
|
||||
onCloseMessage={this.onCloseMessage.bind(this, "showMessage")}
|
||||
className="admin-panel-ban-users__form-message"
|
||||
type="success">
|
||||
{i18n('EMAIL_BANNED_SUCCESSFULLY')}
|
||||
</Message>
|
||||
);
|
||||
case 'fail':
|
||||
return <Message className="admin-panel-ban-users__form-message" type="error">{i18n('ERROR_BANNING_EMAIL')}</Message>;
|
||||
return (
|
||||
<Message
|
||||
showMessage={showMessage}
|
||||
onCloseMessage={this.onCloseMessage.bind(this, "showMessage")}
|
||||
className="admin-panel-ban-users__form-message"
|
||||
type="error">
|
||||
{i18n('ERROR_BANNING_EMAIL')}
|
||||
</Message>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@ -119,10 +147,11 @@ class AdminPanelBanUsers extends React.Component {
|
||||
}
|
||||
}).then(() => {
|
||||
this.setState({
|
||||
addBanStatus: 'success'
|
||||
addBanStatus: 'success',
|
||||
showMessage: true
|
||||
});
|
||||
this.retrieveEmails();
|
||||
}).catch(() => this.setState({addBanStatus: 'fail', loadingForm: false}));
|
||||
}).catch(() => this.setState({addBanStatus: 'fail', loadingForm: false, showMessage: true}));
|
||||
}
|
||||
|
||||
onUnBanClick(email) {
|
||||
@ -150,10 +179,17 @@ class AdminPanelBanUsers extends React.Component {
|
||||
filteredEmails: result.data
|
||||
})).catch(() => this.setState({
|
||||
listError: true,
|
||||
showListErrorMessage: true,
|
||||
loadingList: false,
|
||||
loadingForm: false
|
||||
}));
|
||||
}
|
||||
|
||||
onCloseMessage(showMessage) {
|
||||
this.setState({
|
||||
[showMessage]: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminPanelBanUsers;
|
||||
|
@ -21,29 +21,36 @@ class AdminPanelCustomFieldForm extends React.Component {
|
||||
state = {
|
||||
loading: false,
|
||||
error: null,
|
||||
addForm: {},
|
||||
addForm: {
|
||||
name: "",
|
||||
description: ""
|
||||
},
|
||||
addFormOptions: [],
|
||||
showErrorMessage: true
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loading, addForm, error } = this.state;
|
||||
|
||||
return (
|
||||
<div className="admin-panel-custom-field-form">
|
||||
<Header title={i18n('NEW_CUSTOM_FIELD')} description={i18n('NEW_CUSTOM_FIELD_DESCRIPTION')} />
|
||||
<div className="admin-panel-custom-field-form__form-container">
|
||||
<Form
|
||||
className="admin-panel-custom-field-form__form"
|
||||
loading={this.state.loading}
|
||||
values={this.state.addForm}
|
||||
loading={loading}
|
||||
values={addForm}
|
||||
onChange={this.onAddFormChange.bind(this)}
|
||||
onSubmit={this.onSubmit.bind(this)}>
|
||||
onSubmit={this.onSubmit.bind(this)}
|
||||
onKeyDown={(event) => { if(event.key == 'Enter') event.preventDefault()}}>
|
||||
<FormField name="name" validation="NAME" label={i18n('NAME')} field="input" fieldProps={{size: 'large'}} required/>
|
||||
<FormField name="description" label={i18n('FIELD_DESCRIPTION')} field="input" fieldProps={{size: 'large'}}/>
|
||||
<FormField name="type" label={i18n('TYPE')} field="select" fieldProps={{size: 'large', items: [{content: i18n('TEXT_INPUT')}, {content: i18n('SELECT_INPUT')}]}} required/>
|
||||
{this.state.addForm.type ? this.renderOptionFormFields() : null}
|
||||
{this.state.error ? this.renderErrorMessage() : null}
|
||||
{addForm.type ? this.renderOptionFormFields() : null}
|
||||
{error ? this.renderErrorMessage() : null}
|
||||
<div className="admin-panel-custom-field-form__buttons">
|
||||
<SubmitButton>{i18n('SUBMIT')}</SubmitButton>
|
||||
<Button onClick={this.props.onClose} type="link">{i18n('CLOSE')}</Button>
|
||||
<SubmitButton type="secondary">{i18n('SUBMIT')}</SubmitButton>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
@ -52,9 +59,11 @@ class AdminPanelCustomFieldForm extends React.Component {
|
||||
}
|
||||
|
||||
renderErrorMessage() {
|
||||
const { error, showErrorMessage } = this.state;
|
||||
|
||||
return (
|
||||
<Message type="error">
|
||||
{this.state.error}
|
||||
<Message showMessage={showErrorMessage} onCloseMessage={this.onCloseMessage.bind(this, "showErrorMessage")} type="error">
|
||||
{i18n(error)}
|
||||
</Message>
|
||||
);
|
||||
}
|
||||
@ -78,9 +87,14 @@ class AdminPanelCustomFieldForm extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
onCloseMessage(showMessage) {
|
||||
this.setState({
|
||||
[showMessage]: false
|
||||
});
|
||||
}
|
||||
|
||||
onAddOptionClick(event) {
|
||||
event.preventDefault();
|
||||
|
||||
let addFormOptions = _.clone(this.state.addFormOptions);
|
||||
|
||||
addFormOptions.push("");
|
||||
@ -121,7 +135,7 @@ class AdminPanelCustomFieldForm extends React.Component {
|
||||
this.setState({loading: false, message: null});
|
||||
if(this.props.onChange) this.props.onChange();
|
||||
})
|
||||
.catch(result => this.setState({loading: false, error: result.message}));
|
||||
.catch(result => this.setState({loading: false, error: result.message, showErrorMessage: true}));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,5 +26,6 @@
|
||||
&__buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
@ -29,9 +29,9 @@ class AdminPanelCustomFields extends React.Component {
|
||||
<div className="admin-panel-custom-fields">
|
||||
<Header title={i18n('CUSTOM_FIELDS')} description={i18n('CUSTOM_FIELDS_DESCRIPTION')} />
|
||||
{this.renderCustomFieldList()}
|
||||
<div className="admin-panel-custom-fields__add-button">
|
||||
<Button type="secondary" onClick={this.onNewCustomFieldClick.bind(this)}>
|
||||
<Icon name="plus"/> {i18n('NEW_CUSTOM_FIELD')}
|
||||
<div className="admin-panel-custom-fields__container-button">
|
||||
<Button className="admin-panel-custom-fields__container-button__add-button" type="secondary" onClick={this.onNewCustomFieldClick.bind(this)}>
|
||||
<Icon name="plus" /> {i18n('NEW_CUSTOM_FIELD')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -48,8 +48,7 @@ class AdminPanelCustomFields extends React.Component {
|
||||
{key: 'options', value: i18n('OPTIONS')},
|
||||
{key: 'actions', value: ''},
|
||||
]}
|
||||
rows={this.state.customFields.map(this.getCustomField.bind(this))}
|
||||
/>
|
||||
rows={this.state.customFields.map(this.getCustomField.bind(this))} />
|
||||
);
|
||||
}
|
||||
|
||||
@ -72,11 +71,11 @@ class AdminPanelCustomFields extends React.Component {
|
||||
onNewCustomFieldClick() {
|
||||
ModalContainer.openModal(
|
||||
<AdminPanelCustomFieldForm
|
||||
onClose={ModalContainer.closeModal}
|
||||
onClose={(e) => {e.preventDefault(); ModalContainer.closeModal();}}
|
||||
onChange={() => {
|
||||
this.retrieveCustomFields();
|
||||
ModalContainer.closeModal();
|
||||
}}/>
|
||||
}} />
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -4,8 +4,14 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&__add-button {
|
||||
text-align: left;
|
||||
margin-top: 14px;
|
||||
&__container-button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
|
||||
&__add-button {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,43 +15,57 @@ import Icon from 'core-components/icon';
|
||||
import ModalContainer from 'app-components/modal-container';
|
||||
import InviteUserWidget from 'app/admin/panel/users/invite-user-widget';
|
||||
|
||||
const DEFAULT_USERS_PARAMS = {
|
||||
page: 1,
|
||||
orderBy: 'id',
|
||||
desc: true,
|
||||
search: ''
|
||||
}
|
||||
|
||||
class AdminPanelListUsers extends React.Component {
|
||||
state = {
|
||||
loading: true,
|
||||
users: [],
|
||||
orderBy: 'id',
|
||||
desc: true,
|
||||
usersParams: DEFAULT_USERS_PARAMS,
|
||||
error: false,
|
||||
page: 1,
|
||||
pages: 1
|
||||
pages: 1,
|
||||
showMessage: true
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.retrieveUsers({
|
||||
page: 1,
|
||||
orderBy: 'id',
|
||||
desc: true,
|
||||
search: ''
|
||||
});
|
||||
this.retrieveUsers(DEFAULT_USERS_PARAMS);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="admin-panel-list-users">
|
||||
<Header title={i18n('LIST_USERS')} description={i18n('LIST_USERS_DESCRIPTION')} />
|
||||
{(this.state.error) ? <Message type="error">{i18n('ERROR_RETRIEVING_USERS')}</Message> : this.renderTableAndInviteButton()}
|
||||
{(this.state.error) ? <Message showCloseButton={false} type="error">{i18n('ERROR_RETRIEVING_USERS')}</Message> : this.renderTableAndInviteButton()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderTableAndInviteButton() {
|
||||
const { message, showMessage } = this.state;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SearchBox className="admin-panel-list-users__search-box" placeholder={i18n('SEARCH_USERS')} onSearch={this.onSearch.bind(this)} />
|
||||
<Table {...this.getTableProps()}/>
|
||||
{
|
||||
(message === 'success') ?
|
||||
<Message
|
||||
showMessage={showMessage}
|
||||
onCloseMessage={this.onCloseMessage.bind(this, "showMessage")}
|
||||
className="admin-panel-list-users__success-message"
|
||||
type="success">
|
||||
{i18n('INVITE_USER_SUCCESS')}
|
||||
</Message> :
|
||||
null
|
||||
}
|
||||
<Table {...this.getTableProps()} />
|
||||
<div style={{textAlign: 'right', marginTop: 10}}>
|
||||
<Button onClick={this.onInviteUser.bind(this)} type="secondary" size="medium">
|
||||
<Icon size="sm" name="plus"/> {i18n('INVITE_USER')}
|
||||
<Icon size="sm" name="user-plus" /> {i18n('INVITE_USER')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -59,14 +73,16 @@ class AdminPanelListUsers extends React.Component {
|
||||
}
|
||||
|
||||
getTableProps() {
|
||||
const {loading, users, usersParams, pages } = this.state;
|
||||
|
||||
return {
|
||||
className: 'admin-panel-list-users__table',
|
||||
loading: this.state.loading,
|
||||
loading,
|
||||
headers: this.getTableHeaders(),
|
||||
rows: this.state.users.map(this.getUserRow.bind(this)),
|
||||
rows: users.map(this.getUserRow.bind(this)),
|
||||
pageSize: 10,
|
||||
page: this.state.page,
|
||||
pages: this.state.pages,
|
||||
page: usersParams.page,
|
||||
pages,
|
||||
onPageChange: this.onPageChange.bind(this)
|
||||
};
|
||||
}
|
||||
@ -129,38 +145,57 @@ class AdminPanelListUsers extends React.Component {
|
||||
}
|
||||
|
||||
onSearch(query) {
|
||||
this.retrieveUsers({
|
||||
page: 1,
|
||||
orderBy: 'id',
|
||||
desc: true,
|
||||
const newUsersParams = {
|
||||
...this.state.usersParams,
|
||||
page: DEFAULT_USERS_PARAMS.page,
|
||||
search: query
|
||||
}
|
||||
|
||||
this.retrieveUsers(newUsersParams);
|
||||
|
||||
this.setState({
|
||||
usersParams: newUsersParams
|
||||
});
|
||||
}
|
||||
|
||||
onPageChange(event) {
|
||||
this.retrieveUsers({
|
||||
const newUsersParams = {
|
||||
...this.state.usersParams,
|
||||
page: event.target.value,
|
||||
orderBy: this.state.orderBy,
|
||||
desc: this.state.desc,
|
||||
search: this.state.search
|
||||
}
|
||||
|
||||
this.retrieveUsers(newUsersParams);
|
||||
|
||||
this.setState({
|
||||
usersParams: newUsersParams
|
||||
});
|
||||
}
|
||||
|
||||
orderByTickets(desc) {
|
||||
this.retrieveUsers({
|
||||
page: 1,
|
||||
const newUsersParams = {
|
||||
...this.state.usersParams,
|
||||
orderBy: 'tickets',
|
||||
desc: desc,
|
||||
search: this.state.search
|
||||
desc: desc
|
||||
}
|
||||
|
||||
this.retrieveUsers(newUsersParams);
|
||||
|
||||
this.setState({
|
||||
usersParams: newUsersParams
|
||||
});
|
||||
}
|
||||
|
||||
orderById(desc) {
|
||||
this.retrieveUsers({
|
||||
page: 1,
|
||||
const newUsersParams = {
|
||||
...this.state.usersParams,
|
||||
orderBy: 'id',
|
||||
desc: desc,
|
||||
search: this.state.search
|
||||
desc: desc
|
||||
}
|
||||
|
||||
this.retrieveUsers(newUsersParams);
|
||||
|
||||
this.setState({
|
||||
usersParams: newUsersParams
|
||||
});
|
||||
}
|
||||
|
||||
@ -178,24 +213,43 @@ class AdminPanelListUsers extends React.Component {
|
||||
onInviteUser(user) {
|
||||
ModalContainer.openModal(
|
||||
<div className="admin-panel-list-users__invite-user-form">
|
||||
<InviteUserWidget onSuccess={this.onInviteUserSuccess.bind(this)} />
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<Button onClick={ModalContainer.closeModal} type="link">{i18n('CLOSE')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
<InviteUserWidget
|
||||
onSuccess={this.onInviteUserSuccess.bind(this)}
|
||||
onChangeMessage={this.onChangeMessage.bind(this)} />
|
||||
</div>,
|
||||
{
|
||||
closeButton: {
|
||||
showCloseButton: true
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onChangeMessage(message) {
|
||||
this.setState({
|
||||
message,
|
||||
showMessage: true
|
||||
});
|
||||
}
|
||||
|
||||
onInviteUserSuccess() {
|
||||
ModalContainer.closeModal();
|
||||
|
||||
this.retrieveUsers(DEFAULT_USERS_PARAMS);
|
||||
}
|
||||
|
||||
onUsersRetrieved(result) {
|
||||
const { page, pages, users, orderBy, desc } = result.data;
|
||||
|
||||
this.setState({
|
||||
page: result.data.page * 1,
|
||||
pages: result.data.pages * 1,
|
||||
users: result.data.users,
|
||||
orderBy: result.data.orderBy,
|
||||
desc: (result.data.desc*1),
|
||||
usersParams: {
|
||||
...this.state.usersParams,
|
||||
page: page*1,
|
||||
orderBy: orderBy,
|
||||
desc: desc*1,
|
||||
},
|
||||
pages: pages*1,
|
||||
users: users,
|
||||
error: false,
|
||||
loading: false
|
||||
});
|
||||
@ -207,6 +261,12 @@ class AdminPanelListUsers extends React.Component {
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
|
||||
onCloseMessage(showMessage) {
|
||||
this.setState({
|
||||
[showMessage]: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default connect((store) => {
|
||||
|
@ -22,7 +22,15 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__success-message {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
&__add-user-form {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
&__invite-user-form {
|
||||
min-width: 700px;
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,11 @@ import Message from 'core-components/message';
|
||||
import InfoTooltip from 'core-components/info-tooltip';
|
||||
import Autocomplete from 'core-components/autocomplete';
|
||||
|
||||
const INITIAL_API_VALUE = {
|
||||
page: 1,
|
||||
departments: undefined,
|
||||
};
|
||||
|
||||
class AdminPanelViewUser extends React.Component {
|
||||
|
||||
state = {
|
||||
@ -26,11 +31,13 @@ class AdminPanelViewUser extends React.Component {
|
||||
loading: true,
|
||||
disabled: false,
|
||||
userList: [],
|
||||
message: ''
|
||||
message: '',
|
||||
showMessage: true
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.retrieveUser();
|
||||
this.retrieveUserTickets(INITIAL_API_VALUE);
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -45,37 +52,39 @@ class AdminPanelViewUser extends React.Component {
|
||||
renderInvalid() {
|
||||
return (
|
||||
<div className="admin-panel-view-user__invalid">
|
||||
<Message type="error">{i18n('INVALID_USER')}</Message>
|
||||
<Message showCloseButton={false} type="error">{i18n('INVALID_USER')}</Message>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderUserInfo() {
|
||||
const { name, disabled, email, verified, customfields, loading } = this.state;
|
||||
|
||||
return (
|
||||
<div className="admin-panel-view-user__content">
|
||||
<div className="admin-panel-view-user__info">
|
||||
<div className="admin-panel-view-user__info-item">
|
||||
{i18n('NAME')}
|
||||
<div className="admin-panel-view-user__info-box">
|
||||
{this.state.name}
|
||||
{(this.state.disabled) ? this.renderDisabled() : null}
|
||||
{name}
|
||||
{disabled ? this.renderDisabled() : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-panel-view-user__info-item">
|
||||
{i18n('EMAIL')}
|
||||
<div className="admin-panel-view-user__info-box">
|
||||
{this.state.email}
|
||||
{(!this.state.verified) ? this.renderNotVerified() : null}
|
||||
{email}
|
||||
{(!verified) ? this.renderNotVerified() : null}
|
||||
</div>
|
||||
</div>
|
||||
{this.state.customfields.map(this.renderCustomField.bind(this))}
|
||||
{customfields.map(this.renderCustomField.bind(this))}
|
||||
<div className="admin-panel-view-user__action-buttons">
|
||||
<Button
|
||||
className="admin-panel-view-user__action-button"
|
||||
onClick={this.onDisableClick.bind(this)}
|
||||
size="medium"
|
||||
type={this.state.disabled ? 'tertiary' : 'primary'}>
|
||||
{i18n(this.state.disabled ? 'ENABLE_USER' : 'DISABLE_USER')}
|
||||
type={disabled ? 'tertiary' : 'primary'}>
|
||||
{i18n(disabled ? 'ENABLE_USER' : 'DISABLE_USER')}
|
||||
</Button>
|
||||
<Button className="admin-panel-view-user__action-button" onClick={this.onDeleteClick.bind(this)} size="medium">
|
||||
{i18n('DELETE_AND_BAN')}
|
||||
@ -84,47 +93,65 @@ class AdminPanelViewUser extends React.Component {
|
||||
</div>
|
||||
<span className="separator" />
|
||||
<div className="admin-panel-view-user">
|
||||
<div className="admin-panel-view-user__supervised-users-header">{i18n('SUPERVISED_USER')}</div>
|
||||
<div className="admin-panel-view-user__supervised-users-container">
|
||||
<div className="admin-panel-view-user__supervised-users-header">{i18n('SUPERVISED_USER')}</div>
|
||||
<InfoTooltip className="admin-panel-view-user__info-tooltip" text={i18n('SUPERVISED_USER_INFORMATION')}/>
|
||||
</div>
|
||||
<div className="admin-panel-view-user__supervised-users-content">
|
||||
<Autocomplete
|
||||
onChange={this.onChangeValues.bind(this)}
|
||||
getItemListFromQuery={this.searchUsers}
|
||||
values={this.transformUserListToAutocomplete()}
|
||||
/>
|
||||
getItemListFromQuery={this.searchUsers.bind(this)}
|
||||
values={this.transformUserListToAutocomplete()} />
|
||||
<Button
|
||||
disabled = {this.state.loading}
|
||||
disabled={loading}
|
||||
type="secondary"
|
||||
className="admin-panel-view-user__submit-button"
|
||||
onClick={this.onClickSupervisorUserButton.bind(this)}
|
||||
size="medium"
|
||||
>
|
||||
{i18n('UPDATE')}
|
||||
size="medium">
|
||||
{i18n('UPDATE')}
|
||||
</Button>
|
||||
</div>
|
||||
{this.renderSupervisedUserMessage()}
|
||||
</div>
|
||||
<span className="separator" />
|
||||
<div className="admin-panel-view-user__tickets">
|
||||
<div className="admin-panel-view-user__tickets-title">{i18n('TICKETS')}</div>
|
||||
<TicketList {...this.getTicketListProps()}/>
|
||||
<div className="admin-panel-view-user__tickets-info-container">
|
||||
<div className="admin-panel-view-user__tickets-title">{i18n('TICKETS')}</div>
|
||||
<InfoTooltip className="admin-panel-view-user__info-tooltip" text={i18n('TICKETS_INFORMATION')}/>
|
||||
</div>
|
||||
<TicketList {...this.getTicketListProps()} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderSupervisedUserMessage(){
|
||||
if(this.state.message) {
|
||||
if(this.state.message != 'success'){
|
||||
return(
|
||||
<div className="admin-panel-view-user__supervised-users-message">
|
||||
<Message type="error">{i18n(this.state.message)}</Message>
|
||||
</div>
|
||||
)
|
||||
}else{
|
||||
return(
|
||||
<div className= "admin-panel-view-user__supervised-users-message">
|
||||
<Message type="success">{i18n('SUPERVISED_USERS_UPDATED')}</Message>
|
||||
</div>
|
||||
)
|
||||
const { message, showMessage } = this.state;
|
||||
|
||||
if(message) {
|
||||
if(message !== 'success') {
|
||||
return (
|
||||
<Message
|
||||
showMessage={showMessage}
|
||||
onCloseMessage={this.onCloseMessage.bind(this, "showMessage")}
|
||||
className="admin-panel-view-user__supervised-users-message"
|
||||
type="error">
|
||||
{i18n(message)}
|
||||
</Message>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Message
|
||||
showMessage={showMessage}
|
||||
onCloseMessage={this.onCloseMessage.bind(this, "showMessage")}
|
||||
className="admin-panel-view-user__supervised-users-message"
|
||||
type="success">
|
||||
{i18n('SUPERVISED_USERS_UPDATED')}
|
||||
</Message>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,11 +159,11 @@ class AdminPanelViewUser extends React.Component {
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
|
||||
|
||||
const userIdList = this.state.userList.map((item) => {
|
||||
return item.id;
|
||||
});
|
||||
|
||||
|
||||
API.call({
|
||||
path: '/user/edit-supervised-list',
|
||||
data: {
|
||||
@ -146,12 +173,14 @@ class AdminPanelViewUser extends React.Component {
|
||||
}).then(r => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
message: 'success'
|
||||
message: 'success',
|
||||
showMessage: true
|
||||
})
|
||||
}).catch((r) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
message: r.message
|
||||
message: r.message,
|
||||
showMessage: true
|
||||
})
|
||||
});
|
||||
}
|
||||
@ -161,7 +190,10 @@ class AdminPanelViewUser extends React.Component {
|
||||
userList: newList
|
||||
});
|
||||
}
|
||||
|
||||
searchUsers(query, blacklist = []) {
|
||||
blacklist = blacklist.map(item => {return {isStaff: item.isStaff, id: item.id}});
|
||||
|
||||
return API.call({
|
||||
path: '/ticket/search-authors',
|
||||
data: {
|
||||
@ -170,12 +202,16 @@ class AdminPanelViewUser extends React.Component {
|
||||
searchUsers: 1
|
||||
}
|
||||
}).then(r => {
|
||||
return r.data.authors.map(author => {
|
||||
const authorsListWithoutMe = r.data.authors.filter(author => author.id != this.props.params.userId);
|
||||
|
||||
return authorsListWithoutMe.map(author => {
|
||||
const { id, name } = author;
|
||||
|
||||
return {
|
||||
name: author.name,
|
||||
name,
|
||||
color: "gray",
|
||||
id: author.id*1,
|
||||
content: <div>{author.name}</div>,
|
||||
id: id*1,
|
||||
content: <div>{name}</div>,
|
||||
isStaff: false
|
||||
}});
|
||||
}).catch((r) => {
|
||||
@ -186,10 +222,12 @@ class AdminPanelViewUser extends React.Component {
|
||||
transformUserListToAutocomplete() {
|
||||
return(
|
||||
this.state.userList.map((user) => {
|
||||
const { id, name } = user;
|
||||
|
||||
return ({
|
||||
id: user.id*1,
|
||||
name: user.name,
|
||||
content: <div>{user.name}</div>,
|
||||
id: id*1,
|
||||
name,
|
||||
content: <div>{name}</div>,
|
||||
color: 'grey',
|
||||
isStaff: false
|
||||
});
|
||||
@ -209,37 +247,42 @@ class AdminPanelViewUser extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderCustomField(customfield) {
|
||||
renderCustomField(_customfield) {
|
||||
const { customfield, value, id } = _customfield;
|
||||
|
||||
return (
|
||||
<div className="admin-panel-view-user__info-item">
|
||||
{customfield.customfield}
|
||||
<div className="admin-panel-view-user__info-item" key={`customFieldId__${id}`}>
|
||||
{customfield}
|
||||
<div className="admin-panel-view-user__info-box">
|
||||
{customfield.value}
|
||||
{(value !== "") ? value : <div className="admin-panel-view-user__empty-content">Empty</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getTicketListProps() {
|
||||
const { tickets, loading } = this.state;
|
||||
|
||||
return {
|
||||
type: 'secondary',
|
||||
tickets: this.state.tickets,
|
||||
loading: this.state.loading,
|
||||
tickets,
|
||||
loading,
|
||||
departments: this.props.departments,
|
||||
ticketPath: '/admin/panel/tickets/view-ticket/'
|
||||
};
|
||||
}
|
||||
|
||||
onUserRetrieved(result) {
|
||||
const { name, email, verified, disabled, customfields, userList } = result.data;
|
||||
|
||||
this.setState({
|
||||
name: result.data.name,
|
||||
email: result.data.email,
|
||||
verified: result.data.verified,
|
||||
tickets: result.data.tickets,
|
||||
disabled: result.data.disabled,
|
||||
customfields: result.data.customfields,
|
||||
name,
|
||||
email,
|
||||
verified,
|
||||
disabled,
|
||||
customfields,
|
||||
loading: false,
|
||||
userList: result.data.userList
|
||||
userList
|
||||
});
|
||||
}
|
||||
|
||||
@ -289,6 +332,67 @@ class AdminPanelViewUser extends React.Component {
|
||||
invalid: true
|
||||
}));
|
||||
}
|
||||
|
||||
getTicketListProps() {
|
||||
const { departments, params } = this.props;
|
||||
const { tickets, page, pages, loading } = this.state;
|
||||
|
||||
return {
|
||||
type: 'secondary',
|
||||
userId: params.userId,
|
||||
tickets,
|
||||
loading,
|
||||
departments,
|
||||
ticketPath: '/admin/panel/tickets/view-ticket/',
|
||||
page,
|
||||
pages,
|
||||
onPageChange: this.onPageChange.bind(this),
|
||||
onDepartmentChange: this.onDepartmentChange.bind(this)
|
||||
};
|
||||
}
|
||||
|
||||
onPageChange(event) {
|
||||
this.setState({
|
||||
page: event.target.value
|
||||
});
|
||||
|
||||
this.retrieveUserTickets({page: event.target.value});
|
||||
}
|
||||
|
||||
onDepartmentChange(department) {
|
||||
this.setState({
|
||||
department
|
||||
});
|
||||
|
||||
this.retrieveUserTickets({
|
||||
department: department ? `[${department}]` : undefined
|
||||
});
|
||||
}
|
||||
|
||||
retrieveUserTickets({page, department}) {
|
||||
API.call({
|
||||
path: '/ticket/search',
|
||||
data: {
|
||||
page,
|
||||
departments: department,
|
||||
authors: `[{"id":${this.props.params.userId}, "isStaff":0}]`
|
||||
}
|
||||
}).then((result) => {
|
||||
const data = result.data;
|
||||
|
||||
this.setState({
|
||||
tickets: data.tickets,
|
||||
page: data.page,
|
||||
pages: data.pages
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onCloseMessage(showMessage) {
|
||||
this.setState({
|
||||
[showMessage]: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default connect((store) => {
|
||||
|
@ -9,6 +9,7 @@
|
||||
display: inline-block;
|
||||
margin-right: 20px;
|
||||
min-width: 200px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
&-box {
|
||||
@ -20,14 +21,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__empty-content {
|
||||
font-style: italic;
|
||||
color: $dark-grey;
|
||||
}
|
||||
|
||||
&__action-buttons {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
&__supervised-users-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&__action-button {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
&__tickets-info-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__tickets-title {
|
||||
font-size: $font-size--md;
|
||||
margin-bottom: 20px;
|
||||
@ -58,4 +73,7 @@
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
&__info-tooltip{
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import _ from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
|
||||
@ -13,12 +12,16 @@ import Form from 'core-components/form';
|
||||
import FormField from 'core-components/form-field';
|
||||
import Widget from 'core-components/widget';
|
||||
import Header from 'core-components/header';
|
||||
import Button from 'core-components/button';
|
||||
import ModalContainer from 'app-components/modal-container';
|
||||
import Loading from 'core-components/loading';
|
||||
|
||||
class InviteUserWidget extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
onSuccess: React.PropTypes.func,
|
||||
className: React.PropTypes.string
|
||||
className: React.PropTypes.string,
|
||||
onChangeMessage: React.PropTypes.func
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@ -27,7 +30,8 @@ class InviteUserWidget extends React.Component {
|
||||
this.state = {
|
||||
loading: false,
|
||||
email: null,
|
||||
customFields: []
|
||||
customFields: null,
|
||||
showMessage: true
|
||||
};
|
||||
}
|
||||
|
||||
@ -35,28 +39,45 @@ class InviteUserWidget extends React.Component {
|
||||
API.call({
|
||||
path: '/system/get-custom-fields',
|
||||
data: {}
|
||||
})
|
||||
.then(result => this.setState({customFields: result.data}));
|
||||
}).then(result => {
|
||||
this.setState({
|
||||
customFields: result.data
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Widget className={this.getClass()}>
|
||||
<Header title={i18n('INVITE_USER')} description={i18n('INVITE_USER_VIEW_DESCRIPTION')} />
|
||||
<Form {...this.getFormProps()}>
|
||||
<div className="invite-user-widget__inputs">
|
||||
<FormField {...this.getInputProps()} label={i18n('FULL_NAME')} name="name" validation="NAME" required/>
|
||||
<FormField {...this.getInputProps()} label={i18n('EMAIL')} name="email" validation="EMAIL" required/>
|
||||
{this.state.customFields.map(this.renderCustomField.bind(this))}
|
||||
</div>
|
||||
<div className="invite-user-widget__captcha">
|
||||
<Captcha ref="captcha"/>
|
||||
</div>
|
||||
<SubmitButton type="primary">{i18n('INVITE_USER')}</SubmitButton>
|
||||
</Form>
|
||||
if(!this.state.customFields) return this.renderLoading();
|
||||
|
||||
{this.renderMessage()}
|
||||
</Widget>
|
||||
return (
|
||||
<div className="invite-user-widget__modal-wrapper">
|
||||
<Widget className={this.getClass()}>
|
||||
<Header title={i18n('INVITE_USER')} description={i18n('INVITE_USER_VIEW_DESCRIPTION')} />
|
||||
<Form {...this.getFormProps()}>
|
||||
<div className="invite-user-widget__inputs">
|
||||
<FormField {...this.getInputProps()} label={i18n('FULL_NAME')} name="name" validation="NAME" required />
|
||||
<FormField {...this.getInputProps()} label={i18n('EMAIL')} name="email" validation="EMAIL" required />
|
||||
{this.state.customFields.map(this.renderCustomField.bind(this))}
|
||||
</div>
|
||||
<div className="invite-user-widget__captcha">
|
||||
<Captcha ref="captcha" />
|
||||
</div>
|
||||
<div className="invite-user-widget__buttons-container">
|
||||
<SubmitButton type="secondary">{i18n('INVITE_USER')}</SubmitButton>
|
||||
<Button onClick={(e) => {e.preventDefault(); ModalContainer.closeModal();}} type="link">{i18n('CANCEL')}</Button>
|
||||
</div>
|
||||
</Form>
|
||||
{this.renderMessage()}
|
||||
</Widget>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderLoading() {
|
||||
return (
|
||||
<div className="invite-user-widget__loading">
|
||||
<Loading backgrounded size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -68,7 +89,7 @@ class InviteUserWidget extends React.Component {
|
||||
key={key}
|
||||
label={customField.name}
|
||||
infoMessage={customField.description}
|
||||
field="input"/>
|
||||
field="input" />
|
||||
);
|
||||
} else {
|
||||
const items = customField.options.map(option => ({content: option.name, value: option.name}));
|
||||
@ -80,17 +101,35 @@ class InviteUserWidget extends React.Component {
|
||||
label={customField.name}
|
||||
infoMessage={customField.description}
|
||||
field="select"
|
||||
fieldProps={{size:'medium', items}}/>
|
||||
fieldProps={{size: 'medium', items}} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderMessage() {
|
||||
switch (this.state.message) {
|
||||
case 'success':
|
||||
return <Message type="success">{i18n('INVITE_USER_SUCCESS')}</Message>;
|
||||
const { message, showMessage } = this.state;
|
||||
|
||||
switch (message) {
|
||||
case 'success': // TODO Remove this message case
|
||||
return (
|
||||
<Message
|
||||
showMessage={showMessage}
|
||||
onCloseMessage={this.onCloseMessage.bind(this, "showMessage")}
|
||||
className="invite-user-widget__success-message"
|
||||
type="success">
|
||||
{i18n('INVITE_USER_SUCCESS')}
|
||||
</Message>
|
||||
);
|
||||
case 'fail':
|
||||
return <Message type="error">{i18n('EMAIL_EXISTS')}</Message>;
|
||||
return (
|
||||
<Message
|
||||
showMessage={showMessage}
|
||||
onCloseMessage={this.onCloseMessage.bind(this, "showMessage")}
|
||||
className="invite-user-widget__error-message"
|
||||
type="error">
|
||||
{i18n('EMAIL_EXISTS')}
|
||||
</Message>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@ -148,16 +187,35 @@ class InviteUserWidget extends React.Component {
|
||||
}
|
||||
|
||||
onInviteUserSuccess() {
|
||||
const { onSuccess, onChangeMessage } = this.props;
|
||||
const message = 'success';
|
||||
|
||||
this.setState({
|
||||
loading: false,
|
||||
message: 'success'
|
||||
message,
|
||||
showMessage: true
|
||||
});
|
||||
|
||||
onChangeMessage && onChangeMessage(message);
|
||||
onSuccess && onSuccess();
|
||||
}
|
||||
|
||||
onInviteUserFail() {
|
||||
const { onChangeMessage } = this.props;
|
||||
const message = 'fail';
|
||||
|
||||
this.setState({
|
||||
loading: false,
|
||||
message: 'fail'
|
||||
message,
|
||||
showMessage: true
|
||||
});
|
||||
|
||||
onChangeMessage && onChangeMessage(message);
|
||||
}
|
||||
|
||||
onCloseMessage(showMessage) {
|
||||
this.setState({
|
||||
[showMessage]: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
.invite-user-widget {
|
||||
padding: 30px;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
|
||||
&__form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
&__inputs {
|
||||
@ -16,4 +15,30 @@
|
||||
height: 78px;
|
||||
width: 304px;
|
||||
}
|
||||
|
||||
&__buttons-container {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__success-message,
|
||||
&__error-message {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
&__modal-wrapper {
|
||||
min-width: 700px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
&__loading {
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: $grey;
|
||||
}
|
||||
}
|
||||
|
@ -4,10 +4,9 @@ import store from 'app/store';
|
||||
import ConfigActions from 'actions/config-actions';
|
||||
import i18n from 'lib-app/i18n';
|
||||
|
||||
import Header from 'core-components/header';
|
||||
import Message from 'core-components/message';
|
||||
|
||||
class InstallStep7Completed extends React.Component {
|
||||
class InstallCompleted extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
store.dispatch(ConfigActions.init());
|
||||
@ -19,9 +18,8 @@ class InstallStep7Completed extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="install-step-7">
|
||||
<Header title={i18n('STEP_TITLE', {title: i18n('COMPLETED'), current: 7, total: 7})} description={i18n('STEP_7_DESCRIPTION')}/>
|
||||
<Message title={i18n('INSTALLATION_COMPLETED')} type="success">
|
||||
<div className="install-completed">
|
||||
<Message showCloseButton={false} title={i18n('INSTALLATION_COMPLETED_TITLE')} type="success">
|
||||
{i18n('INSTALLATION_COMPLETED_DESCRIPTION')}
|
||||
</Message>
|
||||
</div>
|
||||
@ -29,4 +27,4 @@ class InstallStep7Completed extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default InstallStep7Completed;
|
||||
export default InstallCompleted;
|
@ -14,7 +14,7 @@ class InstallStep1Language extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="install-step-1">
|
||||
<Header title={i18n('STEP_TITLE', {title: i18n('SELECT_LANGUAGE'), current: 1, total: 7})} description={i18n('STEP_1_DESCRIPTION')}/>
|
||||
<Header title={i18n('STEP_TITLE', {title: i18n('SELECT_LANGUAGE'), current: 1, total: 6})} description={i18n('STEP_1_DESCRIPTION')} />
|
||||
<LanguageSelector {...this.getLanguageSelectorProps()} />
|
||||
<div className="install-step-1__button">
|
||||
<Button size="medium" type="secondary" onClick={() => history.push('/install/step-2')}>
|
||||
|
@ -30,7 +30,7 @@ class InstallStep2Requirements extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="install-step-2">
|
||||
<Header title={i18n('STEP_TITLE', {title: i18n('SERVER_REQUIREMENTS'), current: 2, total: 7})} description={i18n('STEP_2_DESCRIPTION')}/>
|
||||
<Header title={i18n('STEP_TITLE', {title: i18n('SERVER_REQUIREMENTS'), current: 2, total: 6})} description={i18n('STEP_2_DESCRIPTION')} />
|
||||
<div className="install-step-2__refresh">
|
||||
<Button className="install-step-2__refresh-button" type="secondary" size="medium" onClick={this.retrieveRequirements.bind(this)}>
|
||||
<Icon className="install-step-2__refresh-icon" name="refresh" /> {i18n('REFRESH')}
|
||||
|
@ -17,6 +17,7 @@ class InstallStep3Database extends React.Component {
|
||||
state = {
|
||||
loading: false,
|
||||
error: false,
|
||||
showErrorMessage: true,
|
||||
errorMessage: ''
|
||||
};
|
||||
|
||||
@ -24,7 +25,7 @@ class InstallStep3Database extends React.Component {
|
||||
const { loading } = this.state;
|
||||
return (
|
||||
<div className="install-step-3">
|
||||
<Header title={i18n('STEP_TITLE', {title: i18n('DATABASE_CONFIGURATION'), current: 3, total: 7})} description={i18n('STEP_3_DESCRIPTION')}/>
|
||||
<Header title={i18n('STEP_TITLE', {title: i18n('DATABASE_CONFIGURATION'), current: 3, total: 6})} description={i18n('STEP_3_DESCRIPTION')} />
|
||||
{this.renderMessage()}
|
||||
<Form loading={loading} onSubmit={this.onSubmit.bind(this)}>
|
||||
<FormField name="dbHost" label={i18n('DATABASE_HOST')} fieldProps={{size: 'large'}} required/>
|
||||
@ -42,17 +43,19 @@ class InstallStep3Database extends React.Component {
|
||||
}
|
||||
|
||||
renderMessage() {
|
||||
let message = null;
|
||||
const { error, errorMessage, showErrorMessage } = this.state;
|
||||
|
||||
if(this.state.error) {
|
||||
message = (
|
||||
<Message className="install-step-3__message" type="error">
|
||||
{i18n('ERROR_UPDATING_SETTINGS')}: {this.state.errorMessage}
|
||||
</Message>
|
||||
);
|
||||
}
|
||||
|
||||
return message;
|
||||
return (
|
||||
error ?
|
||||
<Message
|
||||
showMessage={showErrorMessage}
|
||||
onCloseMessage={this.onCloseMessage.bind(this, "showErrorMessage")}
|
||||
className="install-step-3__message"
|
||||
type="error">
|
||||
{i18n('ERROR_UPDATING_SETTINGS')}: {errorMessage}
|
||||
</Message> :
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
onPreviousClick(event) {
|
||||
@ -72,10 +75,17 @@ class InstallStep3Database extends React.Component {
|
||||
.catch(({message}) => this.setState({
|
||||
loading: false,
|
||||
error: true,
|
||||
showErrorMessage: true,
|
||||
errorMessage: message
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
onCloseMessage(showMessage) {
|
||||
this.setState({
|
||||
[showMessage]: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default InstallStep3Database;
|
||||
|
@ -24,7 +24,7 @@ class InstallStep4UserSystem extends React.Component {
|
||||
const { form } = this.state
|
||||
return (
|
||||
<div className="install-step-4">
|
||||
<Header title={i18n('STEP_TITLE', {title: i18n('USER_SYSTEM'), current: 4, total: 7})} description={i18n('STEP_4_DESCRIPTION')}/>
|
||||
<Header title={i18n('STEP_TITLE', {title: i18n('USER_SYSTEM'), current: 4, total: 6})} description={i18n('STEP_4_DESCRIPTION')} />
|
||||
<Form onSubmit={this.onSubmit.bind(this)} values={form} onChange={this.onChange.bind(this)}>
|
||||
<div className="install-step-4__container">
|
||||
<FormField
|
||||
|
@ -20,6 +20,7 @@ class InstallStep5Settings extends React.Component {
|
||||
loading: false,
|
||||
form: {},
|
||||
error: false,
|
||||
showErrorMessage: true,
|
||||
errorMessage: ''
|
||||
};
|
||||
|
||||
@ -31,7 +32,7 @@ class InstallStep5Settings extends React.Component {
|
||||
|
||||
return (
|
||||
<div className="install-step-5">
|
||||
<Header title={i18n('STEP_TITLE', {title: i18n('SETTINGS'), current: 5, total: 7})} description={i18n('STEP_5_DESCRIPTION')}/>
|
||||
<Header title={i18n('STEP_TITLE', {title: i18n('SETTINGS'), current: 5, total: 6})} description={i18n('STEP_5_DESCRIPTION')} />
|
||||
{this.renderMessage()}
|
||||
<Form loading={loading} onSubmit={this.onSubmit.bind(this)} value={form} onChange={(form) => this.setState({form})}>
|
||||
<FormField name="title" label={i18n('TITLE')} fieldProps={{size: 'large'}} required/>
|
||||
@ -65,17 +66,19 @@ class InstallStep5Settings extends React.Component {
|
||||
}
|
||||
|
||||
renderMessage() {
|
||||
let message = null;
|
||||
const { error, errorMessage, showErrorMessage } = this.state;
|
||||
|
||||
if(this.state.error) {
|
||||
message = (
|
||||
<Message className="install-step-5__message" type="error">
|
||||
{i18n('ERROR_UPDATING_SETTINGS')}: {this.state.errorMessage}
|
||||
</Message>
|
||||
);
|
||||
}
|
||||
|
||||
return message;
|
||||
return (
|
||||
error ?
|
||||
<Message
|
||||
showMessage={showErrorMessage}
|
||||
onCloseMessage={this.onCloseMessage.bind(this, "showErrorMessage")}
|
||||
className="install-step-5__message"
|
||||
type="error">
|
||||
{i18n('ERROR_UPDATING_SETTINGS')}: {errorMessage}
|
||||
</Message> :
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
onTestSMTPClick(event) {
|
||||
@ -125,10 +128,17 @@ class InstallStep5Settings extends React.Component {
|
||||
.catch(({message}) => this.setState({
|
||||
loading: false,
|
||||
error: true,
|
||||
showErrorMessage: true,
|
||||
errorMessage: message
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
onCloseMessage(showMessage) {
|
||||
this.setState({
|
||||
[showMessage]: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default connect((store) => {
|
||||
|
@ -15,13 +15,14 @@ class InstallStep6Admin extends React.Component {
|
||||
state = {
|
||||
loading: false,
|
||||
error: false,
|
||||
showErrorMessage: true,
|
||||
errorMessage: ''
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="install-step-6">
|
||||
<Header title={i18n('STEP_TITLE', {title: i18n('ADMIN_SETUP'), current: 6, total: 7})} description={i18n('STEP_6_DESCRIPTION')}/>
|
||||
<Header title={i18n('STEP_TITLE', {title: i18n('ADMIN_SETUP'), current: 6, total: 6})} description={i18n('STEP_6_DESCRIPTION')} />
|
||||
{this.renderMessage()}
|
||||
<Form onSubmit={this.onSubmit.bind(this)}>
|
||||
<FormField name="name" validation="NAME" label={i18n('ADMIN_NAME')} fieldProps={{size: 'large'}} required/>
|
||||
@ -36,17 +37,19 @@ class InstallStep6Admin extends React.Component {
|
||||
}
|
||||
|
||||
renderMessage() {
|
||||
let message = null;
|
||||
const { error, errorMessage, showErrorMessage } = this.state;
|
||||
|
||||
if(this.state.error) {
|
||||
message = (
|
||||
<Message className="install-step-6_message" type="error">
|
||||
{i18n('ERROR_UPDATING_SETTINGS')}: {this.state.errorMessage}
|
||||
</Message>
|
||||
);
|
||||
}
|
||||
|
||||
return message;
|
||||
return (
|
||||
error ?
|
||||
<Message
|
||||
showMessage={showErrorMessage}
|
||||
onCloseMessage={this.onCloseMessage.bind(this, "showErrorMessage")}
|
||||
className="install-step-6_message"
|
||||
type="error">
|
||||
{i18n('ERROR_UPDATING_SETTINGS')}: {errorMessage}
|
||||
</Message> :
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
onSubmit(form) {
|
||||
@ -57,14 +60,21 @@ class InstallStep6Admin extends React.Component {
|
||||
path: '/system/init-admin',
|
||||
data: form
|
||||
})
|
||||
.then(() => history.push('/install/step-7'))
|
||||
.then(() => history.push('/install/completed'))
|
||||
.catch(({message}) => this.setState({
|
||||
loading: false,
|
||||
error: true,
|
||||
showErrorMessage: true,
|
||||
errorMessage: message
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
onCloseMessage(showMessage) {
|
||||
this.setState({
|
||||
[showMessage]: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default InstallStep6Admin;
|
||||
export default InstallStep6Admin;
|
||||
|
@ -2,7 +2,6 @@ import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import history from 'lib-app/history';
|
||||
import i18n from 'lib-app/i18n';
|
||||
import API from 'lib-app/api-call';
|
||||
import SessionStore from 'lib-app/session-store';
|
||||
@ -18,6 +17,12 @@ import FormField from 'core-components/form-field';
|
||||
import SubmitButton from 'core-components/submit-button';
|
||||
import Message from 'core-components/message';
|
||||
|
||||
const DEFAULT_CREATE_TICKET_FORM_VALUE = {
|
||||
title: '',
|
||||
email: '',
|
||||
name: ''
|
||||
};
|
||||
|
||||
class CreateTicketForm extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
@ -35,46 +40,53 @@ class CreateTicketForm extends React.Component {
|
||||
loading: false,
|
||||
message: null,
|
||||
form: {
|
||||
title: '',
|
||||
...DEFAULT_CREATE_TICKET_FORM_VALUE,
|
||||
content: TextEditor.createEmpty(),
|
||||
departmentIndex: getPublicDepartmentIndexFromDepartmentId(this.props.defaultDepartmentId),
|
||||
email: '',
|
||||
name: '',
|
||||
departmentIndex: getPublicDepartmentIndexFromDepartmentId(this.props.defaultDepartmentId, SessionStore.getDepartments()),
|
||||
language: this.props.language
|
||||
}
|
||||
},
|
||||
showMessage: true
|
||||
};
|
||||
|
||||
render() {
|
||||
const { userLogged, isDefaultDepartmentLocked, isStaff, onlyOneSupportedLanguage, allowAttachments } = this.props;
|
||||
|
||||
return (
|
||||
<div className="create-ticket-form">
|
||||
<Header title={i18n('CREATE_TICKET')} description={i18n('CREATE_TICKET_DESCRIPTION')} />
|
||||
<Form {...this.getFormProps()}>
|
||||
{(!this.props.userLogged) ? this.renderEmailAndName() : null}
|
||||
<FormField label={i18n('TITLE')} name="title" validation="TITLE" required field="input" fieldProps={{size: 'large'}}/>
|
||||
{(!userLogged) ? this.renderEmailAndName() : null}
|
||||
<FormField label={i18n('TITLE')} name="title" validation="TITLE" required field="input" fieldProps={{size: 'large'}} />
|
||||
<div className="row">
|
||||
{!(this.props.isDefaultDepartmentLocked*1) || this.props.isStaff ?
|
||||
{!(isDefaultDepartmentLocked*1) || isStaff ?
|
||||
<FormField className="col-md-5" label={i18n('DEPARTMENT')} name="departmentIndex" field="select" decorator={DepartmentDropdown} fieldProps={{
|
||||
departments: SessionStore.getDepartments(),
|
||||
size: 'medium'
|
||||
}} /> : null
|
||||
}
|
||||
{!this.props.onlyOneSupportedLanguage ?
|
||||
{!onlyOneSupportedLanguage ?
|
||||
<FormField className="col-md-5" label={i18n('LANGUAGE')} name="language" field="select" decorator={LanguageSelector} fieldProps={{
|
||||
type: 'supported',
|
||||
size: 'medium'
|
||||
}}/> : null
|
||||
}} /> : null
|
||||
}
|
||||
</div>
|
||||
<FormField
|
||||
label={i18n('CONTENT')}
|
||||
name="content"
|
||||
validation="TEXT_AREA"
|
||||
fieldProps={{allowImages: this.props.allowAttachments}}
|
||||
fieldProps={{allowImages: allowAttachments}}
|
||||
required
|
||||
field="textarea" />
|
||||
{(this.props.allowAttachments) ? this.renderFileUpload() : null}
|
||||
{(!this.props.userLogged) ? this.renderCaptcha() : null}
|
||||
<SubmitButton>{i18n('CREATE_TICKET')}</SubmitButton>
|
||||
<div className="create-ticket-form__container">
|
||||
<div className={`create-ticket-form__buttons-container${allowAttachments ? "" : "-without-allow-attachments"}`}>
|
||||
{allowAttachments ? this.renderFileUpload() : null}
|
||||
<SubmitButton type="secondary">{i18n('CREATE_TICKET')}</SubmitButton>
|
||||
</div>
|
||||
<div className="create-ticket-form__captcha-container">
|
||||
{(!userLogged) ? this.renderCaptcha() : null}
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
{this.renderMessage()}
|
||||
</div>
|
||||
@ -84,8 +96,8 @@ class CreateTicketForm extends React.Component {
|
||||
renderEmailAndName() {
|
||||
return (
|
||||
<div className="row">
|
||||
<FormField className="col-md-6" label={i18n('EMAIL')} name="email" validation="EMAIL" required field="input" fieldProps={{size: 'large'}}/>
|
||||
<FormField className="col-md-6" label={i18n('FULL_NAME')} name="name" validation="NAME" required field="input" fieldProps={{size: 'large'}}/>
|
||||
<FormField className="col-md-6" label={i18n('EMAIL')} name="email" validation="EMAIL" required field="input" fieldProps={{size: 'large'}} />
|
||||
<FormField className="col-md-6" label={i18n('FULL_NAME')} name="name" validation="NAME" required field="input" fieldProps={{size: 'large'}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -101,27 +113,49 @@ class CreateTicketForm extends React.Component {
|
||||
renderCaptcha() {
|
||||
return (
|
||||
<div className="create-ticket-form__captcha">
|
||||
<Captcha ref="captcha"/>
|
||||
<Captcha ref="captcha" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderMessage() {
|
||||
switch (this.state.message) {
|
||||
case 'success':
|
||||
return <Message className="create-ticket-form__message" type="success">{i18n('TICKET_SENT')}</Message>;
|
||||
const { message, showMessage } = this.state;
|
||||
|
||||
switch (message) {
|
||||
case 'success': // TODO Remove this message case
|
||||
return (
|
||||
this.props.userLogged ?
|
||||
<Message
|
||||
showMessage={showMessage}
|
||||
onCloseMessage={this.onCloseMessage.bind(this, "showMessage")}
|
||||
className="create-ticket-form__message"
|
||||
type="success">
|
||||
{i18n('TICKET_SENT')}
|
||||
</Message> :
|
||||
null
|
||||
);
|
||||
case 'fail':
|
||||
return <Message className="create-ticket-form__message" type="error">{i18n('TICKET_SENT_ERROR')}</Message>;
|
||||
return (
|
||||
<Message
|
||||
showMessage={showMessage}
|
||||
onCloseMessage={this.onCloseMessage.bind(this, "showMessage")}
|
||||
className="create-ticket-form__message"
|
||||
type="error">
|
||||
{i18n('TICKET_SENT_ERROR')}
|
||||
</Message>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
getFormProps() {
|
||||
const { loading, form } = this.state;
|
||||
|
||||
return {
|
||||
loading: this.state.loading,
|
||||
loading,
|
||||
onSubmit: this.onSubmit.bind(this),
|
||||
values: this.state.form,
|
||||
values: form,
|
||||
onChange: form => this.setState({form})
|
||||
};
|
||||
}
|
||||
@ -147,28 +181,48 @@ class CreateTicketForm extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
onTicketSuccess(email, result) {
|
||||
let message = 'success'
|
||||
this.setState({
|
||||
loading: false,
|
||||
message: message
|
||||
}, () => {
|
||||
if(this.props.onSuccess) {
|
||||
this.props.onSuccess(result, email, message);
|
||||
}
|
||||
});
|
||||
onTicketSuccess() {
|
||||
const { onSuccess, userLogged, language } = this.props;
|
||||
const { form } = this.state;
|
||||
const message = 'success';
|
||||
|
||||
this.setState(
|
||||
{
|
||||
loading: false,
|
||||
message,
|
||||
showMessage: true,
|
||||
form: !userLogged ?
|
||||
{
|
||||
...form,
|
||||
...DEFAULT_CREATE_TICKET_FORM_VALUE,
|
||||
content: TextEditor.createEmpty(),
|
||||
departmentIndex: getPublicDepartmentIndexFromDepartmentId(this.props.defaultDepartmentId, SessionStore.getDepartments()),
|
||||
language
|
||||
} :
|
||||
form
|
||||
},
|
||||
() => {onSuccess && onSuccess(message);}
|
||||
);
|
||||
}
|
||||
|
||||
onTicketFail() {
|
||||
this.setState({
|
||||
loading: false,
|
||||
message: 'fail'
|
||||
message: 'fail',
|
||||
showMessage: true
|
||||
});
|
||||
}
|
||||
|
||||
onCloseMessage(showMessage) {
|
||||
this.setState({
|
||||
[showMessage]: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default connect((store) => {
|
||||
const { language, supportedLanguages } = store.config;
|
||||
|
||||
return {
|
||||
language: _.includes(supportedLanguages, language) ? language : supportedLanguages[0],
|
||||
onlyOneSupportedLanguage: supportedLanguages.length == 1 ? true : false,
|
||||
|
@ -8,6 +8,31 @@
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
&__buttons-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
z-index: 999;
|
||||
|
||||
&-without-allow-attachments {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
z-index: 999;
|
||||
}
|
||||
}
|
||||
|
||||
&__captcha-container {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
&__captcha {
|
||||
margin: 0 auto 20px;
|
||||
height: 78px;
|
||||
|
@ -1,12 +1,15 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {connect} from 'react-redux';
|
||||
import history from 'lib-app/history';
|
||||
import queryString from 'query-string';
|
||||
|
||||
import history from 'lib-app/history';
|
||||
import i18n from 'lib-app/i18n';
|
||||
|
||||
import SessionActions from 'actions/session-actions';
|
||||
import CreateTicketForm from 'app/main/dashboard/dashboard-create-ticket/create-ticket-form';
|
||||
|
||||
import Widget from 'core-components/widget';
|
||||
import Message from 'core-components/message';
|
||||
|
||||
class DashboardCreateTicketPage extends React.Component {
|
||||
|
||||
@ -14,6 +17,10 @@ class DashboardCreateTicketPage extends React.Component {
|
||||
userSystemEnabled: React.PropTypes.bool
|
||||
};
|
||||
|
||||
state = {
|
||||
showMessage: !!queryString.parse(window.location.search)["message"]
|
||||
};
|
||||
|
||||
render() {
|
||||
let Wrapper = 'div';
|
||||
|
||||
@ -23,21 +30,26 @@ class DashboardCreateTicketPage extends React.Component {
|
||||
|
||||
return (
|
||||
<div className={this.getClass()}>
|
||||
<Wrapper>
|
||||
<CreateTicketForm
|
||||
userLogged={(this.props.location.pathname !== '/create-ticket')}
|
||||
onSuccess={this.onCreateTicketSuccess.bind(this)}/>
|
||||
<Wrapper className="dashboard-create-ticket-page__container">
|
||||
<Message // TODO Remove this message
|
||||
showMessage={this.state.showMessage}
|
||||
onCloseMessage={this.onCloseMessage.bind(this)}
|
||||
className="dashboard-create-ticket-page__message"
|
||||
type="success">
|
||||
{i18n('TICKET_NUMBER_SENT')}
|
||||
</Message>
|
||||
<div className={this.getCreateTicketFormClass()}>
|
||||
<CreateTicketForm
|
||||
userLogged={(this.props.location.pathname !== '/create-ticket')}
|
||||
onSuccess={this.onCreateTicketSuccess.bind(this)} />
|
||||
</div>
|
||||
</Wrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onCreateTicketSuccess(result, email, message) {
|
||||
if((this.props.location.pathname !== '/create-ticket')) {
|
||||
history.push(`/dashboard?message=${message}`);
|
||||
} else {
|
||||
setTimeout(() => {history.push('/check-ticket/' + result.data.ticketNumber + '/' + email)}, 1000);
|
||||
}
|
||||
onCreateTicketSuccess(message) {
|
||||
history.push(`${(this.props.location.pathname !== '/create-ticket') ? "/dashboard" : "/"}?message=${message}`);
|
||||
}
|
||||
|
||||
getClass() {
|
||||
@ -49,6 +61,21 @@ class DashboardCreateTicketPage extends React.Component {
|
||||
|
||||
return classNames(classes);
|
||||
}
|
||||
|
||||
getCreateTicketFormClass() {
|
||||
let classes = {
|
||||
'dashboard-create-ticket-page__create-ticket-form': true,
|
||||
'dashboard-create-ticket-page__create-ticket-form__hidden': !!queryString.parse(window.location.search)["message"]
|
||||
};
|
||||
|
||||
return classNames(classes);
|
||||
}
|
||||
|
||||
onCloseMessage() {
|
||||
this.setState({
|
||||
showMessage: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default connect((store) => {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user