Compare commits

...

225 Commits

Author SHA1 Message Date
Maximiliano Redigonda
20720ca4f9
[DEV-371] Improve README (#1219) 2022-06-10 18:03:30 -03:00
Maxi Redigonda
40016635a8 [DEV-367] Use FileDownloader instead of fopen 2022-06-03 16:42:57 -03:00
Maxi Redigonda
68b2d2bf63 [DEV-365] Remove page size selector from ticket list in user view 2022-06-02 18:26:51 -03:00
Maximiliano Redigonda
36c5f3264b
[DEV-143] Handle expired sessions (#1192) 2022-06-02 11:39:31 -03:00
Maximiliano Redigonda
82fd54ffd9
[DEV-108] Change component to select date ranges (#1216)
Update console for better API request/response logs

Add some logs

Remove dateRange and make period select work

Fix package files

Fix stats path

WIP

Fix stats for single staff members

Remove logger
2022-06-01 07:08:36 -03:00
Maximiliano Redigonda
8c732f5dda
[DEV-190] Fix tests in Github Actions (#1217)
* Silly test

* Add better debugging, try starting service

* Try stopping and re-starting the mysql service, the old way

* Check status before running MySQL command

* Now that indeed mysql is not started, make it start

* [DEV-190] Try removing background-running of make run

* Ignore mysql service when not running at the beginning

* [DEV-190] Mount folder with instructions on how to initialize

* [DEV-190] Go back to using normal make run

* Adding -v command to mount volume with init scripts

* Remove SSH terminal to Github Actions instance

* Remove enabling of MySQL

* Remove unnecessary parts from Makefile
2022-06-01 07:00:00 -03:00
Joel Elias Méndez
b620baf5ed
[DEV-268] Fix long loading time in homepage (#1206) 2022-05-30 16:04:32 -03:00
LautaroCesso
d3b47eaa73
[DEV-262] Fix i18n in email header url description (#1212) 2022-05-24 10:20:25 -03:00
LautaroCesso
90e7923aec
[DEV-300] Fix error in upload file ticket comment (#1211) 2022-05-16 17:54:32 -03:00
LautaroCesso
0ecf88237f
[DEV-340] Refactor edit ticket comment no permission validations (#1207)
* Refactor edit ticket comment no permission validations

* Refactor edit comment path
2022-05-16 17:36:06 -03:00
Maximiliano Redigonda
713a5b5ee1
Try to start server before truncate db (#1208) 2022-05-16 17:32:50 -03:00
LautaroCesso
b578a26225
[DEV-339] Fix DropDown style (#1205)
* Fix DropDown style

* xd
2022-05-16 15:07:47 -03:00
Joel Elias Méndez
93fa9e12a3
[DEV-332] Create ruby tests for the new page size parameter (#1196) 2022-05-16 13:45:12 -03:00
Maximiliano Redigonda
3720baf370
[DEV-190] Fix tests (#1204)
* Add sudo

* Try with mysql instead of mysqld
2022-05-11 10:39:50 -03:00
Maximiliano Redigonda
b1ef69c60c
Revert "Fix undefined error (#1129)" (#1203)
This reverts commit a5da776d2734f7aae1091d084f0eea978edb57ff.
2022-05-09 19:19:54 -03:00
Maximiliano Redigonda
615f42a91b
[DEV-190] Fix tests (mysql socket error, initialize mysqld first) (#1202)
* [DEV-190] Fix tests v2

* Try changing to 127.0.0.1

* Try maybe 0.0.0.0 ?

* Try forcing tcp protocol

* Test with equal symbol

* Try with starting the service first

* Add command to start mysqld to both install actions in Makefile

* Remove mounting volume to check who's the culprit
2022-05-05 15:19:16 -03:00
Maximiliano Redigonda
39f8a601db
[DEV-190] Fix tests (#1201)
* [DEV-190] Add extra setup step before running tests

* 🤦
2022-05-04 12:14:03 -03:00
LautaroCesso
c82aaa001e
[DEV-318] Add edit comment tests (#1194)
* WIP Fix ruby tests

* WIP

* WIP

* Add edit ticket comment test
2022-05-04 11:30:37 -03:00
Maximiliano Redigonda
04923b0e9d
[DEV-190] Migrate tests from Travis CI to GitHub Actions (#1198)
* [DEV-315] Create docker routine for frontend that works on Mac

* Add make option to run bash inside container

* [DEV-190] Migrate tests from Travis CI to Github Actions

* Make install step not interactive

* Run build steps before trying to run tests

* Setup vendor permissions prior to running tests

* Add command to setup permissions in files folder too

* Test tests failing

* Move setup vendor permissions into make install, corrects tests

* Revert "Move setup vendor permissions into make install, corrects tests"

This reverts commit 8092cad24cbf73664905e86a188bb1ab79ee9377.

* Revert "Test tests failing"

This reverts commit 57fd123c559be6fd8eb9d5501e426f22f9647a8c.
2022-05-04 11:06:52 -03:00
Maximiliano Redigonda
5e7f39df05
[DEV-315] Create docker routine for frontend that works on Mac (#1175)
* [DEV-315] Create docker routine for frontend that works on Mac

* Add make option to run bash inside container
2022-05-04 11:06:17 -03:00
Maximiliano Redigonda
e190710252
[DEV-299] Fix missing validations (#1195) 2022-05-02 17:41:16 -03:00
Maximiliano Redigonda
a74d6ab4d7
[DEV-328] Fix array offset on null in /user/login (#1185) 2022-05-02 16:48:14 -03:00
Daniele Scasciafratte
a5da776d27
Fix undefined error (#1129) 2022-04-29 16:55:32 -03:00
Guillermo Giuliana
41d7aa5406
[DEV-302] Fix custom filed error message padding (#1190)
* add en translation

* add margin

* add i18n to error

* add minor changes

* add en translations
2022-04-25 18:16:07 -03:00
Guillermo Giuliana
f2adb160be
remove header options when maintenance-mode is on (#1189) 2022-04-25 17:55:35 -03:00
LautaroCesso
62bd70cc3b
[DEV-318] Make staffs able to edit their own content (#1187)
* Fix edit ticket comment permissions

* wip

* Rename ticketEventToArray variable
2022-04-25 12:43:25 -03:00
LautaroCesso
0f6c64674e
Fix ruby tests (#1188)
* Fix ruby tests

* Add pageSize default value
2022-04-25 12:38:44 -03:00
LautaroCesso
861f7dc254
[DEV-214] Fix email template validation (#1136)
* Fix email template validation

* Fix bug with default email templates

* Use template codes instead of selected index in text3 email template field

* Fix bug in no email template forms

* Use componentDidUpdate method
2022-04-19 19:28:51 -03:00
Joel Elias Méndez
83b8e8094b
[DEV-195] Fix php documentation (#1134)
* fix php documentation

* add descriptions to backend docs

* add missing php documentation

* correct some documentation details
2022-04-19 13:18:23 -03:00
Joel Elias Méndez
81cc579d57
[DEV-260] Make modal in CSV import actually check if the password entered is correct (#1155)
* check for correct password when uploading csv file

* allow to import only csv files

* add validation for password in csv import
2022-04-19 13:18:04 -03:00
Joel Elias Méndez
b3c8819d83
[DEV-279] Make ticket table show more tickets overview (#1164)
* make ticket table show more tickets overview

* allow user to choose ticket quantity

* add styles

* fix New Tickets section dropdown

* add commented changes

* correct some code issues
2022-04-19 13:17:48 -03:00
Joel Elias Méndez
86cad910ec
[DEV-222] Search fails when staff has no departments assigned (#1172)
* commented pr

* fix no department assigned issue

* undo irrelevants changes
2022-04-19 13:16:31 -03:00
Guillermo Giuliana
f890fdc2d3
[DEV-161] Add apikey to ticket comment path (#1173)
* update ticket comment path

* update validations

* remove useless line
2022-03-29 13:48:46 -03:00
Maximiliano Redigonda
c64eb9930b
[DEV-258] Add log commands for PHP debugging (#1167) 2022-03-25 19:03:09 -03:00
LautaroCesso
74870c632a
[DEV-290] Fix file attachment in SaaS instances (#1171)
* Update db schema

* Set all AUTO_INCREMENT to 1
2022-03-25 17:25:52 -03:00
Maximiliano Redigonda
e0738f1c88
[DEV-270] Increase token size in file names (#1165) 2022-03-16 09:19:12 -03:00
Maximiliano Redigonda
545f88236d
Changes the order of most calls to assertEquals, minor change in DataStore (#868)
* Changes the order of most calls to assertEquals

* Minor changes in datastore
2022-02-24 09:32:27 -03:00
Joel Elias Méndez
2e5bfa611e
[DEV-216] Fix stat charts in my account (showing only assigned departments) (#1157) 2022-02-23 15:08:51 -03:00
Nícolas Castillo
784d470ba5
Fixed e-mail reply polling (#1154) 2022-02-21 14:12:04 -03:00
LautaroCesso
54a5a9803d
[DEV-239] Remove unexpected error in upload csv file (#1151)
* Remove unexpected error in upload csv file

* Fix bug when render csv file error list

* csv files without eof line work OK

* [DEV-239] Prettify code
2022-02-18 18:33:34 -03:00
Joel Elias Méndez
d5552c0f73
[DEV-215] Email template infinite loading (#1153) 2022-02-15 17:39:00 -03:00
Joel Elias Méndez
daf1db847c
[DEV-236] Modify edit ticket name padding and buttons (#1147) 2022-02-14 14:52:05 -03:00
Joel Elias Méndez
2df12aa5e3
[DEV-234] Ticket status should say ''cancel'' instead of "close" (#1144)
* change button from close to cancel

* add type of closing button
2022-02-09 13:47:21 -03:00
Joel Elias Méndez
c80c026617
[DEV-228] Last activity menu style breaks in some languages (#1145) 2022-02-09 13:45:39 -03:00
Joel Elias Méndez
9cf71dcf66
[DEV-224] Error in edit topic icon color (#1143) 2022-02-08 14:01:43 -03:00
Joel Elias Méndez
a264d384a1
[DEV-130] add current staff to assign list (#1089)
* add current staff to assign list

* create a unique library

* add owner pic to dropdown

* update staffmember list when profilepic changes

* Improve coding

Co-authored-by: LautaroCesso <lautaro_cesso@hotmail.com>
2022-02-08 13:58:21 -03:00
Joel Elias Méndez
d90b7d48c3
[DEV-226] fix modal behavior when pressing enter (#1139) 2022-02-04 12:22:52 -03:00
Joel Elias Méndez
527843f00f
[DEV-217] update tickets when changing departments (#1137)
* update tickets when changing departments

* add loading to ticketlist
2022-02-03 17:19:22 -03:00
kirbypls
3e3a95f518
Add dev4 (#1142) 2022-02-03 03:18:43 -03:00
Guillermo Giuliana
639d40ddb0
[DEV-194] Change same department of a ticket bug (#1135)
* add logic into change-departmetn path

* add ruby tests
2022-01-28 14:22:07 -03:00
Joel Elias Méndez
fbee7275d5
[DEV-128] add padding to ticket-viewer-info (#1133) 2022-01-27 14:53:23 -03:00
Joel Elias Méndez
7df190ea01
[DEV-207] Edit article buttons (#1128)
* edit article buttons

* delete blank spaces

* change article button style

* align buttons

* center header and edit buttons

* fix height of article title

* fix article classnames

* change styles classnames

* improve coding
2022-01-19 12:41:22 -03:00
Joel Elias Méndez
1228d593d0
remove donate link for customers (#1132) 2022-01-19 12:10:40 -03:00
Joel Elias Méndez
c1a7befbed
[DEV-213] add supervised user tooltip (#1131)
* add supervised user tooltip

* change translation
2022-01-19 11:59:52 -03:00
Joel Elias Méndez
6e195a9109
add some loadings (#1127) 2022-01-14 22:19:19 -03:00
LautaroCesso
bf7c1ba8f9
[DEV-198] Change by id order filter (#1108)
* Change by id order in tag selector and tag filter

* Change by id order in departments filter

* Change by id order in owner filter

* Fix requested changes
2022-01-12 21:39:00 -03:00
LautaroCesso
8b4b73402e
Change 4.10.0 to 4.11.0 (#1126) 2022-01-04 13:24:06 -03:00
LautaroCesso
4cf446483d
Update language files (#1125)
* Update language files

* update spanish translations

Co-authored-by: Guillermo Giuliana <guillermogiuliana@hotmail.com>
2022-01-04 13:23:42 -03:00
Joel Elias Méndez
ea6bc7f436
Fix create ticket button issue in create-ticket-form (#1124) 2021-12-30 14:59:37 -03:00
Joel Elias Méndez
37209ef3fc
[DEV-206] Fix bug in login widget transition (#1120)
* fix widget height issue

* Fix bug in login widget transition

Co-authored-by: LautaroCesso <lautaro_cesso@hotmail.com>
2021-12-30 14:57:26 -03:00
Joel Elias Méndez
83e75cc572
fix button issue (#1123) 2021-12-29 17:16:49 -03:00
LautaroCesso
9291aa66a4
[DEV-197] Message bug (#1113)
* Fix message bug

* Add controlled pattern to message component

* Fix bug in admin panel custom fields form message

* Fix bug in dashboard ticket page message

* Fix bug in staff editor messages

* Fix bug in ticket viewer messages

* Fix bug in admin login page messages

* Fix bug in password recovery messages

* Fix bug in main home page login widget messages

* Fix bug in main signup widget messages

* Fix bug in admin panel custom responses message

* Fix bug in admin panel email settings message

* Fix bug in admin panel ban users message

* Fix bug in admin panel ban users message

* Fix bug in main recover password page messages

* Fix bug in dashboard list tickets page message

* Fix bug in dashboard edit profile page messages

* Fix bug in create ticket form messages

* Fix bug in invite user widget messages

* Fix bug in create ticket form messages

* Fix bug in admin panel list users messages

* Fix bug in admin panel system preferences messages

* Fix bug in admin panel advanced settings messages

* Fix bug in install step 3 database message

* Fix bug in install step 5 settings message

* Fix bug in install step 6 admin message

* Fix bug in install completed message

* Fix bug in ticket query list message

* Fix bug in articles list message

* Fix bug in admin pane search tickets message

* Fix bug in admin panel my tickets message

* Fix bug in admin panel new tickets messages

* Fix bug in ticket list messages

* Fix bug in main home page portal message

* Fix bug in dashboard create ticket page message

* Fix bug in main home page messages
2021-12-28 14:03:53 -03:00
Guillermo Giuliana
b9f5f7fcf1
[DEV-205] Users/Staffs should not be able to change the email for one already used by another user/staff (#1121)
* add verification of email on staffs

* add email verification users

* fix inviteStaff ruby test function

* add edit staff ruby tests

* add edit user ruby tests

* update other ruby tests
2021-12-28 00:26:55 -03:00
Guillermo Giuliana
fe1dd1bd48
[DEV-199] Can't put a department in private (#1115)
* add correct validation of repeated name use

* update department create ruby script

* add new edit department ruby tests

* change parameter name of create department ruby scripts

* verify change of private state while editing department ruby tests
2021-12-17 14:10:57 -03:00
Joel Elias Méndez
5dd6b7acdc
[DEV-133] align close button (#1111)
* align close button

* align create ticket buttons

* remove padding excess

* fix align buttons issues

* remove padding excess
2021-12-16 18:03:14 -03:00
Joel Elias Méndez
4bd8df1d5e
[DEV-188] add loading (#1117)
* fix coding issues

* improve coding

* add loading when updating my departments
2021-12-15 15:17:57 -03:00
Joel Elias Méndez
8d7b178fa1
[DEV-151] Invite user modal needs a loading (#1085)
* add loading to modal

* fix coding issues

* fix min-height measures
2021-12-14 15:31:35 -03:00
Guillermo Giuliana
237801e9ed
Revert "add last files (#1110)" (#1114)
This reverts commit 950439bf4776168e7e075c7b944efa7323dfa7e6.
2021-12-13 16:32:53 -03:00
Guillermo Giuliana
950439bf47
add last files (#1110) 2021-12-13 09:57:20 -03:00
LautaroCesso
6cb538616d
Fix message bug (#1109) 2021-12-09 15:23:56 -03:00
Joel Elias Méndez
156b285344
align response actions (#1106) 2021-12-03 12:19:41 -03:00
LautaroCesso
b39e4c2a5f
Fix edit ticket comment (#1107)
* Fix edit ticket comment

* Add some docs comments

* Change some test names
2021-12-02 21:35:37 -03:00
LautaroCesso
5a1b558a6d
Fix some article bugs (#1102) 2021-12-01 20:00:36 -03:00
Guillermo Giuliana
4b9a55b334
[DEV-155] add captcha into login (#1090)
* fix apidoc

* part 1

* pt2

* add classname and css
2021-11-30 01:37:24 -03:00
LautaroCesso
e7daf76274
Fix ticket viewer bug (#1105)
* Fix ticket viewer bug

* Fix ticket viewer bug
2021-11-29 19:49:39 -03:00
LautaroCesso
402af565a9
[DEV-162] Fix ticket comment issues (#1081)
* Fix bug in UI

* Fix bug in backend

* Fix some issues

* Improve coding

* Improve coding
2021-11-26 17:37:33 -03:00
Guillermo Giuliana
f5e9b2602c
[DEV-113] change ticket date according to selected language (#1086)
* fix apidoc

* update tickets date according selected language

* sync language abbreviations

* fix today/yesterday ticket date & hindi bug

* erase useless comments
2021-11-25 17:55:51 -03:00
LautaroCesso
f1d746f9f9
Fix bug in edit email templates (#1101) 2021-11-25 13:57:22 -03:00
Ivan Diaz
c0fc7933ed Use php7.0 for circleci 2021-11-25 11:29:06 +01:00
Ivan Diaz
f31586874e Use old composer file 2021-11-25 11:18:31 +01:00
LautaroCesso
d2c126aad9
Update composer lock (#1100) 2021-11-24 17:28:55 -03:00
LautaroCesso
ffd09b841f
Revert "Update composer lock (#1097)" (#1099)
This reverts commit e23468d3bebd8a19038d70d748d79128f7b72da0.
2021-11-24 17:09:28 -03:00
LautaroCesso
e23468d3be
Update composer lock (#1097)
* update composer lock

* update composer lock
2021-11-24 15:27:16 -03:00
LautaroCesso
84f36e89dc
Fix language settings bug (#1091) 2021-11-24 15:17:38 -03:00
kirbypls
9715cdc9a1
Revert "update composer lock (#1095)" (#1096)
This reverts commit b39dca642b106ad5f21d0520d5e5df0c582041e7.
2021-11-24 15:13:08 -03:00
LautaroCesso
b39dca642b
update composer lock (#1095) 2021-11-24 15:12:02 -03:00
kirbypls
6f6acc925d
Revert "Update package lock by guillegui (#1093)" (#1094)
This reverts commit d2e6dc2fbe5db288cb41160928a189ff0b9f7efd.
2021-11-24 14:28:00 -03:00
Guillermo Giuliana
d2e6dc2fbe
Update package lock by guillegui (#1093) 2021-11-24 14:26:04 -03:00
Guillermo Giuliana
0df57af11e
[DEV-187] Fix duplicated-department-names bug (#1083)
* fix apidoc

* add new custom validation

* add ruby tests

* add frontend error

* take out the ternary
2021-11-24 14:21:16 -03:00
Guillermo Giuliana
8400a1caf0
[DEV-112] Autodetect links into tickets (#1084)
* fix apidoc

* add magicurl editor
2021-11-21 04:01:01 -03:00
Joel Elias Méndez
f045c08b2e
[DEV-147] Add resend user-staff invitation (#1069)
* add resend user-staff invitation

* fix width issue

* fix styles issues

* fix code blank spaces

* improve coding

* improve coding
2021-11-19 14:22:35 -03:00
kirbypls
645e64532b
Update stats-utils.js 2021-11-17 14:33:35 -03:00
Joel Elias Méndez
b9f935df5f
[DEV-153] Fix activity (#1080)
* fix spanish translations

* fix activity

* fix activity

* improve coding

* improve coding

* improve coding

* improve coding
2021-11-17 13:48:37 -03:00
LautaroCesso
edddc8d2c5
[DEV-163] Fix custom fields issue (#1079)
* Fix custom fields issue

* Add custom fields options validation

* Add not blank option validation

* Fix description length
2021-11-17 13:31:39 -03:00
Joel Elias Méndez
eaeaaa647e
[DEV-140] Fix spanish translations (#1074)
* fix spanish translations

* add translations

* add translations

* add more translation
2021-11-16 17:33:22 -03:00
LautaroCesso
d7ccff1a5a
[DEV-26] Update length validations (#1075)
* Update length validations

* Fix language validations

* Remove unnecessary import

* Delete some semicolons
2021-11-11 17:17:39 -03:00
LautaroCesso
c5f1aa2b92
Update font awesome version (#1078) 2021-11-10 14:18:41 -03:00
Guillermo Giuliana
9ed4caf202
[DEV-160] Add user permission into logout path (#1073)
* fix apidoc

* add user verification into logout.php and update ruby tests
2021-11-05 17:06:08 -03:00
LautaroCesso
018863ab3e
Fix email test button (#1076) 2021-11-05 14:44:31 -03:00
Joel Elias Méndez
7015e21966
redirect home after a user signs up (#1070) 2021-11-02 17:39:53 -03:00
Joel Elias Méndez
7cdb6d3603
[DEV-149] add close button to messages (#1071)
* add close button to messages

* improve coding
2021-11-02 17:39:34 -03:00
LautaroCesso
1836849fa5
Delete unused modal (#1072) 2021-11-02 16:33:56 -03:00
Ivan Diaz
9f9e1dbd91
Remove textContent check for non-textarea elements (#1066) 2021-10-21 12:09:13 -03:00
Ivan Diaz
60b1b5eec5
add x-frame-options for cleint php page 2021-10-21 12:07:33 -03:00
Guillermo Giuliana
7b4427d3e3
add php header (#1064) 2021-10-21 11:50:02 -03:00
Joel Elias Méndez
09150d6940
fix message issue (#1063) 2021-10-20 14:10:09 -03:00
Guillermo Giuliana
02cf8f0da3
Update php cookies security (#1056)
* set php cookies

* update edit title doc

* add session renerate id to session createSession function
2021-10-18 22:06:32 -03:00
Guillermo Giuliana
e15bd15f07
Updates to 4.10.0 (#1061)
* change 4.9 to 4.10

* new translations
2021-10-18 22:06:20 -03:00
Guillermo Giuliana
c657d8291f
add controller request secure param (#1060) 2021-10-18 22:05:54 -03:00
Joel Elias Méndez
b2e43430b1
Add email status (#1058) 2021-10-15 15:43:55 -03:00
Joel Elias Méndez
8c17d22ab3
Fix frontend issues (#1055)
* fix login widget bugs

* fix ticket issues

* fix path name
2021-10-14 20:29:57 -03:00
Guillermo Giuliana
143776febe
update api documentation (#971) 2021-10-14 17:57:02 -03:00
Justman10000
6536050fdd
Translation corrected (#1021)
* Corrected translation

* I forgot

* I forgot too

* You already know that Anmelden and Einloggen are the same thing?
2021-10-14 17:52:30 -03:00
Joel Elias Méndez
9a4374d371
[DEV-114] Add "resend verification email" in frontend (#1047)
* resend-verification-token

* force resend email verification

* delete wrong codelines

* improve code

* fix loading
2021-10-14 14:07:23 -03:00
Joel Elias Méndez
b8be664809
fix-response-width (#1054) 2021-10-14 13:29:12 -03:00
Ivan Diaz
e9a1a2e5be
Create SECURITY.md 2021-10-14 02:52:38 -03:00
Joel Elias Méndez
0f976ebde9
Add reopen option after a ticket is closed (#1041) 2021-10-10 15:42:36 -03:00
Guillermo Giuliana
c64f1f1ea6
[DEV-146] resend verification email backend (#1049)
* add path resend signup token

* add ruby tests
2021-10-08 19:07:14 -03:00
Guillermo Giuliana
5d4fe0250b
[DEV-148] Resend invitation backend (#1050)
* add resend-staff-invite path

* add resend-user-invite path

* add departments verification staff invite and ruby test

* add user invite ruby tests

* add resend invite paths and ruby tests
2021-10-08 19:04:25 -03:00
Joel Elias Méndez
af15d0116d
After delete a ticket redirect to previous page (#1044) 2021-10-08 16:09:39 -03:00
Joel Elias Méndez
6ccb389492
[DEV-129] Redirect after ticket is created without user system (#1045)
* Redirect to home/dashboard after ticket is created without user system

* fix min-height in main-home-page-portal component

* Move success messagge

* querystring check

* fix delete ticket button visibility
2021-10-08 16:09:09 -03:00
Joel Elias Méndez
ae076de88f
[DEV-104] - Redirect to home/dashboard after ticket is created without user system (#1037)
* Redirect to home/dashboard after ticket is created without user system

* fix min-height in main-home-page-portal component

* Move success messagge

* querystring check
2021-09-27 19:00:43 -03:00
Joel Elias Méndez
59fb9eaef3
[DEV-129] Remove "Delete ticket" from response form (#1038)
* Delete ticket button for logged user

* define variables
2021-09-27 11:16:25 -03:00
Joel Elias Méndez
ffe7ef8e0b
Change Registration Api Keys name to Api Keys (#1039) 2021-09-27 10:05:23 -03:00
Guillermo Giuliana
27e86c934c
[DEV-131] Fix delete ticket bug (#1035)
* verify ticket author-user before reduce amount of tickets created

* add ruby test

* change geting tickets by id in ruby tests
2021-09-22 18:20:19 -03:00
Ivan Diaz
064f00388a
Fix undefined property check for user 2021-09-18 23:07:09 -03:00
Joel Elias Méndez
fc93ad4c00
[DEV-109] Add Info tooltip warning about tickets not visible (#1031)
* Info tooltip added

* delete blank spaces
2021-09-16 10:50:21 -03:00
Guillermo Giuliana
c22a1cdbb3
[DEV-95] add prevent default when pressing down Enter key (#1033)
* add prevent default when pressing down Enter key

* delete listener and add onkeydown prop
2021-09-15 14:21:19 -03:00
Ivan Diaz
89cb4c18fd Update composer.lock 2021-08-30 00:23:15 -03:00
LautaroCesso
167d7927db
Clear create ticket form after create a ticket with mandatory login disabled (#966)
* Clear create ticket form after create a ticket with mandatory login disabled.

* WIP
2021-08-29 22:12:41 -03:00
LautaroCesso
55c89d58cc
Fix style diff in create ticket form without attachment file button (#974) 2021-08-29 22:11:22 -03:00
LautaroCesso
0bcc775944
Delete date warning (#975) 2021-08-29 22:09:11 -03:00
LautaroCesso
e6441179c9
Fix bug in ticket count in admin panel list users (#967) 2021-01-19 17:15:43 -03:00
LautaroCesso
45a59e0b20
Close panel when inviting user (#960)
* Close panel when inviting user

* Move success invite user message to admin panel list user
2021-01-12 17:39:16 -03:00
Ivan Diaz
9f7b11413c Fix readme version, fix frontend tests 2021-01-08 16:31:06 -03:00
Guillermo Giuliana
e554bb64d1
Release 4.9.0 (#965)
Co-authored-by: CircleCI-BOT <ivan@opensupports.com>
2021-01-08 16:27:24 -03:00
Ivan Diaz
0088332562 Use english as default i18n in case it breaks 2021-01-07 20:53:28 -03:00
Guillermo Giuliana
42c5dd7210
Add updating script 4.9.0 (#964)
* Add updating script 4.9.0

* Update 4.9.0.php
2021-01-07 19:33:20 -03:00
J0WI
1a9e1a852c
Only erase all mails on success (#894) 2021-01-05 21:47:33 -03:00
Guillermo Giuliana
49dc1ab56c
DEV-86 (#959) 2021-01-05 21:02:35 -03:00
dependabot[bot]
1ea4509e4f
Bump axios from 0.18.1 to 0.21.1 in /client (#962)
Bumps [axios](https://github.com/axios/axios) from 0.18.1 to 0.21.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v0.21.1/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.18.1...v0.21.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-01-05 19:58:23 -03:00
Maximiliano Redigonda
c5d6068e97
Add statistics (#904)
* Adds first_closed_at and last_closed_at to Ticket

* Fixes ticket isClosed function

* Adds reopened column to Ticket table

* Adds stats path

* Adds stats for instant tickets

* Adds basic connection with frontend

* Creates cards to display ticketData

* Adds tooltips with descriptions and i18n

* Adds date range filter to backend

* Adds DateRange filter on frontend

* Documents and better structures code

* Makes $dateRange local

* Adds departments filter on backend

* Adds stats path to menu

* Adds first UI for departments filter in stats

* Implements departments filter on frontend

* Improves styling by adding bootstrap rows

* Improves structure of dynamics queries

* Adds tags filter on backend

* Adding TagFilter for statistics WIP

* Adds missing `id` to propTypes TagSelector

* Removes console.warns

* Adapts form to pass tagnames as value as FormField

* Sends tags to API too

* Makes tag-selector change form with tagnames only

* Fixes tag-selector from ticket-viewer

* Removes console.warn

* Removes logs

* Adds owner filter on backend

* Connects owners frontend with backend for stats

* Style changes for date-selector

* Adds tickets by hours stat to /system/stats path

* Adds chart for tickets created on each hour

* Adds better wrap for ticketdata cards

* Adds getAverageFirstReply to backend stats path

* Adds getNumberOfCreatedTicketsByWeekday to backend

* Adds created tickets by weekday chart

* Disables clicking on the legend to toggle data

* Adds base functions for efficiency stats

* Adds getAverageFirstClosed to backend stats

* Adds getAverageLastClosed to backend stats

* Adds table, filters, and groupBy variables to queries

* Adds response structure with mocks

* Adds totalOwners and totalDepartments

* Adds SQL queries to get department/staff hops of a ticket

* Changes incorrect name

* Rolls back addition of near useless function

* Improves tag array management from redux store

* Fix bug in autocomplete filters.

* Sets default date range to current month.

improves date.js.

* Adds i18n

* wip

* Add media query in admin-panel-stats.scss

* Updates date handling in search-ticket-utils

* Makes tooltip open on hover of the entire block

* Fix date range mobile style.

* Add Loading

* Add submit button and clear button in admin panel stats

* Adds tests for stats and comments old ones

* Add test for stats after a ticket has been created

* Makes default dateRange for stats go to the end of the day

* Factors out function to create ticket and adds test

* Adds instant ticket test

* Adds reopened test

* Commit to save technique to test created_by_hour but is prohibitively slow.

* Updates test of created_by_hour to be more lightweight

* Adds test for created_by_weekday

* Fixes default date and renames a function

* Fixes hover bug by extracting card-stat to its own component

* Fix drawbacks with previous change in style - mobile

Co-authored-by: LautaroCesso <lautaro_cesso@hotmail.com>

* Set up 0 as a minimum number for bar chart

* Moves styles from stats cards to the component

* Removes old /system/get-stats path

* Changes name from /system/stats to /system/get-stats

* Restore getCurrentDate in date transformer

Co-authored-by: LautaroCesso <lautaro_cesso@hotmail.com>
Co-authored-by: Ivan Diaz <ivan@opensupports.com>
2020-12-27 16:22:55 -03:00
LautaroCesso
51eee4ed7b
Paginate ticket list in admin panel view user (#944)
Co-authored-by: Ivan Diaz <ivan@opensupports.com>
2020-12-26 19:13:59 -03:00
LautaroCesso
1e0e0134a3
Fix style bug in ticket viewer (#947)
* fix style bug in ticket viewer

* Fix style in invite staff modal

Co-authored-by: Ivan Diaz <ivan@opensupports.com>
2020-12-26 18:25:32 -03:00
LautaroCesso
bc9023b8a6
Auto discard change before delete custom response (#946) 2020-12-26 18:09:22 -03:00
LautaroCesso
534bf3624a
Fix bug custom fields default values undefined (#945)
* Fix bug in selected custom fields in admin panel invite user

* WIP

* Fix style

* WIP
2020-12-26 17:39:51 -03:00
LautaroCesso
a7cb7f376c
Fix bug with discard department change (#942)
* Fix bug with discard department change

* WIP

* WIP

Co-authored-by: Ivan Diaz <ivan@opensupports.com>
2020-12-26 17:39:18 -03:00
Ivan Diaz
c3088406da
Fix ticket date style (#958)
* Fix ticket date style in table componenet

* WIP

* WIP

* Add shortcat date in ticket list

Co-authored-by: LautaroCesso <lautaro_cesso@hotmail.com>
2020-12-26 16:29:30 -03:00
Ivan Diaz
ea8d0719eb
Fix composer for 5.6 (#956)
* Fix composer lock for 5.6 support

* Fix ticketlist test render error
2020-12-24 20:05:28 -03:00
Ivan Diaz
e4a7fe8783 Fix release automation 2020-12-06 20:58:44 -03:00
Ivan Diaz
ca54d19bd9 Fix deploy build 2020-11-26 21:03:26 -03:00
Ivan Diaz
5416ef4009 Add automated release 2020-11-24 00:49:17 -03:00
LautaroCesso
af3d95cf4d
Styles standardization (#934)
* WIP tag create and tag edit pop up

* WIP admin panel email settings

* WIP admin panel viwe article, article list, edit topic, add article

* WIP admin panel list users and invite user widget

* WIP admin panel view user

* WIP admin panel custom fields and admin panel custom field form

* WIP fix plus icon position and type

* WIP admin panel custom responses

* WIP admin panle my tickets and create ticket form

* change check ticket button to green color

* WIP dashboard edit profile page

* Change icon in invite user button

* WIP
2020-11-19 17:37:59 -03:00
Ivan Diaz
3dd76f214d
Remove recaptchakey leftover from test (#937) 2020-11-18 20:52:05 -03:00
LautaroCesso
16435925b6
Group params of open modal function (#935)
* Agrupate params of open modal function

* Add white color prop in modal componenet
2020-11-18 11:47:38 -03:00
Ivan Diaz
ea273970d1
Changing a few texts (#761)
the Spanish version was a mixture of formal, informal and it also had grammatical mistakes. The German version had a few mistakes like 'Fahrkartenummer' which actually means "bus/train ticket number".
2020-11-15 10:51:03 -03:00
Ivan Diaz
b9b21ef950 Add request deployment 2020-11-13 17:32:56 -03:00
LautaroCesso
bb1f5d0ade
Fix bug with departments info in staff editor component (#931) 2020-11-13 14:41:59 -03:00
LautaroCesso
15f765cf85
Fix bug in ticket list in my account and view staff (#921)
* wip

* Delete ticket of staff/get.php response

* Move paginate functions to staff editor component

* wip

* fix assignedTickets and closedTickets values in staff/get-all

* add show closed tickets checkbox in admin panel my account

* WIP

* Add initial api value

* Fix staff/get-all test

* WIP
2020-11-13 14:23:14 -03:00
LautaroCesso
9feb7d6cd4
Add cancel button in api key pop up (#929)
* Add cancel button in api key pop up

* Change space between add api key form buttons

* Add render close button param in open modal container function

* WIP
2020-11-13 14:05:11 -03:00
LautaroCesso
94926f90e6
Fix styles (#916)
* Fix mobile style in admin panle email settings

* Fix style in ticket query filters

* fix style in mobile in ticket query filters
2020-11-13 13:54:54 -03:00
LautaroCesso
b02acfdf7b
Fix bug with private departments in default department feature (#930) 2020-11-12 15:12:15 -03:00
LautaroCesso
560f231e51
Add close ticket button at the end of ticket viewer (#932)
* Add close ticket button at the end of ticket viewer

* WIP

* standardization
2020-11-12 14:13:52 -03:00
Ivan Diaz
7fb7be3860 Add circleci config 2020-10-29 18:30:32 -03:00
Guillermo Giuliana
b8944a3f04
New apikeys permissions (#869)
* back-end  y ruby test

* ruby test

* frontend part

* delete unused component

* resolve relevant  github maxi comments

* delete .catch of getAllkeys

* fix github ivan comments

* add ruby test and minor changes

* search ruby test

* fix name checkticketpermission
2020-10-14 15:08:14 -03:00
LautaroCesso
a64c9f2255
Fix error in searchbox in tickets/search-tickets (#891) 2020-10-02 02:17:22 -03:00
LautaroCesso
994a39ad6d
Add more descriptive error messages when in delete staff. (#892) 2020-09-30 17:35:32 -03:00
Guillermo Giuliana
b73d6d534d
Fix ticket seach query when you are looking for an own ticket that is not in an owned department (#897)
* update departments filter

* update php test
2020-09-29 01:14:06 -03:00
Guillermo Giuliana
d4cdbab203
Indicate it's myAccount when editing staff (#884)
* add myaccount prop

* take out ternary

* user constand insted this.state
2020-09-05 15:37:30 -03:00
Maximiliano Redigonda
80a9a958a8
Revert "Google social login" (#886) 2020-08-28 14:13:23 -03:00
Maximiliano Redigonda
9125944bc3
Revert "Adds Google platform.js script to index.php (#882)" (#885)
This reverts commit d3638787e65ef6afe65ecd2a8e58219ae1c0f84e.
2020-08-28 14:12:10 -03:00
Guillermo Giuliana
c00720d6a2
Minor GitHub bugs (#806)
* text-validation to edit article content

* shows only own department on my account

* add moment and update date

* Update date transformer

* Delete time zone setting.

* Use local date in date range component and utc date in date range filter.

* Fix github comment.

* Fix merge 'Fix filter show bug in ticket search when ordering'.

* Fix merge 'Fix filter show bug in ticket search when ordering' second part.

* Fix getDefaultUTCEndDate function.

Co-authored-by: Ivan Diaz <ivan@opensupports.com>
Co-authored-by: LautaroCesso <lautaro_cesso@hotmail.com>
2020-08-28 07:13:34 -03:00
LautaroCesso
5184c31907
Fix error in edit button for ticket owner. (#872)
* Retrieve staff members after edit staffs.

* Delete retun after throw new RequestException in staff/edit.php

* Resolve github Ivan comments.

* wip
2020-08-28 06:27:04 -03:00
LautaroCesso
817240e0b4
Add loading in Autocomplete when dropdown list is updating. (#863)
* Add loading in Autocomplete when dropdown list is updating.

* Fix error with set state in autocomplete.

* Resolve github Ivan comments.
2020-08-28 05:46:19 -03:00
LautaroCesso
52eae4d242
Thorough ruby tests /ticket/search path (#870)
* Add ticket/search test (ruby)

* Resolve github maxi comments.

* Resolve github maxi comments second part.

* wip

* Rename some test.

* Add query filter test.

* Add test combining multiple parameters.

* wip

* comment query test in search.rb

* comment query test in search.rb second part.
2020-08-28 05:19:47 -03:00
Maximiliano Redigonda
d3638787e6
Adds Google platform.js script to index.php (#882) 2020-08-25 16:08:38 -03:00
Maximiliano Redigonda
68c3975ea4
Merge pull request #860 from opensupports/optimize-ticket-search
Optimize ticket search
2020-08-24 09:40:04 -03:00
Maximiliano Redigonda
6e6d2d83e7
Merge pull request #881 from opensupports/google-social-login
Google social login
2020-08-24 09:34:29 -03:00
Maxi Redigonda
371f111706 Fixes frontend tests by mocking gapi object 2020-08-24 07:50:07 -03:00
Maxi Redigonda
bb89956e9a Fixes admin logout 2020-08-23 18:55:05 -03:00
Maxi Redigonda
a89a465ac9 Adds check to see if email was verified 2020-08-22 00:47:24 -03:00
Maxi Redigonda
9f915a3291 Removes workaround 2020-08-21 00:05:42 -03:00
Maxi Redigonda
0fd23ac6b8 Aligns Google login button 2020-08-20 23:53:44 -03:00
Maxi Redigonda
5021781d25 Implements Google auto signup
If the user doesn't have an account on the system, the first time they
log in with Google, an account will be created.
2020-08-20 23:45:09 -03:00
Maxi Redigonda
47f92569ef Fixes button not showing and re-login 2020-08-20 22:56:31 -03:00
LautaroCesso
c92ecf25dc
Transform staff/user Names Into Hyperlinks. (#875) 2020-08-20 19:15:54 -03:00
Maxi Redigonda
38136ade4d Moves renderGoogleButton out of render 2020-08-20 18:27:43 -03:00
Maxi Redigonda
da534e3018 Adds Google close session functionality 2020-08-20 16:40:15 -03:00
Maxi Redigonda
0518b0ac17 Adds branching for when social login user wasn't created 2020-08-20 16:23:30 -03:00
Maxi Redigonda
5d2e4fcb1f Merge branch 'master' of https://github.com/opensupports/opensupports into social-login 2020-08-20 14:53:39 -03:00
LautaroCesso
7a803dd7ff
Fix security bug, Ticket Number data filtered. (#879) 2020-08-19 23:45:47 -03:00
LautaroCesso
4077dac8c7
Remember me function for staffs (#866)
* fix warning in checbox in form field.

* Add remember me function for staffs.

* Add staff instance in session cookie.

* Add result data staff in get user data in auto login.

* Fix remember me function for user.

* Fix login test rb and add remember me function test in login rb.

* Resolve github maxi comments.
2020-08-19 23:33:40 -03:00
Maxi Redigonda
c4e211518c WIP - Stub to log in with Google 2020-08-19 17:33:38 -03:00
Maxi Redigonda
56ca30a7c3 WIP - validates google token and gets its data 2020-08-19 16:17:44 -03:00
Maxi Redigonda
fad3dce646 Adds google/apiclient to composer json 2020-08-19 16:17:44 -03:00
Maxi Redigonda
deb590ceee Places Google Login button inside login widget 2020-08-19 16:17:44 -03:00
LautaroCesso
e36b984b23 Resolve github maxi comments. 2020-08-18 23:48:06 -03:00
LautaroCesso
e397d45c53 Merge branch 'master' into remember-me-function-for-staffs 2020-08-13 15:57:31 -03:00
LautaroCesso
e4a9366b07 Fix login test rb and add remember me function test in login rb. 2020-08-12 20:32:17 -03:00
LautaroCesso
bbbc845fe7 Fix remember me function for user. 2020-08-12 16:02:39 -03:00
LautaroCesso
a46140381b Add result data staff in get user data in auto login. 2020-08-11 23:51:41 -03:00
LautaroCesso
d72aec3976 Add staff instance in session cookie. 2020-08-11 23:35:10 -03:00
LautaroCesso
1c5d156723
Delete non existent step 7 instalation (#862)
* Delete non existent step 7 instalation

* Add INSTALLATION_COMPLETED_TITLE i18n and delete INSTALLATION_COMPLETED.
2020-08-07 17:29:24 -03:00
Maximiliano Redigonda
f72f2ac074
Merge pull request #867 from opensupports/fix-system-get-logs
Fixes /system/get-logs by adding getFetchAs
2020-08-07 17:25:25 -03:00
Maxi Redigonda
01a494ac23 Fixes /system/get-logs by adding getFetchAs 2020-08-05 16:34:27 -03:00
Maxi Redigonda
4fd9db8651 Assumes all tables exist, updates tests.
An internal function has now a new signature, it receives a query,
and now tests correctly reflect that.
2020-08-03 19:39:48 -03:00
LautaroCesso
01718cf92b Merge branch 'master' into remember-me-function-for-staffs 2020-08-03 12:58:28 -03:00
LautaroCesso
96990c8c04 Add remember me function for staffs. 2020-08-03 12:54:15 -03:00
LautaroCesso
bb9873be4f fix warning in checbox in form field. 2020-08-03 12:52:12 -03:00
Maximiliano Redigonda
4970b18c2d
Merge pull request #861 from chsjr1996/patch-1
Update br.js translation
2020-07-31 19:52:35 -03:00
Carlos H
92e222f10d
Update br.js
Translate remaining "tickets" words
2020-07-31 14:52:49 -03:00
Maximiliano Redigonda
76b7e2c6e7
Merge pull request #859 from opensupports/forbid-get-supervised-tickets-path-to-staffs
Forbids call to /get-supervised-tickets from staff
2020-07-31 12:03:14 -03:00
Maximiliano Redigonda
38c90e2c07
Merge pull request #850 from opensupports/avoid-manual-parsing-of-query-strings-using-library
Avoid manual parsing of query string using queryString library
2020-07-31 12:02:26 -03:00
LautaroCesso
938e25b2fa
Add edit buttons for fields in ticket viewer (#835)
* Fix style color in autocomplete component and dropdown in ticket search and ticket viewer.

* Change style of ticket status edit in ticket viewer.

* Redirect to ticket search when author name was clicked in ticket viewer

* Add i18n ASSIGNED_TO_ME.

* Add content on selected in dropdown component.

* Add styles in edit tags in ticket viewers

* Add edit owner and delete i18n ASSIGNED_TO_ME.

* Redirect to ticket search when owner name was clicked in ticket viewer

* Add style in edit department in ticket viewer.

* Add nav links in owner and author in ticket viewer.
2020-07-30 22:30:48 -03:00
LautaroCesso
1ecf619892
fix autocomplete bug (#855) 2020-07-30 22:13:14 -03:00
LautaroCesso
57b5ea8820
Autocomplete supervised users should not show supervisor user as option (#856)
* Fix bug in supervised user

* Add some line break in admin panel view user.
2020-07-30 21:59:54 -03:00
LautaroCesso
7866880152
Add authors column in dashboard list ticket page. (#857) 2020-07-30 21:52:43 -03:00
LautaroCesso
6d938bead4
Take out remember data of important inputs. (#858) 2020-07-30 21:42:08 -03:00
Maxi Redigonda
5b16393659 Optimizes query to avoid information_schema (-2s)
This actually also corrects a bug (previous to this, the query would
find any table with the correct name in any database).
2020-07-30 15:36:28 -03:00
Carlos H
e3b57bd106
Update br.js
The 'ingressos' word in BR does not represent a "support ticket", but represent a "ticket" of events or places like movie theater, soccer match, etc.

The correct word is already used in this file is "chamados".
2020-07-30 09:26:45 -03:00
Maxi Redigonda
2bf6b2c23f Centralizes calls to expensive functions (-2.5 s). 2020-07-29 15:50:05 -03:00
Maxi Redigonda
384f7c93d7 Avoids redundant call saving 2.5 seconds 2020-07-29 15:36:18 -03:00
Maxi Redigonda
785e2d8ac5 Forbids call to /get-supervised-tickets from staff 2020-07-28 16:36:40 -03:00
Maxi Redigonda
47ea47f971 Avoid manual parsing of query string using queryString library as in the rest of the code. 2020-07-28 15:56:19 -03:00
Alejandro Matos
f31d0aa377
Changing a few texts
the Spanish version was a mixture of formal, informal and it also had grammatical mistakes. The German version had a few mistakes like 'Fahrkartenummer' which actually means "bus/train ticket number".
2020-04-15 01:28:05 -05:00
403 changed files with 36071 additions and 3685 deletions

190
.circleci/config.yml Normal file
View File

@ -0,0 +1,190 @@
version: 2.1
orbs:
php: circleci/php@1.0.2
node: circleci/node@1.1.6
aws-cli: circleci/aws-cli@1.0.0
jobs:
install_composer_packages:
docker:
- image: 'cimg/base:edge'
steps:
- checkout
- php/install-php:
version: '7.0'
- php/install-composer
- run:
name: Install php extensions
command: |
sudo add-apt-repository ppa:ondrej/php
sudo apt update
sudo apt install php7.0-imap -y
sudo apt install php7.0-xml -y
sudo apt install zip unzip php7.0-zip php7.0-mbstring -y
- php/install-packages:
app-dir: server/
- persist_to_workspace:
root: .
paths:
- .
install_node_packages:
docker:
- image: circleci/node:11.15.0-stretch
steps:
- attach_workspace:
at: .
- restore_cache:
keys:
- node-cache-{{ checksum "client/package.json" }}
- run:
name: Install dependencies
command: |
sudo npm install -g npm@6.7.0
sudo npm install -g mocha@6.2.0
cd client && npm install
- save_cache:
paths:
- client/node_modules
key: node-cache-{{ checksum "client/package.json" }}
- persist_to_workspace:
root: .
paths:
- .
deploy_staging_files:
docker:
- image: circleci/node:11.15.0-stretch
environment:
- GIT_COMMIT_DESC: git log --format=oneline -n 1 $CIRCLE_SHA1
steps:
- attach_workspace:
at: .
- deploy:
name: Deploy staging files
command: |
if [ ! "$CIRCLE_BRANCH" = "master" ]; then exit 0; fi
if [[ "$GIT_COMMIT_DESC" = Release* ]]; then exit 0; fi
sudo apt update
sudo apt install -y lftp
make deploy-staging-files
make deploy-staging-population
add_release_commit:
docker:
- image: circleci/node:11.15.0-stretch
parameters:
version:
type: string
default: ""
steps:
- attach_workspace:
at: .
- add_ssh_keys:
fingerprints:
- "45:1e:cf:38:3f:9f:97:87:5b:b8:fd:e1:6c:71:11:41"
- run:
name: Commit new version
command: |
export VERSION=<< parameters.version >>
cd version_upgrades/release_script
npm i
npm run modify-files
cd ../..
ssh-keyscan -H github.com >> ~/.ssh/known_hosts
git config --global user.email "ivan@opensupports.com"
git config --global user.name "CircleCI-BOT"
git add .
git commit -m "Release $VERSION"
git checkout -b release-${VERSION}
git push origin release-${VERSION}
- persist_to_workspace:
root: .
paths:
- .
add_release_tag:
docker:
- image: circleci/node:11.15.0-stretch
parameters:
version:
type: string
default: ""
steps:
- attach_workspace:
at: .
- add_ssh_keys:
fingerprints:
- "45:1e:cf:38:3f:9f:97:87:5b:b8:fd:e1:6c:71:11:41"
- run:
name: Add Release tag
command: |
export VERSION=<< parameters.version >>
sudo apt-get update
sudo apt-get install lftp
make build-release-bundles
make upload-bundles
# make push-prerelease-tag
make populate-staging-release
- persist_to_workspace:
root: .
paths:
- .
parameters:
version:
type: string
default: ""
run_build:
type: boolean
default: true
workflows:
build:
when:
and:
- equal: [ master, << pipeline.git.branch >> ]
- << pipeline.parameters.run_build >>
jobs:
- install_composer_packages
- install_node_packages:
requires:
- install_composer_packages
- deploy_staging_files:
requires:
- install_node_packages
release:
when: << pipeline.parameters.version >>
jobs:
- install_composer_packages
- install_node_packages:
requires:
- install_composer_packages
- add_release_commit:
version: << pipeline.parameters.version >>
requires:
- install_node_packages
- add_release_tag:
version: << pipeline.parameters.version >>
requires:
- add_release_commit

14
.github/workflows/run-tests.yml vendored Normal file
View 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
View File

@ -1,8 +1,6 @@
.idea
.jshintrc
client/package-lock.json
tests/Gemfile.lock
server/composer.lock
.DS_Store
server/vendor
server/files/
!server/files/.gitkeep
@ -11,3 +9,4 @@ server/.dbdata
server/.fakemail
server/apidoc
dist/
.env

80
DEVELOPMENT.md Normal file
View 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
View 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
View File

@ -1,89 +1,61 @@
![OpenSupports](http://www.opensupports.com/logo.png)
<div align="center">
[![Build Status](https://travis-ci.org/opensupports/opensupports.svg?branch=master)](https://travis-ci.org/opensupports/opensupports) v4.8.0
![OpenSupports](https://user-images.githubusercontent.com/25920622/172173126-f0a07319-0cc2-409b-aa22-120187fa4541.png)
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`.
![2022-06-08_10-32_demo](https://user-images.githubusercontent.com/25920622/172630004-988c914b-918e-455c-be48-11f96a00611e.gif)
##### 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.
![2022-06-08_10-32_demo_staff](https://user-images.githubusercontent.com/25920622/172867706-3669c7db-ef86-48df-92a9-8c2bfb19f622.gif)
**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
View File

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

View File

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

11
client/Makefile Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,5 +1,5 @@
export default {
login: stub(),
logout: stub(),
initSession: stub()
checkSession: stub()
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -32,11 +32,11 @@ class ArticleAddModal extends React.Component {
<Form onSubmit={this.onAddNewArticleFormSubmit.bind(this)} loading={this.state.loading}>
<FormField name="title" label={i18n('TITLE')} field="input" fieldProps={{size: 'large'}} validation="TITLE" required/>
<FormField name="content" label={i18n('CONTENT')} field="textarea" validation="TEXT_AREA" required fieldProps={{allowImages: this.props.allowAttachments}}/>
<SubmitButton type="secondary">{i18n('ADD_ARTICLE')}</SubmitButton>
<Button className="article-add-modal__cancel-button" type="link" onClick={(event) => {
event.preventDefault();
ModalContainer.closeModal();
}}>{i18n('CANCEL')}</Button>
<SubmitButton className="article-add-modal__submit-button" type="secondary">{i18n('ADD_ARTICLE')}</SubmitButton>
</Form>
</div>
);

View File

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

View File

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

View File

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

View File

@ -1,6 +1,4 @@
import React from 'react';
import {connect} from 'react-redux';
import classNames from 'classnames';
import DropDown from 'core-components/drop-down';
import Icon from 'core-components/icon';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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;

View File

@ -0,0 +1,42 @@
import React from 'react';
import Tooltip from 'core-components/tooltip';
class StatCard extends React.Component {
static propTypes = {
label: React.PropTypes.string,
description: React.PropTypes.string,
value: React.PropTypes.number,
isPercentage: React.PropTypes.bool
};
state = {
showTooltip: false
};
render() {
const {
label,
description,
value,
isPercentage
} = this.props;
const displayValue = isNaN(value) ? "-" : (isPercentage ? value.toFixed(2) : value);
return (
<Tooltip content={description} show={this.state.showTooltip} >
<div className="stat-card" onMouseEnter={() => this.setState({showTooltip: true})} onMouseLeave={() => this.setState({showTooltip: false})}>
<div className="stat-card__wrapper">
{label}
<div className="stat-card__container">
{displayValue}{isPercentage && !isNaN(value) ? "%" : ""}
</div>
</div>
</div>
</Tooltip>
);
}
}
export default StatCard;

View File

@ -0,0 +1,22 @@
@import "../scss/vars";
.stat-card {
margin: 0 4px;
&__wrapper {
padding: 10px;
min-width: 160px;
margin: 8px auto;
transition: 0.3s;
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
text-align: center;
&:hover {
box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
}
}
&__container {
font-size: $font-size--lg;
padding: 16px;
}
}

View File

@ -1,100 +0,0 @@
import React from 'react';
import {Line} from 'react-chartjs-2';
import i18n from 'lib-app/i18n';
class StatsChart extends React.Component {
static propTypes = {
strokes: React.PropTypes.arrayOf(React.PropTypes.shape({
name: React.PropTypes.string,
values: React.PropTypes.arrayOf(React.PropTypes.shape({
date: React.PropTypes.string,
value: React.PropTypes.number
}))
})),
period: React.PropTypes.number
};
render() {
return (
<div>
<Line data={this.getChartData()} options={this.getChartOptions()} width={800} height={400} redraw/>
</div>
);
}
getChartData() {
let labels = this.getLabels();
let color = {
'CLOSE': 'rgba(150, 20, 20, 0.6)',
'CREATE_TICKET': 'rgba(20, 150, 20, 0.6)',
'SIGNUP': 'rgba(20, 20, 150, 0.6)',
'COMMENT': 'rgba(20, 200, 200, 0.6)',
'ASSIGN': 'rgba(20, 150, 20, 0.6)'
};
let strokes = this.props.strokes.slice();
let datasets = strokes.map((stroke, index) => {
return {
label: i18n('CHART_' + stroke.name),
data: stroke.values.map((val) => val.value),
fill: false,
borderWidth: this.getBorderWidth(),
borderColor: color[stroke.name],
pointBorderColor: color[stroke.name],
pointRadius: 0,
pointHoverRadius: 3,
lineTension: 0.2,
pointHoverBackgroundColor: color[stroke.name],
hitRadius: this.hitRadius(index)
}
});
return {
labels: labels,
datasets: datasets
};
}
getBorderWidth() {
return (this.props.period <= 90) ? 3 : 2;
}
getLabels() {
let months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
let labels = [];
if (!this.props.strokes.length) {
labels = Array.from('x'.repeat(this.props.period));
}
else {
labels = this.props.strokes[0].values.map((item) => {
let idx = item.date.slice(4, 6) - 1;
return item.date.slice(6, 8) + ' ' + months[idx];
});
}
return labels;
}
hitRadius(index) {
if (this.props.period <= 7) return 20;
if (this.props.period <= 30) return 15;
if (this.props.period <= 90) return 10;
return 3;
}
getChartOptions() {
return {
legend: {
display: false
}
};
}
}
export default StatsChart;

View File

@ -1,191 +0,0 @@
import React from 'react';
import _ from 'lodash';
import classNames from 'classnames';
import i18n from 'lib-app/i18n';
import API from 'lib-app/api-call';
import DropDown from 'core-components/drop-down';
import ToggleList from 'core-components/toggle-list';
import StatsChart from 'app-components/stats-chart';
const generalStrokes = ['CREATE_TICKET', 'CLOSE', 'SIGNUP', 'COMMENT'];
const staffStrokes = ['ASSIGN', 'CLOSE'];
const ID = {
'CREATE_TICKET': 0,
'ASSIGN': 0,
'CLOSE': 1,
'SIGNUP': 2,
'COMMENT': 3
};
const statsPeriod = {
'WEEK': 7,
'MONTH': 30
};
class Stats extends React.Component {
static propTypes = {
type: React.PropTypes.string,
staffId: React.PropTypes.number
};
state = {
stats: this.getDefaultStats(),
strokes: this.getStrokes().map((name) => {
return {
name: name,
values: []
}
}),
showed: [0],
period: 0
};
componentDidMount() {
this.retrieve('WEEK');
}
render() {
return (
<div className={this.getClass()}>
<DropDown {...this.getDropDownProps()}/>
<ToggleList {...this.getToggleListProps()} />
<StatsChart {...this.getStatsChartProps()} />
<div className="stats__disable-box">
<div className="stats__disable-box-message">
{i18n('CURRENTLY_UNAVAILABLE')}
</div>
</div>
</div>
);
}
getClass() {
let classes = {
'stats': true,
'stats_staff': this.props.type === 'staff'
};
return classNames(classes);
}
getToggleListProps() {
return {
values: this.state.showed,
className: 'stats__toggle-list',
onChange: this.onToggleListChange.bind(this),
type: this.props.type === 'general' ? 'default' : 'small',
items: this.getStrokes().map((name) => {
return {
className: 'stats__toggle-list_' + name,
content: (
<div className="stats__toggle-list-item">
<div className="stats__toggle-list-item-value">{this.state.stats[name]}</div>
<div className="stats__toggle-list-item-name">{i18n('CHART_' + name)}</div>
</div>
)
}
})
};
}
onToggleListChange(event) {
this.setState({
showed: event.target.value
});
}
getDropDownProps() {
return {
items: Object.keys(statsPeriod).map(key => 'LAST_' + statsPeriod[key] + '_DAYS').map((name) => {
return {
content: i18n(name),
icon: ''
};
}),
onChange: this.onDropDownChange.bind(this),
className: 'stats__dropdown'
}
}
onDropDownChange(event) {
this.retrieve(Object.keys(statsPeriod)[event.index]);
}
getStatsChartProps() {
let showed = this.getShowedArray();
return {
period: this.state.period,
strokes: _.filter(this.state.strokes, (s, i) => showed[i])
};
}
retrieve(periodName) {
API.call({
path: '/system/get-stats',
data: this.getApiCallData(periodName)
}).then(this.onRetrieveSuccess.bind(this));
}
onRetrieveSuccess(result) {
let newStats = this.getDefaultStats();
let newStrokes = this.getStrokes().map((name) => {
return {
name: name,
values: []
};
});
let realPeriod = result.data.length / this.getStrokes().length;
result.data.reverse().map((item) => {
newStats[item.type] += item.value * 1;
newStrokes[ ID[item.type] ].values.push({
date: item.date,
value: item.value * 1
});
});
this.setState({stats: newStats, strokes: newStrokes, period: realPeriod});
}
getShowedArray() {
let showed = this.getStrokes().map(() => false);
for (let i = 0; i < showed.length; i++) {
showed[this.state.showed[i]] = true;
}
return showed;
}
getStrokes() {
return this.props.type === 'general' ? generalStrokes : staffStrokes;
}
getDefaultStats() {
return this.props.type === 'general' ?
{
'CREATE_TICKET': 0,
'CLOSE': 0,
'SIGNUP': 0,
'COMMENT': 0
} :
{
'ASSIGN': 0,
'CLOSE': 0
};
}
getApiCallData(periodName) {
return this.props.type === 'general' ? {period: periodName} : {period: periodName, staffId: this.props.staffId};
}
}
export default Stats;

View File

@ -1,95 +0,0 @@
@import '../scss/vars';
.stats {
pointer-events: none;
position: relative;
&__disable-box {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
background-color: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
&-message {
color: white;
font-size: $font-size--bg;
}
}
&__dropdown {
margin-left: auto;
margin-bottom: 20px;
}
&__toggle-list {
margin-bottom: 20px;
user-select: none;
&-item {
&-value {
font-size: $font-size--lg;
line-height: 80px;
}
&-name {
font-size: $font-size--md;
line-height: 20px;
display: inline-flex;
}
}
&_CREATE_TICKET.toggle-list__selected {
box-shadow: inset 0 -5px 0px 0px rgba(20, 150, 20, 0.6);
}
&_CLOSE.toggle-list__selected {
box-shadow: inset 0 -5px 0px 0px rgba(150, 20, 20, 0.6);
}
&_SIGNUP.toggle-list__selected {
box-shadow: inset 0 -5px 0px 0px rgba(20, 20, 150, 0.6);
}
&_COMMENT.toggle-list__selected {
box-shadow: inset 0 -5px 0px 0px rgba(20, 200, 200, 0.6);
}
&_ASSIGN.toggle-list__selected {
box-shadow: inset 0 -5px 0px 0px rgba(20, 150, 20, 0.6);
}
}
&_staff {
.stats__dropdown {
margin-left: auto;
margin-bottom: 20px;
float: left;
}
.stats__toggle-list {
margin-bottom: 20px;
float: right;
&-item {
&-value {
font-size: $font-size--md;
line-height: 40px;
}
&-name {
font-size: $font-size--sm;
line-height: 20px;
}
}
}
}
}

View File

@ -1,5 +1,6 @@
import React from 'react';
import classNames from 'classnames';
import {connect} from 'react-redux';
import i18n from 'lib-app/i18n';
import API from 'lib-app/api-call';
@ -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);

View File

@ -1,6 +1,7 @@
import React from 'react';
import _ from 'lodash';
import {connect} from 'react-redux';
import queryString from 'query-string';
import i18n from 'lib-app/i18n';
import DateTransformer from 'lib-core/date-transformer';
@ -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) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -37,15 +37,16 @@ class TopicEditModal extends React.Component {
<FormField name="title" label={i18n('TITLE')} fieldProps={{size: 'large'}} validation="TITLE" required />
<FormField name="icon" className="topic-edit-modal__icon" label={i18n('ICON')} decorator={IconSelector} />
<FormField name="color" className="topic-edit-modal__color" label={i18n('COLOR')} decorator={ColorSelector} />
<FormField className="topic-edit-modal__private" label={i18n('PRIVATE')} name="private" field="checkbox"/>
<FormField className="topic-edit-modal__private" label={i18n('PRIVATE')} name="private" field="checkbox" />
<InfoTooltip className="topic-edit-modal__private" text={i18n('PRIVATE_TOPIC_DESCRIPTION')} />
<SubmitButton className="topic-edit-modal__save-button" type="secondary" size="small">
{i18n('SAVE')}
</SubmitButton>
<Button className="topic-edit-modal__discard-button" onClick={this.onDiscardClick.bind(this)} size="small">
{i18n('CANCEL')}
</Button>
<div className="topic-edit-modal__buttons-container">
<Button className="topic-edit-modal__discard-button" onClick={this.onDiscardClick.bind(this)} size="small">
{i18n('CANCEL')}
</Button>
<SubmitButton className="topic-edit-modal__save-button" type="secondary" size="small">
{i18n('SAVE')}
</SubmitButton>
</div>
</Form>
</div>
);

View File

@ -13,9 +13,19 @@
}
&__discard-button {
float: right;
&__buttons-container {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 20px 0 0 0;
}
&__discard-button {
float: left;
}
&__private {
display: inline-block;
padding-right: 10px;

View File

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

View File

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

View File

@ -16,6 +16,9 @@ import Message from 'core-components/message';
import Widget from 'core-components/widget';
import WidgetTransition from 'core-components/widget-transition';
import Captcha from 'app/main/captcha';
const MAX_FREE_LOGIN_ATTEMPTS = 3;
class AdminLoginPage extends React.Component {
state = {
@ -24,11 +27,14 @@ class AdminLoginPage extends React.Component {
recoverFormErrors: {},
recoverSent: false,
loadingLogin: false,
loadingRecover: false
loadingRecover: false,
showRecoverSentMessage: true,
showEmailOrPassordErrorMessage: true
};
componentDidUpdate(prevProps) {
if (!prevProps.session.failed && this.props.session.failed) {
this.setState({showEmailOrPassordErrorMessage : true});
this.refs.loginForm.refs.password.focus();
}
}
@ -49,11 +55,33 @@ class AdminLoginPage extends React.Component {
<div>
<Widget className="admin-login-page__content">
<div className="admin-login-page__image"><img width="100%" src={API.getURL() + '/images/logo.png'} alt="OpenSupports Admin Panel"/></div>
<div className="admin-login-page__login-form">
<Form onSubmit={this.onLoginFormSubmit.bind(this)} loading={this.props.session.pending}>
<FormField name="email" label={i18n('EMAIL')} field="input" validation="EMAIL" fieldProps={{size:'large'}} required />
<FormField name="password" label={i18n('PASSWORD')} field="input" fieldProps={{password:true, size:'large'}} />
<SubmitButton>{i18n('LOG_IN')}</SubmitButton>
<div className="admin-login-page__login-form-container">
<Form {...this.getLoginFormProps()}>
<div className="admin-login-page__login-form-container__login-form__fields">
<FormField
name="email"
label={i18n('EMAIL')}
className="admin-login-page__login-form-container__login-form__fields__email"
field="input"
validation="EMAIL"
fieldProps={{size:'large'}}
required />
<FormField
name="password"
label={i18n('PASSWORD')}
className="admin-login-page__login-form-container__login-form__fields__password"
field="input"
fieldProps={{password:true, size:'large'}} />
<FormField
name="remember"
label={i18n('REMEMBER_ME')}
className="admin-login-page__login-form-container__login-form__fields__remember"
field="checkbox" />
</div>
{this.props.session.loginAttempts > MAX_FREE_LOGIN_ATTEMPTS ? this.renderLoginCaptcha() : null}
<div className="admin-login-page__login-form-container__login-form__submit-button">
<SubmitButton>{i18n('LOG_IN')}</SubmitButton>
</div>
</Form>
</div>
{this.renderRecoverStatus()}
@ -66,46 +94,57 @@ class AdminLoginPage extends React.Component {
);
}
renderLoginCaptcha() {
return(
<div className={`main-home-page__${this.props.sitekey ? "captcha" : "no-captcha"}`}>
<Captcha ref="captcha" />
</div>
)
}
renderPasswordRecovery() {
return (
<div>
<div className="admin-login-page__recovery-form-container">
<PasswordRecovery recoverSent={this.state.recoverSent} formProps={this.getRecoverFormProps()} onBackToLoginClick={this.onBackToLoginClick.bind(this)} renderLogo={true}/>
</div>
);
}
renderRecoverStatus() {
let status = null;
const { showRecoverSentMessage, recoverSent } = this.state;
if (this.state.recoverSent) {
status = (
<Message className="admin-login-page__message" type="info" leftAligned>
{i18n('RECOVER_SENT')}
</Message>
);
}
return status;
return (
recoverSent ?
<Message
showMessage={showRecoverSentMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showRecoverSentMessage")}
className="admin-login-page__message"
type="info"
leftAligned>
{i18n('RECOVER_SENT')}
</Message> :
null
);
}
renderErrorStatus() {
let status = null;
if (this.props.session.failed) {
status = (
<Message className="admin-login-page__error" type="error">
{i18n('EMAIL_OR_PASSWORD')}
</Message>
);
}
return status;
return (
this.props.session.failed ?
<Message
showMessage={this.state.showEmailOrPassordErrorMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showEmailOrPassordErrorMessage")}
className="admin-login-page__error"
type="error">
{i18n('EMAIL_OR_PASSWORD')}
</Message> :
null
);
}
getLoginFormProps() {
return {
loading: this.props.session.pending,
className: 'admin-login-page__form',
className: 'admin-login-page__login-form-container__login-form',
ref: 'loginForm',
onSubmit: this.onLoginFormSubmit.bind(this),
errors: this.getLoginFormErrors(),
@ -114,12 +153,14 @@ class AdminLoginPage extends React.Component {
}
getRecoverFormProps() {
const { loadingRecover, recoverFormErrors } = this.state;
return {
loading: this.state.loadingRecover,
className: 'admin-login-page__form',
loading: loadingRecover,
className: 'admin-login-page__recovery-form-container__recovery-form',
ref: 'recoverForm',
onSubmit: this.onForgotPasswordSubmit.bind(this),
errors: this.state.recoverFormErrors,
errors: recoverFormErrors,
onValidateErrors: this.onRecoverFormErrorsValidation.bind(this)
};
}
@ -184,7 +225,8 @@ class AdminLoginPage extends React.Component {
onRecoverPasswordSent() {
this.setState({
loadingRecover: false,
recoverSent: true
recoverSent: true,
showRecoverSentMessage: true
});
}
@ -198,10 +240,17 @@ class AdminLoginPage extends React.Component {
this.refs.recoverForm.refs.email.focus();
}.bind(this));
}
onCloseMessage(showMessage) {
this.setState({
[showMessage]: false
});
}
}
export default connect((store) => {
return {
session: store.session
session: store.session,
sitekey: store.config.reCaptchaKey
};
})(AdminLoginPage);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,6 @@ import {connect} from 'react-redux';
import _ from 'lodash';
import i18n from 'lib-app/i18n';
import API from 'lib-app/api-call';
import SessionActions from 'actions/session-actions';
import StaffEditor from 'app/admin/panel/staff/staff-editor';

View File

@ -1,25 +1,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);

View File

@ -0,0 +1,110 @@
@import "../../../../scss/vars";
.admin-panel-stats {
text-align: left;
&__form {
&__container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
&__row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 80%;
}
&__col {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
}
}
&__container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
&__button {
margin: 0 10px;
}
}
&__loading {
min-height: 361px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: $grey;
}
&__card-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-around;
}
&__department-option { // Duplicated from ticket-query-filters, please REMOVE
&__name {
margin-left: 10px;
}
}
&__department-selected { // Duplicated from ticket-query-filters, please REMOVE
display: inline-block;
border-radius: 3px;
margin-left: 5px;
padding: 1px;
font-size: 13px;
cursor: default;
&__icon {
padding-right: 5px;
}
}
&__staff-option { // Duplicated from ticket-query-filters, please REMOVE
&__profile-pic {
width: 25px;
height: 25px;
border-radius: 50%;
}
&__name {
margin-left: 10px;
}
}
&__staff-selected { // Duplicated from ticket-query-filters, please REMOVE
display: inline-block;
border-radius: 3px;
margin-left: 5px;
padding: 1px;
font-size: 13px;
cursor: default;
&__profile-pic {
width: 25px;
height: 25px;
border-radius: 50%;
margin-right: 5px;
}
}
}
@media screen and (max-width: 992px) {
.admin-panel-stats__form__container__row {
display: flex;
flex-direction: column;
}
}

View File

@ -4,7 +4,6 @@ import {connect} from 'react-redux';
import ConfigActions from 'actions/config-actions';
import API from 'lib-app/api-call';
import i18n from 'lib-app/i18n';
import ToggleButton from 'app-components/toggle-button';
import AreYouSure from 'app-components/are-you-sure';
import ModalContainer from 'app-components/modal-container';
@ -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

View File

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

View File

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

View File

@ -2,6 +2,7 @@
&__actions{
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
}
}

View File

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

View File

@ -77,10 +77,10 @@ class AdminPanelEmailSettings extends React.Component {
return (
<div>
{this.renderEmailSettings()}
<Header title={i18n('EMAIL_TEMPLATES')} description={i18n('EMAIL_TEMPLATES_DESCRIPTION')}/>
<Header title={i18n('EMAIL_TEMPLATES')} description={i18n('EMAIL_TEMPLATES_DESCRIPTION')} />
<div className="row">
<div className="col-md-3">
<Listing {...this.getListingProps()}/>
<Listing {...this.getListingProps()} />
</div>
{(this.state.selectedIndex !== -1) ? this.renderForm() : null}
</div>
@ -91,7 +91,7 @@ class AdminPanelEmailSettings extends React.Component {
renderLoading() {
return (
<div className="admin-panel-email-settings__loading">
<Loading backgrounded size="large"/>
<Loading backgrounded size="large" />
</div>
);
}
@ -99,27 +99,31 @@ class AdminPanelEmailSettings extends React.Component {
renderEmailSettings() {
return (
<div>
<Header title={i18n('EMAIL_SETTINGS')} description={i18n('EMAIL_SETTINGS_DESCRIPTION')}/>
<Header title={i18n('EMAIL_SETTINGS')} description={i18n('EMAIL_SETTINGS_DESCRIPTION')} />
<Form className="admin-panel-email-settings__email-form"
onSubmit={this.submitEmailAddress.bind(this)}
onChange={emailForm => this.setState({emailForm})}
values={this.state.emailForm}>
<FormField name="server-email"
label={i18n('EMAIL_SERVER_ADDRESS')}
fieldProps={{size: 'large'}}
infoMessage={i18n('EMAIL_SERVER_ADDRESS_DESCRIPTION')}/>
<SubmitButton className="admin-panel-email-settings__submit" type="secondary"
size="small">{i18n('SAVE')}</SubmitButton>
<div className="admin-panel-email-settings__email-container">
<FormField className="admin-panel-email-settings__email-server-address"
name="server-email"
label={i18n('EMAIL_SERVER_ADDRESS')}
fieldProps={{size: 'large'}}
infoMessage={i18n('EMAIL_SERVER_ADDRESS_DESCRIPTION')} />
<SubmitButton className="admin-panel-email-settings__submit" type="secondary"
size="small">{i18n('SAVE')}</SubmitButton>
</div>
</Form>
<Form values={{headerImage: this.state.headerImage}}
<Form className="admin-panel-email-settings__image-form"
values={{headerImage: this.state.headerImage}}
onChange={form => this.setState({headerImage: form.headerImage})}
onSubmit={this.onHeaderImageSubmit.bind(this)}>
<div className="admin-panel-email-settings__image-container">
<FormField className="admin-panel-email-settings__image-header-url"
label={i18n('IMAGE_HEADER_URL')} name="headerImage" required
infoMessage={i18n('IMAGE_HEADER_URL_DESCRIPTION')}
fieldProps={{size: 'large'}}/>
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)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,6 @@ import ModalContainer from 'app-components/modal-container';
import InviteStaffModal from 'app/admin/panel/staff/invite-staff-modal';
import Header from 'core-components/header';
import DropDown from 'core-components/drop-down';
import Button from 'core-components/button';
import Icon from 'core-components/icon';
import Loading from 'core-components/loading';
@ -48,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>
);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,15 @@
import React from 'react';
import _ from 'lodash';
import classNames from 'classnames';
import {connect} from 'react-redux';
import AdminDataActions from 'actions/admin-data-actions';
import i18n from 'lib-app/i18n';
import API from 'lib-app/api-call';
import SessionStore from 'lib-app/session-store';
import TicketList from 'app-components/ticket-list';
import AreYouSure from 'app-components/are-you-sure';
// import Stats from 'app-components/stats';
import Form from 'core-components/form';
import FormField from 'core-components/form-field';
@ -16,62 +18,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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,5 +26,6 @@
&__buttons {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
}

View File

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

View File

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

View File

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

View File

@ -22,7 +22,15 @@
text-align: center;
}
&__success-message {
margin-bottom: 20px;
}
&__add-user-form {
max-width: 500px;
}
&__invite-user-form {
min-width: 700px;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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