diff --git a/judo-ui-react-itest/ActionGroupTest/model/ActionGroupTest-ui.model b/judo-ui-react-itest/ActionGroupTest/model/ActionGroupTest-ui.model index c248a4d0..99fb5bad 100644 --- a/judo-ui-react-itest/ActionGroupTest/model/ActionGroupTest-ui.model +++ b/judo-ui-react-itest/ActionGroupTest/model/ActionGroupTest-ui.model @@ -217,7 +217,7 @@ - + @@ -283,8 +283,8 @@ - + @@ -328,7 +328,7 @@ - + @@ -349,7 +349,7 @@ - + @@ -364,7 +364,7 @@ - + @@ -438,7 +438,7 @@ - + @@ -493,7 +493,7 @@ - + @@ -530,7 +530,7 @@ - + @@ -549,7 +549,7 @@ - + @@ -610,7 +610,7 @@ - + @@ -657,7 +657,7 @@ - + @@ -699,7 +699,7 @@ - + @@ -979,8 +979,8 @@ - + @@ -2124,9 +2124,9 @@ - + - + @@ -2144,7 +2144,7 @@ - + @@ -2310,7 +2310,7 @@ - + @@ -2414,9 +2414,9 @@ - + - + @@ -2434,7 +2434,7 @@ - + @@ -2615,7 +2615,7 @@ - + @@ -2746,7 +2746,7 @@ - + @@ -2834,9 +2834,9 @@ - + - + @@ -2855,7 +2855,7 @@ - + @@ -2936,7 +2936,7 @@ - + @@ -2992,9 +2992,9 @@ - + - + @@ -3013,7 +3013,7 @@ - + @@ -3121,9 +3121,9 @@ - + - + @@ -3199,7 +3199,7 @@ - + @@ -3270,9 +3270,9 @@ - + - + @@ -3374,7 +3374,7 @@ - + @@ -3488,7 +3488,7 @@ - + @@ -3607,7 +3607,7 @@ - + @@ -3734,7 +3734,7 @@ - + @@ -3858,9 +3858,9 @@ - + - + @@ -3899,7 +3899,7 @@ - + @@ -4104,7 +4104,7 @@ - + @@ -4163,7 +4163,7 @@ - + @@ -4246,9 +4246,9 @@ - + - + @@ -4456,7 +4456,7 @@ - + @@ -4542,9 +4542,9 @@ - + - + @@ -4787,7 +4787,7 @@ - + @@ -5053,7 +5053,7 @@ - + @@ -5436,7 +5436,7 @@ - + @@ -5569,7 +5569,7 @@ - + @@ -5637,9 +5637,9 @@ - + - + @@ -5689,7 +5689,7 @@ - + @@ -5961,7 +5961,7 @@ - + @@ -6140,7 +6140,7 @@ - + @@ -6257,7 +6257,7 @@ - + @@ -6385,7 +6385,7 @@ - + @@ -6553,9 +6553,9 @@ - + - + @@ -6868,7 +6868,7 @@ - + @@ -7226,7 +7226,7 @@ - + @@ -7291,9 +7291,9 @@ - + - + diff --git a/judo-ui-react-itest/ActionGroupTestPro/model/ActionGroupTestPro-ui.model b/judo-ui-react-itest/ActionGroupTestPro/model/ActionGroupTestPro-ui.model index c248a4d0..99fb5bad 100644 --- a/judo-ui-react-itest/ActionGroupTestPro/model/ActionGroupTestPro-ui.model +++ b/judo-ui-react-itest/ActionGroupTestPro/model/ActionGroupTestPro-ui.model @@ -217,7 +217,7 @@ - + @@ -283,8 +283,8 @@ - + @@ -328,7 +328,7 @@ - + @@ -349,7 +349,7 @@ - + @@ -364,7 +364,7 @@ - + @@ -438,7 +438,7 @@ - + @@ -493,7 +493,7 @@ - + @@ -530,7 +530,7 @@ - + @@ -549,7 +549,7 @@ - + @@ -610,7 +610,7 @@ - + @@ -657,7 +657,7 @@ - + @@ -699,7 +699,7 @@ - + @@ -979,8 +979,8 @@ - + @@ -2124,9 +2124,9 @@ - + - + @@ -2144,7 +2144,7 @@ - + @@ -2310,7 +2310,7 @@ - + @@ -2414,9 +2414,9 @@ - + - + @@ -2434,7 +2434,7 @@ - + @@ -2615,7 +2615,7 @@ - + @@ -2746,7 +2746,7 @@ - + @@ -2834,9 +2834,9 @@ - + - + @@ -2855,7 +2855,7 @@ - + @@ -2936,7 +2936,7 @@ - + @@ -2992,9 +2992,9 @@ - + - + @@ -3013,7 +3013,7 @@ - + @@ -3121,9 +3121,9 @@ - + - + @@ -3199,7 +3199,7 @@ - + @@ -3270,9 +3270,9 @@ - + - + @@ -3374,7 +3374,7 @@ - + @@ -3488,7 +3488,7 @@ - + @@ -3607,7 +3607,7 @@ - + @@ -3734,7 +3734,7 @@ - + @@ -3858,9 +3858,9 @@ - + - + @@ -3899,7 +3899,7 @@ - + @@ -4104,7 +4104,7 @@ - + @@ -4163,7 +4163,7 @@ - + @@ -4246,9 +4246,9 @@ - + - + @@ -4456,7 +4456,7 @@ - + @@ -4542,9 +4542,9 @@ - + - + @@ -4787,7 +4787,7 @@ - + @@ -5053,7 +5053,7 @@ - + @@ -5436,7 +5436,7 @@ - + @@ -5569,7 +5569,7 @@ - + @@ -5637,9 +5637,9 @@ - + - + @@ -5689,7 +5689,7 @@ - + @@ -5961,7 +5961,7 @@ - + @@ -6140,7 +6140,7 @@ - + @@ -6257,7 +6257,7 @@ - + @@ -6385,7 +6385,7 @@ - + @@ -6553,9 +6553,9 @@ - + - + @@ -6868,7 +6868,7 @@ - + @@ -7226,7 +7226,7 @@ - + @@ -7291,9 +7291,9 @@ - + - + diff --git a/judo-ui-react-itest/CRUDActionsTest/model/CRUDActionsTest-ui.model b/judo-ui-react-itest/CRUDActionsTest/model/CRUDActionsTest-ui.model index 55503235..230f0eb8 100644 --- a/judo-ui-react-itest/CRUDActionsTest/model/CRUDActionsTest-ui.model +++ b/judo-ui-react-itest/CRUDActionsTest/model/CRUDActionsTest-ui.model @@ -9,7 +9,7 @@ - + @@ -40,8 +40,8 @@ - + @@ -102,8 +102,8 @@ - + @@ -131,7 +131,7 @@ - + @@ -248,7 +248,7 @@ - + @@ -300,7 +300,7 @@ - + @@ -586,8 +586,8 @@ - - + + @@ -639,7 +639,7 @@ - + @@ -675,8 +675,8 @@ - - + + @@ -698,9 +698,9 @@ - + - + @@ -713,8 +713,8 @@ - - + + @@ -748,9 +748,9 @@ - + - + @@ -759,8 +759,8 @@ - - + + @@ -802,9 +802,9 @@ - + - + @@ -835,8 +835,8 @@ - - + + @@ -890,8 +890,8 @@ - - + + @@ -913,9 +913,9 @@ - + - + @@ -928,8 +928,8 @@ - - + + @@ -963,9 +963,9 @@ - + - + @@ -974,8 +974,8 @@ - - + + @@ -1017,9 +1017,9 @@ - + - + @@ -1061,8 +1061,8 @@ - - + + @@ -1096,8 +1096,8 @@ - - + + @@ -1149,7 +1149,7 @@ - + @@ -1181,8 +1181,8 @@ - - + + @@ -1190,7 +1190,7 @@ - + @@ -1211,8 +1211,8 @@ - - + + @@ -1224,7 +1224,7 @@ - + @@ -1239,8 +1239,8 @@ - - + + @@ -1252,7 +1252,7 @@ - + @@ -1290,8 +1290,8 @@ - - + + @@ -1341,8 +1341,8 @@ - - + + @@ -1350,7 +1350,7 @@ - + @@ -1371,8 +1371,8 @@ - - + + @@ -1384,7 +1384,7 @@ - + @@ -1399,8 +1399,8 @@ - - + + @@ -1412,7 +1412,7 @@ - + @@ -1461,8 +1461,8 @@ - - + + @@ -1496,8 +1496,8 @@ - - + + @@ -1549,7 +1549,7 @@ - + @@ -1565,8 +1565,8 @@ - - + + @@ -1687,8 +1687,8 @@ - - + + @@ -1740,7 +1740,7 @@ - + @@ -1843,8 +1843,8 @@ - - + + @@ -1896,7 +1896,7 @@ - + @@ -1999,8 +1999,8 @@ - - + + @@ -2052,7 +2052,7 @@ - + @@ -2068,8 +2068,8 @@ - - + + @@ -2187,10 +2187,10 @@ - + - - + + @@ -2199,7 +2199,7 @@ - + @@ -2230,8 +2230,8 @@ - + @@ -2292,8 +2292,8 @@ - + @@ -2321,7 +2321,7 @@ - + @@ -2433,7 +2433,7 @@ - + @@ -2486,7 +2486,7 @@ - + @@ -2776,8 +2776,8 @@ - - + + @@ -2829,7 +2829,7 @@ - + @@ -2865,8 +2865,8 @@ - - + + @@ -2888,9 +2888,9 @@ - + - + @@ -2903,8 +2903,8 @@ - - + + @@ -2938,9 +2938,9 @@ - + - + @@ -2949,8 +2949,8 @@ - - + + @@ -2992,9 +2992,9 @@ - + - + @@ -3025,8 +3025,8 @@ - - + + @@ -3080,8 +3080,8 @@ - - + + @@ -3103,9 +3103,9 @@ - + - + @@ -3118,8 +3118,8 @@ - - + + @@ -3153,9 +3153,9 @@ - + - + @@ -3164,8 +3164,8 @@ - - + + @@ -3207,9 +3207,9 @@ - + - + @@ -3251,8 +3251,8 @@ - - + + @@ -3286,8 +3286,8 @@ - - + + @@ -3339,7 +3339,7 @@ - + @@ -3371,8 +3371,8 @@ - - + + @@ -3380,7 +3380,7 @@ - + @@ -3401,8 +3401,8 @@ - - + + @@ -3414,7 +3414,7 @@ - + @@ -3429,8 +3429,8 @@ - - + + @@ -3442,7 +3442,7 @@ - + @@ -3480,8 +3480,8 @@ - - + + @@ -3531,8 +3531,8 @@ - - + + @@ -3540,7 +3540,7 @@ - + @@ -3561,8 +3561,8 @@ - - + + @@ -3574,7 +3574,7 @@ - + @@ -3589,8 +3589,8 @@ - - + + @@ -3602,7 +3602,7 @@ - + @@ -3651,8 +3651,8 @@ - - + + @@ -3686,8 +3686,8 @@ - - + + @@ -3739,7 +3739,7 @@ - + @@ -3755,8 +3755,8 @@ - - + + @@ -3877,8 +3877,8 @@ - - + + @@ -3930,7 +3930,7 @@ - + @@ -4033,8 +4033,8 @@ - - + + @@ -4086,7 +4086,7 @@ - + @@ -4189,8 +4189,8 @@ - - + + @@ -4242,7 +4242,7 @@ - + @@ -4258,8 +4258,8 @@ - - + + @@ -4377,10 +4377,10 @@ - + - - + + @@ -4389,7 +4389,7 @@ - + @@ -4420,8 +4420,8 @@ - + @@ -4482,8 +4482,8 @@ - + @@ -4511,7 +4511,7 @@ - + @@ -4628,7 +4628,7 @@ - + @@ -4957,8 +4957,8 @@ - - + + @@ -5010,7 +5010,7 @@ - + @@ -5046,8 +5046,8 @@ - - + + @@ -5069,9 +5069,9 @@ - + - + @@ -5084,8 +5084,8 @@ - - + + @@ -5119,9 +5119,9 @@ - + - + @@ -5130,8 +5130,8 @@ - - + + @@ -5173,9 +5173,9 @@ - + - + @@ -5206,8 +5206,8 @@ - - + + @@ -5261,8 +5261,8 @@ - - + + @@ -5284,9 +5284,9 @@ - + - + @@ -5299,8 +5299,8 @@ - - + + @@ -5334,9 +5334,9 @@ - + - + @@ -5345,8 +5345,8 @@ - - + + @@ -5388,9 +5388,9 @@ - + - + @@ -5432,8 +5432,8 @@ - - + + @@ -5467,8 +5467,8 @@ - - + + @@ -5520,7 +5520,7 @@ - + @@ -5552,8 +5552,8 @@ - - + + @@ -5561,7 +5561,7 @@ - + @@ -5582,8 +5582,8 @@ - - + + @@ -5595,7 +5595,7 @@ - + @@ -5610,8 +5610,8 @@ - - + + @@ -5623,7 +5623,7 @@ - + @@ -5661,8 +5661,8 @@ - - + + @@ -5712,8 +5712,8 @@ - - + + @@ -5721,7 +5721,7 @@ - + @@ -5742,8 +5742,8 @@ - - + + @@ -5755,7 +5755,7 @@ - + @@ -5770,8 +5770,8 @@ - - + + @@ -5783,7 +5783,7 @@ - + @@ -5832,8 +5832,8 @@ - - + + @@ -5867,8 +5867,8 @@ - - + + @@ -5920,7 +5920,7 @@ - + @@ -5936,8 +5936,8 @@ - - + + @@ -6058,8 +6058,8 @@ - - + + @@ -6111,7 +6111,7 @@ - + @@ -6214,8 +6214,8 @@ - - + + @@ -6267,7 +6267,7 @@ - + @@ -6370,8 +6370,8 @@ - - + + @@ -6423,7 +6423,7 @@ - + @@ -6439,8 +6439,8 @@ - - + + @@ -6558,8 +6558,8 @@ - + - - + + diff --git a/judo-ui-react-itest/OperationParametersTest/model/OperationParametersTest-ui.model b/judo-ui-react-itest/OperationParametersTest/model/OperationParametersTest-ui.model index 70f17949..93db98a2 100644 --- a/judo-ui-react-itest/OperationParametersTest/model/OperationParametersTest-ui.model +++ b/judo-ui-react-itest/OperationParametersTest/model/OperationParametersTest-ui.model @@ -15,7 +15,7 @@ - + @@ -42,7 +42,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -91,7 +91,7 @@ - + @@ -191,7 +191,7 @@ - + @@ -365,7 +365,7 @@ - + @@ -536,7 +536,7 @@ - + @@ -635,7 +635,7 @@ - + @@ -1084,6 +1084,7 @@ DELETE + @@ -1093,6 +1094,7 @@ + REFRESH @@ -1162,8 +1164,8 @@ - - + + @@ -1197,9 +1199,9 @@ - + - + @@ -1213,7 +1215,7 @@ - + @@ -1239,8 +1241,8 @@ - - + + @@ -1291,7 +1293,7 @@ - + @@ -1316,7 +1318,7 @@ - + @@ -1339,8 +1341,8 @@ - - + + @@ -1374,9 +1376,9 @@ - + - + @@ -1412,8 +1414,8 @@ - - + + @@ -1447,8 +1449,8 @@ - - + + @@ -1487,7 +1489,7 @@ - + @@ -1537,7 +1539,7 @@ - + @@ -1559,8 +1561,8 @@ - - + + @@ -1583,8 +1585,8 @@ - - + + @@ -1665,7 +1667,7 @@ - + @@ -1716,7 +1718,7 @@ - + @@ -1785,9 +1787,9 @@ - - - + + + @@ -1840,7 +1842,7 @@ - + @@ -1856,9 +1858,9 @@ - - - + + + @@ -1946,7 +1948,7 @@ - + @@ -1997,7 +1999,7 @@ - + @@ -2096,8 +2098,8 @@ - - + + @@ -2149,7 +2151,7 @@ - + @@ -2221,8 +2223,8 @@ - - + + @@ -2244,9 +2246,9 @@ - - - + + + @@ -2281,7 +2283,7 @@ - + @@ -2301,11 +2303,11 @@ - - - - - + + + + + @@ -2325,8 +2327,8 @@ - - + + @@ -2348,7 +2350,7 @@ - + @@ -2374,11 +2376,11 @@ - - - - - + + + + + @@ -2433,7 +2435,7 @@ - + @@ -2469,7 +2471,7 @@ - + @@ -2482,8 +2484,8 @@ - - + + @@ -2506,7 +2508,7 @@ - + @@ -2527,7 +2529,7 @@ - + @@ -2548,9 +2550,9 @@ - - - + + + @@ -2586,8 +2588,8 @@ - - + + @@ -2636,9 +2638,9 @@ - - - + + + @@ -2674,7 +2676,7 @@ - + @@ -2708,7 +2710,7 @@ - + @@ -2768,8 +2770,8 @@ - - + + @@ -2821,7 +2823,7 @@ - + @@ -2880,7 +2882,7 @@ - + @@ -2892,8 +2894,8 @@ - - + + @@ -2927,9 +2929,9 @@ - + - + @@ -2989,11 +2991,11 @@ - - - - - + + + + + @@ -3048,7 +3050,7 @@ - + @@ -3064,11 +3066,11 @@ - - - - - + + + + + @@ -3110,7 +3112,7 @@ - + @@ -3122,8 +3124,8 @@ - - + + @@ -3157,9 +3159,9 @@ - + - + @@ -3243,8 +3245,8 @@ - - + + @@ -3267,8 +3269,8 @@ - - + + @@ -3291,8 +3293,8 @@ - - + + @@ -3321,7 +3323,7 @@ - + @@ -3338,8 +3340,8 @@ - - + + @@ -3380,7 +3382,7 @@ - + @@ -3401,9 +3403,9 @@ - - - + + + @@ -3454,7 +3456,7 @@ - + @@ -3513,8 +3515,8 @@ - - + + @@ -3549,7 +3551,7 @@ - + @@ -3583,7 +3585,7 @@ - + @@ -3617,9 +3619,9 @@ - - - + + + @@ -3654,13 +3656,13 @@ - - - - - - - + + + + + + + @@ -3715,7 +3717,7 @@ - + @@ -3759,7 +3761,7 @@ - + @@ -3786,9 +3788,9 @@ - - - + + + @@ -3839,7 +3841,7 @@ - + @@ -3852,7 +3854,7 @@ - + @@ -3869,8 +3871,8 @@ - - + + @@ -3911,8 +3913,8 @@ - - + + @@ -3935,8 +3937,8 @@ - - + + @@ -3959,8 +3961,8 @@ - - + + @@ -4009,8 +4011,8 @@ - - + + @@ -4045,7 +4047,7 @@ - + @@ -4079,7 +4081,7 @@ - + @@ -4113,9 +4115,9 @@ - - - + + + @@ -4165,8 +4167,8 @@ - - + + @@ -4189,7 +4191,7 @@ - + @@ -4202,8 +4204,8 @@ - - + + @@ -4238,7 +4240,7 @@ - + @@ -4303,8 +4305,8 @@ - - + + @@ -4339,7 +4341,7 @@ - + @@ -4372,12 +4374,12 @@ - - - - - - + + + + + + @@ -4427,7 +4429,7 @@ - + @@ -4467,7 +4469,7 @@ - + @@ -4484,7 +4486,7 @@ - + @@ -4497,8 +4499,8 @@ - - + + @@ -4533,8 +4535,8 @@ - - + + @@ -4599,7 +4601,7 @@ - + @@ -4650,7 +4652,7 @@ - + @@ -4734,13 +4736,13 @@ - - - - - - - + + + + + + + @@ -4775,11 +4777,11 @@ - - - - - + + + + + @@ -4811,13 +4813,13 @@ - - - - - - - + + + + + + + @@ -4868,7 +4870,7 @@ - + @@ -4919,7 +4921,7 @@ - + @@ -4982,7 +4984,7 @@ - + @@ -5033,7 +5035,7 @@ - + @@ -5091,8 +5093,8 @@ - - + + @@ -5126,9 +5128,9 @@ - + - + @@ -5140,7 +5142,7 @@ - + @@ -5202,11 +5204,11 @@ - - - - - + + + + + @@ -5260,7 +5262,7 @@ - + @@ -5276,11 +5278,11 @@ - - - - - + + + + + @@ -5346,7 +5348,7 @@ - + @@ -5359,8 +5361,8 @@ - - + + @@ -5394,9 +5396,9 @@ - + - + @@ -5451,7 +5453,7 @@ - + @@ -5502,7 +5504,7 @@ - + @@ -5520,6 +5522,9 @@ + + + @@ -5544,5 +5549,5 @@ - + diff --git a/judo-ui-react/pom.xml b/judo-ui-react/pom.xml index 6e134266..d74d03df 100644 --- a/judo-ui-react/pom.xml +++ b/judo-ui-react/pom.xml @@ -51,6 +51,13 @@ judo-ui-typescript-rest-commons ${judo-ui-typescript-rest-version} + + + org.junit.jupiter + junit-jupiter + 5.5.1 + test + 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 ce50ba36..3c48e301 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 @@ -229,7 +229,11 @@ public static String getDialogOpenParameters(PageDefinition pageDefinition) { result.add("data: " + classDataName(getReferenceClassType(pageDefinition), "")); } if (!pageDefinition.getContainer().isIsSelector()) { - result.add("templateDataOverride?: Partial<" + classDataName(getReferenceClassType(pageDefinition), ">")); + 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") + "[]"); } @@ -267,6 +271,12 @@ public static String getFormOpenParameters(PageDefinition pageDefinition, Action } } else { tokens.add("data"); + if (isRelationOpenCreateActionOnEagerView(pageDefinition, action)) { + if (tokens.size() < 2) { + tokens.add("undefined"); + } + tokens.add("true"); + } } } if (isRelationOpenCreateActionOnForm(pageDefinition, action)) { @@ -386,6 +396,13 @@ public static boolean isRelationOpenCreateActionOnForm(PageDefinition pageDefini && relationType.isIsInlineCreatable(); } + public static boolean isRelationOpenCreateActionOnEagerView(PageDefinition pageDefinition, Action action) { + return pageDefinition.getContainer().isView() + && action.getIsOpenFormAction() + && action.getTargetDataElement() instanceof RelationType relationType + && relationType.isIsInlineCreatable(); + } + public static String postCallOperationActionParams(PageDefinition page, ActionDefinition actionDefinition) { List tokens = new ArrayList<>(); if (actionDefinition.getTargetType() != null) { @@ -477,7 +494,7 @@ public static String actionTargetPageName(Action action) { } public static boolean createNestedValidation(RelationType relationType, PageDefinition pageDefinition) { - return pageDefinition.getContainer().isForm() && relationType != null && (relationType.isIsInlineCreatable() || relationType.getIsCreateValidatable() || (relationType.getIsMemberTypeTransient() && !relationType.getTarget().isIsMapped())); + return relationType != null && !pageDefinition.getContainer().isTable() && (relationType.getIsCreateValidatable() || relationType.getIsUpdateValidatable()); } public static boolean skipNestedValidationBody(PageDefinition pageDefinition) { @@ -485,7 +502,7 @@ public static boolean skipNestedValidationBody(PageDefinition pageDefinition) { } public static boolean isRowActionCRUD(ActionDefinition actionDefinition) { - return actionDefinition.getIsRemoveAction() || actionDefinition.getIsDeleteAction(); + return actionDefinition.getIsRemoveAction() || actionDefinition.getIsRowDeleteAction(); } public static boolean allowRefreshAfterOperationCall(Action action) { @@ -514,4 +531,16 @@ public static Action getRowViewActionForCreateOpenAction(Action action) { } return null; } + + public static boolean isActionParentEagerElement(Action action) { + Table table = getTableParentForActionDefinition(action.getActionDefinition()); + Link link = getLinkParentForActionDefinition(action.getActionDefinition()); + if (table != null) { + return table.isIsEager(); + } + if (link != null) { + return link.isIsEager(); + } + return false; + } } diff --git a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiPageContainerHelper.java b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiPageContainerHelper.java index a7029ed7..0cb22645 100644 --- a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiPageContainerHelper.java +++ b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiPageContainerHelper.java @@ -4,6 +4,7 @@ import hu.blackbelt.judo.meta.ui.*; import hu.blackbelt.judo.meta.ui.data.ClassType; import hu.blackbelt.judo.meta.ui.data.EnumerationType; +import hu.blackbelt.judo.ui.generator.react.mask.MaskEntry; import hu.blackbelt.judo.ui.generator.typescript.rest.commons.UiCommonsHelper; import lombok.extern.java.Log; @@ -149,32 +150,80 @@ public static String containerButtonAvailable(Button button) { tokens.add("editMode"); } else if (button.getActionDefinition().getIsCancelAction() || button.getActionDefinition().getIsUpdateAction()) { tokens.add("editMode"); + } else if (button.getActionDefinition().getIsRefreshAction()) { + // In case of dialogs that are open in draft mode, we should consider checking actions + // because it could be possible that there are no valid use cases where these actions + // should be generated in the first place. + tokens.add("!isDraft"); } else { tokens.add("!editMode"); } return String.join(" && ", tokens); } - public static String getMaskForTable(Table table) { + public static MaskEntry getMaskForTable(Table table, PageDefinition pageDefinition, Integer counter) { + MaskEntry mask = new MaskEntry(Set.of(), null); Set columnAttributeNames = table.getColumns().stream() .map(c -> c.getAttributeType().getName()) .collect(Collectors.toSet()); columnAttributeNames.addAll(table.getAdditionalMaskAttributes().stream().map(NamedElement::getName).collect(Collectors.toSet())); - String tableColumns = String.join(",", columnAttributeNames.stream().sorted().toList()); + mask.addPrimitives(columnAttributeNames); + if (table.isIsEager() && counter < 5) { + // table items can be potentially opened, therefore we need the target's attributes as well + Button openPageButton = table.getRowActionButtonGroup().getButtons().stream() + .filter(b -> b.getActionDefinition().getIsOpenPageAction()) + .findFirst().orElse(null); + if (openPageButton != null) { + OpenPageActionDefinition def = (OpenPageActionDefinition) openPageButton.getActionDefinition(); + Action openAction = pageDefinition.getActions().stream().filter(a -> a.getActionDefinition().equals(def)).findFirst().orElse(null); + if (openAction != null && openAction.getTargetPageDefinition() != null) { + MaskEntry viewMask = getMaskForView(openAction.getTargetPageDefinition(), counter + 1); + mask.addPrimitives(viewMask.getPrimitives()); + mask.addRelations(viewMask.getRelations()); + } + } + } + + return mask; + } - return "{" + tableColumns + "}"; + public static String serializeMaskForTable(Table table, PageDefinition pageDefinition) { + return "{" + getMaskForTable(table, pageDefinition, 0).serialize() + "}"; } - public static String getMaskForLink(Link link) { + public static MaskEntry getMaskForLink(Link link, PageDefinition pageDefinition, Integer counter) { + MaskEntry mask = new MaskEntry(Set.of(), null); Set columnAttributeNames = ((List) link.getColumns()).stream().map(c -> c.getAttributeType().getName()).collect(Collectors.toSet()); columnAttributeNames.addAll(link.getAdditionalMaskAttributes().stream().map(NamedElement::getName).collect(Collectors.toSet())); - String linkColumns = String.join(",", columnAttributeNames.stream().sorted().toList()); + mask.addPrimitives(columnAttributeNames); - return "{" + linkColumns + "}"; + if (link.isIsEager() && counter < 5) { + // link items can be potentially opened, therefore we need the target's attributes as well + Button openPageButton = link.getActionButtonGroup().getButtons().stream() + .filter(b -> b.getActionDefinition().getIsOpenPageAction()) + .findFirst().orElse(null); + if (openPageButton != null) { + OpenPageActionDefinition def = (OpenPageActionDefinition) openPageButton.getActionDefinition(); + Action openAction = pageDefinition.getActions().stream().filter(a -> a.getActionDefinition().equals(def)).findFirst().orElse(null); + if (openAction != null && openAction.getTargetPageDefinition() != null) { + MaskEntry viewMask = getMaskForView(openAction.getTargetPageDefinition(), counter + 1); + mask.addPrimitives(viewMask.getPrimitives()); + mask.addRelations(viewMask.getRelations()); + } + } + } + + return mask; + } + + public static String serializeMaskForLink(Link link, PageDefinition pageDefinition) { + return "{" + getMaskForLink(link, pageDefinition, 0).serialize() + "}"; } - public static String getMaskForView(PageContainer container) { - Set mask = new LinkedHashSet<>(); + public static MaskEntry getMaskForView(PageDefinition pageDefinition, Integer counter) { + MaskEntry mask = new MaskEntry(Set.of(), null); + PageContainer container = pageDefinition.getContainer(); + Set tokens = new LinkedHashSet<>(); Set inputs = new HashSet<>(); collectVisualElementsMatchingCondition(container, (VisualElement element) -> element instanceof AttributeBased, inputs); @@ -193,17 +242,30 @@ public static String getMaskForView(PageContainer container) { attributeNames.add(container.getTitleAttribute().getName()); } - mask.addAll(attributeNames.stream().sorted().toList()); + mask.addPrimitives(attributeNames); for (Table table: ((List) container.getTables()).stream().filter(t -> t.getRelationType().getIsRelationKindComposition() || t.getRelationType().getIsRelationKindAggregation()).toList()) { - mask.add(table.getDataElement().getName() + getMaskForTable(table)); + MaskEntry tableMask = getMaskForTable(table, pageDefinition, counter + 1); + tableMask.setRelationName(table.getDataElement().getName()); + mask.addRelations(tableMask); } for (Link link: ((List) container.getLinks()).stream().filter(t -> t.getRelationType().getIsRelationKindComposition() || t.getRelationType().getIsRelationKindAggregation()).toList()) { - mask.add(link.getDataElement().getName() + getMaskForLink(link)); + MaskEntry linkMask = getMaskForLink(link, pageDefinition, counter + 1); + linkMask.setRelationName(link.getDataElement().getName()); + mask.addRelations(linkMask); } - return "{" + String.join(",", mask) + "}"; + return mask; + } + + public static String serializeMaskForView(PageDefinition pageDefinition) { + try { + return "{" + getMaskForView(pageDefinition, 0).serialize() + "}"; + } catch (StackOverflowError e) { + // keeping this for debugging purposes + throw e; + } } public static boolean containerHasDateInput(PageContainer container) { @@ -222,6 +284,10 @@ public static boolean containerHasTable(PageContainer container) { return !container.getTables().isEmpty(); } + public static Table getFirstTableForContainer(PageContainer container) { + return (Table) container.getTables().stream().findFirst().orElse(null); + } + public static boolean containerHasNumericInput(PageContainer container) { return !collectElementsOfType(container, new ArrayList<>(), NumericInput.class).isEmpty(); } 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 48bd33fa..61ff176a 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 @@ -457,4 +457,15 @@ public static String calculateNavigationRoute(PageDefinition pageDefinition) { public static boolean hasExportAction(PageDefinition pageDefinition) { return pageDefinition.getActions().stream().anyMatch(page -> page.getIsExportAction()); } + + public static boolean isDialogValidationSupported(PageDefinition pageDefinition) { + if (pageDefinition.getRelationType() != null) { + if (pageDefinition.getContainer().isView() && pageDefinition.getRelationType().getIsUpdateValidatable()) { + return true; + } else if (pageDefinition.getContainer().isForm() && pageDefinition.getRelationType().getIsCreateValidatable()) { + return true; + } + } + return false; + } } diff --git a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiWidgetHelper.java b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiWidgetHelper.java index 2bc3c7c1..aada7c21 100644 --- a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiWidgetHelper.java +++ b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiWidgetHelper.java @@ -377,20 +377,21 @@ public static String tableRowButtonDisabledConditions(Button button, Table table if (container.isView()) { result += "(isFormUpdateable ? !isFormUpdateable() : false) || "; } - } else if (button.getActionDefinition().getIsDeleteAction()) { + } else if (button.getActionDefinition().getIsRowDeleteAction()) { if (!container.isTable()) { - result += "editMode || "; + if (!table.isIsEager()) { + result += "editMode || "; + } if (table.getEnabledBy() != null) { result += "(ownerData ? !ownerData." + table.getEnabledBy().getName() + " : false) || "; } } + result += "(typeof row.__deleteable === 'boolean' && !row.__deleteable) || "; if (container.isView()) { result += "(isFormUpdateable ? !isFormUpdateable() : false) || "; } - - result += "!row.__deleteable || "; } else if (!container.isTable()) { result += "editMode || "; } @@ -444,5 +445,6 @@ public static boolean isLinkAssociation(Link link) { public static boolean displayTableHeading(Table table, PageContainer container) { return elementHasIconOrLabel(table) && !container.isIsSelector() && !container.isTable(); + } } diff --git a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/mask/MaskEntry.java b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/mask/MaskEntry.java new file mode 100644 index 00000000..15deb521 --- /dev/null +++ b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/mask/MaskEntry.java @@ -0,0 +1,73 @@ +package hu.blackbelt.judo.ui.generator.react.mask; + +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; + +public class MaskEntry { + private final Set primitives = new LinkedHashSet<>(); + private String relationName; + private final Set relations = new LinkedHashSet<>(); + + public MaskEntry(Set primitives, String relationName) { + if (primitives != null && !primitives.isEmpty()) { + this.primitives.addAll(primitives); + } + if (relationName != null) { + this.relationName = relationName; + } + } + + public String getRelationName() { + return relationName; + } + + public MaskEntry setRelationName(String rn) { + relationName = rn; + return this; + } + + public Set getPrimitives() { + return primitives; + } + + public Set getRelations() { + return relations; + } + + public MaskEntry addPrimitives(String... p) { + primitives.addAll(Set.of(p)); + return this; + } + + public MaskEntry addPrimitives(Set ps) { + primitives.addAll(ps); + return this; + } + + public MaskEntry addRelations(MaskEntry... r) { + relations.addAll(Set.of(r)); + return this; + } + + public MaskEntry addRelations(Set rs) { + relations.addAll(rs); + return this; + } + + public String serialize() { + String p = primitives.stream() + .sorted() + .collect(Collectors.joining(",")); + p += (!p.isEmpty() && !relations.isEmpty()) ? "," : ""; + String r = relations.stream() + .sorted(Comparator.comparing(MaskEntry::getRelationName)) + .map(MaskEntry::serialize) + .collect(Collectors.joining(",")); + if (relationName != null) { + return relationName + "{" + p + r + "}"; + } + return p + r; + } +} diff --git a/judo-ui-react/src/main/resources/actor/src/components/table/EagerTable.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/components/table/EagerTable.tsx.hbs index 7e3622c5..aa583ec5 100644 --- a/judo-ui-react/src/main/resources/actor/src/components/table/EagerTable.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/components/table/EagerTable.tsx.hbs @@ -64,7 +64,6 @@ interface EagerTableProps[]; tableRowActions: TableRowAction[]; tableFilterOptions: FilterOption[]; - mask: string; isOwnerLoading?: boolean; validationError?: string; actions: any; @@ -80,7 +79,7 @@ interface EagerTableProps[]; additionalToolbarButtons?: (...args: any[]) => Record; tableHasSelectorColumn?: boolean; - maskAction?: () => string; + maskAction: () => string; ownerData?: any; checkboxSelection?: boolean; isFormUpdateable?: () => boolean; @@ -101,7 +100,6 @@ export function EagerTable>({ - _mask: mask, + _mask: maskAction(), _seek: { limit: rowsPerPage + 1, }, 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 fcb36fe9..fa5e615a 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 @@ -68,7 +68,6 @@ interface LazyTableProps[]; tableRowActions: TableRowAction[]; tableFilterOptions: FilterOption[]; - mask: string; isOwnerLoading?: boolean; validationError?: string; actions: any; @@ -83,7 +82,7 @@ interface LazyTableProps[]; containerHasTable?: boolean; hasRefreshAction?: boolean; - maskAction?: () => string; + maskAction: () => string; fetch: any; refreshCounter: number; ownerData?: any; @@ -112,7 +111,6 @@ export function LazyTable>({ - _mask: mask, + _mask: maskAction(), _seek: { limit: rowsPerPage + 1, }, 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 9e1ff928..21638fee 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 @@ -54,7 +54,7 @@ export function {{ componentName link }}(props: {{ componentName link }}Props) { visible: (value: any) => exists(value) && !!actions.{{ simpleActionDefinitionName actionDefinition }}, disabled: editMode && !({{ boolValue link.dataElement.isInlineCreatable }} && !isDraft), onClick: async (value: {{ classDataName link.dataElement.target 'Stored' }} | null) => { - await actions.{{ simpleActionDefinitionName actionDefinition }}!(value!, {{# if container.form }}true{{ else }}false{{/ if }}); + await actions.{{ simpleActionDefinitionName actionDefinition }}!(value!, {{# if link.isEager }}true{{ else }}false{{/ if }}); }, title: t('judo.component.SingleRelationInput.navigate', { defaultValue: 'Navigate to element' }) as string, {{/ if }} @@ -66,7 +66,7 @@ export function {{ componentName link }}(props: {{ componentName link }}Props) { }, title: t('judo.component.SingleRelationInput.create', { defaultValue: 'Create' }) as string, {{/ if }} - {{# if button.actionDefinition.isDeleteAction }} + {{# if button.actionDefinition.isRowDeleteAction }} dropDownButtonIsVisible: (value: any) => exists(value) && !readOnly && !isRequired && value.__deleteable && !!actions.{{ simpleActionDefinitionName actionDefinition }}, disabled: disabled || editMode, onClick: async (value: {{ classDataName link.dataElement.target 'Stored' }} | null) => { @@ -101,7 +101,7 @@ export function {{ componentName link }}(props: {{ componentName link }}Props) { if (actions.{{ link.dataElement.name }}RefreshAction) { try { const queryCustomizer: {{ classDataName link.dataElement.target 'QueryCustomizer' }} = { - _mask: '{{ getMaskForLink link }}', + _mask: actions.get{{ firstToUpper link.relationType.name }}Mask!(), }; const { data: result } = await actions.{{ link.dataElement.name }}RefreshAction!(queryCustomizer); return result || null; // result can be empty string for no data... @@ -138,7 +138,7 @@ export function {{ componentName link }}(props: {{ componentName link }}Props) { { operator: StringOperation.like, value: searchText } ], } : {}), - _mask: '{{ getMaskForLink link }}', + _mask: actions.get{{ firstToUpper link.relationType.name }}Mask!(), _orderBy: [ {{# with (getSortColumnForLink link) as |sortColumn| }} { attribute: '{{ sortColumn.attributeType.name }}', descending: {{ isSortDirectionDescending sortColumn }} }, 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 502afa0a..a4a9870b 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 @@ -11,6 +11,7 @@ export interface {{ componentName link }}ActionDefinitions { {{/ if }} is{{ firstToUpper link.relationType.name }}Required?: ({{{ inputModifierParams container false }}}) => boolean; is{{ firstToUpper link.relationType.name }}Disabled?: ({{{ inputModifierParams container true }}}) => boolean; + get{{ firstToUpper link.relationType.name }}Mask?: () => string; } export interface {{ componentName link }}Props { 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 e7dc663d..7eee86e2 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 @@ -226,7 +226,6 @@ export function {{ componentName table }}(props: {{ componentName table }}Props) tableColumns={columns} tableRowActions={rowActions} tableFilterOptions={filterOptions} - mask={ '{{ getMaskForTable table }}' } isOwnerLoading={isOwnerLoading} validationError={validationError} actions={actions} @@ -247,7 +246,7 @@ export function {{ componentName table }}(props: {{ componentName table }}Props) {{/ if }} {{# each table.rowActionDefinitions as |actionDefinition| }} {{# if actionDefinition.isOpenPageAction }} - onRowClick={ actions.{{ simpleActionDefinitionName actionDefinition }} ? async (params: GridRowParams<{{ classDataName (getReferenceClassType table) 'Stored' }}>) => await actions.{{ simpleActionDefinitionName actionDefinition }}!(params.row, {{# if container.form }}true{{ else }}false{{/ if }}) : undefined } + onRowClick={ actions.{{ simpleActionDefinitionName actionDefinition }} ? async (params: GridRowParams<{{ classDataName (getReferenceClassType table) 'Stored' }}>) => await actions.{{ simpleActionDefinitionName actionDefinition }}!(params.row, {{# if table.isEager }}true{{ else }}false{{/ if }}) : undefined } {{/ if }} {{/ each }} sidekickComponentFilter={sidekickComponentFilter} @@ -267,9 +266,7 @@ export function {{ componentName table }}(props: {{ componentName table }}Props) ownerData={ownerData} editMode={editMode} {{/ unless }} - {{# if (getRefreshActionDefinitionForTable table) }} - maskAction={actions.get{{ firstToUpper table.relationName }}Mask} - {{/ if }} + maskAction={actions.get{{ firstToUpper table.relationName }}Mask!} enabledByName='{{ table.enabledBy.name }}' dataElement={ ownerData?.{{ table.dataElement.name }} } relationName='{{ table.relationName }}' @@ -292,7 +289,6 @@ export function {{ componentName table }}(props: {{ componentName table }}Props) tableColumns={columns} tableRowActions={rowActions} tableFilterOptions={filterOptions} - mask={ '{{ getMaskForTable table }}' } isOwnerLoading={isOwnerLoading} validationError={validationError} actions={actions} @@ -326,9 +322,7 @@ export function {{ componentName table }}(props: {{ componentName table }}Props) {{# if getRefreshActionDefinitionForTable table }} hasRefreshAction={ true } {{/ if }} - {{# if (getRefreshActionDefinitionForTable table) }} - maskAction={actions.get{{ firstToUpper table.relationName }}Mask} - {{/ if }} + maskAction={actions.get{{ firstToUpper table.relationName }}Mask!} {{# if container.isSelector }} {{# with (getRangeActionDefinitionForTable table) as |actionDefinition| }} fetch={ actions.{{ simpleActionDefinitionName actionDefinition }} } 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 ffdd096d..0192c9e2 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 @@ -10,13 +10,13 @@ import type { Filter, FilterOption } from '~/components-api'; import type { DialogResult } from '~/utilities'; export interface {{ componentName table }}ActionDefinitions { + get{{ firstToUpper table.relationName }}Mask?: () => string; {{# each table.tableActionDefinitions as |actionDefinition| }} {{# if actionDefinition.isFilterAction }} {{ simpleActionDefinitionName actionDefinition }}?: (id: string, filterOptions: FilterOption[], model?: GridFilterModel, filters?: Filter[]) => Promise<{ model?: GridFilterModel; filters?: Filter[] }>; {{ else }} {{# if actionDefinition.isRefreshAction }} {{ simpleActionDefinitionName actionDefinition }}?: (queryCustomizer: {{ classDataName (getReferenceClassType table) 'QueryCustomizer' }}) => Promise>; - get{{ firstToUpper table.relationName }}Mask?: () => string; {{ else }} {{# if actionDefinition.isExportAction }} {{ simpleActionDefinitionName actionDefinition }}?: (queryCustomizer: {{ classDataName (getReferenceClassType table) 'QueryCustomizer' }}) => Promise; 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 129a499b..cd3788dd 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 @@ -79,11 +79,11 @@ export default function {{ containerComponentName container }}Dialog({{# unless isDraft {{/ unless }} } = props; - {{# unless container.table }} + {{# if container.view }} const queryCustomizer: {{ classDataName container.dataElement 'QueryCustomizer' }} = { - _mask: {{# unless container.form }}actions.getMask ? actions.getMask!() : {{/ unless }}'{{ getMaskForView container }}', + _mask: actions.getMask?.(), }; - {{/ unless }} + {{/ if }} {{# if (containerHasCreateAction container) }} const handleCreateDropdownToggle = useCallback(() => { @@ -196,11 +196,19 @@ export default function {{ containerComponentName container }}Dialog({{# unless : {{# unless button.actionDefinition.autoOpenAfterCreate }}t('{{ getTranslationKeyForVisualElement button }}', { defaultValue: '{{ button.label }}' }){{ else }}t('judo.pages.create-and-navigate', { defaultValue: 'Create and open' }){{/ unless }} } {{ else }} - {{# and button.actionDefinition.isUpdateAction button.actionDefinition.autoCloseOnSave }} - {t('judo.pages.save-and-close', { defaultValue: 'Save and close' })} + {{# if button.actionDefinition.isUpdateAction }} + { + isDraft + ? t('judo.dialogs.draft.submit', { defaultValue: 'Ok' }) + : t('{{ getTranslationKeyForVisualElement button }}', { defaultValue: '{{ button.label }}' }) + } {{ else }} - {t('{{ getTranslationKeyForVisualElement button }}', { defaultValue: '{{ button.label }}' })} - {{/ and }} + {{# and button.actionDefinition.isUpdateAction button.actionDefinition.autoCloseOnSave }} + {t('judo.pages.save-and-close', { defaultValue: 'Save and close' })} + {{ else }} + {t('{{ getTranslationKeyForVisualElement button }}', { defaultValue: '{{ button.label }}' })} + {{/ and }} + {{/ if }} {{/ if }} {{# if button.actionDefinition.isCreateAction }} 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 70d0653d..130c7741 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 @@ -38,11 +38,12 @@ export default function {{ containerComponentName container }}Page ({{# unless ( const { t } = useTranslation(); const { navigate, back } = useJudoNavigation(); const { actions, isLoading, editMode, refreshCounter{{# unless container.table }}, data, isFormUpdateable, isFormDeleteable, storeDiff, validation, setValidation, submit{{/ unless }} } = props; - {{# unless container.table }} + 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' }} = { - _mask: {{# unless container.form }}actions.getMask ? actions.getMask!() : {{/ unless }}'{{ getMaskForView container }}', + _mask: actions.getMask?.(), }; - {{/ unless }} + {{/ if }} {{/ unless }} return ( 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 9d16e163..cf2fbf6f 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 @@ -43,9 +43,9 @@ {{# each (getTextInputsWithTypeAhead container) as |ve| }} get{{ firstToUpper ve.attributeType.name }}Options?: (searchText: string, data: {{ classDataName container.dataElement '' }}, editMode: boolean, validation: Map) => Promise; {{/ each }} - {{# if container.view }} + {{# unless container.form }} getMask?: () => string; - {{/ if }} + {{/ unless }} } export interface {{ containerComponentName container }}PageActions extends {{ pageContainerActionDefinitionTypeName container }} { diff --git a/judo-ui-react/src/main/resources/actor/src/dialogs/customization.ts.hbs b/judo-ui-react/src/main/resources/actor/src/dialogs/customization.ts.hbs index 321fff2f..4cb7e93a 100644 --- a/judo-ui-react/src/main/resources/actor/src/dialogs/customization.ts.hbs +++ b/judo-ui-react/src/main/resources/actor/src/dialogs/customization.ts.hbs @@ -7,6 +7,7 @@ import { {{ restParamName imp }} } from '~/services/data-api/model/{{ restParamName imp }}'; {{/ each }} import type { {{ containerComponentName page.container }}DialogActionsExtended, {{ pageName page }}Props } from './types'; + import type { DialogResultReason } from '~/utilities'; export const {{ camelCaseNameToInterfaceKey (pageName page) }}_ACTIONS_HOOK_INTERFACE_KEY = '{{ camelCaseNameToInterfaceKey (pageName page) }}_ACTIONS_HOOK'; export type {{ containerComponentName page.container }}ActionsHook = ( @@ -24,9 +25,9 @@ {{/ if }} submit: () => Promise, {{# if (pageHasOutputTarget page) }} - onSubmit: (result?: {{ classDataName (getPageOutputTarget page) 'Stored' }}, isDraft?: boolean, openCreated?: boolean) => Promise, + onSubmit: (result?: {{ classDataName (getPageOutputTarget page) 'Stored' }}, reason?: DialogResultReason, openCreated?: boolean) => Promise, {{ else }} - onSubmit: (result?: {{ dialogDataType page }}{{# if page.container.table }}[]{{/ if }}, isDraft?: boolean, openCreated?: boolean) => Promise, + onSubmit: (result?: {{ dialogDataType page }}{{# if page.container.table }}[]{{/ if }}, reason?: DialogResultReason, openCreated?: boolean) => Promise, {{/ if }} ) => {{ containerComponentName page.container }}DialogActionsExtended; {{ else }} 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 bd428c94..92f18bb7 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 @@ -54,13 +54,13 @@ export const use{{ pageName page }} = (): ({{{ getDialogOpenParameters page }}}) result: 'close', }); }} - onSubmit={async (result, isDraft, openCreated) => { + onSubmit={async (dataParam, variant = 'submit', openCreated = false) => { await closeDialog(); resolve({ - result: isDraft ? 'submit-draft' : 'submit', + result: variant, openCreated, {{# if (dialogHasResult page) }} - data: result, + data: dataParam, {{/ if }} }); }} 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 72861cee..47c41a30 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,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 }}useCallback, useRef, {{/ unless }}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'; @@ -30,6 +30,10 @@ import { {{# unless page.container.table }}useCallback, useEffect, useRef, {{/ u useErrorHandler, isErrorNestedValidationError, cleanUpPayload, + decoratePayloadWithMetaFromData, + {{# if page.container.view }} + cloneDeep, + {{/ if }} } from '~/utilities'; import type { DialogResult, @@ -113,6 +117,20 @@ export default function {{ pageName page }}(props: {{ pageName page }}Props) { // 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 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]); + {{/ if }} + // Callback section const storeDiff: (attributeName: keyof {{ classDataName (getReferenceClassType page) '' }}, value: any) => void = useCallback((attributeName: keyof {{ classDataName (getReferenceClassType page) '' }}, value: any) => { {{# if isDebugPrint }}// include: actor/src/fragments/page/store-diff-body.hbs{{/ if }} @@ -122,7 +140,7 @@ export default function {{ pageName page }}(props: {{ pageName page }}Props) { {{# if page.container.form }} return true; {{ else }} - return {{ boolValue (isPageUpdateable page) }} && typeof data?.__updateable === 'boolean' && data?.__updateable; + return isDraft || ({{ boolValue (isPageUpdateable page) }} && typeof data?.__updateable === 'boolean' && data?.__updateable); {{/ if }} }, [data]); const isFormDeleteable = useCallback(() => { @@ -135,11 +153,31 @@ export default function {{ pageName page }}(props: {{ pageName page }}Props) { {{# if page.container.view }} const getPageQueryCustomizer: () => {{ classDataName (getReferenceClassType page) 'QueryCustomizer' }} = () => ({ - _mask: actions.getMask ? actions.getMask!() : '{{ getMaskForView page.container }}', + _mask: actions.getMask ? actions.getMask!() : '{{ serializeMaskForView page }}', }); {{/ if }} {{/ unless }} + // Masks + {{# if page.container.table }} + {{# with (getFirstTableForContainer page.container) as |table| }} + const getMask: () => string = () => { + return '{{ serializeMaskForTable table page }}'; + }; + {{/ with }} + {{/ if }} + {{# if page.container.view }} + const getMask = () => '{{ serializeMaskForView page }}'; + {{/ if }} + {{# unless page.container.table }} + {{# each page.container.links as |link| }} + const get{{ firstToUpper link.relationType.name }}Mask = () => '{{ serializeMaskForLink link page }}'; + {{/ each }} + {{# each page.container.tables as |table| }} + const get{{ firstToUpper table.relationName }}Mask = () => '{{ serializeMaskForTable table page }}'; + {{/ each }} + {{/ unless }} + // Private actions const submit = async () => { {{# with (getCreateActionForPage page) as |action| }} @@ -152,70 +190,72 @@ export default function {{ pageName page }}(props: {{ pageName page }}Props) { await {{ simpleActionDefinitionName action.actionDefinition }}(); {{/ with }} }; - const refresh = async () => { - {{# if page.container.table }} - setRefreshCounter((prev) => prev + 1); - {{/ if }} - {{# if page.container.view }} - {{# with (getRefreshActionDefinitionForContainer page.container) as |refreshActionDefinition| }} - if (actions.{{ simpleActionDefinitionName refreshActionDefinition }}) { - await actions.{{ simpleActionDefinitionName refreshActionDefinition }}!(processQueryCustomizer(getPageQueryCustomizer())); + const refresh = async (forced = false) => { + if (!isDraft) { + {{# if page.container.table }} + setRefreshCounter((prev) => prev + 1); + {{/ if }} + {{# if page.container.view }} + if (!editMode || forced) { + {{# with (getRefreshActionDefinitionForContainer page.container) as |refreshActionDefinition| }} + if (actions.{{ simpleActionDefinitionName refreshActionDefinition }}) { + await actions.{{ simpleActionDefinitionName refreshActionDefinition }}!(processQueryCustomizer(getPageQueryCustomizer())); + } + {{/ with }} } - {{/ with }} - {{/ if }} + {{/ if }} + } }; // Validation const validate: (data: {{ dialogBareDataType page }}) => Promise = async (data) => { - {{# if page.dataElement.isCreateValidatable }} - try { + {{# if (isDialogValidationSupported page) }} if (ownerValidation) { await ownerValidation(data); } else { - await {{ getServiceImplForPage page }}.validateCreate({{# unless page.dataElement.isAccess }}ownerData, {{/ unless }}data); - } - } catch (error: any) { - if (isDraft && isErrorNestedValidationError(error, '{{ page.dataElement.name }}')) { - throw error; + await {{ getServiceImplForPage page }}.validate{{# if page.container.view }}Update{{ else }}Create{{/ if }}({{# unless page.dataElement.isAccess }}ownerData, {{/ unless }}data); } - } {{/ if }} }; {{# each page.container.links as |link| }} - {{# if (createNestedValidation link.relationType page) }} - const validate{{ firstToUpper link.relationType.name }} = async (linkData: {{ classDataName link.relationType.target '' }}): Promise => { - {{# if link.relationType.isCreateValidatable }} - await {{ getServiceImplForPage page }}.validateCreate({{# unless page.dataElement.isAccess }}ownerData, {{/ unless }}{ - ...cleanUpPayload({ - ...payloadDiff.current, - {{ link.relationType.name }}: { - ...linkData, - } as any - }), - } as {{ dialogBareDataType page }}); - {{ else }} - // not yet supported - {{/ if }} - }; - {{/ if }} + {{# 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| }} - {{# if (createNestedValidation table.relationType page) }} - const validate{{ firstToUpper table.relationType.name }} = async (tableData: {{ classDataName table.relationType.target '' }}): Promise => { - {{# if table.relationType.isCreateValidatable }} - await {{ getServiceImplForPage page }}.validateCreate({{# unless page.dataElement.isAccess }}ownerData, {{/ unless }}{ - ...cleanUpPayload({ - ...payloadDiff.current, - {{ table.relationType.name }}: [ - { ...tableData } as any - ], - }), - } as {{ dialogBareDataType page }}); - {{ else }} - // not yet supported - {{/ if }} - }; - {{/ if }} + {{# 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 }} // Pandino Action overrides @@ -249,6 +289,17 @@ export default function {{ pageName page }}(props: {{ pageName page }}Props) { {{# each page.actions as |action| }} {{ simpleActionDefinitionName action.actionDefinition }}, {{/ each }} + {{# unless page.container.form }} + getMask, + {{/ unless }} + {{# unless page.container.table }} + {{# each page.container.links as |link| }} + get{{ firstToUpper link.relationType.name }}Mask, + {{/ each }} + {{# each page.container.tables as |table| }} + get{{ firstToUpper table.relationName }}Mask, + {{/ each }} + {{/ unless }} ...(customActions ?? {}), }; @@ -286,22 +337,22 @@ export default function {{ pageName page }}(props: {{ pageName page }}Props) { }; // Effect section - {{# if (pageShouldInitialize page) }} - useEffect(() => { - actions.{{ simpleActionDefinitionName page.container.onInit }}!({{# if page.container.view }}getPageQueryCustomizer(){{/ if }}); - }, []); - {{ else }} - {{# if page.container.form }} - useEffect(() => { - if (templateDataOverride) { - setData((prevData) => ({ ...prevData, ...templateDataOverride })); - payloadDiff.current = { - ...(templateDataOverride as Record), - }; - } - }, []); - {{/ if}} - {{/ if }} + useEffect(() => { + {{# if (pageShouldInitialize page) }} + if (!isDraft) { + actions.{{ simpleActionDefinitionName page.container.onInit }}!({{# if page.container.view }}getPageQueryCustomizer(){{/ 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 }} + }, []); {{/ unless }} return ( 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 de2a65c5..b9797571 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 @@ -7,6 +7,7 @@ import { {{ restParamName imp }} } from '~/services/data-api/model/{{ restParamName imp }}'; {{/ each }} import type { {{ containerComponentName page.container }}DialogActions } from '~/containers/{{ containerPath page.container }}/types'; + import type { DialogResultReason } from '~/utilities'; export type {{ containerComponentName page.container }}DialogActionsExtended = {{ containerComponentName page.container }}DialogActions & { {{# each (getAllCallOperationActions page) as |action| }} @@ -27,14 +28,14 @@ ownerData: any; {{# and (isPageDataElementUnmappedSingle page) page.container.view }}data: {{ classDataName (getReferenceClassType page) '' }};{{/ and }} {{# if page.container.isRelationSelector }}alreadySelected: {{ classDataName (getReferenceClassType page) 'Stored' }}[];{{/ if }} - {{# unless page.container.isSelector }}templateDataOverride?: Partial<{{ classDataName (getReferenceClassType page) '' }}>;{{/ unless }} + {{# 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; onClose: () => Promise; {{# if (pageHasOutputTarget page) }} - onSubmit: (result?: {{ classDataName (getPageOutputTarget page) 'Stored' }}, isDraft?: boolean, openCreated?: boolean) => Promise; + onSubmit: (result?: {{ classDataName (getPageOutputTarget page) 'Stored' }}, reason?: DialogResultReason, openCreated?: boolean) => Promise; {{ else }} - onSubmit: (result?: {{ dialogDataType page }}{{# if page.container.table }}[]{{/ if }}, isDraft?: boolean, openCreated?: boolean) => Promise; + onSubmit: (result?: {{ dialogDataType page }}{{# if page.container.table }}[]{{/ if }}, reason?: DialogResultReason, openCreated?: boolean) => Promise; {{/ if }} {{# unless page.container.table}} maskRequest?: string; diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/AutocompleteSetAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/AutocompleteSetAction.fragment.hbs index 9de40fe1..9e42cd7c 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/AutocompleteSetAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/AutocompleteSetAction.fragment.hbs @@ -5,13 +5,7 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async (selected storeDiff('{{ link.dataElement.name }}', selected); {{ else }} await {{ getServiceImplForPage page }}.set{{ firstToUpper link.dataElement.name }}(data, selected); - {{# if page.container.view }} - if (!editMode) { - {{# with (getRefreshActionDefinitionForContainer page.container) as |actionDefinition| }} - await actions.{{ simpleActionDefinitionName actionDefinition }}!(processQueryCustomizer(getPageQueryCustomizer())); - {{/ with }} - } - {{/ if }} + await refresh(); {{/ if }} } catch (error) { handleError(error); diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/BulkDeleteAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/BulkDeleteAction.fragment.hbs index 0e13715b..fadbaa9f 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/BulkDeleteAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/BulkDeleteAction.fragment.hbs @@ -1,50 +1,59 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async (selectedRows: {{ classDataName action.actionDefinition.bulkOf.targetType 'Stored' }}[]): Promise>> => { {{# with (getTableParentForActionDefinition action.actionDefinition) as |table| }} return new Promise((resolve) => { - openCRUDDialog<{{ classDataName (getReferenceClassType table) 'Stored' }}>({ - dialogTitle: t('{{ getTranslationKeyForVisualElement (translationElementForBulkAction action) }}', { defaultValue: '{{ defaultLabelForBulkAction action }}' }), - {{# with (getFirstTitleColumnForTable table) as |column| }} - itemTitleFn: (item) => item.{{ column.attributeType.name }}!, - {{ else }} - itemTitleFn: (item) => t('judo.placeholder', { defaultValue: 'placeholder' }) as string, - {{/ with }} - selectedItems: selectedRows, - action: async (item, successHandler: () => void, errorHandler: (error: any) => void) => { - try { - if (actions.{{ simpleActionDefinitionName actionDefinition.bulkOf }}) { - await actions.{{ simpleActionDefinitionName actionDefinition.bulkOf }}!(item, true); + {{# if table.isEager }} + const selectedIds = selectedRows.map(r => r.__identifier); + const newList = (data?.{{ table.dataElement.name }} ?? []).filter((c: any) => !selectedIds.includes(c.__identifier)); + storeDiff('{{ table.dataElement.name }}', newList); + resolve({ + result: 'delete', + data: [], + }); + {{ else }} + openCRUDDialog<{{ classDataName (getReferenceClassType table) 'Stored' }}>({ + dialogTitle: t('{{ getTranslationKeyForVisualElement (translationElementForBulkAction action) }}', { defaultValue: '{{ defaultLabelForBulkAction action }}' }), + {{# with (getFirstTitleColumnForTable table) as |column| }} + itemTitleFn: (item) => item.{{ column.attributeType.name }}!, + {{ else }} + itemTitleFn: (item) => t('judo.placeholder', { defaultValue: 'placeholder' }) as string, + {{/ with }} + selectedItems: selectedRows, + action: async (item, successHandler: () => void, errorHandler: (error: any) => void) => { + try { + if (actions.{{ simpleActionDefinitionName actionDefinition.bulkOf }}) { + await actions.{{ simpleActionDefinitionName actionDefinition.bulkOf }}!(item, true); + } + successHandler(); + } catch (error) { + errorHandler(error); } - successHandler(); - } catch (error) { - errorHandler(error); - } - }, - autoCloseOnSuccess: true - , - onClose: async (needsRefresh) => { - if (needsRefresh) { - {{# if page.container.table }} - setRefreshCounter((prev) => prev + 1); - {{/ if }} - {{# if page.container.view }} - {{# with (getRefreshActionDefinitionForContainer page.container) as |refreshActionDefinition| }} - if (actions.{{ simpleActionDefinitionName refreshActionDefinition }}) { - await actions.{{ simpleActionDefinitionName refreshActionDefinition }}!(processQueryCustomizer(getPageQueryCustomizer())); - } - {{/ with }} - {{/ if }} - resolve({ - result: 'submit', - data: [], - }); - } else { - resolve({ - result: 'close', - data: [], - }); - } - }, - }); + }, + autoCloseOnSuccess: true, + onClose: async (needsRefresh) => { + if (needsRefresh) { + {{# if page.container.table }} + setRefreshCounter((prev) => prev + 1); + {{/ if }} + {{# if page.container.view }} + {{# with (getRefreshActionDefinitionForContainer page.container) as |refreshActionDefinition| }} + if (actions.{{ simpleActionDefinitionName refreshActionDefinition }}) { + await actions.{{ simpleActionDefinitionName refreshActionDefinition }}!(processQueryCustomizer(getPageQueryCustomizer())); + } + {{/ with }} + {{/ if }} + resolve({ + result: 'delete', + data: [], + }); + } else { + resolve({ + result: 'close', + data: [], + }); + } + }, + }); + {{/ if }} }); {{/ with }} }; 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 03d0e764..31632880 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 @@ -45,16 +45,7 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async ({{# if a {{/ or }} {{ else }} {{# if (allowRefreshAfterOperationCall action) }} - {{# if page.container.table }} - setRefreshCounter((prev) => prev + 1); - {{/ if }} - {{# if page.container.view }} - if (!editMode) { - {{# with (getRefreshActionDefinitionForContainer page.container) as |actionDefinition| }} - await actions.{{ simpleActionDefinitionName actionDefinition }}!(processQueryCustomizer(getPageQueryCustomizer())); - {{/ with }} - } - {{/ if }} + await refresh(); {{/ if }} {{/ if }} diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/CancelAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/CancelAction.fragment.hbs index 407721dc..ac7f39e2 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/CancelAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/CancelAction.fragment.hbs @@ -1,6 +1,12 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async () => { - {{# with (getRefreshActionDefinitionForContainer page.container) as |actionDefinition| }} + {{# and page.openInDialog page.container.view }} + if (isDraft) { + rollbackData(); + } else { + {{/ and }} // no need to set editMode to false, given refresh should do it implicitly - await {{ simpleActionDefinitionName actionDefinition }}(processQueryCustomizer(getPageQueryCustomizer())); - {{/ with }} + await refresh(true); + {{# and page.openInDialog page.container.view }} + } + {{/ and }} }; 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 90435627..cb4909ea 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 @@ -6,20 +6,11 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async () => { try { setIsLoading(true); await {{ getServiceImplForPage page }}.set{{ firstToUpper action.targetDataElement.name }}({{# if (pageHasSignedId page) }}{ __signedIdentifier: signedIdentifier } as JudoIdentifiable{{ else }}ownerData{{/ if }}, []); - {{# if page.container.table }} - setRefreshCounter((prev) => prev + 1); - {{/ if }} - {{# if page.container.view }} - if (!editMode) { - {{# with (getRefreshActionDefinitionForContainer page.container) as |actionDefinition| }} - await actions.{{ simpleActionDefinitionName actionDefinition }}!(processQueryCustomizer(getPageQueryCustomizer())); - {{/ with }} - } - {{/ if }} + await refresh(); } catch(e) { console.error(e); } finally { - setIsLoading(false); + setIsLoading(false); } {{/ if }} {{/ with }} 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 ade59082..05b07741 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 @@ -5,13 +5,14 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async (openCrea try { setIsLoading(true); await validate(cleanUpPayload(payloadDiff.current)); - onSubmit(payloadDiff.current, true); + onSubmit(payloadDiff.current, 'submit-draft'); } catch (error) { - if (!isErrorNestedValidationError(error, '{{ page.dataElement.name }}')) { + if (ownerValidation && !isErrorNestedValidationError(error, '{{ page.dataElement.name }}')) { // relation form has no remaining error(s), proceed with submission - onSubmit(payloadDiff.current, true); + onSubmit(payloadDiff.current, 'submit-draft'); } else { - handleError<{{ classDataName (getReferenceClassType page) '' }}>(error, { setValidation }, data, isDraft ? '{{ page.dataElement.name }}' : undefined); + let relationName: string | undefined = (isDraft && ownerValidation) ? '{{ page.dataElement.name }}' : undefined; + handleError<{{ classDataName (getReferenceClassType page) '' }}>(error, { setValidation }, data, relationName); } } finally { setIsLoading(false); @@ -30,7 +31,7 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async (openCrea await customActions.post{{ firstToUpper (simpleActionDefinitionName action.actionDefinition) }}(data, res, onSubmit, onClose, openCreated); } else { showSuccessSnack(t('judo.action.create.success', { defaultValue: 'Create successful' })); - await onSubmit(res, isDraft, openCreated); + await onSubmit(res, isDraft ? 'submit-draft' : 'submit', openCreated); } } catch (error) { handleError<{{ classDataName (getReferenceClassType page) '' }}>(error, { setValidation }, data); diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/DeleteAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/DeleteAction.fragment.hbs index c9a3dd3b..0965bbf1 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/DeleteAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/DeleteAction.fragment.hbs @@ -1,15 +1,18 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async ({{# if action.actionDefinition.targetType }}target: {{ classDataName action.actionDefinition.targetType 'Stored' }}{{# if action.actionDefinition.isBulkCapable }}, silentMode?: boolean{{/ if }}{{/ if}}) => { try { - const confirmed = {{# if action.actionDefinition.isBulkCapable }}!silentMode ? {{/ if }}await openConfirmDialog( - 'row-delete-action', + const confirmed = await openConfirmDialog( + 'delete-action', t('judo.modal.confirm.confirm-delete', { defaultValue: 'Are you sure you would like to delete the selected element?' }), t('judo.modal.confirm.confirm-title', { defaultValue: 'Confirm action' }), - ){{# if action.actionDefinition.isBulkCapable }} : true{{/ if }}; + ); if (confirmed) { - await {{ getServiceImplForPage page }}.delete{{# if action.targetDataElement }}{{ firstToUpper action.targetDataElement.name }}{{/ if }}({{# if action.actionDefinition.targetType }}target{{ else }}data{{/ if }}); - {{# if action.actionDefinition.isBulkCapable }} - if (!silentMode) { + {{# if page.openInDialog }} + if (isDraft) { + onSubmit({{# if action.actionDefinition.targetType }}target{{ else }}data{{/ if }}, 'delete'); + return; + } {{/ if }} + await {{ getServiceImplForPage page }}.delete{{# if action.targetDataElement }}{{ firstToUpper action.targetDataElement.name }}{{/ if }}({{# if action.actionDefinition.targetType }}target{{ else }}data{{/ if }}); showSuccessSnack(t('judo.action.delete.success', { defaultValue: 'Delete successful' })); {{# if page.container.table }} setRefreshCounter((prev) => prev + 1); @@ -17,35 +20,22 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async ({{# if a {{# if page.container.view }} {{# unless action.actionDefinition.isContainedRelationAction }} {{# if page.openInDialog }} - onSubmit(); + onSubmit({{# if action.actionDefinition.targetType }}target{{ else }}data{{/ if }}, 'delete'); {{ else }} navigateBack(); {{/ if }} {{ else }} {{# if (containerIsRefreshable page.container) }} - {{# with (getRefreshActionDefinitionForContainer page.container) as |refreshActionDefinition| }} - {{ simpleActionDefinitionName refreshActionDefinition }}(getPageQueryCustomizer()); - {{/ with }} + await refresh(); {{/ if }} {{/ unless }} {{/ if }} - {{# if action.actionDefinition.isBulkCapable }} - } - {{/ if }} } } catch(error) { - {{# if action.actionDefinition.isBulkCapable }} - if (!silentMode) { - {{/ if }} - {{# if action.actionDefinition.targetType }} - handleError<{{ classDataName action.actionDefinition.targetType '' }}>(error, undefined, target); - {{ else }} - handleError(error, undefined, data); - {{/ if }} - {{# if action.actionDefinition.isBulkCapable }} - } else { - throw error; - } + {{# if action.actionDefinition.targetType }} + handleError<{{ classDataName action.actionDefinition.targetType '' }}>(error, undefined, target); + {{ else }} + handleError(error, undefined, data); {{/ if }} } }; 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 07508f2b..18b0113c 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 @@ -20,16 +20,7 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async () => { {{/ if }}, returnedData ); - {{# if page.container.table }} - setRefreshCounter((prev) => prev + 1); - {{/ if }} - {{# if page.container.view }} - if (!editMode) { - {{# with (getRefreshActionDefinitionForContainer page.container) as |actionDefinition| }} - await actions.{{ simpleActionDefinitionName actionDefinition }}!(processQueryCustomizer(getPageQueryCustomizer())); - {{/ with }} - } - {{/ if }} + await refresh(); } catch(e) { console.error(e); } finally { 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 c9e94f29..c8a26dcc 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 @@ -21,6 +21,14 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async ({{# if a {{# if page.container.view }} if (result === 'submit' && !editMode) { await actions.{{ simpleActionDefinitionName actionDefinition }}!(processQueryCustomizer(getPageQueryCustomizer())); + } else if (result === 'submit-draft' && returnedData) { + const decoratedData = { + ...returnedData, + __identifier: `draft:${uuidv4()}`, + }; + const newData = {{# if action.targetDataElement.isCollection }}[...(data.{{ action.targetDataElement.name }} || []), decoratedData]{{ else }}decoratedData{{/ if }} + storeDiff('{{ action.targetDataElement.name }}', newData); + return; } {{/ if }} {{# if page.container.table }} 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 6c547fe8..5f1f9be3 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,15 +1,6 @@ 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 }}}); - {{# with (getRefreshActionDefinitionForContainer page.container) as |actionDefinition| }} - {{# if page.container.view }} - if (result === 'submit' && !editMode) { - await actions.{{ simpleActionDefinitionName actionDefinition }}!(processQueryCustomizer(getPageQueryCustomizer())); - } - {{/ if }} - {{# if page.container.table }} - if (result === 'submit') { - setRefreshCounter((prev) => prev + 1); - } - {{/ if }} - {{/ with }} + 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 6c99c22f..2327e9ca 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,12 +1,6 @@ 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 }}}); if (result === 'submit') { - {{# if page.container.view }} - if (!editMode) { - {{# with (getRefreshActionDefinitionForContainer page.container) as |actionDefinition| }} - await actions.{{ simpleActionDefinitionName actionDefinition }}!(processQueryCustomizer(getPageQueryCustomizer())); - {{/ with }} - } - {{/ if }} + 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 0e2cc6d7..bb19f496 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,7 +1,7 @@ -const {{ simpleActionDefinitionName action.actionDefinition }} = async (target: {{ classDataName action.actionDefinition.targetType '' }} | {{ classDataName action.actionDefinition.targetType 'Stored' }}, isDraft?: boolean) => { - if (isDraft && (!target || !(target as {{ classDataName action.actionDefinition.targetType 'Stored' }}).__signedIdentifier)) { - {{# with (getOpenFormActionPairForOpenPageAction page action) as |openFormAction| }} - const { result, data: returnedData } = await open{{ pageName openFormAction.targetPageDefinition }}( +const {{ simpleActionDefinitionName action.actionDefinition }} = async (target: {{ classDataName action.actionDefinition.targetType '' }} | {{ classDataName action.actionDefinition.targetType 'Stored' }}, isDraftParam?: boolean) => { + {{# 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 }} @@ -11,22 +11,24 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async (target: data {{/ if }} {{/ if }}, - target, + target as {{ classDataName action.actionDefinition.targetType 'Stored' }}, true, - {{# if page.container.form }} - validate{{ firstToUpper openFormAction.targetDataElement.name }} + {{# if action.targetDataElement.isUpdateValidatable }} + validate{{ firstToUpper action.targetDataElement.name }} {{/ if }} ); // we might need to differentiate result handling between operation inputs and crud relation creates if (result === 'submit-draft' && returnedData) { {{# if action.targetDataElement.isCollection }} - const existingIndex = (payloadDiff.current.{{ action.targetDataElement.name }} || []).findIndex((r: { __identifier?: string }) => r.__identifier === returnedData.__identifier); + const existingIndex = (data.{{ action.targetDataElement.name }} || []).findIndex((r: { __identifier?: string }) => r.__identifier === returnedData.__identifier); if (existingIndex > -1) { - payloadDiff.current.{{ action.targetDataElement.name }}[existingIndex] = { + // we need to create a copy to keep order, and because mui datagrid freezes elements, and crashes on reference updates + const updatedList = [...(data.{{ action.targetDataElement.name }} || [])]; + updatedList[existingIndex] = { ...returnedData, }; + storeDiff('{{ action.targetDataElement.name }}', updatedList); } - storeDiff('{{ action.targetDataElement.name }}', [...(payloadDiff.current.{{ action.targetDataElement.name }} || [])]); {{ else }} storeDiff('{{ action.targetDataElement.name }}', { ...returnedData @@ -34,20 +36,28 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async (target: {{/ if }} return; } - {{/ with }} - } else if (!isDraft) { + if (result === 'delete' && returnedData) { + {{# if action.targetDataElement.isCollection }} + const existingIndex = (data.{{ action.targetDataElement.name }} || []).findIndex((r: { __identifier?: string }) => r.__identifier === returnedData.__identifier); + if (existingIndex > -1) { + // we need to create a copy to keep order, and because mui datagrid freezes elements, and crashes on reference updates + const updatedList = [...(data.{{ action.targetDataElement.name }} || [])]; + updatedList.splice(existingIndex, 1); + storeDiff('{{ action.targetDataElement.name }}', updatedList); + } + {{ else }} + storeDiff('{{ action.targetDataElement.name }}', null); + {{/ if }} + return; + } + if (result === 'close') { + return; + } + } else { + {{/ and }} {{# if action.targetPageDefinition.openInDialog }} await open{{ pageName action.targetPageDefinition }}(target!); - {{# with (getRefreshActionDefinitionForContainer page.container) as |actionDefinition| }} - {{# if page.container.view }} - if (!editMode) { - await actions.{{ simpleActionDefinitionName actionDefinition }}!(processQueryCustomizer(getPageQueryCustomizer())); - } - {{/ if }} - {{# if page.container.table }} - setRefreshCounter((prev) => prev + 1); - {{/ if }} - {{/ with }} + await refresh(); {{ else }} {{# if action.targetPageDefinition }} {{# if (pageHasSignedId action.targetPageDefinition) }} @@ -61,5 +71,7 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async (target: // There was no .targetPageDefinition for this action. Target Page is most likely empty in the model! {{/ if }} {{/ if }} - } + {{# and (isActionParentEagerElement action) action.targetPageDefinition.openInDialog }} + } + {{/ and }} }; 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 4525f781..37d6d534 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 @@ -8,13 +8,7 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async (): Promi storeDiff('{{ link.dataElement.name }}', returnedData[0]); {{ else }} await {{ getServiceImplForPage page }}.set{{ firstToUpper link.dataElement.name }}(data, returnedData[0]); - {{# if page.container.view }} - if (!editMode) { - {{# with (getRefreshActionDefinitionForContainer page.container) as |actionDefinition| }} - await actions.{{ simpleActionDefinitionName actionDefinition }}!(processQueryCustomizer(getPageQueryCustomizer())); - {{/ with }} - } - {{/ if }} + await refresh(); {{/ if }} return returnedData[0]; } catch(error) { 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 37fc2ba4..5b49c418 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 @@ -17,12 +17,8 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async (queryCus setData(result); setLatestViewData(result); // re-set payloadDiff - payloadDiff.current = { - __identifier: result.__identifier, - __signedIdentifier: result.__signedIdentifier, - __version: result.__version, - __entityType: result.__entityType, - } as Record; + 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 37b09b44..c5976a73 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 @@ -11,16 +11,7 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async (target?: } await {{ getServiceImplForPage page }}.remove{{ firstToUpper action.targetDataElement.name }}({{# if page.container.table }}{ __signedIdentifier: signedIdentifier } as JudoIdentifiable{{ else }}data{{/ if }}, [target!]); if (!silentMode) { - {{# if page.container.table }} - setRefreshCounter((prev) => prev + 1); - {{/ if }} - {{# if page.container.view }} - if (!editMode) { - {{# with (getRefreshActionDefinitionForContainer page.container) as |actionDefinition| }} - await actions.{{ simpleActionDefinitionName actionDefinition }}!(processQueryCustomizer(getPageQueryCustomizer())); - {{/ with }} - } - {{/ if }} + await refresh(); } } catch(error) { if (!silentMode) { 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 new file mode 100644 index 00000000..cc51131b --- /dev/null +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/RowDeleteAction.fragment.hbs @@ -0,0 +1,59 @@ +const {{ simpleActionDefinitionName action.actionDefinition }} = async ({{# if action.actionDefinition.targetType }}target: {{ classDataName action.actionDefinition.targetType 'Stored' }}{{# if action.actionDefinition.isBulkCapable }}, silentMode?: boolean{{/ if }}{{/ if}}) => { + try { + const confirmed = {{# if action.actionDefinition.isBulkCapable }}!silentMode ? {{/ if }}await openConfirmDialog( + 'row-delete-action', + t('judo.modal.confirm.confirm-delete', { defaultValue: 'Are you sure you would like to delete the selected element?' }), + t('judo.modal.confirm.confirm-title', { defaultValue: 'Confirm action' }), + ){{# if action.actionDefinition.isBulkCapable }} : true{{/ if }}; + if (confirmed) { + {{# if (isActionParentEagerElement action) }} + {{# with (getTableParentForActionDefinition action.actionDefinition) as |table| }} + const newList = (data?.{{ table.dataElement.name }} ?? []).filter((c: any) => c.__identifier !== target.__identifier); + storeDiff('{{ table.dataElement.name }}', newList); + {{/ with }} + {{# with (getLinkParentForActionDefinition action.actionDefinition) as |link| }} + storeDiff('{{ link.dataElement.name }}', null); + {{/ with }} + {{ else }} + await {{ getServiceImplForPage page }}.delete{{# if action.targetDataElement }}{{ firstToUpper action.targetDataElement.name }}{{/ if }}({{# if action.actionDefinition.targetType }}target{{ else }}data{{/ if }}); + {{# if action.actionDefinition.isBulkCapable }} + if (!silentMode) { + {{/ if }} + showSuccessSnack(t('judo.action.delete.success', { defaultValue: 'Delete successful' })); + {{# if page.container.table }} + setRefreshCounter((prev) => prev + 1); + {{/ if }} + {{# if page.container.view }} + {{# unless action.actionDefinition.isContainedRelationAction }} + {{# if page.openInDialog }} + onSubmit({{# if action.actionDefinition.targetType }}target{{ else }}data{{/ if }}, 'delete'); + {{ else }} + navigateBack(); + {{/ if }} + {{ else }} + {{# if (containerIsRefreshable page.container) }} + await refresh(); + {{/ if }} + {{/ unless }} + {{/ if }} + {{# if action.actionDefinition.isBulkCapable }} + } + {{/ if }} + {{/ if }} + } + } catch(error) { + {{# if action.actionDefinition.isBulkCapable }} + if (!silentMode) { + {{/ if }} + {{# if action.actionDefinition.targetType }} + handleError<{{ classDataName action.actionDefinition.targetType '' }}>(error, undefined, target); + {{ else }} + handleError(error, undefined, data); + {{/ if }} + {{# if action.actionDefinition.isBulkCapable }} + } else { + throw error; + } + {{/ if }} + } +}; diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/UnsetAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/UnsetAction.fragment.hbs index 9c223cb7..31a73500 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/UnsetAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/UnsetAction.fragment.hbs @@ -4,13 +4,7 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async ({{# if a storeDiff('{{ link.dataElement.name }}', null); {{ else }} await {{ getServiceImplForPage page }}.unset{{ firstToUpper link.dataElement.name }}(data); - {{# if page.container.view }} - if (!editMode) { - {{# with (getRefreshActionDefinitionForContainer page.container) as |actionDefinition| }} - await actions.{{ simpleActionDefinitionName actionDefinition }}!(processQueryCustomizer(getPageQueryCustomizer())); - {{/ with }} - } - {{/ if }} + await refresh(); {{/ if }} {{/ 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 e3575adc..577d4b55 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 @@ -2,9 +2,31 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async () => { {{# if (hasPageRequiredBy page) }} {{> actor/src/fragments/page/local-validate.fragment.hbs }} {{/ if }} + {{# if page.openInDialog }} + if (isDraft) { + try { + setIsLoading(true); + await validate(cleanUpPayload(payloadDiff.current)); + // 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 }}')) { + // relation form has no remaining error(s), proceed with submission + onSubmit(payloadDiff.current, 'submit-draft'); + } else { + let relationName: string | undefined = (isDraft && ownerValidation) ? '{{ page.dataElement.name }}' : undefined; + handleError<{{ classDataName (getReferenceClassType page) '' }}>(error, { setValidation }, data, relationName); + } + } finally { + setIsLoading(false); + } + + return; + } + {{/ if }} setIsLoading(true); try { - const { data: res } = await {{ getServiceImplForPage page }}.update(payloadDiff.current); + const { data: res } = await {{ getServiceImplForPage page }}.update(cleanUpPayload(payloadDiff.current)); 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 4599aba9..f2d2527f 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 @@ -33,6 +33,7 @@ import { OBJECTCLASS } from '@pandino/pandino-api'; {{# if (containerHasTimeInput page.container) }}uiTimeToServiceTime,{{/ if }} useErrorHandler, cleanUpPayload, + decoratePayloadWithMetaFromData, {{# if (hasExportAction page) }}fileHandling,{{/ if }} } from '~/utilities'; import type { @@ -146,11 +147,31 @@ export default function {{ pageName page }}() { {{# if page.container.view }} const getPageQueryCustomizer: () => {{ classDataName (getReferenceClassType page) 'QueryCustomizer' }} = () => ({ - _mask: actions.getMask ? actions.getMask!() : '{{ getMaskForView page.container }}', + _mask: actions.getMask ? actions.getMask!() : '{{ serializeMaskForView page }}', }); {{/ if }} {{/ unless }} + // Masks + {{# if page.container.table }} + {{# with (getFirstTableForContainer page.container) as |table| }} + const getMask: () => string = () => { + return '{{ serializeMaskForTable table page }}'; + }; + {{/ with }} + {{/ if }} + {{# if page.container.view }} + const getMask = () => '{{ serializeMaskForView page }}'; + {{/ if }} + {{# unless page.container.table }} + {{# each page.container.links as |link| }} + const get{{ firstToUpper link.relationType.name }}Mask = () => '{{ serializeMaskForLink link page }}'; + {{/ each }} + {{# each page.container.tables as |table| }} + const get{{ firstToUpper table.relationName }}Mask = () => '{{ serializeMaskForTable table page }}'; + {{/ each }} + {{/ unless }} + // Private actions const submit = async () => { {{# with (getCreateActionForPage page) as |action| }} @@ -163,19 +184,53 @@ export default function {{ pageName page }}() { await {{ simpleActionDefinitionName action.actionDefinition }}(); {{/ with }} }; - const refresh = async () => { + const refresh = async (forced = false) => { {{# if page.container.table }} setRefreshCounter((prev) => prev + 1); {{/ if }} {{# if page.container.view }} - {{# with (getRefreshActionDefinitionForContainer page.container) as |refreshActionDefinition| }} - if (actions.{{ simpleActionDefinitionName refreshActionDefinition }}) { - await actions.{{ simpleActionDefinitionName refreshActionDefinition }}!(processQueryCustomizer(getPageQueryCustomizer())); - } - {{/ with }} + if (!editMode || forced) { + {{# with (getRefreshActionDefinitionForContainer page.container) as |refreshActionDefinition| }} + if (actions.{{ simpleActionDefinitionName refreshActionDefinition }}) { + await actions.{{ simpleActionDefinitionName refreshActionDefinition }}!(processQueryCustomizer(getPageQueryCustomizer())); + } + {{/ with }} + } {{/ if }} }; + // 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 }} + {{/ unless }} + // Pandino Action overrides const { service: customActionsHook } = useTrackService<{{ containerComponentName page.container }}ActionsHook>(`(${OBJECTCLASS}=${ {{~ camelCaseNameToInterfaceKey (pageName page) }}_ACTIONS_HOOK_INTERFACE_KEY})`); const customActions: {{ containerComponentName page.container }}PageActionsExtended | undefined = customActionsHook?.( @@ -219,6 +274,17 @@ export default function {{ pageName page }}() { {{# each page.actions as |action| }} {{ simpleActionDefinitionName action.actionDefinition }}, {{/ each }} + {{# unless page.container.form }} + getMask, + {{/ unless }} + {{# unless page.container.table }} + {{# each page.container.links as |link| }} + get{{ firstToUpper link.relationType.name }}Mask, + {{/ each }} + {{# each page.container.tables as |table| }} + get{{ firstToUpper table.relationName }}Mask, + {{/ each }} + {{/ unless }} ...(customActions ?? {}), }; 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 0ca7ae28..f1f42e3c 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,6 +1,7 @@ {{> fragment.header.hbs }} import type { RandomUtils } from './interfaces'; +import type { JudoStored } from '../services/data-api/common/JudoStored'; export const exists = (element: any) => element !== undefined && element !== null; @@ -53,3 +54,24 @@ export const GENERATOR_UUID_V4 = '{{ getUUIDv4 }}'; 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; + } + if (data.__signedIdentifier) { + payload.__signedIdentifier = data.__signedIdentifier; + } + if (data.__version) { + payload.__version = data.__version; + } + if (data.__entityType) { + payload.__entityType = data.__entityType; + } + 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 5945d285..52c26a0e 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 @@ -13,8 +13,10 @@ export type ColumnActionsProvider = ( options?: ColumnsActionsOptions, ) => GridColDef[]; +export type DialogResultReason = 'close' | 'submit' | 'submit-draft' | 'delete'; + export interface DialogResult { - result: 'close' | 'submit' | 'submit-draft'; + result: DialogResultReason; data?: T; openCreated?: boolean; } diff --git a/judo-ui-react/src/test/java/hu/blackbelt/judo/ui/generator/react/MaskEntryTest.java b/judo-ui-react/src/test/java/hu/blackbelt/judo/ui/generator/react/MaskEntryTest.java new file mode 100644 index 00000000..4f1154da --- /dev/null +++ b/judo-ui-react/src/test/java/hu/blackbelt/judo/ui/generator/react/MaskEntryTest.java @@ -0,0 +1,48 @@ +package hu.blackbelt.judo.ui.generator.react; + +import hu.blackbelt.judo.ui.generator.react.mask.MaskEntry; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class MaskEntryTest { + @Test + void testEmpty() { + MaskEntry mask = new MaskEntry(Set.of(), null); + + assertEquals("", mask.serialize()); + } + + @Test + void testOnlyPrimitives() { + MaskEntry mask = new MaskEntry(Set.of("b", "a", "c"), null); + + assertEquals("a,b,c", mask.serialize()); + } + + @Test + void testWithRelations() { + MaskEntry mask = new MaskEntry(Set.of("b", "a", "c"), null); + MaskEntry rel1 = new MaskEntry(Set.of("j", "f"), "rel1"); + MaskEntry rel2 = new MaskEntry(Set.of("r", "k"), "rel2"); + mask.addRelations(rel1, rel2); + + assertEquals("a,b,c,rel1{f,j},rel2{k,r}", mask.serialize()); + } + + @Test + void testComplex() { + MaskEntry mask = new MaskEntry(Set.of("a", "b"), null); + MaskEntry rel1 = new MaskEntry(Set.of("j", "f"), "rel1"); + MaskEntry rel11 = new MaskEntry(Set.of("x", "i"), "rel11"); + rel1.addRelations(rel11); + MaskEntry rel111 = new MaskEntry(Set.of("u", "n"), "rel111"); + rel11.addRelations(rel111); + MaskEntry rel2 = new MaskEntry(Set.of("r", "k"), "rel2"); + mask.addRelations(rel1, rel2); + + assertEquals("a,b,rel1{f,j,rel11{i,x,rel111{n,u}}},rel2{k,r}", mask.serialize()); + } +} diff --git a/pom.xml b/pom.xml index b7c12a9e..591001f7 100644 --- a/pom.xml +++ b/pom.xml @@ -55,9 +55,9 @@ 18.14.2 8.9.2 - 1.1.0.20240521_213816_bd94ae76_develop + 1.1.0.20240531_103848_5dc2f3ba_feature_JNG_5742_improve_meta_model 1.0.0.20231009_184136_321053b3_develop - 1.0.0.20240308_130018_c6d4c7e2_feature_JNG_5598_command_masks + 1.0.0.20240528_150529_45bc3759_develop 3.0.0-M7