diff --git a/judo-ui-react-itest/RelationTest/LICENSE.txt b/judo-ui-react-itest/RelationTest/LICENSE.txt new file mode 100644 index 00000000..d3087e4c --- /dev/null +++ b/judo-ui-react-itest/RelationTest/LICENSE.txt @@ -0,0 +1,277 @@ +Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + +"Contributor" means any person or entity that Distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions Distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement +or any Secondary License (as applicable), including Contributors. + +"Derivative Works" shall mean any work, whether in Source Code or other +form, that is based on (or derived from) the Program and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. + +"Modified Works" shall mean any work in Source Code or other form that +results from an addition to, deletion from, or modification of the +contents of the Program, including, for purposes of clarity any new file +in Source Code form that contains any contents of the Program. Modified +Works shall not include works that contain only declarations, +interfaces, types, classes, structures, or files of the Program solely +in each case in order to link to, bind by name, or subclass the Program +or Modified Works thereof. + +"Distribute" means the acts of a) distributing or b) making available +in any manner that enables the transfer of a copy. + +"Source Code" means the form of a Program preferred for making +modifications, including but not limited to software source code, +documentation source, and configuration files. + +"Secondary License" means either the GNU General Public License, +Version 2.0, or any later versions of that license, including any +exceptions or additional permissions as identified by the initial +Contributor. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + +3. REQUIREMENTS + +3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + +3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + +3.3 Contributors may not remove or alter any copyright, patent, +trademark, attribution notices, disclaimers of warranty, or limitations +of liability ("notices") contained within the Program from any copy of +the Program which they Distribute, provided that Contributors may add +their own appropriate notices. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, +the Contributor who includes the Program in a commercial product +offering should do so in a manner which does not create potential +liability for other Contributors. Therefore, if a Contributor includes +the Program in a commercial product offering, such Contributor +("Commercial Contributor") hereby agrees to defend and indemnify every +other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits +and other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the Program +in a commercial product offering. The obligations in this section do not +apply to any claims or Losses relating to any actual or alleged +intellectual property infringement. In order to qualify, an Indemnified +Contributor must: a) promptly notify the Commercial Contributor in +writing of such claim, and b) allow the Commercial Contributor to control, +and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those performance +claims and warranties, and if a court requires any other Contributor to +pay any damages as a result, the Commercial Contributor must pay +those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF +TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR +PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all +risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs +or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software +or hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it +fails to comply with any of the material terms or conditions of this +Agreement and does not cure such failure in a reasonable period of +time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use +and distribution of the Program as soon as reasonably practicable. +However, Recipient's obligations under this Agreement and any licenses +granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and +may only be modified in the following manner. The Agreement Steward +reserves the right to publish new versions (including revisions) of +this Agreement from time to time. No one other than the Agreement +Steward has the right to modify this Agreement. The Eclipse Foundation +is the initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +Distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is published, +Contributor may elect to Distribute the Program (including its +Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient +receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted +under this Agreement are reserved. Nothing in this Agreement is intended +to be enforceable by any entity that is not a Contributor or Recipient. +No third-party beneficiary rights are created under this Agreement. + +Exhibit A - Form of Secondary Licenses Notice + +"This Source Code may also be made available under the following +Secondary Licenses when the conditions for such availability set forth +in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), +version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. diff --git a/judo-ui-react-itest/RelationTest/model/RelationTest-ui.model b/judo-ui-react-itest/RelationTest/model/RelationTest-ui.model new file mode 100644 index 00000000..103acf2a --- /dev/null +++ b/judo-ui-react-itest/RelationTest/model/RelationTest-ui.model @@ -0,0 +1,2580 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LIST + CREATE + VALIDATE_CREATE + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + + + LIST + CREATE + VALIDATE_CREATE + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + + + LIST + CREATE + VALIDATE_CREATE + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + + + LIST + CREATE + VALIDATE_CREATE + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + + + LIST + CREATE + VALIDATE_CREATE + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + + + LIST + CREATE + VALIDATE_CREATE + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + + + LIST + CREATE + VALIDATE_CREATE + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + + + + + REFRESH + + + + LIST + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + + + LIST + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + + + LIST + CREATE + SET + UNSET + RANGE + VALIDATE_CREATE + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + + + LIST + CREATE + VALIDATE_CREATE + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + + + LIST + SET + UNSET + RANGE + REFRESH + UPDATE + VALIDATE_UPDATE + + + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + TEMPLATE + + + + LIST + CREATE + SET + ADD + REMOVE + RANGE + VALIDATE_CREATE + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + + + LIST + CREATE + VALIDATE_CREATE + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + + + LIST + CREATE + SET + ADD + REMOVE + RANGE + VALIDATE_CREATE + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + + + LIST + CREATE + SET + ADD + REMOVE + RANGE + VALIDATE_CREATE + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + + + LIST + CREATE + VALIDATE_CREATE + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + + + LIST + CREATE + SET + UNSET + RANGE + VALIDATE_CREATE + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + + + LIST + CREATE + VALIDATE_CREATE + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + + + LIST + CREATE + SET + UNSET + RANGE + VALIDATE_CREATE + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + + + LIST + CREATE + VALIDATE_CREATE + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + + + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + TEMPLATE + + + + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + TEMPLATE + + + + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + TEMPLATE + + + + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + TEMPLATE + + + + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + TEMPLATE + + + + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + TEMPLATE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/judo-ui-react-itest/RelationTest/pom.xml b/judo-ui-react-itest/RelationTest/pom.xml new file mode 100644 index 00000000..21baecb0 --- /dev/null +++ b/judo-ui-react-itest/RelationTest/pom.xml @@ -0,0 +1,26 @@ + + + 4.0.0 + + hu.blackbelt.judo.generator + judo-ui-react-itest + ${revision} + + relationtest-frontend-react + ${revision} + + JUDO UI React Frontend Generator ITest - RelationTest + + pom + + + RelationTest + ${basedir}/generator-overrides + ${project.parent.parent.basedir}/.nodejs + + + + relation_test__actor + + diff --git a/judo-ui-react-itest/RelationTest/relation_test__actor/LICENSE.txt b/judo-ui-react-itest/RelationTest/relation_test__actor/LICENSE.txt new file mode 100644 index 00000000..d3087e4c --- /dev/null +++ b/judo-ui-react-itest/RelationTest/relation_test__actor/LICENSE.txt @@ -0,0 +1,277 @@ +Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + +"Contributor" means any person or entity that Distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions Distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement +or any Secondary License (as applicable), including Contributors. + +"Derivative Works" shall mean any work, whether in Source Code or other +form, that is based on (or derived from) the Program and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. + +"Modified Works" shall mean any work in Source Code or other form that +results from an addition to, deletion from, or modification of the +contents of the Program, including, for purposes of clarity any new file +in Source Code form that contains any contents of the Program. Modified +Works shall not include works that contain only declarations, +interfaces, types, classes, structures, or files of the Program solely +in each case in order to link to, bind by name, or subclass the Program +or Modified Works thereof. + +"Distribute" means the acts of a) distributing or b) making available +in any manner that enables the transfer of a copy. + +"Source Code" means the form of a Program preferred for making +modifications, including but not limited to software source code, +documentation source, and configuration files. + +"Secondary License" means either the GNU General Public License, +Version 2.0, or any later versions of that license, including any +exceptions or additional permissions as identified by the initial +Contributor. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + +3. REQUIREMENTS + +3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + +3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + +3.3 Contributors may not remove or alter any copyright, patent, +trademark, attribution notices, disclaimers of warranty, or limitations +of liability ("notices") contained within the Program from any copy of +the Program which they Distribute, provided that Contributors may add +their own appropriate notices. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, +the Contributor who includes the Program in a commercial product +offering should do so in a manner which does not create potential +liability for other Contributors. Therefore, if a Contributor includes +the Program in a commercial product offering, such Contributor +("Commercial Contributor") hereby agrees to defend and indemnify every +other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits +and other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the Program +in a commercial product offering. The obligations in this section do not +apply to any claims or Losses relating to any actual or alleged +intellectual property infringement. In order to qualify, an Indemnified +Contributor must: a) promptly notify the Commercial Contributor in +writing of such claim, and b) allow the Commercial Contributor to control, +and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those performance +claims and warranties, and if a court requires any other Contributor to +pay any damages as a result, the Commercial Contributor must pay +those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF +TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR +PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all +risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs +or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software +or hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it +fails to comply with any of the material terms or conditions of this +Agreement and does not cure such failure in a reasonable period of +time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use +and distribution of the Program as soon as reasonably practicable. +However, Recipient's obligations under this Agreement and any licenses +granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and +may only be modified in the following manner. The Agreement Steward +reserves the right to publish new versions (including revisions) of +this Agreement from time to time. No one other than the Agreement +Steward has the right to modify this Agreement. The Eclipse Foundation +is the initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +Distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is published, +Contributor may elect to Distribute the Program (including its +Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient +receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted +under this Agreement are reserved. Nothing in this Agreement is intended +to be enforceable by any entity that is not a Contributor or Recipient. +No third-party beneficiary rights are created under this Agreement. + +Exhibit A - Form of Secondary Licenses Notice + +"This Source Code may also be made available under the following +Secondary Licenses when the conditions for such availability set forth +in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), +version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. diff --git a/judo-ui-react-itest/RelationTest/relation_test__actor/pom.xml b/judo-ui-react-itest/RelationTest/relation_test__actor/pom.xml new file mode 100644 index 00000000..7d4d6c28 --- /dev/null +++ b/judo-ui-react-itest/RelationTest/relation_test__actor/pom.xml @@ -0,0 +1,193 @@ + + 4.0.0 + + + hu.blackbelt.judo.generator + relationtest-frontend-react + ${revision} + + relationtest-application-frontend-react-relation_test__actor + RelationTest - Actor frontend react + RelationTest - Actor react frontend + + bundle + + + actor + actor + Actor + Actor + + ${project.parent.basedir}/model/${model-name}-ui.model + + ${project.parent.parent.parent.basedir}/.nodejs + ${basedir}/target/frontend-react + + + + + + org.apache.felix + maven-bundle-plugin + 5.1.8 + true + + + /${model-name}/${actor} + + /=${generation-target}/dist + + + + + + + hu.blackbelt.judo.meta + judo-ui-generator-maven-plugin + ${judo-meta-ui-version} + + + execute-ui-services-generation + generate-sources + + generate + + + + mvn:hu.blackbelt.judo.generator:judo-ui-typescript-rest-api:${judo-ui-typescript-rest-version} + mvn:hu.blackbelt.judo.generator:judo-ui-typescript-rest-service:${judo-ui-typescript-rest-version} + mvn:hu.blackbelt.judo.generator:judo-ui-typescript-rest-axios:${judo-ui-typescript-rest-version} + + ui-typescript-rest + + hu.blackbelt.judo.generator.commons, + hu.blackbelt.judo.ui.generator.typescript.rest + + + ${actor-fq-name} + + ${ui-model} + ${generation-target}/src/services + + + + execute-ui-generation + generate-sources + + generate + + + + mvn:hu.blackbelt.judo.generator:judo-ui-react:${revision} + + ui-react + + hu.blackbelt.judo.generator.commons, + hu.blackbelt.judo.ui.generator.typescript.rest.commons, + hu.blackbelt.judo.ui.generator.react + + + ${actor-fq-name} + + ${ui-model} + ${generation-target} + + true + ${model-name} + ${appScope} + ${appVersion} + + ${defaultLanguage} + ${tablePageLimit} + + ${muiLicensePlan} + + + + + + + hu.blackbelt.judo.meta + hu.blackbelt.judo.meta.ui.model + ${judo-meta-ui-version} + + + hu.blackbelt.judo.generator + judo-generator-commons + ${judo-generator-commons-version} + + + hu.blackbelt.judo.generator + judo-ui-typescript-rest-commons + ${judo-ui-typescript-rest-version} + + + hu.blackbelt.judo.generator + judo-ui-typescript-rest-api + ${judo-ui-typescript-rest-version} + + + hu.blackbelt.judo.generator + judo-ui-typescript-rest-service + ${judo-ui-typescript-rest-version} + + + hu.blackbelt.judo.generator + judo-ui-typescript-rest-axios + ${judo-ui-typescript-rest-version} + + + hu.blackbelt.judo.generator + judo-ui-react + ${revision} + + + + + + com.github.eirslett + frontend-maven-plugin + ${frontend-maven-plugin-version} + + + pnpm install + + pnpm + + generate-sources + + install + + + + + format code + + pnpm + + generate-sources + + run format + + + + + build + + pnpm + + generate-sources + + run build + + + + + ${node-install-dir} + ${generation-target} + + + + + diff --git a/judo-ui-react-itest/pom.xml b/judo-ui-react-itest/pom.xml index ee91845d..08bbb639 100644 --- a/judo-ui-react-itest/pom.xml +++ b/judo-ui-react-itest/pom.xml @@ -32,6 +32,7 @@ ActionGroupTestPro CRUDActionsTest OperationParametersTest + RelationTest diff --git a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiActionsHelper.java b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiActionsHelper.java index 3c48e301..5b23ae31 100644 --- a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiActionsHelper.java +++ b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiActionsHelper.java @@ -222,31 +222,6 @@ public static String getServiceMethodSuffix(Action action) { return suffix; } - public static String getDialogOpenParameters(PageDefinition pageDefinition) { - List result = new ArrayList<>(); - result.add("ownerData: any"); - if (pageDefinition.getContainer().isView() && isPageDataElementUnmappedSingle(pageDefinition)) { - result.add("data: " + classDataName(getReferenceClassType(pageDefinition), "")); - } - if (!pageDefinition.getContainer().isIsSelector()) { - if (pageDefinition.getContainer().isView()) { - result.add("templateDataOverride?: " + classDataName(getReferenceClassType(pageDefinition), "Stored")); - } else { - result.add("templateDataOverride?: Partial<" + classDataName(getReferenceClassType(pageDefinition), ">")); - } - } else if (pageDefinition.getContainer().isIsRelationSelector()) { - result.add("alreadySelected: " + classDataName(getReferenceClassType(pageDefinition), "Stored") + "[]"); - } - result.add("isDraft?: boolean"); - if (!pageDefinition.getContainer().isIsSelector()) { - result.add("ownerValidation?: (data: " + classDataName(getReferenceClassType(pageDefinition), "") + ") => Promise"); - } - if (pageDefinition.getContainer().isForm()) { - result.add("maskRequest?: string"); - } - return String.join(", ", result); - } - public static boolean isPageDataElementUnmappedSingle(PageDefinition pageDefinition) { return pageHasOutputTarget(pageDefinition) && !getPageOutputTarget(pageDefinition).isIsMapped() @@ -254,81 +229,11 @@ public static boolean isPageDataElementUnmappedSingle(PageDefinition pageDefinit !parameterType.isIsCollection(); } - public static String getFormOpenParameters(PageDefinition pageDefinition, Action action) { - List tokens = new ArrayList<>(); - - if (action.getActionDefinition().getIsOpenFormAction() && pageDefinition.getContainer().isIsRelationSelector()) { - return "ownerData"; - } - if (action.getActionDefinition().getTargetType() != null) { - tokens.add("target"); - } else { - if (pageDefinition.getContainer().isTable()) { - if (pageDefinition.getRelationType() != null && !pageDefinition.getRelationType().isIsAccess()) { - tokens.add("{ __signedIdentifier: signedIdentifier } as JudoIdentifiable"); - } else { - tokens.add("null as any"); - } - } else { - tokens.add("data"); - if (isRelationOpenCreateActionOnEagerView(pageDefinition, action)) { - if (tokens.size() < 2) { - tokens.add("undefined"); - } - tokens.add("true"); - } - } - } - if (isRelationOpenCreateActionOnForm(pageDefinition, action)) { - if (tokens.size() < 2) { - tokens.add("undefined"); - } - tokens.add("true"); - tokens.add("validate" + firstToUpper(action.getTargetDataElement().getName())); - - Link link = getLinkParentForActionDefinition(action.getActionDefinition()); - - if (link != null) { - var col = getFirstAutocompleteColumnForLink(link); - tokens.add("'{" + (col != null ? col.getAttributeType().getName() : "") + "}'"); - } + public static RelationType getRelationTypeForActionTarget(Action action) { + if (action.getTargetDataElement() instanceof RelationType check) { + return check; } - - return String.join(", ", tokens); - } - - public static String getSelectorOpenActionParameters(Action action, PageContainer container) { - List tokens = new ArrayList<>(); - if (container.isTable()) { - if (action.getTargetPageDefinition().getContainer().isIsRelationSelector()) { - tokens.add("{ __signedIdentifier: signedIdentifier }"); - } else { - tokens.add("[]"); - } - } else { - if (action.getActionDefinition().getTargetType() != null) { - tokens.add("target!"); - } else { - tokens.add("data"); - } - } - - if (action.getTargetPageDefinition().getContainer().isIsRelationSelector()) { - if (action.getTargetDataElement() instanceof RelationType check) { - if (container.isTable()) { - tokens.add("[]"); - } else { - String result = "data." + check.getName(); - boolean isCollection = check.isIsCollection(); - if (isCollection) { - tokens.add(result + " ?? []"); - } else { - tokens.add(result + "? [" + result + "] : []"); - } - } - } - } - return String.join(", ", tokens); + return null; } public static boolean isActionAddOrSet(ActionDefinition actionDefinition) { @@ -369,7 +274,7 @@ public static String refreshActionDataParameter(Action action) { return "ownerData"; } if (pageHasSignedId(pageDefinition)) { - return "{ __signedIdentifier: signedIdentifier } as JudoIdentifiable"; + return "{ __signedIdentifier: signedIdentifier } as any"; } if (isSingleAccessPage(pageDefinition)) { return "singletonHost.current"; @@ -390,10 +295,7 @@ public static String postCreateActionParams(PageDefinition page, ActionDefinitio public static boolean isRelationOpenCreateActionOnForm(PageDefinition pageDefinition, Action action) { return pageDefinition.getContainer().isForm() - && action.getIsOpenFormAction() - && action.getTargetDataElement() != null - && action.getTargetDataElement() instanceof RelationType relationType - && relationType.isIsInlineCreatable(); + && action.getIsOpenFormAction(); } public static boolean isRelationOpenCreateActionOnEagerView(PageDefinition pageDefinition, Action action) { @@ -543,4 +445,16 @@ public static boolean isActionParentEagerElement(Action action) { } return false; } + + public static boolean isActionParentEagerTable(Action action) { + Table table = getTableParentForActionDefinition(action.getActionDefinition()); + return table != null && table.isIsEager(); + } + + public static boolean isRefreshPageInitializer(Action action, PageDefinition page) { + if (action.getIsRefreshAction()) { + return page.getContainer().getOnInit() != null && page.getContainer().getOnInit().equals(action.getActionDefinition()); + } + return false; + } } diff --git a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiImportHelper.java b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiImportHelper.java index 2c31f22c..ab074759 100644 --- a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiImportHelper.java +++ b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiImportHelper.java @@ -127,31 +127,27 @@ public static void fillFlattenedVisualElements(Container container, SortedSet getTableAPIImports(Table table, PageContainer container) { Set res = new HashSet<>(); if (!container.isTable() && container.getDataElement() instanceof ClassType dataElement) { res.add(classDataName(dataElement, "")); - res.add(classDataName(dataElement, "Stored")); } ClassType classType = getReferenceClassType(table); if (classType != null) { res.add(classDataName(classType, "")); - res.add(classDataName(classType, "Stored")); - res.add(classDataName(classType, "QueryCustomizer")); } - return res.stream().sorted().collect(Collectors.joining(", ")); + return res.stream().sorted().toList(); } - public static String getLinkAPIImports(Link link, PageContainer container) { + public static List getLinkAPIImports(Link link, PageContainer container) { Set res = new HashSet<>(); if (!container.isTable() && container.getDataElement() instanceof ClassType dataElement) { res.add(classDataName(dataElement, "")); - res.add(classDataName(dataElement, "Stored")); } if (link.getDataElement() instanceof RelationType relationType) { @@ -159,15 +155,9 @@ public static String getLinkAPIImports(Link link, PageContainer container) { if (classType != null) { res.add(classDataName(classType, "")); - res.add(classDataName(classType, "Stored")); - res.add(classDataName(classType, "QueryCustomizer")); } } - if (link.getRefreshActionDefinition() != null) { - res.add("JudoRestResponse"); - } - - return res.stream().sorted().collect(Collectors.joining(", ")); + return res.stream().sorted().toList(); } } diff --git a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiPageHelper.java b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiPageHelper.java index 61ff176a..eeb4eec7 100644 --- a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiPageHelper.java +++ b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiPageHelper.java @@ -30,6 +30,7 @@ import java.util.stream.Collectors; import static hu.blackbelt.judo.ui.generator.react.UiActionsHelper.getActionOperationOutputClassType; +import static hu.blackbelt.judo.ui.generator.react.UiActionsHelper.isPageDataElementUnmappedSingle; import static hu.blackbelt.judo.ui.generator.react.UiWidgetHelper.collectVisualElementsMatchingCondition; import static hu.blackbelt.judo.ui.generator.react.UiWidgetHelper.getReferenceClassType; import static hu.blackbelt.judo.ui.generator.typescript.rest.commons.UiCommonsHelper.*; @@ -458,7 +459,7 @@ public static boolean hasExportAction(PageDefinition pageDefinition) { return pageDefinition.getActions().stream().anyMatch(page -> page.getIsExportAction()); } - public static boolean isDialogValidationSupported(PageDefinition pageDefinition) { + public static boolean isValidationSupported(PageDefinition pageDefinition) { if (pageDefinition.getRelationType() != null) { if (pageDefinition.getContainer().isView() && pageDefinition.getRelationType().getIsUpdateValidatable()) { return true; @@ -468,4 +469,20 @@ public static boolean isDialogValidationSupported(PageDefinition pageDefinition) } return false; } + + public static String dialogDataInitialValue(PageDefinition pageDefinition) { + if (pageDefinition.getContainer().isTable()) { + return "[]"; + } else { + if (pageDefinition.getContainer().isView()) { + if (isPageDataElementUnmappedSingle(pageDefinition)) { + return "output"; + } else { + return "(isDraft ? simpleCloneDeep(getValue(ownerData, dataPath!, {})) : {})"; + } + } else { + return "(templateDataOverride ?? {})"; + } + } + } } diff --git a/judo-ui-react/src/main/resources/actor/src/components/dialog/FilterDialog.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/components/dialog/FilterDialog.tsx.hbs index d3d81533..ab2b4193 100644 --- a/judo-ui-react/src/main/resources/actor/src/components/dialog/FilterDialog.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/components/dialog/FilterDialog.tsx.hbs @@ -30,7 +30,7 @@ import type { Operation, } from '../../components-api'; import { FilterType } from '../../components-api'; -import { dateToJudoDateString, exists, uiDateToServiceDate, serviceDateToUiDate } from '../../utilities'; +import { exists } from '../../utilities'; import { mainContainerPadding } from '../../theme'; import { _BooleanOperation, _EnumerationOperation, _NumericOperation, _StringOperation } from '~/services/data-api/common/operations'; import { DropdownButton } from '../DropdownButton'; @@ -154,9 +154,9 @@ const FilterInput = ({ filter, setFilterValue, valueId }: FilterInputProps) => { setFilterValue(filter, uiDateToServiceDate(newValue))} + onChange={(newValue) => setFilterValue(filter, newValue)} slotProps={ { textField: { InputProps: { @@ -175,7 +175,7 @@ const FilterInput = ({ filter, setFilterValue, valueId }: FilterInputProps) => { boolean; enabledByName?: string; relationName: string; + filtersSerializer: FiltersSerializer; } export function EagerTable>(props: EagerTableProps) { @@ -120,6 +121,7 @@ export function EagerTable([]); const [sortModel, setSortModel] = useState(getItemParsedWithDefault(sortModelKey, defaultSortParamsForTable)); const [filterModel, setFilterModel] = useState(getItemParsedWithDefault(filterModelKey, { items: [] })); - const [filters, setFilters] = useState(getItemParsedWithDefault(filtersKey, [])); + const [filters, setFilters] = useState(filtersSerializer.deserialize(getItemParsedWithDefault(filtersKey, []))); {{# if isMUILicensePlanPro }} const [columnState, setColumnState] = useState(getItemParsedWithDefault(columnStateKey, [])); {{/ if }} @@ -264,7 +266,7 @@ export function EagerTable) => { // remove previous filter values, so that we can always start with a clean slate diff --git a/judo-ui-react/src/main/resources/actor/src/components/table/LazyTable.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/components/table/LazyTable.tsx.hbs index fa5e615a..0cc89dad 100644 --- a/judo-ui-react/src/main/resources/actor/src/components/table/LazyTable.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/components/table/LazyTable.tsx.hbs @@ -45,7 +45,7 @@ import { mapFilterToFilterModel, {{/ if }} } from '~/utilities'; -import type { TableRowAction{{# if isMUILicensePlanPro }}, PersistedColumnInfo{{/ if }} } from '~/utilities'; +import type { TableRowAction, FiltersSerializer{{# if isMUILicensePlanPro }}, PersistedColumnInfo{{/ if }} } from '~/utilities'; import type { JudoStored } from '~/services/data-api/common/JudoStored'; import type { QueryCustomizer } from '~/services/data-api/common/QueryCustomizer'; import { CustomTablePagination } from '~/components'; @@ -96,6 +96,7 @@ interface LazyTableProps>(props: LazyTableProps) { @@ -139,6 +140,7 @@ export function LazyTable([]); const [sortModel, setSortModel] = useState(getItemParsedWithDefault(sortModelKey, defaultSortParamsForTable)); const [filterModel, setFilterModel] = useState(getItemParsedWithDefault(filterModelKey, { items: [] })); - const [filters, setFilters] = useState(getItemParsedWithDefault(filtersKey, [])); + const [filters, setFilters] = useState(filtersSerializer.deserialize(getItemParsedWithDefault(filtersKey, []))); {{# if isMUILicensePlanPro }} const [columnState, setColumnState] = useState(getItemParsedWithDefault(columnStateKey, [])); {{/ if }} @@ -308,7 +310,7 @@ export function LazyTable { setPage(0); setFilters(newFilters); - setItemStringified(filtersKey, newFilters); + setItemStringified(filtersKey, filtersSerializer.serialize(newFilters)); setQueryCustomizer((prevQueryCustomizer: S | QueryCustomizer) => { // remove previous filter values, so that we can always start with a clean slate diff --git a/judo-ui-react/src/main/resources/actor/src/components/table/table-row-actions.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/components/table/table-row-actions.tsx.hbs index 0b2e497e..b6bbe8f5 100644 --- a/judo-ui-react/src/main/resources/actor/src/components/table/table-row-actions.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/components/table/table-row-actions.tsx.hbs @@ -17,7 +17,7 @@ export const columnsActionCalculator: ColumnActionsProvider = ( t: TFunction, isLoading: boolean, getSelectedRows: () => any[], - ownerdata?: any, + ownerData?: any, options?: ColumnsActionsOptions, ): GridActionsColDef[] => { const safeOptions: ColumnsActionsOptions = { @@ -51,7 +51,7 @@ export const columnsActionCalculator: ColumnActionsProvider = ( key={a.id} variant="text" startIcon={a.icon} - disabled={a.disabled ? a.disabled(params.row, isLoading, getSelectedRows, ownerdata) : false} + disabled={a.disabled ? a.disabled(params.row, isLoading, getSelectedRows, ownerData) : false} onClick={() => a.action!(params.row)} > {a.label ?? ''} @@ -66,7 +66,7 @@ export const columnsActionCalculator: ColumnActionsProvider = ( label: action.label, startIcon: action.icon, onClick: () => action.action!(params.row), - disabled: action.disabled ? action.disabled(params.row, isLoading, getSelectedRows, ownerdata) : false, + disabled: action.disabled ? action.disabled(params.row, isLoading, getSelectedRows, ownerData) : false, }))} > diff --git a/judo-ui-react/src/main/resources/actor/src/components/widgets/Tags.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/components/widgets/Tags.tsx.hbs index 6cfd96b3..b3e638be 100644 --- a/judo-ui-react/src/main/resources/actor/src/components/widgets/Tags.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/components/widgets/Tags.tsx.hbs @@ -12,7 +12,7 @@ import { MdiIcon } from '~/components/MdiIcon'; import { debounce } from '@mui/material/utils'; import { debounceInputs } from '~/config/general'; import { QueryCustomizer } from '~/services/data-api/common/QueryCustomizer'; -import { FilterBytypesString } from '~/services/data-api/rest/FilterBytypesString'; +import { FilterByTypesString } from '~/services/data-api/rest/FilterByTypesString'; import { StringOperation } from '~/services/data-api/model/StringOperation'; export interface TagsProps { @@ -78,7 +78,7 @@ export function Tags (props: TagsProps) { const handleSearch = async (searchText: string) => { try { setLoading(true); - const filter: FilterBytypesString[] = (ownerData[name] as T[] ?? []).map((c: any) => ({ + const filter: FilterByTypesString[] = (ownerData[name] as T[] ?? []).map((c: any) => ({ value: c[autoCompleteAttribute]!, operator: StringOperation.notEqual, })); diff --git a/judo-ui-react/src/main/resources/actor/src/containers/components/link/index.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/containers/components/link/index.tsx.hbs index 21638fee..129fe5fe 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/components/link/index.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/components/link/index.tsx.hbs @@ -6,16 +6,23 @@ import { exists, processQueryCustomizer } from '~/utilities'; import { MdiIcon } from '~/components'; import { SingleRelationInput } from '~/components/widgets'; import { StringOperation } from '~/services/data-api/model/StringOperation'; -import type { - {{ getLinkAPIImports link container }} -} from '~/services/data-api'; +{{# each (getLinkAPIImports link container) as |imp| }} + import type { + {{ imp }}, + {{ imp }}Stored, + } from '~/services/data-api/model/{{ imp }}'; + import type { + {{ imp }}QueryCustomizer, + } from '~/services/data-api/rest/{{ imp }}QueryCustomizer'; +{{/ each }} +import type { JudoRestResponse } from '~/services/data-api/rest/requestResponse'; import { SingleRelationInputButtonProps } from '~/utilities/application/interfaces'; import type { {{ componentName link }}ActionDefinitions, {{ componentName link }}Props } from './types'; // XMIID: {{ getXMIID link }} // Name: {{ link.name }} export function {{ componentName link }}(props: {{ componentName link }}Props) { - const { ownerData, actions, storeDiff, submit, validationError, disabled, readOnly, editMode, isLoading, isDraft{{# unless link.isEager }}, refreshCounter{{/ unless }} } = props; + const { ownerData, actions, dataPath, storeDiff, submit, validationError, disabled, readOnly, editMode, isLoading, isDraft{{# unless link.isEager }}, refreshCounter{{/ unless }} } = props; const { t } = useTranslation(); const isRequired = useMemo(() => { diff --git a/judo-ui-react/src/main/resources/actor/src/containers/components/link/types.ts.hbs b/judo-ui-react/src/main/resources/actor/src/containers/components/link/types.ts.hbs index a4a9870b..56a97e31 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/components/link/types.ts.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/components/link/types.ts.hbs @@ -1,6 +1,13 @@ -import type { - {{ getLinkAPIImports link container }} -} from '~/services/data-api'; +{{# each (getLinkAPIImports link container) as |imp| }} + import type { + {{ imp }}, + {{ imp }}Stored, + } from '~/services/data-api/model/{{ imp }}'; + import type { + {{ imp }}QueryCustomizer, + } from '~/services/data-api/rest/{{ imp }}QueryCustomizer'; +{{/ each }} +import type { JudoRestResponse } from '~/services/data-api/rest/requestResponse'; export interface {{ componentName link }}ActionDefinitions { {{# each link.actionDefinitions as |actionDefinition| }} @@ -16,6 +23,7 @@ export interface {{ componentName link }}ActionDefinitions { export interface {{ componentName link }}Props { ownerData: {{ classDataName container.dataElement '' }} | {{ classDataName container.dataElement 'Stored' }}; + dataPath?: string; actions: {{ componentName link }}ActionDefinitions; storeDiff: (attributeName: keyof {{ classDataName container.dataElement '' }}, value: any) => void; submit: () => Promise; diff --git a/judo-ui-react/src/main/resources/actor/src/containers/components/table/index.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/containers/components/table/index.tsx.hbs index 7eee86e2..0b0ecfd4 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/components/table/index.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/components/table/index.tsx.hbs @@ -47,9 +47,15 @@ import type { ContextMenuApi } from '~/components/table/ContextMenu'; import type { Filter, FilterOption } from '~/components-api'; import { FilterType } from '~/components-api'; import { CUSTOM_VISUAL_ELEMENT_INTERFACE_KEY } from '~/custom'; -import type { - {{ getTableAPIImports table container }} -} from '~/services/data-api'; +{{# each (getTableAPIImports table container) as |imp| }} + import type { + {{ imp }}, + {{ imp }}Stored, + } from '~/services/data-api/model/{{ imp }}'; + import type { + {{ imp }}QueryCustomizer, + } from '~/services/data-api/rest/{{ imp }}QueryCustomizer'; +{{/ each }} {{# or (tableHasNumericColumn table) (tableHasDateColumn table) (tableHasDateTimeColumn table) }} import { useL10N } from '~/l10n/l10n-context'; {{/ or }} @@ -67,24 +73,19 @@ import { {{# if (tableHasBinaryColumn table) }} fileHandling, {{/ if }} - {{# or (tableHasDateTimeColumn table) (tableHasDateColumn table) }} - serviceDateToUiDate, - {{/ or }} - {{# if (tableHasTimeColumn table) }} - serviceTimeToUiTime, - {{/ if }} {{# if isUseInlineColumnFilters }} mapFilterModelToFilters, mapFilterToFilterModel, {{/ if }} mapAllFiltersToQueryCustomizerProperties, processQueryCustomizer, + serializeFilters, + deserializeFilters, {{# unless table.isEager }} useErrorHandler, {{/ unless }} } from '~/utilities'; -import type { SidekickComponentProps, DialogResult, TableRowAction, ToolBarActionProps, ColumnCustomizerHook{{# if isMUILicensePlanPro }}, PersistedColumnInfo{{/ if }} } from '~/utilities'; -import { useDataStore } from '~/hooks'; +import type { SidekickComponentProps, DialogResult, TableRowAction, ToolBarActionProps, ColumnCustomizerHook, FiltersSerializer{{# if isMUILicensePlanPro }}, PersistedColumnInfo{{/ if }} } from '~/utilities'; import { OBJECTCLASS } from '@pandino/pandino-api'; import { useTrackComponent, ComponentProxy } from '@pandino/react-hooks'; {{# if (stringValueIsTrue useTableRowHighlighting) }} @@ -94,15 +95,22 @@ import { TABLE_ROW_HIGHLIGHTING_HOOK_INTERFACE_KEY, transformRowStylings } from import type { RowStylerConfigured, TableRowHighlightingHook } from '~/theme/table-row-highlighting'; {{/ if }} import type { {{ componentName table }}ActionDefinitions, {{ componentName table }}Props } from './types'; +import { {{ classDataName (getReferenceClassType table) 'StoredSerializer' }} } from '~/services/data-api/rest/{{ classDataName (getReferenceClassType table) 'Serializer' }}'; export const {{ camelCaseNameToInterfaceKey (componentName table) }}_SIDEKICK_COMPONENT_INTERFACE_KEY = '{{ componentName table }}SidekickComponent'; +export const filtersSerializer: FiltersSerializer = { + serialize: (filters: Filter[]) => serializeFilters<{{ classDataName (getReferenceClassType table) 'Stored' }}>(filters, {{ classDataName (getReferenceClassType table) 'StoredSerializer' }}.getInstance()), + deserialize: (filters: Filter[]) => deserializeFilters<{{ classDataName (getReferenceClassType table) 'Stored' }}>(filters, {{ classDataName (getReferenceClassType table) 'StoredSerializer' }}.getInstance()), +}; + // XMIID: {{ getXMIID table }} // Name: {{ table.name }} export function {{ componentName table }}(props: {{ componentName table }}Props) { const { uniqueId, actions, + dataPath, refreshCounter, isOwnerLoading, isDraft, @@ -123,7 +131,6 @@ export function {{ componentName table }}(props: {{ componentName table }}Props) const sidekickComponentFilter = `(&(${OBJECTCLASS}=${CUSTOM_VISUAL_ELEMENT_INTERFACE_KEY})(component=${ {{ camelCaseNameToInterfaceKey (componentName table) }}_SIDEKICK_COMPONENT_INTERFACE_KEY }))`; const { openConfirmDialog } = useConfirmDialog(); - const { getItemParsed, getItemParsedWithDefault, setItemStringified } = useDataStore('sessionStorage'); const { t } = useTranslation(); {{# unless table.isEager }} const handleError = useErrorHandler(); @@ -270,6 +277,7 @@ export function {{ componentName table }}(props: {{ componentName table }}Props) enabledByName='{{ table.enabledBy.name }}' dataElement={ ownerData?.{{ table.dataElement.name }} } relationName='{{ table.relationName }}' + filtersSerializer={filtersSerializer} {{# if container.isSelector }} selectionDiff={selectionDiff} setSelectionDiff={setSelectionDiff} @@ -306,6 +314,7 @@ export function {{ componentName table }}(props: {{ componentName table }}Props) {{/ if }} {{# if container.isRelationSelector }} containerIsRelationSelector={ true } + alreadySelected={alreadySelected} {{/ if }} {{# if table.showTotalCount }} showTotalCount={ true } @@ -345,6 +354,7 @@ export function {{ componentName table }}(props: {{ componentName table }}Props) {{/ unless }} enabledByName='{{ table.enabledBy.name }}' relationName='{{ table.relationName }}' + filtersSerializer={filtersSerializer} {{# if container.isSelector }} selectionDiff={selectionDiff} setSelectionDiff={setSelectionDiff} diff --git a/judo-ui-react/src/main/resources/actor/src/containers/components/table/types.ts.hbs b/judo-ui-react/src/main/resources/actor/src/containers/components/table/types.ts.hbs index 0192c9e2..e9d8332d 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/components/table/types.ts.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/components/table/types.ts.hbs @@ -2,9 +2,15 @@ import type { ElementType, Dispatch, SetStateAction } from 'react'; import type { GridFilterModel, } from '@mui/x-data-grid{{ getMUIDataGridPlanSuffix }}'; -import type { - {{ getTableAPIImports table container }} -} from '~/services/data-api'; +{{# each (getTableAPIImports table container) as |imp| }} + import type { + {{ imp }}, + {{ imp }}Stored, + } from '~/services/data-api/model/{{ imp }}'; + import type { + {{ imp }}QueryCustomizer, + } from '~/services/data-api/rest/{{ imp }}QueryCustomizer'; +{{/ each }} import type { JudoRestResponse } from '~/services/data-api/rest/requestResponse'; import type { Filter, FilterOption } from '~/components-api'; import type { DialogResult } from '~/utilities'; @@ -55,6 +61,7 @@ export interface {{ componentName table }}ActionDefinitions { export interface {{ componentName table }}Props { uniqueId: string; actions: {{ componentName table }}ActionDefinitions; + dataPath?: string; refreshCounter: number; isOwnerLoading?: boolean; isDraft?: boolean; diff --git a/judo-ui-react/src/main/resources/actor/src/containers/container.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/containers/container.tsx.hbs index 9df29fa5..2568778e 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/container.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/container.tsx.hbs @@ -40,7 +40,7 @@ import type { {{# unless (containerIsEmptyDashboard container) }}{{ pageContaine export default function {{ containerComponentName container }}(props: {{ containerComponentName container }}Props) { {{# unless (containerIsEmptyDashboard container) }} // Container props - const { refreshCounter, isLoading, isDraft, actions: pageActions{{# if container.isSelector }}, selectionDiff, setSelectionDiff{{/ if }}{{# if container.isRelationSelector }}, alreadySelected{{/ if }}{{# unless container.table }}, data, isFormUpdateable, isFormDeleteable, storeDiff, editMode, validation, setValidation, submit{{/ unless }} } = props; + const { refreshCounter, isLoading, isDraft, dataPath, actions: pageActions{{# if container.isSelector }}, selectionDiff, setSelectionDiff{{/ if }}{{# if container.isRelationSelector }}, alreadySelected{{/ if }}{{# unless container.table }}, data, isFormUpdateable, isFormDeleteable, storeDiff, editMode, validation, setValidation, submit{{/ unless }} } = props; // Container hooks const { t } = useTranslation(); diff --git a/judo-ui-react/src/main/resources/actor/src/containers/dialog.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/containers/dialog.tsx.hbs index cd3788dd..4bcda2fc 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/dialog.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/dialog.tsx.hbs @@ -28,8 +28,11 @@ import { useConfirmDialog } from '~/components/dialog'; import type { {{ classDataName container.dataElement '' }}, {{ classDataName container.dataElement 'Stored' }}, - {{ classDataName container.dataElement 'QueryCustomizer' }} - } from '~/services/data-api'; + } from '~/services/data-api/model/{{ classDataName container.dataElement '' }}'; + import type { + {{ classDataName container.dataElement 'QueryCustomizer' }}, + } from '~/services/data-api/rest/{{ classDataName container.dataElement 'QueryCustomizer' }}'; + import type { {{ containerComponentName container }}ActionDefinitions, {{ containerComponentName container }}DialogActions, @@ -57,6 +60,7 @@ export default function {{ containerComponentName container }}Dialog({{# unless onClose {{# unless (containerIsEmptyDashboard container) }}, actions, + dataPath, isLoading, editMode, refreshCounter, @@ -119,6 +123,7 @@ export default function {{ containerComponentName container }}Dialog({{# unless <{{ containerComponentName container }} actions={actions} + dataPath={dataPath} refreshCounter={refreshCounter} isLoading={isLoading} {{# if container.isSelector }} diff --git a/judo-ui-react/src/main/resources/actor/src/containers/page.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/containers/page.tsx.hbs index 130c7741..4a5f270f 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/page.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/page.tsx.hbs @@ -17,8 +17,11 @@ import { mainContainerPadding } from '~/theme'; import type { {{ classDataName container.dataElement '' }}, {{ classDataName container.dataElement 'Stored' }}, - {{ classDataName container.dataElement 'QueryCustomizer' }} - } from '~/services/data-api'; + } from '~/services/data-api/model/{{ classDataName container.dataElement '' }}'; + import type { + {{ classDataName container.dataElement 'QueryCustomizer' }}, + } from '~/services/data-api/rest/{{ classDataName container.dataElement 'QueryCustomizer' }}'; + import type { {{ containerComponentName container }}ActionDefinitions, {{ containerComponentName container }}PageActions, @@ -37,7 +40,7 @@ export default function {{ containerComponentName container }}Page ({{# unless ( {{# unless (containerIsEmptyDashboard container) }} const { t } = useTranslation(); const { navigate, back } = useJudoNavigation(); - const { actions, isLoading, editMode, refreshCounter{{# unless container.table }}, data, isFormUpdateable, isFormDeleteable, storeDiff, validation, setValidation, submit{{/ unless }} } = props; + const { actions, dataPath, isLoading, editMode, refreshCounter{{# unless container.table }}, data, isFormUpdateable, isFormDeleteable, storeDiff, validation, setValidation, submit{{/ unless }} } = props; const isDraft = false; // currently no page can be opened as draft, but we need this variable anyway {{# if container.view }} const queryCustomizer: {{ classDataName container.dataElement 'QueryCustomizer' }} = { @@ -88,6 +91,7 @@ export default function {{ containerComponentName container }}Page ({{# unless ( <{{ containerComponentName container }} actions={actions} + dataPath={dataPath} refreshCounter={refreshCounter} {{# unless (containerIsEmptyDashboard container) }} isLoading={isLoading} diff --git a/judo-ui-react/src/main/resources/actor/src/containers/types.ts.hbs b/judo-ui-react/src/main/resources/actor/src/containers/types.ts.hbs index cf2fbf6f..595a62c5 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/types.ts.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/types.ts.hbs @@ -64,6 +64,7 @@ isLoading: boolean; editMode: boolean; refreshCounter: number; + dataPath?: string; {{# unless container.table }} data: {{ classDataName container.dataElement 'Stored' }}; isFormUpdateable: () => boolean; @@ -87,6 +88,7 @@ onClose: () => Promise; {{# unless (containerIsEmptyDashboard container) }} actions: {{ containerComponentName container }}DialogActions; + dataPath?: string; isLoading: boolean; editMode: boolean; refreshCounter: number; @@ -126,6 +128,7 @@ export interface {{ containerComponentName container }}Props { refreshCounter: number; isLoading: boolean; actions: {{ pageContainerActionDefinitionTypeName container }}; + dataPath?: string; {{# if container.isSelector }} selectionDiff: {{ classDataName container.dataElement 'Stored' }}[]; setSelectionDiff: Dispatch>; diff --git a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/dateinput.hbs b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/dateinput.hbs index 18139606..4b656c8e 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/dateinput.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/dateinput.hbs @@ -48,10 +48,10 @@ }) } views={['year', 'month', 'day']} label={ t('{{ getTranslationKeyForVisualElement child }}', { defaultValue: '{{ child.label }}' }) as string } - value={ serviceDateToUiDate(data.{{ child.attributeType.name }} ?? null) } + value={ data.{{ child.attributeType.name }} ?? null } readOnly={ {{ boolValue child.attributeType.isReadOnly }} || !isFormUpdateable() } disabled={actions?.is{{ firstToUpper child.attributeType.name }}Disabled ? actions.is{{ firstToUpper child.attributeType.name }}Disabled(data, editMode, isLoading) : ({{# if child.enabledBy }}!data.{{ child.enabledBy.name }} ||{{/ if }} isLoading)} - onChange={ (newValue?: any | null) => { + onChange={ (newValue?: Date | null) => { storeDiff('{{ child.attributeType.name }}', newValue); {{# if child.onBlur }} if (actions?.on{{ firstToUpper child.attributeType.name }}BlurAction) { diff --git a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/datetimeinput.hbs b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/datetimeinput.hbs index 6bc418a6..3020d486 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/datetimeinput.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/datetimeinput.hbs @@ -50,10 +50,10 @@ } } views={['year', 'month', 'day', 'hours', 'minutes', 'seconds']} label={ t('{{ getTranslationKeyForVisualElement child }}', { defaultValue: '{{ child.label }}' }) as string } - value={ serviceDateToUiDate(data.{{ child.attributeType.name }} ?? null) } + value={ data.{{ child.attributeType.name }} ?? null } readOnly={ {{ boolValue child.attributeType.isReadOnly }} || !isFormUpdateable() } disabled={actions?.is{{ firstToUpper child.attributeType.name }}Disabled ? actions.is{{ firstToUpper child.attributeType.name }}Disabled(data, editMode, isLoading) : ({{# if child.enabledBy }}!data.{{ child.enabledBy.name }} ||{{/ if }} isLoading)} - onChange={ (newValue: Date) => { + onChange={ (newValue: Date | null) => { storeDiff('{{ child.attributeType.name }}', newValue); {{# if child.onBlur }} if (actions?.on{{ firstToUpper child.attributeType.name }}BlurAction) { diff --git a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/link.hbs b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/link.hbs index 044b8ca9..db954cdf 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/link.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/link.hbs @@ -15,9 +15,10 @@ > {{/ if }} <{{ componentName child }} - disabled={ {{# if child.enabledBy }}!data.{{ child.enabledBy.name }} ||{{/ if }} false } + disabled={ {{# if child.enabledBy }}!data.{{ child.enabledBy.name }} ||{{/ if }} false || isLoading } readOnly={ {{ boolValue child.relationType.isReadOnly }} || !isFormUpdateable() } ownerData={data} + dataPath={dataPath ? (dataPath + '.{{ child.dataElement.name }}') : '{{ child.dataElement.name }}'} editMode={editMode} isLoading={isLoading} isDraft={isDraft} diff --git a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/table.hbs b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/table.hbs index a3461db6..2587a187 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/table.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/table.hbs @@ -33,6 +33,7 @@ <{{ componentName child }} uniqueId={'{{ getXMIID child }}'} actions={actions} + dataPath={dataPath ? (dataPath + '.{{ child.dataElement.name }}') : '{{ child.dataElement.name }}'} {{# if container.isSelector }} selectionDiff={selectionDiff} setSelectionDiff={setSelectionDiff} diff --git a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/timeinput.hbs b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/timeinput.hbs index 91c6e506..6e02e1c7 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/timeinput.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/timeinput.hbs @@ -50,7 +50,7 @@ }) } views={['hours', 'minutes', 'seconds']} label={ t('{{ getTranslationKeyForVisualElement child }}', { defaultValue: '{{ child.label }}' }) as string } - value={ serviceTimeToUiTime(data.{{ child.attributeType.name }} ?? null) } + value={ data.{{ child.attributeType.name }} ?? null } readOnly={ {{ boolValue child.attributeType.isReadOnly }} || !isFormUpdateable() } disabled={actions?.is{{ firstToUpper child.attributeType.name }}Disabled ? actions.is{{ firstToUpper child.attributeType.name }}Disabled(data, editMode, isLoading) : ({{# if child.enabledBy }}!data.{{ child.enabledBy.name }} ||{{/ if }} isLoading)} {{# if child.onBlur }} @@ -60,7 +60,7 @@ } } } {{/ if }} - onChange={ (newValue: string | null | undefined) => { + onChange={ (newValue?: Date | null) => { storeDiff('{{ child.attributeType.name }}', newValue); } } /> diff --git a/judo-ui-react/src/main/resources/actor/src/custom/custom-element-types.ts.hbs b/judo-ui-react/src/main/resources/actor/src/custom/custom-element-types.ts.hbs index 3b490af5..2499bd97 100644 --- a/judo-ui-react/src/main/resources/actor/src/custom/custom-element-types.ts.hbs +++ b/judo-ui-react/src/main/resources/actor/src/custom/custom-element-types.ts.hbs @@ -22,7 +22,7 @@ export interface CustomFormVisualElementProps { readonly editMode: boolean; /** - * State modifier method which implicitly updates the `data` and adds an entry to the `payloadDiff` as well. + * State modifier method which implicitly updates the `data` with additional meta attributes. * * It is highly recommended to use this method to update the data/state on pages in order to be consistent with the * framework and reduce erroneous behaviour. diff --git a/judo-ui-react/src/main/resources/actor/src/dialogs/hooks.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/dialogs/hooks.tsx.hbs index 92f18bb7..0ab37fef 100644 --- a/judo-ui-react/src/main/resources/actor/src/dialogs/hooks.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/dialogs/hooks.tsx.hbs @@ -5,7 +5,20 @@ import type { DialogResult } from '~/utilities'; import type { {{ classDataName imp '' }}, {{ classDataName imp 'Stored' }} } from '~/services/data-api/model/{{ classDataName imp '' }}'; {{/ each }} -export const use{{ pageName page }} = (): ({{{ getDialogOpenParameters page }}}) => Promise{{/ if }};{{/ unless }} + ownerValidation?: (target: any) => Promise; + isDraft?: boolean; + {{# unless page.container.table}} + maskRequest?: string; + {{/ unless }} + dataPath?: string; +} + +export const use{{ pageName page }} = (): ({ ownerData{{# and (isPageDataElementUnmappedSingle page) page.container.view }}, data{{/ and }}{{# if page.container.isRelationSelector }}, alreadySelected{{/ if }}{{# unless page.container.isSelector }}, templateDataOverride{{/ unless }}, ownerValidation, isDraft{{# unless page.container.table}}, maskRequest{{/ unless }}, dataPath }: {{ pageName page }}HookProps) => Promise import('~/dialogs/{{ pagePath page }}')); - return ({{{ getDialogOpenParameters page }}}) => new Promise((resolve) => { + return ({ ownerData{{# and (isPageDataElementUnmappedSingle page) page.container.view }}, data{{/ and }}{{# if page.container.isRelationSelector }}, alreadySelected{{/ if }}{{# unless page.container.isSelector }}, templateDataOverride{{/ unless }}, ownerValidation, isDraft{{# unless page.container.table}}, maskRequest{{/ unless }}, dataPath }: {{ pageName page }}HookProps) => new Promise((resolve) => { createDialog({ {{# if page.dialogSize }} fullWidth: true, @@ -43,11 +56,10 @@ export const use{{ pageName page }} = (): ({{{ getDialogOpenParameters page }}}) {{/ unless }} isDraft={isDraft} {{# unless page.container.table }} + maskRequest={maskRequest} ownerValidation={ownerValidation} {{/ unless }} - {{# if page.container.form }} - maskRequest={maskRequest} - {{/ if }} + dataPath={dataPath} onClose={async () => { await closeDialog(); resolve({ diff --git a/judo-ui-react/src/main/resources/actor/src/dialogs/index.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/dialogs/index.tsx.hbs index 47c41a30..5d2292f7 100644 --- a/judo-ui-react/src/main/resources/actor/src/dialogs/index.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/dialogs/index.tsx.hbs @@ -1,11 +1,12 @@ {{> fragment.header.hbs }} -import { {{# unless page.container.table }}useCallback, useRef, {{/ unless }}useEffect, useState, useMemo, lazy, Suspense } from 'react'; +import { useRef, useCallback, useEffect, useState, useMemo, lazy, Suspense } from 'react'; {{# unless (containerIsEmptyDashboard page.container) }} import type { FC, ReactNode, Dispatch, SetStateAction } from 'react'; import { OBJECTCLASS } from '@pandino/pandino-api'; import { useTrackService } from '@pandino/react-hooks'; import type { JudoIdentifiable } from '~/services/data-api/common/JudoIdentifiable'; + import { draftIdentifierPrefix } from '~/services/data-api/common/utils'; import type { JudoRestResponse } from '~/services/data-api/rest/requestResponse'; {{# if (containerHasTableWithTotalCount page.container) }} import { X_JUDO_COUNT_RECORDS } from '~/services/data-api/rest/headers'; @@ -25,15 +26,11 @@ import { {{# unless page.container.table }}useCallback, useRef, {{/ unless }}use import { {{# if (hasPageRequiredBy page) }}passesLocalValidation,{{/ if }} processQueryCustomizer, - {{# if (containerHasDateInput page.container) }}uiDateToServiceDate,{{/ if }} - {{# if (containerHasTimeInput page.container) }}uiTimeToServiceTime,{{/ if }} useErrorHandler, isErrorNestedValidationError, - cleanUpPayload, - decoratePayloadWithMetaFromData, - {{# if page.container.view }} - cloneDeep, - {{/ if }} + simpleCloneDeep, + getValue, + setValue, } from '~/utilities'; import type { DialogResult, @@ -66,20 +63,28 @@ import { {{# unless page.container.table }}useCallback, useRef, {{/ unless }}use {{/ unless }} {{# unless (containerIsEmptyDashboard page.container) }} - {{# unless page.container.table }} - {{# if isDebugPrint }}// include: actor/src/fragments/page/payload-converter.fragment.hbs{{/ if }} - {{> actor/src/fragments/page/payload-converter.fragment.hbs classType=page.dataElement.target page=page }} - {{/ unless }} - const {{ containerComponentName page.container }}DialogContainer = lazy(() => import('~/containers/{{ containerPath page.container }}/{{ containerComponentName page.container }}DialogContainer')); {{/ unless }} // XMIID: {{ getXMIID page }} // Name: {{ page.name }} export default function {{ pageName page }}(props: {{ pageName page }}Props) { - const { ownerData, {{# and (isPageDataElementUnmappedSingle page) page.container.view }}data: output, {{/ and }}{{# if page.container.isRelationSelector }}alreadySelected, {{/ if }}{{# unless page.container.isSelector }}templateDataOverride, {{/ unless }}{{# unless page.container.table }}maskRequest, {{/ unless }}onClose, onSubmit, isDraft, ownerValidation } = props; + const { + ownerData, + {{# and (isPageDataElementUnmappedSingle page) page.container.view }}data: output,{{/ and }} + {{# if page.container.isRelationSelector }}alreadySelected,{{/ if }} + {{# unless page.container.isSelector }}templateDataOverride,{{/ unless }} + ownerValidation, + isDraft, + {{# unless page.container.table}}maskRequest,{{/ unless }} + dataPath, + onClose, + onSubmit, + } = props; {{# unless (containerIsEmptyDashboard page.container) }} + const owner = useRef(ownerData ?? null); + // Services const {{ firstToLower (getServiceImplForPage page) }} = useMemo(() => new {{ getServiceClassForPage page }}(judoAxiosProvider), []); {{# each (getRelatedServicesForPage page) as |rel| }} @@ -101,34 +106,22 @@ export default function {{ pageName page }}(props: {{ pageName page }}Props) { const [isLoading, setIsLoading] = useState(false); const [editMode, setEditMode] = useState({{# if page.container.form }}true{{ else }}false{{/ if }}); const [refreshCounter, setRefreshCounter] = useState(0); - const [data, setData] = useState<{{ dialogDataType page }}{{# if page.container.table }}[]{{/ if }}>( - {{# if page.container.table }} - [] - {{ else }} - {{# and (isPageDataElementUnmappedSingle page) page.container.view }}output{{ else }}{}{{/ and }} as {{ classDataName (getReferenceClassType page) 'Stored' }} - {{/ if }} - ); + const [data, setData] = useState<{{ dialogDataType page }}{{# if page.container.table }}[]{{/ if }}>({{ dialogDataInitialValue page }} as {{ dialogDataType page }}{{# if page.container.table }}[]{{/ if }}); {{# if page.container.isSelector }} const [selectionDiff, setSelectionDiff] = useState<{{ dialogDataType page }}[]>([]); {{/ if }} {{# unless page.container.table }} const [validation, setValidation] = useState>(new Map()); - // Ref section - const payloadDiff = useRef>({} as unknown as Record); - {{# if page.container.view}} // Rollback handling - const rollbackPoint = useRef<{{ dialogDataType page }} | null>((isDraft && templateDataOverride) ? cloneDeep<{{ dialogDataType page }}>(templateDataOverride) : null); + const rollbackPoint = useRef<{{ dialogDataType page }} | null>(isDraft ? simpleCloneDeep(getValue<{{ dialogDataType page }}>(ownerData, dataPath!, {})) : null); const rollbackData = useCallback(() => { if (rollbackPoint.current) { setData(rollbackPoint.current); - // re-set payloadDiff - payloadDiff.current = {} as unknown as Record; - decoratePayloadWithMetaFromData(payloadDiff.current, rollbackPoint.current); setEditMode(false); } - }, [data, editMode, payloadDiff, rollbackPoint]); + }, [data, editMode, dataPath, ownerData, isDraft]); {{/ if }} // Callback section @@ -206,57 +199,22 @@ export default function {{ pageName page }}(props: {{ pageName page }}Props) { {{/ if }} } }; + const produceDataAdjustedOwner = useCallback(() => { + const copy = simpleCloneDeep(owner.current); + setValue(copy, dataPath!, simpleCloneDeep(data)); + return copy; + }, [data, owner]); // Validation - const validate: (data: {{ dialogBareDataType page }}) => Promise = async (data) => { - {{# if (isDialogValidationSupported page) }} + const validate: (target: any) => Promise = useCallback(async (target) => { + {{# if (isValidationSupported page) }} if (ownerValidation) { - await ownerValidation(data); + await ownerValidation(target); } else { - await {{ getServiceImplForPage page }}.validate{{# if page.container.view }}Update{{ else }}Create{{/ if }}({{# unless page.dataElement.isAccess }}ownerData, {{/ unless }}data); + await {{ getServiceImplForPage page }}.validate{{# if page.container.view }}Update{{ else }}Create{{/ if }}({{# unless page.dataElement.isAccess }}owner.current, {{/ unless }}target); } {{/ if }} - }; - {{# each page.container.links as |link| }} - {{# and (createNestedValidation link.relationType page) link.isEager }} - const validate{{ firstToUpper link.relationType.name }} = async (linkData: {{ classDataName link.relationType.target '' }}): Promise => { - try { - await validate({ - ...cleanUpPayload({ - ...payloadDiff.current, - {{ link.relationType.name }}: { - ...linkData, - } as any - }), - } as {{ dialogBareDataType page }}); - } catch (error: any) { - if (isErrorNestedValidationError(error, '{{ link.relationType.name }}')) { - throw error; - } - } - }; - {{/ and }} - {{/ each }} - {{# each page.container.tables as |table| }} - {{# and (createNestedValidation table.relationType page) table.isEager }} - const validate{{ firstToUpper table.relationType.name }} = async (tableData: {{ classDataName table.relationType.target '' }}): Promise => { - try { - await validate({ - ...cleanUpPayload({ - ...payloadDiff.current, - {{ table.relationType.name }}: [ - { ...tableData } as any - ], - }), - } as {{ dialogBareDataType page }}); - } catch (error: any) { - if (isErrorNestedValidationError(error, '{{ table.relationType.name }}')) { - throw error; - } - } - }; - {{/ and }} - {{/ each }} + }, [{{# if (isValidationSupported page) }}ownerValidation, {{# unless page.dataElement.isAccess }}ownerData, {{/ unless }}data, {{ getServiceImplForPage page }}{{/ if }}]); // Pandino Action overrides const { service: customActionsHook } = useTrackService<{{ containerComponentName page.container }}ActionsHook>(`(${OBJECTCLASS}=${ {{~ camelCaseNameToInterfaceKey (pageName page) }}_ACTIONS_HOOK_INTERFACE_KEY})`); @@ -339,17 +297,17 @@ export default function {{ pageName page }}(props: {{ pageName page }}Props) { // Effect section useEffect(() => { {{# if (pageShouldInitialize page) }} - if (!isDraft) { + {{# if page.container.view }} + if (!isDraft) { + actions.{{ simpleActionDefinitionName page.container.onInit }}!({{# if page.container.view }}getPageQueryCustomizer(){{/ if }}); + } + {{ else }} actions.{{ simpleActionDefinitionName page.container.onInit }}!({{# if page.container.view }}getPageQueryCustomizer(){{/ if }}); - } + {{/ if }} {{/ if }} {{# unless page.container.table }} if (templateDataOverride) { setData((prevData) => ({ ...prevData, ...templateDataOverride })); - {{# if page.container.view }} - // re-set payloadDiff - decoratePayloadWithMetaFromData(payloadDiff.current, templateDataOverride); - {{/ if }} } {{/ unless }} }, []); @@ -364,6 +322,7 @@ export default function {{ pageName page }}(props: {{ pageName page }}Props) { ownerData={ownerData} onClose={onClose} actions={actions} + dataPath={dataPath} isLoading={isLoading} editMode={editMode} refreshCounter={refreshCounter} diff --git a/judo-ui-react/src/main/resources/actor/src/dialogs/types.ts.hbs b/judo-ui-react/src/main/resources/actor/src/dialogs/types.ts.hbs index b9797571..83494cd4 100644 --- a/judo-ui-react/src/main/resources/actor/src/dialogs/types.ts.hbs +++ b/judo-ui-react/src/main/resources/actor/src/dialogs/types.ts.hbs @@ -30,7 +30,7 @@ {{# if page.container.isRelationSelector }}alreadySelected: {{ classDataName (getReferenceClassType page) 'Stored' }}[];{{/ if }} {{# unless page.container.isSelector }}templateDataOverride?: {{# if page.container.view }}{{ classDataName (getReferenceClassType page) 'Stored' }}{{ else }}Partial<{{ classDataName (getReferenceClassType page) '' }}>{{/ if }};{{/ unless }} isDraft?: boolean; - ownerValidation?: (data: {{ dialogBareDataType page }}) => Promise; + ownerValidation?: (target: any) => Promise; onClose: () => Promise; {{# if (pageHasOutputTarget page) }} onSubmit: (result?: {{ classDataName (getPageOutputTarget page) 'Stored' }}, reason?: DialogResultReason, openCreated?: boolean) => Promise; @@ -40,6 +40,7 @@ {{# unless page.container.table}} maskRequest?: string; {{/ unless }} + dataPath?: string; } {{ else }} export const _ = 'placeholder'; diff --git a/judo-ui-react/src/main/resources/actor/src/fragments/container/common-imports.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/fragments/container/common-imports.fragment.hbs index c3c541f5..5c381658 100644 --- a/judo-ui-react/src/main/resources/actor/src/fragments/container/common-imports.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/fragments/container/common-imports.fragment.hbs @@ -41,12 +41,4 @@ import { {{# if (containerHasBinaryInput container) }} fileHandling, {{/ if }} - {{# or (containerHasDateInput container) (containerHasDateTimeInput container) }} - uiDateToServiceDate, - serviceDateToUiDate, - {{/ or }} - {{# if (containerHasTimeInput container) }} - uiTimeToServiceTime, - serviceTimeToUiTime, - {{/ if }} } from '~/utilities'; diff --git a/judo-ui-react/src/main/resources/actor/src/fragments/page/payload-converter.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/fragments/page/payload-converter.fragment.hbs deleted file mode 100644 index 25551a2b..00000000 --- a/judo-ui-react/src/main/resources/actor/src/fragments/page/payload-converter.fragment.hbs +++ /dev/null @@ -1,33 +0,0 @@ -export const convert{{ pageName page }}Payload = (attributeName: keyof {{ classDataName (getReferenceClassType page) '' }}, value: any): any => { - {{# if (containerHasDateInput page.container) }} - const dateTypes: string[] = [{{# each (getWritableDateAttributesForClass classType) as |aName| }} - '{{ aName }}', - {{/ each }}]; - {{/ if }} - {{# if (containerHasDateTimeInput page.container) }} - const dateTimeTypes: string[] = [{{# each (getWritableDateTimeAttributesForClass classType) as |aName| }} - '{{ aName }}', - {{/ each }}]; - {{/ if }} - {{# if (containerHasTimeInput page.container) }} - const timeTypes: string[] = [{{# each (getWritableTimeAttributesForClass classType) as |aName| }} - '{{ aName }}', - {{/ each }}]; - {{/ if }} - {{# if (containerHasDateInput page.container) }} - if (dateTypes.includes(attributeName as string)) { - return uiDateToServiceDate(value); - } - {{/ if }} - {{# if (containerHasDateTimeInput page.container) }} - if (dateTimeTypes.includes(attributeName as string)) { - return value; - } - {{/ if }} - {{# if (containerHasTimeInput page.container) }} - if (timeTypes.includes(attributeName as string)) { - return uiTimeToServiceTime(value); - } - {{/ if }} - return value; -}; diff --git a/judo-ui-react/src/main/resources/actor/src/fragments/page/store-diff-body.hbs b/judo-ui-react/src/main/resources/actor/src/fragments/page/store-diff-body.hbs index 05451dfa..4b9399e1 100644 --- a/judo-ui-react/src/main/resources/actor/src/fragments/page/store-diff-body.hbs +++ b/judo-ui-react/src/main/resources/actor/src/fragments/page/store-diff-body.hbs @@ -1,10 +1,7 @@ -{{# if (payloadDiffHasItems classType) }} - payloadDiff.current![attributeName] = convert{{ pageName page }}Payload(attributeName, value); - setData((prevData) => ({ - ...prevData, - [attributeName]: value, - })); - if (!editMode) { - setEditMode(true); - } -{{/ if }} +setData((prevData) => ({ + ...prevData, + [attributeName]: value, +})); +if (!editMode) { + setEditMode(true); +} diff --git a/judo-ui-react/src/main/resources/actor/src/fragments/relation/column.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/fragments/relation/column.fragment.hbs index 8eff616a..55e556d3 100644 --- a/judo-ui-react/src/main/resources/actor/src/fragments/relation/column.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/fragments/relation/column.fragment.hbs @@ -54,7 +54,6 @@ valueFormatter: (value?: string) => { {{/ if }} {{/ if }} {{# if (isColumnTimestamp column) }} -valueGetter: (value?: Date) => value && serviceDateToUiDate(value), valueFormatter: (value?: Date) => { return value && new Intl.DateTimeFormat(l10nLocale, { year: 'numeric', @@ -71,7 +70,6 @@ filterOperators: dateTimeColumnOperators, {{/ if }} {{/ if }} {{# if (isColumnTime column) }} -valueGetter: (value?: Date) => value && serviceTimeToUiTime(value), valueFormatter: (value?: Date) => { return value && new Intl.DateTimeFormat(l10nLocale, { hour: '2-digit', @@ -81,7 +79,6 @@ valueFormatter: (value?: Date) => { }, {{/ if }} {{# if (isColumnDate column) }} -valueGetter: (value?: Date) => value && new Date(value), valueFormatter: (value?: Date) => { return value && new Intl.DateTimeFormat(l10nLocale, { year: 'numeric', month: '2-digit', day: '2-digit' }).format(value); }, diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/AutocompleteRangeAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/AutocompleteRangeAction.fragment.hbs index 682e82ad..ad2b1b72 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/AutocompleteRangeAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/AutocompleteRangeAction.fragment.hbs @@ -1,7 +1,7 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async (queryCustomizer: {{ classDataName action.actionDefinition.targetType 'QueryCustomizer' }}): Promise<{{ classDataName action.actionDefinition.targetType 'Stored' }}[]> => { {{# with (getLinkParentForActionDefinition action.actionDefinition) as |link| }} try { - const { data: result } = await {{ getServiceImplForPage page }}.getRange{{# if (isActionOnOperationInput action) }}On{{ firstToUpper (getOperationNameForActionOnInput action) }}{{/ if }}For{{ firstToUpper link.dataElement.name }}(cleanUpPayload(data), queryCustomizer); + const { data: result } = await {{ getServiceImplForPage page }}.getRange{{# if (isActionOnOperationInput action) }}On{{ firstToUpper (getOperationNameForActionOnInput action) }}{{/ if }}For{{ firstToUpper link.dataElement.name }}(data, queryCustomizer); return result; } catch (error: any) { handleError(error); diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/CallOperationAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/CallOperationAction.fragment.hbs index 31632880..6bbe14b4 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/CallOperationAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/CallOperationAction.fragment.hbs @@ -4,8 +4,8 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async ({{# if a setIsLoading(true); {{# if operation.output }}const { data: result } = {{/ if }}await {{ getServiceImplForPage page }}.{{ operation.name }}{{ operationCallSuffix action }}( {{# if page.container.form }} - {{# unless operation.isStatic }}ownerData{{/ unless }} - {{# if operation.input }}{{# unless operation.isStatic }}, {{/ unless }}cleanUpPayload(payloadDiff.current){{/ if }} + {{# unless operation.isStatic }}produceDataAdjustedOwner(){{/ unless }} + {{# if operation.input }}{{# unless operation.isStatic }}, {{/ unless }}data{{/ if }} {{ else }} {{# if page.container.isSelector }} {{# unless operation.isStatic }}ownerData, {{/ unless }}selectionDiff[0] @@ -55,7 +55,9 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async ({{# if a _identifier: result.__identifier, }); {{# if action.targetPageDefinition.openInDialog }} - await open{{ pageName operation.postCallAccessNavigation.pageDefinition }}(pcan[0]); + await open{{ pageName operation.postCallAccessNavigation.pageDefinition }}({ + ownerData: pcan[0] + }); {{ else }} navigate(routeTo{{ pageName operation.postCallAccessNavigation.pageDefinition }}(pcan[0].__signedIdentifier)); {{/ if }} @@ -65,7 +67,10 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async ({{# if a {{ else }} {{# if operation.output }} {{# if action.targetPageDefinition.openInDialog }} - await open{{ pageName action.targetPageDefinition }}({{# if page.container.view }}data{{ else }}null{{/ if }}, result); + await open{{ pageName action.targetPageDefinition }}({ + ownerData: produceDataAdjustedOwner(), + data: result, + }); {{ else }} navigate(routeTo{{ pageName action.targetPageDefinition }}(result.__signedIdentifier)); {{/ if }} @@ -74,7 +79,7 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async ({{# if a } } catch (error) { {{# unless page.container.table }} - handleError<{{ classDataName (getReferenceClassType page) '' }}>(error, { setValidation }, data); + handleError<{{ classDataName (getReferenceClassType page) '' }}>(error, { setValidation }, owner.current, dataPath); {{ else }} handleError(error); {{/ unless }} diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/ClearAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/ClearAction.fragment.hbs index cb4909ea..71e480df 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/ClearAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/ClearAction.fragment.hbs @@ -5,7 +5,7 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async () => { {{ else }} try { setIsLoading(true); - await {{ getServiceImplForPage page }}.set{{ firstToUpper action.targetDataElement.name }}({{# if (pageHasSignedId page) }}{ __signedIdentifier: signedIdentifier } as JudoIdentifiable{{ else }}ownerData{{/ if }}, []); + await {{ getServiceImplForPage page }}.set{{ firstToUpper action.targetDataElement.name }}(owner.current, []); await refresh(); } catch(e) { console.error(e); diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/CreateAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/CreateAction.fragment.hbs index 05b07741..98699676 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/CreateAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/CreateAction.fragment.hbs @@ -4,15 +4,16 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async (openCrea if (isDraft) { try { setIsLoading(true); - await validate(cleanUpPayload(payloadDiff.current)); - onSubmit(payloadDiff.current, 'submit-draft'); + const validationData = simpleCloneDeep(owner.current); + setValue(validationData, dataPath!, data); + await validate(validationData); + onSubmit(data, 'submit-draft'); } catch (error) { - if (ownerValidation && !isErrorNestedValidationError(error, '{{ page.dataElement.name }}')) { + if (ownerValidation && !isErrorNestedValidationError(error, dataPath)) { // relation form has no remaining error(s), proceed with submission - onSubmit(payloadDiff.current, 'submit-draft'); + onSubmit(data, 'submit-draft'); } else { - let relationName: string | undefined = (isDraft && ownerValidation) ? '{{ page.dataElement.name }}' : undefined; - handleError<{{ classDataName (getReferenceClassType page) '' }}>(error, { setValidation }, data, relationName); + handleError<{{ classDataName (getReferenceClassType page) '' }}>(error, { setValidation }, owner.current, dataPath!); } } finally { setIsLoading(false); @@ -22,11 +23,10 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async (openCrea } {{/ unless }} setIsLoading(true); - const payload: typeof payloadDiff.current = cleanUpPayload(payloadDiff.current); const queryCustomizer: { _mask?: string } | undefined = maskRequest ? { _mask: maskRequest, } : undefined; - const { data: res } = await {{ getServiceImplForPage page }}.create{{# if action.targetDataElement }}{{ firstToUpper action.targetDataElement.name }}{{/ if }}({{# unless action.ownerDataElement.isAccess }}ownerData, {{/ unless }}payload, queryCustomizer); + const { data: res } = await {{ getServiceImplForPage page }}.create{{# if action.targetDataElement }}{{ firstToUpper action.targetDataElement.name }}{{/ if }}({{# unless action.ownerDataElement.isAccess }}owner.current, {{/ unless }}data, queryCustomizer); if (customActions?.post{{ firstToUpper (simpleActionDefinitionName action.actionDefinition) }}) { await customActions.post{{ firstToUpper (simpleActionDefinitionName action.actionDefinition) }}(data, res, onSubmit, onClose, openCreated); } else { @@ -34,7 +34,7 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async (openCrea await onSubmit(res, isDraft ? 'submit-draft' : 'submit', openCreated); } } catch (error) { - handleError<{{ classDataName (getReferenceClassType page) '' }}>(error, { setValidation }, data); + handleError<{{ classDataName (getReferenceClassType page) '' }}>(error, { setValidation }, owner.current, dataPath); } finally { setIsLoading(false); } diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/ExportAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/ExportAction.fragment.hbs index d84bb3bc..671e81f2 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/ExportAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/ExportAction.fragment.hbs @@ -8,7 +8,7 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async (queryCus {{# if container.isRelationSelector }} const response = await {{ getServiceImplForPage page }}.export(undefined, queryCustomizer); {{ else }} - const response = await {{ getServiceImplForPage page }}.export{{# if action.targetDataElement }}{{ firstToUpper action.targetDataElement.name }}{{/ if }}({{# if (pageHasSignedId page) }}{ __signedIdentifier: signedIdentifier } as JudoIdentifiable{{ else }}undefined{{/ if }}, queryCustomizer); + const response = await {{ getServiceImplForPage page }}.export{{# if action.targetDataElement }}{{ firstToUpper action.targetDataElement.name }}{{/ if }}({{# if (pageHasSignedId page) }}{ __signedIdentifier: signedIdentifier } as any{{ else }}undefined{{/ if }}, queryCustomizer); {{/ if }} exportFile(response); diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/GetTemplateAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/GetTemplateAction.fragment.hbs index b8c8ab06..8a54aacf 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/GetTemplateAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/GetTemplateAction.fragment.hbs @@ -4,17 +4,17 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async (): Promi const response = await {{ getServiceImplForPage page }}.getTemplate{{ firstToUpper (getServiceMethodSuffix action) }}(); const { data: result } = response; setData(result as {{ classDataName action.actionDefinition.targetType 'Stored' }}); - payloadDiff.current = { - ...(result as Record), - }; + if (!ownerData) { + owner.current = simpleCloneDeep(data); + } if (customActions?.post{{ firstToUpper (simpleActionDefinitionName action.actionDefinition) }}) { - await customActions.post{{ firstToUpper (simpleActionDefinitionName action.actionDefinition) }}(ownerData, result, storeDiff); + await customActions.post{{ firstToUpper (simpleActionDefinitionName action.actionDefinition) }}(produceDataAdjustedOwner(), result, storeDiff); } if (templateDataOverride) { setData((prevData) => ({ ...prevData, ...templateDataOverride })); - payloadDiff.current = { - ...(templateDataOverride as Record), - }; + if (!ownerData) { + owner.current = simpleCloneDeep(templateDataOverride); + } } return response; } catch (error) { diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/OpenAddSelectorAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/OpenAddSelectorAction.fragment.hbs index 18b0113c..14415598 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/OpenAddSelectorAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/OpenAddSelectorAction.fragment.hbs @@ -1,5 +1,20 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async () => { - const { result, data: returnedData } = await open{{ pageName action.targetPageDefinition }}({{{ getSelectorOpenActionParameters action page.container }}}); + const { result, data: returnedData } = await open{{ pageName action.targetPageDefinition }}({ + {{# unless page.container.table }} + ownerData: data, + {{ else }} + ownerData: getValue(owner.current, dataPath!, data), + {{/ unless }} + alreadySelected: {{# with (getRelationTypeForActionTarget action) as |rel| }} + {{# if page.container.table }} + [] + {{ else }} + data.{{ rel.name }}{{# if rel.isCollection }} ?? []{{ else }} ? [data.{{ rel.name }}] : []{{/ if }} + {{/ if }} + {{ else }} + undefined + {{/ with }} + }); if (result === 'submit') { if (Array.isArray(returnedData) && returnedData.length) { {{# with (getTableParentForActionDefinition action.actionDefinition) as |table| }} @@ -9,15 +24,7 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async () => { try { setIsLoading(true); await {{ getServiceImplForPage page }}.add{{ firstToUpper action.targetDataElement.name }}( - {{# if (pageHasSignedId page) }} - { __signedIdentifier: signedIdentifier } as JudoIdentifiable - {{ else }} - {{# if page.openInDialog }} - ownerData - {{ else }} - data - {{/ if }} - {{/ if }}, + owner.current, returnedData ); await refresh(); diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/OpenCreateFormAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/OpenCreateFormAction.fragment.hbs index c8a26dcc..8acb1e1c 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/OpenCreateFormAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/OpenCreateFormAction.fragment.hbs @@ -1,5 +1,25 @@ -const {{ simpleActionDefinitionName action.actionDefinition }} = async ({{# if action.actionDefinition.targetType }}target: {{ classDataName action.actionDefinition.targetType 'Stored' }}, templateDataOverride?: Partial<{{ classDataName action.actionDefinition.targetType '' }}>, {{/ if}}isDraft?: boolean, ownerValidation?: (data: any) => Promise) => { - const { result, data: returnedData, openCreated } = await open{{ actionTargetPageName action }}({{{ getFormOpenParameters page action }}}); +const {{ simpleActionDefinitionName action.actionDefinition }} = async ({{# if action.actionDefinition.targetType }}templateDataOverride?: Partial<{{ classDataName action.actionDefinition.targetType '' }}>, {{/ if}}) => { + {{# if (isActionParentEagerTable action) }} + const itemIndex = (data.{{ action.targetDataElement.name }} || []).length; // length gives next without -1-ing it + {{/ if }} + const { result, data: returnedData, openCreated } = await open{{ actionTargetPageName action }}({ + ownerData: produceDataAdjustedOwner(), + {{# or (isRelationOpenCreateActionOnForm page action) (isActionParentEagerElement action) }} + ownerValidation: validate, + {{/ or }} + {{# with (getLinkParentForActionDefinition action.actionDefinition) as |link| }} + {{# with (getFirstAutocompleteColumnForLink link) as |col| }} + {{# unless link.isEager }} + maskRequest: '{ {{~ col.attributeType.name ~}} }', + {{/ unless }} + {{/ with }} + {{/ with }} + {{# if page.container.isRelationSelector }} + maskRequest: getMask(), + {{/ if }} + isDraft: {{# or (isRelationOpenCreateActionOnForm page action) (isActionParentEagerElement action) }}true{{ else }}false{{/ or }}, + dataPath: `${dataPath ? (dataPath + '.') : ''}{{# if (isActionParentEagerElement action) }}{{ action.targetPageDefinition.dataElement.name }}{{/ if }}{{# if (isActionParentEagerTable action) }}[${itemIndex}]{{/ if }}` + }); {{# if page.container.isRelationSelector }} if (result === 'submit' && returnedData) { onSubmit([returnedData]); @@ -10,9 +30,9 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async ({{# if a if (result === 'submit-draft' && returnedData) { const decoratedData = { ...returnedData, - __identifier: `draft:${uuidv4()}`, + __identifier: `${draftIdentifierPrefix}${uuidv4()}`, }; - const newData = {{# if action.targetDataElement.isCollection }}[...(payloadDiff.current.{{ action.targetDataElement.name }} || []), decoratedData]{{ else }}decoratedData{{/ if }} + const newData = {{# if action.targetDataElement.isCollection }}[...(data.{{ action.targetDataElement.name }} || []), decoratedData]{{ else }}decoratedData{{/ if }} storeDiff('{{ action.targetDataElement.name }}', newData); return; } @@ -24,7 +44,7 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async ({{# if a } else if (result === 'submit-draft' && returnedData) { const decoratedData = { ...returnedData, - __identifier: `draft:${uuidv4()}`, + __identifier: `${draftIdentifierPrefix}${uuidv4()}`, }; const newData = {{# if action.targetDataElement.isCollection }}[...(data.{{ action.targetDataElement.name }} || []), decoratedData]{{ else }}decoratedData{{/ if }} storeDiff('{{ action.targetDataElement.name }}', newData); diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/OpenOperationInputFormAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/OpenOperationInputFormAction.fragment.hbs index 5f1f9be3..02327b63 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/OpenOperationInputFormAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/OpenOperationInputFormAction.fragment.hbs @@ -1,5 +1,11 @@ -const {{ simpleActionDefinitionName action.actionDefinition }} = async ({{# if action.actionDefinition.targetType }}target: {{ classDataName action.actionDefinition.targetType 'Stored' }}, templateDataOverride?: Partial<{{ classDataName action.actionDefinition.targetType '' }}>, {{/ if}}isDraft?: boolean, ownerValidation?: (data: any) => Promise) => { - const { result, data: returnedData } = await open{{ actionTargetPageName action }}({{{ getFormOpenParameters page action }}}); +const {{ simpleActionDefinitionName action.actionDefinition }} = async ({{# if action.actionDefinition.targetType }}target: {{ classDataName action.actionDefinition.targetType 'Stored' }}, {{/ if}}isDraft?: boolean, ownerValidation?: (target: any) => Promise) => { + const { result, data: returnedData } = await open{{ actionTargetPageName action }}({ + {{# if action.actionDefinition.targetType }} + ownerData: target, + {{ else }} + ownerData: produceDataAdjustedOwner(), + {{/ if }} + }); if (result === 'submit'{{# if page.container.view }} && !editMode{{/ if }}) { await refresh(); } diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/OpenOperationInputSelectorAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/OpenOperationInputSelectorAction.fragment.hbs index 2327e9ca..cd111983 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/OpenOperationInputSelectorAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/OpenOperationInputSelectorAction.fragment.hbs @@ -1,5 +1,11 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async ({{# if action.actionDefinition.targetType }}target?: {{ classDataName action.actionDefinition.targetType 'Stored' }}{{/ if }}) => { - const { result, data: returnedData } = await open{{ pageName action.targetPageDefinition }}({{{ getSelectorOpenActionParameters action page.container }}}); + const { result, data: returnedData } = await open{{ pageName action.targetPageDefinition }}({ + {{# if action.actionDefinition.targetType }} + ownerData: target!, + {{ else }} + ownerData: produceDataAdjustedOwner(), + {{/ if }} + }); if (result === 'submit') { await refresh(); } diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/OpenPageAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/OpenPageAction.fragment.hbs index bb19f496..9056ceb5 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/OpenPageAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/OpenPageAction.fragment.hbs @@ -1,22 +1,15 @@ -const {{ simpleActionDefinitionName action.actionDefinition }} = async (target: {{ classDataName action.actionDefinition.targetType '' }} | {{ classDataName action.actionDefinition.targetType 'Stored' }}, isDraftParam?: boolean) => { +const {{ simpleActionDefinitionName action.actionDefinition }} = async (target: {{ classDataName action.actionDefinition.targetType 'Stored' }}, isDraftParam?: boolean) => { + {{# if (isActionParentEagerTable action) }} + const itemIndex = {{# with (getTableParentForActionDefinition action.actionDefinition) as |table| }}(data.{{ table.relationName }} || []).findIndex(r => r.__identifier === target.__identifier)!{{/ with }}; + {{/ if }} {{# and (isActionParentEagerElement action) action.targetPageDefinition.openInDialog }} if (isDraftParam) { - const { result, data: returnedData } = await open{{ pageName action.targetPageDefinition }}( - {{# if (pageHasSignedId page) }} - { __signedIdentifier: signedIdentifier } as JudoIdentifiable - {{ else }} - {{# if page.openInDialog }} - ownerData - {{ else }} - data - {{/ if }} - {{/ if }}, - target as {{ classDataName action.actionDefinition.targetType 'Stored' }}, - true, - {{# if action.targetDataElement.isUpdateValidatable }} - validate{{ firstToUpper action.targetDataElement.name }} - {{/ if }} - ); + const { result, data: returnedData } = await open{{ pageName action.targetPageDefinition }}({ + ownerData: produceDataAdjustedOwner(), + isDraft: true, + ownerValidation: validate, + dataPath: `${dataPath ? (dataPath + '.') : ''}{{ action.targetPageDefinition.dataElement.name }}{{# if (isActionParentEagerTable action) }}[${itemIndex!}]{{/ if }}` + }); // we might need to differentiate result handling between operation inputs and crud relation creates if (result === 'submit-draft' && returnedData) { {{# if action.targetDataElement.isCollection }} @@ -56,7 +49,9 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async (target: } else { {{/ and }} {{# if action.targetPageDefinition.openInDialog }} - await open{{ pageName action.targetPageDefinition }}(target!); + await open{{ pageName action.targetPageDefinition }}({ + ownerData: target!, + }); await refresh(); {{ else }} {{# if action.targetPageDefinition }} diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/OpenSetSelectorAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/OpenSetSelectorAction.fragment.hbs index 37d6d534..49ef7e2b 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/OpenSetSelectorAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/OpenSetSelectorAction.fragment.hbs @@ -1,6 +1,21 @@ {{# with (getLinkParentForActionDefinition action.actionDefinition) as |link| }} const {{ simpleActionDefinitionName action.actionDefinition }} = async (): Promise<{{ classDataName (getReferenceClassType link) 'Stored' }} | undefined> => { - const { result, data: returnedData } = await open{{ pageName action.targetPageDefinition }}({{{ getSelectorOpenActionParameters action page.container }}}); + const { result, data: returnedData } = await open{{ pageName action.targetPageDefinition }}({ + {{# unless page.container.table }} + ownerData: data, + {{ else }} + ownerData: getValue(owner.current, dataPath!, data), + {{/ unless }} + alreadySelected: {{# with (getRelationTypeForActionTarget action) as |rel| }} + {{# if container.table }} + [] + {{ else }} + data.{{ rel.name }}{{# if rel.isCollection }} ?? []{{ else }} ? [data.{{ rel.name }}] : []{{/ if }} + {{/ if }} + {{ else }} + undefined + {{/ with }} + }); if (result === 'submit') { if (Array.isArray(returnedData) && returnedData.length) { try { diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/PreFetchAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/PreFetchAction.fragment.hbs index 88074669..c41bc5a9 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/PreFetchAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/PreFetchAction.fragment.hbs @@ -1,5 +1,5 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async (): Promise> => { - return {{ getServiceImplForPage page }}.get{{ firstToUpper action.targetDataElement.name }}({{# if (pageHasSignedId page) }}{{# if page.openInDialog }}data{{ else }}{ __signedIdentifier: signedIdentifier } as JudoIdentifiable{{/ if }}{{ else }}data{{/ if }}, { + return {{ getServiceImplForPage page }}.get{{ firstToUpper action.targetDataElement.name }}({{# if (pageHasSignedId page) }}{{# if page.openInDialog }}data{{ else }}{ __signedIdentifier: signedIdentifier } as any{{/ if }}{{ else }}data{{/ if }}, { _mask: '{}', }); }; diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/RefreshAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/RefreshAction.fragment.hbs index 5b49c418..c8021c58 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/RefreshAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/RefreshAction.fragment.hbs @@ -1,6 +1,14 @@ {{# if action.targetDataElement }} const {{ simpleActionDefinitionName action.actionDefinition }} = async (queryCustomizer: {{ classDataName action.targetDataElement.target 'QueryCustomizer' }}): Promise> => { - return {{ getServiceImplForPage page }}.{{# if action.targetDataElement.isCollection }}list{{ else }}get{{/ if }}{{ firstToUpper action.targetDataElement.name }}({{{ refreshActionDataParameter action }}}, queryCustomizer{{# with (getTableParentForActionDefinition action.actionDefinition) as |table| }}{{# if table.showTotalCount }}, { [X_JUDO_COUNT_RECORDS]: 'true' }{{/ if }}{{/ with }}); + const result = await {{ getServiceImplForPage page }}.{{# if action.targetDataElement.isCollection }}list{{ else }}get{{/ if }}{{ firstToUpper action.targetDataElement.name }}({{{ refreshActionDataParameter action }}}, queryCustomizer{{# with (getTableParentForActionDefinition action.actionDefinition) as |table| }}{{# if table.showTotalCount }}, { [X_JUDO_COUNT_RECORDS]: 'true' }{{/ if }}{{/ with }}); + {{# if page.container.view }} + {{# if (isRefreshPageInitializer action page) }} + if (!isDraft) { + owner.current = result.data; + } + {{/ if }} + {{/ if }} + return result; } {{ else }} const {{ simpleActionDefinitionName action.actionDefinition }} = async (queryCustomizer: {{ classDataName (getReferenceClassType page) 'QueryCustomizer' }}): Promise> => { @@ -9,16 +17,18 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async (queryCus setEditMode(false); {{# if page.container.table }} {{# with (getTableParentForActionDefinition action.actionDefinition) as |table| }} - return {{ getServiceImplForPage page }}.list({{# if (pageHasSignedId page) }}{ __signedIdentifier: signedIdentifier } as JudoIdentifiable{{ else }}{{# if page.openInDialog }}ownerData{{ else }}undefined{{/ if }}{{/ if }}, queryCustomizer{{# if table.showTotalCount }}, { [X_JUDO_COUNT_RECORDS]: 'true' }{{/ if }}); + return {{ getServiceImplForPage page }}.list({{# if (pageHasSignedId page) }}{ __signedIdentifier: signedIdentifier } as any{{ else }}{{# if page.openInDialog }}ownerData{{ else }}undefined{{/ if }}{{/ if }}, queryCustomizer{{# if table.showTotalCount }}, { [X_JUDO_COUNT_RECORDS]: 'true' }{{/ if }}); {{/ with }} {{ else }} const response = await {{ getServiceImplForPage page }}.refresh({{{ refreshActionDataParameter action }}}, getPageQueryCustomizer()); const { data: result } = response; + {{# if (isRefreshPageInitializer action page) }} + if (!isDraft) { + owner.current = result; + } + {{/ if }} setData(result); setLatestViewData(result); - // re-set payloadDiff - payloadDiff.current = {} as unknown as Record; - decoratePayloadWithMetaFromData(payloadDiff.current, result); if (customActions?.post{{ firstToUpper (simpleActionDefinitionName action.actionDefinition) }}) { await customActions?.post{{ firstToUpper (simpleActionDefinitionName action.actionDefinition) }}(result{{# unless page.container.table }}, storeDiff, setValidation{{/ unless }}); } diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/RemoveAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/RemoveAction.fragment.hbs index c5976a73..0e752f01 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/RemoveAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/RemoveAction.fragment.hbs @@ -9,7 +9,7 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async (target?: if (!silentMode) { setIsLoading(true); } - await {{ getServiceImplForPage page }}.remove{{ firstToUpper action.targetDataElement.name }}({{# if page.container.table }}{ __signedIdentifier: signedIdentifier } as JudoIdentifiable{{ else }}data{{/ if }}, [target!]); + await {{ getServiceImplForPage page }}.remove{{ firstToUpper action.targetDataElement.name }}({{# if page.container.table }}{ __signedIdentifier: signedIdentifier } as any{{ else }}data{{/ if }}, [target!]); if (!silentMode) { await refresh(); } diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/RowDeleteAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/RowDeleteAction.fragment.hbs index cc51131b..377e158f 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/RowDeleteAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/RowDeleteAction.fragment.hbs @@ -32,7 +32,9 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async ({{# if a {{/ if }} {{ else }} {{# if (containerIsRefreshable page.container) }} - await refresh(); + {{# unless (isActionParentEagerElement action) }} + await refresh(); + {{/ unless }} {{/ if }} {{/ unless }} {{/ if }} diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/SelectorRangeAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/SelectorRangeAction.fragment.hbs index 4486e1af..af394ca4 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/SelectorRangeAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/SelectorRangeAction.fragment.hbs @@ -2,13 +2,13 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async (queryCus {{# with (getTableParentForActionDefinition action.actionDefinition) as |table| }} try { {{# if container.isRelationSelector }} - return {{ getServiceImplForPage page }}.getRange(cleanUpPayload(ownerData), queryCustomizer{{# if table.showTotalCount }}, { [X_JUDO_COUNT_RECORDS]: 'true' }{{/ if }}); + return {{ getServiceImplForPage page }}.getRange(produceDataAdjustedOwner(), queryCustomizer{{# if table.showTotalCount }}, { [X_JUDO_COUNT_RECORDS]: 'true' }{{/ if }}); {{ else }} - return {{ getServiceImplForPage page }}.getRange{{ firstToUpper (getServiceMethodSuffix action) }}(cleanUpPayload(ownerData), queryCustomizer{{# if table.showTotalCount }}, { [X_JUDO_COUNT_RECORDS]: 'true' }{{/ if }}); + return {{ getServiceImplForPage page }}.getRange{{ firstToUpper (getServiceMethodSuffix action) }}(produceDataAdjustedOwner(), queryCustomizer{{# if table.showTotalCount }}, { [X_JUDO_COUNT_RECORDS]: 'true' }{{/ if }}); {{/ if }} } catch (error: any) { handleError(error); - return Promise.resolve({ data: [], headers: error.response?.headers, status: error.response?.status }); + return Promise.resolve({ data: [], headers: error.response?.headers, status: error.response?.status, statusText: 'fallback', config: { headers: error.response?.headers } }); } {{/ with }} }; diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/UpdateAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/UpdateAction.fragment.hbs index 577d4b55..c1ddbb89 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/UpdateAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/UpdateAction.fragment.hbs @@ -6,16 +6,17 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async () => { if (isDraft) { try { setIsLoading(true); - await validate(cleanUpPayload(payloadDiff.current)); + const validationData = simpleCloneDeep(owner.current); + setValue(validationData, dataPath!, data); + await validate(validationData); // we send data back in draft mode, because the owner is responsible to handle it in-memory onSubmit(data, 'submit-draft'); } catch (error) { - if (ownerValidation && !isErrorNestedValidationError(error, '{{ page.dataElement.name }}')) { + if (ownerValidation && !isErrorNestedValidationError(error, dataPath!)) { // relation form has no remaining error(s), proceed with submission - onSubmit(payloadDiff.current, 'submit-draft'); + onSubmit(data, 'submit-draft'); } else { - let relationName: string | undefined = (isDraft && ownerValidation) ? '{{ page.dataElement.name }}' : undefined; - handleError<{{ classDataName (getReferenceClassType page) '' }}>(error, { setValidation }, data, relationName); + handleError<{{ classDataName (getReferenceClassType page) '' }}>(error, { setValidation }, owner.current, dataPath!); } } finally { setIsLoading(false); @@ -26,7 +27,7 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async () => { {{/ if }} setIsLoading(true); try { - const { data: res } = await {{ getServiceImplForPage page }}.update(cleanUpPayload(payloadDiff.current)); + const { data: res } = await {{ getServiceImplForPage page }}.update(data); if (res) { showSuccessSnack(t('judo.action.save.success', { defaultValue: 'Changes saved' })); {{# and page.openInDialog action.actionDefinition.autoCloseOnSave }} diff --git a/judo-ui-react/src/main/resources/actor/src/pages/index.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/pages/index.tsx.hbs index f2d2527f..dc7a4cfb 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/index.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/index.tsx.hbs @@ -1,6 +1,6 @@ {{> fragment.header.hbs }} -import { {{# unless page.container.table }}useCallback, useEffect, useRef, {{/ unless }}useState, useMemo, lazy, Suspense } from 'react'; +import { {{# unless page.container.table }}useEffect, {{/ unless }}useCallback, useRef, useState, useMemo, lazy, Suspense } from 'react'; import { OBJECTCLASS } from '@pandino/pandino-api'; {{# if (containerIsEmptyDashboard page.container) }} import { ComponentProxy } from '@pandino/react-hooks'; @@ -10,6 +10,7 @@ import { OBJECTCLASS } from '@pandino/pandino-api'; import type { FC, ReactNode, Dispatch, SetStateAction } from 'react'; import { useTrackService } from '@pandino/react-hooks'; import type { JudoIdentifiable } from '~/services/data-api/common/JudoIdentifiable'; + import { draftIdentifierPrefix } from '~/services/data-api/common/utils'; import type { JudoRestResponse } from '~/services/data-api/rest/requestResponse'; {{# if (containerHasTableWithTotalCount page.container) }} import { X_JUDO_COUNT_RECORDS } from '~/services/data-api/rest/headers'; @@ -29,11 +30,10 @@ import { OBJECTCLASS } from '@pandino/pandino-api'; import { {{# if (hasPageRequiredBy page) }}passesLocalValidation,{{/ if }} processQueryCustomizer, - {{# if (containerHasDateInput page.container) }}uiDateToServiceDate,{{/ if }} - {{# if (containerHasTimeInput page.container) }}uiTimeToServiceTime,{{/ if }} useErrorHandler, - cleanUpPayload, - decoratePayloadWithMetaFromData, + simpleCloneDeep, + getValue, + setValue, {{# if (hasExportAction page) }}fileHandling,{{/ if }} } from '~/utilities'; import type { @@ -72,11 +72,6 @@ import { OBJECTCLASS } from '@pandino/pandino-api'; {{/ if }} {{# unless (containerIsEmptyDashboard page.container) }} - {{# unless page.container.table }} - {{# if isDebugPrint }}// include: actor/src/fragments/page/payload-converter.fragment.hbs{{/ if }} - {{> actor/src/fragments/page/payload-converter.fragment.hbs classType=page.dataElement.target page=page }} - {{/ unless }} - const {{ containerComponentName page.container }}PageContainer = lazy(() => import('~/containers/{{ containerPath page.container }}/{{ containerComponentName page.container }}PageContainer')); {{/ unless }} @@ -84,9 +79,16 @@ import { OBJECTCLASS } from '@pandino/pandino-api'; // Name: {{ page.name }} export default function {{ pageName page }}() { {{# unless (containerIsEmptyDashboard page.container) }} + const dataPath = ''; + const isDraft = false; + const owner = useRef(null); + {{# if (pageHasSignedId page) }} // Router params section const { signedIdentifier } = useParams(); + {{# if page.container.table }} + owner.current = { __signedIdentifier: signedIdentifier }; + {{/ if }} {{/ if }} // Services @@ -123,7 +125,6 @@ export default function {{ pageName page }}() { {{# if (isSingleAccessPage page) }} const singletonHost = useRef<{ __signedIdentifier: string }>({} as unknown as { __signedIdentifier: string }); {{/ if }} - const payloadDiff = useRef>({} as unknown as Record); // Callback section const storeDiff: (attributeName: keyof {{ classDataName (getReferenceClassType page) '' }}, value: any) => void = useCallback((attributeName: keyof {{ classDataName (getReferenceClassType page) '' }}, value: any) => { @@ -198,37 +199,19 @@ export default function {{ pageName page }}() { } {{/ if }} }; + const produceDataAdjustedOwner = useCallback(() => { + const copy = simpleCloneDeep(owner.current); + setValue(copy, dataPath, simpleCloneDeep(data)); + return copy; + }, [data, owner]); // Validation {{# unless page.container.table }} - {{# each page.container.links as |link| }} - {{# and page.dataElement.isUpdateValidatable link.relationType.isUpdateValidatable link.isEager }} - const validate{{ firstToUpper link.relationType.name }} = async (linkData: {{ classDataName link.relationType.target '' }}): Promise => { - await {{ getServiceImplForPage page }}.validateUpdate({{# unless page.dataElement.isAccess }}null as any, {{/ unless }}{ - ...cleanUpPayload({ - ...payloadDiff.current, - {{ link.relationType.name }}: { - ...linkData, - } as any - }), - } as {{ classDataName (getReferenceClassType page) 'Stored' }}); - }; - {{/ and }} - {{/ each }} - {{# each page.container.tables as |table| }} - {{# and page.dataElement.isUpdateValidatable table.relationType.isUpdateValidatable table.isEager }} - const validate{{ firstToUpper table.relationType.name }} = async (tableData: {{ classDataName table.relationType.target '' }}): Promise => { - await {{ getServiceImplForPage page }}.validateUpdate({{# unless page.dataElement.isAccess }}null as any, {{/ unless }}{ - ...cleanUpPayload({ - ...payloadDiff.current, - {{ table.relationType.name }}: [ - { ...tableData } as any - ], - }), - } as {{ classDataName (getReferenceClassType page) 'Stored' }}); - }; - {{/ and }} - {{/ each }} + const validate: (target: any) => Promise = useCallback(async (target) => { + {{# if (isValidationSupported page) }} + await {{ getServiceImplForPage page }}.validateUpdate({{# unless page.dataElement.isAccess }}null as any, {{/ unless }}target); + {{/ if }} + }, [{{# if (isValidationSupported page) }}data, {{ getServiceImplForPage page }}{{/ if }}]); {{/ unless }} // Pandino Action overrides @@ -319,6 +302,7 @@ export default function {{ pageName page }}() { const res = await getSingletonPayload(); if (res?.__signedIdentifier) { singletonHost.current = res; + owner.current = res; } else { navigate('*'); return; @@ -338,6 +322,7 @@ export default function {{ pageName page }}() { <{{ containerComponentName page.container }}PageContainer actions={actions} + dataPath={dataPath} isLoading={isLoading} editMode={editMode} refreshCounter={refreshCounter} diff --git a/judo-ui-react/src/main/resources/actor/src/utilities/error-handling.ts.hbs b/judo-ui-react/src/main/resources/actor/src/utilities/error-handling.ts.hbs index 3891bf7c..89965b6d 100644 --- a/judo-ui-react/src/main/resources/actor/src/utilities/error-handling.ts.hbs +++ b/judo-ui-react/src/main/resources/actor/src/utilities/error-handling.ts.hbs @@ -46,7 +46,7 @@ export const useErrorHandler = () => { const { showErrorSnack } = useSnacks(); const openFaultDialog = useFaultDialog(); - return (error: any, options?: ErrorHandlingOption, payload?: T, relationName?: string) => { + return (error: any, options?: ErrorHandlingOption, payload?: T, dataPath?: string) => { console.error(error); const errorResults: ErrorProcessResult = { errorToastConfig: { @@ -117,10 +117,8 @@ export const useErrorHandler = () => { if (exists(errorList[0].location)) { errorResults.validation = new Map(); if (typeof options?.setValidation === 'function') { - const errorRelations: Record> = {}; - - if (relationName) { - validateRelationErrors(errorList, relationName, errorResults.validation, t); + if (dataPath) { + validateRelationErrors(errorList, dataPath, errorResults.validation, t); } else { validateWithNestedErrors(errorList, errorResults.validation, t); } @@ -145,15 +143,15 @@ export const useErrorHandler = () => { }; }; -function validateRelationErrors(errorList: ServerError[], relationName: string, validation: Map, t: any): void { +function validateRelationErrors(errorList: ServerError[], dataPath: string, validation: Map, t: any): void { errorList.forEach((error) => { - if ((error.location.startsWith(relationName + '.') || error.location.startsWith(relationName + '['))) { - const idx = error.location.startsWith(relationName + '.') ? error.location.indexOf('.') + 1 : error.location.lastIndexOf(']') + 2; + if (error.location.startsWith(dataPath)) { + const idx = error.location.includes('.') ? error.location.lastIndexOf('.') + 1 : 0; validation.set( error.location.substring(idx) as keyof T, t(`judo.error.validation-failed.${error.code}`, { defaultValue: error.code, - relation: relationName, + relation: dataPath, error, }) as string, ); @@ -197,11 +195,14 @@ export const isErrorOperationFault = (error: any): boolean => { return error?.response?.status === 422; }; -export const isErrorNestedValidationError = (error: any, relation: string): boolean => { +export const isErrorNestedValidationError = (error: any, dataPath?: string): boolean => { + if (dataPath === undefined || dataPath === null) { + return false; + } const { response } = error; return response && response.status === 400 && Array.isArray(response.data) && response.data.length - && response.data.some((e: any) => e.location.startsWith(relation + '.') || e.location.startsWith(relation + '[')); + && response.data.some((e: any) => e.location.startsWith(dataPath)); } diff --git a/judo-ui-react/src/main/resources/actor/src/utilities/filter-helper.test.ts b/judo-ui-react/src/main/resources/actor/src/utilities/filter-helper.test.ts index 76db9b83..b2441d7d 100644 --- a/judo-ui-react/src/main/resources/actor/src/utilities/filter-helper.test.ts +++ b/judo-ui-react/src/main/resources/actor/src/utilities/filter-helper.test.ts @@ -1,15 +1,20 @@ -import { expect, describe, it } from 'vitest'; -import { _NumericOperation, _StringOperation, _BooleanOperation, _EnumerationOperation } from '~/services/data-api/common'; +import { describe, expect, it } from 'vitest'; +import type { Filter } from '~/components-api'; +import { FilterType } from '~/components-api'; +import { + _BooleanOperation, + _EnumerationOperation, + _NumericOperation, + _StringOperation, +} from '~/services/data-api/common'; import { applyInMemoryFilters, - filterByStringOperation, - filterByNumericOperation, - filterByDateOperation, filterByBooleanOperation, + filterByDateOperation, filterByEnumerationOperation, + filterByNumericOperation, + filterByStringOperation, } from '~/utilities/filter-helper'; -import type { Filter, Operation, FilterOption } from '~/components-api'; -import { FilterType } from '~/components-api'; enum TestEnum { yayy = 'YAYY', @@ -20,16 +25,43 @@ interface TestType { name: string; age: number; isOkay?: boolean; - registered: string; - left: string; + registered: Date; + left: Date; happy: TestEnum; } const data: TestType[] = [ - { name: 'Jake', age: 31, isOkay: true, registered: '2023-06-19', left: '2023-06-20T13:20:00.000Z', happy: TestEnum.yayy }, - { name: 'Andrea', age: 14, isOkay: true, registered: '2023-02-19', left: '2023-02-20T13:20:00.000Z', happy: TestEnum.nay }, - { name: 'Julia', age: 55, registered: '2023-04-19', left: '2023-04-20T13:18:00.000Z', happy: TestEnum.nay }, - { name: 'Henry', age: 41, isOkay: false, registered: '2023-04-18', left: '2023-04-20T13:20:00.000Z', happy: TestEnum.yayy }, + { + name: 'Jake', + age: 31, + isOkay: true, + registered: new Date('2023-06-19'), + left: new Date('2023-06-20T13:20:00.000Z'), + happy: TestEnum.yayy, + }, + { + name: 'Andrea', + age: 14, + isOkay: true, + registered: new Date('2023-02-19'), + left: new Date('2023-02-20T13:20:00.000Z'), + happy: TestEnum.nay, + }, + { + name: 'Julia', + age: 55, + registered: new Date('2023-04-19'), + left: new Date('2023-04-20T13:18:00.000Z'), + happy: TestEnum.nay, + }, + { + name: 'Henry', + age: 41, + isOkay: false, + registered: new Date('2023-04-18'), + left: new Date('2023-04-20T13:20:00.000Z'), + happy: TestEnum.yayy, + }, ]; describe('filterByStringOperation', () => { @@ -47,7 +79,7 @@ describe('filterByStringOperation', () => { expect(result.length).toBe(3); expect(result[0].name).toBe('Andrea'); - expect(result.map(r => r.name)).toEqual(['Andrea', 'Julia', 'Henry']); + expect(result.map((r) => r.name)).toEqual(['Andrea', 'Julia', 'Henry']); }); it('like', () => { @@ -55,7 +87,7 @@ describe('filterByStringOperation', () => { const result = filterByStringOperation(filter, data); expect(result.length).toBe(1); - expect(result.map(r => r.name)).toEqual(['Henry']); + expect(result.map((r) => r.name)).toEqual(['Henry']); }); it('greaterOrEqual', () => { @@ -63,7 +95,7 @@ describe('filterByStringOperation', () => { const result = filterByStringOperation(filter, data); expect(result.length).toBe(2); - expect(result.map(r => r.name)).toEqual(['Jake', 'Julia']); + expect(result.map((r) => r.name)).toEqual(['Jake', 'Julia']); }); it('greaterThan', () => { @@ -71,7 +103,7 @@ describe('filterByStringOperation', () => { const result = filterByStringOperation(filter, data); expect(result.length).toBe(1); - expect(result.map(r => r.name)).toEqual(['Julia']); + expect(result.map((r) => r.name)).toEqual(['Julia']); }); it('lessOrEqual', () => { @@ -79,7 +111,7 @@ describe('filterByStringOperation', () => { const result = filterByStringOperation(filter, data); expect(result.length).toBe(3); - expect(result.map(r => r.name)).toEqual(['Jake', 'Andrea', 'Henry']); + expect(result.map((r) => r.name)).toEqual(['Jake', 'Andrea', 'Henry']); }); it('lessThan', () => { @@ -87,7 +119,7 @@ describe('filterByStringOperation', () => { const result = filterByStringOperation(filter, data); expect(result.length).toBe(2); - expect(result.map(r => r.name)).toEqual(['Andrea', 'Henry']); + expect(result.map((r) => r.name)).toEqual(['Andrea', 'Henry']); }); }); @@ -97,7 +129,7 @@ describe('filterByNumericOperation', () => { const result = filterByNumericOperation(filter, data); expect(result.length).toBe(1); - expect(result.map(r => r.name)).toEqual(['Jake']); + expect(result.map((r) => r.name)).toEqual(['Jake']); }); it('notEqual', () => { @@ -105,7 +137,7 @@ describe('filterByNumericOperation', () => { const result = filterByNumericOperation(filter, data); expect(result.length).toBe(3); - expect(result.map(r => r.name)).toEqual(['Andrea', 'Julia', 'Henry']); + expect(result.map((r) => r.name)).toEqual(['Andrea', 'Julia', 'Henry']); }); it('lessThan', () => { @@ -113,7 +145,7 @@ describe('filterByNumericOperation', () => { const result = filterByNumericOperation(filter, data); expect(result.length).toBe(1); - expect(result.map(r => r.name)).toEqual(['Andrea']); + expect(result.map((r) => r.name)).toEqual(['Andrea']); }); it('lessOrEqual', () => { @@ -121,7 +153,7 @@ describe('filterByNumericOperation', () => { const result = filterByNumericOperation(filter, data); expect(result.length).toBe(2); - expect(result.map(r => r.name)).toEqual(['Jake', 'Andrea']); + expect(result.map((r) => r.name)).toEqual(['Jake', 'Andrea']); }); it('greaterThan', () => { @@ -129,7 +161,7 @@ describe('filterByNumericOperation', () => { const result = filterByNumericOperation(filter, data); expect(result.length).toBe(2); - expect(result.map(r => r.name)).toEqual(['Julia', 'Henry']); + expect(result.map((r) => r.name)).toEqual(['Julia', 'Henry']); }); it('greaterOrEqual', () => { @@ -137,7 +169,7 @@ describe('filterByNumericOperation', () => { const result = filterByNumericOperation(filter, data); expect(result.length).toBe(3); - expect(result.map(r => r.name)).toEqual(['Jake', 'Julia', 'Henry']); + expect(result.map((r) => r.name)).toEqual(['Jake', 'Julia', 'Henry']); }); }); @@ -147,7 +179,7 @@ describe('filterByDateOperation', () => { const result = filterByDateOperation(filter, data); expect(result.length).toBe(1); - expect(result.map(r => r.name)).toEqual(['Jake']); + expect(result.map((r) => r.name)).toEqual(['Jake']); }); it('notEqual', () => { @@ -155,7 +187,7 @@ describe('filterByDateOperation', () => { const result = filterByDateOperation(filter, data); expect(result.length).toBe(3); - expect(result.map(r => r.name)).toEqual(['Andrea', 'Julia', 'Henry']); + expect(result.map((r) => r.name)).toEqual(['Andrea', 'Julia', 'Henry']); }); it('lessThan', () => { @@ -163,7 +195,7 @@ describe('filterByDateOperation', () => { const result = filterByDateOperation(filter, data); expect(result.length).toBe(1); - expect(result.map(r => r.name)).toEqual(['Andrea']); + expect(result.map((r) => r.name)).toEqual(['Andrea']); }); it('lessOrEqual', () => { @@ -171,7 +203,7 @@ describe('filterByDateOperation', () => { const result = filterByDateOperation(filter, data); expect(result.length).toBe(3); - expect(result.map(r => r.name)).toEqual(['Andrea', 'Julia', 'Henry']); + expect(result.map((r) => r.name)).toEqual(['Andrea', 'Julia', 'Henry']); }); it('greaterThan', () => { @@ -179,7 +211,7 @@ describe('filterByDateOperation', () => { const result = filterByDateOperation(filter, data); expect(result.length).toBe(2); - expect(result.map(r => r.name)).toEqual(['Jake', 'Julia']); + expect(result.map((r) => r.name)).toEqual(['Jake', 'Julia']); }); it('greaterOrEqual', () => { @@ -187,7 +219,7 @@ describe('filterByDateOperation', () => { const result = filterByDateOperation(filter, data); expect(result.length).toBe(3); - expect(result.map(r => r.name)).toEqual(['Jake', 'Julia', 'Henry']); + expect(result.map((r) => r.name)).toEqual(['Jake', 'Julia', 'Henry']); }); }); @@ -197,7 +229,7 @@ describe('filterByDateTimeOperation', () => { const result = filterByDateOperation(filter, data); expect(result.length).toBe(1); - expect(result.map(r => r.name)).toEqual(['Jake']); + expect(result.map((r) => r.name)).toEqual(['Jake']); }); it('notEqual', () => { @@ -205,7 +237,7 @@ describe('filterByDateTimeOperation', () => { const result = filterByDateOperation(filter, data); expect(result.length).toBe(3); - expect(result.map(r => r.name)).toEqual(['Andrea', 'Julia', 'Henry']); + expect(result.map((r) => r.name)).toEqual(['Andrea', 'Julia', 'Henry']); }); it('lessThan', () => { @@ -213,7 +245,7 @@ describe('filterByDateTimeOperation', () => { const result = filterByDateOperation(filter, data); expect(result.length).toBe(1); - expect(result.map(r => r.name)).toEqual(['Andrea']); + expect(result.map((r) => r.name)).toEqual(['Andrea']); }); it('lessOrEqual', () => { @@ -221,7 +253,7 @@ describe('filterByDateTimeOperation', () => { const result = filterByDateOperation(filter, data); expect(result.length).toBe(3); - expect(result.map(r => r.name)).toEqual(['Andrea', 'Julia', 'Henry']); + expect(result.map((r) => r.name)).toEqual(['Andrea', 'Julia', 'Henry']); }); it('greaterThan', () => { @@ -229,7 +261,7 @@ describe('filterByDateTimeOperation', () => { const result = filterByDateOperation(filter, data); expect(result.length).toBe(2); - expect(result.map(r => r.name)).toEqual(['Jake', 'Henry']); + expect(result.map((r) => r.name)).toEqual(['Jake', 'Henry']); }); it('greaterOrEqual', () => { @@ -237,7 +269,7 @@ describe('filterByDateTimeOperation', () => { const result = filterByDateOperation(filter, data); expect(result.length).toBe(3); - expect(result.map(r => r.name)).toEqual(['Jake', 'Julia', 'Henry']); + expect(result.map((r) => r.name)).toEqual(['Jake', 'Julia', 'Henry']); }); }); @@ -247,7 +279,7 @@ describe('filterByBooleanOperation', () => { const result = filterByBooleanOperation(filter, data); expect(result.length).toBe(2); - expect(result.map(r => r.name)).toEqual(['Jake', 'Andrea']); + expect(result.map((r) => r.name)).toEqual(['Jake', 'Andrea']); }); it('equals - false', () => { @@ -255,7 +287,7 @@ describe('filterByBooleanOperation', () => { const result = filterByBooleanOperation(filter, data); expect(result.length).toBe(1); - expect(result.map(r => r.name)).toEqual(['Henry']); + expect(result.map((r) => r.name)).toEqual(['Henry']); }); }); @@ -265,7 +297,7 @@ describe('filterByTrinaryLogicOperation', () => { const result = filterByBooleanOperation(filter, data); expect(result.length).toBe(2); - expect(result.map(r => r.name)).toEqual(['Jake', 'Andrea']); + expect(result.map((r) => r.name)).toEqual(['Jake', 'Andrea']); }); it('equals - false', () => { @@ -273,7 +305,7 @@ describe('filterByTrinaryLogicOperation', () => { const result = filterByBooleanOperation(filter, data); expect(result.length).toBe(1); - expect(result.map(r => r.name)).toEqual(['Henry']); + expect(result.map((r) => r.name)).toEqual(['Henry']); }); it('equals - undefined', () => { @@ -281,7 +313,7 @@ describe('filterByTrinaryLogicOperation', () => { const result = filterByBooleanOperation(filter, data); expect(result.length).toBe(1); - expect(result.map(r => r.name)).toEqual(['Julia']); + expect(result.map((r) => r.name)).toEqual(['Julia']); }); }); @@ -291,7 +323,7 @@ describe('filterByEnumerationOperation', () => { const result = filterByEnumerationOperation(filter, data); expect(result.length).toBe(2); - expect(result.map(r => r.name)).toEqual(['Jake', 'Henry']); + expect(result.map((r) => r.name)).toEqual(['Jake', 'Henry']); }); it('notEquals', () => { @@ -299,7 +331,7 @@ describe('filterByEnumerationOperation', () => { const result = filterByEnumerationOperation(filter, data); expect(result.length).toBe(2); - expect(result.map(r => r.name)).toEqual(['Andrea', 'Julia']); + expect(result.map((r) => r.name)).toEqual(['Andrea', 'Julia']); }); }); @@ -310,7 +342,7 @@ describe('filter combinations', () => { const result = applyInMemoryFilters([happyIsYayy, registeredBefore], data); expect(result.length).toBe(1); - expect(result.map(r => r.name)).toEqual(['Andrea']); + expect(result.map((r) => r.name)).toEqual(['Andrea']); }); it('string with numeric', () => { @@ -320,7 +352,7 @@ describe('filter combinations', () => { const result = applyInMemoryFilters([nameLessOrEqual, ageLessOrEqual], data); expect(result.length).toBe(2); - expect(result.map(r => r.name)).toEqual(['Jake', 'Andrea']); + expect(result.map((r) => r.name)).toEqual(['Jake', 'Andrea']); }); }); @@ -426,7 +458,11 @@ function createTrinaryLogicFilter(attributeName: keyof TestType, operator: _Bool }; } -function createEnumerationFilter(attributeName: keyof TestType, operator: _EnumerationOperation, value: keyof T): Filter { +function createEnumerationFilter( + attributeName: keyof TestType, + operator: _EnumerationOperation, + value: keyof T, +): Filter { return { id: 'filterByEnumerationOperation', operationId: 'filterByEnumerationOperation', diff --git a/judo-ui-react/src/main/resources/actor/src/utilities/filter-helper.ts.hbs b/judo-ui-react/src/main/resources/actor/src/utilities/filter-helper.ts.hbs index 4fcb3e97..2588a2f4 100644 --- a/judo-ui-react/src/main/resources/actor/src/utilities/filter-helper.ts.hbs +++ b/judo-ui-react/src/main/resources/actor/src/utilities/filter-helper.ts.hbs @@ -5,8 +5,8 @@ import { GridLogicOperator } from '@mui/x-data-grid{{ getMUIDataGridPlanSuffix } import { isEqual, compareAsc } from 'date-fns'; import type { Filter, FilterOption, Operation } from '../components-api'; import { FilterType } from '../components-api'; -import { dateToJudoDateString } from './helper'; -import { serviceDateToUiDate } from './form-utils'; +import type { Serializer } from '~/services/data-api/common/Serializer'; +import { simpleCloneDeep } from './helper'; type FilterBy = { value: any; @@ -16,20 +16,11 @@ type FilterBy = { export const mapFiltersToQueryCustomizerProperty = (filters: Filter[], property: string): FilterBy[] | undefined => { if (!filters.some((filter) => filter.filterOption.attributeName === property)) return undefined; - const convertFilterValue = (filter: Filter): any => { - if (filter.filterOption.filterType === FilterType.dateTime && filter.filterBy.value instanceof Date) { - return filter.filterBy.value.toISOString(); - } else if (filter.filterOption.filterType === FilterType.date && filter.filterBy.value instanceof Date) { - return dateToJudoDateString(filter.filterBy.value); - } - return filter.filterBy.value; - }; - return filters .filter((filter) => filter.filterOption.attributeName === property && filter.filterBy.value !== undefined && filter.filterBy.value !== null) .map((filter) => { return { - value: convertFilterValue(filter), + value: filter.filterBy.value, operator: filter.filterBy.operator, }; }); @@ -129,17 +120,17 @@ export function filterByDateOperation(filter: Filter, data: T[]): T[] { const attributeName = filter.filterOption.attributeName as keyof T; switch (filter.filterBy.operator) { case _NumericOperation.equal: - return data.filter((d) => isEqual(serviceDateToUiDate(d[attributeName]), filter.filterBy.value)); + return data.filter((d) => isEqual(d[attributeName] as Date, filter.filterBy.value)); case _NumericOperation.notEqual: - return data.filter((d) => !isEqual(serviceDateToUiDate(d[attributeName]), filter.filterBy.value)); + return data.filter((d) => !isEqual(d[attributeName] as Date, filter.filterBy.value)); case _NumericOperation.lessThan: - return data.filter((d) => compareAsc(serviceDateToUiDate(d[attributeName]), filter.filterBy.value) < 0); + return data.filter((d) => compareAsc(d[attributeName] as Date, filter.filterBy.value) < 0); case _NumericOperation.lessOrEqual: - return data.filter((d) => compareAsc(serviceDateToUiDate(d[attributeName]), filter.filterBy.value) <= 0); + return data.filter((d) => compareAsc(d[attributeName] as Date, filter.filterBy.value) <= 0); case _NumericOperation.greaterThan: - return data.filter((d) => compareAsc(serviceDateToUiDate(d[attributeName]), filter.filterBy.value) > 0); + return data.filter((d) => compareAsc(d[attributeName] as Date, filter.filterBy.value) > 0); case _NumericOperation.greaterOrEqual: - return data.filter((d) => compareAsc(serviceDateToUiDate(d[attributeName]), filter.filterBy.value) >= 0); + return data.filter((d) => compareAsc(d[attributeName] as Date, filter.filterBy.value) >= 0); default: return [...data]; } @@ -395,3 +386,32 @@ export const buildFilter: FilterBuilder = (filterType, operator, attributeName, }, }; }; + +export interface FiltersSerializer { + serialize: (filters: Filter[]) => Filter[]; + deserialize: (filters: Filter[]) => Filter[]; +} + +export const serializeFilters = (filters: Filter[], serializer: Serializer): Filter[] => { + return simpleCloneDeep(filters).map(a => ({ + ...a, + filterBy: { + ...a.filterBy, + value: serializer.serialize({ + [a.filterOption.attributeName]: a.filterBy.value, + } as T)[a.filterOption.attributeName], + }, + })); +} + +export const deserializeFilters = (filters: Filter[], serializer: Serializer): Filter[] => { + return filters.map(a => ({ + ...a, + filterBy: { + ...a.filterBy, + value: serializer.deserialize({ + [a.filterOption.attributeName as keyof T]: a.filterBy.value, + })[a.filterOption.attributeName as keyof T], + }, + })); +}; diff --git a/judo-ui-react/src/main/resources/actor/src/utilities/form-utils.ts.hbs b/judo-ui-react/src/main/resources/actor/src/utilities/form-utils.ts.hbs index afba5295..28ed58f3 100644 --- a/judo-ui-react/src/main/resources/actor/src/utilities/form-utils.ts.hbs +++ b/judo-ui-react/src/main/resources/actor/src/utilities/form-utils.ts.hbs @@ -4,39 +4,6 @@ import type { Dispatch, SetStateAction } from 'react'; import { format, parse } from 'date-fns'; import type { TFunction } from 'i18next'; -export const uiDateToServiceDate = (date?: any | null): string | null => { - if (date === undefined || date === null) { - return null; - } - - const resolved: Date = typeof date === 'string' ? new Date(date) : date; - - return format(resolved, 'yyyy-MM-dd') -}; - -export const serviceDateToUiDate = (dateStr?: any) => { - if (typeof dateStr === 'string') { - return new Date(dateStr); - } - return dateStr; -}; - -export const uiTimeToServiceTime = (time?: any | null): string | null => { - if(time === undefined || time === null) { - return null; - } - // const resolved: Date = typeof time === 'string' ? new Date(time) : time; - // return resolved.getUTCHours().toString().padStart(2, '0') + ':' + resolved.getUTCMinutes().toString().padStart(2, '0'); - return format(time, 'HH:mm:ss'); -} - -export const serviceTimeToUiTime = (timeStr?: any) => { - if (typeof timeStr === 'string') { - return parse(timeStr, 'HH:mm:ss', new Date()); - } - return timeStr; -}; - export function passesLocalValidation(data: T, requiredByRecord: Record, t: TFunction, setValidation: Dispatch>>): boolean { const failsRequired = (input: any): boolean => input === null || input === undefined || input === ''; const errorList: string[] = []; @@ -59,28 +26,3 @@ export function passesLocalValidation(data: T, requiredByRecord: Record = {}; - for (const key in input) { - const value = input[key]; - if (Array.isArray(value)) { - payload[key] = value.map((r) => { - if (r.__identifier && r.__identifier.startsWith(prefix)) { - const newRow = { ...r }; - delete newRow.__identifier; - return newRow; - } - return r; - }); - } else if (value && value.__identifier && value.__identifier.startsWith(prefix)) { - const newItem = { ...value }; - delete newItem.__identifier; - payload[key] = newItem; - } else if (key !== '__identifier' || !value.startsWith(prefix)) { - payload[key] = value; - } - } - return payload; -} diff --git a/judo-ui-react/src/main/resources/actor/src/utilities/helper.test.ts b/judo-ui-react/src/main/resources/actor/src/utilities/helper.test.ts new file mode 100644 index 00000000..331b2ae3 --- /dev/null +++ b/judo-ui-react/src/main/resources/actor/src/utilities/helper.test.ts @@ -0,0 +1,173 @@ +import { describe, expect, it } from 'vitest'; +import { getValue, setValue } from './helper'; + +describe('getValue function', () => { + const input = { a: [{ b: { c: 3 } }] }; + + it('basic functionality', () => { + expect(getValue(input, 'a[0].b.c')).toBe(3); + expect(getValue(input, 'a.b.c', 'default')).toBe('default'); + }); + + it('nested properties', () => { + expect(getValue(input, 'a[0].b')).toBeTruthy(); + expect(getValue(input, 'a[0].b.c')).toBe(3); + expect(getValue(input, 'a[0].b.c.d', 'default')).toBe('default'); + }); + + it('default values', () => { + expect(getValue(input, 'a.b.c', 'not found')).toBe('not found'); + expect(getValue(input, 'a[1].b.c', 'default')).toBe('default'); + expect(getValue(input, 'a[0].b.x', 42)).toBe(42); + }); + + it('non-existent paths', () => { + expect(getValue(input, 'x.y.z', 'default')).toBe('default'); + expect(getValue(input, 'a[0].x.y.z', 'default')).toBe('default'); + }); + + it('undefined objects return defaults', () => { + expect(getValue(undefined, 'a.b.c', 'default')).toBe('default'); + }); + + it('null objects return nulls', () => { + expect(getValue(null, 'a.b.c', 'default')).toBe(null); + }); + + it('handling empty path', () => { + expect(getValue(input, '', 'default')).toBe(input); + expect(getValue(input, '', null)).toBe(input); + }); + + it('handling missing path', () => { + expect(getValue(input, undefined, 'default')).toBe('default'); + expect(getValue(input)).toBe(null); + }); + + it('arrays as values', () => { + const inputWithArray = { a: { b: [1, 2, 3] } }; + expect(getValue(inputWithArray, 'a.b[0]')).toBe(1); + expect(getValue(inputWithArray, 'a.b[2]')).toBe(3); + expect(getValue(inputWithArray, 'a.b[3]', 'default')).toBe('default'); + }); +}); + +describe('setValue function', () => { + it('sets a simple property', () => { + const obj = {}; + setValue(obj, 'a', 1); + expect(obj).toEqual({ a: 1 }); + }); + + it('sets a nested property', () => { + const obj = {}; + setValue(obj, 'a.b', 2); + expect(obj).toEqual({ a: { b: 2 } }); + }); + + it('sets a property in an array', () => { + const obj = {}; + setValue(obj, 'a[0]', 3); + expect(obj).toEqual({ a: [3] }); + }); + + it('sets a nested property in an array', () => { + const obj = {}; + setValue(obj, 'a[0].b', 4); + expect(obj).toEqual({ a: [{ b: 4 }] }); + }); + + it('creates intermediate objects and arrays', () => { + const obj = {}; + setValue(obj, 'a[0].b.c[1]', 5); + expect(obj).toEqual({ a: [{ b: { c: [undefined, 5] } }] }); + }); + + it('overwrites existing property', () => { + const obj = { a: 1 }; + setValue(obj, 'a', 2); + expect(obj).toEqual({ a: 2 }); + }); + + it('sets property in existing nested object', () => { + const obj = { a: { b: 1 } }; + setValue(obj, 'a.c', 2); + expect(obj).toEqual({ a: { b: 1, c: 2 } }); + }); + + it('handles path with array indices', () => { + const obj = {}; + setValue(obj, 'a[1].b[2]', 6); + expect(obj).toEqual({ a: [undefined, { b: [undefined, undefined, 6] }] }); + }); + + it('handles mixed array and object path', () => { + const obj = {}; + setValue(obj, 'a.b[0].c', 7); + expect(obj).toEqual({ a: { b: [{ c: 7 }] } }); + }); + + it('handles leading dot in path', () => { + const obj = {}; + setValue(obj, '.a.b.c', 8); + expect(obj).toEqual({ a: { b: { c: 8 } } }); + }); + + it('handles trailing dot in path', () => { + const obj = {}; + setValue(obj, 'a.b.c.', 9); + expect(obj).toEqual({ a: { b: { c: 9 } } }); + }); + + it('sets value on non-object target', () => { + const obj = { a: null }; + setValue(obj, 'a.b', 10); + expect(obj).toEqual({ a: { b: 10 } }); + }); + + it('handles number as key', () => { + const obj = {}; + setValue(obj, '1', 11); + expect(obj).toEqual({ '1': 11 }); + }); + + it('creates nested arrays', () => { + const obj = {}; + setValue(obj, 'a[0][1]', 13); + expect(obj).toEqual({ a: [[undefined, 13]] }); + }); + + it('handles complex path with numbers and strings', () => { + const obj = {}; + setValue(obj, 'a[1].b[2].c', 14); + expect(obj).toEqual({ a: [undefined, { b: [undefined, undefined, { c: 14 }] }] }); + }); + + it('throws error for consecutive dots in path', () => { + const obj = {}; + expect(() => setValue(obj, 'a..b', 15)).toThrow(); + }); + + it('throws error for path with empty brackets', () => { + const obj = {}; + expect(() => setValue(obj, 'a[].b', 16)).toThrow(); + }); + + it('handles path with brackets and dots', () => { + const obj = {}; + setValue(obj, 'a[0].b.c[1]', 17); + expect(obj).toEqual({ a: [{ b: { c: [undefined, 17] } }] }); + }); + + it('handles setting a deep nested value', () => { + const obj = {}; + setValue(obj, 'a.b.c.d.e.f.g', 18); + expect(obj).toEqual({ a: { b: { c: { d: { e: { f: { g: 18 } } } } } } }); + }); + + it('does not overwrite existing nested objects', () => { + const obj = { a: { b: { c: 1 } } }; + setValue(obj, 'a.b.d', 19); + expect(obj).toEqual({ a: { b: { c: 1, d: 19 } } }); + }); +}); diff --git a/judo-ui-react/src/main/resources/actor/src/utilities/helper.ts.hbs b/judo-ui-react/src/main/resources/actor/src/utilities/helper.ts.hbs index f1f42e3c..461f5329 100644 --- a/judo-ui-react/src/main/resources/actor/src/utilities/helper.ts.hbs +++ b/judo-ui-react/src/main/resources/actor/src/utilities/helper.ts.hbs @@ -1,77 +1,138 @@ -{{> fragment.header.hbs }} +////////////////////////////////////////////////////////////////////////////// +// G E N E R A T E D S O U R C E +// -------------------------------- +// Factory expression: +// Path expression: 'src/utilities/helper.ts' +// Template name: actor/src/utilities/helper.ts +// Base URL: mvn:hu.blackbelt.judo.generator:judo-ui-react:1.0.0-SNAPSHOT +// Template file: actor/src/utilities/helper.ts.hbs -import type { RandomUtils } from './interfaces'; import type { JudoStored } from '../services/data-api/common/JudoStored'; +import type { RandomUtils } from './interfaces'; export const exists = (element: any) => element !== undefined && element !== null; -export const dateToJudoDateString = (date: Date | null | undefined) => { - // TODO: consider l10n - return date ? date.toISOString().substring(0, 10) : date; -}; - -export const simpleCloneDeep = (input: any): any => { - if (input === null || input === undefined) { - return input; - } else if (Array.isArray(input)) { - return input.map(simpleCloneDeep); - } else if (typeof input === 'object') { - let payload: Record = {}; - for (const key in input) { - payload[key] = simpleCloneDeep(input[key]); - } - return payload; - } +export const simpleCloneDeep = (input: T): T => { + if (input === null || input === undefined) { + return input; + } else if (Array.isArray(input)) { + return input.map(simpleCloneDeep) as T; + } else if (input instanceof Date) { return input; + } else if (input instanceof Set) { + return new Set(Array.from(input).map(simpleCloneDeep)) as T; + } else if (typeof input === 'object') { + let payload: Record = {}; + for (const key in input) { + payload[key] = simpleCloneDeep(input[key]); + } + return payload as T; + } + return input; }; -export const stringToBooleanSelect = (booleanString ?: string | null) : boolean | null => { - if(!booleanString || !booleanString.trim()) { - return null; - } else { - return booleanString === 'true'; - } -} +export const stringToBooleanSelect = (booleanString?: string | null): boolean | null => { + if (!booleanString || !booleanString.trim()) { + return null; + } else { + return booleanString === 'true'; + } +}; -export const booleanToStringSelect = (booleanParam ?: boolean | null ) : string | null => { - if (booleanParam === null || booleanParam === undefined ) { - return ' '; - } - else if (booleanParam) { - return 'true'; - } else { - return 'false'; - } -} +export const booleanToStringSelect = (booleanParam?: boolean | null): string | null => { + if (booleanParam === null || booleanParam === undefined) { + return ' '; + } else if (booleanParam) { + return 'true'; + } else { + return 'false'; + } +}; -export const endWithSlash = (input: string): string => input.endsWith('/') ? input : input + '/'; +export const endWithSlash = (input: string): string => (input.endsWith('/') ? input : input + '/'); // only for testing purposes -export const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); +export const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); -export const GENERATOR_UUID_V4 = '{{ getUUIDv4 }}'; +export const GENERATOR_UUID_V4 = 'c8376ca5-6435-47e8-a8a0-18fba3a17807'; export const randomUtils: RandomUtils = { getGeneratorUUIDv4: () => GENERATOR_UUID_V4, }; -export const decoratePayloadWithMetaFromData: (payload: any, data: JudoStored) => any = (payload, data) => { - if (data.__identifier) { - payload.__identifier = data.__identifier; +export const isPlainObj = (value: any) => !!value && Object.getPrototypeOf(value) === Object.prototype; + +const pathCache: Map = new Map(); + +export function getValue(input: any, path?: string, defaultValue?: any): T | null { + if (input === undefined) { + return defaultValue!; + } else if (input === null) { + return input; + } + if (path === null || path === undefined) { + return defaultValue ?? null; + } + if (!path.length) { + return input; + } + + let pathArray: string[]; + if (!pathCache.has(path)) { + pathCache.set(path, path.replace(/\[(\w+)\]/g, '.$1').split('.')); + } + pathArray = pathCache.get(path)!; + + let result: any = input; + + for (let i = 0; i < pathArray.length; i++) { + result = result[pathArray[i]]; + if (result === undefined || result === null) { + break; + } + } + + return result === undefined ? defaultValue : result; +} + +export function setValue(target: any, path: string, value: any): void { + if (path === null || path === undefined) { + return } - if (data.__signedIdentifier) { - payload.__signedIdentifier = data.__signedIdentifier; + if (path.length === 0) { + if (isPlainObj(target) && isPlainObj(value)) { + for (let key in value) { + target[key] = value[key]; + } + } + return; } - if (data.__version) { - payload.__version = data.__version; + if (path.includes('..') || path.includes('[]')) { + throw new Error(`Invalid path syntax: ${path}!`); } - if (data.__entityType) { - payload.__entityType = data.__entityType; + + let pathArray: string[]; + if (!pathCache.has(path)) { + pathCache.set(path, path.replace(/\[(\w+)\]/g, '.$1').split('.').filter(s => s.trim().length)); + } + pathArray = pathCache.get(path)!; + + let current = target; + + for (let i = 0; i < pathArray.length; i++) { + const key = pathArray[i]; + const isLastKey = i === pathArray.length - 1; + + if (isLastKey) { + current[key] = value; + } else { + if (current[key] === null || current[key] === undefined) { + // Determine if the next key is a number to create an array or an object + const nextKey = pathArray[i + 1]; + current[key] = isNaN(Number(nextKey)) ? {} : []; + } + current = current[key]; + } } - return payload; } -export const cloneDeep = (obj: object): T => { - // We know that this is slow, if becomes a bottleneck, we can replace this with a dep, or write it manually - return JSON.parse(JSON.stringify(obj)) as T; -}; diff --git a/judo-ui-react/src/main/resources/actor/src/utilities/interfaces.ts.hbs b/judo-ui-react/src/main/resources/actor/src/utilities/interfaces.ts.hbs index 52c26a0e..4694e653 100644 --- a/judo-ui-react/src/main/resources/actor/src/utilities/interfaces.ts.hbs +++ b/judo-ui-react/src/main/resources/actor/src/utilities/interfaces.ts.hbs @@ -9,7 +9,7 @@ export type ColumnActionsProvider = ( actions: TableRowAction[], t: TFunction, isLoading: boolean, getSelectedRows: () => RStored[], - ownerdata?: RStored, + ownerData?: RStored, options?: ColumnsActionsOptions, ) => GridColDef[]; diff --git a/judo-ui-react/src/main/resources/actor/src/utilities/table-row-actions.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/utilities/table-row-actions.tsx.hbs index c4498c0d..b608fa3c 100644 --- a/judo-ui-react/src/main/resources/actor/src/utilities/table-row-actions.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/utilities/table-row-actions.tsx.hbs @@ -11,7 +11,7 @@ export interface TableRowAction { row: RStored, isLoading: boolean, getSelectedRows: () => R[], - ownerdata?: any, + ownerData?: any, ) => boolean, disabledExpression?: string; isCRUD?: boolean; diff --git a/judo-ui-react/src/main/resources/actor/src/utilities/table.test.ts b/judo-ui-react/src/main/resources/actor/src/utilities/table.test.ts index bdb032c2..c4f6b436 100644 --- a/judo-ui-react/src/main/resources/actor/src/utilities/table.test.ts +++ b/judo-ui-react/src/main/resources/actor/src/utilities/table.test.ts @@ -59,12 +59,12 @@ describe('isRowSelectable', () => { expect(isRowSelectable(rangeResultRow, true)).toBe(true); }); - it('selectable, event though backend says it is already selected', () => { + it('not selectable, respects __selected flag', () => { const rangeResultRowSelected: ResponseTestType = { ...rangeResultRow, __selected: true, }; - expect(isRowSelectable(rangeResultRowSelected, true)).toBe(true); + expect(isRowSelectable(rangeResultRowSelected, true)).toBe(false); }); it('not selectable, present in memory', () => { diff --git a/judo-ui-react/src/main/resources/actor/src/utilities/table.ts.hbs b/judo-ui-react/src/main/resources/actor/src/utilities/table.ts.hbs index c04a7aa8..ecaf5b10 100644 --- a/judo-ui-react/src/main/resources/actor/src/utilities/table.ts.hbs +++ b/judo-ui-react/src/main/resources/actor/src/utilities/table.ts.hbs @@ -76,7 +76,7 @@ export const isRowSelectable = >(row: T & { __selected?: // This branch should be used by aggregation tables because in these cases callers would pass their in-memory // item list which we need to check. return !alreadySelectedItems.find(i => i.__identifier === row.__identifier); - } else if (!single) { + } else if (!single || typeof row?.__selected === 'boolean') { // This branch should be used by association table relations which trigger instantly and always have to check the // backend state. return !row.__selected; diff --git a/judo-ui-react/src/main/resources/ui-react.yaml b/judo-ui-react/src/main/resources/ui-react.yaml index f2406428..6619f3e9 100644 --- a/judo-ui-react/src/main/resources/ui-react.yaml +++ b/judo-ui-react/src/main/resources/ui-react.yaml @@ -568,6 +568,11 @@ templates: pathExpression: "'src/utilities/helper.ts'" templateName: actor/src/utilities/helper.ts.hbs + - name: actor/src/utilities/helper.test.ts + pathExpression: "'src/utilities/helper.test.ts'" + templateName: actor/src/utilities/helper.test.ts + copy: true + - name: actor/src/utilities/index.tsx pathExpression: "'src/utilities/index.tsx'" templateName: actor/src/utilities/index.tsx.hbs diff --git a/pom.xml b/pom.xml index 9f4b9747..a1bb76c1 100644 --- a/pom.xml +++ b/pom.xml @@ -57,7 +57,7 @@ 1.1.0.20240604_105835_a46ed24f_develop 1.0.0.20231009_184136_321053b3_develop - 1.0.0.20240528_150529_45bc3759_develop + 1.0.0.20240610_201205_f6f4ea61_feature_JNG_5742_eager_relations 3.0.0-M7