Compare commits

..

134 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
343 changed files with 5731 additions and 2460 deletions

View File

@ -5,16 +5,22 @@ orbs:
aws-cli: circleci/aws-cli@1.0.0
jobs:
install_composer_packages:
executor: php/default
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 install php-imap
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/

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

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.

View File

@ -22,7 +22,11 @@ deploy-staging-population:
--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

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.9.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.

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

821
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "OpenSupports",
"version": "4.9.0",
"version": "4.11.0",
"author": "Ivan Diaz <contact@opensupports.com>",
"description": "Open source ticket system made with PHP and ReactJS",
"repository": {
@ -9,11 +9,11 @@
},
"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": "./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"
@ -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",
@ -74,12 +74,13 @@
"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",
@ -94,4 +95,4 @@
"redux": "^3.5.2",
"redux-promise-middleware": "^3.3.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

@ -101,7 +101,7 @@ export default {
};
},
initSession() {
checkSession() {
return {
type: 'CHECK_SESSION',
payload: new Promise((resolve, reject) => {

View File

@ -39,10 +39,10 @@ class ArticlesList extends React.Component {
const { errored, loading } = this.props;
if(errored) {
return <Message type="error">{i18n('ERROR_RETRIEVING_ARTICLES')}</Message>;
return <Message showCloseButton={false} type="error">{i18n('ERROR_RETRIEVING_ARTICLES')}</Message>;
}
return loading ? <Loading /> : this.renderContent();
return loading ? <Loading className="articles-list__loading" backgrounded size="large"/> : this.renderContent();
}
renderContent() {

View File

@ -5,6 +5,11 @@
position: relative;
}
&__loading {
min-width: 300px;
min-height: 300px;
}
&__add-topic-button {
display: flex;
flex-direction: row;

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

@ -11,7 +11,7 @@ class ModalContainer extends React.Component {
static openModal(
content,
options={noPadding: false, outsideClick: false, closeButton: {showCloseButton: false, whiteColor: false}}
) {
) {
store.dispatch(
ModalActions.openModal({
content,

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

@ -16,7 +16,7 @@ class PopupMessage extends React.Component {
static open(props) {
ModalContainer.openModal(
<PopupMessage {...props} />,
{noPadding: true, outsideClick: true}
{noPadding: true, outsideClick: true, closeButton: {showCloseButton: false, whiteColor: false}}
);
}
@ -27,7 +27,7 @@ class PopupMessage extends React.Component {
render() {
return (
<div className="popup-message">
<Message {...this.props} className="popup-message__message" />
<Message {...this.props} showCloseButton={false} className="popup-message__message" />
<Button
className="popup-message__close-button"
iconName="times"

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

@ -31,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 = {
@ -92,12 +94,7 @@ class TicketEvent extends React.Component {
}
renderComment() {
const {
author,
date,
edit,
file
} = this.props;
const { author, date, edit, file } = this.props;
const customFields = (author && author.customfields) || [];
return (
@ -142,10 +139,13 @@ class TicketEvent extends React.Component {
}
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>
)
}

View File

@ -15,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,7 +32,8 @@ class TicketList extends React.Component {
]),
closedTicketsShown: React.PropTypes.bool,
onClosedTicketsShownChange: React.PropTypes.func,
onDepartmentChange: React.PropTypes.func
onDepartmentChange: React.PropTypes.func,
showPageSizeDropdown: React.PropTypes.bool
};
static defaultProps = {
@ -40,7 +43,8 @@ class TicketList extends React.Component {
departments: [],
ticketPath: '/dashboard/ticket/',
type: 'primary',
closedTicketsShown: false
closedTicketsShown: false,
showPageSizeDropdown: true
};
state = {
@ -48,25 +52,32 @@ class TicketList extends React.Component {
};
render() {
const { type, showDepartmentDropdown, onClosedTicketsShownChange } = this.props;
const { type, showDepartmentDropdown, onClosedTicketsShownChange, showPageSizeDropdown } = this.props;
const pages = [5, 10, 20, 50];
return (
<div className="ticket-list">
<div className="ticket-list__filters">
{(type === 'primary') ? this.renderMessage() : null}
{
((type === 'secondary') && 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
}
{onClosedTicketsShownChange ? this.renderFilterCheckbox() : null}
</div>
<Table {...this.getTableProps()} />
</div>
);
}
renderFilterCheckbox() {
return (
<Checkbox
@ -90,14 +101,34 @@ class TicketList extends React.Component {
renderMessage() {
switch (queryString.parse(window.location.search)["message"]) {
case 'success':
return <Message className="create-ticket-form__message" type="success">{i18n('TICKET_SENT')}</Message>
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;
@ -123,7 +154,7 @@ class TicketList extends React.Component {
loading,
headers: this.getTableHeaders(),
rows: this.getTableRows(),
pageSize: 10,
pageSize: this.state.tickets,
page,
pages,
onPageChange
@ -226,7 +257,7 @@ class TicketList extends React.Component {
}
getTableRows() {
return this.getTickets().map(this.gerTicketTableObject.bind(this));
return this.getTickets().map(this.getTicketTableObject.bind(this));
}
getTickets() {
@ -240,7 +271,7 @@ class TicketList extends React.Component {
);
}
gerTicketTableObject(ticket) {
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);
@ -248,7 +279,7 @@ class TicketList extends React.Component {
const ticketDate = (
((dateTodayWithOutHoursAndMinutes - ticketDateWithOutHoursAndMinutes) > 1) ?
stringTicketLocalDateFormat :
`${(dateTodayWithOutHoursAndMinutes - ticketDateWithOutHoursAndMinutes) ? "Yesterday" : "Today"} at ${stringTicketLocalDateFormat.slice(-5)}`
`${(dateTodayWithOutHoursAndMinutes - ticketDateWithOutHoursAndMinutes) ? i18n("YESTERDAY_AT") : i18n("TODAY_AT")} ${stringTicketLocalDateFormat.slice(-5)}`
);
let titleText = (this.isTicketUnread(ticket)) ? title + ' (1)' : title;
@ -293,6 +324,10 @@ class TicketList extends React.Component {
}
}
}
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,6 +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 ticketUtils from 'lib-app/ticket-utils';
import Form from 'core-components/form';
import SubmitButton from 'core-components/submit-button';
@ -34,7 +35,8 @@ class TicketQueryFilters extends React.Component {
formState,
filters,
showFilters,
ticketQueryListState
ticketQueryListState,
staffList
} = this.props;
return (
@ -47,22 +49,15 @@ 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__first-row">
<div className="ticket-query-filters__second-row">
<FormField
label={i18n('DATE')}
name="dateRange"
field="date-range"
fieldProps={{defaultValue: formState.dateRange}} />
<FormField
label={i18n('STATUS')}
name="closed"
label={i18n('PERIOD')}
name="period"
field="select"
fieldProps={{
items: this.getStatusItems(),
className: 'ticket-query-filters__status-drop-down'
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'
}} />
</div>
<div className="ticket-query-filters__second-row">
<FormField
label={i18n('DEPARTMENTS')}
name="departments"
@ -72,9 +67,17 @@ class TicketQueryFilters extends React.Component {
label={i18n('OWNER')}
name="owners"
field="autocomplete"
fieldProps={{items: this.getStaffList()}} />
fieldProps={{items: ticketUtils.getStaffList({staffList}, 'toAutocomplete')}} />
</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"
@ -131,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
}});
});
}
@ -159,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) {
@ -212,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;
@ -233,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')},
@ -298,8 +267,8 @@ class TicketQueryFilters extends React.Component {
true
);
if(formEdited && formState.dateRange.valid) {
const filtersForAPI = searchTicketsUtils.prepareFiltersForAPI(listConfigWithCompleteAuthorsList.filters);
if(formEdited) {
const filtersForAPI = searchTicketsUtils.getFiltersForAPI(listConfigWithCompleteAuthorsList.filters);
const currentPath = window.location.pathname;
const urlQuery = searchTicketsUtils.getFiltersForURL({
filters: filtersForAPI,
@ -312,24 +281,23 @@ 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) {
const newStartDate = data.dateRange.startDate ? data.dateRange.startDate : searchTicketsUtils.getDefaultLocalStartDate();
const newEndDate = data.dateRange.endDate ? data.dateRange.endDate : searchTicketsUtils.getDefaultlocalEndDate();
const departmentsId = data.departments.map(department => department.id);
const staffsId = data.owners.map(staff => staff.id);
const tagsName = this.tagsNametoTagsId(data.tags);
@ -341,11 +309,6 @@ class TicketQueryFilters extends React.Component {
owners: staffsId,
departments: departmentsId,
authors: authors,
dateRange: {
...data.dateRange,
startDate: newStartDate,
endDate: newEndDate
}
});
}
@ -366,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

@ -58,7 +58,7 @@
align-items: flex-start;
}
&__status-drop-down > .drop-down__current-item {
&__drop-down > .drop-down__current-item {
background-color: $very-light-grey;
&:focus {

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

@ -10,6 +10,7 @@ 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';
@ -71,6 +72,7 @@ class TicketViewer extends React.Component {
editTags: false,
editOwner: false,
editDepartment: false,
showTicketCommentErrorMessage: true
};
componentDidMount() {
@ -83,35 +85,61 @@ class TicketViewer extends React.Component {
render() {
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()}
{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={userStaff ? MentionsParser.parse(ticket.content) : ticket.content}
isLastComment={!events.filter(event => event.type === "COMMENT").length}
author={author}
isTicketClosed={closed}
content={userStaff ? MentionsParser.parse(content) : content}
userStaff={userStaff}
userId={userId}
date={ticket.date}
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={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>
{(!ticket.closed && (editable || !assignmentAllowed)) ? this.renderResponseField() : (this.showDeleteButton()) ? this.renderDeleteTicketButton() : 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;
@ -141,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>
@ -238,7 +266,7 @@ class TicketViewer extends React.Component {
onRemoveClick={this.removeTag.bind(this)}
onTagSelected={this.addTag.bind(this)}
loading={this.state.tagSelectorLoading} />
{this.renderCancelButton("Tags")}
{this.renderCancelButton("Tags", "CLOSE")}
</div>
);
}
@ -246,16 +274,20 @@ class TicketViewer extends React.Component {
renderEditStatus() {
return (
<div className="ticket-viewer__edit-status__buttons">
{this.renderCancelButton("Status")}
{this.props.ticket.closed ?
<Button type='secondary' size="medium" onClick={this.onReopenClick.bind(this)}>
{i18n('RE_OPEN')}
</Button> :
this.renderCloseTicketButton()}
{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;
@ -317,7 +349,7 @@ class TicketViewer extends React.Component {
if(assignmentAllowed && ticket.owner) {
ownerNode = (
<a className="ticket-viewer__info-owner-name" href={this.searchTickets(filtersOnlyWithOwner)}>
{ticket.owner.name}
{ticketUtils.renderStaffSelected(ticket.owner)}
</a>
);
} else {
@ -344,7 +376,7 @@ class TicketViewer extends React.Component {
className="ticket-viewer__editable-dropdown" items={items}
selectedIndex={selectedIndex}
onChange={this.onAssignmentChange.bind(this)} />
{this.renderCancelButton("Owner")}
{this.renderCancelButton("Owner", "CANCEL")}
</div>
);
}
@ -360,7 +392,7 @@ class TicketViewer extends React.Component {
departments={departments}
selectedIndex={_.findIndex(departments, {id: ticket.department.id})}
onChange={this.onDepartmentDropdownChanged.bind(this)} />
{this.renderCancelButton("Department")}
{this.renderCancelButton("Department", "CANCEL")}
</div>
);
}
@ -387,26 +419,30 @@ class TicketViewer extends React.Component {
)
}
renderCancelButton(option) {
return <Button type='link' size="medium" onClick={() => this.setState({["edit"+option]: false})}>{i18n('CLOSE')}</Button>
renderCancelButton(option, type) {
return <Button type='link' size="medium" onClick={() => this.setState({["edit"+option]: false})}>{i18n(type)}</Button>
}
renderTicketEvent(options, index) {
renderTicketEvent(isTicketClosed, ticketEventObject, index) {
const { userStaff, ticket, userId, allowAttachments } = this.props;
const { edit, editId } = this.state;
const { content, author, id} = ticketEventObject;
if(userStaff && typeof options.content === 'string') {
options.content = MentionsParser.parse(options.content);
if(userStaff && typeof content === 'string') {
ticketEventObject.content = MentionsParser.parse(content);
}
return (
<TicketEvent
{...options}
author={(!_.isEmpty(options.author)) ? options.author : ticket.author}
{...ticketEventObject}
isLastComment={ticketEventObject.isLastComment}
author={(!_.isEmpty(author)) ? author : ticket.author}
userStaff={userStaff}
isTicketClosed={isTicketClosed}
userId={userId}
onEdit={this.onEdit.bind(this, options.id)}
edit={this.state.edit && this.state.editId == options.id}
onToggleEdit={this.onToggleEdit.bind(this, options.id)}
onEdit={this.onEdit.bind(this, id)}
edit={edit && editId == id}
onToggleEdit={this.onToggleEdit.bind(this, id)}
key={index}
allowAttachments={allowAttachments} />
);
@ -427,14 +463,15 @@ class TicketViewer extends React.Component {
</div>
<div className="ticket-viewer__response-field row">
<FormField name="content" validation="TEXT_AREA" required field="textarea" fieldProps={{allowImages: allowAttachments}} />
<div className="ticket-viewer__response-buttons">
{allowAttachments ? <FormField name="file" field="file" /> : null}
<SubmitButton type="secondary">{i18n('RESPOND_TICKET')}</SubmitButton>
</div>
<div className="ticket-viewer__buttons-column">
<div className="ticket-viewer__buttons-row">
{(this.showDeleteButton()) ? this.renderDeleteTicketButton() : null}
{this.renderCloseTicketButton()}
<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>
@ -494,8 +531,16 @@ 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>
);
}
@ -636,7 +681,7 @@ class TicketViewer extends React.Component {
}
}).then((result) => {
this.onTicketModification(result);
history.push('/admin/panel/tickets/my-tickets/');
history.push(history.goBack());
});
}
@ -722,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({
@ -740,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() {
@ -754,10 +799,11 @@ class TicketViewer extends React.Component {
this.onTicketModification();
}
onFailCommentFail() {
onEditCommentFail() {
this.setState({
loading: false,
commentError: true
commentError: true,
showTicketCommentErrorMessage: true
});
}
@ -780,6 +826,7 @@ class TicketViewer extends React.Component {
loading: false,
commentValue: TextEditor.createEmpty(),
commentError: false,
commentFile: null,
commentEdited: false
});
@ -789,7 +836,8 @@ class TicketViewer extends React.Component {
onCommentFail() {
this.setState({
loading: false,
commentError: true
commentError: true,
showTicketCommentErrorMessage: true
});
}
@ -800,37 +848,19 @@ class TicketViewer extends React.Component {
}
getStaffAssignmentItems() {
const { userDepartments, userId, ticket } = this.props;
const { staffMembers, ticket } = this.props;
let staffAssignmentItems = [
{content: i18n('NONE'), contentOnSelected: i18n('NONE'), id: 0}
];
if(_.some(userDepartments, {id: ticket.department.id})) {
staffAssignmentItems.push({
content: i18n('ASSIGN_TO_ME'),
contentOnSelected: this.getCurrentStaff().name,
id: userId
});
}
staffAssignmentItems = staffAssignmentItems.concat(
_.map(
this.getStaffList(),
({id, name}) => ({content: name, contentOnSelected: name, id: id*1})
ticketUtils.getStaffList({staffList: staffMembers, ticket}, 'toDropDown').map(
({id, content}) => ({content, id: id*1})
)
);
return staffAssignmentItems;
}
getStaffList() {
const { userId, staffMembers, ticket } = this.props;
return _.filter(staffMembers, ({id, departments}) => {
return (id != userId) && _.some(departments, {id: ticket.department.id});
})
}
getCurrentStaff() {
const { userId, staffMembers, ticket } = this.props;
@ -843,18 +873,24 @@ class TicketViewer extends React.Component {
showDeleteButton() {
const { ticket, userLevel, userId, userStaff } = this.props;
const { owner, author, closed } = ticket || {};
const { staff, id } = author || {};
if(!ticket.owner) {
if(!owner) {
if(userLevel === 3) return true;
if(userId == ticket.author.id*1) {
if((userStaff && ticket.author.staff) || (!userStaff && !ticket.author.staff)){
return true;
}
if(userId == id*1) {
return (userStaff && staff && closed);
}
}
return false;
}
onCloseMessage(showMessage) {
this.setState({
[showMessage]: false
});
}
}
export default connect((store) => {

View File

@ -33,6 +33,9 @@
justify-content: space-between;
align-items: flex-start;
width: 250px;
position: absolute;
left: 0;
bottom: 0;
}
&__edited-title-text {
@ -77,7 +80,10 @@
margin-right: 6px;
.input__text {
height: 25px;
height: 30px;
text-align: center;
padding-top: 12px;
border-radius: 5px;
}
}
@ -88,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;
@ -108,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;
@ -168,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;
@ -206,16 +228,21 @@
}
}
&-buttons {
&-container {
display: flex;
flex-direction: row;
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 {
}
@media screen and (max-width: 1151px) {

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

@ -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();
}
}
@ -72,6 +78,7 @@ class AdminLoginPage extends React.Component {
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>
@ -87,6 +94,14 @@ 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 className="admin-login-page__recovery-form-container">
@ -96,31 +111,34 @@ class AdminLoginPage extends React.Component {
}
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() {
@ -135,10 +153,7 @@ class AdminLoginPage extends React.Component {
}
getRecoverFormProps() {
const {
loadingRecover,
recoverFormErrors
} = this.state;
const { loadingRecover, recoverFormErrors } = this.state;
return {
loading: loadingRecover,
@ -210,7 +225,8 @@ class AdminLoginPage extends React.Component {
onRecoverPasswordSent() {
this.setState({
loadingRecover: false,
recoverSent: true
recoverSent: true,
showRecoverSentMessage: true
});
}
@ -224,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

@ -31,4 +31,10 @@
&__error {
margin-top: 30px;
}
&__captcha {
margin: 10px auto 20px;
height: 78px;
width: 304px;
}
}

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 size="medium" onClick={this.onDeleteClick.bind(this, article)}>
{i18n('DELETE')}
</Button>
<Button className="admin-panel-view-article__edit-button" size="medium" onClick={this.onEditClick.bind(this, article)} type="tertiary">
{i18n('EDIT')}
</Button>
<div 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>
);

View File

@ -4,17 +4,25 @@
word-break: break-word;
}
&__edit-buttons {
&__header-wrapper {
display: flex;
flex-direction: row;
height: 35px;
}
&__header-buttons {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 200px;
margin-bottom: 20px;
width: 50px;
margin-bottom: 5px;
margin-left: 15px;
}
&__edit-button {
margin-right: 20px;
&__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

@ -1,14 +1,12 @@
import React from 'react';
import { connect } from 'react-redux';
import { Bar, HorizontalBar } from 'react-chartjs-2';
import date from 'lib-app/date';
import API from 'lib-app/api-call';
import i18n from 'lib-app/i18n';
import StatCard from 'app-components/stat-card';
import statsUtils from 'lib-app/stats-utils';
import date from 'lib-app/date';
import Header from 'core-components/header';
import Tooltip from 'core-components/tooltip';
import Form from 'core-components/form';
import FormField from 'core-components/form-field';
import Icon from 'core-components/icon';
@ -21,7 +19,7 @@ class AdminPanelStats extends React.Component {
state = {
loading: true,
rawForm: {
dateRange: this.getInitialDateRange(),
period: 0,
departments: [],
owners: [],
tags: []
@ -29,29 +27,19 @@ class AdminPanelStats extends React.Component {
ticketData: {}
};
getInitialDateRange() {
let firstDayOfMonth = new Date();
firstDayOfMonth.setDate(1);
firstDayOfMonth.setHours(0);
firstDayOfMonth.setMinutes(0);
let todayAtNight = new Date();
todayAtNight.setHours(23);
todayAtNight.setMinutes(59);
return {
startDate: date.getFullDate(firstDayOfMonth),
endDate: date.getFullDate(todayAtNight)
}
}
componentDidMount() {
this.retrieveStats();
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() {
const {
loading,
rawForm
} = this.state;
const { loading, rawForm, ticketData } = this.state;
return (
<div className="admin-panel-stats">
@ -60,7 +48,7 @@ class AdminPanelStats extends React.Component {
<div className="admin-panel-stats__form__container">
<div className="admin-panel-stats__form__container__row">
<div className="admin-panel-stats__form__container__col">
<FormField name="dateRange" label={i18n('DATE')} field="date-range" fieldProps={{defaultValue: rawForm.dateRange}} />
<FormField name="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">
@ -90,98 +78,20 @@ class AdminPanelStats extends React.Component {
<span className="separator" />
</div>
</div>
{loading ? <div className="admin-panel-stats__loading"><Loading backgrounded size="large" /></div> : this.renderStatistics()}
{
loading ?
<div className="admin-panel-stats__loading"><Loading backgrounded size="large" /></div> :
statsUtils.renderStatistics({showStatCards: true, showStatsByHours: true, showStatsByDays: true, ticketData})
}
</div>
)
}
renderStatistics() {
const primaryBlueWithTransparency = (alpha) => `rgba(32, 184, 197, ${alpha})`;
const ticketsByHoursChartData = {
labels: Array.from(Array(24).keys()),
datasets: [
{
label: 'Created Tickets by Hour',
backgroundColor: primaryBlueWithTransparency(0.2),
borderColor: primaryBlueWithTransparency(1),
borderWidth: 1,
hoverBackgroundColor: primaryBlueWithTransparency(0.4),
hoverBorderColor: primaryBlueWithTransparency(1),
data: this.state.ticketData.created_by_hour
}
]
};
const primaryGreenWithTransparency = (alpha) => `rgba(130, 202, 156, ${alpha})`;
const ticketsByWeekdayChartData = {
labels: [
i18n('MONDAY'),
i18n('TUESDAY'),
i18n('WEDNESDAY'),
i18n('THURSDAY'),
i18n('FRIDAY'),
i18n('SATURDAY'),
i18n('SUNDAY')
],
datasets: [
{
label: 'Created Tickets by Weekday',
backgroundColor: primaryGreenWithTransparency(0.2),
borderColor: primaryGreenWithTransparency(1),
borderWidth: 1,
hoverBackgroundColor: primaryGreenWithTransparency(0.4),
hoverBorderColor: primaryGreenWithTransparency(1),
data: this.state.ticketData.created_by_weekday
}
]
}
return (
<div>
{this.renderStatCards()}
<Bar
options={this.getStatsOptions('y')}
data={ticketsByHoursChartData}
legend={{onClick: null}} /> {/* Weird, but if you only set the legend here, it changes that of the HorizontalBar next too*/}
<HorizontalBar
options={this.getStatsOptions('x')}
data={ticketsByWeekdayChartData}
legend={{onClick: null}} />
</div>
);
}
renderStatCards() {
const {created, open, closed, instant, reopened} = this.state.ticketData;
return (
<div className="admin-panel-stats__card-list">
<StatCard label={i18n('CREATED')} description={i18n('CREATED_DESCRIPTION')} value={created} isPercentage={false} />
<StatCard label={i18n('OPEN')} description={i18n('OPEN_DESCRIPTION')} value={open} isPercentage={false} />
<StatCard label={i18n('CLOSED')} description={i18n('CLOSED_DESCRIPTION')} value={closed} isPercentage={false} />
<StatCard label={i18n('INSTANT')} description={i18n('INSTANT_DESCRIPTION')} value={100*instant / closed} isPercentage={true} />
<StatCard label={i18n('REOPENED')} description={i18n('REOPENED_DESCRIPTION')} value={100*reopened / created} isPercentage={true} />
</div>
)
}
getStatsOptions(axis) {
return {
scales: {
[`${axis}Axes`]: [{
ticks: {
beginAtZero: true
}
}]
}
}
}
clearFormValues(event) {
event.preventDefault();
this.setState({
rawForm: {
dateRange: this.getInitialDateRange(),
period: 0,
departments: [],
owners: [],
tags: []
@ -199,10 +109,6 @@ class AdminPanelStats extends React.Component {
});
}
getSelectedTagIds() {
return this.props.tags.filter(tag => _.includes(this.state.rawForm.tags, tag.name)).map(tag => tag.id);
}
getStaffItems() {
const getStaffProfilePic = (staff) => {
return staff.profilePic ? API.getFileLink(staff.profilePic) : (API.getURL() + '/images/profile.png');
@ -252,30 +158,31 @@ class AdminPanelStats extends React.Component {
});
}
retrieveStats() {
const { rawForm } = this.state;
const { startDate, endDate } = rawForm.dateRange;
API.call({
path: '/system/get-stats',
data: {
dateRange: "[" + startDate.toString() + "," + endDate.toString() + "]",
departments: "[" + rawForm.departments.map(department => department.id) + "]",
owners: "[" + rawForm.owners.map(owner => owner.id) + "]",
tags: "[" + this.getSelectedTagIds() + "]"
}
}).then(({data}) => {
this.setState({ticketData: data, loading: false});
}).catch((error) => {
if (showLogs) console.error('ERROR: ', error);
})
}
onFormChange(newFormState) {
this.setState({rawForm: newFormState});
}
onFormSubmit() {
this.retrieveStats();
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
}
};
}
}

View File

@ -27,7 +27,9 @@ class AdminPanelAdvancedSettings extends React.Component {
messageContent: '',
selectedAPIKey: -1,
APIKeys: [],
error: ''
error: '',
showMessage: true,
showAPIKeyMessage: true
};
componentDidMount() {
@ -36,7 +38,7 @@ class AdminPanelAdvancedSettings extends React.Component {
render() {
const { config } = this.props;
const { messageType, error, selectedAPIKey } = this.state;
const { messageType, error, selectedAPIKey, showAPIKeyMessage } = this.state;
return (
<div className="admin-panel-advanced-settings">
@ -88,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">
{error ? <Message type="error">{i18n(error)}</Message> : ((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>
@ -102,11 +113,16 @@ class AdminPanelAdvancedSettings extends React.Component {
}
renderMessage() {
const { messageType, messageTitle, messageContent } = this.state;
const { messageType, messageTitle, messageContent, showMessage } = this.state;
return (
<Message className="admin-panel-advanced-settings__message" type={messageType} title={messageTitle}>
{messageContent}
<Message
showMessage={showMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showMessage")}
className="admin-panel-advanced-settings__message"
type={messageType}
title={messageTitle}>
{messageContent}
</Message>
);
}
@ -154,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 {
@ -270,10 +286,11 @@ 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) {
@ -287,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) {
@ -303,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() {
@ -347,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

@ -65,12 +65,12 @@ class AdminPanelCustomTagsModal extends React.Component {
<FormField name="name" label={i18n('NAME')} fieldProps={{size: 'large'}} required={nameRequired} />
<FormField name="color" label={i18n('COLOR')} decorator={ColorSelector} />
<div className='admin-panel-custom-tags-modal__actions'>
<Button onClick={this.onDiscardClick.bind(this)} size="small">
{i18n('CANCEL')}
</Button>
<SubmitButton type="secondary" size="small">
{i18n('SAVE')}
</SubmitButton>
<Button onClick={this.onDiscardClick.bind(this)} size="small">
{i18n('CANCEL')}
</Button>
</div>
</Form>
</div>

View File

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

View File

@ -122,7 +122,7 @@ class AdminPanelEmailSettings extends React.Component {
<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')}
infoMessage={i18n('IMAGE_HEADER_DESCRIPTION')}
fieldProps={{size: 'large'}} />
<SubmitButton className="admin-panel-email-settings__image-header-submit" type="secondary"
size="small">{i18n('SAVE')}</SubmitButton>
@ -169,7 +169,7 @@ class AdminPanelEmailSettings extends React.Component {
</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>
@ -179,39 +179,61 @@ class AdminPanelEmailSettings extends React.Component {
}
renderForm() {
const {
form,
language,
selectedIndex,
edited
} = this.state;
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={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'}} />
{(form.text2) ?
<FormField key="text2" label={i18n('TEXT') + '2'} name="text2" validation="TEXT_AREA" required
decorator={'textarea'}
fieldProps={{className: 'admin-panel-email-settings__text-area'}} /> : null}
{(form.text3) ?
<FormField key="text3" label={i18n('TEXT') + '3'} name="text3" validation="TEXT_AREA" required
decorator={'textarea'}
fieldProps={{className: 'admin-panel-email-settings__text-area'}} /> : null}
<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__optional-buttons">
@ -223,11 +245,7 @@ class AdminPanelEmailSettings extends React.Component {
{edited ? this.renderDiscardButton() : null}
</div>
<div className="admin-panel-email-settings__save-button">
<SubmitButton
key="submit-email-template"
type="secondary"
size="small"
onClick={(e) => {e.preventDefault(); this.onFormSubmit(form);}}>
<SubmitButton key="submit-email-template" type="secondary" size="small">
{i18n('SAVE')}
</SubmitButton>
</div>
@ -257,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

@ -26,7 +26,8 @@ class AdminPanelSystemPreferences extends React.Component {
message: null,
values: {
maintenance: false,
}
},
showMessage: true
};
componentDidMount() {
@ -113,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;
}
@ -126,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
});
}
@ -154,14 +174,15 @@ class AdminPanelSystemPreferences extends React.Component {
'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
});
}
@ -201,7 +222,8 @@ class AdminPanelSystemPreferences extends React.Component {
onRecoverSettingsFail() {
this.setState({
message: 'error'
message: 'error',
showMessage: true
});
}
@ -210,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

@ -43,6 +43,9 @@ class AdminPanelDepartments extends React.Component {
editedAddDepartmentForm: false,
editedDefaultDepartmentForm: false,
errorMessage: null,
showErrorMessage: true,
showSuccessMessage: true,
showDefaultDepartmentErrorMessage: true,
errors: {},
defaultDepartmentError: null,
form: {
@ -55,7 +58,7 @@ class AdminPanelDepartments extends React.Component {
};
render() {
const { errorMessage, formLoading, selectedIndex } = this.state;
const { errorMessage, formLoading, selectedIndex, showErrorMessage } = this.state;
return (
<div className="admin-panel-departments">
@ -65,7 +68,13 @@ class AdminPanelDepartments extends React.Component {
<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'}} />
@ -98,15 +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" >
@ -234,6 +247,12 @@ class AdminPanelDepartments extends React.Component {
};
}
onCloseMessage(showMessage) {
this.setState({
[showMessage]: false
});
}
onItemChange(index) {
if(this.state.editedAddDepartmentForm) {
AreYouSure.openModal(i18n('WILL_LOSE_CHANGES'), this.updateForm.bind(this, index));
@ -255,8 +274,8 @@ class AdminPanelDepartments extends React.Component {
}
}).then(() => {
this.retrieveDepartments(true);
this.setState({formLoading: false, errorMessage: false, defaultDepartmentError: false});
}).catch(result => this.setState({formLoading: false, defaultDepartmentError: result.message}));
this.setState({formLoading: false, errorMessage: false, defaultDepartmentError: false, showSuccessMessage: true});
}).catch(result => this.setState({formLoading: false, defaultDepartmentError: result.message, showDefaultDepartmentErrorMessage: true}));
}
onFormSubmit(form) {
@ -273,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',
@ -285,9 +304,9 @@ class AdminPanelDepartments extends React.Component {
this.setState({formLoading: false,errorMessage: false, defaultDepartmentError: null});
this.retrieveDepartments();
this.onItemChange(-1);
}).catch(() => {
}).catch(result => {
this.onItemChange.bind(this, -1);
this.setState({formLoading: false, defaultDepartmentError: null});
this.setState({formLoading: false, errorMessage: result.message, showErrorMessage: true, defaultDepartmentError: null});
});
}
}
@ -316,7 +335,7 @@ 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) {

View File

@ -50,7 +50,7 @@ class AdminPanelStaffMembers extends React.Component {
<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

@ -10,7 +10,6 @@ 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';
@ -19,6 +18,7 @@ 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,
@ -52,30 +52,41 @@ class StaffEditor extends React.Component {
department: undefined,
departments: this.getUserDepartments(),
closedTicketsShown: false,
sendEmailOnNewTicket: this.props.sendEmailOnNewTicket
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() {
const {
name,
level,
profilePic,
myAccount,
staffId,
staffList,
userId
} = this.props;
const {
message,
tickets,
loadingPicture,
email
} = this.state;
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 (
@ -144,6 +155,8 @@ class StaffEditor extends React.Component {
<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>
@ -154,9 +167,66 @@ class StaffEditor extends React.Component {
);
}
renderReInviteStaffButton () {
const inviteStaffButtonContent = <div><Icon name="user-plus" /> {i18n('INVITE_STAFF')}</div>;
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 } = this.state;
let messageType = (message === 'FAIL') ? 'error' : 'success';
const { message, showMessage } = this.state;
const messageType = (message === 'FAIL') ? 'error' : 'success';
let _message = null;
switch (message) {
@ -180,7 +250,15 @@ class StaffEditor extends React.Component {
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() {
@ -221,7 +299,7 @@ class StaffEditor extends React.Component {
renderDepartmentsInfo() {
const { departments } = this.state;
const departmentsAssigned = this.getDepartments().filter((_department, index) => departments.includes(index))
const departmentsAssigned = this.getDepartments().filter((_department, index) => departments.includes(index));
return (
<Form values={{departments: Array.from({length: departmentsAssigned.length}, (value, index) => index)}}>
@ -231,11 +309,17 @@ class StaffEditor extends React.Component {
}
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() {
@ -244,7 +328,10 @@ class StaffEditor extends React.Component {
<span className="separator" />
<div className="staff-editor__tickets">
<div className="staff-editor__tickets-title">{i18n('TICKETS_ASSIGNED')}</div>
<TicketList {...this.getTicketListProps()} />
{this.state.ticketListLoading ?
<Loading className="staff-editor__ticketlist-loading" backgrounded size="large"/> :
<TicketList {...this.getTicketListProps()} />
}
</div>
</div>
);
@ -276,16 +363,8 @@ class StaffEditor extends React.Component {
}
getTicketListProps() {
const {
staffId,
departments
} = this.props;
const {
tickets,
page,
pages,
closedTicketsShown
} = this.state;
const { staffId, departments } = this.props;
const { tickets, page, pages, closedTicketsShown } = this.state;
return {
type: 'secondary',
@ -311,6 +390,7 @@ class StaffEditor extends React.Component {
departmentIndexes.push(index);
}
});
return departmentIndexes;
}
@ -344,11 +424,12 @@ class StaffEditor extends React.Component {
}
onSubmit(eventType, form) {
const {
myAccount,
staffId,
onChange
} = this.props;
this.setState({
loadingStats: true,
ticketListLoading: true
});
const { myAccount, staffId, onChange } = this.props;
let departments;
if(form.departments) {
@ -369,21 +450,33 @@ class StaffEditor extends React.Component {
}
}).then(() => {
this.retrieveStaffMembers();
window.scrollTo(0,0);
this.setState({message: eventType});
window.scrollTo(0,250);
this.setState({
message: eventType,
showMessage: true,
ticketListLoading: false
});
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;
const { staffId, onDelete } = this.props;
return API.call({
path: '/staff/delete',
data: {
@ -391,16 +484,13 @@ class StaffEditor extends React.Component {
}
}).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;
const { myAcount, staffId, onChange } = this.props;
this.setState({
loadingPicture: true
});
@ -417,10 +507,11 @@ class StaffEditor extends React.Component {
loadingPicture: false
});
this.retrieveStaffMembers();
onChange && onChange();
}).catch(() => {
window.scrollTo(0,0);
this.setState({message: 'FAIL', loadingPicture: false});
this.setState({message: 'FAIL', loadingPicture: false, showMessage: true});
});
}
@ -466,10 +557,7 @@ class StaffEditor extends React.Component {
}
onClosedTicketsShownChange() {
const {
department,
closedTicketsShown
} = this.state;
const { department, closedTicketsShown } = this.state;
const newClosedValue = !closedTicketsShown;
this.setState({
@ -492,6 +580,12 @@ class StaffEditor extends React.Component {
department: newDepartmentFilter ? `[${newDepartmentFilter}]` : undefined
}
}
onCloseMessage(showMessage) {
this.setState({
[showMessage]: false
});
}
}
export default connect((store) => {

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

@ -41,7 +41,8 @@ class AdminPanelCustomResponses extends React.Component {
title: '',
content: TextEditor.createEmpty(),
language: this.props.language
}
},
showErrorMessage: true
};
componentDidMount() {
@ -103,8 +104,16 @@ class AdminPanelCustomResponses extends React.Component {
);
}
renderErrorMessage() {
const { showErrorMessage, error } = this.state;
return(
<Message className="admin-panel-custom-responses__message" type="error">{i18n(this.state.error)}</Message>
<Message
showMessage={showErrorMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showErrorMessage")}
className="admin-panel-custom-responses__message"
type="error">
{i18n(error)}
</Message>
)
}
renderOptionalButtons() {
@ -202,7 +211,7 @@ class AdminPanelCustomResponses extends React.Component {
this.onItemChange(-1);
}).catch((e) => {
this.onItemChange.bind(this, -1)
this.setState({error: e.message, formLoading:false});
this.setState({error: e.message, formLoading:false, showErrorMessage: true});
});
}
}
@ -267,6 +276,12 @@ class AdminPanelCustomResponses extends React.Component {
)
);
}
onCloseMessage(showMessage) {
this.setState({
[showMessage]: false
});
}
}
export default connect((store) => {

View File

@ -26,17 +26,18 @@ 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')}
@ -68,10 +69,14 @@ class AdminPanelMyTickets extends React.Component {
onClosedTicketsShownChange: this.onClosedTicketsShownChange.bind(this),
pages,
page,
onPageChange: event => this.retrieveMyTickets(event.target.value),
onPageChange: event => this.retrieveMyTickets({page: event.target.value}),
onDepartmentChange: departmentId => {
this.setState({departmentId});
this.retrieveMyTickets(1, closedTicketsShown, departmentId);
this.setState({departmentId})
this.retrieveMyTickets({page: 1, departmentId});
},
onPageSizeChange: pageSize => {
this.setState({pageSize});
this.retrieveMyTickets({page: 1, pageSize});
},
};
}
@ -81,7 +86,7 @@ class AdminPanelMyTickets extends React.Component {
return {
closedTicketsShown: !state.closedTicketsShown
};
}, () => this.retrieveMyTickets());
}, () => this.retrieveMyTickets({}));
}
onCreateTicket() {
@ -100,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

@ -5,6 +5,8 @@
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,7 +38,8 @@ export function updateSearchTicketsFromURL() {
...store.getState().searchFilters.ticketQueryListState,
page: (currentSearchParams.page || INITIAL_PAGE)*1
},
searchTicketsUtils.prepareFiltersForAPI(listConfig.filters)
searchTicketsUtils.getFiltersForAPI(listConfig.filters),
currentSearchParams.pageSize
));
});
}
@ -49,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">
@ -67,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,16 +21,16 @@ class AdminPanelCustomFieldForm extends React.Component {
state = {
loading: false,
error: null,
addForm: {},
addForm: {
name: "",
description: ""
},
addFormOptions: [],
showErrorMessage: true
};
render() {
const {
loading,
addForm,
error
} = this.state;
const { loading, addForm, error } = this.state;
return (
<div className="admin-panel-custom-field-form">
@ -41,7 +41,8 @@ class AdminPanelCustomFieldForm extends React.Component {
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/>
@ -58,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>
);
}
@ -84,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("");
@ -127,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

@ -15,39 +15,53 @@ 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)} />
{
(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">
@ -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,44 +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) {
const {
orderBy,
desc,
search
} = this.state;
this.retrieveUsers({
const newUsersParams = {
...this.state.usersParams,
page: event.target.value,
orderBy,
desc,
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
});
}
@ -184,21 +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>
<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
});
@ -210,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

@ -31,7 +31,8 @@ class AdminPanelViewUser extends React.Component {
loading: true,
disabled: false,
userList: [],
message: ''
message: '',
showMessage: true
};
componentDidMount() {
@ -51,7 +52,7 @@ 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>
);
}
@ -92,7 +93,10 @@ 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)}
@ -111,7 +115,10 @@ class AdminPanelViewUser extends React.Component {
</div>
<span className="separator" />
<div className="admin-panel-view-user__tickets">
<div className="admin-panel-view-user__tickets-title">{i18n('TICKETS')}</div>
<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>
@ -119,22 +126,32 @@ class AdminPanelViewUser extends React.Component {
}
renderSupervisedUserMessage(){
const { message } = this.state;
const { message, showMessage } = this.state;
if(message) {
if(message != 'success') {
if(message !== 'success') {
return (
<div className="admin-panel-view-user__supervised-users-message">
<Message type="error">{i18n(message)}</Message>
</div>
<Message
showMessage={showMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showMessage")}
className="admin-panel-view-user__supervised-users-message"
type="error">
{i18n(message)}
</Message>
);
} else {
return (
<div className= "admin-panel-view-user__supervised-users-message">
<Message type="success">{i18n('SUPERVISED_USERS_UPDATED')}</Message>
</div>
<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;
}
}
@ -156,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
})
});
}
@ -368,6 +387,12 @@ class AdminPanelViewUser extends React.Component {
});
});
}
onCloseMessage(showMessage) {
this.setState({
[showMessage]: false
});
}
}
export default connect((store) => {

View File

@ -30,10 +30,19 @@
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;
@ -64,4 +73,7 @@
margin-left: 15px;
}
&__info-tooltip{
margin-left: 10px;
}
}

View File

@ -14,12 +14,14 @@ 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) {
@ -28,7 +30,8 @@ class InviteUserWidget extends React.Component {
this.state = {
loading: false,
email: null,
customFields: null
customFields: null,
showMessage: true
};
}
@ -36,32 +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() {
if(!this.state.customFields) return null;
if(!this.state.customFields) return this.renderLoading();
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>
<div className="invite-user-widget__buttons-container">
<Button onClick={(e) => {e.preventDefault(); ModalContainer.closeModal();}} type="link">{i18n('CANCEL')}</Button>
<SubmitButton type="secondary">{i18n('INVITE_USER')}</SubmitButton>
</div>
</Form>
{this.renderMessage()}
</Widget>
<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>
);
}
@ -91,11 +107,29 @@ class InviteUserWidget extends React.Component {
}
renderMessage() {
switch (this.state.message) {
case 'success':
return <Message className="invite-user-widget__success-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 className="invite-user-widget__error-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;
}
@ -153,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

@ -18,7 +18,7 @@
&__buttons-container {
display: flex;
flex-direction: row;
flex-direction: row-reverse;
justify-content: space-between;
align-items: center;
}
@ -27,4 +27,18 @@
&__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

@ -19,7 +19,7 @@ class InstallCompleted extends React.Component {
render() {
return (
<div className="install-completed">
<Message title={i18n('INSTALLATION_COMPLETED_TITLE')} type="success">
<Message showCloseButton={false} title={i18n('INSTALLATION_COMPLETED_TITLE')} type="success">
{i18n('INSTALLATION_COMPLETED_DESCRIPTION')}
</Message>
</div>

View File

@ -17,6 +17,7 @@ class InstallStep3Database extends React.Component {
state = {
loading: false,
error: false,
showErrorMessage: true,
errorMessage: ''
};
@ -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

@ -20,6 +20,7 @@ class InstallStep5Settings extends React.Component {
loading: false,
form: {},
error: false,
showErrorMessage: true,
errorMessage: ''
};
@ -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,6 +15,7 @@ class InstallStep6Admin extends React.Component {
state = {
loading: false,
error: false,
showErrorMessage: true,
errorMessage: ''
};
@ -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) {
@ -61,10 +64,17 @@ class InstallStep6Admin extends React.Component {
.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

@ -17,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 = {
@ -34,23 +40,16 @@ class CreateTicketForm extends React.Component {
loading: false,
message: null,
form: {
title: '',
...DEFAULT_CREATE_TICKET_FORM_VALUE,
content: TextEditor.createEmpty(),
departmentIndex: getPublicDepartmentIndexFromDepartmentId(this.props.defaultDepartmentId, SessionStore.getDepartments()),
email: '',
name: '',
language: this.props.language
}
},
showMessage: true
};
render() {
const {
userLogged,
isDefaultDepartmentLocked,
isStaff,
onlyOneSupportedLanguage,
allowAttachments
} = this.props;
const { userLogged, isDefaultDepartmentLocked, isStaff, onlyOneSupportedLanguage, allowAttachments } = this.props;
return (
<div className="create-ticket-form">
@ -79,10 +78,14 @@ class CreateTicketForm extends React.Component {
fieldProps={{allowImages: allowAttachments}}
required
field="textarea" />
<div className="create-ticket-form__buttons-container">
{allowAttachments ? this.renderFileUpload() : null}
{(!userLogged) ? this.renderCaptcha() : null}
<SubmitButton type="secondary">{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()}
@ -116,21 +119,38 @@ class CreateTicketForm extends React.Component {
}
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;
const { loading, form } = this.state;
return {
loading,
@ -161,29 +181,48 @@ class CreateTicketForm extends React.Component {
}
}
onTicketSuccess(email, result) {
const { onSuccess } = this.props;
onTicketSuccess() {
const { onSuccess, userLogged, language } = this.props;
const { form } = this.state;
const message = 'success';
this.setState(
{
loading: false,
message
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(result, email, message);}
() => {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,11 +8,29 @@
margin-top: 20px;
}
&__container {
display: flex;
flex-direction: row-reverse;
}
&__buttons-container {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
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 {

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

View File

@ -7,4 +7,18 @@
float: none;
}
}
}
&__container {
display: flex;
flex-direction: column;
justify-content: center;
}
&__create-ticket-form__hidden {
display: none;
}
&__message {
margin-bottom: 30px;
}
}

View File

@ -33,6 +33,8 @@ class DashboardEditProfilePage extends React.Component {
customFields: [],
customFieldsFrom: {},
loadingCustomFields: false,
showChangeEmailMessage: true,
showChangePasswordMessage: true
};
componentDidMount() {
@ -115,12 +117,7 @@ class DashboardEditProfilePage extends React.Component {
}
renderCustomField(customField, key) {
const {
type,
name,
description,
options
} = customField;
const { type, name, description, options } = customField;
if(type === 'text') {
return (
@ -140,22 +137,58 @@ class DashboardEditProfilePage extends React.Component {
}
renderMessageEmail() {
switch (this.state.messageEmail) {
const { messageEmail, showChangeEmailMessage } = this.state;
switch (messageEmail) {
case 'success':
return <Message className="edit-profile-page__message" type="success">{i18n('EMAIL_CHANGED')}</Message>;
return (
<Message
showMessage={showChangeEmailMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showChangeEmailMessage")}
className="edit-profile-page__message"
type="success">
{i18n('EMAIL_CHANGED')}
</Message>
);
case 'fail':
return <Message className="edit-profile-page__message" type="error">{i18n('EMAIL_EXISTS')}</Message>;
return (
<Message
showMessage={showChangeEmailMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showChangeEmailMessage")}
className="edit-profile-page__message"
type="error">
{i18n('EMAIL_EXISTS')}
</Message>
);
default:
return null;
}
}
renderMessagePass() {
switch (this.state.messagePass) {
const { messagePass, showChangePasswordMessage } = this.state;
switch (messagePass) {
case 'success':
return <Message className="edit-profile-page__message" type="success">{i18n('PASSWORD_CHANGED')}</Message>;
return (
<Message
showMessage={showChangePasswordMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showChangePasswordMessage")}
className="edit-profile-page__message"
type="success">
{i18n('PASSWORD_CHANGED')}
</Message>
);
case 'fail':
return <Message className="edit-profile-page__message" type="error">{i18n('OLD_PASSWORD_INCORRECT')}</Message>;
return (
<Message
showMessage={showChangePasswordMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showChangePasswordMessage")}
className="edit-profile-page__message"
type="error">
{i18n('OLD_PASSWORD_INCORRECT')}
</Message>
);
default:
return null;
}
@ -213,12 +246,14 @@ class DashboardEditProfilePage extends React.Component {
}).then(function () {
this.setState({
loadingEmail: false,
messageEmail: "success"
messageEmail: "success",
showChangeEmailMessage: true
});
}.bind(this)).catch(function (){
this.setState({
loadingEmail: false,
messageEmail: 'fail'
messageEmail: 'fail',
showChangeEmailMessage: true
})
}.bind(this));
}
@ -237,12 +272,14 @@ class DashboardEditProfilePage extends React.Component {
}).then(function () {
this.setState({
loadingPass: false,
messagePass: "success"
messagePass: "success",
showChangePasswordMessage: true
});
}.bind(this)).catch(function (){
this.setState({
loadingPass: false,
messagePass: 'fail'
messagePass: 'fail',
showChangePasswordMessage: true
})
}.bind(this));
}
@ -277,6 +314,12 @@ class DashboardEditProfilePage extends React.Component {
});
});
}
onCloseMessage(showMessage) {
this.setState({
[showMessage]: false
});
}
}
export default connect((store) => {

View File

@ -33,7 +33,8 @@ class DashboardListTicketsPage extends React.Component {
ownTickets: true
},
message: '',
loading: false
loading: false,
showMessage: true
};
componentDidMount() {
@ -42,16 +43,8 @@ class DashboardListTicketsPage extends React.Component {
}
render() {
const {
userUsers
} = this.props;
const {
loading,
page,
pages,
tickets,
message
} = this.state;
const { userUsers } = this.props;
const { loading, page, pages, tickets, message, showMessage } = this.state;
return (
<div className="dashboard-ticket-list">
@ -63,8 +56,18 @@ class DashboardListTicketsPage extends React.Component {
page={page}
pages={pages}
tickets={tickets}
showPageSizeDropdown={false}
type={userUsers.length ? "secondary" : "primary"} />
{message ? <Message type="error" >{i18n(message)}</Message> : null}
{
message ?
<Message
showMessage={showMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showMessage")}
type="error">
{i18n(message)}
</Message> :
null
}
</div>
);
}
@ -130,11 +133,18 @@ class DashboardListTicketsPage extends React.Component {
this.setState({
tickets: [],
message: r.message,
showMessage: true,
loading: false
})
});
}
onCloseMessage(showMessage) {
this.setState({
[showMessage]: false
});
}
}

View File

@ -16,6 +16,7 @@ class DashboardTicketPage extends React.Component {
state = {
error: null,
ticket: null,
showErrorMessage: true
};
componentDidMount() {
@ -23,7 +24,7 @@ class DashboardTicketPage extends React.Component {
}
render() {
const {ticket, error} = this.state;
const { ticket, error } = this.state;
return (
<div className="dashboard-ticket-page">
@ -33,11 +34,11 @@ class DashboardTicketPage extends React.Component {
}
renderContent() {
const {ticket, error} = this.state;
const { ticket, error, showErrorMessage } = this.state;
if(error) {
return (
<Message type="error">
<Message showMessage={showErrorMessage} onCloseMessage={this.onCloseMessage.bind(this, "showErrorMessage")} type="error">
{i18n(error)}
</Message>
);
@ -71,12 +72,18 @@ class DashboardTicketPage extends React.Component {
});
}
})
.catch(result => this.setState({error: result.message}));
.catch(result => this.setState({error: result.message, showErrorMessage: true}));
}
retrieveUserData() {
store.dispatch(SessionActions.getUserData());
}
onCloseMessage(showMessage) {
this.setState({
[showMessage]: false
});
}
}
export default DashboardTicketPage;

View File

@ -17,6 +17,14 @@ import FormField from 'core-components/form-field';
import Widget from 'core-components/widget';
import WidgetTransition from 'core-components/widget-transition';
import Message from 'core-components/message';
import Loading from 'core-components/loading';
import Captcha from 'app/main/captcha';
const UNVERIFIED_USER_STEP = 0;
const LOADING_STEP = 1;
const REQUEST_RESULT_STEP = 2;
const MAX_FREE_LOGIN_ATTEMPTS = 3;
class MainHomePageLoginWidget extends React.Component {
@ -26,11 +34,17 @@ class MainHomePageLoginWidget extends React.Component {
recoverFormErrors: {},
recoverSent: false,
loadingLogin: false,
loadingRecover: false
loadingRecover: false,
reSendEMailVerificationLoading: false,
reSendEmailVerificationStep: UNVERIFIED_USER_STEP,
reSendEmailVerificationMessage: "",
showRecoverSentMessage: true,
showReSendEmailVerificationMessage: true
};
componentDidUpdate(prevProps) {
if (!prevProps.session.failed && this.props.session.failed) {
this.setState({showReSendEmailVerificationMessage : true});
this.refs.loginForm.refs.password.focus();
}
}
@ -53,17 +67,70 @@ class MainHomePageLoginWidget extends React.Component {
<FormField placeholder={i18n('PASSWORD_LOWERCASE')} name="password" className="login-widget__input" required fieldProps={{password: true}}/>
<FormField name="remember" label={i18n('REMEMBER_ME')} className="login-widget__input" field="checkbox"/>
</div>
{this.props.session.loginAttempts > MAX_FREE_LOGIN_ATTEMPTS ? this.renderLoginCaptcha() : null}
<div className="login-widget__submit-button">
<SubmitButton type="primary">{i18n('LOG_IN')}</SubmitButton>
</div>
</Form>
<Button className="login-widget__forgot-password" type="link" onClick={this.onForgotPasswordClick.bind(this)} onMouseDown={(event) => {event.preventDefault()}}>
{i18n('FORGOT_PASSWORD')}
</Button>
<div className="main-home-page__link-buttons-container">
<Button className="login-widget__forgot-password" type="link" onClick={this.onForgotPasswordClick.bind(this)} onMouseDown={(event) => {event.preventDefault()}}>
{i18n('FORGOT_PASSWORD')}
</Button>
{this.renderReSendEmailVerificationSection()}
</div>
</Widget>
);
}
renderLoginCaptcha() {
return(
<div className={`main-home-page__${this.props.sitekey ? "captcha" : "no-captcha"}`}>
<Captcha ref="captcha" />
</div>
)
}
renderReSendEmailVerificationSection() {
const { reSendEmailVerificationMessage,reSendEmailVerificationStep, showReSendEmailVerificationMessage } = this.state;
if(this.props.session.failMessage === 'UNVERIFIED_USER') {
switch (reSendEmailVerificationStep) {
case UNVERIFIED_USER_STEP:
return (
<Button className="login-widget__resend-verification-token" type="link" onClick={this.onReSendEmailVerificationClick.bind(this)}>
{i18n('RESEND_EMAIL_VERIFICATION')}
</Button>
);
case LOADING_STEP:
return <Loading className="login-widget__loading" />;
case REQUEST_RESULT_STEP:
return (
(reSendEmailVerificationMessage === "success") ?
<Message
showMessage={showReSendEmailVerificationMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showReSendEmailVerificationMessage")}
className="login-widget__resend-email-verification-success"
type="success"
leftAligned>
{i18n('RESEND_EMAIL_VERIFICATION_SUCCESS')}
</Message> :
<Message
showMessage={showReSendEmailVerificationMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showReSendEmailVerificationMessage")}
className="login-widget__resend-email-verification-fail"
type="error"
leftAligned>
{i18n('RESEND_EMAIL_VERIFICATION_FAIL')}
</Message>
);
}
} else {
return null;
}
}
renderPasswordRecovery() {
return (
<PasswordRecovery ref="passwordRecovery" recoverSent={this.state.recoverSent} formProps={this.getRecoverFormProps()} onBackToLoginClick={this.onBackToLoginClick.bind(this)}/>
@ -71,17 +138,20 @@ class MainHomePageLoginWidget extends React.Component {
}
renderRecoverStatus() {
let status = null;
const { recoverSent, showRecoverSentMessage} = this.state;
if (this.state.recoverSent) {
status = (
<Message className="login-widget__message" type="info" leftAligned>
{i18n('RECOVER_SENT')}
</Message>
);
}
return status;
return (
recoverSent ?
<Message
showMessage={showRecoverSentMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showRecoverSentMessage")}
className="login-widget__message"
type="info"
leftAligned>
{i18n('RECOVER_SENT')}
</Message> :
null
);
}
getLoginFormProps() {
@ -108,7 +178,6 @@ class MainHomePageLoginWidget extends React.Component {
getLoginFormErrors() {
let errors = _.extend({}, this.state.loginFormErrors);
if (this.props.session.failed) {
if (this.props.session.failMessage === 'INVALID_CREDENTIALS') {
errors.password = i18n('ERROR_PASSWORD');
@ -123,6 +192,10 @@ class MainHomePageLoginWidget extends React.Component {
}
onLoginFormSubmit(formState) {
this.setState({
reSendEmailVerificationStep: UNVERIFIED_USER_STEP,
email: formState.email
})
this.props.dispatch(SessionActions.login(formState));
}
@ -166,7 +239,8 @@ class MainHomePageLoginWidget extends React.Component {
onRecoverPasswordSent() {
this.setState({
loadingRecover: false,
recoverSent: true
recoverSent: true,
showRecoverSentMessage: true
});
}
@ -178,11 +252,41 @@ class MainHomePageLoginWidget extends React.Component {
}
}, () => this.refs.passwordRecovery.focusEmail());
}
onReSendEmailVerificationClick() {
this.setState({
reSendEmailVerificationStep: LOADING_STEP,
})
API.call({
path: '/user/resend-email-token',
data: {
email: this.state.email
}
}).then(() => {
this.setState({
reSendEmailVerificationStep: REQUEST_RESULT_STEP,
reSendEmailVerificationMessage: 'success'
})
}).catch(() => {
this.setState({
reSendEmailVerificationStep: REQUEST_RESULT_STEP,
reSendEmailVerificationMessage: 'error'
})
});
}
onCloseMessage(showMessage) {
this.setState({
[showMessage]: false
});
}
}
export default connect((store) => {
return {
session: store.session
session: store.session,
sitekey: store.config.reCaptchaKey
};
})(MainHomePageLoginWidget);
})(MainHomePageLoginWidget);

View File

@ -18,6 +18,24 @@
&__message {
margin-top: 18px;
}
&__loading > span{
border-top: 1.1em solid rgba($dark-grey, 0.2);
border-right: 1.1em solid rgba($dark-grey, 0.2);
border-bottom: 1.1em solid rgba($dark-grey, 0.2);
border-left: 1.1em solid $dark-grey;
margin-top: 10px;
}
}
.main-home-page__link-buttons-container {
display: flex;
flex-direction: column;
margin: auto;
}
.main-home-page__widget {
min-height: 391px;
}
@media screen and (min-width: 379px) {

View File

@ -7,30 +7,45 @@ import Widget from 'core-components/widget';
import Card from 'core-components/card';
import i18n from 'lib-app/i18n';
import Header from 'core-components/header';
import queryString from 'query-string';
import Message from 'core-components/message';
class MainHomePagePortal extends React.Component {
static propTypes = {
type: React.PropTypes.oneOf(['default', 'complete'])
};
state = {
showMessage: !!queryString.parse(window.location.search)["message"]
};
render() {
return (
<Widget className={classNames('main-home-page-portal', this.props.className)}>
<div className="main-home-page-portal__title">
<Header title={this.props.title || i18n('SUPPORT_CENTER')} description={i18n('SUPPORT_CENTER_DESCRIPTION')} />
</div>
<div className="main-home-page-portal__cards">
<div className="main-home-page-portal__card col-md-4">
<Card {...this.getTicketsCardProps()}/>
<div className="main-home-page-portal__message">
<Message
showMessage={this.state.showMessage}
onCloseMessage={this.onCloseMessage.bind(this)}
className="dashboard-create-ticket-page__message"
type="success">
{i18n('TICKET_NUMBER_SENT')}
</Message>
<Widget className={classNames('main-home-page-portal', this.props.className)}>
<div className="main-home-page-portal__title">
<Header title={this.props.title || i18n('SUPPORT_CENTER')} description={i18n('SUPPORT_CENTER_DESCRIPTION')} />
</div>
<div className="main-home-page-portal__card col-md-4">
<Card {...((this.props.type === 'complete') ? this.getViewTicketCardProps() : this.getAccountCardProps())} />
<div className="main-home-page-portal__cards">
<div className="main-home-page-portal__card col-md-4">
<Card {...this.getTicketsCardProps()}/>
</div>
<div className="main-home-page-portal__card col-md-4">
<Card {...((this.props.type === 'complete') ? this.getViewTicketCardProps() : this.getAccountCardProps())} />
</div>
<div className="main-home-page-portal__card col-md-4">
<Card {...this.getArticlesCardProps()} />
</div>
</div>
<div className="main-home-page-portal__card col-md-4">
<Card {...this.getArticlesCardProps()} />
</div>
</div>
</Widget>
</Widget>
</div>
);
}
@ -75,6 +90,12 @@ class MainHomePagePortal extends React.Component {
onButtonClick: () => history.push('/check-ticket')
};
}
onCloseMessage() {
this.setState({
showMessage: false
});
}
}
export default connect((store) => {

View File

@ -9,12 +9,19 @@ import MainHomePagePortal from 'app/main/main-home/main-home-page-portal';
import Message from 'core-components/message';
class MainHomePage extends React.Component {
state = {
showMessage: true
}
componentDidUpdate(prevProps) {
if (!prevProps.session && this.props.session) {
this.setState({showMessage : true});
}
}
render() {
const {
config,
loginForm
} = this.props;
const { config, loginForm } = this.props;
return (
<div className="main-home-page row">
{this.renderMessage()}
@ -50,16 +57,26 @@ class MainHomePage extends React.Component {
renderSuccess() {
return (
<Message title={i18n('VERIFY_SUCCESS')} type="success" className="main-home-page__message">
{i18n('VERIFY_SUCCESS_DESCRIPTION')}
<Message
showMessage={this.state.showMessage}
onCloseMessage={this.onCloseMessage.bind(this)}
title={i18n('VERIFY_SUCCESS')}
type="success"
className="main-home-page__message">
{i18n('VERIFY_SUCCESS_DESCRIPTION')}
</Message>
);
}
renderFailed() {
return (
<Message title={i18n('VERIFY_FAILED')} type="error" className="main-home-page__message">
{i18n('VERIFY_FAILED_DESCRIPTION')}
<Message
showMessage={this.state.showMessage}
onCloseMessage={this.onCloseMessage.bind(this)}
title={i18n('VERIFY_FAILED')}
type="error"
className="main-home-page__message">
{i18n('VERIFY_FAILED_DESCRIPTION')}
</Message>
);
}
@ -85,6 +102,12 @@ class MainHomePage extends React.Component {
return classNames(classes);
}
onCloseMessage() {
this.setState({
showMessage: false
});
}
}
export default connect((store) => {

View File

@ -10,4 +10,10 @@
margin-left: 20px;
margin-right: 20px;
}
&__captcha {
margin: 10px auto 20px;
height: 78px;
width: 304px;
}
}

View File

@ -20,8 +20,6 @@ class MainLayoutFooter extends React.Component {
return (
<div className="main-layout-footer__extra-links">
<a className="main-layout-footer__extra-link" href="http://www.opensupports.com/documentation/" target="_blank">Documentation</a>
<span> | </span>
<a className="main-layout-footer__extra-link" href="http://www.opensupports.com/download/#donation" target="_blank">Donate</a>
</div>
);
}

View File

@ -13,12 +13,24 @@ class MainLayoutHeader extends React.Component {
render() {
return (
<div className="main-layout-header">
{this.renderAccessLinks()}
<LanguageSelector {...this.getLanguageSelectorProps()} />
{this.renderHeaderOptions()}
</div>
);
}
renderHeaderOptions(){
let result = null;
if(!this.props.config['maintenance-mode']){
result = (<div>
{this.renderAccessLinks()}
<LanguageSelector {...this.getLanguageSelectorProps()} />
</div>)
}
return result;
}
renderAccessLinks() {
const {
session,

View File

@ -8,7 +8,7 @@
display: flex;
flex-direction: row;
justify-content: space-between;
min-height: 40px;
&__user-name {
color: $primary-red;
}

View File

@ -8,8 +8,12 @@
border-radius: 4px;
transition: max-height 0.15s ease-out;
.main-layout-header__languages > .drop-down__current-item {
min-height: unset;
}
&--content {
min-height: 400px;
min-height: 453px;
padding: 20px;
@media screen and (min-width: 993px) and (max-width: 1032px) {

View File

@ -23,7 +23,8 @@ class MainRecoverPasswordPage extends React.Component {
this.state = {
recoverStatus: 'waiting',
loading: false
loading: false,
showMessage: true
}
}
@ -47,11 +48,27 @@ class MainRecoverPasswordPage extends React.Component {
}
renderRecoverStatus() {
switch (this.state.recoverStatus) {
const { recoverStatus, showMessage } = this.state;
switch (recoverStatus) {
case 'valid':
return <Message type="success">{i18n('VALID_RECOVER')}</Message>;
return (
<Message
showMessage={showMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showMessage")}
type="success">
{i18n('VALID_RECOVER')}
</Message>
);
case 'invalid':
return <Message type="error">{i18n('INVALID_RECOVER')}</Message>;
return (
<Message
showMessage={showMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showMessage")}
type="error">
{i18n('INVALID_RECOVER')}
</Message>
);
case 'waiting':
return null;
}
@ -78,6 +95,7 @@ class MainRecoverPasswordPage extends React.Component {
setTimeout(() => {history.push((response.data.staff*1) ? '/admin' : '/')}, 2000);
this.setState({
recoverStatus: 'valid',
showMessage: true,
loading: false
});
}
@ -85,9 +103,16 @@ class MainRecoverPasswordPage extends React.Component {
onPasswordRecoverFail() {
this.setState({
recoverStatus: 'invalid',
showMessage: true,
loading: false
});
}
onCloseMessage(showMessage) {
this.setState({
[showMessage]: false
});
}
}
export default MainRecoverPasswordPage;

View File

@ -5,6 +5,7 @@ import classNames from 'classnames';
import i18n from 'lib-app/i18n';
import API from 'lib-app/api-call';
import history from 'lib-app/history';
import SessionStore from 'lib-app/session-store';
import Captcha from 'app/main/captcha';
import SubmitButton from 'core-components/submit-button';
@ -26,16 +27,29 @@ class MainSignUpWidget extends React.Component {
this.state = {
loading: false,
email: null,
customFields: null
customFields: null,
showMessage: true,
message: null
};
}
componentDidMount() {
API.call({
path: '/system/get-custom-fields',
data: {}
})
.then(result => this.setState({customFields: result.data}));
if(!SessionStore.getItem('customFields')) {
API.call({
path: '/system/get-custom-fields',
data: {}
})
.then(result => {
SessionStore.storeCustomField(result.data);
this.setState({
customFields: result.data
});
})
} else {
this.setState({
customFields: SessionStore.getCustomFields()
});
}
}
render() {
@ -89,11 +103,27 @@ class MainSignUpWidget extends React.Component {
}
renderMessage() {
switch (this.state.message) {
const { message, showMessage } = this.state;
switch (message) {
case 'success':
return <Message type="success">{i18n('SIGNUP_SUCCESS')}</Message>;
return (
<Message
showMessage={showMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showMessage")}
type="success">
{i18n('SIGNUP_SUCCESS')}
</Message>
);
case 'fail':
return <Message type="error">{i18n('EMAIL_EXISTS')}</Message>;
return (
<Message
showMessage={showMessage}
onCloseMessage={this.onCloseMessage.bind(this, "showMessage")}
type="error">
{i18n('EMAIL_EXISTS')}
</Message>
);
default:
return null;
}
@ -153,16 +183,24 @@ class MainSignUpWidget extends React.Component {
onSignupSuccess() {
this.setState({
loading: false,
message: 'success'
message: 'success',
showMessage: true
}, () => {
setTimeout(() => {history.push('/check-ticket')}, 2000);
setTimeout(() => {history.push('/')}, 2000);
});
}
onSignupFail() {
this.setState({
loading: false,
message: 'fail'
message: 'fail',
showMessage: true
});
}
onCloseMessage(showMessage) {
this.setState({
[showMessage]: false
});
}
}

View File

@ -1,4 +1,4 @@
opensupports_version = '4.9.0';
opensupports_version = '4.11.0';
root = 'http://localhost:3000';
apiRoot = 'http://localhost:3000/api';
globalIndexPath = '';

View File

@ -10,6 +10,7 @@
background-color: $very-light-grey;
border: 1px solid $grey;
min-height: 38px;
display: block;
&:focus-within {
outline: none;

View File

@ -12,6 +12,10 @@
border-radius: 4px 4px 0 0;
color: $primary-black;
padding: 6px;
min-height: 38px;
display: flex;
align-items: center;
justify-content: center;
&:focus {
outline: none;

View File

@ -39,10 +39,4 @@
user-select: none;
}
}
&_select {
.form-field__label {
padding-bottom: 10px;
}
}
}

View File

@ -39,7 +39,13 @@ class Form extends React.Component {
}
componentDidMount() {
this.setState(this.getInitialFormAndValidations());
this.setState(this.getInitialFormAndValidations(this.props.children));
}
componentDidUpdate(nextProps) {
if(nextProps.values != this.props.values) {
this.setState(this.getInitialFormAndValidations(nextProps.children));
}
}
render() {
@ -135,12 +141,11 @@ class Form extends React.Component {
return newErrors;
}
getInitialFormAndValidations() {
getInitialFormAndValidations(children) {
let form = {};
let validations = {};
reactDFS(this.props.children, (child) => {
reactDFS(children, (child) => {
if (this.isValidField(child)) {
form[child.props.name] = child.props.value || FormField.getDefaultValue(child.props.field);

View File

@ -10,19 +10,24 @@ class Message extends React.Component {
title: React.PropTypes.string,
children: React.PropTypes.node,
leftAligned: React.PropTypes.bool,
onCloseMessage: React.PropTypes.func,
type: React.PropTypes.oneOf(['success', 'error', 'info', 'warning'])
};
static defaultProps = {
type: 'info',
leftAligned: false
leftAligned: false,
showCloseButton: true,
showMessage: true
};
render() {
return (
<Motion {...this.getAnimationProps()}>
{this.renderMessage.bind(this)}
</Motion>
this.props.showMessage ?
<Motion {...this.getAnimationProps()}>
{this.renderMessage.bind(this)}
</Motion> :
null
);
}
@ -38,26 +43,35 @@ class Message extends React.Component {
}
renderMessage(style) {
return this.renderMessageContent(style);
}
renderMessageContent(style) {
const { children, title, showCloseButton } = this.props;
return (
<div className={this.getClass()} style={style} aria-live="assertive">
<Icon className="message__icon" name={this.getIconName()} size={this.getIconSize()} />
<div className="message__title">{this.props.title}</div>
<div className="message__content">{this.props.children}</div>
<div className="message__title">{title}</div>
<div className="message__content">{children}</div>
{showCloseButton ? this.renderCloseButton() : null}
</div>
)
}
getClass() {
const { type, title, leftAligned, className } = this.props
let classes = {
'message': true,
'message_success': (this.props.type === 'success'),
'message_error': (this.props.type === 'error'),
'message_info': (this.props.type === 'info'),
'message_warning': (this.props.type === 'warning'),
'message_with-title': (this.props.title),
'message_left-aligned': (this.props.leftAligned),
'message_success': (type === 'success'),
'message_error': (type === 'error'),
'message_info': (type === 'info'),
'message_warning': (type === 'warning'),
'message_with-title': title,
'message_left-aligned': leftAligned,
[this.props.className]: (this.props.className)
[className]: className
};
return classNames(classes);
@ -77,6 +91,20 @@ class Message extends React.Component {
getIconSize() {
return (this.props.title) ? '2x' : 'lg';
}
renderCloseButton() {
return (
<span className="message__close-icon" onClick={this.onCloseMessage.bind(this)}>
<Icon name="times" size="1x"/>
</span>
);
}
onCloseMessage() {
const { onCloseMessage } = this.props
onCloseMessage && onCloseMessage();
}
}
export default Message;

View File

@ -111,4 +111,11 @@
padding-left: 28px;
}
}
&__close-icon {
cursor: pointer;
position: absolute;
top: 5px;
right: 10px;
}
}

View File

@ -11,6 +11,9 @@
z-index: 1000;
&__content {
display: flex;
align-items: center;
justify-content: center;
position: relative;
margin: auto;
background-color: white;

View File

@ -18,7 +18,7 @@ class TagSelector extends React.Component {
render() {
const items = this.props.items.map(tag => ({...tag, content: this.renderTagOption(tag)}));
const values = items.filter(item => _.includes(this.props.values, item.name));
const values = this.props.values.map((tagName) => items.find(_tag => (_tag.name === tagName)));
return (
<div className="tag-selector">

View File

@ -2,11 +2,12 @@ import React from 'react';
import classNames from 'classnames';
import ReactQuill, { Quill } from 'react-quill';
import ImageResize from 'quill-image-resize-module-react';
import MagicUrl from 'quill-magic-url';
import {isIE} from 'lib-core/navigator';
import Base64ImageParser from 'lib-core/base64-image-parser';
Quill.register('modules/ImageResize', ImageResize);
Quill.register('modules/magicUrl', MagicUrl)
class TextEditor extends React.Component {
static propTypes = {
@ -113,6 +114,7 @@ class TextEditor extends React.Component {
],
},
ImageResize: {parchment: Quill.import('parchment')},
magicUrl: true
};
}

View File

@ -3,7 +3,6 @@
display: inline-block;
&--widget {
-webkit-perspective: 0;
-webkit-backface-visibility: hidden;
-webkit-transform: translate3d(0,0,0);
visibility: visible;

View File

@ -102,6 +102,7 @@ export default {
'CHANGE_EMAIL': 'Mudar o e-mail',
'CHANGE_PASSWORD': 'Mudar a senha',
'NAME': 'Nome',
'APPLY': 'Aplicar',
'SIGNUP_DATE': 'Data de inscrição',
'CLEAR': 'Claro',
'SEARCH': 'Procurar',
@ -121,6 +122,7 @@ export default {
'NO_RESULTS': 'Nenhum resultado',
'DELETE_AND_BAN': 'Excluir e banir',
'STAFF_LEVEL': 'Nível Pessoal',
'TICKETS_ASSIGNED': 'Ingressos atribuídos',
'ASSIGNED': 'Atribuído',
'ASSIGNED_TICKETS': '{tickets} chamados atribuídos',
'CLOSED_TICKETS': '{tickets} chamados fechados',
@ -133,9 +135,10 @@ export default {
'LEVEL_3': 'Nível 3 (Chamados + Artigos + Equipe)',
'LEVEL_1_DESCRIPTION': 'Só pode responder aos chamados e gerenciar os usuários',
'LEVEL_2_DESCRIPTION': 'Pode fazer o que o nível 1 faz e pode criar ou editar artigos e pode criar respostas customizadas.',
'LEVEL_3_DESCRIPTION': 'Pode fazero que o nível 2 faz e pode criar ou editar membros da equipe e pode gerenciar todo o sistema.',
'LEVEL_3_DESCRIPTION': 'Pode fazer o que o nível 2 faz e pode criar ou editar membros da equipe e pode gerenciar todo o sistema.',
'UPDATE_EMAIL': 'Atualizar e-mail',
'UPDATE_PASSWORD': 'Atualizar senha',
'UPDATE_CUSTOM_FIELDS': 'Atualize campos personalizados',
'UPDATE_LEVEL': 'Nível de atualização',
'UPDATE_DEPARTMENTS': 'Atualizar departamentos',
'EDIT_STAFF': 'Editar membro da equipe',
@ -189,7 +192,7 @@ export default {
'BACKUP_DATABASE': 'Backup do banco de dados',
'DELETE_ALL_USERS': 'Excluir todos os usuários',
'PLEASE_CONFIRM_PASSWORD': 'Confirme sua senha para fazer essas alterações',
'REGISTRATION_API_KEYS': 'API de registro',
'API_KEYS': 'Chaves API',
'NAME_OF_KEY': 'Nome da chave',
'KEY': 'Chave',
'ADD_API_KEY': 'Adicionar chave de API',
@ -228,11 +231,22 @@ export default {
'OPTIONS': 'Opções',
'FIELD_DESCRIPTION': 'Descrição do campo (opcional)',
'DESCRIPTION_ADD_CUSTOM_TAG': 'aqui você pode adicionar uma nova tag personalizada',
'PERMISSIONS': 'Permissões',
'TICKET_CREATION_PERMISSION': 'Permitir a criação de bilhetes',
'TICKET_CHECK_PERMISSION': 'Permitir verificar o bilheteiro',
'TICKET_NUMBER_RETURN_PERMISSION': 'Permitir retorno do número do ticket',
'USER_CREATION_PERMISSION': 'Permitir a criação do usuário',
'DESCRIPTION_EDIT_CUSTOM_TAG': 'aqui você pode editar uma marca personalizada',
'CUSTOM_FIELDS': 'Os campos personalizados',
'CHART_CREATE_TICKET': 'Chamados criados',
'CHART_CLOSE': 'Chamados fechados',
'RESEND_EMAIL_VERIFICATION': 'Reenviar a verificação por email.',
'RESEND_EMAIL_VERIFICATION_SUCCESS': 'O correio foi enviado com sucesso',
'RESEND_EMAIL_VERIFICATION_FAIL': 'ocorreu um erro',
'USER_UNLOGGED_IN': 'Este usuário nunca foi logado antes',
'RESEND_STAFF_INVITATION_SUCCESS': 'O convite foi enviado com sucesso',
'RESEND_STAFF_INVITATION_FAIL': 'O convite não pôde ser enviado',
'CHART_SIGNUP': 'Inscrições',
'CHART_COMMENT': 'Respondidos',
'CHART_ASSIGN': 'Atribuídos',
@ -284,6 +298,7 @@ export default {
'DATABASE_NAME': 'Nome do banco de dados MySQL',
'DATABASE_USER': 'Usuário do MySQL',
'DATABASE_PASSWORD': 'Senha do MySQL',
'INSTALLATION_COMPLETED_TITLE': 'Instalação completa',
'ADMIN_NAME': 'Nome da conta de administrador',
'ADMIN_EMAIL': 'E-mail da conta de administrador',
'ADMIN_PASSWORD': 'Senha da conta de administrador',
@ -368,6 +383,7 @@ export default {
'INVITE_USER_VIEW_DESCRIPTION': 'Aqui você pode convidar um usuário para se juntar nosso sistema de suporte, ele só precisará fornecer sua senha para criar um novo usuário.',
'ERROR_NAME': 'Nome inválido',
'INVITE_STAFF_DESCRIPTION': 'Aqui você pode convidar membros do pessoal para suas equipes.',
'TICKETS_INFORMATION' : 'Ingressos de departamentos que você não atribuiu não ficarão visíveis.',
'ERROR_TITLE': 'Título inválido',
'ERROR_EMAIL': 'E-mail inválido',
'ERROR_CONTENT_SHORT': 'Conteúdo muito curto',
@ -393,6 +409,7 @@ export default {
'ERRORS_FOUND': 'Erros encontrados',
'ERROR_IMAGE_SIZE': 'Nenhuma imagem pode ter um tamanho maior que {size} MB',
'USER_DISABLED': 'Esta conta está desativada.',
'NAME_ALREADY_USED': 'Nome já usado',
'INVALID_SYNTAX': 'Sintaxe inválida.',
'DEPARTMENT_PRIVATE_TICKETS': 'Este departamento tem chamados criados por não funcionários e não pode ser privado',
'CURRENTLY_UNAVAILABLE': 'Atualmente indisponivel',
@ -404,10 +421,12 @@ export default {
'INVALID_SUPERVISED_USERS': 'usuários supervisionadas inválidos',
'INVALID_DEFAULT_DEPARTMENT': 'escolhido departamento padrão é inválido',
'TICKET_SENT': 'O chamado foi aberto com sucesso.',
'TICKET_NUMBER_SENT': 'O tíquete foi criado com sucesso e um e-mail com o número do tíquete foi enviado.',
'SUPERVISOR_CAN_NOT_SUPERVISE_HIMSELF': 'Supervisor não pode fiscalizar a si mesmo',
'VALID_RECOVER': 'Senha recuperada com êxito',
'EMAIL_EXISTS': 'e-mail já existe',
'INVITE_USER_SUCCESS': 'Você convidou um novo usuário com sucesso em nosso sistema de suporte',
'WILL_DELETE_CUSTOM_TAG': 'A tag personalizada será excluída.',
'ARE_YOU_SURE': 'Você tem certeza?',
'EMAIL_WILL_CHANGE': 'O e-mail atual será alterado',
'PASSWORD_WILL_CHANGE': 'A senha atual será alterada',
@ -426,6 +445,8 @@ export default {
'WILL_DELETE_STAFF': 'Este membro da equipe será excluído e todos os seus chamados serão desatribuídos.',
'WILL_RECOVER_EMAIL_TEMPLATE': 'Este modelo de e-mail será recuperado para seu valor padrão neste idioma.',
'SUCCESS_IMPORTING_CSV_DESCRIPTION': 'O arquivo CSV foi importado com êxito',
'TODAY_AT': 'Hoje às',
'YESTERDAY_AT': 'Ontem às',
'SUCCESS_DELETING_ALL_USERS': 'Os usuários foram excluídos com êxito',
'SUCCESSFUL_CONNECTION': 'Conexão bem sucedida',
'UNSUCCESSFUL_CONNECTION': 'Conexão sem sucesso',
@ -456,5 +477,24 @@ export default {
'TEST_SMTP_CONNECTION': 'Testar conexão SMTP',
'SERVER_ERROR': 'Não é possível se conectar ao servidor.',
'EMAIL_SERVER_ADDRESS': 'Endereço do servidor de email',
'EMAIL_SERVER_ADDRESS_DESCRIPTION': 'Endereço onde os e-mails serão recebidos e enviados'
'EMAIL_SERVER_ADDRESS_DESCRIPTION': 'Endereço onde os e-mails serão recebidos e enviados',
'CREATED': 'Criada',
'CREATED_DESCRIPTION': 'Created tickets during the selected time range',
'OPEN': 'Open',
'OPEN_DESCRIPTION': 'Tíquetes criados durante o intervalo de tempo selecionado',
'CLOSED_DESCRIPTION': 'Tíquetes fechados criados durante o intervalo de tempo selecionado',
'INSTANT': 'Instante',
'INSTANT_DESCRIPTION': 'Porcentagem de tíquetes fechados após uma única resposta da equipe sobre o total de tíquetes fechados',
'REOPENED': 'Reaberta',
'REOPENED_DESCRIPTION': 'Porcentagem de tíquetes reabertos sobre o total de tíquetes criados',
'MONDAY': 'Segunda-feira',
'TUESDAY': 'Terça-feira',
'WEDNESDAY': 'quarta-feira',
'THURSDAY': 'quinta-feira',
'FRIDAY': 'sexta-feira',
'SATURDAY': 'sábado',
'SUNDAY': 'Domigo',
'TICKET_NUMBER_SENT': 'O tíquete foi criado com sucesso e um e-mail com o número do tíquete foi enviado.',
'TICKETS_INFORMATION' : 'Ingressos de departamentos que você não atribuiu não ficarão visíveis.',
'API_KEYS': 'Chaves API'
};

View File

@ -102,6 +102,7 @@ export default {
'CHANGE_EMAIL': '更改電子郵件',
'CHANGE_PASSWORD': '更改密碼',
'NAME': '名稱',
'APPLY': '申请',
'SIGNUP_DATE': '註冊日期',
'CLEAR': '明确',
'SEARCH': '搜索',
@ -121,6 +122,7 @@ export default {
'NO_RESULTS': '沒有結果',
'DELETE_AND_BAN': '刪除和禁止',
'STAFF_LEVEL': '員工級別',
'TICKETS_ASSIGNED': '分配的门票',
'ASSIGNED': '分配',
'ASSIGNED_TICKETS': '{ticket}分配的票',
'CLOSED_TICKETS': '{ticket}已關閉的門票',
@ -136,6 +138,7 @@ export default {
'LEVEL_3_DESCRIPTION': '可以做每一級2可以創建或編輯員工並可以管理整個系統。',
'UPDATE_EMAIL': '更新電子郵件',
'UPDATE_PASSWORD': '更新密碼',
'UPDATE_CUSTOM_FIELDS': '更新自定义字段',
'UPDATE_LEVEL': '更新級別',
'UPDATE_DEPARTMENTS': '更新部門',
'EDIT_STAFF': '編輯員工',
@ -189,7 +192,6 @@ export default {
'BACKUP_DATABASE': '備份數據庫',
'DELETE_ALL_USERS': '刪除所有用戶',
'PLEASE_CONFIRM_PASSWORD': '請確認您的密碼進行更改',
'REGISTRATION_API_KEYS': '註冊API密鑰',
'NAME_OF_KEY': '密鑰名稱',
'KEY': '鍵',
'ADD_API_KEY': '添加API密鑰',
@ -229,11 +231,22 @@ export default {
'FIELD_DESCRIPTION': '字段描述(可选)',
'DESCRIPTION_ADD_CUSTOM_TAG': '在这里,您可以添加新的自定义标记',
'DESCRIPTION_EDIT_CUSTOM_TAG': '在这里你可以编辑自定义标签',
'PERMISSIONS': '权限',
'TICKET_CREATION_PERMISSION': '允许票务创造',
'TICKET_CHECK_PERMISSION': '允许票务检查',
'TICKET_NUMBER_RETURN_PERMISSION': '允许票号返回',
'USER_CREATION_PERMISSION': '允许用户创建',
'CUSTOM_FIELDS': '自定义字段',
'CHART_CREATE_TICKET': '已創建門票',
'CHART_CLOSE': '門票已關閉',
'CHART_SIGNUP': '註冊',
'RESEND_EMAIL_VERIFICATION': '重新发送电子邮件验证',
'RESEND_EMAIL_VERIFICATION_SUCCESS': '邮件已成功发送',
'RESEND_EMAIL_VERIFICATION_FAIL': '发生了错误',
'USER_UNLOGGED_IN': '此用户以前从未登录过',
'RESEND_STAFF_INVITATION_SUCCESS': '邀请成功发送',
'RESEND_STAFF_INVITATION_FAIL': '无法发送邀请',
'CHART_COMMENT': '回复',
'CHART_ASSIGN': '分配',
@ -285,6 +298,7 @@ export default {
'DATABASE_USER': 'MySQL用戶',
'DATABASE_PASSWORD': 'MySQL密碼',
'ADMIN_NAME': '管理員帳戶名稱',
'INSTALLATION_COMPLETED_TITLE': '安装完成',
'ADMIN_EMAIL': '管理帳戶電子郵件',
'ADMIN_PASSWORD': '管理員帳號密碼',
'ADMIN_PASSWORD_DESCRIPTION': '請記住這個密碼。需要訪問管理面板。您可以稍後更改。',
@ -395,6 +409,7 @@ export default {
'USER_DISABLED': '此帐户已被停用。',
'INVALID_SYNTAX': '无效的语法。',
'DEPARTMENT_PRIVATE_TICKETS': '这个部门有非工作人员创建的门票,不能是私人的',
'NAME_ALREADY_USED': '已使用的名称',
'CURRENTLY_UNAVAILABLE': '当前不可用',
'CAN_NOT_DELETE_DEFAULT_DEPARTMENT': '默认部门不能被删除',
@ -411,6 +426,7 @@ export default {
'ARE_YOU_SURE': '你確定?',
'EMAIL_WILL_CHANGE': '當前的電子郵件將被更改',
'PASSWORD_WILL_CHANGE': '當前密碼將被更改',
'WILL_DELETE_CUSTOM_TAG': '自定义标记将被删除。',
'EMAIL_CHANGED': '電子郵件已成功更改',
'PASSWORD_CHANGED': '密碼已成功更改',
'OLD_PASSWORD_INCORRECT': '舊密碼不正確',
@ -429,6 +445,8 @@ export default {
'SUCCESS_DELETING_ALL_USERS': '用戶已成功刪除',
'SUCCESSFUL_CONNECTION': '成功连接',
'UNSUCCESSFUL_CONNECTION': '连接不成功',
'TODAY_AT': '今天',
'YESTERDAY_AT': '昨天',
'SERVER_CREDENTIALS_WORKING': '服务器凭据正常工作',
'DELETE_CUSTOM_FIELD_SURE': '某些用户可能正在使用此字段。你确定你要删除吗?',
@ -456,5 +474,25 @@ export default {
'TEST_SMTP_CONNECTION': '测试SMTP连接',
'SERVER_ERROR': '无法连接到服务器。',
'EMAIL_SERVER_ADDRESS': '电邮服务器地址',
'EMAIL_SERVER_ADDRESS_DESCRIPTION': '地址将收到和发送邮件'
'EMAIL_SERVER_ADDRESS_DESCRIPTION': '地址将收到和发送邮件',
/////////////////////
'CREATED': '已创建',
'CREATED_DESCRIPTION': '在选定时间范围内创建的工单',
'OPEN': '打开',
'OPEN_DESCRIPTION': '在选定时间范围内创建的未结票',
'CLOSED_DESCRIPTION': '在选定时间范围内创建的已关闭工单',
'INSTANT': '立即的',
'INSTANT_DESCRIPTION': '单个工作人员回复后关闭的门票占关闭的门票总数的百分比',
'REOPENED': '重新开放',
'REOPENED_DESCRIPTION': '重新打开的工单占创建的工单总数的百分比',
'MONDAY': '周一',
'TUESDAY': '周二',
'WEDNESDAY': '周三',
'THURSDAY': '周四',
'FRIDAY': '星期五',
'SATURDAY': '周六',
'SUNDAY': '星期日',
'TICKET_NUMBER_SENT': '已成功创建票证,并已发送带有票证编号的电子邮件。',
'TICKETS_INFORMATION' : '来自您未分配部门的工单将不可见。',
'API_KEYS': 'API 密钥'
};

View File

@ -8,8 +8,8 @@ export default {
'DEFAULT': 'Standard',
'PASSWORD': 'Passwort',
'REPEAT_PASSWORD': 'Passwort wiederholen',
'LOG_IN': 'Einloggen',
'SIGN_UP': 'Anmelden',
'LOG_IN': 'Anmelden',
'SIGN_UP': 'Registrieren',
'FORGOT_PASSWORD': 'Passwort vergessen',
'RECOVER_PASSWORD': 'Passwort wiederherstellen',
'SET_UP_PASSWORD': 'Richten Sie Ihr Passwort',
@ -102,6 +102,7 @@ export default {
'CHANGE_EMAIL': 'E-Mail ändern',
'CHANGE_PASSWORD': 'Passwort ändern',
'NAME': 'Name',
'APPLY': 'Sich bewerben',
'SIGNUP_DATE': 'Anmeldedatum',
'CLEAR': 'klar',
'SEARCH': 'Suche',
@ -121,6 +122,7 @@ export default {
'NO_RESULTS': 'Keine Ergebnisse',
'DELETE_AND_BAN': 'Löschen und blockieren',
'STAFF_LEVEL': 'Mitarbeiter-Ebene',
'TICKETS_ASSIGNED': 'Tickets zugewiesen',
'ASSIGNED': 'Zugewiesen',
'ASSIGNED_TICKETS': '{tickets} zugeordnete Tickets',
'CLOSED_TICKETS': '{tickets} geschlossene Tickets',
@ -136,6 +138,7 @@ export default {
'LEVEL_3_DESCRIPTION': 'Kann alles aus Stufe 2, kann Mitarbeiter erstellen oder bearbeiten und kann das gesamte System verwalten.',
'UPDATE_EMAIL': 'E-Mail aktualisieren',
'UPDATE_PASSWORD': 'Kennwort aktualisieren',
'UPDATE_CUSTOM_FIELDS': 'Benutzerdefinierte Felder aktualisieren.',
'UPDATE_LEVEL': 'Stufe aktualisieren',
'UPDATE_DEPARTMENTS': 'Abteilungen aktualisieren',
'EDIT_STAFF': 'Mitarbeiter bearbeiten',
@ -189,7 +192,6 @@ export default {
'BACKUP_DATABASE': 'Sicherungsdatenbank',
'DELETE_ALL_USERS': 'Alle Benutzer löschen',
'PLEASE_CONFIRM_PASSWORD': 'Bitte bestätigen Sie Ihr Passwort, um diese Änderungen vorzunehmen!',
'REGISTRATION_API_KEYS': 'Registrierungs-API Schlüssel',
'NAME_OF_KEY': 'Name des Schlüssels',
'KEY': 'Schlüssel',
'ADD_API_KEY': 'API-Schlüssel hinzufügen',
@ -207,10 +209,10 @@ export default {
'HIMSELF': 'selbst',
'SUPERVISED_USERS_UPDATED': 'Betreute Nutzer aktualisiert',
'ADD_USER': 'Benutzer hinzufügen',
'INVITE_USER': 'laden Sie Benutzer',
'INVITE_STAFF': 'einladen Personal',
'INVITE_USER': 'Benutzer einladen',
'INVITE_STAFF': 'Personal einladen',
'UPLOAD_FILE': 'Datei hochladen',
'PRIVATE': 'Privatgelände',
'PRIVATE': 'Privat',
'ENABLE_USER': 'Benutzer aktivieren',
'DISABLE_USER': 'Benutzer deaktivieren',
'SHOW_CLOSED_TICKETS': 'Geschlossene Tickets anzeigen',
@ -229,11 +231,22 @@ export default {
'FIELD_DESCRIPTION': 'Feldbeschreibung (optional)',
'DESCRIPTION_ADD_CUSTOM_TAG': 'Hier können Sie ein neues benutzerdefiniertes Tag hinzufügen',
'DESCRIPTION_EDIT_CUSTOM_TAG': 'Hier können Sie einen benutzerdefinierten Tag bearbeiten',
'PERMISSIONS': 'Berechtigungen',
'TICKET_CREATION_PERMISSION': 'Erlauben Sie die Kartenkreation',
'TICKET_CHECK_PERMISSION': 'Ticketcheck zulassen',
'TICKET_NUMBER_RETURN_PERMISSION': 'Erlauben Sie die Ticketnummer zurück',
'USER_CREATION_PERMISSION': 'Benutzerkreibung zulassen',
'CUSTOM_FIELDS': 'Benutzerdefinierte Felder',
'CHART_CREATE_TICKET': 'Tickets erstellt',
'CHART_CLOSE': 'Tickets geschlossen',
'CHART_SIGNUP': 'Anmeldungen',
'RESEND_EMAIL_VERIFICATION': 'E-Mail-Überprüfung erneut senden',
'RESEND_EMAIL_VERIFICATION_SUCCESS': 'Die Mail wurde erfolgreich gesendet',
'RESEND_EMAIL_VERIFICATION_FAIL': 'Ein Fehler ist aufgetreten',
'USER_UNLOGGED_IN': 'Dieser Benutzer hat sich noch nie angemeldet',
'RESEND_STAFF_INVITATION_SUCCESS': 'Die Einladung wurde erfolgreich gesendet',
'RESEND_STAFF_INVITATION_FAIL': 'Die Einladung konnte nicht gesendet werden',
'CHART_COMMENT': 'Antworten',
'CHART_ASSIGN': 'Tickets zugewiesen',
@ -285,6 +298,7 @@ export default {
'DATABASE_USER': 'MySQL Benutzer',
'DATABASE_PASSWORD': 'MySQL Passwort',
'ADMIN_NAME': 'Admin Kontoname',
'INSTALLATION_COMPLETED_TITLE': 'Installation abgeschlossen',
'ADMIN_EMAIL': 'Admin-Konto E-Mail',
'ADMIN_PASSWORD': 'Admin-Konto Passwort',
'ADMIN_PASSWORD_DESCRIPTION': 'Bitte merken Sie sich dieses Passwort. Es ist für den Zugriff auf das Admin-Panel erforderlich. Sie können es später ändern.',
@ -395,6 +409,7 @@ export default {
'USER_DISABLED': 'Dieser Account ist deaktiviert.',
'INVALID_SYNTAX': 'Ungültiger Satzbau.',
'DEPARTMENT_PRIVATE_TICKETS': 'Diese Abteilung hat Tickets, die von Nicht-Mitarbeitern erstellt wurden, und kann nicht privat sein',
'NAME_ALREADY_USED': 'Name bereits benutzt',
'CURRENTLY_UNAVAILABLE': 'momentan nicht verfügbar',
'CAN_NOT_DELETE_DEFAULT_DEPARTMENT': 'Standard-Abteilung kann nicht gelöscht werden',
@ -411,6 +426,7 @@ export default {
'ARE_YOU_SURE': 'Sind Sie sicher?',
'EMAIL_WILL_CHANGE': 'Die aktuelle E-Mail-Adresse wird geändert.',
'PASSWORD_WILL_CHANGE': 'Das aktuelle Passwort wird geändert.',
'WILL_DELETE_CUSTOM_TAG': 'Das benutzerdefinierte Tag wird gelöscht.',
'EMAIL_CHANGED': 'Die E-Mail-Adresse wurde erfolgreich geändert.',
'PASSWORD_CHANGED': 'Das Passwort wurde erfolgreich geändert.',
'OLD_PASSWORD_INCORRECT': 'Das alte Passwort ist falsch.',
@ -429,6 +445,8 @@ export default {
'SUCCESS_DELETING_ALL_USERS': 'Die Benutzer wurden erfolgreich gelöscht.',
'SUCCESSFUL_CONNECTION': 'Erfolgreiche Verbindung',
'UNSUCCESSFUL_CONNECTION': 'Verbindung fehlgeschlagen',
'TODAY_AT': 'Heute um',
'YESTERDAY_AT': 'Gestern um',
'SERVER_CREDENTIALS_WORKING': 'Server-Anmeldeinformationen funktionieren ordnungsgemäß',
'DELETE_CUSTOM_FIELD_SURE': 'Einige Benutzer verwenden dieses Feld möglicherweise. Möchten Sie es wirklich löschen?',
@ -451,10 +469,29 @@ export default {
'LEFT_EMPTY_DATABASE': 'Leer lassen für automatische Datenbankerstellung.',
'REMEMBER_ME': 'Merken',
'EMAIL_LOWERCASE': 'E-Mail',
'DEFAULT_PORT': 'Leave leer für 3306 als Standard',
'DEFAULT_PORT': 'Für 3306 als Standard leer lassen',
'PASSWORD_LOWERCASE': 'Passwort',
'TEST_SMTP_CONNECTION': 'SMTP Verbindung testen',
'SERVER_ERROR': 'Kann nicht mit dem Server verbinden.',
'EMAIL_SERVER_ADDRESS': 'E-Mail-Serveradresse',
'EMAIL_SERVER_ADDRESS_DESCRIPTION': 'Adresse, an die Mails gesendet und gesendet werden'
'EMAIL_SERVER_ADDRESS_DESCRIPTION': 'Adresse, an die Mails empfangen und gesendet werden',
'CREATED': 'Erstellt',
'CREATED_DESCRIPTION': 'Erstellte Tickets im ausgewählten Zeitraum',
'OPEN': 'Offen',
'OPEN_DESCRIPTION': 'Offene Tickets, die im ausgewählten Zeitraum erstellt wurden',
'CLOSED_DESCRIPTION': 'Geschlossene Tickets, die im ausgewählten Zeitraum erstellt wurden',
'INSTANT': 'Sofortig',
'INSTANT_DESCRIPTION': 'Percentage of tickets closed after a single staff reply over the total of tickets closed',
'REOPENED': 'Wieder geöffnet',
'REOPENED_DESCRIPTION': 'Prozentsatz der wiedereröffneten Tickets an der Gesamtzahl der erstellten Tickets',
'MONDAY': 'Montag',
'TUESDAY': 'Dienstag',
'WEDNESDAY': 'Mittwoch',
'THURSDAY': 'Donnerstag',
'FRIDAY': 'Freitag',
'SATURDAY': 'Samstag',
'SUNDAY': 'Sonntag',
'TICKET_NUMBER_SENT': 'Ticket wurde erfolgreich erstellt und eine E-Mail mit der Ticketnummer wurde gesendet.',
'TICKETS_INFORMATION' : 'Tickets von Abteilungen, die Sie nicht zugewiesen haben, werden nicht angezeigt.',
'API_KEYS': 'API-Schlüssel'
};

View File

@ -27,6 +27,7 @@ export default {
'TICKET_LIST': 'Ticket List',
'SUPPORT_CENTER': 'Support Center',
'SUPERVISED_USER': 'Supervised users',
'SUPERVISED_USER_INFORMATION': 'Allows to see tickets from another user',
'DEPARTMENT': 'Department',
'DEFAULT_DEPARTMENT': 'Default Department',
'AUTHOR': 'Author',
@ -77,6 +78,7 @@ export default {
'OWNER': 'Owner',
'OWNED': 'Owned',
'STATUS': 'Status',
'PERIOD': 'Period',
'NONE': 'None',
'ANY': 'Any',
'OPENED': 'Opened',
@ -190,7 +192,7 @@ export default {
'BACKUP_DATABASE': 'Backup database',
'DELETE_ALL_USERS': 'Delete all users',
'PLEASE_CONFIRM_PASSWORD': 'Please confirm your password to make these changes',
'REGISTRATION_API_KEYS': 'Registration API keys',
'API_KEYS': 'API keys',
'NAME_OF_KEY': 'Name of key',
'KEY': 'Key',
'ADD_API_KEY': 'Add API Key',
@ -224,6 +226,7 @@ export default {
'NEW_CUSTOM_FIELD': 'New Custom field',
'TYPE': 'Type',
'SELECT_INPUT': 'Select input',
'TEXT': 'Text',
'TEXT_INPUT': 'Text input',
'OPTION': 'Option {index}',
'OPTIONS': 'Options',
@ -241,6 +244,13 @@ export default {
'CHART_SIGNUP': 'Signups',
'CHART_COMMENT': 'Replies',
'CHART_ASSIGN': 'Assigned',
'RESEND_EMAIL_VERIFICATION': 'Resend e-mail verification',
'RESEND_EMAIL_VERIFICATION_SUCCESS': 'The mail was sent successfully',
'RESEND_EMAIL_VERIFICATION_FAIL': 'An error has occurred',
'USER_UNLOGGED_IN': 'This user has never logged in before',
'RESEND_STAFF_INVITATION_SUCCESS': 'The invitation was sent successfully',
'RESEND_STAFF_INVITATION_FAIL': 'The invitation could not be sent',
'SESSION_EXPIRED': 'Session expired',
//ACTIVITIES
'ACTIVITY_COMMENT': 'commented ticket',
@ -362,6 +372,8 @@ export default {
'CUSTOM_FIELDS_DESCRIPTION': 'Custom fields are defined additional fields the users are able to fill to provide more information about them.',
'INVITE_USER_VIEW_DESCRIPTION': 'Here you can invite an user to join our support system, he will just need to provide his password to create a new user.',
'INVITE_STAFF_DESCRIPTION': 'Here you can invite staff members to your teams.',
'TICKETS_INFORMATION' : 'Tickets from departments you dont have assigned wont be visible.',
'SESSION_EXPIRED_DESCRIPTION': 'Your session has timed out. Please log in again.',
//ERRORS
'EMAIL_OR_PASSWORD': 'Email or password invalid',
@ -403,11 +415,16 @@ export default {
'INVALID_SUPERVISED_USERS': 'Invalid supervised users',
'SUPERVISOR_CAN_NOT_SUPERVISE_HIMSELF': 'Supervisor can not supervise himself',
'NAME_ALREADY_USED': 'Name already used',
'INVALID_PAGE_SIZE': 'Invalid page size',
'INVALID_CUSTOM_FIELD_TYPE': 'Invalid custom field type',
'INVALID_CUSTOM_FIELD_OPTIONS': 'Invalid custom field options',
'CUSTOM_FIELD_ALREADY_EXISTS': 'Custom field already exists',
//MESSAGES
'SIGNUP_SUCCESS': 'You have registered successfully in our support system.',
'INVITE_USER_SUCCESS': 'You have invited a new user successfully in our support system',
'TICKET_SENT': 'Ticket has been created successfully.',
'TICKET_NUMBER_SENT': 'Ticket has been created successfully and an email with the ticket number has been sent.',
'VALID_RECOVER': 'Password recovered successfully',
'EMAIL_EXISTS': 'Email already exists',
'ARE_YOU_SURE': 'Confirm action',
@ -437,6 +454,8 @@ export default {
'TITLE_EDITED': '(title edited)',
'COMMENT_EDITED': '(comment edited)',
'TODAY_AT': 'Today at',
'YESTERDAY_AT': 'Yesterday at',
'LAST_7_DAYS': 'Last 7 days',
'LAST_30_DAYS': 'Last 30 days',
'LAST_90_DAYS': 'Last 90 days',
@ -464,7 +483,6 @@ export default {
'CREATED_DESCRIPTION': 'Created tickets during the selected time range',
'OPEN': 'Open',
'OPEN_DESCRIPTION': 'Open tickets created during the selected time range',
'CLOSED': 'Closed',
'CLOSED_DESCRIPTION': 'Closed tickets created during the selected time range',
'INSTANT': 'Instant',
'INSTANT_DESCRIPTION': 'Percentage of tickets closed after a single staff reply over the total of tickets closed',

View File

@ -1,7 +1,7 @@
export default {
'WELCOME': 'Bienvenido',
'TICKETS': 'Tickets',
'ARTICLES': 'Artículo',
'ARTICLES': 'Artículos',
'ACCOUNT': 'Cuenta',
'SUBMIT': 'Enviar',
'EMAIL': 'Email',
@ -28,9 +28,9 @@ export default {
'SUPPORT_CENTER': 'Centro de Soporte',
'SUPERVISED_USER': 'Los usuarios supervisados',
'DEPARTMENT': 'Departamento',
'DEFAULT_DEPARTMENT': 'Por defecto Departamento',
'DEFAULT_DEPARTMENT': 'Departamento predeterminado',
'AUTHOR': 'Autor',
'AUTHORS': 'autores',
'AUTHORS': 'Autores',
'DATE': 'Fecha',
'RESPOND': 'Responder',
'RESPOND_TICKET': 'Responder ticket',
@ -52,14 +52,14 @@ export default {
'NEW_TICKETS': 'Nuevos Tickets',
'ALL_TICKETS': 'Todos los Tickets',
'CUSTOM_RESPONSES': 'Respuestas Personalizadas',
'SEARCH_TICKETS': 'Buscar entradas',
'SEARCH_TICKETS': 'Buscar tickets.',
'CUSTOM_LIST': 'Lista personalizada',
'CUSTOM_TAGS': 'Etiquetas personalizadas',
'LIST_USERS': 'Lista de Usuarios',
'TAGS': 'Etiquetas',
'BAN_USERS': 'Bloquear Usuarios',
'LIST_ARTICLES': 'Lista de Artículos',
'STAFF_MEMBERS': 'Staff Members',
'STAFF_MEMBERS': 'Miembros del staff',
'DEPARTMENTS': 'Departamentos',
'SYSTEM_PREFERENCES': 'Preferencias del Sistema',
'ADVANCED_SETTINGS': 'Opciones Avanzadas',
@ -82,7 +82,7 @@ export default {
'OPENED': 'Abierto',
'CLOSED': 'Cerrado',
'CLOSE': 'Cerrar',
'ANY': 'Alguna',
'ANY': 'Cualquiera',
'RE_OPEN': 'Reabrir',
'ASSIGN_TO_ME': 'Asignar',
'UN_ASSIGN': 'Desasignar',
@ -102,8 +102,9 @@ export default {
'CHANGE_EMAIL': 'Cambiar email',
'CHANGE_PASSWORD': 'Cambiar contraseña',
'NAME': 'Nombre',
'APPLY': 'Aplicar',
'SIGNUP_DATE': 'Fecha de registro',
'CLEAR': 'Claro',
'CLEAR': 'Limpiar',
'SEARCH': 'Buscar',
'SEARCH_USERS': 'Buscar usuarios...',
'SEARCH_EMAIL': 'Buscar email...',
@ -121,6 +122,7 @@ export default {
'NO_RESULTS': 'No hay resultados',
'DELETE_AND_BAN': 'Borrar y bloquear',
'STAFF_LEVEL': 'Nivel de Staff',
'TICKETS_ASSIGNED': 'Tickets asignados',
'ASSIGNED': 'Asignado',
'ASSIGNED_TICKETS': '{tickets} Tickets Asignados',
'CLOSED_TICKETS': '{tickets} Tickets Cerrados',
@ -131,13 +133,14 @@ export default {
'LEVEL_1': 'Nivel 1 (Tickets)',
'LEVEL_2': 'Nivel 2 (Tickets + Artículos)',
'LEVEL_3': 'Nivel 3 (Tickets + Artículos + Staff)',
'LEVEL_1_DESCRIPTION': 'sólo puede responder tickets y administrar usuarios.',
'LEVEL_2_DESCRIPTION': 'puede hacer lo mismo que el Nivel 1, y además, crear o editar artículos y crear respuestas personalizadas.',
'LEVEL_3_DESCRIPTION': 'Puede hacer lo mismo que el Nivel 2, y además, crear o editar miembros del personal y puede gestionar todo el sistema.',
'LEVEL_1_DESCRIPTION': 'Sólo puede responder tickets y administrar usuarios.',
'LEVEL_2_DESCRIPTION': 'Puede hacer lo mismo que el Nivel 1, y además, crear o editar artículos y crear respuestas personalizadas.',
'LEVEL_3_DESCRIPTION': 'Puede hacer lo mismo que el nivel 2, además, crear o editar miembros del staff y gestionar todo el sistema.',
'UPDATE_EMAIL': 'Actualizar correo electrónico',
'UPDATE_PASSWORD': 'Actualizar contraseña',
'UPDATE_CUSTOM_FIELDS': 'Actualizar campos personalizados',
'UPDATE_LEVEL': 'Actualizar nivel',
'UPDATE_DEPARTMENTS': 'Actializar departamentos',
'UPDATE_DEPARTMENTS': 'Actualizar departamentos',
'EDIT_STAFF': 'Editar miembro del staff',
'ADD_DEPARTMENT': 'Añadir departamento',
'UPDATE_DEPARTMENT': 'Actualizar Departamento',
@ -182,14 +185,14 @@ export default {
'ALL_NOTIFICATIONS': 'Todas las notificaciones',
'VERIFY_SUCCESS': 'Usuario verificado',
'VERIFY_FAILED': 'No se pudo verificar',
'ENABLE_MANDATORY_LOGIN': 'entrada obligatoria para los clientes',
'ENABLE_MANDATORY_LOGIN': 'Logueo obligatorio para clientes',
'ENABLE_USER_SYSTEM': 'Usar sistema de usuarios para clientes',
'ENABLE_USER_REGISTRATION': 'Habilitar registro de usuarios',
'INCLUDE_USERS_VIA_CSV': 'Incluir usuarios por archivo CSV',
'BACKUP_DATABASE': 'Backup database',
'API_KEYS': 'Claves API',
'DELETE_ALL_USERS': 'Borrar todos los usuarios',
'PLEASE_CONFIRM_PASSWORD': 'Por favor, confirme su contraseña para hacer estos cambios',
'REGISTRATION_API_KEYS': 'Registro de API keys',
'NAME_OF_KEY': 'Nombre de la clave',
'KEY': 'Clave',
'ADD_API_KEY': 'Añadir nueva clave',
@ -204,21 +207,21 @@ export default {
'STAFF_UPDATED': 'Miembro de Staff actualizado',
'UPDATE': 'Actualizar',
'NEVER': 'Nunca',
'HIMSELF': 'si mismo',
'SUPERVISED_USERS_UPDATED': 'Los usuarios supervisados actualizan',
'HIMSELF': 'Si mismo',
'SUPERVISED_USERS_UPDATED': 'Los usuarios supervisados han sido actualizados.',
'ADD_USER': 'Añadir un usuario',
'INVITE_USER': 'invitar usuario',
'INVITE_STAFF': 'invitar staff',
'INVITE_USER': 'Invitar usuario',
'INVITE_STAFF': 'Invitar staff',
'UPLOAD_FILE': 'Subir archivo',
'PRIVATE': 'privado',
'PRIVATE': 'Privado',
'ENABLE_USER': 'Habilitar usuario',
'DISABLE_USER': 'Deshabilitar usuario',
'SHOW_CLOSED_TICKETS': 'Mostrar Tickets Cerrados',
'LOCKED': 'bloqueado',
'LOCKED': 'Bloqueado',
'IMAGE_HEADER_URL': 'URL del encabezado de la imagen',
'IMAGE_HEADER_DESCRIPTION': 'Imagen que se utilizará como encabezado del correo electrónico.',
'EMAIL_SETTINGS': 'Ajustes del correo electrónico',
'SHOW_MY_TICKETS': 'Mostrar mis entradas',
'SHOW_MY_TICKETS': 'Mostrar mis tickets.',
'ADDITIONAL_FIELDS': 'Campos Adicionales',
'NEW_CUSTOM_FIELD': 'Nuevo campo personalizado',
'TYPE': 'Tipo',
@ -228,44 +231,55 @@ export default {
'OPTIONS': 'Opciones',
'FIELD_DESCRIPTION': 'Descripción del campo (opcional)',
'DESCRIPTION_ADD_CUSTOM_TAG': 'Aquí puede agregar una nueva etiqueta personalizada',
'DESCRIPTION_EDIT_CUSTOM_TAG': 'aquí se puede editar una etiqueta personalizada',
'PERMISSIONS': 'Permisos',
'TICKET_CREATION_PERMISSION': 'Permitir la creación de tickets',
'TICKET_CHECK_PERMISSION': 'Permitir la revision del ticket',
'TICKET_NUMBER_RETURN_PERMISSION': 'Permitir la devolución del número de ticket',
'USER_CREATION_PERMISSION': 'Permitir la creación del usuario',
'DESCRIPTION_EDIT_CUSTOM_TAG': 'Aquí se puede editar una etiqueta personalizada',
'CUSTOM_FIELDS': 'Campos Personalizados',
'CHART_CREATE_TICKET': 'Tickets creados',
'CHART_CLOSE': 'Tickets cerrados',
'RESEND_EMAIL_VERIFICATION': 'Reenviar la verificación de correo electrónico',
'RESEND_EMAIL_VERIFICATION_SUCCESS': 'El mail fue enviado correctamente',
'RESEND_EMAIL_VERIFICATION_FAIL': 'Se ha producido un error',
'USER_UNLOGGED_IN': 'Este usuario nunca ha iniciado sesión antes',
'RESEND_STAFF_INVITATION_SUCCESS': 'La invitación fue enviada exitosamente.',
'RESEND_STAFF_INVITATION_FAIL': 'La invitación no pudo ser enviada.',
'CHART_SIGNUP': 'Registros',
'CHART_COMMENT': 'Respuestas',
'CHART_ASSIGN': 'Asignaciones',
//ACTIVITIES
'ACTIVITY_COMMENT': 'comentó el ticket',
'ACTIVITY_ASSIGN': 'se asignó el ticket',
'ACTIVITY_UN_ASSIGN': 'se desasignó el ticket',
'ACTIVITY_CLOSE': 'cerró el ticket',
'ACTIVITY_CREATE_TICKET': 'creó el ticket',
'ACTIVITY_RE_OPEN': 'reabrió el ticket',
'ACTIVITY_DEPARTMENT_CHANGED': 'cambió el departamento del ticket',
'ACTIVITY_EDIT_COMMENT': 'editó un comentario del ticket',
'ACTIVITY_COMMENT': 'Comentó el ticket',
'ACTIVITY_ASSIGN': 'Se asignó el ticket',
'ACTIVITY_UN_ASSIGN': 'Se desasignó el ticket',
'ACTIVITY_CLOSE': 'Cerró el ticket',
'ACTIVITY_CREATE_TICKET': 'Creó el ticket',
'ACTIVITY_RE_OPEN': 'Reabrió el ticket',
'ACTIVITY_DEPARTMENT_CHANGED': 'Cambió el departamento del ticket',
'ACTIVITY_EDIT_COMMENT': 'Editó un comentario del ticket',
'ACTIVITY_EDIT_SETTINGS': 'editó las preferencias',
'ACTIVITY_SIGNUP': 'se registró',
'ACTIVITY_EDIT_TITLE': 'editó el titulo del ticket',
'ACTIVITY_ADD_TOPIC': 'añadió el tema',
'ACTIVITY_ADD_ARTICLE': 'añadió el articulo',
'ACTIVITY_DELETE_TOPIC': 'eliminó el tema',
'ACTIVITY_DELETE_ARTICLE': 'eliminó el artículo',
'ACTIVITY_EDIT_ARTICLE': 'editó el artículo',
'ACTIVITY_INVITE': 'usuario invitado',
'ACTIVITY_ADD_STAFF': 'añadió el staff',
'ACTIVITY_ADD_DEPARTMENT': 'añadió el departamento',
'ACTIVITY_DELETE_DEPARTMENT': 'borró el departamento',
'ACTIVITY_EDIT_DEPARTMENT': 'editó el departamento',
'ACTIVITY_ADD_CUSTOM_RESPONSE': 'añadió una respuesta personalizada',
'ACTIVITY_DELETE_CUSTOM_RESPONSE': 'borró una respuesta personalizada',
'ACTIVITY_EDIT_CUSTOM_RESPONSE': 'editó una respuesta personalizada',
'ACTIVITY_BAN_USER': 'bloqueó el usuario',
'ACTIVITY_DELETE_USER': 'borró el usuario',
'ACTIVITY_UN_BAN_USER': 'desbloqueó el usuario',
'ACTIVITY_EDIT_SETTINGS': 'Editó las preferencias',
'ACTIVITY_SIGNUP': 'Se registró',
'ACTIVITY_EDIT_TITLE': 'Editó el titulo del ticket',
'ACTIVITY_ADD_TOPIC': 'Añadió el tema',
'ACTIVITY_ADD_ARTICLE': 'Añadió el articulo',
'ACTIVITY_DELETE_TOPIC': 'Eliminó el tema',
'ACTIVITY_DELETE_ARTICLE': 'Eliminó el artículo',
'ACTIVITY_EDIT_ARTICLE': 'Editó el artículo',
'ACTIVITY_INVITE': 'Usuario invitado',
'ACTIVITY_ADD_STAFF': 'Añadió el staff',
'ACTIVITY_ADD_DEPARTMENT': 'Añadió el departamento',
'ACTIVITY_DELETE_DEPARTMENT': 'Borró el departamento',
'ACTIVITY_EDIT_DEPARTMENT': 'Editó el departamento',
'ACTIVITY_ADD_CUSTOM_RESPONSE': 'Añadió una respuesta personalizada',
'ACTIVITY_DELETE_CUSTOM_RESPONSE': 'Borró una respuesta personalizada',
'ACTIVITY_EDIT_CUSTOM_RESPONSE': 'Editó una respuesta personalizada',
'ACTIVITY_BAN_USER': 'Bloqueó el usuario',
'ACTIVITY_DELETE_USER': 'Borró el usuario',
'ACTIVITY_UN_BAN_USER': 'Desbloqueó el usuario',
'SERVER_REQUIREMENTS': 'Requisitos del servidor',
'DATABASE_CONFIGURATION': 'Configuracion de la base de datos',
@ -284,6 +298,7 @@ export default {
'DATABASE_NAME': 'Nombre de la base de datos MySQL',
'DATABASE_USER': 'Usuario MySQL',
'DATABASE_PASSWORD': 'Contraseña MySQL',
'INSTALLATION_COMPLETED_TITLE': 'instalación completa',
'ADMIN_NAME': 'Nombre de la cuenta admin',
'ADMIN_EMAIL': 'Email de la cuenta admin',
'ADMIN_PASSWORD': 'Contraseña de la cuenta admin',
@ -334,8 +349,8 @@ export default {
'SYSTEM_PREFERENCES_DESCRIPTION': 'Aquí puede editar las preferencias del sistema.',
'VERIFY_SUCCESS_DESCRIPTION': 'Su usuario ha sido verificado correctamente. Puede iniciar sesión ahora.',
'VERIFY_FAILED_DESCRIPTION': 'No se pudo hacer la verificación.',
'MANDATORY_LOGIN_DISABLED': 'entrada obligatoria ha sido desactivado',
'STATISTICS_DESCRIPTION': 'Aquí puede ver estadisticas relacionadas con tickets y registros.',
'MANDATORY_LOGIN_DISABLED': 'El logueo obligatorio ha sido desactivado',
'STATISTICS_DESCRIPTION': 'Aquí puede ver estadisticas relacionadas con tickets y logueos.',
'ADVANCED_SETTINGS_DESCRIPTION': 'Aquí puede cambiar la configuración avanzada de su sistema. Tenga cuidado, los cambios que realice no se pueden revertir.',
'USER_SYSTEM_DISABLED': 'Se ha inhabilitado el sistema de usuarios',
'USER_SYSTEM_ENABLED': 'Se ha habilitado el sistema de usuarios',
@ -347,14 +362,14 @@ export default {
'EDIT_PROFILE_VIEW_DESCRIPTION': 'Aquí puede editar su usuario cambiando el correo electrónico o la contraseña.',
'ENABLE_USER_SYSTEM_DESCRIPTION': 'Habilitar/Deshabilitar el uso de un sistema de usuario. Si lo deshabilita, todos los usuarios serán eliminados pero los tickets serán guardados. Si lo habilita, se crearán los usuarios de los tickets existentes.',
'CSV_DESCRIPTION': 'El archivo CSV debe tener 3 columnas: correo electrónico, contraseña, nombre. No hay límite en el recuento de filas. Se creará un usuario por fila en el archivo.',
'SMTP_SERVER_DESCRIPTION': 'La configuracion de SMTP permite que la applicacion mande emails. Si no es configurado, ningún mail sera enviado OpenSupports.',
'SMTP_SERVER_DESCRIPTION': 'La configuración de SMTP permite que la aplicación envíe emails. Si no es configurado, ningún mail será enviado por OpenSupports.',
'IMAP_SERVER_DESCRIPTION': 'La configuración del servidor IMAP permite que la aplicación cree tickets de los correos electrónicos enviados a un buzón.',
'ENABLE_USER_DESCRIPTION': 'Esta acción permite al usuario iniciar sesión y crear tickets.',
'LOCK_DEPARTMENT_DESCRIPTION': 'Permiten a los usuarios crear entradas sólo en el departamento por defecto',
'LOCK_DEPARTMENT_DESCRIPTION': 'Permitir a los usuarios crear tickets solo en el departamento predeterminado.',
'DISABLE_USER_DESCRIPTION': 'El usuario estará deshabilitado y no podrá iniciar sesión y crear tickets.',
'PRIVATE_RESPONSE_DESCRIPTION': 'Esta respuesta solo será vista por los miembros del personal.',
'PRIVATE_RESPONSE_DESCRIPTION': 'Esta respuesta solo será vista por los miembros del staff.',
'PRIVATE_TOPIC_DESCRIPTION': 'Este tema solo será visto por los miembros del personal.',
'PRIVATE_DEPARTMENT_DESCRIPTION': 'Este departamento solo será visto por miembros del personal.',
'PRIVATE_DEPARTMENT_DESCRIPTION': 'Este departamento solo será visto por miembros del staff.',
'EMAIL_SETTINGS_DESCRIPTION': 'Aquí puede editar la configuración para recibir y enviar correos electrónicos a sus clientes.',
'IMAP_POLLING_DESCRIPTION': 'La verificación de la bandeja de entrada no se hará automáticamente por OpenSupports. Debe realizar solicitudes POST periódicamente a esta URL para procesar los correos electrónicos: {url}',
'NEW_CUSTOM_FIELD_DESCRIPTION': 'Aquí puede crear un campo personalizado para un usuario, puede ser un cuadro de texto en blanco o un conjunto fijo de opciones.',
@ -367,7 +382,7 @@ export default {
'ERROR_PASSWORD': 'Contraseña Incorrecta',
'INVITE_USER_VIEW_DESCRIPTION': 'Aquí se puede invitar a un usuario a nuestro sistema de soporte, que sólo tendrá que proporcionar su contraseña para crear una cuenta nueva.',
'ERROR_NAME': 'Nombre Incorrecto',
'INVITE_STAFF_DESCRIPTION': 'Aquí se puede invitar a los miembros del personal a sus equipos.',
'INVITE_STAFF_DESCRIPTION': 'Aquí puedes invitar a alguien para que sea parte del staff.',
'ERROR_TITLE': 'Título incorrecto',
'ERROR_EMAIL': 'Email inválido',
'ERROR_CONTENT_SHORT': 'Contenido demasiado corto',
@ -382,38 +397,40 @@ export default {
'ERROR_RETRIEVING_BAN_LIST': 'Se ha producido un error al intentar recuperar la lista de correos electrónicos bloqueados.',
'ERROR_BANNING_EMAIL': 'Se ha producido un error al intentar bloquear el email.',
'INVALID_NAME': 'nombre inválido',
'ERROR_RETRIEVING_ARTICLES': 'Se ha producido un error al intentar recuperar los artículos.',
'ERROR_RETRIEVING_ARTICLES': 'Se ha producido un error al intentar mostrar los artículos.',
'ERROR_LIST': 'Seleccione al menos uno',
'INVALID_TITLE': 'título inválido',
'INVALID_TITLE': 'Título inválido',
'ERROR_URL': 'URL incorrecta',
'UNVERIFIED_EMAIL': 'El email no esta verificado aún',
'ERROR_UPDATING_SETTINGS': 'Un error ocurrió mientras se intentaban actualizar las preferencias',
'UNVERIFIED_EMAIL': 'El email aún no ha sido verificado',
'ERROR_UPDATING_SETTINGS': 'Un error ocurrió mientras se intentaban actualizar las configuraciones.',
'INVALID_EMAIL_OR_TICKET_NUMBER': 'Email o numero de ticket inválido',
'INVALID_FILE': 'Archivo inválido',
'ERRORS_FOUND': 'Se encontraron errores',
'ERROR_IMAGE_SIZE': 'Ninguna imagen puede tener un tamaño superior a {size} MB',
'USER_DISABLED': 'Esta cuenta está deshabilitada.',
'INVALID_SYNTAX': 'Sintaxis inválida.',
'DEPARTMENT_PRIVATE_TICKETS': 'Este departamento tiene entradas creadas por personal no administrativo y no puede ser privado.',
'CURRENTLY_UNAVAILABLE': 'actualmente no disponible',
'CAN_NOT_DELETE_DEFAULT_DEPARTMENT': 'departamento por defecto no se puede eliminar',
'NAME_ALREADY_USED': 'Nombre en uso',
'DEPARTMENT_PRIVATE_TICKETS': 'Este departamento tiene tickets creados por usuarios, no puede ser privado.',
'CURRENTLY_UNAVAILABLE': 'Actualmente no disponible',
'CAN_NOT_DELETE_DEFAULT_DEPARTMENT': 'No se puede eliminar el departamento predeterminado.',
'DEFAULT_DEPARTMENT_CAN_NOT_BE_PRIVATE': 'El departamento predeterminado no puede ser privado.',
'DEFAULT_DEPARTMENT_CAN_NOT_BE_PRIVATE': 'departamento por defecto no puede ser privada',
//MESSAGES
'SIGNUP_SUCCESS': 'Se ha registrado con éxito en nuestro sistema de soporte.',
'INVALID_SUPERVISED_USERS': 'usuarios supervisados no válidos',
'INVALID_DEFAULT_DEPARTMENT': 'departamento elegido por defecto no es válido',
'INVALID_SUPERVISED_USERS': 'Usuarios supervisados inválidos.',
'INVALID_DEFAULT_DEPARTMENT': 'El departamento predeterminado elegido no es válido.',
'TICKET_SENT': 'El ticket se ha creado correctamente.',
'SUPERVISOR_CAN_NOT_SUPERVISE_HIMSELF': 'Supervisor no puede supervisar el propio',
'VALID_RECOVER': 'La contraseña se recuperó correctamente',
'EMAIL_EXISTS': 'El email ya existe',
'INVITE_USER_SUCCESS': 'Has invitado a un nuevo usuario con éxito en nuestro sistema de soporte',
'SUPERVISOR_CAN_NOT_SUPERVISE_HIMSELF': 'El supervisor no puede supervisarse a sí mismo.',
'VALID_RECOVER': 'La contraseña se recuperó correctamente.',
'EMAIL_EXISTS': 'El email ya existe.',
'INVITE_USER_SUCCESS': 'Has invitado a un nuevo usuario con éxito en nuestro sistema de soporte.',
'ARE_YOU_SURE': '¿Estás seguro?',
'EMAIL_WILL_CHANGE': 'El correo electrónico actual se cambiará',
'PASSWORD_WILL_CHANGE': 'Se cambiará la contraseña actual',
'EMAIL_CHANGED': 'Se ha cambiado correctamente el correo electrónico',
'PASSWORD_CHANGED': 'La contraseña ha sido cambiada correctamente',
'OLD_PASSWORD_INCORRECT': 'La antigua contraseña es incorrecta',
'EMAIL_WILL_CHANGE': 'El correo electrónico actual se cambiará.',
'WILL_DELETE_CUSTOM_TAG': 'La etiqueta personalizada será eliminada.',
'PASSWORD_WILL_CHANGE': 'Se cambiará la contraseña actual.',
'EMAIL_CHANGED': 'Se ha cambiado correctamente el correo electrónico.',
'PASSWORD_CHANGED': 'La contraseña ha sido cambiada correctamente.',
'OLD_PASSWORD_INCORRECT': 'La antigua contraseña es incorrecta.',
'WILL_LOSE_CHANGES': 'No has guardado. Los cambios se perderán.',
'WILL_DELETE_CUSTOM_RESPONSE': 'La respuesta personalizada se eliminará.',
'WILL_DELETE_DEPARTMENT': 'Se eliminará el departamento. Todos los tickets serán transferidos al departamento seleccionado.',
@ -428,6 +445,8 @@ export default {
'SUCCESS_IMPORTING_CSV_DESCRIPTION': 'El archivo CSV se ha importado correctamente',
'SUCCESS_DELETING_ALL_USERS': 'Los usuarios se han eliminado correctamente',
'SUCCESSFUL_CONNECTION': 'Conexión exitosa',
'TODAY_AT': 'Hoy a las',
'YESTERDAY_AT': 'Ayer a las',
'UNSUCCESSFUL_CONNECTION': 'Conexión fallida',
'SERVER_CREDENTIALS_WORKING': 'Las credenciales del servidor están funcionando correctamente',
'DELETE_CUSTOM_FIELD_SURE': 'Algunos usuarios pueden estar usando este campo. ¿Seguro que quiere borrarlo?',
@ -440,21 +459,42 @@ export default {
'LAST_365_DAYS': 'Últimos 365 dias',
'TEST': 'Prueba',
'ACTIVITY_COMMENT_THIS': 'comentó este ticket',
'ACTIVITY_ASSIGN_THIS': 'se asignó este ticket para',
'ACTIVITY_UN_ASSIGN_THIS': 'se desasignó este ticket para',
'ACTIVITY_CLOSE_THIS': 'cerró este ticket',
'ACTIVITY_CREATE_TICKET_THIS': 'creó este ticket',
'ACTIVITY_RE_OPEN_THIS': 'reabrió este ticket',
'ACTIVITY_DEPARTMENT_CHANGED_THIS': 'cambió el departamento de este ticket a ',
'DATE_PREFIX': 'el día',
'ACTIVITY_COMMENT_THIS': 'Comentó este ticket',
'ACTIVITY_ASSIGN_THIS': 'Se asignó este ticket a.',
'ACTIVITY_UN_ASSIGN_THIS': 'Se desasignó este ticket a.',
'ACTIVITY_CLOSE_THIS': 'Cerró este ticket',
'ACTIVITY_CREATE_TICKET_THIS': 'Creó este ticket',
'ACTIVITY_RE_OPEN_THIS': 'Reabrió este ticket',
'ACTIVITY_DEPARTMENT_CHANGED_THIS': 'Cambió el departamento de este ticket a ',
'DATE_PREFIX': 'El día',
'LEFT_EMPTY_DATABASE': 'Dejar vacío para la creación automática de bases de datos',
'REMEMBER_ME': 'Recordarme',
'EMAIL_LOWERCASE': 'email',
'EMAIL_LOWERCASE': 'Email',
'DEFAULT_PORT': 'Deje en blanco para 3306 por defecto',
'PASSWORD_LOWERCASE': 'contraseña',
'PASSWORD_LOWERCASE': 'Contraseña',
'TEST_SMTP_CONNECTION': 'Probar conexion de SMTP',
'SERVER_ERROR': 'No es posible conectar con el servidor.',
'EMAIL_SERVER_ADDRESS': 'Dirección del servidor de correo electrónico',
'EMAIL_SERVER_ADDRESS_DESCRIPTION': 'Dirección donde se recibirán y enviarán los correos.'
'EMAIL_SERVER_ADDRESS_DESCRIPTION': 'Dirección donde se recibirán y enviarán los correos.',
'CREATED': 'Creado',
'REOPENED': 'Reabierto',
'CREATED_DESCRIPTION': 'Tickets creados durante el intervalo de tiempo seleccionado',
'OPEN': 'Abierto',
'OPEN_DESCRIPTION': 'Tickets abiertos creados durante el intervalo de tiempo seleccionado',
'CLOSED_DESCRIPTION': 'Tickets cerrados creados durante el intervalo de tiempo seleccionado',
'INSTANT': 'Instante',
'INSTANT_DESCRIPTION': 'Porcentaje de tickets cerrados despues de que un solo staff responda sobre el total de tickets cerrados',
'REOPENED': 'Reabierto',
'SUNDAY': 'domingo',
'REOPENED_DESCRIPTION': 'Porcentaje de tickers que fueron reabiertos sobre el total de tickets creados',
'MONDAY': 'Lunes',
'TUESDAY': 'Martes',
'WEDNESDAY': 'Miercoles',
'THURSDAY': 'Jueves',
'FRIDAY': 'Viernes',
'SATURDAY': 'Sabado',
'SUNDAY': 'Domingo',
'TICKET_NUMBER_SENT': 'El ticket han sido creado satisfactoriamente y un email con el numero de ticket ha sido enviado.',
'TICKETS_INFORMATION' : 'Los tickets de departamentos que no tengas asignados no serán visible.',
'API_KEYS': 'API keys'
};

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