[Rt-commit] rt branch, 5.0-trunk, updated. rt-5.0.1-232-g2da10d0f3f
? sunnavy
sunnavy at bestpractical.com
Thu Apr 1 13:44:00 EDT 2021
The branch, 5.0-trunk has been updated
via 2da10d0f3f908d08811799b297d81552ac62b941 (commit)
via 2c3a518cf2fc2aac087877baa99caf80e517e596 (commit)
via 101d4a49e1bc28b9de58f82a9b7a5c9cf0e72ab8 (commit)
via 44877abae8008ebb85a0ae6ba95f5eceb874db30 (commit)
via 65136a9b6f2d23f2cb054c112391be58d3420cf5 (commit)
via df5f7b3cf9c685160e43b67f491512377170e2ec (commit)
via d895bba7d7cc335b1544a7b416ef6c1b595ccc8a (commit)
via 8408a86aa10e8b87bd5b8190d31995a4a2358d75 (commit)
via 8b71da11b8fb32b7ac57f095ad8d620602191382 (commit)
via cca7cb20559188ce565c84823dc7f04be92bcef3 (commit)
via 9b34a0f8e1f476606d43dce8775ca40fd40fe268 (commit)
via acd7eb687e7a9db1876fd438ec7d174092dfc549 (commit)
via 06224a80eb8894bb4ad9885b8b5a4ee4cd2711ad (commit)
via ba59372e25bf788c203c701616c7d1d74a3c4b2f (commit)
via 48dbc77ec017cae67b19652a6c59a1b42464bc15 (commit)
via d49a1a5263c737c64654add2c0a6ee66979dd180 (commit)
via db60fb9e79bd2909bb380e378dceb60106d41522 (commit)
via 54fe6d06a2439f5f3f203e77b051bd7acc412ac3 (commit)
via 19f2e08a5ce52e910e81c84bf13db5ed20c585fe (commit)
via 72c299bf7c4d76763dd1ae2ecec3da30332bcf5c (commit)
via 0ee4e69495e75fabf956efd8486639298ebc3de6 (commit)
via 72f21a9579d74174ead0c2bcafae9fc921ca23e0 (commit)
via 3fe198a5b090bc8b249e00e80a4e72062ddecc35 (commit)
via 9aab297d9507cf2c66e4f88b2f5889b97e158cd9 (commit)
via dcdbade3c0ef85c412f3fde7aafb30db4845f3ce (commit)
via 38ce6ca36e0b855677a5b2f50775725ed4d29b8c (commit)
via 8cb0f8354a38c0d50311b39c717c786093122450 (commit)
via 0cca648652c29d40439f92c5364065b097de6b90 (commit)
via 97297bef4ba94ab2edefbc79c0e6548c5d27931d (commit)
via 591579de086cdb279dcb9723e3c51fd0a6f0bdf7 (commit)
via dcd3ee96f6847d7e9f48bbdc0720d69d30e4ed41 (commit)
via 6fc6be9791f5d7fe6a7ed5c10aa98de8a5a4631f (commit)
via a993c48696190c8a7549913e1cfc076cba327510 (commit)
via d36154468351f1e5bbbdd41f61b0af220b62e587 (commit)
via fe5c51454013d5e9fa9f04217bf1c9f312e2096e (commit)
via ae488f31230a7689abf7707b16ce26b66706b8c6 (commit)
via 5fa0bfda3980747e1d4625fdc46060c351b47f5b (commit)
via b753aa411bf3f5946eeeea09aff8471b446f3d7e (commit)
via 6d6304741f8b15466261ea010a944a498e258a8a (commit)
via be277569dd34e5cf36895a572257bfa899772133 (commit)
via 92aff8842dd62df5623384cf25097076876d7df8 (commit)
via b1e6dfd2f8dc71ba70aaf00e7c11d55fe133b6d0 (commit)
via d2a595fcdb58713e0596e89317e4fe11b3b4b632 (commit)
via 63c81c99b99b34474c6b3423d500fdf8885a4de9 (commit)
via 124db83e80913c661fda4e53ba7730bf0f4696ca (commit)
via 0116ce10e86c697fb3df3f1737364cc9f5baa58b (commit)
via 7f379a59d0be8207750779e83b6f10c3df14bb6d (commit)
via 77c7f617c959f0134fe3685cf288e55097abcf4b (commit)
via ed65ee9026bf06665e128d034eb68ab615f53dcd (commit)
via 69b9a5d58099cf41f9a7d3d9fe6d5e6f6c1dacf3 (commit)
via 7d895ac15f2577c63034f4d51f77244f8c362929 (commit)
via ffdc0925cd2446a6e4515f5f0aaede546812dd79 (commit)
via 6aef6880de9083b0ed83ee5dd2ba033a9e77ecaa (commit)
via cf8d115a9ba7795e29a050d32d5369686e9c6835 (commit)
via 9cb360f8352b15f733e0998bff4f5e3f2ce3d083 (commit)
via a661236d645c4693af6de42701883a80ada49de0 (commit)
via c9231d05710432a23a92cc5ed551eb7a8b435978 (commit)
via 6d44d7e7ab939ed88c49b5bcdc3e5f5131967dcb (commit)
via 938968cc9f63d64047552ded541c9fad5ab55bf7 (commit)
via 458dc3ce482db5e773c6d71f66e2c381019cb3b3 (commit)
via 4e2d9bf1dae090144ef6630ef0bd96318cab01d9 (commit)
via eedf30d8efff673d45b3f0beb0f1c8359df05768 (commit)
via 7f4f5d42201c2ca28a6e42886fea2fc9afb0ab63 (commit)
via d6127ad940cfb182d37eaf2179c7648a4168c71d (commit)
via e31fa8917a21eabad2c17ddc17d334ca602944bb (commit)
via 807fc60c2273a9bee39b6601bcfd845bb6c4448c (commit)
via 181ce8e91ed126aa4a7e4e5d14460ebf75c4cb79 (commit)
via 77f60c16026f8d1e06fc4627afc42425f690c975 (commit)
via dded56f87f43104bd40e48c6469112a2326b8c76 (commit)
via 36d726805f9139228a02f5e2ad542fb20367427d (commit)
via bb33cfd6983312d86fb339ff0ece99beab7bbf6f (commit)
via 2f4fb5a400033dd69b37a7dee902e289829da7ae (commit)
via 72d0156301b181c8f3d1ee62c6a0c924c6391398 (commit)
via 755ebc672687b0ff4920cec027f0e3f7eb6acc67 (commit)
via c237545f3acb8160582e115b425ff44737f95957 (commit)
via 801e13a18327df2ae84c54fb14be1d3dbbddb4d2 (commit)
via 51dce7dc9b65f6ea4b932ff0c6b351298346d6a8 (commit)
via 49d72a69819932d62a7c5cefb3d4b956b2dafdfd (commit)
via 073933b8f7ab8453aa44d82fe7d9d8caeff95b5d (commit)
via 8c9aca29a97457a3faee3adb14ad63cc002301ea (commit)
via b1e4c79ee961ff789fe6aa2ac2c5daf8253992d4 (commit)
via d6a996bf5c51d7f0c7d4a04ad92e52a12d01b647 (commit)
via 8e5909337be8581f912cacea51a844acd4abc1a3 (commit)
via 674e4f5171c840942fa3455ad16abaaa3edc4f6a (commit)
via 2071d5bc79c09dae1200294d89a74abcc3fd42a9 (commit)
via 2e3c228f330ae0048a6674bdb7302d8afa87d3c9 (commit)
via c38437afba84a38bc48049f77fd07f6ae2da7293 (commit)
via f0d5babe23d74e12e771a130eae782d2e1069738 (commit)
via ee1985edc64409d8d7ecca5cbecb0ece483b165a (commit)
via c3f994533048bf11e1d9c0db8a6d895d5672280b (commit)
via ca1cee922ea1c084449d442158a62cd4ddc9eed1 (commit)
via fed93849071b9460953c36d7f2a6d74f2588c953 (commit)
via 2043c8a45e645b475da77b5d6f4ddc763abb356c (commit)
via fcbca1973bff5edcbaa965ec8fb75f16ff4a0d39 (commit)
via 6a8ea9f51aee3002eb9a6361f1fe22d8c4130376 (commit)
via eff063c2481838f072f9b6398e98328c765b5cdb (commit)
via 8c914a0aef00e31e289533f04637987b196f91be (commit)
via 855c9df08d09402111d1ee1597f21930dbd29988 (commit)
via 574c2e0ec13e03f9e2392b4713c31cfa73f710b9 (commit)
via 1baa631446e6e1fbc395df7d41044d5c45c5520d (commit)
via 93faa6da1fba197a94c49c3b6da0da4fd1dd3941 (commit)
via c7c2988f7b0e6fe4d10c8eb8a6e5c57346d63a37 (commit)
via e74805287b50a8753cb3f73f79335622631a4f25 (commit)
via ef729ae7ea410435cb2be551ceb1996425da9b36 (commit)
via 9f603a421277f558231a387ea06872a3aa7fe59f (commit)
via 006676e5f0091ab18a9bbb9bdffb682f8334f1fc (commit)
via a3c5c3f073598d4c008c7a49ef0ec63229827bfa (commit)
via ba843e17394f37bd2925b934732c8ea5dfe8712b (commit)
via a903bc01a608fec77b0e2c13f3d096935a2821dc (commit)
via 516aa424fd3ad1fd9b00fc02080829325baf0bbb (commit)
via dd264d68d38e1df31908a890fd9038b01ccf30f6 (commit)
via ba03f06a26b5a44945aa175ddb78f22e23078b44 (commit)
via 0d20bdf79fffd9aed6e2c29d177fc978f00a9b25 (commit)
via 298da0b7b1e7dca2e88cff6152dbe168f8b51d6a (commit)
via 8750e4b643b5703b3aae52d625e0b59c4a2180f4 (commit)
via 18b5d085d89c775fd79fe9a20f25ddacf1ae8ef1 (commit)
via c58ada2ba416dd51f1f35043cca9ae38dc6bd803 (commit)
via 757ccab492182288eb5bdfb94d4af69e60de9bc9 (commit)
via a1e09f28739fd164d8de73f77dde64e8bcd6c595 (commit)
via b001d35d72ce2c6bebe2a68a0b496db1833f46d3 (commit)
via 9cbf7b129633b07b6990c6a57fa3339fc658d196 (commit)
via 214ec3857bad78615d61a62bbcd24fa848b4ed78 (commit)
via 1d1745b55eb548636170b2ddb058dbac5edf0e4a (commit)
via 1ab8481c909c9fb2645b8a39342374da25a07e2a (commit)
via 6449c83016f50a07945277bd2fa60c93323d2ea9 (commit)
via 52bf8213a26aae7d74f4d2fc260cc37e06de145d (commit)
via cc64be89fc56b2d82db336c420f96585b3b53bf3 (commit)
via 0b5efed83e8554362049c3999d21461507c07d75 (commit)
via 7b90ad070641eee11d7337afc1a4c9b92bfd31d1 (commit)
via 768861023940d8297fc45bf2d14dcdc198030a38 (commit)
via d1ec80c78a05fb175be64c3f808585b509ce0c92 (commit)
via 69bfd11c84eb860d0c6f10aead91e5e91fe11f25 (commit)
via f9d1c4ca0bb73dec9ee4ca7af9a4839fbb181a0b (commit)
via 5857fdf9f92fcb914560a6d1a17ad9f68378c159 (commit)
via b47c88327557c63aec2721f8fb3b1ea80d33ca43 (commit)
via f336a221215cc624c0a3ae21638d34e414900141 (commit)
via 245807296a0b8456f39b28d4b1fa344259c454b6 (commit)
via 31ca5c2ab297d4593102e6fb28f5ae18644109d0 (commit)
via 2505933f070b68f0bc59feb4e060f59c05359eb6 (commit)
via 5dc91ca1477e4d8e10ab2406adb80835deff4dd7 (commit)
via 7c57aa072b1d0f1c591da5bdea5cac0320d458fe (commit)
via b15a6cf52c9eda8dfee1abf6de070eb7b70741b5 (commit)
via b83819374cd5ebdc5c8ddf3e3e1bd168a03e3e0c (commit)
via feec798d3d18fa4151042675e3eef359ba883010 (commit)
via de3e4f500064e00bc84f54336dfc8638607a1b5c (commit)
via 406252f3112fda7dfce4cf590f77cf11b2df5b30 (commit)
via 6389f6eaf01ae9d9e7f5987243480c63d60d41ba (commit)
via 9d05b93c1317f8b7b8f5edf4bd0ad65a676bbfaf (commit)
via 49ae6ce096cb70fc6b043505ed4981e6b508fb34 (commit)
via 39a112d9d520e738314f03e2df00a8d64e430c34 (commit)
via ae10cd4540132cd35d30cf79558facde6d844b7e (commit)
via 6cf274f5bc5b38db59f9c55c2482efd1c32effe5 (commit)
via 807096bb2b0a8376379f4c047aa5183eae370597 (commit)
via e48a276d2713b1bb120e55d681b98c6a201c7921 (commit)
via fdc698a95b0b162924232b6f3a1acc631708ed6f (commit)
via a821bfcf92ec39ae657d19583860e7d4ae213218 (commit)
via 35236873a7abb91542256b8fa7e0a52941dcf81f (commit)
via 4526de5c6980956733e3a5c37760d283bdb3b611 (commit)
via d0f2b70a14292615ff329dd9256f78e2c554ca9c (commit)
via 87bc2add4ec45df15a0aeeb7b12203df075e31c3 (commit)
via eed358f53e28bee8d6f2540f13b9faf7a4951a43 (commit)
via 43f8d97b24b4b33606eb6243974b33bd4ba4d898 (commit)
via 55b70e4963c4f22c0ca512930e5fe3a81b74114b (commit)
via d5113497de3c43a301d724375da451cdcc497c65 (commit)
via 26bf392d817288ec5231894acce4af9929ae807d (commit)
via c42cdcc2ffb1eb68110852aa9b35522027209251 (commit)
via b367e508aecc5962424f3ffe81d19fbc137b9e5a (commit)
via 23a848df5ffcb7637e77dec6e52c55b98852b675 (commit)
via 7b048865db28ce6f558a3c7c487727190718e9c8 (commit)
via f061a7f528f1378bf04cc374b0c9f2eeb386fc18 (commit)
via d1a9e08613d58f152470eeab9110e8f58ff13bda (commit)
via ab2fb018bba2ff2bfad61453708d9f3c9acb6634 (commit)
via 5ed396095275a5e94e4cc86969a2da438f564e40 (commit)
via 9ae1fc046a4c3ff766e6e650e7f98c5fdc6f885b (commit)
via 547eee59572d71ddc525ef80c2ad580c1df1e713 (commit)
via fac9e840c6cbb382ec8ceb7c28eec1f8cef505ab (commit)
via 82d92b9072eab550a38ad9a7f0dfcfb2c405c93b (commit)
via a6fc87ebb3729d1fe9128651731634ba97d6c14c (commit)
via f10597b4167a1e5a8e5a4ba2c28b27dd6ab70af6 (commit)
via 1e28eef1981ae4350eefbf31c0348cf6a498dfab (commit)
via 820df69baa211e0733e3865a579dedf6f6d31037 (commit)
via 86055935f3ac8e951317f6afd041eca4404a1a44 (commit)
via 315cf966b09a97834014a286f36a7f8d974ce2de (commit)
via ee4d6d6020f9c6f56b9cc1db0764db456694ce2a (commit)
via dc1b2b79c241d06d25741894f1d692b68eb91e64 (commit)
via 7f679f5e840145b91aeec2b2dd2ccc3b21e1517a (commit)
via c152c2dcb0c41d3b0d65f18daafda1d2db76ba6e (commit)
via e020bd194812b00a08631815e7658fc2658e6294 (commit)
via b6150f7e90e97de7f60a324d28d81817ddba7d7d (commit)
via de5e1be4fa841fe0d7f6f3eac376bd0edcaa58b4 (commit)
via 6bf51e3198b80a1c864334f801c17d783fa8beda (commit)
via 60e0752809c813de6eab7cc8b7558601faa3758e (commit)
from 67f7316ee38759f4fc72fa8d80b1fe032a415cd0 (commit)
Summary of changes:
Makefile.in | 2 +-
configure.ac | 4 +-
devel/third-party/README | 1 +
docs/UPGRADING-4.4 | 10 +
docs/authentication.pod | 7 +
etc/RT_Config.pm.in | 28 ++
etc/initialdata | 13 +
etc/upgrade/4.3.0/content | 2 +-
etc/upgrade/4.4.5/content | 19 ++
lib/RT/Action/Notify.pm | 126 +++----
lib/RT/Action/SendEmail.pm | 8 +
lib/RT/Asset.pm | 4 +-
lib/RT/Attachment.pm | 58 +++-
lib/RT/Authen/ExternalAuth.pm | 33 +-
lib/RT/Authen/ExternalAuth/LDAP.pm | 217 ++++++++-----
lib/RT/Condition/{Overdue.pm => ViaInterface.pm} | 33 +-
lib/RT/Config.pm | 49 ++-
lib/RT/Crypt.pm | 119 +++++--
lib/RT/Crypt/GnuPG.pm | 72 +++-
lib/RT/Crypt/SMIME.pm | 361 +++++++++++++++++++--
lib/RT/CustomField.pm | 39 +++
lib/RT/CustomRole.pm | 6 +-
lib/RT/Dashboard/Mailer.pm | 20 +-
lib/RT/GroupMember.pm | 10 +-
lib/RT/GroupMembers.pm | 2 +-
lib/RT/I18N.pm | 50 ++-
lib/RT/Interface/Email.pm | 3 +
lib/RT/Interface/REST.pm | 14 +-
lib/RT/Interface/Web.pm | 96 +++++-
lib/RT/Interface/Web/MenuBuilder.pm | 3 +-
lib/RT/ObjectCustomFields.pm | 27 ++
lib/RT/Principal.pm | 16 +
lib/RT/Record.pm | 14 +-
lib/RT/Record/AddAndSort.pm | 19 +-
lib/RT/Report/Tickets.pm | 12 +-
lib/RT/Shredder.pm | 23 +-
lib/RT/Shredder/Dependencies.pm | 1 +
lib/RT/Shredder/Plugin/Objects.pm | 3 +-
lib/RT/Shredder/Plugin/SQLDump.pm | 2 +
lib/RT/Test/{GnuPG.pm => Crypt.pm} | 158 +++++++--
lib/RT/Test/GnuPG.pm | 7 +
lib/RT/Test/SMIME.pm | 9 +
lib/RT/Test/Web.pm | 8 +
lib/RT/Tickets.pm | 4 +-
lib/RT/Transaction.pm | 13 +-
lib/RT/User.pm | 31 +-
sbin/rt-validator.in | 58 +++-
share/html/Admin/Elements/Portal | 1 +
share/html/Admin/Groups/Members.html | 9 +
.../dhandler => Admin/Helpers/ClearMasonCache} | 7 +-
share/html/Admin/Tools/Configuration.html | 23 ++
share/html/Admin/Users/Keys.html | 10 +-
.../AuthTokens.html => Crypt/GetGPGPubkey.html} | 38 ++-
.../Create.html => Crypt/GetSMIMECert.html} | 79 +++--
.../html/Dashboards/Elements/ShowPortlet/dashboard | 1 +
share/html/Dashboards/Render.html | 8 +-
share/html/Dashboards/Subscription.html | 28 ++
share/html/Elements/Crypt/KeyIssues | 4 +
share/html/Elements/Crypt/SelectKeyForEncryption | 6 +-
share/html/Elements/Crypt/SignEncryptWidget | 9 +-
share/html/Elements/CryptStatus | 64 +++-
share/html/Elements/EmailInput | 5 +
share/html/Elements/RT__Ticket/ColumnMap | 10 +-
share/html/Elements/ShowLinksOfType | 6 +-
share/html/Elements/ShowSearch | 4 +
share/html/Helpers/PreviewScrips | 8 +-
share/html/Helpers/ShowSimplifiedRecipients | 2 +-
share/html/Prefs/Other.html | 2 +-
share/html/REST/1.0/Forms/attachment/default | 1 +
share/html/REST/1.0/Forms/ticket/attachments | 1 +
share/html/REST/1.0/Forms/ticket/default | 1 +
share/html/REST/1.0/Forms/ticket/history | 13 +-
share/html/REST/1.0/dhandler | 3 +-
share/html/Search/Chart | 129 +++++++-
share/html/Search/Chart.html | 63 +++-
share/html/Search/Elements/Article | 4 +-
share/html/Search/Elements/EditFormat | 4 +-
share/html/Search/Elements/SelectGroupBy | 10 +
share/html/Search/JSChart | 71 +++-
share/html/SelfService/Create.html | 6 +
share/html/Ticket/Create.html | 8 +-
share/html/Ticket/Elements/UpdateCc | 8 +-
share/html/Ticket/Forward.html | 2 +-
share/html/Ticket/Modify.html | 6 +
share/html/Ticket/ModifyAll.html | 6 +
share/html/Ticket/Update.html | 8 +-
share/html/Widgets/SavedSearch | 15 +-
share/html/m/ticket/create | 8 +-
share/html/m/ticket/reply | 2 +-
share/static/js/util.js | 69 +++-
t/api/customfield.t | 57 ++++
t/api/transaction.t | 102 ++++++
t/assets/web.t | 16 +
t/crypt/gnupg/attachments-in-db.t | 3 +-
t/crypt/no-signer-address.t | 3 +-
t/crypt/smime/attachments-in-db.t | 4 +-
t/crypt/smime/bad-recipients.t | 6 +-
t/crypt/smime/crl-check.t | 46 +++
t/crypt/smime/extract-email-address.t | 31 ++
t/crypt/smime/other-certs.t | 77 +++++
t/crypt/smime/revoked.t | 74 +++++
t/customroles/notify.t | 48 ++-
t/data/smime/keys/CAWithCRL/cacert.pem | 22 ++
t/data/smime/keys/CAWithCRL/mycrl.cnf | 1 +
t/data/smime/keys/CAWithCRL/private/cakey.pem | 30 ++
t/data/smime/keys/dianne at skoll.ca.crt | 34 ++
t/data/smime/keys/revoked-ca.pem | 49 +++
t/data/smime/keys/revoked at example.com.pem | 39 +++
t/data/smime/keys/sender-crl at example.com.key | 30 ++
t/data/smime/keys/sender-crl at example.com.pem | 23 ++
t/data/smime/keys/smime at example.com.crt | 33 ++
t/externalauth/ldap_email_login.t | 93 ++++++
t/mail/crypt-gnupg.t | 29 +-
...going.t => crypt-per-queue-outgoing-protocol.t} | 37 ++-
t/mail/gnupg-bad.t | 5 +-
t/mail/gnupg-incoming.t | 46 ++-
t/mail/gnupg-outgoing-encrypted-plaintext.t | 3 +-
t/mail/gnupg-outgoing-encrypted.t | 3 +-
t/mail/gnupg-outgoing-plain-plaintext.t | 3 +-
t/mail/gnupg-outgoing-plain.t | 3 +-
t/mail/gnupg-outgoing-signed-plaintext.t | 3 +-
t/mail/gnupg-outgoing-signed.t | 3 +-
t/mail/gnupg-outgoing-signed_encrypted-plaintext.t | 3 +-
t/mail/gnupg-outgoing-signed_encrypted.t | 3 +-
t/mail/gnupg-realmail.t | 2 +-
t/mail/gnupg-reverification.t | 2 +-
t/mail/gnupg-special.t | 2 +-
t/mail/mime_decoding.t | 10 +
t/mail/smime/incoming.t | 66 +++-
t/mail/smime/other-signed.t | 18 +-
t/mail/smime/outgoing.t | 10 +-
t/mail/smime/realmail.t | 10 +-
t/mail/smime/reject_on_unencrypted.t | 16 +-
.../CVE-2012-4735-incoming-encryption-header.t | 2 +-
t/security/CVE-2012-4735-sign-any-key.t | 7 +-
t/security/CVE-2012-4735-sign-encrypt-header.t | 3 +-
t/shredder/03plugin_objects.t | 33 ++
t/shredder/03plugin_users.t | 19 ++
t/shredder/04organization.t | 34 ++
t/ticket/search.t | 20 +-
t/ticket/search_by_txn.t | 32 +-
t/validator/role_groups.t | 33 ++
t/web/admin_user.t | 10 +-
t/web/crypt-gnupg.t | 29 +-
t/web/gnupg-headers.t | 3 +-
t/web/gnupg-select-keys-on-create.t | 10 +-
t/web/gnupg-select-keys-on-update.t | 10 +-
t/web/gnupg-tickyboxes.t | 2 +-
t/web/html_template.t | 34 +-
t/web/saved_search_chart.t | 55 ++--
t/web/smime/outgoing.t | 61 ++--
t/web/ticket_time.t | 149 +++++++++
t/web/ticket_txn_content.t | 43 ++-
153 files changed, 3600 insertions(+), 663 deletions(-)
create mode 100644 etc/upgrade/4.4.5/content
copy lib/RT/Condition/{Overdue.pm => ViaInterface.pm} (81%)
copy lib/RT/Test/{GnuPG.pm => Crypt.pm} (75%)
copy share/html/{SelfService/Attachment/dhandler => Admin/Helpers/ClearMasonCache} (89%)
copy share/html/{Admin/Users/AuthTokens.html => Crypt/GetGPGPubkey.html} (74%)
copy share/html/{Admin/Actions/Create.html => Crypt/GetSMIMECert.html} (59%)
create mode 100644 t/crypt/smime/crl-check.t
create mode 100644 t/crypt/smime/extract-email-address.t
create mode 100644 t/crypt/smime/other-certs.t
create mode 100644 t/crypt/smime/revoked.t
create mode 100644 t/data/smime/keys/CAWithCRL/cacert.pem
create mode 100644 t/data/smime/keys/CAWithCRL/mycrl.cnf
create mode 100644 t/data/smime/keys/CAWithCRL/private/cakey.pem
create mode 100644 t/data/smime/keys/dianne at skoll.ca.crt
create mode 100644 t/data/smime/keys/revoked-ca.pem
create mode 100644 t/data/smime/keys/revoked at example.com.pem
create mode 100644 t/data/smime/keys/sender-crl at example.com.key
create mode 100644 t/data/smime/keys/sender-crl at example.com.pem
create mode 100644 t/data/smime/keys/smime at example.com.crt
create mode 100644 t/externalauth/ldap_email_login.t
copy t/mail/{smime/outgoing.t => crypt-per-queue-outgoing-protocol.t} (58%)
create mode 100644 t/shredder/03plugin_objects.t
create mode 100644 t/shredder/04organization.t
create mode 100644 t/validator/role_groups.t
create mode 100644 t/web/ticket_time.t
- Log -----------------------------------------------------------------
commit 2da10d0f3f908d08811799b297d81552ac62b941
Merge: 67f7316ee3 2c3a518cf2
Author: sunnavy <sunnavy at bestpractical.com>
Date: Wed Mar 31 05:23:07 2021 +0800
Merge branch '4.4-trunk' into 5.0-trunk
diff --cc devel/third-party/README
index ac45a3e02a,f9227a89f7..4883f6b8cf
--- a/devel/third-party/README
+++ b/devel/third-party/README
@@@ -52,16 -22,17 +52,17 @@@ License: MI
* eyedropper.svg
Description: color picker icon
Origin: http://thenounproject.com/noun/eye-dropper/ (The Noun Project)
+ Author: The Noun Project
License: CC BY 3.0
-* jquery-1.11.3.js
+* jquery-3.4.1.js
Description: DOM manipulation
-Origin: http://code.jquery.com/jquery-1.11.3.js
+Origin: http://code.jquery.com/jquery-3.4.1.js
License: MIT
-* jquery-modal-0.5.2.js
-Description: modal popup dialogs
-Origin: https://github.com/kylefox/jquery-modal
+* jquery-jgrowl-1.4.5/
+Description: javascript notifications
+Origin: https://github.com/stanlemon/jGrowl/tree/1.4.5
License: MIT
* jquery-tablesorter-2.0.5b.js
diff --cc etc/RT_Config.pm.in
index 352fa73ea0,85b04929df..e9aba3fa32
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@@ -3204,11 -2370,21 +3204,22 @@@ giving quick access back to a search af
=cut
-Set($QuoteFolding, 1);
+Set($AssetHideSimpleSearch, 0);
-=item C<$QuoteWrapWidth>
++=item C<$AssetMultipleOwner>
+
-C<$QuoteWrapWidth> controls the number of columns to use when wrapping
-quoted text within transactions.
++By default an asset is limited to a single user as an owner. By setting
++this to a true value, you can allow multiple users and groups as owner.
++If you change this back to a false value while having multiple owners
++set on any assets, RT's behavior may be inconsistent.
+
+ =cut
+
-Set($QuoteWrapWidth, 70);
++Set($AssetMultipleOwner, 0);
+
=back
-
-=head1 Application logic
+=head2 Message box properties
=over 4
@@@ -3867,9 -3094,13 +3891,13 @@@ Set( %SMIME
CAPath => undef,
AcceptUntrustedCAs => undef,
Passphrase => undef,
+ OtherCertificatesToSend => undef,
+ CheckCRL => 0,
+ CheckOCSP => 0,
+ CheckRevocationDownloadTimeout => 30,
);
-=head2 GnuPG configuration
+=head3 GnuPG configuration
A full description of the (somewhat extensive) GnuPG integration can
be found by running the command `perldoc L<RT::Crypt::GnuPG>` (or
diff --cc lib/RT/Config.pm
index d12902ca57,8053db1de3..0e9152aaaf
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@@ -1496,465 -1251,7 +1526,468 @@@ our %META
ServiceAgreements => {
Type => 'HASH',
},
+ AssetHideSimpleSearch => {
+ Widget => '/Widgets/Form/Boolean',
+ },
++ AssetMultipleOwner => {
++ Widget => '/Widgets/Form/Boolean',
++ },
+ AssetShowSearchResultCount => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ AllowUserAutocompleteForUnprivileged => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ AlwaysDownloadAttachments => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ AmbiguousDayInFuture => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ AmbiguousDayInPast => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ ApprovalRejectionNotes => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ ArticleOnTicketCreate => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ AutoCreateNonExternalUsers => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ AutocompleteOwnersForSearch => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ CanonicalizeRedirectURLs => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ CanonicalizeURLsInFeeds => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ ChartsTimezonesInDB => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ CheckMoreMSMailHeaders => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ DateDayBeforeMonth => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ DisplayTotalTimeWorked => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ DontSearchFileAttachments => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ DropLongAttachments => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ EditCustomFieldsSingleColumn => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ EnableReminders => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ EnablePriorityAsString => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ ExternalStorageDirectLink => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ ForceApprovalsView => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ ForwardFromUser => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ Framebusting => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ HideArticleSearchOnReplyCreate => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ HideResolveActionsWithDependencies => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ HideTimeFieldsFromUnprivilegedUsers => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ LoopsToRTOwner => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ MessageBoxIncludeSignature => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ MessageBoxIncludeSignatureOnComment => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ OnlySearchActiveTicketsInSimpleSearch => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ ParseNewMessageForTicketCcs => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ PreferDateTimeFormatNatural => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ PreviewScripMessages => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ RecordOutgoingEmail => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ RestrictLoginReferrer => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ RestrictReferrer => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ SearchResultsAutoRedirect => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ SelfServiceUseDashboard => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ ShowBccHeader => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ ShowEditSystemConfig => {
+ Immutable => 1,
+ Widget => '/Widgets/Form/Boolean',
+ },
+ ShowEditLifecycleConfig => {
+ Immutable => 1,
+ Widget => '/Widgets/Form/Boolean',
+ },
+ ShowMoreAboutPrivilegedUsers => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ ShowRTPortal => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ ShowRemoteImages => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ ShowTransactionImages => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ StoreLoops => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ StrictLinkACL => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ SuppressInlineTextFiles => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ TreatAttachedEmailAsFiles => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ TruncateLongAttachments => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ TrustHTMLAttachments => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ UseFriendlyFromLine => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ UseFriendlyToLine => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ UseOriginatorHeader => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ UseSQLForACLChecks => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ UseTransactionBatch => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ ValidateUserEmailAddresses => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ WebFallbackToRTLogin => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ WebFlushDbCacheEveryRequest => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ WebHttpOnlyCookies => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ WebRemoteUserAuth => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ WebRemoteUserAutocreate => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ WebRemoteUserContinuous => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ WebRemoteUserGecos => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ WebSecureCookies => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ WikiImplicitLinks => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ HideOneTimeSuggestions => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ LinkArticlesOnInclude => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ SelfServiceCorrespondenceOnly => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ SelfServiceDownloadUserData => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ SelfServiceShowGroupTickets => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ SelfServiceShowArticleSearch => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ ShowSearchResultCount => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ AllowGroupAutocompleteForUnprivileged => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+
+ AttachmentListCount => {
+ Widget => '/Widgets/Form/Integer',
+ },
+ AutoLogoff => {
+ Widget => '/Widgets/Form/Integer',
+ },
+ BcryptCost => {
+ Widget => '/Widgets/Form/Integer',
+ },
+ DefaultSummaryRows => {
+ Widget => '/Widgets/Form/Integer',
+ },
+ DropdownMenuLimit => {
+ Widget => '/Widgets/Form/Integer',
+ },
+ ExternalStorageCutoffSize => {
+ Widget => '/Widgets/Form/Integer',
+ },
+ LogoutRefresh => {
+ Widget => '/Widgets/Form/Integer',
+ },
+ MaxAttachmentSize => {
+ Widget => '/Widgets/Form/Integer',
+ },
+ MaxFulltextAttachmentSize => {
+ Widget => '/Widgets/Form/Integer',
+ },
+ MinimumPasswordLength => {
+ Widget => '/Widgets/Form/Integer',
+ },
+ MoreAboutRequestorGroupsLimit => {
+ Widget => '/Widgets/Form/Integer',
+ },
+ TicketsItemMapSize => {
+ Widget => '/Widgets/Form/Integer',
+ },
+
+ AssetDefaultSearchResultOrderBy => {
+ Widget => '/Widgets/Form/String',
+ },
+ CanonicalizeEmailAddressMatch => {
+ Widget => '/Widgets/Form/String',
+ },
+ CanonicalizeEmailAddressReplace => {
+ Widget => '/Widgets/Form/String',
+ },
+ CommentAddress => {
+ Widget => '/Widgets/Form/String',
+ },
+ CorrespondAddress => {
+ Widget => '/Widgets/Form/String',
+ },
+ DashboardAddress => {
+ Widget => '/Widgets/Form/String',
+ },
+ DashboardSubject => {
+ Widget => '/Widgets/Form/String',
+ },
+ DefaultErrorMailPrecedence => {
+ Widget => '/Widgets/Form/String',
+ },
+ DefaultMailPrecedence => {
+ Widget => '/Widgets/Form/String',
+ },
+ DefaultSearchResultOrderBy => {
+ Widget => '/Widgets/Form/String',
+ },
+ EmailOutputEncoding => {
+ Widget => '/Widgets/Form/String',
+ },
+ FriendlyFromLineFormat => {
+ Widget => '/Widgets/Form/String',
+ },
+ FriendlyToLineFormat => {
+ Widget => '/Widgets/Form/String',
+ },
+ LDAPHost => {
+ Widget => '/Widgets/Form/String',
+ },
+ LDAPUser => {
+ Widget => '/Widgets/Form/String',
+ },
+ LDAPPassword => {
+ Widget => '/Widgets/Form/String',
+ Obfuscate => sub {
+ my ($config, $sources, $user) = @_;
+ return $user->loc('Password not printed');
+ },
+ },
+ LDAPBase => {
+ Widget => '/Widgets/Form/String',
+ },
+ LDAPGroupBase => {
+ Widget => '/Widgets/Form/String',
+ },
+ LogDir => {
+ Immutable => 1,
+ Widget => '/Widgets/Form/String',
+ },
+ LogToFileNamed => {
+ Immutable => 1,
+ Widget => '/Widgets/Form/String',
+ },
+ LogoAltText => {
+ Widget => '/Widgets/Form/String',
+ },
+ LogoLinkURL => {
+ Widget => '/Widgets/Form/String',
+ },
+ LogoURL => {
+ Widget => '/Widgets/Form/String',
+ },
+ OwnerEmail => {
+ Widget => '/Widgets/Form/String',
+ },
+ QuoteWrapWidth => {
+ Widget => '/Widgets/Form/Integer',
+ },
+ RedistributeAutoGeneratedMessages => {
+ Widget => '/Widgets/Form/String',
+ },
+ SelfServiceRequestUpdateQueue => {
+ Widget => '/Widgets/Form/String',
+ },
+ SendmailArguments => {
+ Widget => '/Widgets/Form/String',
+ },
+ SendmailBounceArguments => {
+ Widget => '/Widgets/Form/String',
+ },
+ SendmailPath => {
+ Widget => '/Widgets/Form/String',
+ },
+ SetOutgoingMailFrom => {
+ Widget => '/Widgets/Form/String',
+ },
+ Timezone => {
+ Widget => '/Widgets/Form/String',
+ },
+ VERPPrefix => {
+ Widget => '/Widgets/Form/String',
+ WidgetArguments => { Hints => 'rt-', },
+ },
+ VERPDomain => {
+ Widget => '/Widgets/Form/String',
+ WidgetArguments => {
+ Callback => sub { return { Hints => RT->Config->Get( 'Organization') } },
+ },
+ },
+ WebImagesURL => {
+ Widget => '/Widgets/Form/String',
+ },
+
+ AssetDefaultSearchResultOrder => {
+ Widget => '/Widgets/Form/Select',
+ WidgetArguments => { Values => [qw(ASC DESC)] },
+ },
+ LogToSyslog => {
+ Immutable => 1,
+ Widget => '/Widgets/Form/Select',
+ WidgetArguments => { Values => [qw(debug info notice warning error critical alert emergency)] },
+ },
+ LogToSTDERR => {
+ Immutable => 1,
+ Widget => '/Widgets/Form/Select',
+ WidgetArguments => { Values => [qw(debug info notice warning error critical alert emergency)] },
+ },
+ LogToFile => {
+ Immutable => 1,
+ Widget => '/Widgets/Form/Select',
+ WidgetArguments => { Values => [qw(debug info notice warning error critical alert emergency)] },
+ },
+ LogStackTraces => {
+ Immutable => 1,
+ Widget => '/Widgets/Form/Select',
+ WidgetArguments => { Values => [qw(debug info notice warning error critical alert emergency)] },
+ },
+ StatementLog => {
+ Widget => '/Widgets/Form/Select',
+ WidgetArguments => { Values => ['', qw(debug info notice warning error critical alert emergency)] },
+ },
+
+ DefaultCatalog => {
+ Widget => '/Widgets/Form/Select',
+ WidgetArguments => {
+ Description => 'Default catalog', #loc
+ Callback => sub {
+ my $ret = { Values => [], ValuesLabel => {} };
+ my $c = RT::Catalogs->new( $HTML::Mason::Commands::session{'CurrentUser'} );
+ $c->UnLimit;
+ while ( my $catalog = $c->Next ) {
+ next unless $catalog->CurrentUserHasRight("CreateAsset");
+ push @{ $ret->{Values} }, $catalog->Id;
+ $ret->{ValuesLabel}{ $catalog->Id } = $catalog->Name;
+ }
+ return $ret;
+ },
+ }
+ },
+ DefaultSearchResultOrder => {
+ Widget => '/Widgets/Form/Select',
+ WidgetArguments => { Values => [qw(ASC DESC)] },
+ },
+ LogToSyslogConf => {
+ Immutable => 1,
+ },
+ ShowMobileSite => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ StaticRoots => {
+ Type => 'ARRAY',
+ Immutable => 1,
+ },
+ EmailSubjectTagRegex => {
+ Immutable => 1,
+ },
+ ExtractSubjectTagMatch => {
+ Immutable => 1,
+ },
+ ExtractSubjectTagNoMatch => {
+ Immutable => 1,
+ },
+ WebNoAuthRegex => {
+ Immutable => 1,
+ },
+ SelfServiceRegex => {
+ Immutable => 1,
+ },
);
my %OPTIONS = ();
my @LOADED_CONFIGS = ();
diff --cc lib/RT/Interface/Web.pm
index 96cbd51251,698b7e5624..3170c298ba
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@@ -273,9 -266,15 +274,17 @@@ sub HandleRequest
require Module::Refresh;
Module::Refresh->refresh;
}
+ else {
+ require File::Spec;
+ my $mason_cache_created = ( stat File::Spec->catdir( $RT::MasonDataDir, 'obj' ) )[ 9 ] // '';
+ if ( ( $HTML::Mason::Commands::m->{rt_mason_cache_created} // '' ) ne $mason_cache_created ) {
+ $HTML::Mason::Commands::m->interp->flush_code_cache;
+ $HTML::Mason::Commands::m->{rt_mason_cache_created} = $mason_cache_created;
+ }
+ }
+ RT->Config->RefreshConfigFromDatabase();
+
$HTML::Mason::Commands::r->content_type("text/html; charset=utf-8");
$HTML::Mason::Commands::m->{'rt_base_time'} = [ Time::HiRes::gettimeofday() ];
@@@ -1920,11 -1898,41 +1948,46 @@@ sub RequestENV
return $name ? $env->{$name} : $env;
}
+sub ClientIsIE {
+ # IE 11.0 dropped "MSIE", so we can't use that alone
+ return RequestENV('HTTP_USER_AGENT') =~ m{MSIE|Trident/} ? 1 : 0;
+}
+
+ =head2 ClearMasonCache
+
+ Delete current mason cache.
+
+ =cut
+
+ sub ClearMasonCache {
+ require File::Path;
+ require File::Spec;
+ my $mason_obj_dir = File::Spec->catdir( $RT::MasonDataDir, 'obj' );
+
+ my $error;
+
+ # There is a race condition that other processes add new cache items while
+ # remove_tree is running, which could prevent it from deleting the whole "obj"
+ # directory with errors like "Directory not empty". Let's try for a few times
+ # here to get around it.
+
+ for ( 1 .. 10 ) {
+ last unless -e $mason_obj_dir;
+ File::Path::remove_tree( $mason_obj_dir, { safe => 1, error => \$error } );
+ }
+
+ if ( $error && @$error ) {
+
+ # Only one dir is specified, so there will be only one error if any
+ my ( $file, $message ) = %{ $error->[0] };
+ RT->Logger->error("Failed to clear mason cache: $file => $message");
+ return ( 0, HTML::Mason::Commands::loc( "Failed to clear mason cache: [_1] => [_2]", $file, $message ) );
+ }
+ else {
+ return ( 1, HTML::Mason::Commands::loc('Cache cleared') );
+ }
+ }
+
package HTML::Mason::Commands;
use vars qw/$r $m %session/;
@@@ -4984,104 -4708,29 +5044,108 @@@ Returns an array of results messages
=cut
-sub JSON {
- RT::Interface::Web::EncodeJSON(@_);
-}
+sub ProcessAuthToken {
+ my %args = (
+ ARGSRef => undef,
+ @_
+ );
+ my $args_ref = $args{ARGSRef};
-sub CSSClass {
- my $value = shift;
- return '' unless defined $value;
- $value =~ s/[^A-Za-z0-9_-]/_/g;
- return $value;
-}
+ my @results;
+ my $token = RT::AuthToken->new( $session{CurrentUser} );
-sub GetCustomFieldInputName {
- RT::Interface::Web::GetCustomFieldInputName(@_);
+ if ( $args_ref->{Create} ) {
+
+ # Don't require password for systems with some form of federated auth
+ # or if configured to not require a password
+ my %res = $session{'CurrentUser'}->CurrentUserRequireToSetPassword();
+ my $require_password = 1;
+ if ( RT->Config->Get('DisablePasswordForAuthToken') or not $res{'CanSet'}) {
+ $require_password = 0;
+ }
+
+ if ( !length( $args_ref->{Description} ) ) {
+ push @results, loc("Description cannot be blank.");
+ }
+ elsif ( $require_password && !length( $args_ref->{Password} ) ) {
+ push @results, loc("Please enter your current password.");
+ }
+ elsif ( $require_password && !$session{CurrentUser}->IsPassword( $args_ref->{Password} ) ) {
+ push @results, loc("Please enter your current password correctly.");
+ }
+ else {
+ my ( $ok, $msg, $auth_string ) = $token->Create(
+ Owner => $args_ref->{Owner},
+ Description => $args_ref->{Description},
+ );
+ if ($ok) {
+ push @results, $msg;
+ push @results,
+ loc(
+ '"[_1]" is your new authentication token. Treat it carefully like a password. Please save it now because you cannot access it again.',
+ $auth_string
+ );
+ }
+ else {
+ push @results, loc('Unable to create a new authentication token. Contact your RT administrator.');
+ RT->Logger->error('Unable to create authentication token: ' . $msg);
+ }
+ }
+ }
+ elsif ( $args_ref->{Update} || $args_ref->{Revoke} ) {
+
+ $token->Load( $args_ref->{Token} );
+ if ( $token->Id ) {
+ if ( $args_ref->{Update} ) {
+ if ( length( $args_ref->{Description} ) ) {
+ if ( $args_ref->{Description} ne $token->Description ) {
+ my ( $ok, $msg ) = $token->SetDescription( $args_ref->{Description} );
+ push @results, $msg;
+ }
+ }
+ else {
+ push @results, loc("Description cannot be blank.");
+ }
+ }
+ elsif ( $args_ref->{Revoke} ) {
+ my ( $ok, $msg ) = $token->Delete;
+ push @results, $msg;
+ }
+ }
+ else {
+ push @results, loc("Could not find token: [_1]", $args_ref->{Token});
+ }
+ }
+ return @results;
}
-sub GetCustomFieldInputNamePrefix {
- RT::Interface::Web::GetCustomFieldInputNamePrefix(@_);
+=head3 CachedCustomFieldValues FIELD
+
+Similar to FIELD->Values, but caches the return value of FIELD->Values
+in $m->notes in anticipation of it being used again.
+
+=cut
+
+sub CachedCustomFieldValues {
+ my $cf = shift;
+
+ my $key = 'CF-' . $cf->Id . '-Values';
+
+ if ($m->notes($key)) {
+ # Reset the iterator so we always start from the beginning
+ $m->notes($key)->GotoFirstItem;
+ return $m->notes($key);
+ }
+
+ # Wasn't in the cache; grab it and cache it.
+ $m->notes($key, $cf->Values);
+ return $m->notes($key);
}
+ sub PreprocessTimeUpdates {
+ RT::Interface::Web::PreprocessTimeUpdates(@_);
+ }
+
package RT::Interface::Web;
RT::Base->_ImportOverlays();
diff --cc lib/RT/Interface/Web/MenuBuilder.pm
index e40e26aa3d,0000000000..781565c043
mode 100644,000000..100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@@ -1,1699 -1,0 +1,1700 @@@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2021 Best Practical Solutions, LLC
+# <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+=head1 NAME
+
+RT::Interface::Web::MenuBuilder
+
+=cut
+
+use strict;
+use warnings;
+
+package RT::Interface::Web::MenuBuilder;
+
+sub loc { HTML::Mason::Commands::loc( @_ ); }
+
+sub QueryString {
+ my %args = @_;
+ my $u = URI->new();
+ $u->query_form(map { $_ => $args{$_} } sort keys %args);
+ return $u->query;
+}
+
+sub BuildMainNav {
+ my $request_path = shift;
+ my $top = shift;
+ my $widgets = shift;
+ my $page = shift;
+
+ my %args = ( @_ );
+
+ my $query_string = $args{QueryString};
+ my $query_args = $args{QueryArgs};
+
+ my $current_user = $HTML::Mason::Commands::session{CurrentUser};
+
+ if ($request_path =~ m{^/Asset/}) {
+ $widgets->child( asset_search => raw_html => $HTML::Mason::Commands::m->scomp('/Asset/Elements/Search') );
+ $widgets->child( create_asset => raw_html => $HTML::Mason::Commands::m->scomp('/Asset/Elements/CreateAsset') );
+ }
+ elsif ($request_path =~ m{^/Articles/}) {
+ $widgets->child( article_search => raw_html => $HTML::Mason::Commands::m->scomp('/Articles/Elements/GotoArticle') );
+ $widgets->child( create_article => raw_html => $HTML::Mason::Commands::m->scomp('/Articles/Elements/CreateArticleButton') );
+ } else {
+ $widgets->child( simple_search => raw_html => $HTML::Mason::Commands::m->scomp('SimpleSearch', Placeholder => loc('Search Tickets')) );
+ $widgets->child( create_ticket => raw_html => $HTML::Mason::Commands::m->scomp('CreateTicket') );
+ }
+
+ my $home = $top->child( home => title => loc('Homepage'), path => '/' );
+ $home->child( create_ticket => title => loc("Create Ticket"),
+ path => "/Ticket/Create.html" );
+
+ my $search = $top->child( search => title => loc('Search'), path => '/Search/Simple.html' );
+
+ my $tickets = $search->child( tickets => title => loc('Tickets'), path => '/Search/Build.html' );
+ $tickets->child( simple => title => loc('Simple Search'), path => "/Search/Simple.html" );
+ $tickets->child( new => title => loc('New Search'), path => "/Search/Build.html?NewQuery=1" );
+
+ my $recents = $tickets->child( recent => title => loc('Recently Viewed'));
+ for my $ticket ( $current_user->RecentlyViewedTickets ) {
+ my $title = $ticket->{subject} || loc( "(No subject)" );
+ if ( length $title > 50 ) {
+ $title = substr($title, 0, 47);
+ $title =~ s/\s+$//;
+ $title .= "...";
+ }
+ $title = "#$ticket->{id}: " . $title;
+ $recents->child( "$ticket->{id}" => title => $title, path => "/Ticket/Display.html?id=" . $ticket->{id} );
+ }
+
+ $search->child( articles => title => loc('Articles'), path => "/Articles/Article/Search.html" )
+ if $current_user->HasRight( Right => 'ShowArticlesMenu', Object => RT->System );
+
+ $search->child( users => title => loc('Users'), path => "/User/Search.html" );
+
+ $search->child( groups =>
+ title => loc('Groups'),
+ path => "/Group/Search.html",
+ description => 'Group search'
+ );
+
+ my $search_assets;
+ if ($HTML::Mason::Commands::session{CurrentUser}->HasRight( Right => 'ShowAssetsMenu', Object => RT->System )) {
+ $search_assets = $search->child( assets => title => loc("Assets"), path => "/Search/Build.html?Class=RT::Assets" );
+ if (!RT->Config->Get('AssetHideSimpleSearch')) {
+ $search_assets->child("asset_simple", title => loc("Simple Search"), path => "/Asset/Search/");
+ }
+ $search_assets->child("assetsql", title => loc("New Search"), path => "/Search/Build.html?Class=RT::Assets;NewQuery=1");
+ }
+
+
+ my $txns = $search->child( transactions => title => loc('Transactions'), path => '/Search/Build.html?Class=RT::Transactions;ObjectType=RT::Ticket' );
+ my $txns_tickets = $txns->child( tickets => title => loc('Tickets'), path => "/Search/Build.html?Class=RT::Transactions;ObjectType=RT::Ticket" );
+ $txns_tickets->child( new => title => loc('New Search'), path => "/Search/Build.html?Class=RT::Transactions;ObjectType=RT::Ticket;NewQuery=1" );
+
+ my $reports = $top->child( reports =>
+ title => loc('Reports'),
+ description => loc('Reports and Dashboards'),
+ path => loc('/Reports'),
+ );
+
+ unless ($HTML::Mason::Commands::session{'dashboards_in_menu'}) {
+ my $dashboards_in_menu = $current_user->UserObj->Preferences(
+ 'DashboardsInMenu',
+ {},
+ );
+
+ unless ($dashboards_in_menu->{dashboards}) {
+ my ($default_dashboards) =
+ RT::System->new( $current_user )
+ ->Attributes
+ ->Named('DashboardsInMenu');
+ if ($default_dashboards) {
+ $dashboards_in_menu = $default_dashboards->Content;
+ }
+ }
+
+ $HTML::Mason::Commands::session{'dashboards_in_menu'} = $dashboards_in_menu->{dashboards} || [];
+ }
+
+ my @dashboards;
+ for my $id ( @{$HTML::Mason::Commands::session{'dashboards_in_menu'}} ) {
+ my $dash = RT::Dashboard->new( $current_user );
+ my ( $status, $msg ) = $dash->LoadById($id);
+ if ( $status ) {
+ push @dashboards, $dash;
+ } else {
+ $RT::Logger->debug( "Failed to load dashboard $id: $msg, removing from menu" );
+ $home->RemoveDashboardMenuItem(
+ DashboardId => $id,
+ CurrentUser => $HTML::Mason::Commands::session{CurrentUser}->UserObj,
+ );
+ @{ $HTML::Mason::Commands::session{'dashboards_in_menu'} } =
+ grep { $_ != $id } @{ $HTML::Mason::Commands::session{'dashboards_in_menu'} };
+ }
+ }
+
+ if (@dashboards) {
+ for my $dash (@dashboards) {
+ $reports->child( 'dashboard-' . $dash->id,
+ title => $dash->Name,
+ path => '/Dashboards/' . $dash->id . '/' . $dash->Name
+ );
+ }
+ }
+
+ # Get the list of reports in the Reports menu
+ unless ( $HTML::Mason::Commands::session{'reports_in_menu'}
+ && ref( $HTML::Mason::Commands::session{'reports_in_menu'}) eq 'ARRAY'
+ && @{$HTML::Mason::Commands::session{'reports_in_menu'}}
+ ) {
+ my $reports_in_menu = $current_user->UserObj->Preferences(
+ 'ReportsInMenu',
+ {},
+ );
+ unless ( $reports_in_menu && ref $reports_in_menu eq 'ARRAY' ) {
+ my ($default_reports) =
+ RT::System->new( RT->SystemUser )
+ ->Attributes
+ ->Named('ReportsInMenu');
+ if ($default_reports) {
+ $reports_in_menu = $default_reports->Content;
+ }
+ else {
+ $reports_in_menu = [];
+ }
+ }
+
+ $HTML::Mason::Commands::session{'reports_in_menu'} = $reports_in_menu || [];
+ }
+
+ for my $report ( @{$HTML::Mason::Commands::session{'reports_in_menu'}} ) {
+ $reports->child( $report->{id} =>
+ title => $report->{title},
+ path => $report->{path},
+ );
+ }
+
+ $reports->child( edit => title => loc('Update This Menu'), path => '/Prefs/DashboardsInMenu.html' );
+ $reports->child( more => title => loc('All Dashboards'), path => '/Dashboards/index.html' );
+ my $dashboard = RT::Dashboard->new( $current_user );
+ if ( $dashboard->CurrentUserCanCreateAny ) {
+ $reports->child('dashboard_create' => title => loc('New Dashboard'), path => "/Dashboards/Modify.html?Create=1" );
+ }
+
+
+ if ($current_user->HasRight( Right => 'ShowArticlesMenu', Object => RT->System )) {
+ my $articles = $top->child( articles => title => loc('Articles'), path => "/Articles/index.html");
+ $articles->child( articles => title => loc('Overview'), path => "/Articles/index.html" );
+ $articles->child( topics => title => loc('Topics'), path => "/Articles/Topics.html" );
+ $articles->child( create => title => loc('Create'), path => "/Articles/Article/PreCreate.html" );
+ $articles->child( search => title => loc('Search'), path => "/Articles/Article/Search.html" );
+ }
+
+ if ($current_user->HasRight( Right => 'ShowAssetsMenu', Object => RT->System )) {
+ my $assets = $top->child(
+ "assets",
+ title => loc("Assets"),
+ path => RT->Config->Get('AssetHideSimpleSearch') ? "/Search/Build.html?Class=RT::Assets;NewQuery=1" : "/Asset/Search/",
+ );
+ $assets->child( "create", title => loc("Create"), path => "/Asset/Create.html" );
+ if (!RT->Config->Get('AssetHideSimpleSearch')) {
+ $assets->child( "simple_search", title => loc("Simple Search"), path => "/Asset/Search/" );
+ }
+ $assets->child( "search", title => loc("New Search"), path => "/Search/Build.html?Class=RT::Assets;NewQuery=1" );
+ }
+
+ my $tools = $top->child( tools => title => loc('Tools'), path => '/Tools/index.html' );
+
+ $tools->child( my_day =>
+ title => loc('My Day'),
+ description => loc('Easy updating of your open tickets'),
+ path => '/Tools/MyDay.html',
+ );
+
+ if ( RT->Config->Get('EnableReminders') ) {
+ $tools->child( my_reminders =>
+ title => loc('My Reminders'),
+ description => loc('Easy viewing of your reminders'),
+ path => '/Tools/MyReminders.html',
+ );
+ }
+
+ if ( $current_user->HasRight( Right => 'ShowApprovalsTab', Object => RT->System ) ) {
+ $tools->child( approval =>
+ title => loc('Approval'),
+ description => loc('My Approvals'),
+ path => '/Approvals/',
+ );
+ }
+
+ if ( $current_user->HasRight( Right => 'ShowConfigTab', Object => RT->System ) )
+ {
+ _BuildAdminMenu( $request_path, $top, $widgets, $page, %args );
+ }
+
+ my $username = '<span class="current-user">'
+ . $HTML::Mason::Commands::m->interp->apply_escapes($current_user->Name, 'h')
+ . '</span>';
+ my $about_me = $top->child( 'preferences' =>
+ title => loc('Logged in as [_1]', $username),
+ escape_title => 0,
+ path => '/User/Summary.html?id=' . $current_user->id,
+ sort_order => 99,
+ );
+
+ $about_me->child( rt_name => title => loc("RT for [_1]", RT->Config->Get('rtname')), path => '/' );
+
+ if ( $current_user->UserObj
+ && $current_user->HasRight( Right => 'ModifySelf', Object => RT->System )) {
+ my $settings = $about_me->child( settings => title => loc('Settings'), path => '/Prefs/Other.html' );
+ $settings->child( options => title => loc('Preferences'), path => '/Prefs/Other.html' );
+ $settings->child( about_me => title => loc('About me'), path => '/Prefs/AboutMe.html' );
+ if ( $current_user->HasRight( Right => 'ManageAuthTokens', Object => RT->System ) ) {
+ $settings->child( auth_tokens => title => loc('Auth Tokens'), path => '/Prefs/AuthTokens.html' );
+ }
+ $settings->child( search_options => title => loc('Search options'), path => '/Prefs/SearchOptions.html' );
+ $settings->child( myrt => title => loc('RT at a glance'), path => '/Prefs/MyRT.html' );
+ $settings->child( dashboards_in_menu =>
+ title => loc('Modify Reports menu'),
+ path => '/Prefs/DashboardsInMenu.html',
+ );
+ $settings->child( queue_list => title => loc('Queue list'), path => '/Prefs/QueueList.html' );
+
+ my $search_menu = $settings->child( 'saved-searches' => title => loc('Saved Searches') );
+ my $searches = [ $HTML::Mason::Commands::m->comp( "/Search/Elements/SearchesForObject",
+ Object => RT::System->new( $current_user )) ];
+ my $i = 0;
+
+ for my $search (@$searches) {
+ $search_menu->child( "search-" . $i++ =>
+ title => $search->[1],
+ path => "/Prefs/Search.html?"
+ . QueryString( name => ref( $search->[2] ) . '-' . $search->[2]->Id ),
+ );
+
+ }
+
+ if ( $request_path =~ qr{/Prefs/(?:SearchOptions|CustomDateRanges)\.html} ) {
+ $page->child(
+ search_options => title => loc('Search Preferences'),
+ path => "/Prefs/SearchOptions.html"
+ );
+ $page->child(
+ custom_date_ranges => title => loc('Custom Date Ranges'),
+ path => "/Prefs/CustomDateRanges.html"
+ )
+ }
+
+ if ( $request_path =~ m{^/Prefs/AuthTokens\.html} ) {
+ $page->child( create_auth_token => title => loc('Create'),
+ raw_html => q[<a class="btn menu-item" href="#create-auth-token" data-toggle="modal" rel="modal:open">].loc("Create")."</a>"
+ );
+ }
+ }
+ my $logout_url = RT->Config->Get('LogoutURL');
+ if ( $current_user->Name
+ && ( !RT->Config->Get('WebRemoteUserAuth')
+ || RT->Config->Get('WebFallbackToRTLogin') )) {
+ $about_me->child( logout => title => loc('Logout'), path => $logout_url );
+ }
+ if ( $request_path =~ m{^/Dashboards/(\d+)?}) {
+ if ( my $id = ( $1 || $HTML::Mason::Commands::DECODED_ARGS->{'id'} ) ) {
+ my $obj = RT::Dashboard->new( $current_user );
+ $obj->LoadById($id);
+ if ( $obj and $obj->id ) {
+ $page->child( basics => title => loc('Basics'), path => "/Dashboards/Modify.html?id=" . $obj->id);
+ $page->child( content => title => loc('Content'), path => "/Dashboards/Queries.html?id=" . $obj->id);
+ $page->child( subscription => title => loc('Subscription'), path => "/Dashboards/Subscription.html?id=" . $obj->id)
+ if $obj->CurrentUserCanSubscribe;
+ $page->child( show => title => loc('Show'), path => "/Dashboards/" . $obj->id . "/" . $obj->Name)
+ }
+ }
+ }
+
+
+ my $search_results_page_menu;
+ if ( $request_path =~ m{^/Ticket/} ) {
+ if ( ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} || '' ) =~ /^(\d+)$/ ) {
+ my $id = $1;
+ my $obj = RT::Ticket->new( $current_user );
+ $obj->Load($id);
+
+ if ( $obj and $obj->id ) {
+ my $actions = $page->child( actions => title => loc('Actions'), sort_order => 95 );
+
+ my %can = %{ $obj->CurrentUser->PrincipalObj->HasRights( Object => $obj ) };
+ # since CurrentUserCanSetOwner returns ($ok, $msg), the parens ensure that $can{} gets $ok
+ ( $can{'_ModifyOwner'} ) = $obj->CurrentUserCanSetOwner();
+ my $can = sub {
+ unless ($_[0] eq 'ExecuteCode') {
+ return $can{$_[0]} || $can{'SuperUser'};
+ } else {
+ return !RT->Config->Get('DisallowExecuteCode')
+ && ( $can{'ExecuteCode'} || $can{'SuperUser'} );
+ }
+ };
+
+ $page->child( bookmark => raw_html => $HTML::Mason::Commands::m->scomp( '/Ticket/Elements/Bookmark', id => $id ), sort_order => 98 );
+
+ if ($can->('ModifyTicket')) {
+ $page->child( timer => raw_html => $HTML::Mason::Commands::m->scomp( '/Ticket/Elements/PopupTimerLink', id => $id ), sort_order => 99 );
+ }
+
+ $page->child( display => title => loc('Display'), path => "/Ticket/Display.html?id=" . $id );
+ $page->child( history => title => loc('History'), path => "/Ticket/History.html?id=" . $id );
+
+ # comment out until we can do it for an individual custom field
+ #if ( $can->('ModifyTicket') || $can->('ModifyCustomField') ) {
+ $page->child( basics => title => loc('Basics'), path => "/Ticket/Modify.html?id=" . $id );
+
+ #}
+
+ if ( $can->('ModifyTicket') || $can->('_ModifyOwner') || $can->('Watch') || $can->('WatchAsAdminCc') ) {
+ $page->child( people => title => loc('People'), path => "/Ticket/ModifyPeople.html?id=" . $id );
+ }
+
+ if ( $can->('ModifyTicket') ) {
+ $page->child( dates => title => loc('Dates'), path => "/Ticket/ModifyDates.html?id=" . $id );
+ $page->child( links => title => loc('Links'), path => "/Ticket/ModifyLinks.html?id=" . $id );
+ }
+
+ #if ( $can->('ModifyTicket') || $can->('ModifyCustomField') || $can->('_ModifyOwner') ) {
+ $page->child( jumbo => title => loc('Jumbo'), path => "/Ticket/ModifyAll.html?id=" . $id );
+ #}
+
+ if ( RT->Config->Get('EnableReminders') ) {
+ $page->child( reminders => title => loc('Reminders'), path => "/Ticket/Reminders.html?id=" . $id );
+ }
+
+ if ( $can->('ModifyTicket') or $can->('ReplyToTicket') ) {
+ $actions->child( reply => title => loc('Reply'), path => "/Ticket/Update.html?Action=Respond;id=" . $id );
+ }
+
+ if ( $can->('ModifyTicket') or $can->('CommentOnTicket') ) {
+ $actions->child( comment => title => loc('Comment'), path => "/Ticket/Update.html?Action=Comment;id=" . $id );
+ }
+
+ if ( $can->('ForwardMessage') ) {
+ $actions->child( forward => title => loc('Forward'), path => "/Ticket/Forward.html?id=" . $id );
+ }
+
+ my $hide_resolve_with_deps = RT->Config->Get('HideResolveActionsWithDependencies')
+ && $obj->HasUnresolvedDependencies;
+
+ my $current = $obj->Status;
+ my $lifecycle = $obj->LifecycleObj;
+ my $i = 1;
+ foreach my $info ( $lifecycle->Actions($current) ) {
+ my $next = $info->{'to'};
+ next unless $lifecycle->IsTransition( $current => $next );
+
+ my $check = $lifecycle->CheckRight( $current => $next );
+ next unless $can->($check);
+
+ next if $hide_resolve_with_deps
+ && $lifecycle->IsInactive($next)
+ && !$lifecycle->IsInactive($current);
+
+ my $action = $info->{'update'} || '';
+ my $url = '/Ticket/';
+ $url .= "Update.html?". QueryString(
+ $action
+ ? (Action => $action)
+ : (SubmitTicket => 1, Status => $next),
+ DefaultStatus => $next,
+ id => $id,
+ );
+ my $key = $info->{'label'} || ucfirst($next);
+ $actions->child( $key => title => loc( $key ), path => $url);
+ }
+
+ my ($can_take, $tmsg) = $obj->CurrentUserCanSetOwner( Type => 'Take' );
+ my ($can_steal, $smsg) = $obj->CurrentUserCanSetOwner( Type => 'Steal' );
+ my ($can_untake, $umsg) = $obj->CurrentUserCanSetOwner( Type => 'Untake' );
+ if ( $can_take ){
+ $actions->child( take => title => loc('Take'), path => "/Ticket/Display.html?Action=Take;id=" . $id );
+ }
+ elsif ( $can_steal ){
+ $actions->child( steal => title => loc('Steal'), path => "/Ticket/Display.html?Action=Steal;id=" . $id );
+ }
+ elsif ( $can_untake ){
+ $actions->child( untake => title => loc('Untake'), path => "/Ticket/Display.html?Action=Untake;id=" . $id );
+ }
+
+ # TODO needs a "Can extract article into a class applied to this queue" check
+ $actions->child( 'extract-article' =>
+ title => loc('Extract Article'),
+ path => "/Articles/Article/ExtractIntoClass.html?Ticket=".$obj->id,
+ ) if $current_user->HasRight( Right => 'ShowArticlesMenu', Object => RT->System );
+
+ $actions->child( 'edit_assets' =>
+ title => loc('Edit Assets'),
+ path => "/Asset/Search/Bulk.html?Query=Linked=" . $obj->id,
+ ) if $can->('ModifyTicket')
+ && $HTML::Mason::Commands::session{CurrentUser}->HasRight( Right => 'ShowAssetsMenu', Object => RT->System );
+
+ if ( defined $HTML::Mason::Commands::session{"collection-RT::Tickets"} ) {
+ # we have to update session data if we get new ItemMap
- my $updatesession = 1 unless ( $HTML::Mason::Commands::session{"collection-RT::Tickets"}->{'item_map'} );
++ my $updatesession;
++ $updatesession = 1 unless ( $HTML::Mason::Commands::session{"collection-RT::Tickets"}->{'item_map'} );
+
+ my $item_map = $HTML::Mason::Commands::session{"collection-RT::Tickets"}->ItemMap;
+
+ if ($updatesession) {
+ $HTML::Mason::Commands::session{"collection-RT::Tickets"}->PrepForSerialization();
+ }
+
+ my $search = $search_results_page_menu = $HTML::Mason::Commands::m->notes('search-results-page-menu', RT::Interface::Web::Menu->new());
+
+ # Don't display prev links if we're on the first ticket
+ if ( $item_map->{$id}->{prev} ) {
+ $search->child(
+ first => title => q{<span class="fas fa-angle-double-left"></span>},
+ escape_title => 0,
+ class => "nav",
+ path => "/Ticket/Display.html?id=" . $item_map->{first},
+ attributes => {
+ 'data-toggle' => 'tooltip',
+ 'data-original-title' => loc('First'),
+ alt => loc('First'),
+ },
+ );
+ $search->child(
+ prev => title => q{<span class="fas fa-angle-left"></span>},
+ escape_title => 0,
+ class => "nav",
+ path => "/Ticket/Display.html?id=" . $item_map->{$id}->{prev},
+ attributes => {
+ 'data-toggle' => 'tooltip',
+ 'data-original-title' => loc('Prev'),
+ alt => loc('Prev'),
+ },
+ );
+ }
+ # Don't display next links if we're on the last ticket
+ if ( $item_map->{$id}->{next} ) {
+ $search->child(
+ next => title => q{<span class="fas fa-angle-right"></span>},
+ escape_title => 0,
+ class => "nav",
+ path => "/Ticket/Display.html?id=" . $item_map->{$id}->{next},
+ attributes => {
+ 'data-toggle' => 'tooltip',
+ 'data-original-title' => loc('Next'),
+ alt => loc('Next'),
+ },
+ );
+ if ( $item_map->{last} ) {
+ $search->child(
+ last => title => q{<span class="fas fa-angle-double-right"></span>},
+ escape_title => 0,
+ class => "nav",
+ path => "/Ticket/Display.html?id=" . $item_map->{last},
+ attributes => {
+ 'data-toggle' => 'tooltip',
+ 'data-original-title' => loc('Last'),
+ alt => loc('Last'),
+ },
+ );
+ }
+ }
+ }
+ }
+ }
+ }
+
+ # display "View ticket" link in transactions
+ if ( $request_path =~ m{^/Transaction/Display.html} ) {
+ if ( ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} || '' ) =~ /^(\d+)$/ ) {
+ my $txn_id = $1;
+ my $txn = RT::Transaction->new( $current_user );
+ $txn->Load( $txn_id );
+ my $object = $txn->Object;
+ if ( $object->Id ) {
+ my $object_type = $object->RecordType;
+ if ( $object_type eq 'Ticket' ) {
+ $page->child( view_ticket => title => loc("View ticket"), path => "/Ticket/Display.html?id=" . $object->Id );
+ }
+ }
+ }
+ }
+
+ # Scope here so we can share in the Privileged callback
+ my $args = '';
+ my $has_query = '';
+ if (
+ (
+ $request_path =~ m{^/(?:Ticket|Transaction|Search)/}
+ && $request_path !~ m{^/Search/Simple\.html}
+ )
+ || ( $request_path =~ m{^/Search/Simple\.html}
+ && $HTML::Mason::Commands::DECODED_ARGS->{'q'} )
+
+ # TODO: Asset simple search and SQL search don't share query, we
+ # can't simply link to SQL search page on asset pages without
+ # identifying if it's from simple search or SQL search. For now,
+ # show "Current Search" only if asset simple search is disabled.
+
+ || ( $search_assets && $request_path =~ m{^/Asset/(?!Search/)} && RT->Config->Get('AssetHideSimpleSearch') )
+ )
+ {
+ my $class = $HTML::Mason::Commands::DECODED_ARGS->{Class}
+ || ( $request_path =~ m{^/(Transaction|Ticket|Asset)/} ? "RT::$1s" : 'RT::Tickets' );
+
+ my $search;
+ if ( $class eq 'RT::Tickets' ) {
+ $search = $top->child('search')->child('tickets');
+ }
+ elsif ( $class eq 'RT::Assets' ) {
+ $search = $search_assets;
+ }
+ else {
+ $search = $txns_tickets;
+ }
+
+ my $hash_name = join '-', 'CurrentSearchHash', $class,
+ $HTML::Mason::Commands::DECODED_ARGS->{ObjectType} || ( $class eq 'RT::Transactions' ? 'RT::Ticket' : () );
+ my $current_search = $HTML::Mason::Commands::session{$hash_name} || {};
+ my $search_id = $HTML::Mason::Commands::DECODED_ARGS->{'SavedSearchLoad'} || $HTML::Mason::Commands::DECODED_ARGS->{'SavedSearchId'} || $current_search->{'SearchId'} || '';
+ my $chart_id = $HTML::Mason::Commands::DECODED_ARGS->{'SavedChartSearchId'} || $current_search->{SavedChartSearchId};
+
+ $has_query = 1 if ( $HTML::Mason::Commands::DECODED_ARGS->{'Query'} or $current_search->{'Query'} );
+
+ my %query_args;
+ my %fallback_query_args = (
+ SavedSearchId => ( $search_id eq 'new' ) ? undef : $search_id,
+ SavedChartSearchId => $chart_id,
+ (
+ map {
+ my $p = $_;
+ $p => $HTML::Mason::Commands::DECODED_ARGS->{$p} || $current_search->{$p}
+ } qw(Query Format OrderBy Order Page Class ObjectType ResultPage ExtraQueryParams),
+ ),
+ RowsPerPage => (
+ defined $HTML::Mason::Commands::DECODED_ARGS->{'RowsPerPage'}
+ ? $HTML::Mason::Commands::DECODED_ARGS->{'RowsPerPage'}
+ : $current_search->{'RowsPerPage'}
+ ),
+ );
+
+ if ( my $extra_params = $fallback_query_args{ExtraQueryParams} ) {
+ for my $param ( ref $extra_params eq 'ARRAY' ? @$extra_params : $extra_params ) {
+ $fallback_query_args{$param}
+ = $HTML::Mason::Commands::DECODED_ARGS->{$param} || $current_search->{$param};
+ }
+ }
+
+ $fallback_query_args{Class} ||= $class;
+ $fallback_query_args{ObjectType} ||= 'RT::Ticket' if $class eq 'RT::Transactions';
+
+ if ($query_string) {
+ $args = '?' . $query_string;
+ }
+ else {
+ my %final_query_args = ();
+ # key => callback to avoid unnecessary work
+
+ if ( my $extra_params = $query_args->{ExtraQueryParams} ) {
+ $final_query_args{ExtraQueryParams} = $extra_params;
+ for my $param ( ref $extra_params eq 'ARRAY' ? @$extra_params : $extra_params ) {
+ $final_query_args{$param} = $query_args->{$param};
+ }
+ }
+
+ for my $param (keys %fallback_query_args) {
+ $final_query_args{$param} = defined($query_args->{$param})
+ ? $query_args->{$param}
+ : $fallback_query_args{$param};
+ }
+
+ for my $field (qw(Order OrderBy)) {
+ if ( ref( $final_query_args{$field} ) eq 'ARRAY' ) {
+ $final_query_args{$field} = join( "|", @{ $final_query_args{$field} } );
+ } elsif (not defined $final_query_args{$field}) {
+ delete $final_query_args{$field};
+ }
+ else {
+ $final_query_args{$field} ||= '';
+ }
+ }
+
+ $args = '?' . QueryString(%final_query_args);
+ }
+
+ my $current_search_menu;
+ if ( $class eq 'RT::Tickets' && $request_path =~ m{^/Ticket}
+ || $class eq 'RT::Transactions' && $request_path =~ m{^/Transaction}
+ || $class eq 'RT::Assets' && $request_path =~ m{^/Asset/(?!Search/)} )
+ {
+ $current_search_menu = $search->child( current_search => title => loc('Current Search') );
+ $current_search_menu->path("/Search/Results.html$args") if $has_query;
+
+ if ( $search_results_page_menu && $has_query ) {
+ $search_results_page_menu->child(
+ current_search => title => q{<span class="fas fa-list"></span>},
+ escape_title => 0,
+ sort_order => -1,
+ path => "/Search/Results.html$args",
+ attributes => {
+ 'data-toggle' => 'tooltip',
+ 'data-original-title' => loc('Return to Search Results'),
+ alt => loc('Return to Search Results'),
+ },
+ );
+ }
+ }
+ else {
+ $current_search_menu = $page;
+ }
+
+ $current_search_menu->child( edit_search =>
+ title => loc('Edit Search'), path => "/Search/Build.html" . ( ($has_query) ? $args : '' ) );
+ if ( $current_user->HasRight( Right => 'ShowSearchAdvanced', Object => RT->System ) ) {
+ $current_search_menu->child( advanced => title => loc('Advanced'), path => "/Search/Edit.html$args" );
+ }
+ if ($has_query) {
+ my $result_page = $HTML::Mason::Commands::DECODED_ARGS->{ResultPage};
+ if ( $result_page ) {
+ if ( my $web_path = RT->Config->Get('WebPath') ) {
+ $result_page =~ s!^$web_path!!;
+ }
+ }
+ else {
+ $result_page = '/Search/Results.html';
+ }
+
+ $current_search_menu->child( results => title => loc('Show Results'), path => "$result_page$args" );
+ }
+
+ if ( $has_query ) {
+ if ( $class eq 'RT::Tickets' ) {
+ if ( $current_user->HasRight( Right => 'ShowSearchBulkUpdate', Object => RT->System ) ) {
+ $current_search_menu->child( bulk => title => loc('Bulk Update'), path => "/Search/Bulk.html$args" );
+ }
+ $current_search_menu->child( chart => title => loc('Chart'), path => "/Search/Chart.html$args" );
+ }
+ elsif ( $class eq 'RT::Assets' ) {
+ $current_search_menu->child( bulk => title => loc('Bulk Update'), path => "/Asset/Search/Bulk.html$args" );
+ }
+
+ my $more = $current_search_menu->child( more => title => loc('Feeds') );
+
+ $more->child( spreadsheet => title => loc('Spreadsheet'), path => "/Search/Results.tsv$args" );
+
+ if ( $class eq 'RT::Tickets' ) {
+ my %rss_data
+ = map { $_ => $query_args->{$_} || $fallback_query_args{$_} || '' } qw(Query Order OrderBy);
+ my $RSSQueryString = "?"
+ . QueryString(
+ Query => $rss_data{Query},
+ Order => $rss_data{Order},
+ OrderBy => $rss_data{OrderBy}
+ );
+ my $RSSPath = join '/', map $HTML::Mason::Commands::m->interp->apply_escapes( $_, 'u' ),
+ $current_user->UserObj->Name,
+ $current_user->UserObj->GenerateAuthString(
+ $rss_data{Query} . $rss_data{Order} . $rss_data{OrderBy} );
+
+ $more->child( rss => title => loc('RSS'), path => "/NoAuth/rss/$RSSPath/$RSSQueryString" );
+ my $ical_path = join '/', map $HTML::Mason::Commands::m->interp->apply_escapes( $_, 'u' ),
+ $current_user->UserObj->Name,
+ $current_user->UserObj->GenerateAuthString( $rss_data{Query} ),
+ $rss_data{Query};
+ $more->child( ical => title => loc('iCal'), path => '/NoAuth/iCal/' . $ical_path );
+
+ #XXX TODO better abstraction of SuperUser right check
+ if ( $current_user->HasRight( Right => 'SuperUser', Object => RT->System ) ) {
+ my $shred_args = QueryString(
+ Search => 1,
+ Plugin => 'Tickets',
+ 'Tickets:query' => $rss_data{'Query'},
+ 'Tickets:limit' => $query_args->{'Rows'},
+ );
+
+ $more->child(
+ shredder => title => loc('Shredder'),
+ path => '/Admin/Tools/Shredder/?' . $shred_args
+ );
+ }
+ }
+ }
+ }
+
+ if ( $request_path =~ m{^/Article/} ) {
+ if ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} && $HTML::Mason::Commands::DECODED_ARGS->{'id'} =~ /^\d+$/ ) {
+ my $id = $HTML::Mason::Commands::DECODED_ARGS->{'id'};
+ $page->child( display => title => loc('Display'), path => "/Articles/Article/Display.html?id=".$id );
+ $page->child( history => title => loc('History'), path => "/Articles/Article/History.html?id=".$id );
+ $page->child( modify => title => loc('Modify'), path => "/Articles/Article/Edit.html?id=".$id );
+ }
+ }
+
+ if ( $request_path =~ m{^/Articles/} ) {
+ $page->child( search => title => loc("Search"), path => "/Articles/Article/Search.html" );
+ if ( $request_path =~ m{^/Articles/Article/} and ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} || '' ) =~ /^(\d+)$/ ) {
+ my $id = $1;
+ my $obj = RT::Article->new( $current_user );
+ $obj->Load($id);
+
+ if ( $obj and $obj->id ) {
+ $page->child( display => title => loc("Display"), path => "/Articles/Article/Display.html?id=" . $id );
+ $page->child( history => title => loc('History'), path => '/Articles/Article/History.html?id=' . $id );
+
+ if ( $obj->CurrentUserHasRight('ModifyArticle') ) {
+ $page->child(modify => title => loc('Modify'), path => '/Articles/Article/Edit.html?id=' . $id );
+ }
+ }
+ }
+
+ }
+
+ if ($request_path =~ m{^/Asset/} and $HTML::Mason::Commands::DECODED_ARGS->{id} and $HTML::Mason::Commands::DECODED_ARGS->{id} !~ /\D/) {
+ _BuildAssetMenu( $request_path, $top, $widgets, $page, %args );
+ } elsif ( $request_path =~ m{^/Asset/Search/(?:index\.html)?$}
+ || ( $request_path =~ m{^/Asset/Search/Bulk\.html$} && $HTML::Mason::Commands::DECODED_ARGS->{Catalog} ) ) {
+ my %search = map @{$_},
+ grep defined $_->[1] && length $_->[1],
+ map {ref $HTML::Mason::Commands::DECODED_ARGS->{$_} ? [$_, $HTML::Mason::Commands::DECODED_ARGS->{$_}[0]] : [$_, $HTML::Mason::Commands::DECODED_ARGS->{$_}] }
+ grep /^(?:q|SearchAssets|!?(Name|Description|Catalog|Status|Role\..+|CF\..+)|Order(?:By)?|Page)$/,
+ keys %$HTML::Mason::Commands::DECODED_ARGS;
+
+ if ( $request_path =~ /Bulk/) {
+ $page->child('search',
+ title => loc('Show Results'),
+ path => '/Asset/Search/?' . (keys %search ? QueryString(%search) : ''),
+ );
+ } else {
+ $page->child('bulk',
+ title => loc('Bulk Update'),
+ path => '/Asset/Search/Bulk.html?' . (keys %search ? QueryString(%search) : ''),
+ );
+ }
+
+ $page->child('csv',
+ title => loc('Download Spreadsheet'),
+ path => '/Search/Results.tsv?' . QueryString(%search, Class => 'RT::Assets'),
+ );
+ } elsif ($request_path =~ m{^/Asset/Search/}) {
+ my %search = map @{$_},
+ grep defined $_->[1] && length $_->[1],
+ map {ref $HTML::Mason::Commands::DECODED_ARGS->{$_} ? [$_, $HTML::Mason::Commands::DECODED_ARGS->{$_}[0]] : [$_, $HTML::Mason::Commands::DECODED_ARGS->{$_}] }
+ grep /^(?:q|SearchAssets|!?(Name|Description|Catalog|Status|Role\..+|CF\..+)|Order(?:By)?|Page)$/,
+ keys %$HTML::Mason::Commands::DECODED_ARGS;
+
+ my $current_search = $HTML::Mason::Commands::session{"CurrentSearchHash-RT::Assets"} || {};
+ my $search_id = $HTML::Mason::Commands::DECODED_ARGS->{'SavedSearchLoad'} || $HTML::Mason::Commands::DECODED_ARGS->{'SavedSearchId'} || $current_search->{'SearchId'} || '';
+ my $args = '';
+ my $has_query;
+ $has_query = 1 if ( $HTML::Mason::Commands::DECODED_ARGS->{'Query'} or $current_search->{'Query'} );
+
+ my %query_args;
+ my %fallback_query_args = (
+ Class => 'RT::Assets',
+ SavedSearchId => ( $search_id eq 'new' ) ? undef : $search_id,
+ (
+ map {
+ my $p = $_;
+ $p => $HTML::Mason::Commands::DECODED_ARGS->{$p} || $current_search->{$p}
+ } qw(Query Format OrderBy Order Page)
+ ),
+ RowsPerPage => (
+ defined $HTML::Mason::Commands::DECODED_ARGS->{'RowsPerPage'}
+ ? $HTML::Mason::Commands::DECODED_ARGS->{'RowsPerPage'}
+ : $current_search->{'RowsPerPage'}
+ ),
+ );
+
+ if ($query_string) {
+ $args = '?' . $query_string;
+ }
+ else {
+ my %final_query_args = ();
+ # key => callback to avoid unnecessary work
+
+ for my $param (keys %fallback_query_args) {
+ $final_query_args{$param} = defined($query_args->{$param})
+ ? $query_args->{$param}
+ : $fallback_query_args{$param};
+ }
+
+ for my $field (qw(Order OrderBy)) {
+ if ( ref( $final_query_args{$field} ) eq 'ARRAY' ) {
+ $final_query_args{$field} = join( "|", @{ $final_query_args{$field} } );
+ } elsif (not defined $final_query_args{$field}) {
+ delete $final_query_args{$field};
+ }
+ else {
+ $final_query_args{$field} ||= '';
+ }
+ }
+
+ $args = '?' . QueryString(%final_query_args);
+ }
+
+ $page->child('edit_search',
+ title => loc('Edit Search'),
+ path => '/Search/Build.html' . $args,
+ );
+ $page->child( advanced => title => loc('Advanced'), path => '/Search/Edit.html' . $args );
+ if ($has_query) {
+ $page->child( results => title => loc('Show Results'), path => '/Search/Results.html' . $args );
+ $page->child('bulk',
+ title => loc('Bulk Update'),
+ path => '/Asset/Search/Bulk.html' . $args,
+ );
+ my $more = $page->child( more => title => loc('Feeds') );
+ $more->child( spreadsheet => title => loc('Spreadsheet'), path => "/Search/Results.tsv$args" );
+ }
+ } elsif ($request_path =~ m{^/Admin/Global/CustomFields/Catalog-Assets\.html$}) {
+ $page->child("create", title => loc("Create New"), path => "/Admin/CustomFields/Modify.html?Create=1;LookupType=" . RT::Asset->CustomFieldLookupType);
+ } elsif ($request_path =~ m{^/Admin/CustomFields(/|/index\.html)?$}
+ and $HTML::Mason::Commands::DECODED_ARGS->{'Type'} and $HTML::Mason::Commands::DECODED_ARGS->{'Type'} eq RT::Asset->CustomFieldLookupType) {
+ $page->child("create")->path( $page->child("create")->path . ";LookupType=" . RT::Asset->CustomFieldLookupType );
+ } elsif ($request_path =~ m{^/Admin/Assets/Catalogs/}) {
+ my $actions = $request_path =~ m{/((index|Create)\.html)?$}
+ ? $page
+ : $page->child("catalogs", title => loc("Catalogs"), path => "/Admin/Assets/Catalogs/");
+
+ $actions->child("select", title => loc("Select"), path => "/Admin/Assets/Catalogs/");
+ $actions->child("create", title => loc("Create"), path => "/Admin/Assets/Catalogs/Create.html");
+
+ my $catalog = RT::Catalog->new( $current_user );
+ $catalog->Load($HTML::Mason::Commands::DECODED_ARGS->{id}) if $HTML::Mason::Commands::DECODED_ARGS->{id};
+
+ if ($catalog->id and $catalog->CurrentUserCanSee) {
+ my $query = "id=" . $catalog->id;
+ $page->child("modify", title => loc("Basics"), path => "/Admin/Assets/Catalogs/Modify.html?$query");
+ $page->child("people", title => loc("Roles"), path => "/Admin/Assets/Catalogs/Roles.html?$query");
+
+ $page->child("cfs", title => loc("Asset Custom Fields"), path => "/Admin/Assets/Catalogs/CustomFields.html?$query");
+
+ $page->child("group-rights", title => loc("Group Rights"), path => "/Admin/Assets/Catalogs/GroupRights.html?$query");
+ $page->child("user-rights", title => loc("User Rights"), path => "/Admin/Assets/Catalogs/UserRights.html?$query");
+
+ $page->child("default-values", title => loc('Default Values'), path => "/Admin/Assets/Catalogs/DefaultValues.html?$query");
+ }
+ }
+
+ if ( $request_path =~ m{^/User/(Summary|History)\.html} ) {
+ if ($page->child('summary')) {
+ # Already set up from having AdminUser and ShowConfigTab;
+ # but rename "Basics" to "Edit" in this context
+ $page->child( 'basics' )->title( loc('Edit') );
+ } elsif ( $current_user->HasRight( Object => $RT::System, Right => 'ShowUserHistory' ) ) {
+ $page->child( display => title => loc('Summary'), path => '/User/Summary.html?id=' . $HTML::Mason::Commands::DECODED_ARGS->{'id'} );
+ $page->child( history => title => loc('History'), path => '/User/History.html?id=' . $HTML::Mason::Commands::DECODED_ARGS->{'id'} );
+ }
+ }
+
+ if ( $request_path =~ /^\/(?:index.html|$)/ ) {
+ my $alt = loc('Edit');
+ $page->child( edit => raw_html => q[<a id="page-edit" class="menu-item" href="] . RT->Config->Get('WebPath') . qq[/Prefs/MyRT.html"><span class="fas fa-cog" alt="$alt" data-toggle="tooltip" data-placement="top" data-original-title="$alt"></span></a>] );
+ }
+
+ if ( $request_path =~ m{^/Admin/Tools/(Configuration|EditConfig|ConfigHistory)} ) {
+ $page->child( display => title => loc('View'), path => "/Admin/Tools/Configuration.html" );
+ $page->child( modify => title => loc('Edit'), path => "/Admin/Tools/EditConfig.html" ) if RT->Config->Get('ShowEditSystemConfig');
+ $page->child( history => title => loc('History'), path => "/Admin/Tools/ConfigHistory.html" );
+ }
+
+ # due to historical reasons of always having been in /Elements/Tabs
+ $HTML::Mason::Commands::m->callback( CallbackName => 'Privileged', Path => $request_path, Search_Args => $args, Has_Query => $has_query, ARGSRef => \%args, CallbackPage => '/Elements/Tabs' );
+}
+
+sub _BuildAssetMenu {
+ my $request_path = shift;
+ my $top = shift;
+ my $widgets = shift;
+ my $page = shift;
+
+ my %args = ( @_ );
+
+ my $current_user = $HTML::Mason::Commands::session{CurrentUser};
+
+ my $id = $HTML::Mason::Commands::DECODED_ARGS->{id};
+ my $asset = RT::Asset->new( $current_user );
+ $asset->Load($id);
+
+ if ($asset->id) {
+ $page->child("display", title => HTML::Mason::Commands::loc("Display"), path => "/Asset/Display.html?id=$id");
+ $page->child("history", title => HTML::Mason::Commands::loc("History"), path => "/Asset/History.html?id=$id");
+ $page->child("basics", title => HTML::Mason::Commands::loc("Basics"), path => "/Asset/Modify.html?id=$id");
+ $page->child("links", title => HTML::Mason::Commands::loc("Links"), path => "/Asset/ModifyLinks.html?id=$id");
+ $page->child("people", title => HTML::Mason::Commands::loc("People"), path => "/Asset/ModifyPeople.html?id=$id");
+ $page->child("dates", title => HTML::Mason::Commands::loc("Dates"), path => "/Asset/ModifyDates.html?id=$id");
+
+ for my $grouping (RT::CustomField->CustomGroupings($asset)) {
+ my $cfs = $asset->CustomFields;
+ $cfs->LimitToGrouping( $asset => $grouping );
+ next unless $cfs->Count;
+ $page->child(
+ "cf-grouping-$grouping",
+ title => HTML::Mason::Commands::loc($grouping),
+ path => "/Asset/ModifyCFs.html?id=$id;Grouping=" . $HTML::Mason::Commands::m->interp->apply_escapes($grouping, 'u'),
+ );
+ }
+
+ _BuildAssetMenuActionSubmenu( $request_path, $top, $widgets, $page, %args, Asset => $asset );
+ }
+}
+
+sub _BuildAssetMenuActionSubmenu {
+ my $request_path = shift;
+ my $top = shift;
+ my $widgets = shift;
+ my $page = shift;
+
+ my %args = (
+ Asset => undef,
+ @_
+ );
+
+ my $asset = $args{Asset};
+ my $id = $asset->id;
+
+ my $actions = $page->child("actions", title => HTML::Mason::Commands::loc("Actions"));
+ $actions->child("create-linked-ticket", title => HTML::Mason::Commands::loc("Create linked ticket"), path => "/Asset/CreateLinkedTicket.html?Asset=$id");
+
+ my $status = $asset->Status;
+ my $lifecycle = $asset->LifecycleObj;
+ for my $action ( $lifecycle->Actions($status) ) {
+ my $next = $action->{'to'};
+ next unless $lifecycle->IsTransition( $status => $next );
+
+ my $check = $lifecycle->CheckRight( $status => $next );
+ next unless $asset->CurrentUserHasRight($check);
+
+ my $label = $action->{'label'} || ucfirst($next);
+ $actions->child(
+ $label,
+ title => HTML::Mason::Commands::loc($label),
+ path => "/Asset/Modify.html?id=$id;Update=1;DisplayAfter=1;Status="
+ . $HTML::Mason::Commands::m->interp->apply_escapes($next, 'u'),
+
+ class => "asset-lifecycle-action",
+ attributes => {
+ 'data-current-status' => $status,
+ 'data-next-status' => $next,
+ },
+ );
+ }
+}
+
+sub _BuildAdminMenu {
+ my $request_path = shift;
+ my $top = shift;
+ my $widgets = shift;
+ my $page = shift;
+
+ my %args = ( @_ );
+
+ my $current_user = $HTML::Mason::Commands::session{CurrentUser};
+
+ my $admin = $top->child( admin => title => loc('Admin'), path => '/Admin/' );
+ if ( $current_user->HasRight( Object => RT->System, Right => 'AdminUsers' ) ) {
+ my $users = $admin->child( users =>
+ title => loc('Users'),
+ description => loc('Manage users and passwords'),
+ path => '/Admin/Users/',
+ );
+ $users->child( select => title => loc('Select'), path => "/Admin/Users/" );
+ $users->child( create => title => loc('Create'), path => "/Admin/Users/Modify.html?Create=1" );
+ }
+ my $groups = $admin->child( groups =>
+ title => loc('Groups'),
+ description => loc('Manage groups and group membership'),
+ path => '/Admin/Groups/',
+ );
+ $groups->child( select => title => loc('Select'), path => "/Admin/Groups/" );
+ $groups->child( create => title => loc('Create'), path => "/Admin/Groups/Modify.html?Create=1" );
+
+ my $queues = $admin->child( queues =>
+ title => loc('Queues'),
+ description => loc('Manage queues and queue-specific properties'),
+ path => '/Admin/Queues/',
+ );
+ $queues->child( select => title => loc('Select'), path => "/Admin/Queues/" );
+ $queues->child( create => title => loc('Create'), path => "/Admin/Queues/Modify.html?Create=1" );
+
+ if ( $current_user->HasRight( Object => RT->System, Right => 'AdminCustomField' ) ) {
+ my $cfs = $admin->child( 'custom-fields' =>
+ title => loc('Custom Fields'),
+ description => loc('Manage custom fields and custom field values'),
+ path => '/Admin/CustomFields/',
+ );
+ $cfs->child( select => title => loc('Select'), path => "/Admin/CustomFields/" );
+ $cfs->child( create => title => loc('Create'), path => "/Admin/CustomFields/Modify.html?Create=1" );
+ }
+
+ if ( $current_user->HasRight( Object => RT->System, Right => 'AdminCustomRoles' ) ) {
+ my $roles = $admin->child( 'custom-roles' =>
+ title => loc('Custom Roles'),
+ description => loc('Manage custom roles'),
+ path => '/Admin/CustomRoles/',
+ );
+ $roles->child( select => title => loc('Select'), path => "/Admin/CustomRoles/" );
+ $roles->child( create => title => loc('Create'), path => "/Admin/CustomRoles/Modify.html?Create=1" );
+ }
+
+ if ( $current_user->HasRight( Object => RT->System, Right => 'ModifyScrips' ) ) {
+ my $scrips = $admin->child( 'scrips' =>
+ title => loc('Scrips'),
+ description => loc('Manage scrips'),
+ path => '/Admin/Scrips/',
+ );
+ $scrips->child( select => title => loc('Select'), path => "/Admin/Scrips/" );
+ $scrips->child( create => title => loc('Create'), path => "/Admin/Scrips/Create.html" );
+ }
+
+ if ( RT->Config->Get('ShowEditLifecycleConfig')
+ && $current_user->HasRight( Object => RT->System, Right => 'SuperUser' ) )
+ {
+ my $lifecycles = $admin->child(
+ lifecycles => title => loc('Lifecycles'),
+ path => '/Admin/Lifecycles/',
+ );
+
+ $lifecycles->child( select => title => loc('Select'), path => '/Admin/Lifecycles/' );
+ $lifecycles->child( create => title => loc('Create'), path => '/Admin/Lifecycles/Create.html' );
+ }
+
+ my $admin_global = $admin->child( global =>
+ title => loc('Global'),
+ description => loc('Manage properties and configuration which apply to all queues'),
+ path => '/Admin/Global/',
+ );
+
+ my $scrips = $admin_global->child( scrips =>
+ title => loc('Scrips'),
+ description => loc('Modify scrips which apply to all queues'),
+ path => '/Admin/Global/Scrips.html',
+ );
+ $scrips->child( select => title => loc('Select'), path => "/Admin/Global/Scrips.html" );
+ $scrips->child( create => title => loc('Create'), path => "/Admin/Scrips/Create.html?Global=1" );
+
+ my $conditions = $admin_global->child( conditions =>
+ title => loc('Conditions'),
+ description => loc('Edit system conditions'),
+ path => '/Admin/Global/Conditions.html',
+ );
+ $conditions->child( select => title => loc('Select'), path => "/Admin/Global/Conditions.html" );
+ $conditions->child( create => title => loc('Create'), path => "/Admin/Conditions/Create.html" );
+
+ my $actions = $admin_global->child( actions =>
+ title => loc('Actions'),
+ description => loc('Edit system actions'),
+ path => '/Admin/Global/Actions.html',
+ );
+ $actions->child( select => title => loc('Select'), path => "/Admin/Global/Actions.html" );
+ $actions->child( create => title => loc('Create'), path => "/Admin/Actions/Create.html" );
+
+ my $templates = $admin_global->child( templates =>
+ title => loc('Templates'),
+ description => loc('Edit system templates'),
+ path => '/Admin/Global/Templates.html',
+ );
+ $templates->child( select => title => loc('Select'), path => "/Admin/Global/Templates.html" );
+ $templates->child( create => title => loc('Create'), path => "/Admin/Global/Template.html?Create=1" );
+
+ my $cfadmin = $admin_global->child( 'custom-fields' =>
+ title => loc('Custom Fields'),
+ description => loc('Modify global custom fields'),
+ path => '/Admin/Global/CustomFields/index.html',
+ );
+ $cfadmin->child( users =>
+ title => loc('Users'),
+ description => loc('Select custom fields for all users'),
+ path => '/Admin/Global/CustomFields/Users.html',
+ );
+ $cfadmin->child( groups =>
+ title => loc('Groups'),
+ description => loc('Select custom fields for all user groups'),
+ path => '/Admin/Global/CustomFields/Groups.html',
+ );
+ $cfadmin->child( queues =>
+ title => loc('Queues'),
+ description => loc('Select custom fields for all queues'),
+ path => '/Admin/Global/CustomFields/Queues.html',
+ );
+ $cfadmin->child( tickets =>
+ title => loc('Tickets'),
+ description => loc('Select custom fields for tickets in all queues'),
+ path => '/Admin/Global/CustomFields/Queue-Tickets.html',
+ );
+ $cfadmin->child( transactions =>
+ title => loc('Ticket Transactions'),
+ description => loc('Select custom fields for transactions on tickets in all queues'),
+ path => '/Admin/Global/CustomFields/Queue-Transactions.html',
+ );
+ $cfadmin->child( 'custom-fields' =>
+ title => loc('Articles'),
+ description => loc('Select Custom Fields for Articles in all Classes'),
+ path => '/Admin/Global/CustomFields/Class-Article.html',
+ );
+ $cfadmin->child( 'assets' =>
+ title => loc('Assets'),
+ description => loc('Select Custom Fields for Assets in all Catalogs'),
+ path => '/Admin/Global/CustomFields/Catalog-Assets.html',
+ );
+
+ my $article_admin = $admin->child( articles => title => loc('Articles'), path => "/Admin/Articles/index.html" );
+ my $class_admin = $article_admin->child(classes => title => loc('Classes'), path => '/Admin/Articles/Classes/' );
+ $class_admin->child( select =>
+ title => loc('Select'),
+ description => loc('Modify and Create Classes'),
+ path => '/Admin/Articles/Classes/',
+ );
+ $class_admin->child( create =>
+ title => loc('Create'),
+ description => loc('Modify and Create Custom Fields for Articles'),
+ path => '/Admin/Articles/Classes/Modify.html?Create=1',
+ );
+
+
+ my $cfs = $article_admin->child( 'custom-fields' =>
+ title => loc('Custom Fields'),
+ path => '/Admin/CustomFields/index.html?'.$HTML::Mason::Commands::m->comp('/Elements/QueryString', Type => 'RT::Class-RT::Article'),
+ );
+ $cfs->child( select =>
+ title => loc('Select'),
+ path => '/Admin/CustomFields/index.html?'.$HTML::Mason::Commands::m->comp('/Elements/QueryString', Type => 'RT::Class-RT::Article'),
+ );
+ $cfs->child( create =>
+ title => loc('Create'),
+ path => '/Admin/CustomFields/Modify.html?'.$HTML::Mason::Commands::m->comp("/Elements/QueryString", Create=>1, LookupType=> "RT::Class-RT::Article" ),
+ );
+
+ my $assets_admin = $admin->child( assets => title => loc("Assets"), path => '/Admin/Assets/' );
+ my $catalog_admin = $assets_admin->child( catalogs =>
+ title => loc("Catalogs"),
+ description => loc("Modify asset catalogs"),
+ path => "/Admin/Assets/Catalogs/"
+ );
+ $catalog_admin->child( "select", title => loc("Select"), path => $catalog_admin->path );
+ $catalog_admin->child( "create", title => loc("Create"), path => "Create.html" );
+
+
+ my $assets_cfs = $assets_admin->child( "cfs",
+ title => loc("Custom Fields"),
+ description => loc("Modify asset custom fields"),
+ path => "/Admin/CustomFields/?Type=" . RT::Asset->CustomFieldLookupType
+ );
+ $assets_cfs->child( "select", title => loc("Select"), path => $assets_cfs->path );
+ $assets_cfs->child( "create", title => loc("Create"), path => "/Admin/CustomFields/Modify.html?Create=1;LookupType=" . RT::Asset->CustomFieldLookupType);
+
+ $admin_global->child( 'group-rights' =>
+ title => loc('Group Rights'),
+ description => loc('Modify global group rights'),
+ path => '/Admin/Global/GroupRights.html',
+ );
+ $admin_global->child( 'user-rights' =>
+ title => loc('User Rights'),
+ description => loc('Modify global user rights'),
+ path => '/Admin/Global/UserRights.html',
+ );
+ $admin_global->child( 'my-rt' =>
+ title => loc('RT at a glance'),
+ description => loc('Modify the default "RT at a glance" view'),
+ path => '/Admin/Global/MyRT.html',
+ );
+
+ if (RT->Config->Get('SelfServiceUseDashboard')) {
+ if ($current_user->HasRight( Right => 'ModifyDashboard', Object => RT->System ) ) {
+ my $self_service = $admin_global->child( selfservice_home =>
+ title => loc('Self Service Home Page'),
+ description => loc('Edit self service home page dashboard'),
+ path => '/Admin/Global/SelfServiceHomePage.html');
+ if ( $request_path =~ m{^/Admin/Global/SelfServiceHomePage} ) {
+ $page->child(content => title => loc('Content'), path => '/Admin/Global/SelfServiceHomePage.html');
+ $page->child(show => title => loc('Show'), path => '/SelfService');
+ }
+ }
+ }
+ $admin_global->child( 'dashboards-in-menu' =>
+ title => loc('Modify Reports menu'),
+ description => loc('Customize dashboards in menu'),
+ path => '/Admin/Global/DashboardsInMenu.html',
+ );
+ $admin_global->child( 'topics' =>
+ title => loc('Topics'),
+ description => loc('Modify global article topics'),
+ path => '/Admin/Global/Topics.html',
+ );
+
+ my $admin_tools = $admin->child( tools =>
+ title => loc('Tools'),
+ description => loc('Use other RT administrative tools'),
+ path => '/Admin/Tools/',
+ );
+ $admin_tools->child( configuration =>
+ title => loc('System Configuration'),
+ description => loc('Detailed information about your RT setup'),
+ path => '/Admin/Tools/Configuration.html',
+ );
+ $admin_tools->child( theme =>
+ title => loc('Theme'),
+ description => loc('Customize the look of your RT'),
+ path => '/Admin/Tools/Theme.html',
+ );
+ if (RT->Config->Get('StatementLog')
+ && $current_user->HasRight( Right => 'SuperUser', Object => RT->System )) {
+ $admin_tools->child( 'sql-queries' =>
+ title => loc('SQL Queries'),
+ description => loc('Browse the SQL queries made in this process'),
+ path => '/Admin/Tools/Queries.html',
+ );
+ }
+ $admin_tools->child( rights_inspector =>
+ title => loc('Rights Inspector'),
+ description => loc('Search your configured rights'),
+ path => '/Admin/Tools/RightsInspector.html',
+ );
+ $admin_tools->child( shredder =>
+ title => loc('Shredder'),
+ description => loc('Permanently wipeout data from RT'),
+ path => '/Admin/Tools/Shredder',
+ );
+
+ if ( $request_path =~ m{^/Admin/(Queues|Users|Groups|CustomFields|CustomRoles)} ) {
+ my $type = $1;
+
+ my %labels = (
+ Queues => loc("Queues"),
+ Users => loc("Users"),
+ Groups => loc("Groups"),
+ CustomFields => loc("Custom Fields"),
+ CustomRoles => loc("Custom Roles"),
+ );
+
+ my $section;
+ if ( $request_path =~ m|^/Admin/$type/?(?:index.html)?$|
+ || ( $request_path =~ m|^/Admin/$type/(?:Modify.html)$|
+ && $HTML::Mason::Commands::DECODED_ARGS->{'Create'} )
+ )
+ {
+ $section = $page;
+
+ } else {
+ $section = $page->child( select => title => $labels{$type},
+ path => "/Admin/$type/" );
+ }
+
+ $section->child( select => title => loc('Select'), path => "/Admin/$type/" );
+ $section->child( create => title => loc('Create'), path => "/Admin/$type/Modify.html?Create=1" );
+ }
+
+ if ( $request_path =~ m{^/Admin/Queues} ) {
+ if ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} && $HTML::Mason::Commands::DECODED_ARGS->{'id'} =~ /^\d+$/
+ ||
+ $HTML::Mason::Commands::DECODED_ARGS->{'Queue'} && $HTML::Mason::Commands::DECODED_ARGS->{'Queue'} =~ /^\d+$/
+ ) {
+ my $id = $HTML::Mason::Commands::DECODED_ARGS->{'Queue'} || $HTML::Mason::Commands::DECODED_ARGS->{'id'};
+ my $queue_obj = RT::Queue->new( $current_user );
+ $queue_obj->Load($id);
+
+ if ( $queue_obj and $queue_obj->id ) {
+ my $queue = $page;
+ $queue->child( basics => title => loc('Basics'), path => "/Admin/Queues/Modify.html?id=" . $id );
+ $queue->child( people => title => loc('Watchers'), path => "/Admin/Queues/People.html?id=" . $id );
+
+ my $templates = $queue->child(templates => title => loc('Templates'), path => "/Admin/Queues/Templates.html?id=" . $id);
+ $templates->child( select => title => loc('Select'), path => "/Admin/Queues/Templates.html?id=".$id);
+ $templates->child( create => title => loc('Create'), path => "/Admin/Queues/Template.html?Create=1;Queue=".$id);
+
+ my $scrips = $queue->child( scrips => title => loc('Scrips'), path => "/Admin/Queues/Scrips.html?id=" . $id);
+ $scrips->child( select => title => loc('Select'), path => "/Admin/Queues/Scrips.html?id=" . $id );
+ $scrips->child( create => title => loc('Create'), path => "/Admin/Scrips/Create.html?Queue=" . $id);
+
+ my $cfs = $queue->child( 'custom-fields' => title => loc('Custom Fields') );
+ my $ticket_cfs = $cfs->child( 'tickets' => title => loc('Tickets'),
+ path => '/Admin/Queues/CustomFields.html?SubType=RT::Ticket;id=' . $id );
+
+ my $txn_cfs = $cfs->child( 'transactions' => title => loc('Transactions'),
+ path => '/Admin/Queues/CustomFields.html?SubType=RT::Ticket-RT::Transaction;id='.$id );
+
+ $queue->child( 'group-rights' => title => loc('Group Rights'), path => "/Admin/Queues/GroupRights.html?id=".$id );
+ $queue->child( 'user-rights' => title => loc('User Rights'), path => "/Admin/Queues/UserRights.html?id=" . $id );
+ $queue->child( 'history' => title => loc('History'), path => "/Admin/Queues/History.html?id=" . $id );
+ $queue->child( 'default-values' => title => loc('Default Values'), path => "/Admin/Queues/DefaultValues.html?id=" . $id );
+
+ # due to historical reasons of always having been in /Elements/Tabs
+ $HTML::Mason::Commands::m->callback( CallbackName => 'PrivilegedQueue', queue_id => $id, page_menu => $queue, CallbackPage => '/Elements/Tabs' );
+ }
+ }
+ }
+ if ( $request_path =~ m{^(/Admin/Users|/User/(Summary|History)\.html)} and $admin->child("users") ) {
+ if ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} && $HTML::Mason::Commands::DECODED_ARGS->{'id'} =~ /^\d+$/ ) {
+ my $id = $HTML::Mason::Commands::DECODED_ARGS->{'id'};
+ my $obj = RT::User->new( $current_user );
+ $obj->Load($id);
+
+ if ( $obj and $obj->id ) {
+ $page->child( basics => title => loc('Basics'), path => "/Admin/Users/Modify.html?id=" . $id );
+ $page->child( memberships => title => loc('Memberships'), path => "/Admin/Users/Memberships.html?id=" . $id );
+ $page->child( history => title => loc('History'), path => "/Admin/Users/History.html?id=" . $id );
+ $page->child( 'my-rt' => title => loc('RT at a glance'), path => "/Admin/Users/MyRT.html?id=" . $id );
+ $page->child( 'dashboards-in-menu' =>
+ title => loc('Modify Reports menu'),
+ path => '/Admin/Users/DashboardsInMenu.html?id=' . $id,
+ );
+ if ( RT->Config->Get('Crypt')->{'Enable'} ) {
+ $page->child( keys => title => loc('Private keys'), path => "/Admin/Users/Keys.html?id=" . $id );
+ }
+ $page->child( 'summary' => title => loc('User Summary'), path => "/User/Summary.html?id=" . $id );
+
+ if ( $current_user->HasRight( Right => 'ManageAuthTokens', Object => RT->System ) ) {
+ my $auth_tokens = $page->child(
+ auth_tokens => title => loc('Auth Tokens'),
+ path => '/Admin/Users/AuthTokens.html?id=' . $id
+ );
+
+ if ( $request_path =~ m{^/Admin/Users/AuthTokens\.html} ) {
+ $auth_tokens->child(
+ select_auth_token => title => loc('Select'),
+ path => '/Admin/Users/AuthTokens.html?id=' . $id,
+ );
+ $auth_tokens->child(
+ create_auth_token => title => loc('Create'),
+ raw_html =>
+ q[<a class="btn menu-item" href="#create-auth-token" data-toggle="modal" rel="modal:open">]
+ . loc("Create") . "</a>"
+ );
+ }
+ }
+ }
+ }
+
+ }
+
+ if ( $request_path =~ m{^(/Admin/Groups|/Group/(Summary|History)\.html)} ) {
+ if ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} && $HTML::Mason::Commands::DECODED_ARGS->{'id'} =~ /^\d+$/ ) {
+ my $id = $HTML::Mason::Commands::DECODED_ARGS->{'id'};
+ my $obj = RT::Group->new( $current_user );
+ $obj->Load($id);
+
+ if ( $obj and $obj->id ) {
+ $page->child( basics => title => loc('Basics'), path => "/Admin/Groups/Modify.html?id=" . $obj->id );
+ $page->child( members => title => loc('Members'), path => "/Admin/Groups/Members.html?id=" . $obj->id );
+ $page->child( memberships => title => loc('Memberships'), path => "/Admin/Groups/Memberships.html?id=" . $obj->id );
+ $page->child( 'links' =>
+ title => loc("Links"),
+ path => "/Admin/Groups/ModifyLinks.html?id=" . $obj->id,
+ description => loc("Group links"),
+ );
+ $page->child( 'group-rights' => title => loc('Group Rights'), path => "/Admin/Groups/GroupRights.html?id=" . $obj->id );
+ $page->child( 'user-rights' => title => loc('User Rights'), path => "/Admin/Groups/UserRights.html?id=" . $obj->id );
+ $page->child( history => title => loc('History'), path => "/Admin/Groups/History.html?id=" . $obj->id );
+ $page->child( 'summary' =>
+ title => loc("Group Summary"),
+ path => "/Group/Summary.html?id=" . $obj->id,
+ description => loc("Group summary page"),
+ );
+ }
+ }
+ }
+
+ if ( $request_path =~ m{^/Admin/CustomFields/} ) {
+ if ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} && $HTML::Mason::Commands::DECODED_ARGS->{'id'} =~ /^\d+$/ ) {
+ my $id = $HTML::Mason::Commands::DECODED_ARGS->{'id'};
+ my $obj = RT::CustomField->new( $current_user );
+ $obj->Load($id);
+
+ if ( $obj and $obj->id ) {
+ $page->child( basics => title => loc('Basics'), path => "/Admin/CustomFields/Modify.html?id=".$id );
+ $page->child( 'group-rights' => title => loc('Group Rights'), path => "/Admin/CustomFields/GroupRights.html?id=" . $id );
+ $page->child( 'user-rights' => title => loc('User Rights'), path => "/Admin/CustomFields/UserRights.html?id=" . $id );
+ unless ( $obj->IsOnlyGlobal ) {
+ $page->child( 'applies-to' => title => loc('Applies to'), path => "/Admin/CustomFields/Objects.html?id=" . $id );
+ }
+ }
+ }
+ }
+
+ if ( $request_path =~ m{^/Admin/CustomRoles} ) {
+ if ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} && $HTML::Mason::Commands::DECODED_ARGS->{'id'} =~ /^\d+$/ ) {
+ my $id = $HTML::Mason::Commands::DECODED_ARGS->{'id'};
+ my $obj = RT::CustomRole->new( $current_user );
+ $obj->Load($id);
+
+ if ( $obj and $obj->id ) {
+ $page->child( basics => title => loc('Basics'), path => "/Admin/CustomRoles/Modify.html?id=".$id );
+ $page->child( 'applies-to' => title => loc('Applies to'), path => "/Admin/CustomRoles/Objects.html?id=" . $id );
+ $page->child( 'visibility' => title => loc('Visibility'), path => "/Admin/CustomRoles/Visibility.html?id=" . $id );
+ }
+ }
+ }
+
+ if ( $request_path =~ m{^/Admin/Scrips/} ) {
+ if ( $HTML::Mason::Commands::m->request_args->{'id'} && $HTML::Mason::Commands::m->request_args->{'id'} =~ /^\d+$/ ) {
+ my $id = $HTML::Mason::Commands::m->request_args->{'id'};
+ my $obj = RT::Scrip->new( $current_user );
+ $obj->Load($id);
+
+ my ( $admin_cat, $create_path_arg, $from_query_param );
+ my $from_arg = $HTML::Mason::Commands::DECODED_ARGS->{'From'} || q{};
+ my ($from_queue) = $from_arg =~ /^(\d+)$/;
+ if ( $from_queue ) {
+ $admin_cat = "Queues/Scrips.html?id=$from_queue";
+ $create_path_arg = "?Queue=$from_queue";
+ $from_query_param = ";From=$from_queue";
+ }
+ elsif ( $from_arg eq 'Global' ) {
+ $admin_cat = 'Global/Scrips.html';
+ $create_path_arg = '?Global=1';
+ $from_query_param = ';From=Global';
+ }
+ else {
+ $admin_cat = 'Scrips';
+ $from_query_param = $create_path_arg = q{};
+ }
+ my $scrips = $page->child( scrips => title => loc('Scrips'), path => "/Admin/${admin_cat}" );
+ $scrips->child( select => title => loc('Select'), path => "/Admin/${admin_cat}" );
+ $scrips->child( create => title => loc('Create'), path => "/Admin/Scrips/Create.html${create_path_arg}" );
+
+ $page->child( basics => title => loc('Basics') => path => "/Admin/Scrips/Modify.html?id=" . $id . $from_query_param );
+ $page->child( 'applies-to' => title => loc('Applies to'), path => "/Admin/Scrips/Objects.html?id=" . $id . $from_query_param );
+ }
+ elsif ( $request_path =~ m{^/Admin/Scrips/(index\.html)?$} ) {
+ HTML::Mason::Commands::PageMenu->child( select => title => loc('Select') => path => "/Admin/Scrips/" );
+ HTML::Mason::Commands::PageMenu->child( create => title => loc('Create') => path => "/Admin/Scrips/Create.html" );
+ }
+ elsif ( $request_path =~ m{^/Admin/Scrips/Create\.html$} ) {
+ my ($queue) = $HTML::Mason::Commands::DECODED_ARGS->{'Queue'} && $HTML::Mason::Commands::DECODED_ARGS->{'Queue'} =~ /^(\d+)$/;
+ my $global_arg = $HTML::Mason::Commands::DECODED_ARGS->{'Global'};
+ if ($queue) {
+ HTML::Mason::Commands::PageMenu->child( select => title => loc('Select') => path => "/Admin/Queues/Scrips.html?id=$queue" );
+ HTML::Mason::Commands::PageMenu->child( create => title => loc('Create') => path => "/Admin/Scrips/Create.html?Queue=$queue" );
+ } elsif ($global_arg) {
+ HTML::Mason::Commands::PageMenu->child( select => title => loc('Select') => path => "/Admin/Global/Scrips.html" );
+ HTML::Mason::Commands::PageMenu->child( create => title => loc('Create') => path => "/Admin/Scrips/Create.html?Global=1" );
+ } else {
+ HTML::Mason::Commands::PageMenu->child( select => title => loc('Select') => path => "/Admin/Scrips" );
+ HTML::Mason::Commands::PageMenu->child( create => title => loc('Create') => path => "/Admin/Scrips/Create.html" );
+ }
+ }
+ }
+
+ if ( $request_path =~ m{^/Admin/Lifecycles} && $current_user->HasRight( Object => RT->System, Right => 'SuperUser' ) ) {
+ if (defined($HTML::Mason::Commands::DECODED_ARGS->{'Name'}) && defined($HTML::Mason::Commands::DECODED_ARGS->{'Type'}) ) {
+ my $lifecycles = $page->child( 'lifecycles' =>
+ title => loc('Lifecycles'),
+ description => loc('Manage lifecycles'),
+ path => '/Admin/Lifecycles/',
+ );
+ $lifecycles->child( select => title => loc('Select'), path => "/Admin/Lifecycles/" );
+ $lifecycles->child( create => title => loc('Create'), path => "/Admin/Lifecycles/Create.html" );
+
+ my $LifecycleObj = RT::Lifecycle->new();
+ $LifecycleObj->Load(Name => $HTML::Mason::Commands::DECODED_ARGS->{'Name'}, Type => $HTML::Mason::Commands::DECODED_ARGS->{'Type'});
+
+ if ($LifecycleObj->Name && $LifecycleObj->{data}{type} eq $HTML::Mason::Commands::DECODED_ARGS->{'Type'}) {
+ my $Name_uri = $LifecycleObj->Name;
+ my $Type_uri = $LifecycleObj->Type;
+ RT::Interface::Web::EscapeURI(\$Name_uri);
+ RT::Interface::Web::EscapeURI(\$Type_uri);
+
+ unless ( RT::Interface::Web->ClientIsIE ) {
+ $page->child( basics => title => loc('Modify'), path => "/Admin/Lifecycles/Modify.html?Type=" . $Type_uri . ";Name=" . $Name_uri );
+ }
+ $page->child( actions => title => loc('Actions'), path => "/Admin/Lifecycles/Actions.html?Type=" . $Type_uri . ";Name=" . $Name_uri );
+ $page->child( rights => title => loc('Rights'), path => "/Admin/Lifecycles/Rights.html?Type=" . $Type_uri . ";Name=" . $Name_uri );
+ $page->child( mappings => title => loc('Mappings'), path => "/Admin/Lifecycles/Mappings.html?Type=" . $Type_uri . ";Name=" . $Name_uri );
+ $page->child( advanced => title => loc('Advanced'), path => "/Admin/Lifecycles/Advanced.html?Type=" . $Type_uri . ";Name=" . $Name_uri );
+ }
+ }
+ else {
+ $page->child( select => title => loc('Select'), path => "/Admin/Lifecycles/" );
+ $page->child( create => title => loc('Create'), path => "/Admin/Lifecycles/Create.html" );
+ }
+ }
+
+ if ( $request_path =~ m{^/Admin/Global/Scrips\.html} ) {
+ $page->child( select => title => loc('Select'), path => "/Admin/Global/Scrips.html" );
+ $page->child( create => title => loc('Create'), path => "/Admin/Scrips/Create.html?Global=1" );
+ }
+
+ if ( $request_path =~ m{^/Admin(?:/Global)?/Conditions} ) {
+ $page->child( select => title => loc('Select'), path => "/Admin/Global/Conditions.html" );
+ $page->child( create => title => loc('Create'), path => "/Admin/Conditions/Create.html" );
+ }
+
+ if ( $request_path =~ m{^/Admin(?:/Global)?/Actions} ) {
+ $page->child( select => title => loc('Select'), path => "/Admin/Global/Actions.html" );
+ $page->child( create => title => loc('Create'), path => "/Admin/Actions/Create.html" );
+ }
+
+ if ( $request_path =~ m{^/Admin/Global/Templates?\.html} ) {
+ $page->child( select => title => loc('Select'), path => "/Admin/Global/Templates.html" );
+ $page->child( create => title => loc('Create'), path => "/Admin/Global/Template.html?Create=1" );
+ }
+
+ if ( $request_path =~ m{^/Admin/Articles/Classes/} ) {
+ if ( my $id = $HTML::Mason::Commands::DECODED_ARGS->{'id'} ) {
+ my $obj = RT::Class->new( $current_user );
+ $obj->Load($id);
+
+ if ( $obj and $obj->id ) {
+ my $section = $page->child( select => title => loc("Classes"), path => "/Admin/Articles/Classes/" );
+ $section->child( select => title => loc('Select'), path => "/Admin/Articles/Classes/" );
+ $section->child( create => title => loc('Create'), path => "/Admin/Articles/Classes/Modify.html?Create=1" );
+
+ $page->child( basics => title => loc('Basics'), path => "/Admin/Articles/Classes/Modify.html?id=".$id );
+ $page->child( topics => title => loc('Topics'), path => "/Admin/Articles/Classes/Topics.html?id=".$id );
+ $page->child( 'custom-fields' => title => loc('Custom Fields'), path => "/Admin/Articles/Classes/CustomFields.html?id=".$id );
+ $page->child( 'group-rights' => title => loc('Group Rights'), path => "/Admin/Articles/Classes/GroupRights.html?id=".$id );
+ $page->child( 'user-rights' => title => loc('User Rights'), path => "/Admin/Articles/Classes/UserRights.html?id=".$id );
+ $page->child( 'applies-to' => title => loc('Applies to'), path => "/Admin/Articles/Classes/Objects.html?id=$id" );
+ }
+ } else {
+ $page->child( select => title => loc('Select'), path => "/Admin/Articles/Classes/" );
+ $page->child( create => title => loc('Create'), path => "/Admin/Articles/Classes/Modify.html?Create=1" );
+ }
+ }
+}
+
+sub BuildSelfServiceNav {
+ my $request_path = shift;
+ my $top = shift;
+ my $widgets = shift;
+ my $page = shift;
+
+ my %args = ( @_ );
+
+ my $current_user = $HTML::Mason::Commands::session{CurrentUser};
+
+ if ( RT->Config->Get('SelfServiceUseDashboard')
+ && $request_path =~ m{^/SelfService/(?:index\.html)?$}
+ && $current_user->HasRight(
+ Right => 'ShowConfigTab',
+ Object => RT->System
+ )
+ && $current_user->HasRight( Right => 'ModifyDashboard', Object => RT->System )
+ )
+ {
+ $page->child( content => title => loc('Content'), path => '/Admin/Global/SelfServiceHomePage.html' );
+ $page->child( show => title => loc('Show'), path => '/SelfService/' );
+ }
+
+ my $queues = RT::Queues->new( $current_user );
+ $queues->UnLimit;
+
+ my $queue_count = 0;
+ my $queue_id;
+
+ while ( my $queue = $queues->Next ) {
+ next unless $queue->CurrentUserHasRight('CreateTicket');
+ $queue_id = $queue->id;
+ $queue_count++;
+ last if ( $queue_count > 1 );
+ }
+
+ my $home = $top->child( home => title => loc('Homepage'), path => '/' );
+
+ if ( $queue_count > 1 ) {
+ $home->child( new => title => loc('Create Ticket'), path => '/SelfService/CreateTicketInQueue.html' );
+ } elsif ( $queue_id ) {
+ $home->child( new => title => loc('Create Ticket'), path => '/SelfService/Create.html?Queue=' . $queue_id );
+ }
+
+ my $menu_label = loc('Tickets');
+ my $menu_path = '/SelfService/';
+ if ( RT->Config->Get('SelfServiceUseDashboard') ) {
+ $menu_path = '/SelfService/Open.html';
+ }
+ my $tickets = $top->child( tickets => title => $menu_label, path => $menu_path );
+ $tickets->child( open => title => loc('Open tickets'), path => '/SelfService/Open.html' );
+ $tickets->child( closed => title => loc('Closed tickets'), path => '/SelfService/Closed.html' );
+
+ $top->child( "assets", title => loc("Assets"), path => "/SelfService/Asset/" )
+ if $current_user->HasRight( Right => 'ShowAssetsMenu', Object => RT->System );
+
+ my $username = '<span class="current-user">'
+ . $HTML::Mason::Commands::m->interp->apply_escapes($current_user->Name, 'h')
+ . '</span>';
+ my $about_me = $top->child( preferences =>
+ title => loc('Logged in as [_1]', $username),
+ escape_title => 0,
+ sort_order => 99,
+ );
+
+ if ( ( RT->Config->Get('SelfServiceUserPrefs') || '' ) eq 'view-info' ||
+ $current_user->HasRight( Right => 'ModifySelf', Object => RT->System ) ) {
+ $about_me->child( prefs => title => loc('Preferences'), path => '/SelfService/Prefs.html' );
+ }
+
+ my $logout_url = RT->Config->Get('LogoutURL');
+ if ( $current_user->Name
+ && ( !RT->Config->Get('WebRemoteUserAuth')
+ || RT->Config->Get('WebFallbackToRTLogin') )) {
+ $about_me->child( logout => title => loc('Logout'), path => $logout_url );
+ }
+
+ if ( RT->Config->Get('SelfServiceShowArticleSearch') ) {
+ $widgets->child( 'goto-article' => raw_html => $HTML::Mason::Commands::m->scomp('/SelfService/Elements/SearchArticle') );
+ }
+
+ $widgets->child( goto => raw_html => $HTML::Mason::Commands::m->scomp('/SelfService/Elements/GotoTicket') );
+
+ if ($request_path =~ m{^/SelfService/Asset/} and $HTML::Mason::Commands::DECODED_ARGS->{id}) {
+ my $id = $HTML::Mason::Commands::DECODED_ARGS->{id};
+ $page->child("display", title => loc("Display"), path => "/SelfService/Asset/Display.html?id=$id");
+ $page->child("history", title => loc("History"), path => "/SelfService/Asset/History.html?id=$id");
+
+ if (Menu->child("new")) {
+ my $actions = $page->child("actions", title => loc("Actions"));
+ $actions->child("create-linked-ticket", title => loc("Create linked ticket"), path => "/SelfService/Asset/CreateLinkedTicket.html?Asset=$id");
+ }
+ }
+
+ # due to historical reasons of always having been in /Elements/Tabs
+ $HTML::Mason::Commands::m->callback( CallbackName => 'SelfService', Path => $request_path, ARGSRef => \%args, CallbackPage => '/Elements/Tabs' );
+}
+
+1;
diff --cc share/html/Admin/Tools/Configuration.html
index 6808bafa3a,913f1f0a9b..37a3a694b2
--- a/share/html/Admin/Tools/Configuration.html
+++ b/share/html/Admin/Tools/Configuration.html
@@@ -152,68 -162,88 +152,91 @@@ for my $type (qw/Tickets Queues Transac
}
$index_size++;
</%PERL>
-<tr class="<% $index_size%2 ? 'oddline' : 'evenline'%>">
-<td class="collection-as-table"><% $type %></td>
-<td class="collection-as-table"><% $count %></td>
-</tr>
+ <div class="<% $index_size%2 ? 'oddline' : 'evenline'%> form-row">
+ <div class="collection-as-table value col-6"><% $type %></div>
+ <div class="collection-as-table value col-6"><% $count %></div>
+ </div>
% }
-</table>
-</&>
-</td>
-<td valign="top" class="boxcontainer">
+ </&>
+
+ </div>
+
+ <div class="col-4">
-<&|/Widgets/TitleBox, title => loc("Mason template search order") &>
+ <&|/Widgets/TitleBox, title => loc("Mason template search order") &>
+
+ % if ( $m->{rt_mason_cache_created} ) {
+ % my $mason_obj_date = RT::Date->new( $session{CurrentUser} );
+ % $mason_obj_date->Set( Format => 'Unix', Value => $m->{rt_mason_cache_created} );
+
+ <div class="mason-cache">
- <div class="mason-cache-info"><&|/l&>Cache created</&>: <% $mason_obj_date->AsString %></div>
- <a class="button clear-mason-cache" href="javascript:;"><&|/l&>Clear Mason Cache</&></a>
++ <div class="mason-cache-info py-1 d-inline-block">
++ <&|/l&>Cache created</&>: <% $mason_obj_date->AsString %>
++ </div>
++ <a class="button btn btn-primary btn-sm clear-mason-cache float-right" href="javascript:;"><&|/l&>Clear Mason Cache</&></a>
+ </div>
+ <script type="text/javascript">
+ jQuery('a.clear-mason-cache').click( function() {
+ jQuery.post(RT.Config.WebHomePath + '/Admin/Helpers/ClearMasonCache', function(data) {
+ jQuery('div.mason-cache div.mason-cache-info').text(data.message);
+ }, 'json');
+ return false;
+ });
+ </script>
+
+ % }
+
-<ol>
+ <ol class="list-group-compact list-group">
% foreach my $path ( RT::Interface::Web->ComponentRoots ) {
-<li><% $path %></li>
+ <li class="list-group-item"><% $path %></li>
% }
-</ol>
-</&>
+ </ol>
+ </&>
-<&|/Widgets/TitleBox, title => loc("Static file search order") &>
-<ol>
+ <&|/Widgets/TitleBox, title => loc("Static file search order") &>
+ <ol class="list-group-compact list-group">
% foreach my $path ( (map {$_->{root}} RT->Config->Get('StaticRoots')),
% RT::Interface::Web->StaticRoots ) {
-<li><% $path %></li>
+ <li class="list-group-item"><% $path %></li>
% }
-</ol>
-</&>
+ </ol>
+ </&>
-<&|/Widgets/TitleBox, title => loc("Perl library search order") &>
-<ol>
+ <&|/Widgets/TitleBox, title => loc("Perl library search order") &>
+ <ol class="list-group-compact list-group">
% foreach my $inc (@INC) {
-<li><% $inc %></li>
+ <li class="list-group-item"><% $inc %></li>
% }
-</ol>
-</&>
+ </ol>
+ </&>
-<&|/Widgets/TitleBox, title=> loc("Loaded config files") &>
-<ol>
+ <&|/Widgets/TitleBox, title=> loc("Loaded config files") &>
+ <ol class="list-group-compact list-group">
% foreach my $config (RT->Config->LoadedConfigs) {
% if ($config->{site}) {
-<li><strong><% $config->{filename} %></strong></li>
+ <li class="list-group-item"><strong><% $config->{filename} %></strong></li>
% } else {
-<li><% $config->{filename} %></li>
+ <li class="list-group-item"><% $config->{filename} %></li>
% }
% }
-</ol>
-</&>
+ </ol>
+ </&>
-<&|/Widgets/TitleBox, title=> loc("Logging summary") &>
- <& /Admin/Elements/LoggingSummary &>
-</&>
+ <&|/Widgets/TitleBox, title=> loc("Logging summary") &>
+ <& /Admin/Elements/LoggingSummary &>
+ </&>
+
+ </div>
-</td>
-</table>
+</div>
<&|/Widgets/TitleBox, title => loc("Global Attributes") &>
-<table border="0" cellspacing="0" cellpadding="5" width="100%" class="collection">
-<tr class="collection-as-table">
-<th class="collection-as-table"><&|/l&>Name</&></th>
-<th class="collection-as-table"><&|/l&>Value</&></th>
-</tr>
+ <div class="collection form-row">
+ <div class="collection-as-table label col-6 text-left"><&|/l&>Name</&></div>
+ <div class="collection-as-table label col-6 text-left"><&|/l&>Value</&></div>
+ </div>
% my $attrs = $RT::System->Attributes;
+ % $m->callback( CallbackName => 'ModifySystemAttributes', Attributes => $attrs );
% my $index_size = 0;
% while ( my $attr = $attrs->Next ) {
% next if $attr->Name eq 'UpgradeHistory';
diff --cc share/html/Admin/Users/Keys.html
index 0bfdbb3c7f,bbef3d376f..7acdd8c853
--- a/share/html/Admin/Users/Keys.html
+++ b/share/html/Admin/Users/Keys.html
@@@ -59,9 -59,9 +59,9 @@@
<form action="<%RT->Config->Get('WebPath')%>/Admin/Users/Keys.html" method="post" enctype="multipart/form-data">
<input type="hidden" class="hidden" name="id" value="<% $UserObj->Id %>" />
- % if (RT::Crypt->UseForOutgoing eq 'GnuPG') {
+ % if (RT::Config->Get('GnuPG')->{Enable}) {
<&|/Widgets/TitleBox, title => loc('GnuPG private key') &>
-<& /Widgets/Form/Select,
+ <& /Widgets/Form/Select,
Name => 'PrivateKey',
Description => loc('Private Key'),
Values => \@potential_keys,
@@@ -71,13 -71,9 +71,13 @@@
</&>
% }
- % if (RT::Crypt->UseForOutgoing eq 'SMIME') {
+ % if (RT::Config->Get('SMIME')->{Enable}) {
<&|/Widgets/TitleBox, title => loc('SMIME Certificate') &>
-<textarea name="SMIMECertificate"><% $UserObj->SMIMECertificate || '' %></textarea>
+<div class="form-row">
+ <div class="col-12">
+ <textarea rows="25" class="form-control" name="SMIMECertificate"><% $UserObj->SMIMECertificate || '' %></textarea>
+ </div>
+</div>
</&>
% }
diff --cc share/html/Dashboards/Subscription.html
index 2c1e7ab490,79a5d4e4ff..c5b631c74a
--- a/share/html/Dashboards/Subscription.html
+++ b/share/html/Dashboards/Subscription.html
@@@ -194,65 -159,62 +194,92 @@@
% ? 'selected="selected"'
% : '';
- <option value="<% $value %>" <%$selected|n %>><% $formatted %></option>
+ <option value="<% $value %>" <%$selected|n %>><% $formatted %></option>
% }
-</select>
-(<%$timezone%>)
-</td></tr>
-<tr><td class="label">
-<&|/l&>Language</&>:
-</td><td class="value">
-<& /Elements/SelectLang,
- Name => 'Language',
- Default => $fields{'Language'},
- ShowNullOption => 1,
- &>
-</td></tr>
-<tr><td class="label">
-<&|/l&>Rows</&>:
-</td><td class="value">
-<select name="Rows">
-% for my $rows (1, 2, 5, 10, 15, 20, 25, 50, 75, 100, 0) {
- <option value="<% $rows %>" <% $fields{'Rows'} eq $rows ? 'selected="selected"' : '' |n %>><% loc($rows || 'Unlimited') %></option>
-% }
-</select>
-</td></tr>
+ </select>
+ </div>
+ <div class="col-auto">
+ <span class="current-value form-control">(<%$timezone%>)</span>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="form-row">
+ <div class="label col-3">
+ <&|/l&>Language</&>:
+ </div>
+ <div class="value col-9">
+ <div class="row">
+ <div class="col-auto">
+ <& /Elements/SelectLang,
+ Name => 'Language',
+ Default => $fields{'Language'},
+ ShowNullOption => 1,
+ &>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="form-row">
+ <div class="label col-3">
+ <&|/l&>Rows</&>:
+ </div>
+ <div class="value col-9">
+ <div class="row">
+ <div class="col-auto">
+ <select name="Rows" class="form-control selectpicker">
+% for my $rows (1, 2, 5, 10, 15, 20, 25, 50, 75, 100, 0) {
+ <option value="<% $rows %>" <% $fields{'Rows'} eq $rows ? 'selected="selected"' : '' |n %>><% loc($rows || 'Unlimited') %></option>
+% }
+ </select>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="form-row">
+ <div class="label col-3"></div>
+ <div class="value col-9">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox" id="SuppressIfEmpty" name="SuppressIfEmpty" class="custom-control-input" value="1" <% $fields{'SuppressIfEmpty'} ? 'checked="checked"' : "" |n %>>
+ <label class="custom-control-label" for="SuppressIfEmpty"><&|/l&>Suppress if empty (Check this to avoid sending mail if all searches have no results)</&></label>
+ </div>
+ </div>
+ </div>
-<tr><td align="right"><input type="checkbox" id="SuppressIfEmpty" name="SuppressIfEmpty" value="1" <% $fields{'SuppressIfEmpty'} ? 'checked="checked"' : "" |n %> /></td>
-<td><label for="SuppressIfEmpty"><&|/l&>Suppress if empty (Check this to avoid sending mail if all searches have no results)</&></label><br />
-<input type="hidden"class="hidden" name="SuppressIfEmpty-Magic" value=1 />
-</td></tr>
% $m->callback( %ARGS, CallbackName => 'SubscriptionFormEnd', FieldsRef => \%fields,
% SubscriptionObj => $SubscriptionObj, DashboardObj => $Dashboard );
-</table>
</&>
+ <&| /Widgets/TitleBox, title => loc('Search Context') &>
+
-<p class="description">
-<&|/l&>Most searches show the same results for all users and can be run as the user who owns the dashboard subscription (Subscription owner).</&>
-</p>
-
-<p class="description">
-<&|/l&>For searches like "10 highest priority tickets I own" that contain __CurrentUser__ in the query, the results are specific to each recipient. For dashboards with these searches, select "Each dashboard recipient" below to run each search with the recipient set as the "Current User".</&>
-</p>
-
-<table>
- <tr><td class="label"><&|/l&>Run Dashboard Searches As</&>:</td>
- <td>
- <input type="radio" id="context-subscriber" name="Context" value="subscriber" <% ($fields{'Context'} // '') ne 'recipient' ? 'checked="checked"' : "" |n %>></input>
- <label for="context-subscriber"><&|/l&>Subscription owner</&>(<% $session{CurrentUser}->Name %>)</label>
- <br />
- <input type="radio" id="context-recipient" name="Context" value="recipient" <% ($fields{'Context'} // '') eq 'recipient' ? 'checked="checked"' : "" |n %>></input>
- <label for="context-recipient"><&|/l&>Each dashboard recipient</&></label>
- </td>
- </tr>
-</table>
++ <p class="description mt-3 mt-1 ml-3">
++ <&|/l&>Most searches show the same results for all users and can be run as the user who owns the dashboard subscription (Subscription owner).</&>
++ </p>
++
++ <p class="description mt-3 mt-1 ml-3">
++ <&|/l&>For searches like "10 highest priority tickets I own" that contain __CurrentUser__ in the query, the results are specific to each recipient. For dashboards with these searches, select "Each dashboard recipient" below to run each search with the recipient set as the "Current User".</&>
++ </p>
++
++ <div class="form-row">
++ <div class="label col-3">
++ <&|/l&>Run Dashboard Searches As</&>:
++ </div>
++ <div class="value col-9">
++ <div class="custom-control custom-radio">
++ <input type="radio" id="context-subscriber" name="Context" class="custom-control-input" value="subscriber" <% ($fields{'Context'} // '') ne 'recipient' ? 'checked="checked"' : "" |n %>></input>
++ <label class="custom-control-label" for="context-subscriber"><&|/l&>Subscription owner</&>(<% $session{CurrentUser}->Name %>)</label>
++ </div>
++ <div class="custom-control custom-radio">
++ <input type="radio" id="context-recipient" name="Context" class="custom-control-input" value="recipient" <% ($fields{'Context'} // '') eq 'recipient' ? 'checked="checked"' : "" |n %>></input>
++ <label class="custom-control-label" for="context-recipient"><&|/l&>Each dashboard recipient</&></label>
++ </div>
++ </div>
++ </div>
+ </&>
+
<&| /Widgets/TitleBox, title => loc('Recipients') &>
<& Elements/SubscriptionRecipients,
UserField => $UserField, UserString => $UserString, UserOp => $UserOp,
diff --cc share/html/Elements/EmailInput
index 46e9cb249f,6c99144483..6b2b676b5f
--- a/share/html/Elements/EmailInput
+++ b/share/html/Elements/EmailInput
@@@ -81,14 -79,10 +81,18 @@@
data-autocomplete-include-system
% }
+% if (@options) {
+ data-options="<% JSON(\@options) %>"
+% }
+
+% if (@items) {
+ data-items="<% JSON(\@items) %>"
+% }
+
+ % if ($AutocompleteExclude) {
+ data-autocomplete-exclude="<% $AutocompleteExclude %>"
+ % }
+
/>
% if ($EntryHint) {
<br>
@@@ -146,5 -101,5 +150,6 @@@ $AutocompleteNobody =>
$AutocompleteSystem => 0
$EntryHint => ''
$Placeholder => ''
+$Options => []
+ $AutocompleteExclude => undef
</%ARGS>
diff --cc share/html/Elements/ShowSearch
index 81f7529226,a047c4b966..f8c05d5ab2
--- a/share/html/Elements/ShowSearch
+++ b/share/html/Elements/ShowSearch
@@@ -80,24 -76,12 +80,28 @@@ if ($SavedSearch)
}
$SearchArg->{'SavedSearchId'} ||= $SavedSearch;
$SearchArg->{'SearchType'} ||= 'Ticket';
- if ( $SearchArg->{SearchType} ne 'Ticket' ) {
+ if ( $SearchArg->{SearchType} eq 'Transaction' ) {
+ $class = $SearchArg->{Class} = 'RT::Transactions';
+ $customize
+ = RT->Config->Get('WebPath')
+ . '/Search/Build.html?'
+ . $m->comp( '/Elements/QueryString', SavedSearchLoad => $SavedSearch, Class => 'RT::Transactions' );
+ $ShowCount = RT->Config->Get('TransactionShowSearchResultCount')->{'RT::Ticket'};
+ }
+ elsif ( $SearchArg->{SearchType} eq 'Asset' ) {
+ $class = $SearchArg->{Class} = 'RT::Assets';
+ $customize
+ = RT->Config->Get('WebPath')
+ . '/Search/Build.html?'
+ . $m->comp( '/Elements/QueryString', SavedSearchLoad => $SavedSearch, Class => 'RT::Assets' );
+ $ShowCount = RT->Config->Get('AssetShowSearchResultCount');
+ }
+ elsif ( $SearchArg->{SearchType} ne 'Ticket' ) {
+ if ( $SearchArg->{'SearchType'} eq 'Chart' ) {
+ $SearchArg->{'SavedChartSearchId'} ||= $SavedSearch;
+ }
+
# XXX: dispatch to different handler here
$query_display_component
= '/Search/Elements/' . $SearchArg->{SearchType};
diff --cc share/html/Helpers/PreviewScrips
index 03e10b0308,b0531ee915..febbf8abe8
--- a/share/html/Helpers/PreviewScrips
+++ b/share/html/Helpers/PreviewScrips
@@@ -106,26 -104,26 +106,30 @@@ $submitted{$_} = 1 for split /,/, $ARGS
% my $action = $scrip->ActionObj->Action;
% my @addresses = $action->$type();
% next unless @addresses;
- <ul>
+ <ul class="list-group list-group-compact">
% for my $addr (@addresses) {
- <li>
+ <li class="list-group-item">
% my $checked = $submitted{$addr->address} ? not $squelched{$addr->address} : $squelched_config;
% $m->callback(CallbackName => 'BeforeAddress', Ticket => $TicketObj, Address => $addr, Type => $type, Checked => \$checked);
- % $recips{$addr->address}++;
- <b><%loc($type)%></b>:
+ <div class="form-row">
+ <div class="col-auto">
+ <b><%loc($type)%></b>:
+ </div>
+ <div class="col-auto">
% my $show_checkbox = 1;
% if ( grep {$_ eq $addr->address} @{$action->{NoSquelch}{$type}} ) {
% $show_checkbox = 0;
% }
-
+ <div class="custom-control custom-checkbox">
% if ( $show_checkbox ) {
+ % $recips{$addr->address}++;
- <input type="checkbox" class="checkbox" name="TxnSendMailTo" <% $checked ? 'checked="checked"' : '' |n%> value="<%$addr->address%>" id="TxnSendMailTo-<% $addr->address %>-<% $recips{$addr->address} %>" />
+ <input type="checkbox" class="custom-control-input" name="TxnSendMailTo" <% $checked ? 'checked="checked"' : '' |n%> value="<%$addr->address%>" id="TxnSendMailTo-<% $addr->address %>-<% $recips{$addr->address} %>" />
% }
- <label <% $show_checkbox ? 'class="custom-control-label"' : '' |n%> for="TxnSendMailTo-<% $addr->address %>-<% $recips{$addr->address} %>"><& /Elements/ShowUser, Address => $addr &></label>
- <label
-% if ( $show_checkbox ) {
- for="TxnSendMailTo-<% $addr->address %>-<% $recips{$addr->address} %>"
-% }
- ><& /Elements/ShowUser, Address => $addr &></label>
++ <label <% $show_checkbox ? 'class="custom-control-label"' : '' |n%>
++% if ( $show_checkbox ) {
++ for="TxnSendMailTo-<% $addr->address %>-<% $recips{$addr->address} %>"
++% }
++ ><& /Elements/ShowUser, Address => $addr &></label>
% $m->callback(CallbackName => 'AfterAddress', Ticket => $TicketObj, Address => $addr, Type => $type);
% unless ( $show_checkbox ) {
% if ( $type eq 'Cc' ) {
diff --cc share/html/Prefs/Other.html
index f3de2dd298,e2526dbd91..cb7e730b97
--- a/share/html/Prefs/Other.html
+++ b/share/html/Prefs/Other.html
@@@ -69,14 -69,11 +69,14 @@@
% if ( RT->Config->Get('Crypt')->{'Enable'} ) {
<&|/Widgets/TitleBox, title => loc( 'Cryptography' ) &>
- <&|/l&>Preferred key</&>: <& /Elements/Crypt/SelectKeyForEncryption, EmailAddress => $UserObj->EmailAddress, Default => $UserObj->PreferredKey &>
+ <&|/l&>Preferred GnuPG key</&>: <& /Elements/Crypt/SelectKeyForEncryption, Name => 'PreferredKey', EmailAddress => $UserObj->EmailAddress, Default => $UserObj->PreferredKey, Protocol => 'GnuPG' &>
</&>
% }
-
-<& /Elements/Submit, Name => 'Update', Label => loc('Save Changes') &>
+<div class="form-row">
+ <div class="col-12">
+ <& /Elements/Submit, Name => 'Update', Label => loc('Save Changes') &>
+ </div>
+</div>
</form>
<%INIT>
my @results;
diff --cc share/html/Search/Chart.html
index 985ee2e1c6,e4e04ea001..6adc8356ab
--- a/share/html/Search/Chart.html
+++ b/share/html/Search/Chart.html
@@@ -57,7 -57,7 +57,7 @@@ $m->callback( ARGSRef => \%ARGS, Callba
my $title = loc( "Grouped search results");
- my @search_fields = ( qw(Query GroupBy ChartStyle ChartFunction Width Height ExtraQueryParams), @ExtraQueryParams );
-my @search_fields = qw(Query GroupBy StackedGroupBy ChartStyle ChartFunction Width Height);
++my @search_fields = ( qw(Query GroupBy StackedGroupBy ChartStyle ChartFunction Width Height ExtraQueryParams), @ExtraQueryParams );
my $saved_search = $m->comp( '/Widgets/SavedSearch:new',
SearchType => 'Chart',
SearchFields => [@search_fields],
@@@ -148,96 -137,66 +148,102 @@@ $m->callback( ARGSRef => \%ARGS, QueryA
<input type="hidden" class="hidden" name="Query" value="<% $query{Query} %>" />
<input type="hidden" class="hidden" name="SavedChartSearchId" value="<% $saved_search->{SearchId} || 'new' %>" />
-<&| /Widgets/TitleBox, title => loc('Group by'), class => "chart-group-by" &>
-<fieldset><legend><% loc('Group tickets by') %></legend>
-<& Elements/SelectGroupBy,
- Name => 'GroupBy',
- Query => $query{Query},
- Default => $query{'GroupBy'}[0],
- Stacked => $query{'GroupBy'}[0] eq ($query{StackedGroupBy} // '') ? 1 : 0,
- &>
-</fieldset>
-<fieldset><legend><% loc('and then') %></legend>
-<& Elements/SelectGroupBy,
- Name => 'GroupBy',
- Query => $query{Query},
- Default => $query{'GroupBy'}[1] // q{},
- ShowEmpty => 1,
- Stacked => $query{'GroupBy'}[1] && ($query{'GroupBy'}[1] eq ($query{StackedGroupBy} // '')) ? 1 : 0,
- &>
-</fieldset>
-<fieldset><legend><% loc('and then') %></legend>
-<& Elements/SelectGroupBy,
- Name => 'GroupBy',
- Query => $query{Query},
- Default => $query{'GroupBy'}[2] // q{},
- ShowEmpty => 1,
- Stacked => $query{'GroupBy'}[2] && ($query{'GroupBy'}[2] eq ($query{StackedGroupBy} // '')) ? 1 : 0,
- &>
-</fieldset>
-</&>
-
-<&| /Widgets/TitleBox, title => loc("Calculate"), class => "chart-calculate" &>
-
-<fieldset><legend><% loc('Calculate values of') %></legend>
-<& Elements/SelectChartFunction, Default => $query{'ChartFunction'}[0] &>
-</fieldset>
-<fieldset><legend><% loc('and then') %></legend>
-<& Elements/SelectChartFunction, Default => $query{'ChartFunction'}[1] // q{}, ShowEmpty => 1 &>
-</fieldset>
-<fieldset><legend><% loc('and then') %></legend>
-<& Elements/SelectChartFunction, Default => $query{'ChartFunction'}[2] // q{}, ShowEmpty => 1 &>
-</fieldset>
-
-</&>
-
-<&| /Widgets/TitleBox, title => loc('Picture'), class => "chart-picture" &>
-<input name="ChartStyle" type="hidden" value="<% $query{ChartStyle} %>" />
-<label><% loc('Style') %>: <& Elements/SelectChartType, Default => $query{ChartStyle} =~ /^(pie|bar|table)\b/ ? $1 : undef &></label>
-<span class="width">
-<label><% loc("Width") %>: <input type="text" name="Width" value="<% $query{'Width'} || q{} %>"> <% loc("px") %></label>
-</span>
-<span class="height">
- ×
- <label><% loc("Height") %>: <input type="text" name="Height" value="<% $query{'Height'} || q{} %>"> <% loc("px") %></label>
-</span>
-<div class="include-table">
- <input type="checkbox" name="ChartStyleIncludeTable" <% $query{ChartStyle} =~ /\btable\b/ ? 'checked="checked"' : '' |n %>> <% loc('Include data table') %>
-</div>
-<div class="include-sql">
- <input type="checkbox" name="ChartStyleIncludeSQL" <% $query{ChartStyle} =~ /\bsql\b/ ? 'checked="checked"' : '' |n %>> <% loc('Include TicketSQL query') %>
-</div>
-</&>
+% if ( $query{ExtraQueryParams} ) {
+% for my $input ( ref $query{ExtraQueryParams} eq 'ARRAY' ? @{$query{ExtraQueryParams}} : $query{ExtraQueryParams} ) {
+<input type="hidden" class="hidden" name="ExtraQueryParams" value="<% $input %>" />
+% if ( defined $query{$input} ) {
+<input type="hidden" class="hidden" name="<% $input %>" value="<% $query{$input} %>" />
+% }
+% }
+% }
+
+ <&| /Widgets/TitleBox, title => loc('Group by'), class => "chart-group-by" &>
+ <fieldset><legend><% loc('Group tickets by') %></legend>
+ <& Elements/SelectGroupBy,
+ Name => 'GroupBy',
+ Query => $query{Query},
+ Default => $query{'GroupBy'}[0],
++ Stacked => $query{'GroupBy'}[0] eq ($query{StackedGroupBy} // '') ? 1 : 0,
++ StackedId => 'StackedGroupBy-1',
+ &>
+ </fieldset>
+ <fieldset><legend><% loc('and then') %></legend>
+ <& Elements/SelectGroupBy,
+ Name => 'GroupBy',
+ Query => $query{Query},
+ Default => $query{'GroupBy'}[1] // q{},
+ ShowEmpty => 1,
++ Stacked => $query{'GroupBy'}[1] && ($query{'GroupBy'}[1] eq ($query{StackedGroupBy} // '')) ? 1 : 0,
++ StackedId => 'StackedGroupBy-2',
+ &>
+ </fieldset>
+ <fieldset><legend><% loc('and then') %></legend>
+ <& Elements/SelectGroupBy,
+ Name => 'GroupBy',
+ Query => $query{Query},
+ Default => $query{'GroupBy'}[2] // q{},
+ ShowEmpty => 1,
++ Stacked => $query{'GroupBy'}[2] && ($query{'GroupBy'}[2] eq ($query{StackedGroupBy} // '')) ? 1 : 0,
++ StackedId => 'StackedGroupBy-3',
+ &>
+ </fieldset>
+ </&>
+
+ <&| /Widgets/TitleBox, title => loc("Calculate"), class => "chart-calculate" &>
+ <fieldset><legend><% loc('Calculate values of') %></legend>
+ <& Elements/SelectChartFunction, Default => $query{'ChartFunction'}[0] &>
+ </fieldset>
+ <fieldset><legend><% loc('and then') %></legend>
+ <& Elements/SelectChartFunction, Default => $query{'ChartFunction'}[1] // q{}, ShowEmpty => 1 &>
+ </fieldset>
+ <fieldset><legend><% loc('and then') %></legend>
+ <& Elements/SelectChartFunction, Default => $query{'ChartFunction'}[2] // q{}, ShowEmpty => 1 &>
+ </fieldset>
+ </&>
+
+ <&| /Widgets/TitleBox, title => loc('Picture'), class => "chart-picture" &>
+ <input name="ChartStyle" type="hidden" value="<% $query{ChartStyle} %>" />
+ <div class="form-row">
+ <div class="label col-auto">
+ <label><% loc('Style') %>:</label>
+ </div>
+ <div class="value col-auto">
+ <& Elements/SelectChartType, Default => $query{ChartStyle} =~ /^(pie|bar(?:-stacked)?|line(?:-stacked)?|table)\b/ ? $1 : undef &>
+ </div>
+ <div class="label col-auto">
+ <label><% loc("Width") %>:</label>
+ </div>
+ <div class="value col-auto">
+ <div class="input-group mb-3">
+ <input class="form-control" type="text" name="Width" value="<% $query{'Width'} || q{} %>">
+ <div class="input-group-append">
+ <span class="input-group-text"><&|/l&>px</&></span>
+ </div>
+ </div>
+ </div>
+ <div class="label col-auto">
+ ×
+ <label><% loc("Height") %>:</label>
+ </div>
+ <div class="value col-auto">
+ <div class="input-group mb-3">
+ <input class="form-control" type="text" name="Height" value="<% $query{'Height'} || q{} %>">
+ <div class="input-group-append">
+ <span class="input-group-text"><&|/l&>px</&></span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="include-table custom-control custom-checkbox">
+ <input type="checkbox" id="ChartStyleIncludeTable" name="ChartStyleIncludeTable" class="custom-control-input" <% $query{ChartStyle} =~ /\btable\b/ ? 'checked="checked"' : '' |n %>>
+ <label class="custom-control-label" for="ChartStyleIncludeTable"><&|/l&>Include data table</&></label>
+ </div>
+ <div class="include-sql custom-control custom-checkbox">
+ <input type="checkbox" id="ChartStyleIncludeSQL" name="ChartStyleIncludeSQL" class="custom-control-input" <% $query{ChartStyle} =~ /\bsql\b/ ? 'checked="checked"' : '' |n %>>
+ <label class="custom-control-label" for="ChartStyleIncludeSQL"><&|/l&>Include TicketSQL query</&></label>
+ </div>
+ </&>
+
<script type="text/javascript">
var updateChartStyle = function() {
var val = jQuery(".chart-picture [name=ChartType]").val();
@@@ -259,26 -227,57 +274,70 @@@ jQuery(".chart-picture [name=ChartType]
jQuery(".chart-picture [name=ChartStyleIncludeTable]").change( updateChartStyle );
jQuery(".chart-picture [name=ChartStyleIncludeSQL]").change( updateChartStyle );
+ jQuery("input.stacked-group-checkbox").change( function() {
+ if ( jQuery(this).is(':checked') ) {
+ jQuery("input.stacked-group-checkbox").not(this).prop('checked', false);
+ }
+ });
+
+ jQuery("select[name=GroupBy]").change( function() {
+ // "GroupBy-Groups" could be triggered because the they are fully cloned from "GroupBy"
+ if ( jQuery(this).attr('name') == 'GroupBy-Groups' ) {
- var elem = jQuery(this).next('select[name=GroupBy]');
++ var elem = jQuery(this).closest('fieldset').find('select[name=GroupBy]');
+ setTimeout( function () {
+ elem.change();
+ }, 100 ); // give it a moment to prepare "GroupBy" options
+ }
+ else {
+ jQuery(this).closest('fieldset').find('input.stacked-group-checkbox').val(jQuery(this).val());
+ }
+ });
+
+ jQuery( function() {
+ jQuery("select[name=ChartFunction-Groups]").change( function() {
+ var allow_stacked = jQuery(".chart-picture [name=ChartType]").val() == 'bar';
+
+ var value_count;
+ jQuery("select[name=ChartFunction-Groups]").each( function() {
+ if ( jQuery(this).val() ) {
+ value_count++;
+ if ( value_count > 1 || !jQuery(this).val().match(/count/i) ) {
+ allow_stacked = 0;
+ }
+ }
+ } );
+
+ if ( allow_stacked ) {
+ jQuery("span.stacked-group").removeClass('hidden');
+ jQuery("input.stacked-group-checkbox").prop('disabled', false);
+ }
+ else {
+ jQuery("span.stacked-group").addClass('hidden');
+ jQuery("input.stacked-group-checkbox").prop('disabled', true);
+ }
+ }).change();
+ });
+
</script>
-<& /Elements/Submit, Label => loc('Update Chart'), Name => 'Update' &>
-</form>
-
+ <div class="form-row">
+ <div class="col-12">
+ <& /Elements/Submit, Label => loc('Update Chart'), Name => 'Update' &>
+ </div>
+ </div>
+ </form>
+ </div>
+
+ <div class="col-xl-6">
+ <div class="saved-search">
- <& /Widgets/SavedSearch:show, %ARGS, Action => 'Chart.html', self => $saved_search, Title => loc('Saved charts') &>
++ <& /Widgets/SavedSearch:show, %ARGS, Action => 'Chart.html', self => $saved_search, Title => loc('Saved charts'), AllowCopy => 1 &>
+ </div>
+ </div>
</div>
-<div class="saved-search">
- <& /Widgets/SavedSearch:show, %ARGS, Action => 'Chart.html', self => $saved_search, Title => loc('Saved charts'), AllowCopy => 1 &>
+
</div>
</div>
+
+<%ARGS>
+ at ExtraQueryParams => ()
+</%ARGS>
diff --cc share/html/Search/Elements/EditFormat
index 35a3aaefc0,fbeccbdcd7..ca260ecf32
--- a/share/html/Search/Elements/EditFormat
+++ b/share/html/Search/Elements/EditFormat
@@@ -64,9 -60,10 +64,11 @@@ jQuery( function()
jQuery('[name=AddCol], [name=RemoveCol], [name=ColUp], [name=ColDown]').click( function() {
var name = jQuery(this).attr('name');
var form = jQuery(this).closest('form');
+
jQuery.ajax({
- url: '<% RT->Config->Get('WebPath') %>/Helpers/BuildFormatString?' + name + '=1&' + form.serialize(),
+ url: '<% RT->Config->Get('WebPath') %>/Helpers/BuildFormatString?' + name + '=1',
+ method: 'POST',
+ data: form.serialize(),
success: function (data) {
if ( data.status == 'success' ) {
form.find('input[name=Format]').val(data.Format);
diff --cc share/html/Search/Elements/SelectGroupBy
index 8e8d3ce833,0e2de5cf20..dc0e2e3394
--- a/share/html/Search/Elements/SelectGroupBy
+++ b/share/html/Search/Elements/SelectGroupBy
@@@ -50,6 -50,7 +50,8 @@@ $Name => 'GroupBy
$Default => 'Status'
$Query => ''
$ShowEmpty => 0
+ $Stacked => 0
++$StackedId => "Stacked$Name"
</%args>
<select name="<% $Name %>" class="cascade-by-optgroup">
% if ( $ShowEmpty ) {
@@@ -74,6 -75,11 +76,14 @@@ while ( my ($label, $value) = splice @o
</optgroup>
% }
</select>
+
-<span class="stacked-group">
- <% loc('Stacked?') %> <input name="Stacked<% $Name %>" type="checkbox" class="stacked-group-checkbox" <% $Stacked ? 'checked="checked"' : '' |n %>/>
++<span class="stacked-group d-inline-block">
++ <div class="custom-control custom-checkbox">
++ <input name="Stacked<% $Name %>" id="<% $StackedId %>" type="checkbox" class="custom-control-input stacked-group-checkbox" <% $Stacked ? 'checked="checked"' : '' |n %> />
++ <label for="<% $StackedId %>" class="custom-control-label"><&|/l&>Stacked?</&></label>
++ </div>
+ </span>
+
<%init>
use RT::Report::Tickets;
my $report = RT::Report::Tickets->new( $session{'CurrentUser'} );
diff --cc share/html/Search/JSChart
index 75c3d58f7f,0000000000..027b14fcd4
mode 100644,000000..100644
--- a/share/html/Search/JSChart
+++ b/share/html/Search/JSChart
@@@ -1,251 -1,0 +1,318 @@@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2021 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%args>
+$Cache => undef
+$Query => "id > 0"
+ at GroupBy => ()
+$ChartStyle => 'bar+table+sql'
+ at ChartFunction => 'COUNT'
+$Width => undef
+$Height => undef
+$SavedSearchId => ''
++$StackedGroupBy => undef
+</%args>
+
+% my $id = join '-', 'search-chart', $SavedSearchId || ();
+<canvas id="<% $id %>" width="<% $Width %>" height="<% $Height %>"></canvas>
+<script type="text/javascript">
+var data_labels = <% JSON( \@data_labels ) |n %>;
+
+% if ( $#data == 1 ) {
+var backgroundColor = Chart.colorschemes[<% $scheme_parts[0] |n,j %>][<% $scheme_parts[1] |n,j %>];
+if ( backgroundColor && backgroundColor.length ) {
+ while ( backgroundColor.length < <% scalar @{$data[0]} %>) {
+ backgroundColor = backgroundColor.concat(backgroundColor);
+ }
+}
+% }
+
+var ctx = document.getElementById(<% $id |n,j %>).getContext('2d');
+var searchChart = new Chart(ctx, {
+ type: <% $ChartStyle =~ /\b(bar|line|pie)\b/ ? $1 : 'bar' |n,j %>,
+ data: {
+ labels: <% JSON( [ map { join ' - ', @$_ } @{$data[0]} ] ) |n %>,
+ datasets: [
+% for my $index ( 1 .. $#data ) {
+ {
+% if ( $#data == 1 ) {
+ backgroundColor: backgroundColor || [],
+% }
+
+% if ( $ChartStyle =~ /\bbar|line\b/ ) {
+ label: <% $labels[$index-1] |n,j %>,
+% }
+ data: <% JSON($data[$index]) |n %>
+ }
+% if ( $index != $#data ) {
+ ,
+% }
+% }
+ ]
+ },
+ options: {
+ plugins: {
+ colorschemes: {
+ scheme: <% $scheme |n,j %>
+ }
+ },
+% if ( $ChartStyle =~ /\bbar|line\b/ ) {
+ legend: {
+ display: <% @data == 2 ? 'false' : 'true' %>
+ },
+ tooltips: {
+ callbacks: {
+ label: function(tooltipItem, data) {
+ var label = data.datasets[tooltipItem.datasetIndex].label || '';
+
+ if (label) {
+ label += ': ';
+ }
+ label += data_labels[tooltipItem.datasetIndex][tooltipItem.index];
+ return label;
+ }
+ }
+ },
+ scales: {
+ xAxes: [{
++ stacked: <% $stacked ? 'true' : 'false' %>,
+ scaleLabel: {
+ display: true,
+ labelString: <% join(' - ', map $report->Label( $_), @{ $columns{'Groups'} }) |n,j %>
+ },
+ gridLines: {
+ display: false
+ }
+ }],
+ yAxes: [{
++ stacked: <% $stacked ? 'true' : 'false' %>,
+ scaleLabel: {
+ display: true,
+ labelString: <% $report->Label( $columns{'Functions'}[0] ) |n,j %>
+ },
+ gridLines: {
+ drawTicks: true,
+ drawOnChartArea: false
+ },
+ ticks: {
+ beginAtZero: true,
+ callback: function(value, index, values) {
+% my $info = $report->ColumnInfo($columns{'Functions'}[0]);
+% if ( $info->{META} && ($info->{META}{Display}//'') eq 'DurationAsString' ) {
+ return;
+% } else {
+ if (Math.floor(value) === value) {
+ return value;
+ }
+% }
+ }
+ }
+ }]
+ }
+% }
+ }
+});
+
+var group_by = <% JSON( \@GroupBy ) |n %>;
+var data_queries = <% JSON( \@data_queries ) |n %>;
+
+jQuery('#search-chart').click(function(e) {
+ var slice = searchChart.getElementAtEvent(e);
+ if ( !slice[0] ) return;
+
+ var query = <% $Query =~ /^\s*\(.*\)$/ ? $Query : "( $Query )" |n,j %>;
- if ( data_queries[slice[0]._index] ) {
- query += ' AND ( ' + data_queries[slice[0]._index] + ')';
++ var extra_query;
++% if ( $stacked ) { # data_queries is array of array for stacked charts
++ if ( data_queries[slice[0]._datasetIndex][slice[0]._index] ) {
++ extra_query = data_queries[slice[0]._datasetIndex][slice[0]._index];
++ }
++% } else {
++ if ( data_queries[slice[0]._index] ) {
++ extra_query = data_queries[slice[0]._index];
++ }
++% }
++ if ( extra_query ) {
++ query += ' AND ( ' + extra_query + ')';
+ }
+ window.open(RT.Config.WebPath + '/Search/Results.html?Query=' + encodeURIComponent(query)
+ + '&' + <% $m->comp('/Elements/QueryString', map { $_ => $DECODED_ARGS->{$_} } grep { $_ ne 'Query' } keys %$DECODED_ARGS) |n,j%>);
+});
+
+</script>
+
+<%init>
+s/\D//g for grep defined, $Width, $Height;
+$Width ||= ($ChartStyle =~ /\bpie\b/ ? 400 : 600);
+$Height ||= ($ChartStyle =~ /\bpie\b/ ? $Width : 400);
+$Height = $Width if $ChartStyle =~ /\bpie\b/;
+
+use RT::Report::Tickets;
+my $report = RT::Report::Tickets->new( $session{'CurrentUser'} );
+
+# Default GroupBy we use in RT::Report::Tickets, we also need it here to
+# generate sub queries.
+ at GroupBy = 'Status' unless @GroupBy;
+
+my %columns;
+if ( $Cache and my $data = delete $session{'charts_cache'}{ $Cache } ) {
+ %columns = %{ $data->{'columns'} };
+ $report->Deserialize( $data->{'report'} );
+ $session{'i'}++;
+} else {
+ %columns = $report->SetupGroupings(
+ Query => $Query,
+ GroupBy => \@GroupBy,
+ Function => \@ChartFunction,
+ );
+
+ $report->SortEntries;
+}
+
+my @data = ([],[]);
+my @data_labels;
+my @data_queries;
+while ( my $entry = $report->Next ) {
+ push @{ $data[0] }, [ map $entry->LabelValue( $_ ), @{ $columns{'Groups'} } ];
+ push @data_queries, $entry->Query;
+
+ my @values;
+ my @label_values;
+ foreach my $column ( @{ $columns{'Functions'} } ) {
+ my $v = $entry->RawValue( $column );
+ my $label_v = $entry->LabelValue( $column );
+ unless ( ref $v ) {
+ push @values, $v;
+ push @label_values, $label_v;
+ next;
+ }
+
+ my @subs = $report->FindImplementationCode(
+ $report->ColumnInfo( $column )->{'META'}{'SubValues'}
+ )->( $report );
+ push @values, map $v->{$_}, @subs;
+ push @label_values, map $label_v->{$_}, @subs;
+ }
+
+ my $i = 0;
+ my $j = 0;
+ push @{ $data[++$i] }, $_ foreach @values;
+ push @{ $data_labels[$j++] }, $_ foreach @label_values;
+}
+
+$ChartStyle =~ s/\bpie\b/bar/ if @data > 2;
+
+my @labels;
+if ( $ChartStyle =~ /\b(?:bar|line)\b/ ) {
+ for my $column ( @{$columns{'Functions'}} ) {
+ my $info = $report->ColumnInfo( $column );
+ my @subs;
+ if ( $info->{'META'}{'SubValues'} ) {
+ push @labels, map { join ': ', $report->Label($column), $_ } $report->FindImplementationCode( $info->{'META'}{'SubValues'} )->($report);
+ }
+ else {
+ push @labels, $report->Label($column);
+ }
+ }
+}
+
++my $stacked;
++if ( $StackedGroupBy && $ChartStyle =~ /\bbar\b/ ) {
++ if ( scalar @data > 2 ) {
++ RT->Logger->warning("Invalid stack option: it can't apply to multiple data rows");
++ }
++ else {
++
++ my $labels = $data[0];
++
++ # find the stacked group index
++ require List::MoreUtils;
++ my $stacked_index = List::MoreUtils::first_index { $_ eq $StackedGroupBy } @GroupBy;
++ if ( $stacked_index >= 0 ) {
++ my %rows;
++ my $i = 0;
++ my %new_label;
++
++ for my $label (@$labels) {
++ my @new_label = @$label;
++ splice @new_label, $stacked_index, 1; # remove the stacked group
++ my $key = join ';;;', @new_label;
++ $new_label{$key} = \@new_label;
++ $rows{ $label->[$stacked_index] }{$key}{value} = $data[1][$i];
++ $rows{ $label->[$stacked_index] }{$key}{label} = $data_labels[0][$i];
++ $rows{ $label->[$stacked_index] }{$key}{query} = $data_queries[$i];
++ $i++;
++ }
++
++ @data = [ map { $new_label{$_} } sort { lc $a cmp lc $b } keys %new_label ];
++
++ my @dataset_labels;
++ @data_queries = ();
++ @data_labels = ();
++ for my $stacked_field ( sort { lc $a cmp lc $b } keys %rows ) {
++ push @dataset_labels, join ' - ', $labels[0], $stacked_field;
++ my ( @new_values, @new_labels, @new_queries );
++ for my $key ( sort { lc $a cmp lc $b } keys %new_label ) {
++ push @new_values, $rows{$stacked_field}->{$key}{value} || 0;
++ push @new_labels, $rows{$stacked_field}->{$key}{label};
++ push @new_queries, $rows{$stacked_field}->{$key}{query};
++ }
++ push @data, \@new_values;
++ push @data_labels, \@new_labels;
++ push @data_queries, \@new_queries;
++ }
++ @labels = @dataset_labels;
++ $stacked = 1;
++ }
++ else {
++ RT->Logger->warning("Invalid StackedGroupBy: $StackedGroupBy");
++ }
++ }
++}
++
+my $scheme = RT->Config->Get('JSChartColorScheme', $session{CurrentUser}) || 'brewer.Paired12';
+my @scheme_parts = split /\./, $scheme, 2;
+</%init>
diff --cc share/html/Ticket/Elements/UpdateCc
index 89b23e8eb1,3f8b76059e..b058ecfaca
--- a/share/html/Ticket/Elements/UpdateCc
+++ b/share/html/Ticket/Elements/UpdateCc
@@@ -47,106 -47,67 +47,108 @@@
%# END BPS TAGGED BLOCK }}}
% $m->callback(CallbackName => 'BeforeCc', ARGSRef => \%ARGS, Ticket => $TicketObj, one_time_Ccs => \@one_time_Ccs, txn_addresses => \%txn_addresses);
-<tr><td class="label"><&|/l&>One-time Cc</&>:</td><td><& /Elements/EmailInput, Name => 'UpdateCc', Size => undef, Default => $ARGS{UpdateCc}, AutocompleteMultiple => 1 &>
-<input type="hidden" id="UpdateIgnoreAddressCheckboxes" name="UpdateIgnoreAddressCheckboxes" value="<% $ARGS{UpdateIgnoreAddressCheckboxes} || 0 %>">
- <br />
+<div class="form-row">
+ <div class="label col-2">
+ <&|/l&>One-time Cc</&>:
+ </div>
+ <div class="value col-9">
+ <& /Elements/EmailInput, Name => 'UpdateCc', Size => undef, Default => $ARGS{UpdateCc}, AutocompleteMultiple => 1, Options => \@one_time_Ccs &>
- <input type="hidden" id="UpdateIgnoreAddressCheckboxes" name="UpdateIgnoreAddressCheckboxes" value="0">
++ <input type="hidden" id="UpdateIgnoreAddressCheckboxes" name="UpdateIgnoreAddressCheckboxes" value="<% $ARGS{UpdateIgnoreAddressCheckboxes} || 0 %>">
+ </div>
+</div>
+<div class="form-row mt-0">
+ <div class="label col-2"></div>
+ <div class="value col-9">
+ <div class="form-row">
%if (scalar @one_time_Ccs) {
% if ($hide_cc_suggestions) {
- <a href="#" class="ToggleSuggestions" data-hide-label="<% $hide_label %>" data-show-label="<% $show_label %>">
- <i class="label">(<&|/l&>show suggestions</&>)</i>
- </a>
- <div class="OneTimeCcs hidden">
+ <a href="#" class="ToggleSuggestions" data-hide-label="<% $hide_label %>" data-show-label="<% $show_label %>">
+ <i class="label">(<&|/l&>show suggestions</&>)</i>
+ </a>
+ <div class="OneTimeCcs d-flex flex-wrap hidden">
% }
-<i class="label">(<&|/l&>check to add</&>)</i>
-<input type="checkbox" class="checkbox" id="AllSuggestedCc" name="AllSuggestedCc" value="1" onclick="setCheckbox(this, /^UpdateCc-/); checkboxesToInput('UpdateCc', 'input[name^=UpdateCc-]:checkbox');">
-<label for="AllSuggestedCc"><% loc('All recipients') %></label>
+
+ <div class="col-auto"><span class="far fa-question-circle icon-helper" data-toggle="tooltip" data-placement="top" data-original-title="<&|/l&>check to add</&>"></span></div>
+ <div class="custom-control custom-checkbox">
- <input type="checkbox" class="custom-control-input" id="AllSuggestedCc" name="AllSuggestedCc" value="1" onclick="setCheckbox(this, /^UpdateCc-/, null, true)">
++ <input type="checkbox" class="custom-control-input" id="AllSuggestedCc" name="AllSuggestedCc" value="1" onclick="setCheckbox(this, /^UpdateCc-/); checkboxesToInput('UpdateCc', 'input[name^=UpdateCc-]:checkbox');">
+ <label class="custom-control-label" for="AllSuggestedCc"><% loc('All recipients') %></label>
+ </div>
%}
%foreach my $addr ( @one_time_Ccs ) {
-<span class="ticket-update-suggested-cc">
-<input
- id="UpdateCc-<%$addr%>"
- name="UpdateCc-<%$addr%>"
- class="onetime onetimecc"
- type="checkbox"
+ <div class="col-auto">
+ <span class="ticket-update-suggested-cc">
+ <div class="custom-control custom-checkbox">
+ <input
+ id="UpdateCc-<%$addr%>"
+ name="UpdateCc-<%$addr%>"
+ class="onetime onetimecc custom-control-input"
+ type="checkbox"
+ autocomplete="off"
% my $clean_addr = $txn_addresses{$addr}->format;
- onClick="checkboxToInput('UpdateCc', <% "UpdateCc-$addr" |n,j%>, <%$clean_addr|n,j%> );"
- data-address="<% $clean_addr %>"
- <% $ARGS{'UpdateCc-'.$addr} ? 'checked="checked"' : ''%> >
- <label for="UpdateCc-<%$addr%>"><& /Elements/ShowUser, Address => $txn_addresses{$addr}&></label>
-</span>
++ data-address="<% $clean_addr %>"
+ onClick="checkboxToInput('UpdateCc', <% "UpdateCc-$addr" |n,j%>, <%$clean_addr|n,j%> );"
+ <% $ARGS{'UpdateCc-'.$addr} ? 'checked="checked"' : ''%> >
+ <label class="custom-control-label" for="UpdateCc-<%$addr%>"><& /Elements/ShowUser, Address => $txn_addresses{$addr}&></label>
+ </div>
+ </span>
+ </div>
%}
%if (@one_time_Ccs && $hide_cc_suggestions) {
-</div>
+ </div>
%}
-</td></tr>
-<tr><td class="label"><&|/l&>One-time Bcc</&>:</td><td><& /Elements/EmailInput, Name => 'UpdateBcc', Size => undef, Default => $ARGS{UpdateBcc}, AutocompleteMultiple => 1 &><br />
+ </div>
+ </div>
+</div>
+
+<div class="form-row">
+ <div class="label col-2">
+ <&|/l&>One-time Bcc</&>:
+ </div>
+ <div class="value col-9">
+ <& /Elements/EmailInput, Name => 'UpdateBcc', Size => undef, Default => $ARGS{UpdateBcc}, AutocompleteMultiple => 1, Options => \@one_time_Ccs &>
+ </div>
+</div>
+
+<div class="form-row mt-0">
+ <div class="label col-2"></div>
+ <div class="value col-9">
+ <div class="form-row">
%if (scalar @one_time_Ccs) {
% if ($hide_cc_suggestions) {
- <a href="#" class="ToggleSuggestions" data-hide-label="<% $hide_label %>" data-show-label="<% $show_label %>">
- <i class="label">(<&|/l&>show suggestions</&>)</i>
- </a>
- <div class="OneTimeCcs hidden">
+ <a href="#" class="ToggleSuggestions" data-hide-label="<% $hide_label %>" data-show-label="<% $show_label %>">
+ <i class="label">(<&|/l&>show suggestions</&>)</i>
+ </a>
+ <div class="OneTimeCcs d-flex flex-wrap hidden">
% }
-<i class="label">(<&|/l&>check to add</&>)</i>
-<input type="checkbox" class="checkbox" id="AllSuggestedBcc" name="AllSuggestedBcc" value="1" onclick="setCheckbox(this, /^UpdateBcc-/); checkboxesToInput('UpdateBcc', 'input[name^=UpdateBcc-]:checkbox');">
-<label for="AllSuggestedBcc"><% loc('All recipients') %></label>
+
+ <div class="col-auto"><span class="far fa-question-circle icon-helper" data-toggle="tooltip" data-placement="top" data-original-title="<&|/l&>check to add</&>"></span></div>
+ <div class="custom-control custom-checkbox">
- <input type="checkbox" class="checkbox custom-control-input" id="AllSuggestedBcc" name="AllSuggestedBcc" value="1" onclick="setCheckbox(this, /^UpdateBcc-/, null, true)">
++ <input type="checkbox" class="checkbox custom-control-input" id="AllSuggestedBcc" name="AllSuggestedBcc" value="1" onclick="setCheckbox(this, /^UpdateBcc-/); checkboxesToInput('UpdateBcc', 'input[name^=UpdateBcc-]:checkbox');">
+ <label class="custom-control-label" for="AllSuggestedBcc"><% loc('All recipients') %></label>
+ </div>
%}
%foreach my $addr ( @one_time_Ccs ) {
-<span class="ticket-update-suggested-cc">
-<input
- id="UpdateBcc-<%$addr%>"
- name="UpdateBcc-<%$addr%>"
- class="onetime onetimebcc"
- type="checkbox"
+ <div class="col-auto">
+ <span class="ticket-update-suggested-cc">
+ <div class="custom-control custom-checkbox">
+ <input
+ id="UpdateBcc-<%$addr%>"
+ name="UpdateBcc-<%$addr%>"
+ class="onetime onetimebcc custom-control-input"
+ type="checkbox"
+ autocomplete="off"
% my $clean_addr = $txn_addresses{$addr}->format;
- onClick="checkboxToInput('UpdateBcc', <% "UpdateBcc-$addr" |n,j%>, <%$clean_addr|n,j%> );"
- data-address="<% $clean_addr %>"
- <% $ARGS{'UpdateBcc-'.$addr} ? 'checked="checked"' : ''%> >
- <label for="UpdateBcc-<%$addr%>"><& /Elements/ShowUser, Address => $txn_addresses{$addr}&></label>
-</span>
++ data-address="<% $clean_addr %>"
+ onClick="checkboxToInput('UpdateBcc', <% "UpdateBcc-$addr" |n,j%>, <%$clean_addr|n,j%> );"
+ <% $ARGS{'UpdateBcc-'.$addr} ? 'checked="checked"' : ''%> >
+ <label class="custom-control-label" for="UpdateBcc-<%$addr%>"><& /Elements/ShowUser, Address => $txn_addresses{$addr}&></label>
+ </div>
+ </span>
+ </div>
%}
%if (@one_time_Ccs && $hide_cc_suggestions) {
-</div>
+ </div>
<script type="text/javascript">
jQuery(function() {
jQuery('a.ToggleSuggestions').click(function(e) {
diff --cc share/html/Ticket/Forward.html
index 5a902c8676,b4493b2cfe..6061a417d7
--- a/share/html/Ticket/Forward.html
+++ b/share/html/Ticket/Forward.html
@@@ -51,44 -51,31 +51,44 @@@
% $m->callback(CallbackName => 'BeforeActionList', Actions => \@results, ARGSRef => \%ARGS, Ticket => $TicketObj);
<& /Elements/ListActions, actions => \@results &>
-<form action="Forward.html" id="ForwardMessage" name="ForwardMessage" method="post">
+<form action="Forward.html" id="ForwardMessage" name="ForwardMessage" method="post" class="mx-auto max-width-lg">
% $m->callback( CallbackName => 'FormStart', ARGSRef => \%ARGS );
-<input type="hidden" class="hidden" name="id" value="<% $id %>" /><br />
+<input type="hidden" class="hidden" name="id" value="<% $id %>" />
<input type="hidden" class="hidden" name="QuoteTransaction" value="<% $ARGS{'QuoteTransaction'} || '' %>" />
- <& /Elements/Crypt/SignEncryptWidget:ShowIssues, self => $gnupg_widget &>
+ <& /Elements/Crypt/SignEncryptWidget:ShowIssues, self => $gnupg_widget, Queue => $TicketObj->QueueObj &>
-<table border="0">
-<tr><td align="right"><&|/l&>From</&>:</td>
-<td><% $from %></td></tr>
+<&|/Widgets/TitleBox, title => loc('Message'), class => 'messagedetails' &>
+<div>
+ <div class="form-row">
+ <div class="label col-2"><&|/l&>From</&>:</div>
+ <div class="value col-9"><% $from %></div>
+ </div>
-<tr><td align="right"><&|/l&>Subject</&>:</td>
-<td><input name="Subject" size="60" value="<% $ARGS{'Subject'} || $subject %>" /></td></tr>
+ <div class="form-row">
+ <div class="label col-2"><&|/l&>Subject</&>:</div>
+ <div class="value col-9"><input class="form-control" type="text" name="Subject" value="<% $ARGS{'Subject'} || $subject %>" /></div>
+ </div>
-<tr><td align="right"><&|/l&>To</&>:</td>
-<td><& /Elements/EmailInput, Name => "To", AutocompleteMultiple => 1, Default => $ARGS{'To'} &></td></tr>
+ <div class="form-row">
+ <div class="label col-2"><&|/l&>To</&>:</div>
+ <div class="value col-9"><& /Elements/EmailInput, Name => "To", AutocompleteMultiple => 1, Default => $ARGS{'To'} &></div>
+ </div>
-<tr><td align="right"><&|/l&>Cc</&>:</td>
-<td><& /Elements/EmailInput, Name => "Cc", AutocompleteMultiple => 1, Default => $ARGS{'Cc'} &></td></tr>
+ <div class="form-row">
+ <div class="label col-2"><&|/l&>Cc</&>:</div>
+ <div class="value col-9"><& /Elements/EmailInput, Name => "Cc", AutocompleteMultiple => 1, Default => $ARGS{'Cc'} &></div>
+ </div>
-<tr><td align="right"><&|/l&>Bcc</&>:</td>
-<td><& /Elements/EmailInput, Name => "Bcc", AutocompleteMultiple => 1, Default => $ARGS{'Bcc'} &></td></tr>
+ <div class="form-row">
+ <div class="label col-2"><&|/l&>Bcc</&>:</div>
+ <div class="value col-9"><& /Elements/EmailInput, Name => "Bcc", AutocompleteMultiple => 1, Default => $ARGS{'Bcc'} &></div>
+ </div>
% if ( $gnupg_widget ) {
-<tr><td> </td><td>
+ <div class="form-row">
+ <span class="label col-2 empty"> </span>
+ <div class="value col-9">
<& /Elements/Crypt/SignEncryptWidget,
self => $gnupg_widget,
TicketObj => $TicketObj,
diff --cc share/html/Ticket/Update.html
index 169e373460,b13e1ea0fb..7b532ca240
--- a/share/html/Ticket/Update.html
+++ b/share/html/Ticket/Update.html
@@@ -60,11 -59,11 +60,11 @@@
<input type="hidden" class="hidden" name="Action" value="<% $Action %>" />
<input type="hidden" class="hidden" name="Token" value="<% $ARGS{'Token'} %>" />
- <& /Elements/Crypt/SignEncryptWidget:ShowIssues, self => $gnupg_widget &>
+ <& /Elements/Crypt/SignEncryptWidget:ShowIssues, self => $gnupg_widget, Queue => $TicketObj->QueueObj &>
<div id="ticket-update-metadata">
- <&|/Widgets/TitleBox, title => loc('Ticket and Transaction') &>
-<table width="100%" border="0">
+ <&|/Widgets/TitleBox, title => loc('Ticket and Transaction'), class => 'ticket-info-basics' &>
+<div>
% $m->callback(CallbackName => 'AfterTableOpens', ARGSRef => \%ARGS, Ticket => $TicketObj);
% my $skip;
diff --cc share/static/js/util.js
index e6195c0459,31122ed331..bd9cc01a5a
--- a/share/static/js/util.js
+++ b/share/static/js/util.js
@@@ -191,21 -211,37 +193,47 @@@ function checkboxToInput(target,checkbo
}
}
else{
- tar.val(tar.val().replace(val+', ',''));
- tar.val(tar.val().replace(val,''));
+ emails = jQuery.grep(emails, function(email) {
+ return email != val;
+ });
}
jQuery('#UpdateIgnoreAddressCheckboxes').val(true);
+
+ var selectize = tar[0].selectize;
+ if ( selectize ) {
+ if( box.prop('checked') ) {
+ selectize.createItem(val, false);
+ }
+ else {
+ selectize.removeItem(val, true);
+ }
+ }
- tar.change();
+ tar.val(emails.join(', ')).change();
+ }
+
+ function checkboxesToInput(target,checkboxes) {
+ var tar = jQuery('#' + escapeCssSelector(target));
+
+ var emails = jQuery.grep(tar.val().split(/,\s*/), function(email) {
+ return email.match(/\S/) ? true : false;
+ });
+
+ jQuery(checkboxes).each(function(index, checkbox) {
+ var val = jQuery(checkbox).attr('data-address');
+ if(jQuery(checkbox).prop('checked')){
+ if ( emails.indexOf(val) == -1 ) {
+ emails.unshift(val);
+ }
+ }
+ else{
+ emails = jQuery.grep(emails, function(email) {
+ return email != val;
+ });
+ }
+ });
+
+ jQuery('#UpdateIgnoreAddressCheckboxes').val(true);
+ tar.val(emails.join(', ')).change();
}
// ahah for back compatibility as plugins may still use it
@@@ -555,37 -565,19 +593,50 @@@ function addprincipal_onchange(ev, ui)
}
}
+function refreshCollectionListRow(tbody, table, success, error) {
+ var params = {
+ DisplayFormat : table.data('display-format'),
+ ObjectClass : table.data('class'),
+ MaxItems : table.data('max-items'),
+ InlineEdit : table.hasClass('inline-edit'),
+
+ i : tbody.data('index'),
+ ObjectId : tbody.data('record-id'),
+ Warning : tbody.data('warning')
+ };
+
+ tbody.addClass('refreshing');
+
+ jQuery.ajax({
+ url : RT.Config.WebHomePath + '/Helpers/CollectionListRow',
+ method : 'GET',
+ data : params,
+ success: function (response) {
+ var index = tbody.data('index');
+ tbody.replaceWith(response);
+ // Get the new replaced tbody
+ tbody = table.find('tbody[data-index=' + index + ']');
+ initDatePicker(tbody);
+ tbody.find('.selectpicker').selectpicker();
+ if (success) { success(response) }
+ },
+ error: error
+ });
+}
+
+ // disable submit on enter in autocomplete boxes
+ jQuery(function() {
+ jQuery('input[data-autocomplete], input.ui-autocomplete-input').each(function() {
+ var input = jQuery(this);
+
+ input.on('keypress', function(event) {
+ if (event.keyCode === 13 && jQuery('ul.ui-autocomplete').is(':visible')) {
+ return false;
+ }
+ });
+ });
+ });
+
function escapeCssSelector(str) {
return str.replace(/([^A-Za-z0-9_-])/g,'\\$1');
}
diff --cc t/assets/web.t
index 8e6f75fdad,479c02e79b..45080e1006
--- a/t/assets/web.t
+++ b/t/assets/web.t
@@@ -107,6 -107,22 +107,22 @@@ diag "Create with CFs in other groups"
$m->content_unlike(qr/Purchased.*?must match .*?Year/, "Lacks validation error for Purchased");
}
+ diag "Bulk update";
+ {
- $m->follow_link_ok( { id => 'assets-search' }, "Asset search page" );
++ $m->follow_link_ok( { id => 'assets-simple_search' }, "Asset search page" );
+ $m->submit_form_ok( { form_id => 'AssetSearch' }, "Search assets" );
+ $m->follow_link_ok( { text => 'Bulk Update' }, "Asset bulk update page" );
+
+ my $form = $m->form_id('BulkUpdate');
+ my $status_input = $form->find_input('UpdateStatus');
+ is_deeply(
+ [ sort $status_input->possible_values ],
+ [ '', 'allocated', 'deleted', 'in-use', 'new', 'recycled', 'stolen' ],
+ 'Status options'
+ );
+ # TODO: test more bulk update actions
+ }
+
# XXX TODO: test other modify pages
done_testing;
diff --cc t/web/html_template.t
index d6d68e8a27,17d0c3e634..abb895c75a
--- a/t/web/html_template.t
+++ b/t/web/html_template.t
@@@ -62,10 -61,35 +62,36 @@@ diag('test real mail outgoing') if $ENV
# $mail is utf8 encoded
my ($mail) = RT::Test->fetch_caught_mails;
$mail = Encode::decode("UTF-8", $mail );
- like( $mail, qr/$template.*$template/s, 'mail has template content $template twice' );
- like( $mail, qr/$subject.*$subject/s, 'mail has ticket subject $sujbect twice' );
- like( $mail, qr/$content.*$content/s, 'mail has ticket content $content twice' );
- like( $mail, qr!<h1>$content</h1>!, 'mail has ticket html content <h1>$content</h1>' );
+ my $quoted_template = MIME::QuotedPrint::encode_qp( Encode::encode( 'UTF-8', $template ), '' );
+ my $quoted_subject = MIME::QuotedPrint::encode_qp( Encode::encode( 'UTF-8', $subject ), '' );
+ my $quoted_content = MIME::QuotedPrint::encode_qp( Encode::encode( 'UTF-8', $content ), '' );
+ like( $mail, qr/$quoted_template.*$quoted_template/s, 'mail has template content $template twice' );
+ like( $mail, qr/$quoted_subject.*$quoted_subject/s, 'mail has ticket subject $sujbect twice' );
+ like( $mail, qr/$quoted_content.*$quoted_content/s, 'mail has ticket content $content twice' );
+ like( $mail, qr!<h1>$quoted_content</h1>!, 'mail has ticket html content <h1>$content</h1>' );
+ }
+
+ diag('test long line mails') if $ENV{TEST_VERBOSE};
+ {
+ $m->get_ok( $baseurl . '/Ticket/Create.html?Queue=1' );
+
+ $m->submit_form(
+ form_name => 'TicketCreate',
+ fields => {
+ Subject => $subject,
+ Content => 'a' x 1000,
+ ContentType => 'text/html',
+ },
++ button => 'SubmitTicket',
+ );
+ $m->content_like( qr/Ticket \d+ created/i, 'created the ticket' );
- $m->follow_link( text => 'Show' );
++ $m->follow_link( url_regex => qr/ShowEmailRecord\.html/ );
+ $m->content_contains( $template, "html has $template" );
+ $m->content_contains( $subject, "html has ticket subject $subject" );
+ $m->text_contains( 'a' x 1000, "html has 1000 continuous a" );
+ my ($mail) = RT::Test->fetch_caught_mails;
+ ok( $mail =~ /Content-Transfer-Encoding: quoted-printable/, 'mail is quoted-printable encoded' );
+ ok( $mail !~ /a{1000}/, 'mail lacks 1000 continuous a' );
}
done_testing;
diff --cc t/web/ticket_time.t
index 0000000000,52ac777abf..909a55b301
mode 000000,100644..100644
--- a/t/web/ticket_time.t
+++ b/t/web/ticket_time.t
@@@ -1,0 -1,147 +1,149 @@@
+ use strict;
+ use warnings;
+ use RT::Test tests => undef;
+
+ my ( $baseurl, $m ) = RT::Test->started_ok;
+ ok( $m->login, "Logged in" );
+
+ my $queue = RT::Test->load_or_create_queue( Name => 'General' );
+ ok( $queue->id, "loaded the General queue" );
+
+ my %valid = (
+ '0' => 0,
+ '-8' => -8,
+ '2' => 2,
+ '5.' => 5,
+ '5.5' => 6,
+ ' 15 ' => 15,
+ '1,000' => 1000,
+ '.5h' => 30,
+ '1.5h' => 90,
+ '2h' => 120,
+ );
+ my @invalid = ( 'a', '3;4', '3+4' );
+
+ $m->goto_create_ticket( $queue );
+ for my $time ( @invalid ) {
+ my ( $number, $hour ) = $time =~ /^(.+?)(h?)$/;
+
+ $m->submit_form_ok(
+ {
+ form_name => 'TicketCreate',
+ fields => { TimeEstimated => $number, $hour ? ( 'TimeEstimated-TimeUnits' => 'hours' ) : (), },
++ button => 'SubmitTicket',
+ },
+ "Submit time $time",
+ );
+ $m->text_contains( 'Invalid TimeEstimated: it should be a number' );
+ $m->text_unlike( qr/Ticket \d+ created in queue/ );
+ }
+
+ for my $time ( sort keys %valid ) {
+ my ( $number, $hour ) = $time =~ /^(.+?)(h?)$/;
+
+ $m->goto_create_ticket( $queue );
+ $m->submit_form_ok(
+ {
+ form_name => 'TicketCreate',
+ fields => { TimeEstimated => $number, $hour ? ( 'TimeEstimated-TimeUnits' => 'hours' ) : (), },
++ button => 'SubmitTicket',
+ },
+ "Submit time $time",
+ );
+ $m->text_lacks( 'Invalid TimeEstimated: it should be a number' );
+ $m->text_like( qr/Ticket \d+ created in queue/ );
+ my $ticket = RT::Test->last_ticket;
+ is( $ticket->TimeEstimated, $valid{$time}, 'TimeEstimated is set' );
+ }
+
+ my $ticket = RT::Test->last_ticket;
+ for my $page ( qw/Modify ModifyAll/ ) {
+ $m->goto_ticket( $ticket->id, $page );
+
+ for my $time ( @invalid ) {
+ my ( $number, $hour ) = $time =~ /^(.+?)(h?)$/;
+
+ $m->submit_form_ok(
+ {
+ form_name => "Ticket$page",
+ fields => { map { $_ => $number, $hour ? ( "$_-TimeUnits" => 'hours' ) : () } qw/TimeLeft TimeWorked/ },
+ },
+ "Submit time $time",
+ );
+
+ $ticket->Load( $ticket->id );
+ for my $field ( qw/TimeLeft TimeWorked/ ) {
+ $m->text_contains( "Invalid $field: it should be a number" );
+ ok( !$ticket->$field, "$field is not updated" );
+ }
+ }
+
+ for my $time ( sort keys %valid ) {
+ my ( $number, $hour ) = $time =~ /^(.+?)(h?)$/;
+
+ $m->submit_form_ok(
+ {
+ form_name => "Ticket$page",
+ fields => { map { $_ => $number, $hour ? ( "$_-TimeUnits" => 'hours' ) : () } qw/TimeLeft TimeWorked/ },
+ },
+ "Submit time $time",
+ );
+ $ticket->Load( $ticket->id );
+
+ for my $field ( qw/TimeLeft TimeWorked/ ) {
+ $m->text_lacks( "Invalid $field: it should be a number" );
+ if ( $field eq 'TimeLeft' ) {
+ $m->text_like( qr/$field changed/ );
+ }
+ else {
+ $m->text_like( qr/worked -?[\d.]+ (?:minute|hour)|adjusted time worked/i );
+ }
+ is( $ticket->$field, $valid{$time}, "$field is updated" );
+ }
+ }
+
+ for my $field ( qw/TimeLeft TimeWorked/ ) {
+ my $set_method = "Set$field";
+ my ( $ret, $msg ) = $ticket->$set_method( 0 );
+ ok( $ret, 'Reset $field to 0' );
+ }
+ }
+
+ $m->goto_ticket( $ticket->id, 'Update' );
+
+ for my $time ( @invalid ) {
+ my ( $number, $hour ) = $time =~ /^(.+?)(h?)$/;
+
+ $m->submit_form_ok(
+ {
+ form_name => 'TicketUpdate',
+ fields => { UpdateTimeWorked => $number, $hour ? ( 'UpdateTimeWorked-TimeUnits' => 'hours' ) : (), },
+ button => 'SubmitTicket',
+ },
+ "Submit time $time",
+ );
+ $m->text_contains( 'Invalid UpdateTimeWorked: it should be a number' );
+ $ticket->Load( $ticket->id );
+ ok( !$ticket->TimeWorked, 'TimeWorked is not updated' );
+ }
+
+ my $time_worked = $ticket->TimeWorked;
+ for my $time ( sort keys %valid ) {
+ my ( $number, $hour ) = $time =~ /^(.+?)(h?)$/;
+
+ $m->goto_ticket( $ticket->id, 'Update' );
+ $m->submit_form_ok(
+ {
+ form_name => 'TicketUpdate',
+ fields => { UpdateTimeWorked => $number, $hour ? ( 'UpdateTimeWorked-TimeUnits' => 'hours' ) : (), },
+ button => 'SubmitTicket',
+ },
+ "Submit time $time",
+ );
+ $m->text_lacks( 'Invalid UpdateTimeWorked: it should be a number' );
+ $ticket->Load( $ticket->id );
+ is( $ticket->TimeWorked, $time_worked + $valid{$time}, 'TimeWorked is updated' );
+ $time_worked = $ticket->TimeWorked;
+ }
+
+ done_testing;
diff --cc t/web/ticket_txn_content.t
index 60240c63fb,773f3afe87..d092aa9015
--- a/t/web/ticket_txn_content.t
+++ b/t/web/ticket_txn_content.t
@@@ -112,3 -112,42 +112,44 @@@ for my $type ( 'text/plain', 'text/html
$m->content_contains("this is main reply content", 'email contains main reply content');
$m->back;
}
+
+ $m->goto_create_ticket( $qid );
+ $m->submit_form_ok(
+ {
+ form_name => 'TicketCreate',
+ fields => {
+ Subject => 'with main body',
+ Content => 'this is main body',
+ Attach => $plain_file,
- }
++ },
++ button => 'SubmitTicket',
+ },
+ 'submit TicketCreate form'
+ );
+ $m->text_like( qr/Ticket \d+ created in queue/, 'ticket is created' );
-$m->content_contains( "Download $plain_name", 'download plain file link' );
-$m->follow_link_ok( { text => 'Reply', url_regex => qr/QuoteTransaction=/ }, 'reply the create transaction' );
++ok( $m->find_link( text => $plain_name ), 'download plain file link' );
++$m->follow_link_ok( { url_regex => qr/QuoteTransaction=/ }, 'reply the create transaction' );
+ my $form = $m->form_name( 'TicketUpdate' );
+ my $content = $form->find_input( 'UpdateContent' );
+ like( $content->value, qr/this is main body/, 'has transaction content' );
+
+ $m->goto_create_ticket( $qid );
+ $m->submit_form_ok(
+ {
+ form_name => 'TicketCreate',
+ fields => {
+ Subject => 'without main body',
+ Attach => $plain_file,
- }
++ },
++ button => 'SubmitTicket',
+ },
+ 'submit TicketCreate form'
+ );
+ $m->text_like( qr/Ticket \d+ created in queue/, 'ticket is created' );
-$m->content_contains( "Download $plain_name", 'download plain file link' );
-$m->follow_link_ok( { text => 'Reply', url_regex => qr/QuoteTransaction=/ }, 'reply the create transaction' );
++ok( $m->find_link( text => $plain_name ), 'download plain file link' );
++$m->follow_link_ok( { url_regex => qr/QuoteTransaction=/ }, 'reply the create transaction' );
+ $form = $m->form_name( 'TicketUpdate' );
+ $content = $form->find_input( 'UpdateContent' );
+ like( $content->value, qr/This transaction appears to have no content/, 'no transaction content' );
+
+ done_testing;
-----------------------------------------------------------------------
More information about the rt-commit
mailing list