Compare commits
	
		
			642 Commits
		
	
	
		
			3.1.6
			...
			5004-icon-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | ba5c8d823d | ||
|  | 6b043d81a8 | ||
|  | e2981f2970 | ||
|  | 804551000a | ||
|  | 254b6a1e23 | ||
|  | 3838e4e605 | ||
|  | 3da22882e9 | ||
|  | 1e8f840993 | ||
|  | 4845a1f7eb | ||
|  | a0952d9a07 | ||
|  | 7fa4e60c82 | ||
|  | 30eead76e6 | ||
|  | 100e5244c8 | ||
|  | ed0399b855 | ||
|  | 27e9c18a4e | ||
|  | c6895713ed | ||
|  | 965ca97ad1 | ||
|  | 7785ce0dc0 | ||
|  | 1a47e2fc76 | ||
|  | b7e96ce6bc | ||
|  | 7a3741165b | ||
|  | e9d5d20e2d | ||
|  | 867a6ad2da | ||
|  | 03507c2a1f | ||
|  | aa79aa5479 | ||
|  | 16005a462d | ||
|  | 82c756b091 | ||
|  | b139eb4a18 | ||
|  | 6af3c8c2a9 | ||
|  | 2c3fbb1467 | ||
|  | 01716119e6 | ||
|  | a50a37ac26 | ||
|  | 11c4277466 | ||
|  | 7d284ce157 | ||
|  | aa1d5ad06b | ||
|  | 00a3010933 | ||
|  | 56a4530ec6 | ||
|  | 89e40a0b8f | ||
|  | 66bd1feb47 | ||
|  | b419e2e303 | ||
|  | dae4ba8044 | ||
|  | fe22afea6a | ||
|  | 69753a9940 | ||
|  | f6e565ba04 | ||
|  | e4fdf24545 | ||
|  | 43a9a3c3b1 | ||
|  | bfd98aaf22 | ||
|  | 4e61c54be5 | ||
|  | 39a85c721d | ||
|  | f9877f8d0b | ||
|  | 92dff4bacd | ||
|  | 338ddf17de | ||
|  | 4e6c8ea367 | ||
|  | 5f92bc83fd | ||
|  | 5e429f3be0 | ||
|  | 2a71175cd4 | ||
|  | aee531bf16 | ||
|  | 2c99909353 | ||
|  | 50e821d5d7 | ||
|  | 06f3f3c0be | ||
|  | 0b09cf5fa9 | ||
|  | 93102837dd | ||
|  | e8d81d814c | ||
|  | f6cf051282 | ||
|  | 328390c2a9 | ||
|  | 6194285b6e | ||
|  | 84a2fbed2e | ||
|  | 5ce3cdb845 | ||
|  | 3e0b5f2fe8 | ||
|  | 94e3fdd7a9 | ||
|  | 69b413040f | ||
|  | ffecf86281 | ||
|  | 4cb3ccc984 | ||
|  | 47e1389548 | ||
|  | 6d6e6fa416 | ||
|  | ad615a76c8 | ||
|  | e48607c743 | ||
|  | 046d56d692 | ||
|  | 604c70ec04 | ||
|  | 59a133cc13 | ||
|  | 0590d81e80 | ||
|  | c8a02d53e8 | ||
|  | deccfdf654 | ||
|  | f2d72b1050 | ||
|  | 3d9bc265dd | ||
|  | abe0b60bf7 | ||
|  | 54bf3f4402 | ||
|  | d3219f0600 | ||
|  | 348ec34446 | ||
|  | 33a5b2527c | ||
|  | 443492d6eb | ||
|  | a7ee31307e | ||
|  | f67268b89a | ||
|  | 42382e1a03 | ||
|  | 419dfbf08b | ||
|  | 70aed23ef0 | ||
|  | 966064328f | ||
|  | 83696abf9d | ||
|  | 2fd7aee4da | ||
|  | 7555e0644f | ||
|  | c363f375b6 | ||
|  | 2220956007 | ||
|  | b3aff3a3e6 | ||
|  | d0ad62a82b | ||
|  | 892933ff75 | ||
|  | 61fd01b871 | ||
|  | 2eba754801 | ||
|  | a7b1ce0cf8 | ||
|  | 2854351909 | ||
|  | fe9354d10b | ||
|  | 71cfa87303 | ||
|  | 802b116b01 | ||
|  | 27b54199f5 | ||
|  | 90ea3c15b3 | ||
|  | acd500fe3d | ||
|  | 8988176c22 | ||
|  | 35f35705a5 | ||
|  | a0033697ea | ||
|  | 49a3eded59 | ||
|  | d50ccea017 | ||
|  | 3ef205fb33 | ||
|  | b1bfba8b01 | ||
|  | 21832a0bd0 | ||
|  | a0b4fc8372 | ||
|  | 3812ed5ed3 | ||
|  | bdb545d6eb | ||
|  | 7650620a78 | ||
|  | 34d8b3ed5e | ||
|  | e3acc49d5e | ||
|  | c3da827222 | ||
|  | 1053fc5121 | ||
|  | cec7a86b54 | ||
|  | 16c49306f3 | ||
|  | 32540dd0e6 | ||
|  | a3c5b75368 | ||
|  | f7a43f83e5 | ||
|  | 98ca0f163e | ||
|  | 5a3e6925e5 | ||
|  | d270da74b4 | ||
|  | 204e0f636b | ||
|  | 1132d39118 | ||
|  | f2227c8f65 | ||
|  | 55cfd6fa99 | ||
|  | 83b30f1c18 | ||
|  | aa74d8160a | ||
|  | f44868384e | ||
|  | 674eb36e15 | ||
|  | e6882337c1 | ||
|  | d48594220e | ||
|  | 64cdee6d36 | ||
|  | a16c72b6a8 | ||
|  | 89839f433b | ||
|  | 3597759692 | ||
|  | d4b001a74e | ||
|  | d76b0d39cd | ||
|  | 92911f6714 | ||
|  | 380d3be819 | ||
|  | 6dfc5e0a41 | ||
|  | d4f18c1731 | ||
|  | e5a0d4094f | ||
|  | 163c1c5b98 | ||
|  | 912a30b28d | ||
|  | de0546b251 | ||
|  | 1b375b81f3 | ||
|  | b4bbae247d | ||
|  | 20ae4a7272 | ||
|  | 7f77a414ec | ||
|  | 2e5a3f4949 | ||
|  | 593726ecb8 | ||
|  | 9745904c5b | ||
|  | 8c53d95610 | ||
|  | 27604fef23 | ||
|  | 2c4dc3334d | ||
|  | 1ebb66f6ca | ||
|  | aed9a868ab | ||
|  | e6ca3af1ed | ||
|  | edc01552f9 | ||
|  | b4d29d4d4a | ||
|  | b5785dab9c | ||
|  | e67afb2611 | ||
|  | f88e536ee0 | ||
|  | 509d609020 | ||
|  | a4ec0f7959 | ||
|  | 884c887e0d | ||
|  | e0059943df | ||
|  | d11934beeb | ||
|  | 964271f9c7 | ||
|  | 97b773d257 | ||
|  | b6a25518e3 | ||
|  | b62c17f94b | ||
|  | 886fb45ad2 | ||
|  | 7b95e64a60 | ||
|  | af42664d73 | ||
|  | 8af821d380 | ||
|  | 97ee6c6745 | ||
|  | b5fb15cd9b | ||
|  | 998219ae9a | ||
|  | e8f0f80f65 | ||
|  | ff35c46d5d | ||
|  | 05c924a9df | ||
|  | 14ac309b6e | ||
|  | 8aec038c24 | ||
|  | 29b128e5e0 | ||
|  | bda9f249bc | ||
|  | c0f1581370 | ||
|  | 9420a52ec7 | ||
|  | c113b3de13 | ||
|  | 29058c163a | ||
|  | bb110ea230 | ||
|  | 785f220cd8 | ||
|  | 16570410a5 | ||
|  | 8085eda431 | ||
|  | 6503498f0a | ||
|  | 10ac7fc369 | ||
|  | da787a9993 | ||
|  | c873b57094 | ||
|  | 93974ccd92 | ||
|  | d7aa792f97 | ||
|  | 375fa9da64 | ||
|  | 28c41e17ad | ||
|  | da3ad40968 | ||
|  | 2464d9ad95 | ||
|  | 011b47a108 | ||
|  | ea747711c3 | ||
|  | 19a8fa09a8 | ||
|  | bea08706cc | ||
|  | 7950ee1241 | ||
|  | a743764345 | ||
|  | cc1c87387b | ||
|  | 1b5b3f7f88 | ||
|  | 2123514c76 | ||
|  | 379fbd7c7e | ||
|  | efdc1b1a1d | ||
|  | 52bbd82e18 | ||
|  | 20a9c051be | ||
|  | 830e475969 | ||
|  | ed4b98b598 | ||
|  | f75e2f221c | ||
|  | 5a75440668 | ||
|  | 53e092e484 | ||
|  | 53d8b97fff | ||
|  | c85667cc13 | ||
|  | 1a8b37b4e3 | ||
|  | 40a2d90e08 | ||
|  | ee269caa4a | ||
|  | d820686e5a | ||
|  | aa2a585e00 | ||
|  | f9e6bccd46 | ||
|  | 3230654ecd | ||
|  | eab512ef22 | ||
|  | a5b53ee373 | ||
|  | ac420247ae | ||
|  | 61198bd7e3 | ||
|  | 9c511b6674 | ||
|  | 9d054543a7 | ||
|  | fc3ec2a0d7 | ||
|  | e7ef73222f | ||
|  | 6623e56a1e | ||
|  | 582eca1877 | ||
|  | 2783100f84 | ||
|  | cb0c484579 | ||
|  | a1bf270ba6 | ||
|  | be5694f149 | ||
|  | 4ff364e2c3 | ||
|  | 2fa6f35873 | ||
|  | 2a4fb7123d | ||
|  | 38a77d2b78 | ||
|  | dc239db256 | ||
|  | a622d19ba7 | ||
|  | 9842d9116c | ||
|  | 19ea8f8515 | ||
|  | 4ba3c937a8 | ||
|  | dbd3f0f85b | ||
|  | 48a2876c48 | ||
|  | 10398b05d8 | ||
|  | 3a91fc17fd | ||
|  | 02893d3e78 | ||
|  | 5124bc6bf8 | ||
|  | 3952a23ba3 | ||
|  | 1048b16f3c | ||
|  | bbbbb1b1e0 | ||
|  | 14b452c996 | ||
|  | bb91a08939 | ||
|  | bffa923f05 | ||
|  | 526b3fda91 | ||
|  | 27fc89ba33 | ||
|  | d70b7ea924 | ||
|  | 1d342a778d | ||
|  | 476016cbcc | ||
|  | bd2c020e84 | ||
|  | da7c7ede02 | ||
|  | 34ed9c5cd8 | ||
|  | 47dd08e74a | ||
|  | 6aae50294f | ||
|  | ec2f6ec46f | ||
|  | 36805b6872 | ||
|  | d78cb2fec7 | ||
|  | 51edb1ef19 | ||
|  | 2ad3af1864 | ||
|  | 525d7356fe | ||
|  | bdf37b0546 | ||
|  | 0d330332f1 | ||
|  | a5f290fd47 | ||
|  | 46f4144172 | ||
|  | 356e332f66 | ||
|  | d16060bdd9 | ||
|  | 3aeb4bd868 | ||
|  | b8bcab109a | ||
|  | b94045fd86 | ||
|  | 87992823d5 | ||
|  | 93b914d4b0 | ||
|  | 61b12f6bbe | ||
|  | d71b22412b | ||
|  | d115d6c241 | ||
|  | e561efb5c5 | ||
|  | e408c6b376 | ||
|  | cd95c63daf | ||
|  | bc6a4a20d2 | ||
|  | bf30c24e8e | ||
|  | 25205f7440 | ||
|  | 6317420d4a | ||
|  | 6c14ed0ef5 | ||
|  | f53bdc8257 | ||
|  | 6d41ecdae0 | ||
|  | 7bd61f2c96 | ||
|  | 76338d4d32 | ||
|  | 3f89bc2733 | ||
|  | b9add237b2 | ||
|  | 87a25df162 | ||
|  | 341f43610a | ||
|  | 67cdf3ef96 | ||
|  | cd98f448e9 | ||
|  | b66af1c8e2 | ||
|  | 06bad61569 | ||
|  | f58766eacf | ||
|  | 8e62b2a749 | ||
|  | 9b86874c2d | ||
|  | 805ed593fb | ||
|  | c604ac2207 | ||
|  | fac79fd068 | ||
|  | da97c5d558 | ||
|  | ae7b9fe62e | ||
|  | e52c2911da | ||
|  | ca37d1ec9d | ||
|  | 07a29ff779 | ||
|  | 1b4a8ebe83 | ||
|  | abf2eacf18 | ||
|  | f808f4e2e8 | ||
|  | ca33d6b799 | ||
|  | 3fd2d07c75 | ||
|  | 940740f15d | ||
|  | 51208fcd0c | ||
|  | 707152d82f | ||
|  | 5538f6dd8a | ||
|  | d601e2caa4 | ||
|  | 46fdf56c79 | ||
|  | b76d692a65 | ||
|  | 6600910163 | ||
|  | a6973bd7ed | ||
|  | d58127730f | ||
|  | 5494c167fc | ||
|  | c5ae0be7b1 | ||
|  | b653914ee0 | ||
|  | c107c5fc92 | ||
|  | 0980c03129 | ||
|  | f6c3fdc806 | ||
|  | 2c2628d816 | ||
|  | 56fe2801eb | ||
|  | 87b1ee9642 | ||
|  | e1c36d232b | ||
|  | 13ee8cec24 | ||
|  | a977b87cb3 | ||
|  | 14dfb9aef8 | ||
|  | d520cde57a | ||
|  | 70167d7d1d | ||
|  | 3389c8160b | ||
|  | c214710f8e | ||
|  | ac6a4945cb | ||
|  | fd1a001a23 | ||
|  | f3c561cd86 | ||
|  | 4e33e785fb | ||
|  | f55ee6e665 | ||
|  | edc5e88d5a | ||
|  | 47bf166a6e | ||
|  | cf26209790 | ||
|  | e55ebde170 | ||
|  | a745ddc164 | ||
|  | 18d0fa2259 | ||
|  | d706c9cb37 | ||
|  | 20d2450cac | ||
|  | 34345461f1 | ||
|  | aa372a1707 | ||
|  | 03648dc7e8 | ||
|  | 66a667fe58 | ||
|  | 1bb3a0eca5 | ||
|  | 0e0bba25c1 | ||
|  | af701d65ac | ||
|  | 08927dfb55 | ||
|  | b27483de9c | ||
|  | b02f69b77a | ||
|  | 598b0c84ab | ||
|  | 22cc8da088 | ||
|  | a70618cdef | ||
|  | faf142cf66 | ||
|  | 1a3cc06935 | ||
|  | a712a9363b | ||
|  | 67e716466f | ||
|  | 3fae03da98 | ||
|  | 361391ceb8 | ||
|  | bf0ca38350 | ||
|  | 437c28e2b8 | ||
|  | c05d18ada1 | ||
|  | cfb300ec06 | ||
|  | 236e668201 | ||
|  | 211d420fb2 | ||
|  | c9b902c2b4 | ||
|  | b8ca4665c1 | ||
|  | ac8b1e19b7 | ||
|  | 960af87fb0 | ||
|  | de7339ae97 | ||
|  | 595933d046 | ||
|  | 789426f80e | ||
|  | 0995af62b6 | ||
|  | c2e03a40b4 | ||
|  | 148e64c3da | ||
|  | c6289ebb2c | ||
|  | 5f4ece6813 | ||
|  | c990ec39d6 | ||
|  | 1fdc600ecd | ||
|  | c855050bcf | ||
|  | e354d2ce29 | ||
|  | d218af8619 | ||
|  | d938e5fb6b | ||
|  | 29ed5b2792 | ||
|  | e39216e65a | ||
|  | 7ac7f9b4c8 | ||
|  | 4709eb9d49 | ||
|  | c13b8266dd | ||
|  | bd58431603 | ||
|  | 3075b82792 | ||
|  | 240082481f | ||
|  | ea95552285 | ||
|  | 5358b06123 | ||
|  | 99391431da | ||
|  | d396f50a9a | ||
|  | affa8ea42b | ||
|  | d711b01fe5 | ||
|  | 6e7fa6f921 | ||
|  | 343cde75a2 | ||
|  | 2dc446e45b | ||
|  | 884b7fa16a | ||
|  | 173e065b68 | ||
|  | 9a3cb0b2b5 | ||
|  | 6beae5a806 | ||
|  | 66f4008bb8 | ||
|  | a0636632a1 | ||
|  | 5dfa47ab6c | ||
|  | e9efe493f9 | ||
|  | 3bd782e62a | ||
|  | 9b49cb2b50 | ||
|  | 963fe87f14 | ||
|  | ade4679e8c | ||
|  | 40060a470b | ||
|  | a6e8fbb54a | ||
|  | 410b938442 | ||
|  | ab7e9f94fa | ||
|  | 28e9ccd372 | ||
|  | 9a66d9addd | ||
|  | 8843bda477 | ||
|  | 3278303eec | ||
|  | f5fd6e3a36 | ||
|  | a173e8e70f | ||
|  | 19dcc3a683 | ||
|  | 20d067c1ea | ||
|  | 9526566799 | ||
|  | 0b9dd82c91 | ||
|  | 19213434f9 | ||
|  | 014691346a | ||
|  | 6738b95c29 | ||
|  | 6a8230ec1e | ||
|  | 5679d264b6 | ||
|  | b20c5f3a8d | ||
|  | 014f206e9c | ||
|  | 068b93befa | ||
|  | 65d8872cea | ||
|  | bffd1d61b2 | ||
|  | 4788b81220 | ||
|  | 9a07fc03c6 | ||
|  | 954f518030 | ||
|  | 9f8ff71757 | ||
|  | 06dd59dc81 | ||
|  | 37265cf4ef | ||
|  | 2531a5283a | ||
|  | 4cc1a5d846 | ||
|  | 8a63275989 | ||
|  | 2d3e5f4ce0 | ||
|  | 5135545c6c | ||
|  | fef93818c9 | ||
|  | 7fc64a84e8 | ||
|  | 02f7cdd5aa | ||
|  | d7dcceef60 | ||
|  | ae5e1570ae | ||
|  | 50baad9624 | ||
|  | fc0041bd91 | ||
|  | 283f7f5992 | ||
|  | 1cf7b95891 | ||
|  | 7bdb8db5ff | ||
|  | de27968e4e | ||
|  | d31abda28f | ||
|  | dcfb4c9a79 | ||
|  | bf065ee11d | ||
|  | 5e3cbadffc | ||
|  | 535ef82e48 | ||
|  | c368bfea3f | ||
|  | 4498e4100e | ||
|  | 4f1e4faede | ||
|  | 3ca045394a | ||
|  | 9a19a1113e | ||
|  | 1cd550022b | ||
|  | bad08bafd7 | ||
|  | f041a21f22 | ||
|  | 712d78ca39 | ||
|  | 93f2910bd2 | ||
|  | d0ef12c486 | ||
|  | 241fd09053 | ||
|  | 208dd2a457 | ||
|  | e34ee44b21 | ||
|  | d5f59307b7 | ||
|  | 64136cc565 | ||
|  | 3e2508c740 | ||
|  | 0853cd65b2 | ||
|  | 01802c817b | ||
|  | 7e10093bb8 | ||
|  | 179032cd4d | ||
|  | 6a6f0d04d6 | ||
|  | add4d9758c | ||
|  | a0d3ea62b2 | ||
|  | 54c17c3175 | ||
|  | 80e60538e2 | ||
|  | 84a76909e2 | ||
|  | 033405fdbc | ||
|  | 9444009a9b | ||
|  | 29e9def314 | ||
|  | 8832a1aa20 | ||
|  | 5beb6dbeee | ||
|  | 1261d26b23 | ||
|  | 0b9dd11fff | ||
|  | 08a607aa6a | ||
|  | e12efc320b | ||
|  | 3ded9de803 | ||
|  | d5b424910f | ||
|  | d94d13737f | ||
|  | b1fa4918e3 | ||
|  | 742aa2fa0d | ||
|  | ce133c1c04 | ||
|  | e4dc1779c3 | ||
|  | 22b4ab6bb2 | ||
|  | 7447e88a50 | ||
|  | a193b79d3d | ||
|  | da380f7464 | ||
|  | 2dcff51125 | ||
|  | b50e0533eb | ||
|  | 711545539f | ||
|  | a6cbceed28 | ||
|  | 837d17ab65 | ||
|  | eff31c4bdc | ||
|  | 6a8f653b73 | ||
|  | 0cdb36f73d | ||
|  | db249356e6 | ||
|  | d509c1a57c | ||
|  | 6802539ccc | ||
|  | 74efaa3c2d | ||
|  | a5223709ba | ||
|  | 2291dc6132 | ||
|  | b2548c158d | ||
|  | 5a48d6d4cd | ||
|  | 7ee2b93b10 | ||
|  | cc611a7a02 | ||
|  | 1a9c34fe40 | ||
|  | ff8eb0ec2b | ||
|  | 28907082f1 | ||
|  | f83174c40a | ||
|  | ec062d008f | ||
|  | a587655a5a | ||
|  | f66b48e586 | ||
|  | 931a2344b4 | ||
|  | dd3c75d298 | ||
|  | 4a4a15de93 | ||
|  | a007ab7f2e | ||
|  | 7b01457038 | ||
|  | 54e6d60fe5 | ||
|  | c2710f4f6f | ||
|  | 20187b51b1 | ||
|  | 4be6d57d98 | ||
|  | 282d52cf0b | ||
|  | a77f8cc3e9 | ||
|  | ea4c0cdbee | ||
|  | ba08cf0417 | ||
|  | 7197153fd5 | ||
|  | b9c1dedab3 | ||
|  | 918943816f | ||
|  | 33cf34f7c7 | ||
|  | febc769df5 | ||
|  | ea483218ea | ||
|  | c8f3ad8ac7 | ||
|  | 7916dc9c05 | ||
|  | 3123a5ee51 | ||
|  | 5b5b06cc06 | ||
|  | f49f692ffa | ||
|  | 10ce681d46 | ||
|  | 08c6ea94cb | ||
|  | fea1da5542 | ||
|  | 32e8f4eac6 | ||
|  | bfe5a8a986 | ||
|  | f2cb5ea44e | ||
|  | c7335ed25b | ||
|  | 5fda57c730 | ||
|  | 3df3096bb4 | ||
|  | bb10d5bb94 | ||
|  | 1704ab7454 | ||
|  | 3275a76fb0 | ||
|  | 81937ddc45 | ||
|  | 9fd929ac1e | ||
|  | 3e6f0acf79 | ||
|  | 7f93d943d7 | ||
|  | c48a15c915 | ||
|  | eb940d6d57 | ||
|  | 9091935d77 | ||
|  | 34e8d2b051 | ||
|  | 0c2ab13c48 | ||
|  | 9489953a8f | ||
|  | b0136d03ea | ||
|  | 9fe73645ad | ||
|  | 54d4079457 | ||
|  | 8e1a21e682 | ||
|  | d84cdca43e | ||
|  | 1c6dcd373d | ||
|  | 4410ce1486 | ||
|  | cef3a01042 | ||
|  | 0c042abcab | ||
|  | f61971bc23 | ||
|  | 12543d2c2a | 
							
								
								
									
										8
									
								
								.github/workflows/tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -12,12 +12,11 @@ permissions: | ||||
| jobs: | ||||
|   build: | ||||
|     permissions: | ||||
|       checks: write  # for coverallsapp/github-action to create new checks | ||||
|       contents: read  # for actions/checkout to fetch code | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       matrix: | ||||
|         node-version: [16, 18, 20] | ||||
|         node-version: [18, 20, 22] | ||||
|     steps: | ||||
|     - uses: actions/checkout@v4 | ||||
|     - name: Use Node.js ${{ matrix.node-version }} | ||||
| @@ -29,8 +28,3 @@ jobs: | ||||
|     - name: Run tests | ||||
|       run: | | ||||
|         npm run test | ||||
|     # - name: Publish to coveralls.io | ||||
|     #   if: ${{ matrix.node-version == 16 }} | ||||
|     #   uses: coverallsapp/github-action@v1.1.2 | ||||
|     #   with: | ||||
|     #     github-token: ${{ github.token }} | ||||
|   | ||||
| @@ -1,4 +0,0 @@ | ||||
| /Gruntfile.js | ||||
| /.git/* | ||||
| *.backup | ||||
| /public/* | ||||
							
								
								
									
										746
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						| @@ -1,638 +1,280 @@ | ||||
| #### 3.1.6: Maintenance Release | ||||
| #### 4.0.8: Maintenance Release | ||||
|  | ||||
| Editor | ||||
|  | ||||
|  - Do not flag env var in num typedInput as error (#4582) @knolleary | ||||
|  - Fix example flow name in import dialog (#4578) @kazuhitoyokoi | ||||
|  - Fix missing node icons in workspace (#4570) @knolleary | ||||
|  - Fix config node sort order when importing (#5000) @knolleary | ||||
|  | ||||
| #### 4.0.7: Maintenance Release | ||||
|  | ||||
| Editor | ||||
|  | ||||
|  - Fix def can be undefined if the type is missing (#4997) @GogoVega | ||||
|  - Fix the user list of nested config node (#4995) @GogoVega | ||||
|  - Support custom login message and button (#4993) @knolleary | ||||
|  | ||||
| #### 4.0.6: Maintenance Release | ||||
|  | ||||
| Editor | ||||
|  | ||||
|  - Roll up various fixes on config node change history (#4975) @knolleary | ||||
|  - Add quotes when installing local tgz to fix spacing in the file path (#4949) @AGhorab-upland | ||||
|  - Validate json dropped into editor to avoid unhelpful error messages (#4964) @knolleary | ||||
|  - Fix junction insert position via context menu (#4974) @knolleary | ||||
|  - Apply zoom scale when calculating annotation positions (#4981) @knolleary | ||||
|  - Handle the import of an incomplete Subflow (#4811) @GogoVega | ||||
|  - Fix updating the Subflow name during a copy (#4809) @GogoVega | ||||
|  - Rename variable to avoid confusion in view.js (#4963) @knolleary | ||||
|  - Change groups.length to groups.size (#4959) @hungtcs | ||||
|  - Remove disabled node types from QuickAddDialog list (#4946) @GogoVega | ||||
|  - Fix `setModulePendingUpdated` with plugins (#4939) @GogoVega | ||||
|  - Missing getSubscriptions in the docs while its implemented (#4934) @ersinpw | ||||
|  - Apply `envVarExcludes` setting to `util.getSetting` into the function node (#4925) @GogoVega | ||||
|  - Fix `envVar` editable list should be sortable (#4932) @GogoVega | ||||
|  - Improve the node name auto-generated with the first available number (#4912) @GogoVega | ||||
|  | ||||
| Runtime | ||||
|  | ||||
|  - Handle undefined env vars (#4581) @knolleary | ||||
|  - fix: Removed offending MD5 crypto hash and replaced with SHA1 and SHA256 … (#4568) @JaysonHurst | ||||
|  - chore: remove never use import code (#4580) @giscafer | ||||
|  - Get the env config node from the parent subflow (#4960) @GogoVega | ||||
|  - Update dependencies (#4987) @knolleary | ||||
|  | ||||
| Nodes | ||||
|  | ||||
|  - fix: template node zh-CN translation (#4575) @giscafer | ||||
|  - Performance : make reading single buffer / string file faster by not re-allocating and handling huge buffers (#4980) @Fadoli | ||||
|  - Make delay node rate limit reset consistent - not send on reset. (#4940) @dceejay | ||||
|  - Fix trigger node date handling for latest time type input (#4915) @dceejay | ||||
|  - Fix delay node not dropping when nodeMessageBufferMaxLength is set (#4973) | ||||
|  - Ensure node.sep is honoured when generating CSV (#4982) @knolleary | ||||
|  | ||||
| #### 3.1.5: Maintenance Release | ||||
|  | ||||
| Runtime | ||||
|  | ||||
|  - Fix require of dns module (#4562) @knolleary | ||||
|  - Ensure global creds object is initialised when adding first cred (#4561) @knolleary | ||||
|  | ||||
| #### 3.1.4: Maintenance Release | ||||
| #### 4.0.5: Maintenance Release | ||||
|  | ||||
| Editor | ||||
|  | ||||
|  - Highlight errors in config node sidebar (#4529) @knolleary | ||||
|  - Improve feedback in import dialog to show conflicted nodes (#4550) @knolleary | ||||
|  - Modify node users info in config editor footer (#4528) @knolleary | ||||
|  - Handle modified-nodes deploy after replacing unknown config node (#4556) @knolleary | ||||
|  - Handle undefined default export when importing module (#4539) @knolleary | ||||
|  - Fix icon scaling for non .svg icons (#4491) @ralphwetzel | ||||
|  - (convertNode) Do not create the credentials object if there is nothing to export (#4544) @GogoVega | ||||
|  - Ensure subflow instance node has g property set (#4538) @knolleary | ||||
|  - Handle importing flow with existing subflow and instance node (#4546) @knolleary | ||||
|  - Update index.mst (#4483) @gorenje | ||||
|  - Include top level property name when copying path from context (#4527) @knolleary | ||||
|  - Add handling to disable items on context menu (#4500) @kazuhitoyokoi | ||||
|  - Focus Quick Add dialog from context menu (#4516) @kazuhitoyokoi | ||||
|  - Fix subflow ports in Quick Add dialog (#4518) @kazuhitoyokoi | ||||
|  - Fix location of subflow ports in palette (#4502) @kazuhitoyokoi | ||||
|  - Client/Editor Events: fix off-in-on pattern emulating once (#4484) @gorenje | ||||
|  - Restore caching busting functionality without using explict version number (#4512) @knolleary | ||||
|  - Do not translate the list of available languages (#4531) @GogoVega | ||||
|  - Add French translation of v3.1.3 changes (#4477) @GogoVega | ||||
|  - i18n(es-ES) Spanish Spain translation (#4495) @joebordes | ||||
|  - Add missing validation messages (#4487) @GogoVega | ||||
|  - Add Japanese translations for v3.1.3 (#4498) @kazuhitoyokoi | ||||
|  - Replace `rename` by `edit` for the menu flow label (#4506) @GogoVega | ||||
|  - Update editor.json fix typo in German translation (#4552) @guidoffm | ||||
|  - Refix link call node can call out of a subflow (#4908) @GogoVega | ||||
|  | ||||
| #### 4.0.4: Maintenance Release | ||||
|  | ||||
| Editor | ||||
|  | ||||
|  - Fix `link call` node can call out of a subflow (#4892) @GogoVega | ||||
|  - Fix wrong unlock state when event is triggered after deployment (#4889) @GogoVega | ||||
|  - i18n(App) update with latest language file changes (#4903) @joebordes | ||||
|  - fix typo: depreciated (#4895) @dxdc | ||||
|  | ||||
| Runtime | ||||
|  | ||||
|  - Bump the github-actions group with 1 update (#4554) @app/dependabot | ||||
|  - Clone objects types when getting env values (#4519) @knolleary | ||||
|  - Ensure global-config credential env vars are merged on deploy (#4526) @knolleary | ||||
|  - Update dev dependencies (#4893) @knolleary | ||||
|  | ||||
| Nodes | ||||
|  | ||||
|  - 21-httprequest.js remove unused code, because of broken use of toLowercase (#4522) @gorenje | ||||
|  | ||||
| #### 3.1.3: Maintenance Release | ||||
|  | ||||
| Editor | ||||
|  | ||||
|  - Add missing en-us messages (#4475) @knolleary | ||||
|  | ||||
| #### 3.1.2: Maintenance Release | ||||
|  | ||||
| Editor  | ||||
|  | ||||
|  - Relax some node validators to allow undefined value (#4471) @knolleary | ||||
|  - Fix switch validation of typeof field (#4465) @knolleary | ||||
|  - Use move cursor when hovering on group border (#4467) @knolleary | ||||
|  - Added action list Chinese (Simplified and Traditional) translation + v3.1.1 changes (#4470) @wangyiyi2056 | ||||
|  - Add French translation of `action-list` + v3.1.1 changes (#4466) @GogoVega | ||||
|   | ||||
|  Runtime | ||||
|  - MQTT: Allow msg.userProperties to have number values (#4900) @hardillb | ||||
|  | ||||
|  - Ensure nested groups inside subflows have their g props remapped (#4472) @knolleary | ||||
|   | ||||
| #### 3.1.1: Maintenance Release | ||||
| #### 4.0.3: Maintenance Release | ||||
|  | ||||
| Editor | ||||
|  | ||||
|  - Fix debug filter (#4461) @knolleary | ||||
|  - Fix various issues with debug pop-out window (#4459) @knolleary | ||||
|  - Ensure subflow instances keep track of their groups (#4457) @knolleary | ||||
|  - Fix `validateNodeProperty` without validator provided (#4455) @GogoVega | ||||
|  - Debounce node-removed notifications (#4453) @knolleary | ||||
|  - Don't try to load the parents of the first commit (#4448) @bonanitech | ||||
|  - Allow a theme to specifiy which theme mermaid should use (#4441) @knolleary | ||||
|  - Update browser title with flow name if set (#4427) @knolleary | ||||
|  - Ensure typeSearch handles undefined node definitions (#4423) @knolleary | ||||
|  - Ensure group w/h are imported if present (#4426) @knolleary | ||||
|  - Hide node status background when there is no status to show (#4425) @knolleary | ||||
|  - Add a close button to the restart-required notification (#4407) @knolleary | ||||
|  - Extend typedInput "num" type validity check to NaN, binary, octal & hex (#4371) @ralphwetzel | ||||
|  - Fix unintended new line in node name (#4399) @kazuhitoyokoi | ||||
|  - Ctrl-Enter does not close tray (Monaco) #4377 (#4382) @hazymat | ||||
|  - fix buffer viewer to handle 0b style binary (#4393) @dceejay | ||||
|  - Rework mermaid integration to support off-DOM rendering (#4364) @knolleary | ||||
|  - Add missing nls labels to context menu (#4365) @knolleary | ||||
|  - Refresh page title after changing tab name (#4850) @kazuhitoyokoi | ||||
|  - Add Japanese translations for v4.0.2 (again) (#4853) @kazuhitoyokoi | ||||
|  - Stay in quick-add mode following context menu insert (#4883) @knolleary | ||||
|  - Do not include Junction type in quick-add for virtual links (#4879) @knolleary | ||||
|  - Multiplayer cursor tracking (#4845) @knolleary | ||||
|  - Hide add-flow options when disabled via editorTheme (#4869) @knolleary | ||||
|  - Fix env-var config select when multiple defined (#4872) @knolleary | ||||
|  - Fix subflow outbound-link filter (#4857) @GogoVega | ||||
|  - Add French translations for v4.0.2 (#4856) @GogoVega | ||||
|  - Fix moving link wires (#4851) @knolleary | ||||
|  - Adjust type search dialog position to prevent x-overflow (#4844) @Steve-Mcl | ||||
|  - fix: modulesInUse might be undefined (#4838) @lorenz-maurer | ||||
|  - Add Japanese translations for v4.0.2 (#4849) @kazuhitoyokoi | ||||
|  - Fix menu to enable/disable selection when it's a group (#4828) @GogoVega | ||||
|  | ||||
| Runtime | ||||
|  | ||||
|  - Bump the github-actions group with 2 updates (#4404) @app/dependabot | ||||
|  - Handle unknown node reference inside subflow module (#4460) @knolleary | ||||
|  - Add modules.install audit event when external module installed (#4452) @knolleary | ||||
|  - Allow import of modules with subpath in specifier (#4451) @knolleary | ||||
|  - Update node-red-admin version (#4438) @knolleary | ||||
|  - Handle false-like env vars properly (#4411) @knolleary | ||||
|  - Only save settings once during node load process (#4409) @knolleary | ||||
|  - Ensure global-config nodes lookup cred values properly (#4405) @knolleary | ||||
|  - Handle credential env var evaluation when no value set (#4362) @knolleary | ||||
|  - Don't commit package-lock.json (#4354) @bonanitech | ||||
|  - Fix env evaluation when one env references another in the same object (#4361) @knolleary | ||||
|  - Add dependabot for Github Actions (#4312) @Rotzbua | ||||
|  - Update outdated Github Actions (#4311) @Rotzbua | ||||
|  - github: Request `npm run test` in PR template (#4348) @ZJvandeWeg | ||||
|  - Add French translation of v3.1.0-beta.4 changes + slight improvements (#4329) @GogoVega | ||||
|  - Handle nodes with multiple input handlers properly (#4332) @knolleary | ||||
|  - Soften the language around unrequited PRs (#4351) @knolleary | ||||
|  - Update dependencies (#4874) @knolleary | ||||
|  - GitHub: Add citation file to enable "Cite this repository" feature (#4861) @lobis | ||||
|  - Remove use of util.log (#4875) @knolleary | ||||
|  | ||||
| Nodes | ||||
|  | ||||
|  - CSV: make CSV export way faster by not re-allocating and handling huge string (#4349) @Fadoli | ||||
|  - Delay: Fix regression in delay node to not pass on msg.reset (#4350) @dceejay | ||||
|  - Link Call: Handle undefined linkType value for existing link-call nodes (#4331) @knolleary | ||||
|  - MQTT: Guard against node.broker being undefined (#4454) @knolleary | ||||
|  - MQTT: check topic length > 0 before publish (#4416) @dceejay | ||||
|  - Switch/Change: Improve validation of switch/change node rules (#4368) @knolleary | ||||
|  - Template: Fix height of description editor in template node (#4346) @kazuhitoyokoi | ||||
|  - Various: Add validators to any fields using msg-typed Input (#4440) @knolleary | ||||
|  - Fix invalid property error in range node example (#4855) | ||||
|  - Fix typo in flow example name (#4854) @kazuhitoyokoi | ||||
|  - Move SNI, ALPN and Verify Server cert out of check (#4882) @hardillb | ||||
|  - Set status of mqtt nodes to "disconnected" when deregistered from broker (#4878) @Steve-Mcl | ||||
|  - MQTT: Ensure will payload is a string (#4873) @knolleary | ||||
|  - Let batch node terminate "early" if msg.parts set to end of sequence (#4829) @dceejay | ||||
|  - Fix unintentional Capitalisation in Split node name (#4835) @dceejay | ||||
|  | ||||
| #### 3.1.0: Milestone Release | ||||
| #### 4.0.2: Maintenance Release | ||||
|  | ||||
| Editor | ||||
|  | ||||
|  - Default filter to All Catalogues and show nodes for small lists (#4318) @knolleary | ||||
|  - Better distinguish between ctrl and meta keys on mac (#4310) @knolleary | ||||
|  - Ensure junction appears when filtering quick-add list (#4297) @knolleary | ||||
|  - Update message catalogs for JSONata Expression editor (#4287) @kazuhitoyokoi | ||||
|  - Add tooltip to relevance sort button in user settings UI (#4288) @kazuhitoyokoi | ||||
|  - Capture workspace dirty state when quick-adding junction (#4283) @knolleary | ||||
|  - Add docs for $clone function (#4284) @knolleary | ||||
|  - Use a more subtle border on the header (#4818) @bonanitech | ||||
|  - Improve the editor's French translations (#4824) @GogoVega | ||||
|  - Clean up orphaned editors (#4821) @Steve-Mcl | ||||
|  - Fix node validation if the property is not required (#4812) @GogoVega | ||||
|  - Ensure mermaid.min.js is cached properly between loads of the editor (#4817) @knolleary | ||||
|  | ||||
| Runtime | ||||
|  | ||||
|  - Dependency updates (#4317) @knolleary | ||||
|  - Ensure storage/util.writeFile handles concurrent write attempts (#4316) @knolleary | ||||
|  - Migrate http -> https for nodered.org (#4313) @Rotzbua | ||||
|  - Add Node 20 to GH Action test matrix (#4305) @Rotzbua | ||||
|  - Handle group-scoped nodes inside subflow (#4301) @knolleary | ||||
|  - Handle non-url-safe chars in context api (#4298) @knolleary | ||||
|  - Fix git pull operation in project feature (#4290) @kazuhitoyokoi | ||||
|  - Change linefeed codes in Korean message catalogs (#4286) @kazuhitoyokoi | ||||
|  - Fix file permissions of message catalogs (#4285) @kazuhitoyokoi | ||||
|  - Update tour (#4278) @knolleary | ||||
|   | ||||
| Nodes | ||||
|  - Allow auth cookie name to be customised (#4815) @knolleary | ||||
|  - Guard against undefined sessions in multiplayer (#4816) @knolleary | ||||
|  | ||||
|  - File: Fix handling in file nodes when number is specified as file name (#4267) @kazuhitoyokoi | ||||
|  - Function: Adding function timeout to settings file (#4265) (#4309) @knolleary | ||||
|  - Function: Fix function setup tab layout (#4299) @knolleary | ||||
|  - HTTP Request: Handle 204 in httprequest JSON (#4262) @sammachin | ||||
|  - JSON: Fix test cases of JSON node (#4275) @kazuhitoyokoi | ||||
|  - MQTT: Remove unnecessary check for clientid if autoUnsub set (#4302) @knolleary | ||||
| #### 4.0.1: Maintenance Release | ||||
|  | ||||
| ##### 3.1.0-beta.4: Beta Release | ||||
| Editor | ||||
|  | ||||
|  Editor | ||||
|  - Ensure subflow instance credential property values are extracted (#4802) @knolleary | ||||
|  - Use `_ADD_` value for both `add new...` and `none` options (#4800) @GogoVega | ||||
|  - Fix the config node select value assignment (#4788) @GogoVega | ||||
|  - Add tooltip for number of subflow instance on info tab (#4786) @kazuhitoyokoi | ||||
|  - Add Japanese translations for v4.0.0 (#4785) @kazuhitoyokoi | ||||
|  | ||||
|  - Add Japanese translation for 3.1.0 (#4252) @kazuhitoyokoi | ||||
|  - Improve Catalogue visibility (#4248) @Steve-Mcl | ||||
|  - Add support for wiring and moving junctions on touch device (#4244) @Steve-Mcl | ||||
|  - Show errors and statuses of config nodes in the sidebar when no catch node is available (#4231) @bvmensvoort | ||||
|  - Improve wiring for horizontally aligned nodes (#4232) @knolleary | ||||
|  - French translation of Welcome Tours (#4200) @GogoVega | ||||
|  - French translation of v3.1.0-beta.3 changes (#4199) @GogoVega | ||||
|  - add Japanese message for 3.1.0 beta 3 (#4209) @HiroyasuNishiyama | ||||
|  - Dont clone the group nodes `node` array when saving edits (#4208) @Steve-Mcl | ||||
| Runtime | ||||
|  | ||||
|  Runtime | ||||
|  | ||||
|  - Add NR_SUBFLOW_NAME/ID/PATH env vars (#4250) @knolleary | ||||
|  - Evaluate all env vars as part of async flow start (#4230) @knolleary | ||||
|  - Add support for httpStatic middleware (#4229) @knolleary | ||||
|  - Ensure group nodes are properly exported in /flow api (#4803) @knolleary | ||||
|  | ||||
|  Nodes | ||||
|  | ||||
|  - Fix JSONata in file nodes (#4246) @kazuhitoyokoi | ||||
|  - Fix timeout icon in function and link call nodes (#4253) @kazuhitoyokoi | ||||
|  - Fix connection keep-alive in http request node (#4228) @knolleary | ||||
|  - adding timeout attribute to function node (#4177) @k1ln | ||||
|  - Fix manual mode join when multiple sequences being handled (#4143) @BitCaesar | ||||
|  - Fix delay node flush issue (#4203) @dceejay | ||||
|  - Update status and catch node labels in group mode (#4207) @Steve-Mcl | ||||
|  - Joins: make using msg.parts optional in join node (#4796) @dceejay | ||||
|  - HTTP Request: UI proxy should setup agents for both http_proxy and https_proxy (#4794) @Steve-Mcl | ||||
|  - HTTP Request: Remove default user agent (#4791) @Steve-Mcl | ||||
|  | ||||
| ##### 3.1.0-beta.3: Beta Release | ||||
| #### 4.0.0: Milestone Release | ||||
|  | ||||
| This marks the next major release of Node-RED. The following changes represent | ||||
| those added since the last beta. Check the beta release details below for the complete | ||||
| list. | ||||
|  | ||||
| Breaking Changes | ||||
|  | ||||
|  - Node-RED now requires Node 18.x or later. At the time of release, we recommend | ||||
|    using Node 20. | ||||
|  | ||||
| Editor | ||||
|  | ||||
|  - Select the item that is specified in a deep link URL (#4113) @Steve-Mcl | ||||
|  - Update to Monaco 0.38.0 (#4189) @Steve-Mcl | ||||
|  - Place subflow outputs/inputs relative to current view (#4183) @knolleary | ||||
|  - Enable RED.view.select to select group by id (#4184) @knolleary | ||||
|  - Combine existing env vars when merging groups (#4182) @knolleary | ||||
|  - Avoid creating empty global-config node if not needed (#4153) @knolleary | ||||
|  - Fix group selection when using lasso (#4108) @knolleary | ||||
|  - Use editor path in generating localStorage keys (#4151) @mw75 | ||||
|  - Ensure no node credentials are included when exporting to clipboard (#4112) @knolleary | ||||
|  - Fix jsonata expression test ui (#4097) @knolleary | ||||
|  - Fix search button in palette popover (#4096) @knolleary | ||||
|  - Add `httpStaticCors` (#4761) @knolleary | ||||
|  - Update dependencies (#4763) @knolleary | ||||
|  - Sync master to dev (#4756) @knolleary | ||||
|  - Add tooltip and message validation to `typedInput` (#4747) @GogoVega | ||||
|  - Replace bcrypt with @node-rs/bcrypt (#4744) @knolleary | ||||
|  - Export Nodes dialog refinement (#4746) @Steve-Mcl | ||||
|  | ||||
| #### 4.0.0-beta.4: Beta Release | ||||
|  | ||||
| Editor | ||||
|  | ||||
|  - Fix the Sidebar Config is not refreshed after a deploy (#4734) @GogoVega | ||||
|  - Fix checkboxes are not updated when calling `typedInput("value", "")` (#4729) @GogoVega | ||||
|  - Fix panning with middle mouse button on windows 10/11 (#4716) @corentin-sodebo-voile | ||||
|  - Add Japanese translation for sidebar tooltip (#4727) @kazuhitoyokoi | ||||
|  - Translate the number of items selected in the options list (#4730) @GogoVega | ||||
|  - Fix a checkbox should return a Boolean value and not the string `on` (#4715) @GogoVega | ||||
|  - Deleting a grouped node should update the group (#4714) @GogoVega | ||||
|  - Change the Config Node cursor to `pointer` (#4711) @GogoVega | ||||
|  - Add missing tooltips to Sidebar (#4713) @GogoVega | ||||
|  - Allow nodes to return additional history entries in onEditSave (#4710) @knolleary | ||||
|  - Update to Monaco 0.49.0 (#4725) @Steve-Mcl | ||||
|  - Add Japanese translations for 4.0.0-beta.3 (#4726) @kazuhitoyokoi | ||||
|  - Show lock on deploy if user is read-only (#4706) @knolleary | ||||
|  | ||||
| Runtime | ||||
|  | ||||
|  - Allow options object on each httpStatic configuration (#4109) @kevinGodell | ||||
|  - Ensure non-zero exit codes for errors (#4181) @knolleary | ||||
|  - Ensure external modules are installed synchronously (#4180) @knolleary | ||||
|  - Update dependecies include got (#4155) @knolleary | ||||
|  - Add Japanese translations for v3.1 beta.2 (#4158) @kazuhitoyokoi | ||||
|  - Ensure express server options are applied consistently (#4178) @knolleary | ||||
|  - Remove version info from theme endpoint (#4179) @knolleary | ||||
|  - Add Japanese translations for welcome tour of 3.1.0 beta.2 (#4145) @kazuhitoyokoi | ||||
|  - Added SHA-256 and SHA-512-256 digest authentication (#4100) @sroebert | ||||
|  - Add "timers" types to known types (#4103) @Steve-Mcl | ||||
|  - Ensure all CSS variables are in the output file (#3743) @bonanitech | ||||
|  - Add httpAdminCookieOptions (#4718) @knolleary | ||||
|  - chore: migrate deprecated `util.isArray` (#4724) @Rotzbua | ||||
|  - Add --version cli args (#4707) @knolleary | ||||
|  - feat(grunt): fail if files are missing (#4739) @Rotzbua | ||||
|  - fix(node-red-pi): node-red not started by path (#4736) @Rotzbua | ||||
|  - fix(editor): remove trailing slash (#4735) @Rotzbua | ||||
|  - fix: remove deprecated mqtt.js (#4733) @Rotzbua  | ||||
|  | ||||
| Nodes | ||||
|  | ||||
|  - Allow Catch/Status nodes to be scoped to their group (#4185) @NetHans | ||||
|  - MQTT: Option to disable MQTT topic unsubscribe on disconnect (#4078) @flying7eleven | ||||
|  - Perform Proxy logic more like cURL (#4616) @Steve-Mcl | ||||
|  | ||||
|  | ||||
| ##### 3.1.0-beta.2: Beta Release | ||||
| #### 4.0.0-beta.3: Beta Release | ||||
|  | ||||
| Editor | ||||
|  | ||||
|  - NEW: Add change icon to tabs (#4068) @knolleary | ||||
|  - NEW: Complete overhaul of Group UX (#4079) @knolleary | ||||
|  - NEW: Add link to node help in node edit dialog footer (#4065) @knolleary | ||||
|  - NEW: Added editor feature for connecting multiple nodes to single node (#4051) @sonntam | ||||
|  - NEW: Increase workspace size to 8000x8000 (#4094) @knolleary | ||||
|  - Ensure node buttons are redrawn when flow lock state is changed (#4091) @knolleary | ||||
|  - Prevent loops being created with junction nodes (#4087) @knolleary | ||||
|  - Prevent opening locked node's edit dialog (#4069) @knolleary | ||||
|  - Reverse direction of tab scroll to expected direction (#4064) @knolleary | ||||
|  - Add cancel operation to editableList (#4077) @HiroyasuNishiyama | ||||
|  - Apply Mermaid diagram for project settings UI (#4054) @kazuhitoyokoi | ||||
|  - Add tooltip for show/hide button on info sidebar (#4050) @kazuhitoyokoi | ||||
|  - Fix align nodes on locked tab (#4072) @HiroyasuNishiyama | ||||
|  - Fix importing connected link nodes into a subflow (#4082) @knolleary | ||||
|  - Fix to add empty marker to empty group (#4060) @HiroyasuNishiyama | ||||
|  - Fix image URLs for v3.0 tour (#4053) @kazuhitoyokoi | ||||
|  - Show scrollbar in notification dialog only when needed (#4048) @kazuhitoyokoi | ||||
|  - Update-monaco-and-typings (#4089) @Steve-Mcl | ||||
|  - Update jquery UI (#4088) @knolleary | ||||
|  - Support i18n of lock/unlock buttons in flow property UI (#4049) @kazuhitoyokoi | ||||
|  - Translation kr (#3895) @hae-iotplatform | ||||
|  - Translation zhcn (!!请懂中文的帮忙review) (#3952) @cliyr | ||||
|  - Add French translation of nodes (#3964) @GogoVega | ||||
|  - Add French translation (#3962) @GogoVega | ||||
|  - Portuguese Brazilian (pt-BR) translation (#3804) @FabsMuller | ||||
|   | ||||
|  - Improve background-deploy notification handling (#4692) @knolleary | ||||
|  - Hide workspace tab on middle mouse click (#4657) @Steve-Mcl | ||||
|  - multiplayer: Add user presence indicators (#4666) @knolleary | ||||
|  - Enable updating dependency node of package.json in project feature (#4676) @kazuhitoyokoi | ||||
|  - Add French translations for 4.0.0-beta.2 (#4681) @GogoVega | ||||
|  - Add Japanese translations for 4.0.0-beta.2 (#4674) @kazuhitoyokoi | ||||
|  - Fix saving of conf-type properties in module packaged subflows (#4658) @knolleary | ||||
|  - Add npm install timeout notification (#4662) @hardillb | ||||
|  - Fix undo of subflow env property edits (#4667) @knolleary | ||||
|  - Fix three error typos in monaco.js (#4660) @JoshuaCWebDeveloper | ||||
|  - docs: Add closing paragraph tag (#4664) @ZJvandeWeg | ||||
|  - Avoid login loops when autoLogin enabled but login fails (#4684) @knolleary | ||||
|  | ||||
| Runtime | ||||
|  | ||||
|  - NEW: Generate stable ids for subflow instance internal nodes (#4093) @knolleary | ||||
|  - NEW: Change default file name to flows.json in project feature (#4073) @kazuhitoyokoi | ||||
|  - NEW: Deprecate synchronous access to jsonata (#4090) @knolleary | ||||
|  - Add Node 18 to test matrix (#4084) @knolleary | ||||
|  - Bump minimum nodejs version supported to match documented value (#4086) @knolleary | ||||
|  - Update monaco docs link in settings.js (#4075) @Steve-Mcl | ||||
|  - Remove duplicated messages in the message catalog (#4066) @kazuhitoyokoi | ||||
|  - Ensure errors in preDeliver callback are handled (#3911) @knolleary | ||||
|  - Fix "EADDRINUSE" error (#4046) @bggbr | ||||
|  - Allow blank strings to be used for env var property substitutions (#4672) @knolleary | ||||
|  - Use rfdc for cloning pure JSON values (#4679) @knolleary | ||||
|  - fix: remove outdated Node 11+ check (#4314) @Rotzbua | ||||
|  - feat(ci): add new nodejs v22 (#4694) @Rotzbua | ||||
|  - fix(node): increase required node >=18.5 (#4690) @Rotzbua | ||||
|  - fix(dns): remove outdated node check (#4689) @Rotzbua | ||||
|  - fix(polyfill): remove import module polyfill (#4688) @Rotzbua | ||||
|  - Fix typo (#4686) @Rotzbua | ||||
|  | ||||
| Nodes | ||||
|  | ||||
|  - Link Call: Clear link-call timeouts when node is closed (#4085) @knolleary | ||||
|  - Join: ensure inflight status is cleared when in auto mode (#4083) @knolleary | ||||
|  - File Out: Fix extra newline append for multipart file write (#3915) @dceejay | ||||
|  - Add validators for complete and link call nodes (#4056) @kazuhitoyokoi | ||||
|  - Pass full error object in Function node and copy over cause property (#4685) @knolleary | ||||
|  - Replacing vm.createScript in favour of vm.Script (#4534) @patlux | ||||
|  | ||||
| ##### 3.1.0-beta.1: Beta Release | ||||
| #### 4.0.0-beta.2: Beta Release | ||||
|  | ||||
| Editor | ||||
|  | ||||
|  - NEW: Locking Flows (#3938) @knolleary | ||||
|  - NEW: Improve UX around hiding flows via context menu (#3930) @knolleary | ||||
|  - NEW: Add support for inline image in markdown editor by drag and drop of an image file (#4006) @HiroyasuNishiyama | ||||
|  - NEW: Add support for mermaid diagram to markdown editor (#4007) @HiroyasuNishiyama | ||||
|  - NEW: Support uri fragments for nodes and groups including edit support (#3870) @knolleary | ||||
|  - NEW: Add global environment variable feature (#3941) @HiroyasuNishiyama | ||||
|  | ||||
|  - Remember compact/pretty flow export user choice (#3974) @Steve-Mcl | ||||
|  - fix .red-ui-notification class (#4035) @xiaobinqt | ||||
|  - Fix border radius on Modules list header (#4038) @bonanitech | ||||
|  - fix workspace reference error in case of empty tabs (#4029) @HiroyasuNishiyama | ||||
|  - Disable delete tab menu when single tab exists (#4030) @HiroyasuNishiyama | ||||
|  - Disable hide all menu if all tabs hidden (#4031) @HiroyasuNishiyama | ||||
|  - fix hide subflow tooltip (#4033) @HiroyasuNishiyama | ||||
|  - Fix disabled menu items in project feature (#4027) @kazuhitoyokoi | ||||
|  - Let themes change radialMenu text colors (#3995) @bonanitech | ||||
|  - Add Japanese translations for v3.0.3 (#4012) @kazuhitoyokoi | ||||
|  - Add Japanese translation for v3.1.0-beta.0 (#3997) @kazuhitoyokoi | ||||
|  - Add Japanese translation for v3.1.0-beta.0 (#3916) @kazuhitoyokoi | ||||
|  - Hide subflow category after deleting subflow (#3980) @kazuhitoyokoi | ||||
|  - Prevent dbl-click opening node edit dialog with text selected (#3970) @knolleary | ||||
|  - Handle replacing unknown node inside group or subflow (#3921) @knolleary | ||||
|  - Fix #3939, red border red-ui-typedInput-container (#3949) @Steveorevo | ||||
|  - i18n item URL copy notification & add Japanese message (#3946) @HiroyasuNishiyama | ||||
|  - add Japanese message for item url copy actions (#3947) @HiroyasuNishiyama | ||||
|  - Fix autocomplete entry for responseUrl (#3884) @knolleary | ||||
|  - Fix Japanese translation for JSONata editor (#3872) @HiroyasuNishiyama | ||||
|  - Fix search type with spaces (#3841) @Steve-Mcl | ||||
|  - Fix error hanndling of JSONata expression editor for extended functions (#3871) @HiroyasuNishiyama | ||||
|  - Add button type to the adding SSH key button (#3866) @kazuhitoyokoi | ||||
|  - Check radio button as default in project dialog (#3879) @kazuhitoyokoi | ||||
|  - Add $clone as supported function (#3874) @HiroyasuNishiyama | ||||
|  - Env var jsonata (#3807) @HiroyasuNishiyama | ||||
|  - Add Japanese translation for v3.0.2 (#3852) @kazuhitoyokoi | ||||
|  - Introduce multiplayer feature (#4629) @knolleary | ||||
|  - Separate the "add new config-node" option into a new (+) button (#4627) @GogoVega | ||||
|  - Retain Palette categories collapsed and filter to localStorage (#4634) @knolleary | ||||
|  - Ensure palette filter reapplies and clear up unknown categories (#4637) @knolleary | ||||
|  - Add support for plugin (only) modules to the palette manager (#4620) @knolleary | ||||
|  - Update monaco to latest and node types to 18 LTS (#4615) @Steve-Mcl | ||||
|  | ||||
| Runtime | ||||
|  | ||||
|  - Force IPv4 name resolution to have priority (#4019) @dceejay | ||||
|  - Fix async loading of modules containing both nodes and plugins (#3999) @knolleary | ||||
|  - Use main branch as default in project feature (#4036) @kazuhitoyokoi | ||||
|  - Rename package var to avoid strict mode error (#4020) @knolleary | ||||
|  - Fix typos in settings.js (#4013) @ypid | ||||
|  - Ensure credentials object is removed before returning node in getFlow request (#3971) @knolleary | ||||
|  - Ignore commit error in project feature (#3987) @kazuhitoyokoi | ||||
|  - Update dependencies (#3969) @knolleary | ||||
|  - Add check that node sends object rather than primitive type (#3909) @knolleary | ||||
|  - Ensure key_path is quoted in GIT_SSH_COMMAND in case of spaces in pathname (#3912) @knolleary | ||||
|  - Fix nodesDir scan when node package has js/html in sub dir to package.json (#3867) @Steve-Mcl | ||||
|  - Fix file permissions (#3917) @kazuhitoyokoi | ||||
|  - ci: add minimum GitHub token permissions for workflows (#3907) @boahc077 | ||||
|  - Fix handling of subflow config-node select type in sf module (#4643) @knolleary | ||||
|  - Comms API updates (#4628) @knolleary | ||||
|  - Add French translations for 4.0.0-beta.1 (#4621) @GogoVega | ||||
|  - Add Japanese translations for 4.0.0-beta.1 (#4612) @kazuhitoyokoi | ||||
|  | ||||
| Nodes | ||||
|  - Fix change node handling of replacing with boolean (#4639) @knolleary | ||||
|  | ||||
|  - Catch: fix typo in catch.html (#3965) @we11adam | ||||
|  - Change: Fix change node overwriting msg with itself (#3899) @dceejay | ||||
|  - Comment node: Clarify where the text will appear (#4004) @dirkjanfaber | ||||
|  - CSV: change replace to replaceAll (#3990) @dceejay | ||||
|  - CSV node: check header properties for ' and " (#3920) @dceejay | ||||
|  - CSV: Fix for CSV undefined property (#3906) @dceejay | ||||
|  - Delay: let delay node handle both flush then reset (#3898) @dceejay | ||||
|  - Function: Limit number of ports in function node (#3886) @kazuhitoyokoi | ||||
|  - Function: Remove dot from variable name for external module in function node (#3880) @kazuhitoyokoi | ||||
|  - Function: add function node monaco types util and promisify (#3868) @Steve-Mcl | ||||
|  - HTTP In: Ensure msg.req.headers is enumerable (#3908) @knolleary | ||||
|  - HTTP Request: Support form-data arrays (#3991) @hardillb | ||||
|  - HTTP Request: Fix httprequest tests to be more lenient on error message (#3922) @knolleary | ||||
|  - HTTP Request: Add missing property to node object HTTPRequest (#3842) @hardillb | ||||
|  - HTTP Request/Response: Support sortable list on property UI of http request and http response nodes (#3857) @kazuhitoyokoi | ||||
|  - HTTP Response: Ensure statusCode is a number (#3894) @hardillb | ||||
|  - Inject: Allow Inject node to work with async context stores (#4021) @knolleary | ||||
|  - Join/Batch: Add count to join and batch node labels (#4028) @dceejay | ||||
|  - MQTT: Fix birth topic handling in MQTT node (#3905) @Steve-Mcl | ||||
|  - MQTT: Fix pull-down menus of MQTT configuration node (#3890) @kazuhitoyokoi | ||||
|  - MQTT: Prevent invalid mqtt birth topic crashing node-red (#3869) @Steve-Mcl | ||||
|  - MQTT: ensure sessionExpiry(Interval) is applied (#3840) @Steve-Mcl | ||||
|  - MQTT: Fix mqtt nodes not reconnecting on modified-flows deploy (#3992) @knolleary | ||||
|  - MQTT: fix single subscription mqtt node status (#3966) @Steve-Mcl | ||||
|  - Range: Add drop mode to range node (#3935) @dceejay | ||||
|  - Remove done from describe (#3873) @HiroyasuNishiyama | ||||
|  - Split node: avoid duplicate done call for buffer split (#4000) @knolleary | ||||
|  - Status: Fix typo in 25-status.html (#3981) @kazuhitoyokoi | ||||
|  - TCP Node: ensure newline substitution applies to whole message (#4009) @dceejay | ||||
|  - Template: Add information about environment variable to template node (#3882) @kazuhitoyokoi | ||||
|  - Trigger: Hide trigger node repeat send option if sending nothing (#4023) @dceejay | ||||
|  - Watch: fix watch node test on MacOS/ARM (#3942) @HiroyasuNishiyama | ||||
|  | ||||
| #### 3.0.2: Maintenance Release | ||||
| #### 4.0.0-beta.1: Beta Release | ||||
|  | ||||
| Editor | ||||
|  | ||||
|  - Fix workspace chart bottom property (#3812) @bonanitech | ||||
|  - Update german translation (#3802) @Dennis14e | ||||
|  - Support color reset to the default in subflow and group (#3801) @kazuhitoyokoi | ||||
|  - Allow generateNodeNames to handle names containing regex control chars (#3817) @knolleary | ||||
|  - Hide scrollbars until they're needed (#3808) @bonanitech | ||||
|  - Include junctions/groups when exporting subflows plus related fixes (#3816) @knolleary | ||||
|  - remove console.log (#3820) @Steve-Mcl | ||||
|  - Click on id in debug panel highlights node or flow (#4439) @ralphwetzel | ||||
|  - Support config selection in a subflow env var (#4587) @Steve-Mcl | ||||
|  - Add timestamp formatting options to TypedInput (#4468) @knolleary | ||||
|  - Allow RED.view.select to select links (#4553) @lgrkvst | ||||
|  - Add auto-complete to flow/global/env typedInput types (#4480) @knolleary | ||||
|  - Improve the appearance of the Node-RED primary header (#4598) @joepavitt | ||||
|  | ||||
| Runtime | ||||
|  | ||||
|  - Register subflow module instance node with parent flow (#3818) @knolleary | ||||
|  - let settings.httpNodeAuth accept single middleware or array of middlewares (#4572) @kevinGodell | ||||
|  - Upgrade to JSONata 2.x (#4590) @knolleary | ||||
|  - Bump minimum version to node 18 (#4571) @knolleary | ||||
|  - npm: Remove production flag on npm invocation (#4347) @ZJvandeWeg | ||||
|  - Timer testing fix (#4367) @hlovdal | ||||
|  - Bump to 4.0.0-dev (#4322) @knolleary | ||||
|  | ||||
| Nodes | ||||
|  | ||||
|  - HTTP Request: Allow HTTP Headers not in spec (#3776) @hardillb | ||||
|  | ||||
| #### 3.0.1: Maintenance Release | ||||
|  | ||||
| Editor | ||||
|  | ||||
|  - Allow codeEditor theme to be set even if `codeEditor` is not set in settings.js (#3794) @Steve-Mcl | ||||
|  - Sys info (diagnostics report) amendments (#3793) @Steve-Mcl | ||||
|  - Allow `mode` and `title` to be omitted in `options` argument for `createEditor` (#3791) @Steve-Mcl | ||||
|  - Fix focus issues (#3789) @Steve-Mcl | ||||
|  - Ensure all typedInput buttons have button type set (#3788) @knolleary | ||||
|  - Do not flag hasUsers=false nodes as unused in search (#3787) @knolleary | ||||
|  - Properly position quick-add dialog in all cases (#3786) @knolleary | ||||
|  - Ensure quick-add dialog does not obscure ghost node when shifted (#3785) @knolleary | ||||
|  - Remove use of Object.hasOwn (#3784) @knolleary | ||||
|  | ||||
| #### 3.0.0: Milestone Release | ||||
|  | ||||
| Editor | ||||
|  | ||||
|  - Use theme page and header values if settings.js values are not present (#3767) @Steve-Mcl | ||||
|  - Focus editor for undo after some actions in menu (#3759) @kazuhitoyokoi | ||||
|  - Ensure node icon shade has properly rounded corners (#3763) @knolleary | ||||
|  - Fix storing subflow credential type when input has multiple types (#3762) @knolleary | ||||
|  - Ensure global-config and flow-config have info in the hierarchy popover (#3752) @Steve-Mcl | ||||
|  - Include dirty state in history event (#3748) @Steve-Mcl | ||||
|  - Fix display direction of context sub-menu (#3746) @knolleary | ||||
|  - Fix clear pinned paths of debug sidebar menu (#3745) @HiroyasuNishiyama | ||||
|  - prevent exception generating tooltip for deleted nodes (#3742) @Steve-Mcl | ||||
|  - Fix context menu issues ready for v3 beta.5 (#3741) @Steve-Mcl | ||||
|  - Do not generate new node-ids when pasting a cut flow (#3729) @knolleary | ||||
|  - Fix to prevent node from moving out of workspace (#3731) @HiroyasuNishiyama | ||||
|  - Don't let themes change disabled config node background color (#3736) @bonanitech | ||||
|  - Move colors left behind in #3692 to CSS variables (#3737) @bonanitech | ||||
|  - Fix handling of global debug message (#3733) @HiroyasuNishiyama | ||||
|  - Fix label overflow @ config-node palette (#3730) @ralphwetzel | ||||
|  - Fix defaulting to monaco if settings does not contain codeEditor (#3732) @knolleary | ||||
|  - Disable keyboard shortcut mapping when showing Edit[..]Dialog (#3700) @ralphwetzel | ||||
|  - Update add-junction menu to work in more cases (#3727) @knolleary | ||||
|  - Ensure importMap is not null when using import UI (#3723) @Steve-Mcl | ||||
|  - Add Japanese translations for v3.0-beta.4 (#3724) @kazuhitoyokoi | ||||
|  - Fix "split with" on virtual links (#3766) @Steve-Mcl | ||||
|  | ||||
| Runtime | ||||
|  | ||||
|  - Do not remove unknown credentials of Subflow Modules (#3728) @knolleary | ||||
|  - Add missing entries from beta.4 changelog (#3721) @knolleary | ||||
|  | ||||
| Nodes | ||||
|  | ||||
|  - Change: Fix change node, not handling from field properly when using context (#3754) @Fadoli | ||||
|  - Link Call: Fix linkcall registry bugs (#3751) @Steve-Mcl | ||||
|  - WebSocket: Fix close timeout of websocket node (#3734) @HiroyasuNishiyama | ||||
|  | ||||
| #### 3.0.0-beta.4: Beta Release | ||||
|  | ||||
| Editor | ||||
|  | ||||
|  - Move all colours to CSS variables (#3692) @bonanitech | ||||
|  - Fix clicking on node in workspace to hide context menu (#3696) @knolleary | ||||
|  - Fix credential type input item of subflow template (#3703) @HiroyasuNishiyama | ||||
|  - Add option flag `reimport` to `importNodes` (#3718) @Steve-Mcl | ||||
|  - Update german translation (#3691) @Dennis14e | ||||
|  - List welcome tours in help sidebar (#3717) @knolleary | ||||
|  - Ensure 'hidden flow' count doesn't include subflows (#3715) @knolleary | ||||
|  - Fix Chinese translate (#3706) @hotlong | ||||
|  - Fix use default button for node icon (#3714) @kazuhitoyokoi | ||||
|  - Fix select boxes vertical alignment (#3698) @bonanitech | ||||
|  - Ensure workspace clean after undoing dropped node (#3708) @Steve-Mcl | ||||
|  - Use solid colour as config node icon background to hide text overflow (#3710) @Steve-Mcl | ||||
|  - Increase quick-add height to reveal 2 most recent entries (#3711) @Steve-Mcl | ||||
|  - Set default editor to monaco in absence of user preference (#3702) @knolleary | ||||
|  - Add Japanese translations for v3.0-beta.3 (#3688) @kazuhitoyokoi | ||||
|  - Fix handling of spacebar inside JSON visual editor (#3687) @knolleary | ||||
|  - Fix menu padding to handle both icons and submenus (#3686) @knolleary | ||||
|  - Include scroll offset when positioning quick-add dialog (#3685) @knolleary | ||||
|  | ||||
| Runtime | ||||
|  | ||||
|  - Allow flows to be stopped and started manually (#3719) @knolleary | ||||
|  - Import default export if node is a transpiled es module (#3669) @dschmidt | ||||
|  - Leave Monaco theme commented out by default (#3704) @bonanitech | ||||
|  | ||||
| Nodes | ||||
|  | ||||
|  - CSV: Fix CSV node to handle when outputting text fields (#3716) @dceejay | ||||
|  - Delay: Fix delay rate limit last timing when empty (#3709) @dceejay | ||||
|  - Link: Ensure link-call cache is updated when link-in is modified (#3695) @Steve-Mcl | ||||
|  - Join: Join node in reduce mode doesn't keep existing msg properties (#3670) @dceejay | ||||
|  - Template: Add support for evalulating {{env.<var>}} within a template node (#3690) @cow0w | ||||
|  | ||||
| #### 3.0.0-beta.3: Beta Release | ||||
|  | ||||
| Editor | ||||
|  | ||||
|  - Add Right-Click content menu (#3678) @knolleary | ||||
|  - Fix disable junction (#3671) @HiroyasuNishiyama | ||||
|  - Add Japanese translations for v2.2.3 (#3672) @kazuhitoyokoi | ||||
|  - Reset mouse state when switching tabs (#3643) @knolleary | ||||
|  - Fix uncorrect fix of junction to subflow conversion (#3666) @HiroyasuNishiyama | ||||
|  - Fix undoing junction to subflow (#3653) @HiroyasuNishiyama | ||||
|  - Fix conversion of junction to subflow (#3652) @HiroyasuNishiyama | ||||
|  - Fix to include junction to exported nodes (#3650) @HiroyasuNishiyama | ||||
|  - Fix z-index value for shade to cover nodes in palette (#3649) @kazuhitoyokoi | ||||
|  - Fix to extend escaped subflow category characters (#3647) @HiroyasuNishiyama | ||||
|  - Fix to sanitize tab name (#3646) @HiroyasuNishiyama | ||||
|  - Fix selector placement (#3644) @bonanitech | ||||
|  - Add Japanese translations for v3.0-beta.2 (#3622) @kazuhitoyokoi | ||||
|  - Fix new folder menu of save to library dialog (#3633) @HiroyasuNishiyama | ||||
|  - Fix layer of palette node (#3638) @HiroyasuNishiyama | ||||
|  - Fix to place a node dragged from palette within the workspace (#3637) @HiroyasuNishiyama | ||||
|  - Fix typo in CSS (#3628) @bonanitech | ||||
|  - Use the correct variable for the gutter text color (#3615) @bonanitech | ||||
|  | ||||
|  | ||||
| Runtime | ||||
|  | ||||
|  - Support loading node modules from `nodesdir` (#3676) @Steve-Mcl | ||||
|  - fix buffer parse error message of evaluateNodeProperty (#3624) @HiroyasuNishiyama | ||||
|  | ||||
| Nodes | ||||
|  | ||||
|  - File: Further simplify file node filename entry UX (v3) (#3677) @Steve-Mcl | ||||
|  - Function: Fix initial cursor position of init/finalize tab of function node (#3674) @HiroyasuNishiyama | ||||
|  - Function: Fix ESM module loading in Function node (#3645) @knolleary | ||||
|  - Inject: Fix JSONata evaluation of inject button (#3632) @HiroyasuNishiyama | ||||
|  - TCP: Dont delete TCP socket twice (#3630) @Steve-Mcl | ||||
|  - MQTT Node: define noproxy variable (#3626) @Steve-Mcl | ||||
|  - Debug: i18n debug sidebar node label (#3623) @HiroyasuNishiyama | ||||
|  | ||||
| #### 3.0.0-beta.2: Beta Release | ||||
|  | ||||
| **Migration from 2.x** | ||||
|  | ||||
|  - The 'slice wires' action has changed from Ctrl-RightMouseButton to Alt-LeftMouseButton | ||||
|  | ||||
| Editor | ||||
|  | ||||
|  - Rework Junctions to be more node like in their event handling (#3607) @knolleary | ||||
|  - Change slicing / slice-junction operations over to mouse button 0 (Left Mouse Button) (#3609) @Steve-Mcl | ||||
|  - Do not slice-junction link node wires (#3608) @knolleary | ||||
|  - Handle many-to-one slicing of wires (#3604) @knolleary | ||||
|  - Ensure ACE worker options are set (#3611) @Steve-Mcl | ||||
|  - Remove duplicate history add of ungroup event (#3605) @knolleary | ||||
|  - use text width instead of number of characters for deciding select fi… (#3603) @HiroyasuNishiyama | ||||
|  - Update Japanese info of link call node reflecting update of English info (#3600) @HiroyasuNishiyama | ||||
|  - Fix typedInput label not visible on themes (#3580) @bonanitech | ||||
|  - Fix project switching when junctions are present (#3595) @Steve-Mcl | ||||
|  - Fix junction: when wiring from a regular nodes INPUT, backwards to a junction (#3591) @Steve-Mcl | ||||
|  - Fix error initialising flow tab editor (#3585) @Steve-Mcl | ||||
|  - Add Japanese translations for v3.0-beta.1 (#3576) @kazuhitoyokoi | ||||
|  - Fix image paths where `red/image/typedInput/XXXX.png` should be `red/image/typedInput/XXXX.svg` (#3592) @kazuhitoyokoi | ||||
|  - Fix browser console error Uncaught TypeError when searching certain terms (#3584) @Steve-Mcl | ||||
|  | ||||
| Runtime | ||||
|  | ||||
|  - fix error on system-info action (#3589) @HiroyasuNishiyama | ||||
|  | ||||
| Nodes | ||||
|  | ||||
|  - I18n switch rule selector (#3602) @HiroyasuNishiyama | ||||
|  - Handle removal of event handlers to allow mqtt client.end() to work (#3594) @PhilDay-CT | ||||
|  - update link-call node info according to current behavior (#3597) @HiroyasuNishiyama | ||||
|  | ||||
|  | ||||
| #### 3.0.0-beta.1: Beta Release | ||||
|  | ||||
| **Migration from 2.x** | ||||
|  | ||||
|  - Node-RED now requires Node.js 14.x or later. | ||||
|  - New installs of Node-RED will default to the monaco editor. | ||||
|  | ||||
|  | ||||
| Editor | ||||
|  | ||||
|  - Add Junctions (#3462) @knolleary | ||||
|  - Allow node name to be auto-generated when added (#3478, #3538) @knolleary | ||||
|  - Set monaco as default code editor as of v3.x (#3543) @Steve-Mcl | ||||
|  - Update Monaco to V0.33.0 (#3522) @Steve-Mcl | ||||
|  - Auto-complete Improvements (#3521) @Steve-Mcl | ||||
|  - Add a tooltip to debug sidebar messages to reveal full path to node (#3503) @knolleary | ||||
|  - Fix down arrow triggering menu in search box (#3507) @Steve-Mcl | ||||
|  - Add Japanese translations for v3.0 (#3512) @kazuhitoyokoi | ||||
|  - Add feature: Continuous search tools (search previous, search next) (#3405) @Steve-Mcl | ||||
|  - Add feature: split-wire-to-links (#3399, #3476) @Steve-Mcl | ||||
|  - Add copy button to node properties tables (#3390) @knolleary | ||||
|  - Add info-tab search options dropdown to the regular search (#3395) @Steve-Mcl | ||||
|  - New Feature: Add ability to find modified nodes/flows. (#3392) @Steve-Mcl | ||||
|  - Code editor ux improvements around remembering state of each code editor in a flow (#3553) @Steve-Mcl | ||||
|  - Make it easier to apply themes on SVG icons (#3515) @bonanitech | ||||
|  - Add support of property validation message (#3438) @HiroyasuNishiyama | ||||
|  - Ensure node validation tooltip is closed when field becomes valid (#3570) @knolleary | ||||
|  - Add "search for" buttons to notifications (#3567) @Steve-Mcl | ||||
|  - Don't let themes change node config colors (#3564) @bonanitech | ||||
|  - Fix gap between typedInput containers borders (#3560) @bonanitech | ||||
|  - Fix recording removed links in edit history (#3547) @knolleary | ||||
|  - Remove unused SASS vars (#3536) @bonanitech | ||||
|  - Add custom style for jQuery widgets borders (#3537) @bonanitech | ||||
|  - fix out of scope reference of hasUnusedConfig variable (#3535) @HiroyasuNishiyama | ||||
|  - correct "non string" check parenthesis (#3524) @Steve-Mcl | ||||
|  - Ensure i18n of scoped package name (#3516) @Steve-Mcl | ||||
|  - Prevent shortcut deploy when deploy button shaded (#3517) @Steve-Mcl | ||||
|  - Fix: Sidebar "Configuration" filter button tooltip (#3500) @ralphwetzel | ||||
|  - Add the ability to customize diff colors even more (#3499) @bonanitech | ||||
|  - Do JSON comparison of old value/new value in editor (#3481) @Steve-Mcl | ||||
|  - Fix nodes losing their wires when in an iframe (#3484) @zettca | ||||
|  - Improve scroll into view (#3468) @Steve-Mcl | ||||
|  - Do not show 1st tab if hidden when loading (#3464) @Steve-Mcl | ||||
|  | ||||
| Runtime | ||||
|  | ||||
|  - Fix importing external module from node-red module (#3541) @knolleary | ||||
|  - Add support for multiple static paths with optional static root (#3542) @Steve-Mcl | ||||
|  - Store external token when authenticating if provided (#3460) @ArFe | ||||
|  - Support OAuth/OpenID logout (#3388) @mw75 | ||||
|  - Allow adminAuth to auto-login users when using passport strategy (#3519) @knolleary | ||||
|  - Add runtime diagnostics admin endpoint (#3511) @Steve-Mcl | ||||
|  - Don't start if user has no home directory (#3540) @hardillb | ||||
|  - Error on invalid encrypted credentials (#3498) @sammachin | ||||
|  | ||||
| Nodes | ||||
|  | ||||
|  - Debug: Add message count option to Debug status (#3544 #3551) @rafaelmuynarsk @knolleary | ||||
|  - File: Change basic Filename field to a typedInput (#3533) @Steve-Mcl | ||||
|  - HTTP Request: Add UI for Http Request node headers (#3488) @Steve-Mcl | ||||
|  - Inject: let inject optionally fire at start in only at time mode. (#3385) @dceejay | ||||
|  - Link Call: Dynamic link call (#3463) @Steve-Mcl | ||||
|  - Link Call: Display link targets of nodes in a regular flow, for Link Call nodes inside a subflow (#3528) @Steve-Mcl | ||||
|  - MQTT: MQTT payload auto parsing improvements (#3530) @Steve-Mcl | ||||
|  - MQTT: Add client and Runtime MQTT topic validation (#3563) @Steve-Mcl [dev] | ||||
|  - MQTT: save and restore v5 config user props (#3562) @Steve-Mcl | ||||
|  - MQTT: Fix incorrect MQTT status (#3552) @Steve-Mcl | ||||
|  - MQTT: fix reference error of msg.status in debug node (#3526) @HiroyasuNishiyama | ||||
|  - MQTT: Add unit tests for MQTT nodes (#3497) @Steve-Mcl | ||||
|  - MQTT: fix typo of will properties (#3502) @Steve-Mcl | ||||
|  - MQTT: ensure mqtt v5 props can be set false (#3472) @Steve-Mcl | ||||
|  - Switch: add check for NaN in is of type number to be false (#3409) @dceejay | ||||
|  - TCP: TCP node better split (#3465) @dceejay | ||||
|  - Watch: Update Watch node to use node-watch module (#3559 #3569) @knolleary | ||||
|  - WebSocket: call done after ws disconnects (#3531) @Steve-Mcl | ||||
|  - TCP node - when resetting, if no payload, stay disconnected @dceejay | ||||
|  - HTML node: add option for collecting attributes and content (#4513) @gorenje | ||||
|  - let split node specify property to split on, and join auto join correctly (#4386) @dceejay | ||||
|  - Add RFC4180 compliant mode to CSV node (#4540) @Steve-Mcl | ||||
|  - Fix change node to return boolean if asked (#4525) @dceejay | ||||
|  - Let msg.reset reset Tcp request node connection when in stay connected mode (#4406) @dceejay | ||||
|  - Let debug node status msg length be settable via settings (#4402) @dceejay | ||||
|  - Feat: Add ability to set headers for WebSocket client (#4436) @marcus-j-davies | ||||
|  | ||||
| #### Older Releases | ||||
|  | ||||
|   | ||||
							
								
								
									
										7
									
								
								CITATION.cff
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,7 @@ | ||||
| cff-version: 1.2.0 | ||||
| message: "If you use this software, please cite it as below." | ||||
| title: "Node-RED" | ||||
| authors: | ||||
|   - family-names: "OpenJS Foundation" | ||||
|   - family-names: "Contributors" | ||||
| url: "https://nodered.org" | ||||
							
								
								
									
										73
									
								
								Gruntfile.js
									
									
									
									
									
								
							
							
						
						| @@ -143,6 +143,7 @@ module.exports = function(grunt) { | ||||
|                     "packages/node_modules/@node-red/editor-client/src/js/user.js", | ||||
|                     "packages/node_modules/@node-red/editor-client/src/js/comms.js", | ||||
|                     "packages/node_modules/@node-red/editor-client/src/js/runtime.js", | ||||
|                     "packages/node_modules/@node-red/editor-client/src/js/multiplayer.js", | ||||
|                     "packages/node_modules/@node-red/editor-client/src/js/text/bidi.js", | ||||
|                     "packages/node_modules/@node-red/editor-client/src/js/text/format.js", | ||||
|                     "packages/node_modules/@node-red/editor-client/src/js/ui/state.js", | ||||
| @@ -207,38 +208,52 @@ module.exports = function(grunt) { | ||||
|                     "packages/node_modules/@node-red/editor-client/src/js/ui/touch/radialMenu.js", | ||||
|                     "packages/node_modules/@node-red/editor-client/src/js/ui/tour/*.js" | ||||
|                 ], | ||||
|                 nonull: true, | ||||
|                 dest: "packages/node_modules/@node-red/editor-client/public/red/red.js" | ||||
|             }, | ||||
|             vendor: { | ||||
|                 files: { | ||||
|                     "packages/node_modules/@node-red/editor-client/public/vendor/vendor.js": [ | ||||
|                         "packages/node_modules/@node-red/editor-client/src/vendor/jquery/js/jquery-3.5.1.min.js", | ||||
|                         "packages/node_modules/@node-red/editor-client/src/vendor/jquery/js/jquery-migrate-3.3.0.min.js", | ||||
|                         "packages/node_modules/@node-red/editor-client/src/vendor/jquery/js/jquery-ui.min.js", | ||||
|                         "packages/node_modules/@node-red/editor-client/src/vendor/jquery/js/jquery.ui.touch-punch.min.js", | ||||
|                         "node_modules/marked/marked.min.js", | ||||
|                         "node_modules/dompurify/dist/purify.min.js", | ||||
|                         "packages/node_modules/@node-red/editor-client/src/vendor/d3/d3.v3.min.js", | ||||
|                         "node_modules/i18next/i18next.min.js", | ||||
|                         "node_modules/i18next-http-backend/i18nextHttpBackend.min.js", | ||||
|                         "node_modules/jquery-i18next/jquery-i18next.min.js", | ||||
|                         "node_modules/jsonata/jsonata-es5.min.js", | ||||
|                         "packages/node_modules/@node-red/editor-client/src/vendor/jsonata/formatter.js", | ||||
|                         "packages/node_modules/@node-red/editor-client/src/vendor/ace/ace.js", | ||||
|                         "packages/node_modules/@node-red/editor-client/src/vendor/ace/ext-language_tools.js" | ||||
|                     ], | ||||
|                     // "packages/node_modules/@node-red/editor-client/public/vendor/vendor.css": [ | ||||
|                     //     // TODO: resolve relative resource paths in | ||||
|                     //     //       bootstrap/FA/jquery | ||||
|                     // ], | ||||
|                     "packages/node_modules/@node-red/editor-client/public/vendor/ace/worker-jsonata.js": [ | ||||
|                         "node_modules/jsonata/jsonata-es5.min.js", | ||||
|                         "packages/node_modules/@node-red/editor-client/src/vendor/jsonata/worker-jsonata.js" | ||||
|                     ], | ||||
|                     "packages/node_modules/@node-red/editor-client/public/vendor/mermaid/mermaid.min.js": [ | ||||
|                         "node_modules/mermaid/dist/mermaid.min.js" | ||||
|                     ] | ||||
|                 } | ||||
|                 files: [ | ||||
|                     { | ||||
|                         src: [ | ||||
|                             "packages/node_modules/@node-red/editor-client/src/vendor/jquery/js/jquery-3.5.1.min.js", | ||||
|                             "packages/node_modules/@node-red/editor-client/src/vendor/jquery/js/jquery-migrate-3.3.0.min.js", | ||||
|                             "packages/node_modules/@node-red/editor-client/src/vendor/jquery/js/jquery-ui.min.js", | ||||
|                             "packages/node_modules/@node-red/editor-client/src/vendor/jquery/js/jquery.ui.touch-punch.min.js", | ||||
|                             "node_modules/marked/marked.min.js", | ||||
|                             "node_modules/dompurify/dist/purify.min.js", | ||||
|                             "packages/node_modules/@node-red/editor-client/src/vendor/d3/d3.v3.min.js", | ||||
|                             "node_modules/i18next/i18next.min.js", | ||||
|                             "node_modules/i18next-http-backend/i18nextHttpBackend.min.js", | ||||
|                             "node_modules/jquery-i18next/jquery-i18next.min.js", | ||||
|                             "node_modules/jsonata/jsonata-es5.min.js", | ||||
|                             "packages/node_modules/@node-red/editor-client/src/vendor/jsonata/formatter.js", | ||||
|                             "packages/node_modules/@node-red/editor-client/src/vendor/ace/ace.js", | ||||
|                             "packages/node_modules/@node-red/editor-client/src/vendor/ace/ext-language_tools.js" | ||||
|                         ], | ||||
|                         nonull: true, | ||||
|                         dest: "packages/node_modules/@node-red/editor-client/public/vendor/vendor.js" | ||||
|                     }, | ||||
|                     // { | ||||
|                     //     src: [ | ||||
|                     //         // TODO: resolve relative resource paths in | ||||
|                     //         //       bootstrap/FA/jquery | ||||
|                     //     ], | ||||
|                     //     dest: "packages/node_modules/@node-red/editor-client/public/vendor/vendor.css" | ||||
|                     // }, | ||||
|                     { | ||||
|                         src: [ | ||||
|                             "node_modules/jsonata/jsonata-es5.min.js", | ||||
|                             "packages/node_modules/@node-red/editor-client/src/vendor/jsonata/worker-jsonata.js" | ||||
|                         ], | ||||
|                         nonull: true, | ||||
|                         dest: "packages/node_modules/@node-red/editor-client/public/vendor/ace/worker-jsonata.js", | ||||
|                     }, | ||||
|                     { | ||||
|                         src: "node_modules/mermaid/dist/mermaid.min.js", | ||||
|                         nonull: true, | ||||
|                         dest: "packages/node_modules/@node-red/editor-client/public/vendor/mermaid/mermaid.min.js", | ||||
|                     }, | ||||
|                 ] | ||||
|             } | ||||
|         }, | ||||
|         uglify: { | ||||
|   | ||||
							
								
								
									
										16
									
								
								nodemon.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | ||||
| { | ||||
|     "ignoreRoot": [ | ||||
|         ".git", | ||||
|         ".nyc_output", | ||||
|         ".sass-cache", | ||||
|         "bower-components", | ||||
|         "coverage" | ||||
|     ], | ||||
|     "ignore": [ | ||||
|         "/Gruntfile.js", | ||||
|         "/.git/*", | ||||
|         "*.backup", | ||||
|         "/public/*" | ||||
|     ] | ||||
| } | ||||
|  | ||||
							
								
								
									
										67
									
								
								package.json
									
									
									
									
									
								
							
							
						
						| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "node-red", | ||||
|     "version": "3.1.6", | ||||
|     "version": "4.0.8", | ||||
|     "description": "Low-code programming for event-driven applications", | ||||
|     "homepage": "https://nodered.org", | ||||
|     "license": "Apache-2.0", | ||||
| @@ -26,26 +26,26 @@ | ||||
|         } | ||||
|     ], | ||||
|     "dependencies": { | ||||
|         "acorn": "8.8.2", | ||||
|         "acorn-walk": "8.2.0", | ||||
|         "ajv": "8.12.0", | ||||
|         "async-mutex": "0.4.0", | ||||
|         "acorn": "8.12.1", | ||||
|         "acorn-walk": "8.3.4", | ||||
|         "ajv": "8.17.1", | ||||
|         "async-mutex": "0.5.0", | ||||
|         "basic-auth": "2.0.1", | ||||
|         "bcryptjs": "2.4.3", | ||||
|         "body-parser": "1.20.2", | ||||
|         "body-parser": "1.20.3", | ||||
|         "cheerio": "1.0.0-rc.10", | ||||
|         "clone": "2.1.2", | ||||
|         "content-type": "1.0.5", | ||||
|         "cookie": "0.5.0", | ||||
|         "cookie-parser": "1.4.6", | ||||
|         "cookie": "0.7.2", | ||||
|         "cookie-parser": "1.4.7", | ||||
|         "cors": "2.8.5", | ||||
|         "cronosjs": "1.7.1", | ||||
|         "denque": "2.1.0", | ||||
|         "express": "4.18.2", | ||||
|         "express-session": "1.17.3", | ||||
|         "express": "4.21.2", | ||||
|         "express-session": "1.18.1", | ||||
|         "form-data": "4.0.0", | ||||
|         "fs-extra": "11.1.1", | ||||
|         "got": "12.6.0", | ||||
|         "fs-extra": "11.2.0", | ||||
|         "got": "12.6.1", | ||||
|         "hash-sum": "2.0.0", | ||||
|         "hpagent": "1.2.0", | ||||
|         "https-proxy-agent": "5.0.1", | ||||
| @@ -54,41 +54,42 @@ | ||||
|         "is-utf8": "0.2.1", | ||||
|         "js-yaml": "4.1.0", | ||||
|         "json-stringify-safe": "5.0.1", | ||||
|         "jsonata": "1.8.6", | ||||
|         "jsonata": "2.0.5", | ||||
|         "lodash.clonedeep": "^4.5.0", | ||||
|         "media-typer": "1.1.0", | ||||
|         "memorystore": "1.6.7", | ||||
|         "mime": "3.0.0", | ||||
|         "moment": "2.29.4", | ||||
|         "moment-timezone": "0.5.43", | ||||
|         "mqtt": "4.3.7", | ||||
|         "moment": "2.30.1", | ||||
|         "moment-timezone": "0.5.46", | ||||
|         "mqtt": "5.7.0", | ||||
|         "multer": "1.4.5-lts.1", | ||||
|         "mustache": "4.2.0", | ||||
|         "node-red-admin": "^3.1.2", | ||||
|         "node-red-admin": "^4.0.1", | ||||
|         "node-watch": "0.7.4", | ||||
|         "nopt": "5.0.0", | ||||
|         "oauth2orize": "1.11.1", | ||||
|         "oauth2orize": "1.12.0", | ||||
|         "on-headers": "1.0.2", | ||||
|         "passport": "0.6.0", | ||||
|         "passport": "0.7.0", | ||||
|         "passport-http-bearer": "1.0.1", | ||||
|         "passport-oauth2-client-password": "0.1.2", | ||||
|         "raw-body": "2.5.2", | ||||
|         "semver": "7.5.4", | ||||
|         "tar": "6.1.13", | ||||
|         "tough-cookie": "4.1.3", | ||||
|         "raw-body": "3.0.0", | ||||
|         "rfdc": "^1.3.1", | ||||
|         "semver": "7.6.3", | ||||
|         "tar": "7.4.3", | ||||
|         "tough-cookie": "^5.0.0", | ||||
|         "uglify-js": "3.17.4", | ||||
|         "uuid": "9.0.0", | ||||
|         "ws": "7.5.6", | ||||
|         "uuid": "9.0.1", | ||||
|         "ws": "7.5.10", | ||||
|         "xml2js": "0.6.2" | ||||
|     }, | ||||
|     "optionalDependencies": { | ||||
|         "bcrypt": "5.1.1" | ||||
|         "@node-rs/bcrypt": "1.10.4" | ||||
|     }, | ||||
|     "devDependencies": { | ||||
|         "dompurify": "2.4.1", | ||||
|         "dompurify": "2.5.7", | ||||
|         "grunt": "1.6.1", | ||||
|         "grunt-chmod": "~1.1.1", | ||||
|         "grunt-cli": "~1.4.3", | ||||
|         "grunt-cli": "~1.5.0", | ||||
|         "grunt-concurrent": "3.0.0", | ||||
|         "grunt-contrib-clean": "2.0.1", | ||||
|         "grunt-contrib-compress": "2.0.0", | ||||
| @@ -99,7 +100,7 @@ | ||||
|         "grunt-contrib-watch": "1.1.0", | ||||
|         "grunt-jsdoc": "2.4.1", | ||||
|         "grunt-jsdoc-to-markdown": "6.0.0", | ||||
|         "grunt-jsonlint": "2.1.3", | ||||
|         "grunt-jsonlint": "3.0.0", | ||||
|         "grunt-mkdir": "~1.1.0", | ||||
|         "grunt-npm-command": "~0.1.2", | ||||
|         "grunt-sass": "~3.1.0", | ||||
| @@ -109,11 +110,11 @@ | ||||
|         "jquery-i18next": "1.2.1", | ||||
|         "jsdoc-nr-template": "github:node-red/jsdoc-nr-template", | ||||
|         "marked": "4.3.0", | ||||
|         "mermaid": "^10.4.0", | ||||
|         "mermaid": "11.3.0", | ||||
|         "minami": "1.2.3", | ||||
|         "mocha": "9.2.2", | ||||
|         "node-red-node-test-helper": "^0.3.2", | ||||
|         "nodemon": "2.0.20", | ||||
|         "node-red-node-test-helper": "^0.3.3", | ||||
|         "nodemon": "3.1.7", | ||||
|         "proxy": "^1.0.2", | ||||
|         "sass": "1.62.1", | ||||
|         "should": "13.2.3", | ||||
| @@ -122,6 +123,6 @@ | ||||
|         "supertest": "6.3.3" | ||||
|     }, | ||||
|     "engines": { | ||||
|         "node": ">=14" | ||||
|         "node": ">=18.5" | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -33,6 +33,9 @@ module.exports = { | ||||
|             store: req.query['store'], | ||||
|             req: apiUtils.getRequestLogObject(req) | ||||
|         } | ||||
|         if (req.query['keysOnly'] !== undefined) { | ||||
|             opts.keysOnly = true | ||||
|         } | ||||
|         runtimeAPI.context.getValue(opts).then(function(result) { | ||||
|             res.json(result); | ||||
|         }).catch(function(err) { | ||||
|   | ||||
| @@ -91,6 +91,7 @@ module.exports = { | ||||
|         // Plugins | ||||
|         adminApp.get("/plugins", needsPermission("plugins.read"), plugins.getAll, apiUtil.errorHandler); | ||||
|         adminApp.get("/plugins/messages", needsPermission("plugins.read"), plugins.getCatalogs, apiUtil.errorHandler); | ||||
|         adminApp.get(/^\/plugins\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,needsPermission("plugins.read"),plugins.getConfig,apiUtil.errorHandler); | ||||
|  | ||||
|         adminApp.get("/diagnostics", needsPermission("diagnostics.read"), diagnostics.getReport, apiUtil.errorHandler); | ||||
|  | ||||
|   | ||||
| @@ -40,5 +40,31 @@ module.exports = { | ||||
|             console.log(err.stack); | ||||
|             apiUtils.rejectHandler(req,res,err); | ||||
|         }) | ||||
|     }, | ||||
|     getConfig: function(req, res) { | ||||
|  | ||||
|         let opts = { | ||||
|             user: req.user, | ||||
|             module: req.params[0], | ||||
|             req: apiUtils.getRequestLogObject(req) | ||||
|         } | ||||
|  | ||||
|         if (req.get("accept") === "application/json") { | ||||
|             runtimeAPI.nodes.getNodeInfo(opts.module).then(function(result) { | ||||
|                 res.send(result); | ||||
|             }).catch(function(err) { | ||||
|                 apiUtils.rejectHandler(req,res,err); | ||||
|             }) | ||||
|         } else { | ||||
|             opts.lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages()); | ||||
|             if (/[^0-9a-z=\-\*]/i.test(opts.lang)) { | ||||
|                 opts.lang = "en-US"; | ||||
|             } | ||||
|             runtimeAPI.plugins.getPluginConfig(opts).then(function(result) { | ||||
|                 return res.send(result); | ||||
|             }).catch(function(err) { | ||||
|                 apiUtils.rejectHandler(req,res,err); | ||||
|             }) | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -126,6 +126,14 @@ async function login(req,res) { | ||||
|         if (themeContext.login && themeContext.login.image) { | ||||
|             response.image = themeContext.login.image; | ||||
|         } | ||||
|         if (themeContext.login?.message) { | ||||
|             response.loginMessage = themeContext.login?.message | ||||
|         } | ||||
|         if (themeContext.login?.button) { | ||||
|             response.prompts = [ | ||||
|                 { type: "button", ...themeContext.login.button } | ||||
|             ] | ||||
|         } | ||||
|     } | ||||
|     res.json(response); | ||||
| } | ||||
| @@ -160,20 +168,34 @@ function completeVerify(profile,done) { | ||||
|  | ||||
|  | ||||
| function genericStrategy(adminApp,strategy) { | ||||
|     var crypto = require("crypto") | ||||
|     var session = require('express-session') | ||||
|     var MemoryStore = require('memorystore')(session) | ||||
|     const crypto = require("crypto") | ||||
|     const session = require('express-session') | ||||
|     const MemoryStore = require('memorystore')(session) | ||||
|  | ||||
|     adminApp.use(session({ | ||||
|       // As the session is only used across the life-span of an auth | ||||
|       // hand-shake, we can use a instance specific random string | ||||
|       secret: crypto.randomBytes(20).toString('hex'), | ||||
|       resave: false, | ||||
|       saveUninitialized: false, | ||||
|       store: new MemoryStore({ | ||||
|         checkPeriod: 86400000 // prune expired entries every 24h | ||||
|       }) | ||||
|     })); | ||||
|     const sessionOptions = { | ||||
|         // As the session is only used across the life-span of an auth | ||||
|         // hand-shake, we can use a instance specific random string | ||||
|         secret: crypto.randomBytes(20).toString('hex'), | ||||
|         resave: false, | ||||
|         saveUninitialized: false, | ||||
|         store: new MemoryStore({ | ||||
|           checkPeriod: 86400000 // prune expired entries every 24h | ||||
|         }) | ||||
|     } | ||||
|     if (settings.httpAdminCookieOptions) { | ||||
|         sessionOptions.cookie = { | ||||
|             path: '/', | ||||
|             httpOnly: true, | ||||
|             secure: false, | ||||
|             maxAge: null, | ||||
|             ...settings.httpAdminCookieOptions | ||||
|         } | ||||
|         if (sessionOptions.cookie.name){ | ||||
|             sessionOptions.name = sessionOptions.cookie.name | ||||
|             delete sessionOptions.cookie.name | ||||
|         } | ||||
|     } | ||||
|     adminApp.use(session(sessionOptions)); | ||||
|     //TODO: all passport references ought to be in ./auth | ||||
|     adminApp.use(passport.initialize()); | ||||
|     adminApp.use(passport.session()); | ||||
| @@ -205,11 +227,12 @@ function genericStrategy(adminApp,strategy) { | ||||
|     passport.use(new strategy.strategy(options, verify)); | ||||
|  | ||||
|     adminApp.get('/auth/strategy', | ||||
|         passport.authenticate(strategy.name, {session:false, | ||||
|             failureMessage: true, | ||||
|             failureRedirect: settings.httpAdminRoot | ||||
|         passport.authenticate(strategy.name, { | ||||
|             session:false, | ||||
|             failWithError: true, | ||||
|             failureMessage: true | ||||
|         }), | ||||
|         completeGenerateStrategyAuth, | ||||
|         completeGenericStrategyAuth, | ||||
|         handleStrategyError | ||||
|     ); | ||||
|  | ||||
| @@ -221,14 +244,14 @@ function genericStrategy(adminApp,strategy) { | ||||
|         passport.authenticate(strategy.name, { | ||||
|             session:false, | ||||
|             failureMessage: true, | ||||
|             failureRedirect: settings.httpAdminRoot | ||||
|             failWithError: true | ||||
|         }), | ||||
|         completeGenerateStrategyAuth, | ||||
|         completeGenericStrategyAuth, | ||||
|         handleStrategyError | ||||
|     ); | ||||
|  | ||||
| } | ||||
| function completeGenerateStrategyAuth(req,res) { | ||||
| function completeGenericStrategyAuth(req,res) { | ||||
|     var tokens = req.user.tokens; | ||||
|     delete req.user.tokens; | ||||
|     // Successful authentication, redirect home. | ||||
| @@ -238,6 +261,8 @@ function handleStrategyError(err, req, res, next) { | ||||
|     if (res.headersSent) { | ||||
|         return next(err) | ||||
|     } | ||||
|     // Remove the header that passport auto-adds as we don't need it | ||||
|     res.removeHeader('WWW-Authenticate') | ||||
|     log.audit({event: "auth.login.fail.oauth",error:err.toString()}); | ||||
|     res.redirect(settings.httpAdminRoot + '?session_message='+err.toString()); | ||||
| } | ||||
|   | ||||
| @@ -25,7 +25,7 @@ function hasPermission(userScope,permission) { | ||||
|     } | ||||
|     var i; | ||||
|  | ||||
|     if (util.isArray(permission)) { | ||||
|     if (Array.isArray(permission)) { | ||||
|         // Multiple permissions requested - check each one | ||||
|         for (i=0;i<permission.length;i++) { | ||||
|             if (!hasPermission(userScope,permission[i])) { | ||||
| @@ -36,7 +36,7 @@ function hasPermission(userScope,permission) { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     if (util.isArray(userScope)) { | ||||
|     if (Array.isArray(userScope)) { | ||||
|         if (userScope.length === 0) { | ||||
|             return false; | ||||
|         } | ||||
|   | ||||
| @@ -17,7 +17,7 @@ | ||||
| var util = require("util"); | ||||
| var clone = require("clone"); | ||||
| var bcrypt; | ||||
| try { bcrypt = require('bcrypt'); } | ||||
| try { bcrypt = require('@node-rs/bcrypt'); } | ||||
| catch(e) { bcrypt = require('bcryptjs'); } | ||||
| var users = {}; | ||||
| var defaultUser = null; | ||||
| @@ -33,11 +33,11 @@ function authenticate() { | ||||
|             if (args.length === 2) { | ||||
|                 // Username/password authentication | ||||
|                 var password = args[1]; | ||||
|                 return new Promise(function(resolve,reject) { | ||||
|                     bcrypt.compare(password, user.password, function(err, res) { | ||||
|                         resolve(res?cleanUser(user):null); | ||||
|                     }); | ||||
|                 }); | ||||
|                 return bcrypt.compare(password, user.password).then(res => { | ||||
|                     return res ? cleanUser(user) : null | ||||
|                 }).catch(err => { | ||||
|                     return null | ||||
|                 }) | ||||
|             } else { | ||||
|                 // Try to extract common profile information | ||||
|                 if (args[0].hasOwnProperty('photos') && args[0].photos.length > 0) { | ||||
| @@ -74,7 +74,7 @@ function init(config) { | ||||
|             } else { | ||||
|                 var us = config.users; | ||||
|                 /* istanbul ignore else */ | ||||
|                 if (!util.isArray(us)) { | ||||
|                 if (!Array.isArray(us)) { | ||||
|                     us = [us]; | ||||
|                 } | ||||
|                 for (var i=0;i<us.length;i++) { | ||||
|   | ||||
| @@ -77,6 +77,53 @@ function CommsConnection(ws, user) { | ||||
|         log.trace("comms.close "+self.session); | ||||
|         removeActiveConnection(self); | ||||
|     }); | ||||
|  | ||||
|     const handleAuthPacket = function(msg) { | ||||
|         Tokens.get(msg.auth).then(function(client) { | ||||
|             if (client) { | ||||
|                 Users.get(client.user).then(function(user) { | ||||
|                     if (user) { | ||||
|                         self.user = user; | ||||
|                         log.audit({event: "comms.auth",user:self.user}); | ||||
|                         completeConnection(msg, client.scope,msg.auth,true); | ||||
|                     } else { | ||||
|                         log.audit({event: "comms.auth.fail"}); | ||||
|                         completeConnection(msg, null,null,false); | ||||
|                     } | ||||
|                 }); | ||||
|             } else { | ||||
|                 Users.tokens(msg.auth).then(function(user) { | ||||
|                     if (user) { | ||||
|                         self.user = user; | ||||
|                         log.audit({event: "comms.auth",user:self.user}); | ||||
|                         completeConnection(msg, user.permissions,msg.auth,true); | ||||
|                     } else { | ||||
|                         log.audit({event: "comms.auth.fail"}); | ||||
|                         completeConnection(msg, null,null,false); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|     const completeConnection = function(msg, userScope, session, sendAck) { | ||||
|         try { | ||||
|             if (!userScope || !Permissions.hasPermission(userScope,"status.read")) { | ||||
|                 ws.send(JSON.stringify({auth:"fail"})); | ||||
|                 ws.close(); | ||||
|             } else { | ||||
|                 pendingAuth = false; | ||||
|                 addActiveConnection(self); | ||||
|                 self.token = msg.auth; | ||||
|                 if (sendAck) { | ||||
|                     ws.send(JSON.stringify({auth:"ok"})); | ||||
|                 } | ||||
|             } | ||||
|         } catch(err) { | ||||
|             console.log(err.stack); | ||||
|             // Just in case the socket closes before we attempt | ||||
|             // to send anything. | ||||
|         } | ||||
|     } | ||||
|     ws.on('message', function(data,flags) { | ||||
|         var msg = null; | ||||
|         try { | ||||
| @@ -86,68 +133,34 @@ function CommsConnection(ws, user) { | ||||
|             return; | ||||
|         } | ||||
|         if (!pendingAuth) { | ||||
|             if (msg.subscribe) { | ||||
|             if (msg.auth) { | ||||
|                 handleAuthPacket(msg) | ||||
|             } else if (msg.subscribe) { | ||||
|                 self.subscribe(msg.subscribe); | ||||
|                 // handleRemoteSubscription(ws,msg.subscribe); | ||||
|             } else if (msg.topic) { | ||||
|                 runtimeAPI.comms.receive({ | ||||
|                     user: self.user, | ||||
|                     client: self, | ||||
|                     topic: msg.topic, | ||||
|                     data: msg.data | ||||
|                 }) | ||||
|             } | ||||
|         } else { | ||||
|             var completeConnection = function(userScope,session,sendAck) { | ||||
|                 try { | ||||
|                     if (!userScope || !Permissions.hasPermission(userScope,"status.read")) { | ||||
|                         ws.send(JSON.stringify({auth:"fail"})); | ||||
|                         ws.close(); | ||||
|                     } else { | ||||
|                         pendingAuth = false; | ||||
|                         addActiveConnection(self); | ||||
|                         self.token = msg.auth; | ||||
|                         if (sendAck) { | ||||
|                             ws.send(JSON.stringify({auth:"ok"})); | ||||
|                         } | ||||
|                     } | ||||
|                 } catch(err) { | ||||
|                     console.log(err.stack); | ||||
|                     // Just in case the socket closes before we attempt | ||||
|                     // to send anything. | ||||
|                 } | ||||
|             } | ||||
|             if (msg.auth) { | ||||
|                 Tokens.get(msg.auth).then(function(client) { | ||||
|                     if (client) { | ||||
|                         Users.get(client.user).then(function(user) { | ||||
|                             if (user) { | ||||
|                                 self.user = user; | ||||
|                                 log.audit({event: "comms.auth",user:self.user}); | ||||
|                                 completeConnection(client.scope,msg.auth,true); | ||||
|                             } else { | ||||
|                                 log.audit({event: "comms.auth.fail"}); | ||||
|                                 completeConnection(null,null,false); | ||||
|                             } | ||||
|                         }); | ||||
|                     } else { | ||||
|                         Users.tokens(msg.auth).then(function(user) { | ||||
|                             if (user) { | ||||
|                                 self.user = user; | ||||
|                                 log.audit({event: "comms.auth",user:self.user}); | ||||
|                                 completeConnection(user.permissions,msg.auth,true); | ||||
|                             } else { | ||||
|                                 log.audit({event: "comms.auth.fail"}); | ||||
|                                 completeConnection(null,null,false); | ||||
|                             } | ||||
|                         }); | ||||
|                     } | ||||
|                 }); | ||||
|                 handleAuthPacket(msg) | ||||
|             } else { | ||||
|                 if (anonymousUser) { | ||||
|                     log.audit({event: "comms.auth",user:anonymousUser}); | ||||
|                     self.user = anonymousUser; | ||||
|                     completeConnection(anonymousUser.permissions,null,false); | ||||
|                     completeConnection(msg, anonymousUser.permissions, null, false); | ||||
|                     //TODO: duplicated code - pull non-auth message handling out | ||||
|                     if (msg.subscribe) { | ||||
|                         self.subscribe(msg.subscribe); | ||||
|                     } | ||||
|                 } else { | ||||
|                     log.audit({event: "comms.auth.fail"}); | ||||
|                     completeConnection(null,null,false); | ||||
|                     completeConnection(msg, null,null,false); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -70,7 +70,7 @@ function serveFilesFromTheme(themeValue, themeApp, directory, baseDirectory) { | ||||
|     var result = []; | ||||
|     if (themeValue) { | ||||
|         var array = themeValue; | ||||
|         if (!util.isArray(array)) { | ||||
|         if (!Array.isArray(array)) { | ||||
|             array = [array]; | ||||
|         } | ||||
|  | ||||
| @@ -206,14 +206,26 @@ module.exports = { | ||||
|         } | ||||
|  | ||||
|         if (theme.login) { | ||||
|             let themeContextLogin = {} | ||||
|             let hasLoginTheme = false | ||||
|             if (theme.login.image) { | ||||
|                 url = serveFile(themeApp,"/login/",theme.login.image); | ||||
|                 if (url) { | ||||
|                     themeContext.login = { | ||||
|                         image: url | ||||
|                     } | ||||
|                     themeContextLogin.image = url | ||||
|                     hasLoginTheme = true | ||||
|                 } | ||||
|             } | ||||
|             if (theme.login.message) { | ||||
|                 themeContextLogin.message = theme.login.message | ||||
|                 hasLoginTheme = true | ||||
|             } | ||||
|             if (theme.login.button) { | ||||
|                 themeContextLogin.button = theme.login.button | ||||
|                 hasLoginTheme = true | ||||
|             } | ||||
|             if (hasLoginTheme) { | ||||
|                 themeContext.login = themeContextLogin | ||||
|             } | ||||
|         } | ||||
|         themeApp.get("/", async function(req,res) { | ||||
|             const themePluginList = await runtimeAPI.plugins.getPluginsByType({type:"node-red-theme"}); | ||||
| @@ -233,6 +245,10 @@ module.exports = { | ||||
|             themeSettings.projects = theme.projects; | ||||
|         } | ||||
|  | ||||
|         if (theme.hasOwnProperty("multiplayer")) { | ||||
|             themeSettings.multiplayer = theme.multiplayer; | ||||
|         } | ||||
|  | ||||
|         if (theme.hasOwnProperty("keymap")) { | ||||
|             themeSettings.keymap = theme.keymap; | ||||
|         } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "@node-red/editor-api", | ||||
|     "version": "3.1.6", | ||||
|     "version": "4.0.8", | ||||
|     "license": "Apache-2.0", | ||||
|     "main": "./lib/index.js", | ||||
|     "repository": { | ||||
| @@ -16,25 +16,25 @@ | ||||
|         } | ||||
|     ], | ||||
|     "dependencies": { | ||||
|         "@node-red/util": "3.1.6", | ||||
|         "@node-red/editor-client": "3.1.6", | ||||
|         "@node-red/util": "4.0.8", | ||||
|         "@node-red/editor-client": "4.0.8", | ||||
|         "bcryptjs": "2.4.3", | ||||
|         "body-parser": "1.20.2", | ||||
|         "body-parser": "1.20.3", | ||||
|         "clone": "2.1.2", | ||||
|         "cors": "2.8.5", | ||||
|         "express-session": "1.17.3", | ||||
|         "express": "4.18.2", | ||||
|         "express-session": "1.18.1", | ||||
|         "express": "4.21.2", | ||||
|         "memorystore": "1.6.7", | ||||
|         "mime": "3.0.0", | ||||
|         "multer": "1.4.5-lts.1", | ||||
|         "mustache": "4.2.0", | ||||
|         "oauth2orize": "1.11.1", | ||||
|         "oauth2orize": "1.12.0", | ||||
|         "passport-http-bearer": "1.0.1", | ||||
|         "passport-oauth2-client-password": "0.1.2", | ||||
|         "passport": "0.6.0", | ||||
|         "ws": "7.5.6" | ||||
|         "passport": "0.7.0", | ||||
|         "ws": "7.5.10" | ||||
|     }, | ||||
|     "optionalDependencies": { | ||||
|         "bcrypt": "5.1.0" | ||||
|         "@node-rs/bcrypt": "1.10.4" | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -590,6 +590,8 @@ | ||||
|             }, | ||||
|             "nodeCount": "__label__ Node", | ||||
|             "nodeCount_plural": "__label__ Nodes", | ||||
|             "pluginCount": "__count__ Plugin", | ||||
|             "pluginCount_plural": "__count__ Plugins",     | ||||
|             "moduleCount": "__count__ Modul verfügbar", | ||||
|             "moduleCount_plural": "__count__ Module verfügbar", | ||||
|             "inuse": "In Gebrauch", | ||||
|   | ||||
| @@ -27,7 +27,8 @@ | ||||
|             "lock": "Lock", | ||||
|             "unlock": "Unlock", | ||||
|             "locked": "Locked", | ||||
|             "unlocked": "Unlocked" | ||||
|             "unlocked": "Unlocked", | ||||
|             "format": "Format" | ||||
|         }, | ||||
|         "type": { | ||||
|             "string": "string", | ||||
| @@ -372,8 +373,12 @@ | ||||
|             "deleted": "deleted", | ||||
|             "flowDeleted": "flow deleted", | ||||
|             "flowAdded": "flow added", | ||||
|             "moved": "moved", | ||||
|             "movedTo": "moved to __id__", | ||||
|             "movedFrom": "moved from __id__" | ||||
|             "movedFrom": "moved from __id__", | ||||
|             "none": "none", | ||||
|             "position": "position", | ||||
|             "wires": "wires" | ||||
|         }, | ||||
|         "nodeCount": "__count__ node", | ||||
|         "nodeCount_plural": "__count__ nodes", | ||||
| @@ -382,9 +387,14 @@ | ||||
|         "reviewChanges": "Review Changes", | ||||
|         "noBinaryFileShowed": "Cannot show binary file contents", | ||||
|         "viewCommitDiff": "View Commit Changes", | ||||
|         "commit": "Commit", | ||||
|         "compareChanges": "Compare Changes", | ||||
|         "saveConflict": "Save conflict resolution", | ||||
|         "conflictHeader": "<span>__resolved__</span> of <span>__unresolved__</span> conflicts resolved", | ||||
|         "localChanges": "Local Changes", | ||||
|         "remoteChanges": "Remote Changes", | ||||
|         "useLocalChanges": "use local changes", | ||||
|         "useRemoteChanges": "use remote changes", | ||||
|         "commonVersionError": "Common Version doesn't contain valid JSON:", | ||||
|         "oldVersionError": "Old Version doesn't contain valid JSON:", | ||||
|         "newVersionError": "New Version doesn't contain valid JSON:" | ||||
| @@ -552,7 +562,9 @@ | ||||
|         "types": { | ||||
|             "local": "Local", | ||||
|             "examples": "Examples" | ||||
|         } | ||||
|         }, | ||||
|         "type": "Type", | ||||
|         "name": "Name" | ||||
|     }, | ||||
|     "palette": { | ||||
|         "noInfo": "no information available", | ||||
| @@ -614,6 +626,8 @@ | ||||
|             }, | ||||
|             "nodeCount": "__label__ node", | ||||
|             "nodeCount_plural": "__label__ nodes", | ||||
|             "pluginCount": "__count__ plugin", | ||||
|             "pluginCount_plural": "__count__ plugins", | ||||
|             "moduleCount": "__count__ module available", | ||||
|             "moduleCount_plural": "__count__ modules available", | ||||
|             "inuse": "in use", | ||||
| @@ -641,6 +655,7 @@ | ||||
|             "errors": { | ||||
|                 "catalogLoadFailed": "<p>Failed to load node catalogue.</p><p>Check the browser console for more information</p>", | ||||
|                 "installFailed": "<p>Failed to install: __module__</p><p>__message__</p><p>Check the log for more information</p>", | ||||
|                 "installTimeout": "<p>Install continuing the background.</p><p>Nodes will appear in palette when complete. Check the log for more information.</p>", | ||||
|                 "removeFailed": "<p>Failed to remove: __module__</p><p>__message__</p><p>Check the log for more information</p>", | ||||
|                 "updateFailed": "<p>Failed to update: __module__</p><p>__message__</p><p>Check the log for more information</p>", | ||||
|                 "enableFailed": "<p>Failed to enable: __module__</p><p>__message__</p><p>Check the log for more information</p>", | ||||
| @@ -655,6 +670,9 @@ | ||||
|                     "body": "<p>Removing '__module__'</p><p>Removing the node will uninstall it from Node-RED. The node may continue to use resources until Node-RED is restarted.</p>", | ||||
|                     "title": "Remove nodes" | ||||
|                 }, | ||||
|                 "removePlugin": { | ||||
|                     "body": "<p>Removed plugin __module__. Please reload the editor to clear left-overs.</p>" | ||||
|                 }, | ||||
|                 "update": { | ||||
|                     "body": "<p>Updating '__module__'</p><p>Updating the node will require a restart of Node-RED to complete the update. This must be done manually.</p>", | ||||
|                     "title": "Update nodes" | ||||
| @@ -666,7 +684,8 @@ | ||||
|                     "review": "Open node information", | ||||
|                     "install": "Install", | ||||
|                     "remove": "Remove", | ||||
|                     "update": "Update" | ||||
|                     "update": "Update", | ||||
|                     "understood": "Understood" | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| @@ -719,6 +738,7 @@ | ||||
|             "nodeHelp": "Node Help", | ||||
|             "showHelp": "Show help", | ||||
|             "showInOutline": "Show in outline", | ||||
|             "hideTopics": "Hide topics", | ||||
|             "showTopics": "Show topics", | ||||
|             "noHelp": "No help topic selected", | ||||
|             "changeLog": "Change Log" | ||||
| @@ -793,6 +813,7 @@ | ||||
|                 "branches": "Branches", | ||||
|                 "noBranches": "No branches", | ||||
|                 "deleteConfirm": "Are you sure you want to delete the local branch '__name__'? This cannot be undone.", | ||||
|                 "deleteBranch": "Delete branch", | ||||
|                 "unmergedConfirm": "The local branch '__name__' has unmerged changes that will be lost. Are you sure you want to delete it?", | ||||
|                 "deleteUnmergedBranch": "Delete unmerged branch", | ||||
|                 "gitRemotes": "Git remotes", | ||||
| @@ -914,6 +935,8 @@ | ||||
|         } | ||||
|     }, | ||||
|     "typedInput": { | ||||
|         "selected": "__count__ selected", | ||||
|         "selected_plural": "__count__ selected", | ||||
|         "type": { | ||||
|             "str": "string", | ||||
|             "num": "number", | ||||
| @@ -924,7 +947,14 @@ | ||||
|             "date": "timestamp", | ||||
|             "jsonata": "expression", | ||||
|             "env": "env variable", | ||||
|             "cred": "credential" | ||||
|             "cred": "credential", | ||||
|             "conf-types": "config node" | ||||
|         }, | ||||
|         "date": { | ||||
|             "format": { | ||||
|                 "timestamp": "milliseconds since epoch", | ||||
|                 "object": "JavaScript Date Object" | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "editableList": { | ||||
|   | ||||
| @@ -27,7 +27,8 @@ | ||||
|             "lock": "Bloquear", | ||||
|             "unlock": "Desbloquear", | ||||
|             "locked": "Bloqueado", | ||||
|             "unlocked": "Desbloqueado" | ||||
|             "unlocked": "Desbloqueado", | ||||
|             "format": "Formato" | ||||
|         }, | ||||
|         "type": { | ||||
|             "string": "texto", | ||||
| @@ -303,7 +304,8 @@ | ||||
|                 "missingType": "La entrada no es un flujo válido - elemento __index__ falta la propiedad 'type'" | ||||
|             }, | ||||
|             "conflictNotification1": "Algunos de los nodos que estás importando ya existen en tu espacio de trabajo.", | ||||
|             "conflictNotification2": "Selecciona qué nodos importar y si reemplazar los nodos existentes o importar una copia de los mismos." | ||||
|             "conflictNotification2": "Selecciona qué nodos importar y si reemplazar los nodos existentes o importar una copia de los mismos.", | ||||
|             "alreadyExists": "Este nodo ya existe" | ||||
|         }, | ||||
|         "copyMessagePath": "Ruta copiada", | ||||
|         "copyMessageValue": "Valor copiado", | ||||
| @@ -371,8 +373,12 @@ | ||||
|             "deleted": "eliminado", | ||||
|             "flowDeleted": "flujo eliminado", | ||||
|             "flowAdded": "flujo añadido", | ||||
|             "moved": "movido", | ||||
|             "movedTo": "movido a __id__", | ||||
|             "movedFrom": "movido desde __id__" | ||||
|             "movedFrom": "movido desde __id__", | ||||
|             "none": "ninguno", | ||||
|             "position": "posición", | ||||
|             "wires": "conectores" | ||||
|         }, | ||||
|         "nodeCount": "__count__ nodo", | ||||
|         "nodeCount_plural": "__count__ nodos", | ||||
| @@ -381,9 +387,14 @@ | ||||
|         "reviewChanges": "Revisar Cambios", | ||||
|         "noBinaryFileShowed": "No se puede mostrar el contenido del archivo binario", | ||||
|         "viewCommitDiff": "Ver cambios de commit", | ||||
|         "commit": "Commit", | ||||
|         "compareChanges": "Comparar Cambios", | ||||
|         "saveConflict": "Guardar resolución de conflictos", | ||||
|         "conflictHeader": "<span>__resolved__</span> de <span>__unresolved__</span> conflictos resueltos", | ||||
|         "localChanges": "Cambios Locales", | ||||
|         "remoteChanges": "Cambios Remotos", | ||||
|         "useLocalChanges": "utilizar cambios locales", | ||||
|         "useRemoteChanges": "utilizar cambios remotos", | ||||
|         "commonVersionError": "La versión común no contiene JSON válido:", | ||||
|         "oldVersionError": "La versión anterior no contiene JSON válido:", | ||||
|         "newVersionError": "La versión nueva no contiene JSON válido:" | ||||
| @@ -551,7 +562,9 @@ | ||||
|         "types": { | ||||
|             "local": "Local", | ||||
|             "examples": "Ejemplos" | ||||
|         } | ||||
|         }, | ||||
|         "type": "Tipo", | ||||
|         "name": "Nombre" | ||||
|     }, | ||||
|     "palette": { | ||||
|         "noInfo": "no hay información disponible", | ||||
| @@ -613,6 +626,8 @@ | ||||
|             }, | ||||
|             "nodeCount": "__label__ nodo", | ||||
|             "nodeCount_plural": "__label__ nodos", | ||||
|             "pluginCount": "__count__ extensión", | ||||
|             "pluginCount_plural": "__count__ extensiones", | ||||
|             "moduleCount": "__count__ módulo disponible", | ||||
|             "moduleCount_plural": "__count__ módulos disponibles", | ||||
|             "inuse": "en uso", | ||||
| @@ -640,6 +655,7 @@ | ||||
|             "errors": { | ||||
|                 "catalogLoadFailed": "<p>La carga del catálogo de nodos ha fallado</p><p>Revise la consola del navegador para mas información</p>", | ||||
|                 "installFailed": "<p>Fallo al instalar: __module__</p><p>__message__</p><p>Revise el log para mas información</p>", | ||||
|                 "installTimeout": "<p>La instalación continúa en segundo plano.</p><p>Los nodos aparecerán en la paleta cuando finalice. Consulta el registro para obtener más información.</p>", | ||||
|                 "removeFailed": "<p>Fallo al eliminar: __module__</p><p>__message__</p><p>Revise el log para mas información</p>", | ||||
|                 "updateFailed": "<p>Fallo al actualizar: __module__</p><p>__message__</p><p>Revise el log para mas información</p>", | ||||
|                 "enableFailed": "<p>Fallo al activar: __module__</p><p>__message__</p><p>Revise el log para mas información</p>", | ||||
| @@ -654,6 +670,9 @@ | ||||
|                     "body":"<p>Eliminando '__module__'</p><p>La eliminación del nodo lo desinstalará de Node-RED. Es posible que el nodo siga utilizando recursos hasta que Node-RED sea reiniciado.</p>", | ||||
|                     "title": "Eliminar nodos" | ||||
|                 }, | ||||
|                 "removePlugin": { | ||||
|                     "body": "<p>Extensión __module__ eliminada. Vuelve a cargar el editor para borrar los elementos sobrantes.</p>" | ||||
|                 }, | ||||
|                 "update": { | ||||
|                     "body":"<p>Actualizando '__module__'</p><p>La actualización del nodo requerirá un reinicio manual de Node-RED para completarse. Debe ser reiniciado manualmente.</p>", | ||||
|                     "title": "Actualizar nodos" | ||||
| @@ -665,7 +684,8 @@ | ||||
|                     "review": "Abrir información del nodo", | ||||
|                     "install": "Instalar", | ||||
|                     "remove": "Eliminar", | ||||
|                     "update": "Actualizar" | ||||
|                     "update": "Actualizar", | ||||
|                     "understood": "Entendido" | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| @@ -718,6 +738,7 @@ | ||||
|             "nodeHelp": "Ayuda de nodo", | ||||
|             "showHelp": "Mostrar ayuda", | ||||
|             "showInOutline": "Mostrar en controno", | ||||
|             "hideTopics": "Esconder temas", | ||||
|             "showTopics": "Mostrar temas", | ||||
|             "noHelp": "No hay ningun tema de ayuda seleccionado", | ||||
|             "changeLog": "Registro de Cambios" | ||||
| @@ -792,6 +813,7 @@ | ||||
|                 "branches": "Ramas", | ||||
|                 "noBranches": "Sin ramas", | ||||
|                 "deleteConfirm": "¿Estás seguro de que quieres eliminar la rama local '__name__'? Esta acción no puede deshacerse.", | ||||
|                 "deleteBranch": "Eliminar rama", | ||||
|                 "unmergedConfirm": "La rama local '__name__' tiene cambios no fusionados que se perderán. ¿Estás seguro de que quieres eliminarla?", | ||||
|                 "deleteUnmergedBranch": "Eliminar rama no fusionada", | ||||
|                 "gitRemotes": "Git remotes", | ||||
| @@ -913,6 +935,8 @@ | ||||
|         } | ||||
|     }, | ||||
|     "typedInput": { | ||||
|         "selected": "__count__ seleccionado", | ||||
|         "selected_plural": "__count__ seleccionados", | ||||
|         "type": { | ||||
|             "str": "texto", | ||||
|             "num": "número", | ||||
| @@ -923,7 +947,14 @@ | ||||
|             "date": "marca tiempo", | ||||
|             "jsonata": "expresión", | ||||
|             "env": "variable de entorno", | ||||
|             "cred": "credencial" | ||||
|             "cred": "credencial", | ||||
|             "conf-types": "nodo configuración" | ||||
|         }, | ||||
|         "date": { | ||||
|             "format": { | ||||
|                 "timestamp": "milisegundos desde epoch", | ||||
|                 "object": "Objeto de fecha de JavaScript" | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "editableList": { | ||||
| @@ -1205,6 +1236,18 @@ | ||||
|     "diagnostics": { | ||||
|         "title": "Información Sistema" | ||||
|     }, | ||||
|     "languages": { | ||||
|         "de": "Deutsch", | ||||
|         "en-US": "English", | ||||
|         "es-ES": "Español (España)", | ||||
|         "fr": "Français", | ||||
|         "ja": "日本語", | ||||
|         "ko": "Korean", | ||||
|         "pt-BR": "Português (Brasil)", | ||||
|         "ru": "Русский", | ||||
|         "zh-CN": "简体中文", | ||||
|         "zh-TW": "繁體中文" | ||||
|     }, | ||||
|     "validator": { | ||||
|         "errors": { | ||||
|             "invalid-json": "Datos JSON inválidos: __error__", | ||||
|   | ||||
| @@ -27,7 +27,8 @@ | ||||
|       "lock": "Verrouiller", | ||||
|       "unlock": "Déverrouiller", | ||||
|       "locked": "Verrouillé", | ||||
|       "unlocked": "Déverrouillé" | ||||
|       "unlocked": "Déverrouillé", | ||||
|       "format": "Format" | ||||
|     }, | ||||
|     "type": { | ||||
|       "string": "chaîne de caractères", | ||||
| @@ -54,10 +55,10 @@ | ||||
|   "workspace": { | ||||
|     "defaultName": "Flux __number__", | ||||
|     "editFlow": "Modifier le flux : __name__", | ||||
|     "confirmDelete": "Confirmation de la suppression", | ||||
|     "delete": "Etes-vous sûr de vouloir supprimer '__label__'?", | ||||
|     "dropFlowHere": "Déposer le flux ici", | ||||
|     "dropImageHere": "Déposer l'image ici", | ||||
|     "confirmDelete": "Confirmer la suppression", | ||||
|     "delete": "Êtes-vous sûr de vouloir supprimer '__label__' ?", | ||||
|     "dropFlowHere": "Lâchez le flux ici", | ||||
|     "dropImageHere": "Lâchez l'image ici", | ||||
|     "addFlow": "Ajouter un flux", | ||||
|     "addFlowToRight": "Ajouter un flux à droite", | ||||
|     "closeFlow": "Fermer le flux", | ||||
| @@ -74,7 +75,7 @@ | ||||
|     "enabled": "Activé", | ||||
|     "disabled": "Désactivé", | ||||
|     "info": "Description", | ||||
|     "selectNodes": "Cliquer sur les noeuds pour sélectionner", | ||||
|     "selectNodes": "Cliquer pour sélectionner", | ||||
|     "enableFlow": "Activer le flux", | ||||
|     "disableFlow": "Désactiver le flux", | ||||
|     "lockFlow": "Verrouiller le flux", | ||||
| @@ -98,7 +99,7 @@ | ||||
|         "rtl": "De droite à gauche", | ||||
|         "auto": "Contextuel", | ||||
|         "language": "Langue", | ||||
|         "browserDefault": "Navigateur par défaut" | ||||
|         "browserDefault": "Par défaut du Navigateur" | ||||
|       }, | ||||
|       "sidebar": { | ||||
|         "show": "Afficher la barre latérale" | ||||
| @@ -134,7 +135,7 @@ | ||||
|       "disableSelectedNodes": "Désactiver les noeuds sélectionnés", | ||||
|       "showSelectedNodeLabels": "Afficher les étiquettes des noeuds sélectionnés", | ||||
|       "hideSelectedNodeLabels": "Masquer les étiquettes des noeuds sélectionnés", | ||||
|       "showWelcomeTours": "Afficher les visites guidées pour les nouvelles versions", | ||||
|       "showWelcomeTours": "Afficher les visites guidées des nouvelles versions", | ||||
|       "help": "Site web de Node-RED", | ||||
|       "projects": "Projets", | ||||
|       "projects-new": "Nouveau projet", | ||||
| @@ -143,7 +144,7 @@ | ||||
|       "showNodeLabelDefault": "Afficher l'étiquette des noeuds nouvellement ajoutés", | ||||
|       "codeEditor": "Éditeur de code", | ||||
|       "groups": "Groupes", | ||||
|       "groupSelection": "Grouper cette sélection", | ||||
|       "groupSelection": "Grouper la sélection", | ||||
|       "ungroupSelection": "Dégrouper la sélection", | ||||
|       "groupMergeSelection": "Fusionner la sélection", | ||||
|       "groupRemoveSelection": "Supprimer du groupe", | ||||
| @@ -155,7 +156,7 @@ | ||||
|       "alignMiddle": "Aligner au milieu", | ||||
|       "alignBottom": "Aligner en bas", | ||||
|       "distributeHorizontally": "Répartir horizontalement", | ||||
|       "distributeVertically": "Distribuer verticalement", | ||||
|       "distributeVertically": "Répartir verticalement", | ||||
|       "moveToBack": "Déplacer vers l'arrière", | ||||
|       "moveToFront": "Déplacer vers l'avant", | ||||
|       "moveBackwards": "Reculer", | ||||
| @@ -163,21 +164,21 @@ | ||||
|     } | ||||
|   }, | ||||
|   "actions": { | ||||
|     "toggle-navigator": "Basculer de navigateur", | ||||
|     "zoom-out": "Dézoomer", | ||||
|     "zoom-reset": "Réinitialiser le zoom", | ||||
|     "toggle-navigator": "Basculer l'affichage du navigateur", | ||||
|     "zoom-out": "Réduire", | ||||
|     "zoom-reset": "Réinitialiser", | ||||
|     "zoom-in": "Agrandir", | ||||
|     "search-flows": "Rechercher le flux", | ||||
|     "search-prev": "Précédent", | ||||
|     "search-next": "Suivant", | ||||
|     "search-counter": "\"__term__\" __result__ de __count__" | ||||
|     "search-counter": "\"__term__\" __result__ sur __count__" | ||||
|   }, | ||||
|   "user": { | ||||
|     "loggedInAs": "Connecté en tant que __name__", | ||||
|     "username": "Nom d'utilisateur", | ||||
|     "password": "Mot de passe", | ||||
|     "login": "Connexion", | ||||
|     "loginFailed": "Échec de la connexion", | ||||
|     "login": "Se connecter", | ||||
|     "loginFailed": "Échec de connexion", | ||||
|     "notAuthorized": "Pas autorisé", | ||||
|     "errors": { | ||||
|       "settings": "Vous devez être connecté pour accéder aux paramètres", | ||||
| @@ -193,16 +194,16 @@ | ||||
|     "warning": "<strong>Attention</strong> : __message__", | ||||
|     "warnings": { | ||||
|       "undeployedChanges": "Le noeud a des modifications non déployées", | ||||
|       "nodeActionDisabled": "Actions de noeud désactivées", | ||||
|       "nodeActionDisabledSubflow": "Actions de noeud désactivées dans le sous-flux", | ||||
|       "nodeActionDisabled": "Les actions du noeud sont désactivées", | ||||
|       "nodeActionDisabledSubflow": "Les actions de noeud sont désactivées à l'intérieur du sous-flux", | ||||
|       "missing-types": "<p>Flux arrêtés en raison de types de noeuds manquants.</p>", | ||||
|       "missing-modules": "<p>Flux arrêtés en raison de modules manquants.</p>", | ||||
|       "safe-mode": "<p>Flux arrêtés en mode sans échec.</p><p>Vous pouvez modifier vos flux et déployer les changements pour redémarrer.</p>", | ||||
|       "safe-mode": "<p>Flux arrêtés en mode sans échec.</p><p>Vous pouvez modifier vos flux et déployer ensuite les changements afin de démarrer vos flux.</p>", | ||||
|       "restartRequired": "Node-RED doit être redémarré pour mettre à jour les modules", | ||||
|       "credentials_load_failed": "<p>Les flux se sont arrêtés car les informations d'identification n'ont pas pu être déchiffrées.</p><p>Le fichier d'informations d'identification du flux est chiffré, mais la clé de chiffrement du projet est manquante ou invalide.</p>", | ||||
|       "credentials_load_failed_reset": "<p>Les informations d'identification n'ont pas pu être déchiffrées</p><p>Le fichier d'informations d'identification du flux est chiffré, mais la clé de chiffrement du projet est manquante ou invalide.</p><p>Le fichier d'informations d'identification du flux sera réinitialisé lors du prochain déploiement. Toutes les informations d'identification de flux existantes seront perdues.</p>", | ||||
|       "credentials_load_failed": "<p>Les flux se sont arrêtés car les informations d'identification n'ont pas pu être déchiffrées.</p><p>Le fichier d'informations d'identification du flux est chiffré mais la clé de chiffrement du projet est manquante ou invalide.</p>", | ||||
|       "credentials_load_failed_reset": "<p>Les informations d'identification n'ont pas pu être déchiffrées</p><p>Le fichier d'informations d'identification du flux est chiffré mais la clé de chiffrement du projet est manquante ou invalide.</p><p>Le fichier d'informations d'identification du flux sera réinitialisé lors du prochain déploiement. Toutes les informations d'identification des flux existants seront perdues.</p>", | ||||
|       "missing_flow_file": "<p>Fichier contenant les flux introuvable.</p><p>Le projet n'est pas configuré avec un fichier de flux.</p>", | ||||
|       "missing_package_file": "<p>Fichier de paquetage du projet introuvable.</p><p>Il manque au projet un fichier package.json.</p>", | ||||
|       "missing_package_file": "<p>Fichier de paquetage du projet introuvable.</p><p>Il manque au projet le fichier <code>package.json</code>.</p>", | ||||
|       "project_empty": "<p>Le projet est vide.</p><p>Voulez-vous créer un ensemble de fichiers de projet par défaut ?<br/>Sinon, vous devrez ajouter manuellement des fichiers au projet (en dehors de l'éditeur).</p>", | ||||
|       "project_not_found": "<p>Le projet '__project__' est introuvable.</p>", | ||||
|       "git_merge_conflict": "<p>La fusion automatique des modifications a échoué.</p><p>Corriger les conflits non fusionnés, puis valider le résultat.</p>" | ||||
| @@ -219,7 +220,7 @@ | ||||
|     }, | ||||
|     "project": { | ||||
|       "change-branch": "Changer pour une branche locale '__project__'", | ||||
|       "merge-abort": "Git fusion abandonnée", | ||||
|       "merge-abort": "Fusion Git abandonnée", | ||||
|       "loaded": "Projet '__project__' chargé", | ||||
|       "updated": "Projet '__project__' mis à jour", | ||||
|       "pull": "Projet '__project__' rechargé", | ||||
| @@ -352,7 +353,7 @@ | ||||
|       "backgroundUpdate": "Les flux sur le serveur ont été mis à jour.", | ||||
|       "conflictChecking": "Vérifier si les modifications peuvent être fusionnées automatiquement", | ||||
|       "conflictAutoMerge": "Les modifications n'incluent aucun conflit et peuvent être fusionnées automatiquement.", | ||||
|       "conflictManualMerge": "Les changements incluent des conflits qui doivent être résolus avant de pouvoir être déployés.", | ||||
|       "conflictManualMerge": "Les modifications incluent des conflits qui doivent être résolus avant de pouvoir être déployées.", | ||||
|       "plusNMore": "+ __count__ en plus" | ||||
|     } | ||||
|   }, | ||||
| @@ -372,19 +373,28 @@ | ||||
|       "deleted": "supprimé", | ||||
|       "flowDeleted": "flux supprimé", | ||||
|       "flowAdded": "flux ajouté", | ||||
|       "moved": "déplacé", | ||||
|       "movedTo": "déplacé vers __id__", | ||||
|       "movedFrom": "déplacé depuis __id__" | ||||
|       "movedFrom": "déplacé depuis __id__", | ||||
|       "none": "aucun", | ||||
|       "position": "position", | ||||
|       "wires": "câbles" | ||||
|     }, | ||||
|     "nodeCount": "__count__ noeud", | ||||
|     "nodeCount_plural": "__count__ noeuds", | ||||
|     "local": "Changements locaux", | ||||
|     "remote": "Modifications à distance", | ||||
|     "remote": "Changements distants", | ||||
|     "reviewChanges": "Examiner les modifications", | ||||
|     "noBinaryFileShowed": "Impossible d'afficher le contenu du fichier binaire", | ||||
|     "viewCommitDiff": "Afficher les modifications de validation", | ||||
|     "viewCommitDiff": "Afficher les modifications de la validation", | ||||
|     "commit": "Validation", | ||||
|     "compareChanges": "Comparer les modifications", | ||||
|     "saveConflict": "Enregistrer la résolution des conflits", | ||||
|     "conflictHeader": "<span>__resolved__</span> sur <span>__unresolved__</span> conflit(s) résolu(s)", | ||||
|     "localChanges": "Modifications locales", | ||||
|     "remoteChanges": "Modifications distantes", | ||||
|     "useLocalChanges": "utiliser les modifications locales", | ||||
|     "useRemoteChanges": "utiliser les modifications distantes", | ||||
|     "commonVersionError": "La version commune ne contient pas de JSON valide :", | ||||
|     "oldVersionError": "L'ancienne version ne contient pas de JSON valide :", | ||||
|     "newVersionError": "La nouvelle version ne contient pas de JSON valide :" | ||||
| @@ -395,9 +405,9 @@ | ||||
|     "edit": "Modifier le modèle du sous-flux", | ||||
|     "subflowInstances": "Il existe __count__ instance de ce modèle de sous-flux", | ||||
|     "subflowInstances_plural": "Il existe __count__ instances de ce modèle de sous-flux", | ||||
|     "editSubflowProperties": "modifier les propriétés", | ||||
|     "input": "Entrées:", | ||||
|     "output": "Sorties:", | ||||
|     "editSubflowProperties": "Modifier les propriétés", | ||||
|     "input": "Entrées :", | ||||
|     "output": "Sorties :", | ||||
|     "status": "Statut du noeud", | ||||
|     "deleteSubflow": "Supprimer le sous-flux", | ||||
|     "confirmDelete": "Voulez-vous vraiment supprimer ce sous-flux ?", | ||||
| @@ -411,7 +421,7 @@ | ||||
|     "version": "Version", | ||||
|     "versionPlaceholder": "x.y.z", | ||||
|     "keys": "Mots clés", | ||||
|     "keysPlaceholder": "Mots clés séparés par des virgules", | ||||
|     "keysPlaceholder": "Mots clés séparés par une virgule", | ||||
|     "author": "Auteur", | ||||
|     "authorPlaceholder": "Votre nom <email@exemple.com>", | ||||
|     "desc": "Description", | ||||
| @@ -468,7 +478,7 @@ | ||||
|       "select": "sélection", | ||||
|       "checkbox": "case à cocher", | ||||
|       "spinner": "valeurs à défiler", | ||||
|       "none": "aucune", | ||||
|       "none": "aucun", | ||||
|       "hidden": "masquer la propriété" | ||||
|     }, | ||||
|     "types": { | ||||
| @@ -496,7 +506,7 @@ | ||||
|       "max": "Maximum" | ||||
|     }, | ||||
|     "errors": { | ||||
|       "scopeChange": "La modification de la portée la rendra indisponible pour les noeuds d'autres flux qui l'utilisent", | ||||
|       "scopeChange": "La modification de la portée rendra indisponible ce noeud de configuration aux noeuds d'autres flux qui l'utilisent", | ||||
|       "invalidProperties": "Propriétés invalides :", | ||||
|       "credentialLoadFailed": "Échec du chargement des identifiants du noeud" | ||||
|     } | ||||
| @@ -510,7 +520,7 @@ | ||||
|     "unassigned": "Non attribué", | ||||
|     "global": "Global", | ||||
|     "workspace": "Espace de travail", | ||||
|     "editor": "Boîte de dialogue d'édition", | ||||
|     "editor": "Boîte d'édition", | ||||
|     "selectAll": "Tout sélectionner", | ||||
|     "selectNone": "Ne rien sélectionner", | ||||
|     "selectAllConnected": "Sélectionner tous les éléments connectés", | ||||
| @@ -541,7 +551,7 @@ | ||||
|     "openLibrary": "Ouvrir la bibliothèque...", | ||||
|     "saveToLibrary": "Enregistrer dans la bibliothèque...", | ||||
|     "typeLibrary": "__type__ bibliothèque", | ||||
|     "unnamedType": "Innomé __type__", | ||||
|     "unnamedType": "Sans nom __type__", | ||||
|     "exportedToLibrary": "Noeuds exportés vers la bibliothèque", | ||||
|     "dialogSaveOverwrite": "Une __libraryType__ appelée __libraryName__ existe déjà. Écraser ?", | ||||
|     "invalidFilename": "Nom de fichier non valide", | ||||
| @@ -552,13 +562,15 @@ | ||||
|     "types": { | ||||
|       "local": "Local", | ||||
|       "examples": "Exemples" | ||||
|     } | ||||
|     }, | ||||
|     "type": "Type", | ||||
|     "name": "Nom" | ||||
|   }, | ||||
|   "palette": { | ||||
|     "noInfo": "Pas d'information disponible", | ||||
|     "filter": "Rechercher le noeud", | ||||
|     "search": "Rechercher les modules", | ||||
|     "addCategory": "Ajouter un nouveau...", | ||||
|     "addCategory": "Ajouter une nouvelle...", | ||||
|     "label": { | ||||
|       "subflows": "Sous-flux", | ||||
|       "network": "Réseau", | ||||
| @@ -614,6 +626,8 @@ | ||||
|       }, | ||||
|       "nodeCount": "__label__ noeud", | ||||
|       "nodeCount_plural": "__label__ noeuds", | ||||
|       "pluginCount": "__count__ plugin", | ||||
|       "pluginCount_plural": "__count__ plugins", | ||||
|       "moduleCount": "__count__ module disponible", | ||||
|       "moduleCount_plural": "__count__ modules disponibles", | ||||
|       "inuse": "En cours d'utilisation", | ||||
| @@ -636,11 +650,12 @@ | ||||
|       "sortAZ": "A-Z", | ||||
|       "sortRecent": "Récent", | ||||
|       "more": "+ __count__ en plus", | ||||
|       "upload": "Charger le fichier tgz du module", | ||||
|       "upload": "Charger le fichier .tgz du module", | ||||
|       "refresh": "Actualiser la liste des modules", | ||||
|       "errors": { | ||||
|         "catalogLoadFailed": "<p>Échec du chargement du catalogue de noeuds.</p><p>Vérifier la console du navigateur pour plus d'informations</p>", | ||||
|         "installFailed": "<p>Échec lors de l'installation : __module__</p><p>__message__</p><p>Consulter le journal pour plus d'informations</p>", | ||||
|         "installTimeout": "<p>L'installation continue en arrière-plan.</p><p>Les noeuds apparaîtront dans la palette une fois l'installation terminée. Consulter le journal pour plus d'informations.</p>", | ||||
|         "removeFailed": "<p>Échec lors de la suppression : __module__</p><p>__message__</p><p>Consulter le journal pour plus d'informations</p>", | ||||
|         "updateFailed": "<p>Échec lors de la mise à jour : __module__</p><p>__message__</p><p>Consulter le journal pour plus d'informations</p>", | ||||
|         "enableFailed": "<p>Échec lors de l'activation : __module__</p><p>__message__</p><p>Consulter le journal pour plus d'informations</p>", | ||||
| @@ -648,25 +663,29 @@ | ||||
|       }, | ||||
|       "confirm": { | ||||
|         "install": { | ||||
|           "body": "<p>Installation de '__module__'</p><p>Avant l'installation, veuiller lire la documentation du noeud. Certains noeuds ont des dépendances qui ne peuvent pas être résolues automatiquement et peuvent nécessiter un redémarrage de Node-RED.</p>", | ||||
|           "body": "<p>Installation de '__module__'</p><p>Avant l'installation, veuillez lire la documentation du noeud. Certains noeuds ont des dépendances qui ne peuvent pas être résolues automatiquement et peuvent nécessiter un redémarrage de Node-RED.</p>", | ||||
|           "title": "Installer les noeuds" | ||||
|         }, | ||||
|         "remove": { | ||||
|           "body": "<p>Suppression de '__module__'</p><p>La suppression du noeud le désinstallera de Node-RED. Le noeud peut continuer à utiliser des ressources jusqu'au redémarrage de Node-RED.</p>", | ||||
|           "body": "<p>Suppression de '__module__'</p><p>La suppression du noeud le désinstallera de Node-RED. Le noeud peut continuer à utiliser ses ressources jusqu'au redémarrage de Node-RED.</p>", | ||||
|           "title": "Supprimer les noeuds" | ||||
|         }, | ||||
|         "removePlugin": { | ||||
|           "body": "<p>Suppression du plugin '__module__'. Veuillez recharger l'éditeur afin d'appliquer les changements.</p>" | ||||
|         }, | ||||
|         "update": { | ||||
|           "body": "<p>Mise à jour de '__module__'</p><p>La mise à jour du noeud nécessitera un redémarrage de Node-RED pour terminer la mise à jour. Cela doit être fait manuellement.</p>", | ||||
|           "title": "Mettre à jour les noeuds" | ||||
|         }, | ||||
|         "cannotUpdate": { | ||||
|           "body": "Une mise à jour pour ce noeud est disponible, mais il n'est pas installé dans un emplacement que le gestionnaire de palette peut mettre à jour.<br/><br/>Veuiller vous référer à la documentation pour savoir comment mettre à jour ce noeud." | ||||
|           "body": "Une mise à jour pour ce noeud est disponible, mais il n'est pas installé dans un emplacement que le gestionnaire de palette peut mettre à jour.<br/><br/>Veuillez vous référer à la documentation pour savoir comment mettre à jour ce noeud." | ||||
|         }, | ||||
|         "button": { | ||||
|           "review": "Ouvrir la documentation", | ||||
|           "install": "Installer", | ||||
|           "remove": "Supprimer", | ||||
|           "update": "Mettre à jour" | ||||
|           "update": "Mettre à jour", | ||||
|           "understood": "Compris" | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| @@ -701,8 +720,8 @@ | ||||
|       "nodeHelp": "Aide sur les noeuds", | ||||
|       "none": "Aucun", | ||||
|       "arrayItems": "__count__ éléments", | ||||
|       "showTips": "Vous pouvez ouvrir les astuces à partir du panneau des paramètres", | ||||
|       "outline": "Plan", | ||||
|       "showTips": "Vous pouvez afficher les astuces à partir du panneau des paramètres", | ||||
|       "outline": "Contour", | ||||
|       "empty": "Vide", | ||||
|       "globalConfig": "Noeuds de configuration globale", | ||||
|       "triggerAction": "Déclencher une action", | ||||
| @@ -715,10 +734,11 @@ | ||||
|     "help": { | ||||
|       "name": "Aide", | ||||
|       "label": "Aide", | ||||
|       "search": "Aide à la recherche", | ||||
|       "search": "Rechercher l'aide", | ||||
|       "nodeHelp": "Aide sur les noeuds", | ||||
|       "showHelp": "Afficher l'aide", | ||||
|       "showInOutline": "Afficher dans les grandes lignes", | ||||
|       "hideTopics": "Masquer les sujets", | ||||
|       "showTopics": "Afficher les sujets", | ||||
|       "noHelp": "Aucune rubrique d'aide sélectionnée", | ||||
|       "changeLog": "Journal des modifications" | ||||
| @@ -793,7 +813,8 @@ | ||||
|         "branches": "Branches", | ||||
|         "noBranches": "Pas de branche", | ||||
|         "deleteConfirm": "Êtes-vous sûr de vouloir supprimer la branche locale '__name__' ? Ça ne peut pas être annulé.", | ||||
|         "unmergedConfirm": "La branche locale '__name__' contient des modifications non fusionnées qui seront perdues. Etes-vous sûr de vouloir la supprimer?", | ||||
|         "deleteBranch": "Supprimer la branche", | ||||
|         "unmergedConfirm": "La branche locale '__name__' contient des modifications non fusionnées qui seront perdues. Êtes-vous sûr de vouloir la supprimer?", | ||||
|         "deleteUnmergedBranch": "Supprimer la branche non fusionnée", | ||||
|         "gitRemotes": "Git distant", | ||||
|         "addRemote": "Ajout distant", | ||||
| @@ -837,17 +858,17 @@ | ||||
|         "deleteConfirm": "Êtes-vous sûr de vouloir supprimer la clé SSH __name__ ? Ça ne peut pas être annulé." | ||||
|       }, | ||||
|       "versionControl": { | ||||
|         "unstagedChanges": "Abandon des changements", | ||||
|         "stagedChanges": "Changement mis en place", | ||||
|         "unstageChange": "Ne pas mettre en place le changement", | ||||
|         "stageChange": "Mettre en place le changement", | ||||
|         "unstageAllChange": "Ne pas mettre en place tous les changements", | ||||
|         "stageAllChange": "Mettre en place tous les changements", | ||||
|         "unstagedChanges": "Changements non indexés", | ||||
|         "stagedChanges": "Changements indexés", | ||||
|         "unstageChange": "Annuler l'indexation des changements", | ||||
|         "stageChange": "Indexer les changements", | ||||
|         "unstageAllChange": "Annuler l'indexation de tous les changements", | ||||
|         "stageAllChange": "Indexer tous les changements", | ||||
|         "commitChanges": "Valider les changements", | ||||
|         "resolveConflicts": "Résoudre les conflits", | ||||
|         "head": "En-tête", | ||||
|         "staged": "Mis en place", | ||||
|         "unstaged": "Non mis en place", | ||||
|         "staged": "Indexé", | ||||
|         "unstaged": "Non indexé", | ||||
|         "local": "Local", | ||||
|         "remote": "Distant", | ||||
|         "revert": "Voulez-vous vraiment annuler les modifications apportées à '__file__' ? Ça ne peut pas être annulé.", | ||||
| @@ -881,11 +902,11 @@ | ||||
|         "pushFailed": "L'envoi a échoué car la branche a des validations plus récentes. Tirer et fusionner d'abord, puis envoyer à nouveau.", | ||||
|         "push": "Envoyer", | ||||
|         "pull": "Tirer", | ||||
|         "unablePull": "<p>Impossible d'extraire les modifications à distance ; vos modifications locales non mises en place seraient écrasées.</p><p>Valider vos modifications et réessayer.</p>", | ||||
|         "showUnstagedChanges": "Afficher les modifications non mise en place", | ||||
|         "unablePull": "<p>Impossible d'extraire les modifications à distance; vos modifications locales non mises en place seraient écrasées.</p><p>Valider vos modifications et réessayer.</p>", | ||||
|         "showUnstagedChanges": "Afficher les modifications non indexées", | ||||
|         "connectionFailed": "Impossible de se connecter au référentiel distant: ", | ||||
|         "pullUnrelatedHistory": "<p>Le réferentiel distant a un historique de validations sans rapport.</p><p>Êtes-vous sûr de vouloir extraire les modifications dans votre référentiel local ?</p>", | ||||
|         "pullChanges": "Tirer les changements", | ||||
|         "pullChanges": "Tirer les changements distants", | ||||
|         "history": "Historique", | ||||
|         "projectHistory": "Historique du projet", | ||||
|         "daysAgo": "il y a __count__ jour", | ||||
| @@ -914,6 +935,8 @@ | ||||
|     } | ||||
|   }, | ||||
|   "typedInput": { | ||||
|     "selected": "__count__ sélectionnée", | ||||
|     "selected_plural": "__count__ sélectionnées", | ||||
|     "type": { | ||||
|       "str": "chaîne de caractères", | ||||
|       "num": "nombre", | ||||
| @@ -924,7 +947,14 @@ | ||||
|       "date": "horodatage", | ||||
|       "jsonata": "expression", | ||||
|       "env": "variable d'environnement", | ||||
|       "cred": "identifiant" | ||||
|       "cred": "identifiant", | ||||
|       "conf-types": "noeud de configuration" | ||||
|     }, | ||||
|     "date": { | ||||
|       "format": { | ||||
|         "timestamp": "millisecondes depuis l'époque", | ||||
|         "object": "Objet de date JavaScript" | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "editableList": { | ||||
| @@ -957,7 +987,7 @@ | ||||
|     "result": "Résultat", | ||||
|     "format": "Format", | ||||
|     "compatMode": "Mode de compatibilité activé", | ||||
|     "compatModeDesc": "<h3>Mode de compatibilité JSONata</h3><p> L'expression actuelle semble toujours faire référence à <code>msg</code> et sera donc évaluée en mode de compatibilité. Veuiller mettre à jour l'expression pour ne pas utiliser <code>msg</code> car ce mode sera supprimé à l'avenir.</p><p> Lorsque la prise en charge de JSONata a été ajoutée pour la première fois à Node-RED, il fallait que l'expression référencie l'objet <code>msg</code>. Par exemple, <code>msg.payload</code> serait utilisé pour accéder à la charge utile.</p><p> Cela n'est plus nécessaire car l'expression sera évaluée directement par rapport au message. Pour accéder à la charge utile, l'expression doit être simplement <code>charge utile</code>.</p>", | ||||
|     "compatModeDesc": "<h3>Mode de compatibilité JSONata</h3><p> L'expression actuelle semble toujours faire référence à <code>msg</code> et sera donc évaluée en mode de compatibilité. Veuillez mettre à jour l'expression pour ne pas utiliser <code>msg</code> car ce mode sera supprimé à l'avenir.</p><p> Lorsque la prise en charge de JSONata a été ajoutée pour la première fois à Node-RED, il fallait que l'expression référencie l'objet <code>msg</code>. Par exemple, <code>msg.payload</code> serait utilisé pour accéder à la charge utile.</p><p> Cela n'est plus nécessaire car l'expression sera évaluée directement par rapport au message. Pour accéder à la charge utile, l'expression doit être simplement <code>charge utile</code>.</p>", | ||||
|     "noMatch": "Aucun résultat correspondant", | ||||
|     "errors": { | ||||
|       "invalid-expr": "Expression JSONata non valide :\n  __message__", | ||||
| @@ -980,7 +1010,7 @@ | ||||
|   }, | ||||
|   "jsonEditor": { | ||||
|     "title": "Éditeur JSON", | ||||
|     "format": "Format JSON", | ||||
|     "format": "Formatter JSON", | ||||
|     "rawMode": "Modifier JSON", | ||||
|     "uiMode": "Afficher l'éditeur", | ||||
|     "rawMode-readonly": "JSON", | ||||
| @@ -999,7 +1029,7 @@ | ||||
|   "markdownEditor": { | ||||
|     "title": "Éditeur Markdown", | ||||
|     "expand": "Développer", | ||||
|     "format": "Formaté avec Markdown", | ||||
|     "format": "Formatter avec Markdown", | ||||
|     "heading1": "Rubrique 1", | ||||
|     "heading2": "Rubrique 2", | ||||
|     "heading3": "Rubrique 3", | ||||
| @@ -1073,7 +1103,7 @@ | ||||
|       "credential-key": "Clé de chiffrement des identifiants", | ||||
|       "cant-get-ssh-key": "Erreur! Impossible d'obtenir le chemin de la clé SSH sélectionnée.", | ||||
|       "already-exists2": "Existe déjà", | ||||
|       "git-error": "Erreur git", | ||||
|       "git-error": "Erreur Git", | ||||
|       "connection-failed": "La connexion a échoué", | ||||
|       "not-git-repo": "Ce n'est pas un dépôt Git", | ||||
|       "repo-not-found": "Référentiel introuvable" | ||||
| @@ -1087,7 +1117,7 @@ | ||||
|       "credentials-file": "Fichier d'identifiants" | ||||
|     }, | ||||
|     "encryption-config": { | ||||
|       "setup": "Configuration du chiffrage de votre fichier d'informations d'identification", | ||||
|       "setup": "Configuration du chiffrement de votre fichier d'informations d'identification", | ||||
|       "desc0": "Votre fichier d'informations d'identification de flux peut être chiffré pour sécuriser son contenu.", | ||||
|       "desc1": "Si vous souhaitez stocker ces identifiants dans un référentiel Git public, vous devez les chiffrer en fournissant une phrase clé secrète.", | ||||
|       "desc2": "Votre fichier d'identifiants de flux n'est actuellement pas chiffré.", | ||||
| @@ -1144,9 +1174,9 @@ | ||||
|       "add-ssh-key": "Ajouter une clé ssh", | ||||
|       "credentials-encryption-key": "Clé de chiffrement des identifiants", | ||||
|       "already-exists-2": "Existe déjà", | ||||
|       "git-error": "Erreur git", | ||||
|       "git-error": "Erreur Git", | ||||
|       "con-failed": "La connexion a échoué", | ||||
|       "not-git": "Ce n'est pas un dépôt git", | ||||
|       "not-git": "Ce n'est pas un dépôt Git", | ||||
|       "no-resource": "Référentiel introuvable", | ||||
|       "cant-get-ssh-key-path": "Erreur! Impossible d'obtenir le chemin de la clé SSH sélectionnée.", | ||||
|       "unexpected_error": "Erreur inattendue", | ||||
| @@ -1184,7 +1214,7 @@ | ||||
|     }, | ||||
|     "errors": { | ||||
|       "no-username-email": "Votre client Git n'est pas configuré avec un nom d'utilisateur/e-mail.", | ||||
|       "unexpected": "Une erreur inattendue est apparue", | ||||
|       "unexpected": "Une erreur inattendue est survenue", | ||||
|       "code": "Code" | ||||
|     } | ||||
|   }, | ||||
| @@ -1253,7 +1283,7 @@ | ||||
|     "list-modified-nodes": "Afficher les flux modifiés", | ||||
|     "list-hidden-flows": "Afficher les flux cachés", | ||||
|     "list-flows": "Lister les flux", | ||||
|     "list-subflows": "Liste les sous-flux", | ||||
|     "list-subflows": "Lister les sous-flux", | ||||
|     "go-to-previous-location": "Aller à l'emplacement précédent", | ||||
|     "go-to-next-location": "Aller à l'emplacement suivant", | ||||
|     "copy-selection-to-internal-clipboard": "Copier la sélection dans le presse-papiers", | ||||
| @@ -1313,8 +1343,8 @@ | ||||
|     "align-selection-to-bottom": "Aligner la sélection vers le bas", | ||||
|     "align-selection-to-middle": "Aligner la sélection au centre verticalement", | ||||
|     "align-selection-to-center": "Aligner la sélection au centre horizontalement", | ||||
|     "distribute-selection-horizontally": "Distribuer la sélection horizontalement", | ||||
|     "distribute-selection-vertical": "Distribuer la sélection verticalement", | ||||
|     "distribute-selection-horizontally": "Répartir la sélection horizontalement", | ||||
|     "distribute-selection-vertical": "Répartir la sélection verticalement", | ||||
|     "wire-series-of-nodes": "Connecter les noeuds en série", | ||||
|     "wire-node-to-multiple": "Connecter les noeuds à plusieurs", | ||||
|     "wire-multiple-to-node": "Connecter plusieurs au noeud", | ||||
|   | ||||
| @@ -27,7 +27,8 @@ | ||||
|             "lock": "固定", | ||||
|             "unlock": "固定を解除", | ||||
|             "locked": "固定済み", | ||||
|             "unlocked": "固定なし" | ||||
|             "unlocked": "固定なし", | ||||
|             "format": "形式" | ||||
|         }, | ||||
|         "type": { | ||||
|             "string": "文字列", | ||||
| @@ -281,8 +282,8 @@ | ||||
|             "selected": "選択したフロー", | ||||
|             "current": "現在のタブ", | ||||
|             "all": "全てのタブ", | ||||
|             "compact": "インデントのないJSONフォーマット", | ||||
|             "formatted": "インデント付きのJSONフォーマット", | ||||
|             "compact": "インデントなし", | ||||
|             "formatted": "インデント付き", | ||||
|             "copy": "書き出し", | ||||
|             "export": "ライブラリに書き出し", | ||||
|             "exportAs": "書き出し先", | ||||
| @@ -303,7 +304,8 @@ | ||||
|                 "missingType": "不正なフロー - __index__ 番目の要素に'type'プロパティがありません" | ||||
|             }, | ||||
|             "conflictNotification1": "読み込もうとしているノードのいくつかは、既にワークスペース内に存在しています。", | ||||
|             "conflictNotification2": "読み込むノードを選択し、また既存のノードを置き換えるか、もしくはそれらのコピーを読み込むかも選択してください。" | ||||
|             "conflictNotification2": "読み込むノードを選択し、また既存のノードを置き換えるか、もしくはそれらのコピーを読み込むかも選択してください。", | ||||
|             "alreadyExists": "本ノードは既に存在" | ||||
|         }, | ||||
|         "copyMessagePath": "パスをコピーしました", | ||||
|         "copyMessageValue": "値をコピーしました", | ||||
| @@ -371,8 +373,12 @@ | ||||
|             "deleted": "削除", | ||||
|             "flowDeleted": "削除されたフロー", | ||||
|             "flowAdded": "追加されたフロー", | ||||
|             "moved": "移動", | ||||
|             "movedTo": "__id__ へ移動", | ||||
|             "movedFrom": "__id__ から移動" | ||||
|             "movedFrom": "__id__ から移動", | ||||
|             "none": "なし", | ||||
|             "position": "位置", | ||||
|             "wires": "ワイヤー" | ||||
|         }, | ||||
|         "nodeCount": "__count__ 個のノード", | ||||
|         "nodeCount_plural": "__count__ 個のノード", | ||||
| @@ -381,9 +387,14 @@ | ||||
|         "reviewChanges": "変更を表示", | ||||
|         "noBinaryFileShowed": "バイナリファイルの中身は表示することができません", | ||||
|         "viewCommitDiff": "コミットの内容を表示", | ||||
|         "commit": "コミット", | ||||
|         "compareChanges": "変更を比較", | ||||
|         "saveConflict": "解決して保存", | ||||
|         "conflictHeader": "<span>__unresolved__</span> 個中 <span>__resolved__</span> 個のコンフリクトを解決", | ||||
|         "localChanges": "ローカルの変更", | ||||
|         "remoteChanges": "リモートの変更", | ||||
|         "useLocalChanges": "ローカルの変更を使用", | ||||
|         "useRemoteChanges": "リモートの変更を使用", | ||||
|         "commonVersionError": "共通バージョンは正しいJSON形式ではありません:", | ||||
|         "oldVersionError": "古いバージョンは正しいJSON形式ではありません:", | ||||
|         "newVersionError": "新しいバージョンは正しいJSON形式ではありません:" | ||||
| @@ -551,7 +562,9 @@ | ||||
|         "types": { | ||||
|             "local": "ローカル", | ||||
|             "examples": "サンプル" | ||||
|         } | ||||
|         }, | ||||
|         "type": "型", | ||||
|         "name": "名前" | ||||
|     }, | ||||
|     "palette": { | ||||
|         "noInfo": "情報がありません", | ||||
| @@ -613,6 +626,8 @@ | ||||
|             }, | ||||
|             "nodeCount": "__label__ 個のノード", | ||||
|             "nodeCount_plural": "__label__ 個のノード", | ||||
|             "pluginCount": "__count__ 個のプラグイン", | ||||
|             "pluginCount_plural": "__count__ 個のプラグイン", | ||||
|             "moduleCount": "__count__ 個のモジュール", | ||||
|             "moduleCount_plural": "__count__ 個のモジュール", | ||||
|             "inuse": "使用中", | ||||
| @@ -640,6 +655,7 @@ | ||||
|             "errors": { | ||||
|                 "catalogLoadFailed": "<p>ノードのカタログの読み込みに失敗しました。</p><p>詳細はブラウザのコンソールを確認してください。</p>", | ||||
|                 "installFailed": "<p>追加処理が失敗しました: __module__</p><p>__message__</p><p>詳細はログを確認してください。</p>", | ||||
|                 "installTimeout": "<p>バックグラウンドでインストールが継続されます。</p><p>完了した時にノードが表示されます。詳細はログを確認してください。</p>", | ||||
|                 "removeFailed": "<p>削除処理が失敗しました: __module__</p><p>__message__</p><p>詳細はログを確認してください。</p>", | ||||
|                 "updateFailed": "<p>更新処理が失敗しました: __module__</p><p>__message__</p><p>詳細はログを確認してください。</p>", | ||||
|                 "enableFailed": "<p>有効化処理が失敗しました: __module__</p><p>__message__</p><p>詳細はログを確認してください。</p>", | ||||
| @@ -654,6 +670,9 @@ | ||||
|                     "body": "<p>__module__ を削除します。</p><p>Node-REDからノードを削除します。ノードはNode-REDが再起動されるまで、リソースを使い続ける可能性があります。</p>", | ||||
|                     "title": "ノードを削除" | ||||
|                 }, | ||||
|                 "removePlugin": { | ||||
|                     "body": "<p>プラグイン __module__ を削除しました。ブラウザを再読み込みして残った表示を消してください。</p>" | ||||
|                 }, | ||||
|                 "update": { | ||||
|                     "body": "<p>__module__ を更新します。</p><p>更新を完了するには手動でNode-REDを再起動する必要があります。</p>", | ||||
|                     "title": "ノードの更新" | ||||
| @@ -665,7 +684,8 @@ | ||||
|                     "review": "ノードの情報を参照", | ||||
|                     "install": "追加", | ||||
|                     "remove": "削除", | ||||
|                     "update": "更新" | ||||
|                     "update": "更新", | ||||
|                     "understood": "了解" | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| @@ -718,6 +738,7 @@ | ||||
|             "nodeHelp": "ノードヘルプ", | ||||
|             "showHelp": "ヘルプを表示", | ||||
|             "showInOutline": "アウトラインに表示", | ||||
|             "hideTopics": "トピックを非表示", | ||||
|             "showTopics": "トピックを表示", | ||||
|             "noHelp": "ヘルプのトピックが未選択", | ||||
|             "changeLog": "更新履歴" | ||||
| @@ -792,6 +813,7 @@ | ||||
|                 "branches": "ブランチ", | ||||
|                 "noBranches": "ブランチなし", | ||||
|                 "deleteConfirm": "本当にローカルブランチ'__name__'を削除しますか?削除すると元に戻すことはできません。", | ||||
|                 "deleteBranch": "ブランチを削除", | ||||
|                 "unmergedConfirm": "ローカルブランチ'__name__'にはマージされていない変更があります。この変更は削除されます。本当に削除しますか?", | ||||
|                 "deleteUnmergedBranch": "マージされていないブランチを削除", | ||||
|                 "gitRemotes": "Gitリモート", | ||||
| @@ -913,6 +935,8 @@ | ||||
|         } | ||||
|     }, | ||||
|     "typedInput": { | ||||
|         "selected": "__count__個を選択", | ||||
|         "selected_plural": "__count__個を選択", | ||||
|         "type": { | ||||
|             "str": "文字列", | ||||
|             "num": "数値", | ||||
| @@ -923,7 +947,14 @@ | ||||
|             "date": "日時", | ||||
|             "jsonata": "JSONata式", | ||||
|             "env": "環境変数", | ||||
|             "cred": "認証情報" | ||||
|             "cred": "認証情報", | ||||
|             "conf-types": "設定ノード" | ||||
|         }, | ||||
|         "date": { | ||||
|             "format": { | ||||
|                 "timestamp": "エポックからの経過ミリ秒", | ||||
|                 "object": "JavaScript日付オブジェクト" | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "editableList": { | ||||
| @@ -1229,7 +1260,7 @@ | ||||
|     }, | ||||
|     "env-var": { | ||||
|         "environment": "環境変数", | ||||
|         "header": "大域環境変数", | ||||
|         "header": "グローバル環境変数", | ||||
|         "revert": "破棄" | ||||
|     }, | ||||
|     "action-list": { | ||||
| @@ -1381,7 +1412,7 @@ | ||||
|         "copy-item-edit-url": "要素の編集URLをコピー", | ||||
|         "move-flow-to-start": "フローを先頭に移動", | ||||
|         "move-flow-to-end": "フローを末尾に移動", | ||||
|         "show-global-env": "大域環境変数を表示", | ||||
|         "show-global-env": "グローバル環境変数を表示", | ||||
|         "lock-flow": "フローを固定", | ||||
|         "unlock-flow": "フローの固定を解除", | ||||
|         "show-node-help": "ノードのヘルプを表示" | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "@node-red/editor-client", | ||||
|     "version": "3.1.6", | ||||
|     "version": "4.0.8", | ||||
|     "license": "Apache-2.0", | ||||
|     "repository": { | ||||
|         "type": "git", | ||||
|   | ||||
| @@ -26,6 +26,15 @@ RED.comms = (function() { | ||||
|     var reconnectAttempts = 0; | ||||
|     var active = false; | ||||
|  | ||||
|     RED.events.on('login', function(username) { | ||||
|         // User has logged in | ||||
|         // Need to upgrade the connection to be authenticated | ||||
|         if (ws && ws.readyState == 1) { | ||||
|             const auth_tokens = RED.settings.get("auth-tokens"); | ||||
|             ws.send(JSON.stringify({auth:auth_tokens.access_token})) | ||||
|         } | ||||
|     }) | ||||
|  | ||||
|     function connectWS() { | ||||
|         active = true; | ||||
|         var wspath; | ||||
| @@ -56,6 +65,7 @@ RED.comms = (function() { | ||||
|                     ws.send(JSON.stringify({subscribe:t})); | ||||
|                 } | ||||
|             } | ||||
|             emit('connect') | ||||
|         } | ||||
|  | ||||
|         ws = new WebSocket(wspath); | ||||
| @@ -180,9 +190,53 @@ RED.comms = (function() { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function send(topic, msg) { | ||||
|         if (ws && ws.readyState == 1) { | ||||
|             ws.send(JSON.stringify({ | ||||
|                 topic, | ||||
|                 data: msg | ||||
|             })) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     const eventHandlers = {}; | ||||
|     function on(evt,func) { | ||||
|         eventHandlers[evt] = eventHandlers[evt]||[]; | ||||
|         eventHandlers[evt].push(func); | ||||
|     } | ||||
|     function off(evt,func) { | ||||
|         const handler = eventHandlers[evt]; | ||||
|         if (handler) { | ||||
|             for (let i=0;i<handler.length;i++) { | ||||
|                 if (handler[i] === func) { | ||||
|                     handler.splice(i,1); | ||||
|                     return; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     function emit() { | ||||
|         const evt = arguments[0] | ||||
|         const args = Array.prototype.slice.call(arguments,1); | ||||
|         if (eventHandlers[evt]) { | ||||
|             let cpyHandlers = [...eventHandlers[evt]]; | ||||
|             for (let i=0;i<cpyHandlers.length;i++) { | ||||
|                 try { | ||||
|                     cpyHandlers[i].apply(null, args); | ||||
|                 } catch(err) { | ||||
|                     console.warn("RED.comms.emit error: ["+evt+"] "+(err.toString())); | ||||
|                     console.warn(err); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         connect: connectWS, | ||||
|         subscribe: subscribe, | ||||
|         unsubscribe:unsubscribe | ||||
|         unsubscribe:unsubscribe, | ||||
|         on, | ||||
|         off, | ||||
|         send | ||||
|     } | ||||
| })(); | ||||
|   | ||||
| @@ -29,7 +29,14 @@ RED.history = (function() { | ||||
|         } | ||||
|         return RED.nodes.junction(id); | ||||
|     } | ||||
|  | ||||
|     function ensureUnlocked(id, flowsToLock) { | ||||
|         const flow = id && (RED.nodes.workspace(id) || RED.nodes.subflow(id) || null); | ||||
|         const isLocked = flow ? flow.locked : false; | ||||
|         if (flow && isLocked) { | ||||
|             flow.locked = false; | ||||
|             flowsToLock.add(flow) | ||||
|         } | ||||
|     } | ||||
|     function undoEvent(ev) { | ||||
|         var i; | ||||
|         var len; | ||||
| @@ -59,18 +66,46 @@ RED.history = (function() { | ||||
|                         t: 'replace', | ||||
|                         config: RED.nodes.createCompleteNodeSet(), | ||||
|                         changed: {}, | ||||
|                         rev: RED.nodes.version() | ||||
|                         moved: {}, | ||||
|                         complete: true, | ||||
|                         rev: RED.nodes.version(), | ||||
|                         dirty: RED.nodes.dirty() | ||||
|                     }; | ||||
|                     var selectedTab = RED.workspaces.active(); | ||||
|                     inverseEv.config.forEach(n => { | ||||
|                         const node = RED.nodes.node(n.id) | ||||
|                         if (node) { | ||||
|                             inverseEv.changed[n.id] = node.changed | ||||
|                             inverseEv.moved[n.id] = node.moved | ||||
|                         } | ||||
|                     }) | ||||
|                     RED.nodes.clear(); | ||||
|                     var imported = RED.nodes.import(ev.config); | ||||
|                     // Clear all change flags from the import | ||||
|                     RED.nodes.dirty(false); | ||||
|  | ||||
|                     const flowsToLock = new Set() | ||||
|                      | ||||
|                     imported.nodes.forEach(function(n) { | ||||
|                         if (ev.changed[n.id]) { | ||||
|                             ensureUnlocked(n.z, flowsToLock) | ||||
|                             n.changed = true; | ||||
|                             inverseEv.changed[n.id] = true; | ||||
|                         } | ||||
|                         if (ev.moved[n.id]) { | ||||
|                             ensureUnlocked(n.z, flowsToLock) | ||||
|                             n.moved = true; | ||||
|                         } | ||||
|                     }) | ||||
|                     flowsToLock.forEach(flow => { | ||||
|                         flow.locked = true | ||||
|                     }) | ||||
|  | ||||
|                     RED.nodes.version(ev.rev); | ||||
|                     RED.view.redraw(true); | ||||
|                     RED.palette.refresh(); | ||||
|                     RED.workspaces.refresh(); | ||||
|                     RED.workspaces.show(selectedTab, true); | ||||
|                     RED.sidebar.config.refresh(); | ||||
|                 } else { | ||||
|                     var importMap = {}; | ||||
|                     ev.config.forEach(function(n) { | ||||
| @@ -418,10 +453,61 @@ RED.history = (function() { | ||||
|                                     RED.events.emit("nodes:change",newConfigNode); | ||||
|                                 } | ||||
|                             }); | ||||
|                         } else if (i === "env" && ev.node.type.indexOf("subflow:") === 0) { | ||||
|                             // Subflow can have config node in node.env | ||||
|                             let nodeList = ev.node.env || []; | ||||
|                             nodeList = nodeList.reduce((list, prop) => { | ||||
|                                 if (prop.type === "conf-type" && prop.value) { | ||||
|                                     list.push(prop.value); | ||||
|                                 } | ||||
|                                 return list; | ||||
|                             }, []); | ||||
|  | ||||
|                             nodeList.forEach(function(id) { | ||||
|                                 const configNode = RED.nodes.node(id); | ||||
|                                 if (configNode) { | ||||
|                                     if (configNode.users.indexOf(ev.node) !== -1) { | ||||
|                                         configNode.users.splice(configNode.users.indexOf(ev.node), 1); | ||||
|                                         RED.events.emit("nodes:change", configNode); | ||||
|                                     } | ||||
|                                 } | ||||
|                             }); | ||||
|  | ||||
|                             nodeList = ev.changes.env || []; | ||||
|                             nodeList = nodeList.reduce((list, prop) => { | ||||
|                                 if (prop.type === "conf-type" && prop.value) { | ||||
|                                     list.push(prop.value); | ||||
|                                 } | ||||
|                                 return list; | ||||
|                             }, []); | ||||
|  | ||||
|                             nodeList.forEach(function(id) { | ||||
|                                 const configNode = RED.nodes.node(id); | ||||
|                                 if (configNode) { | ||||
|                                     if (configNode.users.indexOf(ev.node) === -1) { | ||||
|                                         configNode.users.push(ev.node); | ||||
|                                         RED.events.emit("nodes:change", configNode); | ||||
|                                     } | ||||
|                                 } | ||||
|                             }); | ||||
|                         } | ||||
|                         if (i === "credentials" && ev.changes[i]) { | ||||
|                             // Reset - Only want to keep the changes | ||||
|                             inverseEv.changes[i] = {}; | ||||
|                             for (const [key, value] of Object.entries(ev.changes[i])) { | ||||
|                                 // Edge case: node.credentials is cleared after a deploy, so we can't | ||||
|                                 // capture values for the inverse event when undoing past a deploy | ||||
|                                 if (ev.node.credentials) { | ||||
|                                     inverseEv.changes[i][key] = ev.node.credentials[key]; | ||||
|                                 } | ||||
|                                 ev.node.credentials[key] = value; | ||||
|                             } | ||||
|                         } else { | ||||
|                             ev.node[i] = ev.changes[i]; | ||||
|                         } | ||||
|                         ev.node[i] = ev.changes[i]; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 ev.node.dirty = true; | ||||
|                 ev.node.changed = ev.changed; | ||||
|  | ||||
| @@ -501,6 +587,24 @@ RED.history = (function() { | ||||
|                     RED.editor.updateNodeProperties(ev.node,outputMap); | ||||
|                     RED.editor.validateNode(ev.node); | ||||
|                 } | ||||
|                 // If it's a Config Node, validate user nodes too. | ||||
|                 // NOTE: The Config Node must be validated before validating users. | ||||
|                 if (ev.node.users) { | ||||
|                     const validatedNodes = new Set(); | ||||
|                     const userStack = ev.node.users.slice(); | ||||
|  | ||||
|                     validatedNodes.add(ev.node.id); | ||||
|                     while (userStack.length) { | ||||
|                         const node = userStack.pop(); | ||||
|                         if (!validatedNodes.has(node.id)) { | ||||
|                             validatedNodes.add(node.id); | ||||
|                             if (node.users) { | ||||
|                                 userStack.push(...node.users); | ||||
|                             } | ||||
|                             RED.editor.validateNode(node); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 if (ev.links) { | ||||
|                     inverseEv.createdLinks = []; | ||||
|                     for (i=0;i<ev.links.length;i++) { | ||||
|   | ||||
							
								
								
									
										561
									
								
								packages/node_modules/@node-red/editor-client/src/js/multiplayer.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,561 @@ | ||||
| RED.multiplayer = (function () { | ||||
|  | ||||
|     // activeSessionId - used to identify sessions across websocket reconnects | ||||
|     let activeSessionId | ||||
|  | ||||
|     let headerWidget | ||||
|     // Map of session id to { session:'', user:{}, location:{}} | ||||
|     let sessions = {} | ||||
|     // Map of username to { user:{}, sessions:[] } | ||||
|     let users = {} | ||||
|  | ||||
|     function addUserSession (session) { | ||||
|         if (sessions[session.session]) { | ||||
|             // This is an existing connection that has been authenticated | ||||
|             const existingSession = sessions[session.session] | ||||
|             if (existingSession.user.username !== session.user.username) { | ||||
|                 removeUserHeaderButton(users[existingSession.user.username]) | ||||
|             } | ||||
|         } | ||||
|         sessions[session.session] = session | ||||
|         const user = users[session.user.username] = users[session.user.username] || { | ||||
|             user: session.user, | ||||
|             sessions: [] | ||||
|         } | ||||
|         if (session.user.profileColor === undefined) { | ||||
|             session.user.profileColor = (1 + Math.floor(Math.random() * 5)) | ||||
|         } | ||||
|         session.location = session.location || {} | ||||
|         user.sessions.push(session) | ||||
|  | ||||
|         if (session.session === activeSessionId) { | ||||
|             // This is the current user session - do not add a extra button for them | ||||
|         } else { | ||||
|             if (user.sessions.length === 1) { | ||||
|                 if (user.button) { | ||||
|                     clearTimeout(user.inactiveTimeout) | ||||
|                     clearTimeout(user.removeTimeout) | ||||
|                     user.button.removeClass('inactive') | ||||
|                 } else { | ||||
|                     addUserHeaderButton(user) | ||||
|                 } | ||||
|             } | ||||
|             sessions[session.session].location = session.location | ||||
|             updateUserLocation(session.session) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function removeUserSession (sessionId, isDisconnected) { | ||||
|         removeUserLocation(sessionId) | ||||
|         const session = sessions[sessionId] | ||||
|         delete sessions[sessionId] | ||||
|         const user = users[session.user.username] | ||||
|         const i = user.sessions.indexOf(session) | ||||
|         user.sessions.splice(i, 1) | ||||
|         if (isDisconnected) { | ||||
|             removeUserHeaderButton(user) | ||||
|         } else { | ||||
|             if (user.sessions.length === 0) { | ||||
|                 // Give the user 5s to reconnect before marking inactive | ||||
|                 user.inactiveTimeout = setTimeout(() => { | ||||
|                     user.button.addClass('inactive') | ||||
|                     // Give the user further 20 seconds to reconnect before removing them | ||||
|                     // from the user toolbar entirely | ||||
|                     user.removeTimeout = setTimeout(() => { | ||||
|                         removeUserHeaderButton(user) | ||||
|                     }, 20000) | ||||
|                 }, 5000) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function addUserHeaderButton (user) { | ||||
|         user.button = $('<li class="red-ui-multiplayer-user"><button type="button" class="red-ui-multiplayer-user-icon"></button></li>') | ||||
|             .attr('data-username', user.user.username) | ||||
|             .prependTo("#red-ui-multiplayer-user-list"); | ||||
|         var button = user.button.find("button") | ||||
|         RED.popover.tooltip(button, user.user.username) | ||||
|         button.on('click', function () { | ||||
|             const location = user.sessions[0].location | ||||
|             revealUser(location) | ||||
|         }) | ||||
|  | ||||
|         const userProfile = RED.user.generateUserIcon(user.user) | ||||
|         userProfile.appendTo(button) | ||||
|     } | ||||
|  | ||||
|     function removeUserHeaderButton (user) { | ||||
|         user.button.remove() | ||||
|         delete user.button | ||||
|     } | ||||
|  | ||||
|     function getLocation () { | ||||
|         const location = { | ||||
|             workspace: RED.workspaces.active() | ||||
|         } | ||||
|         const editStack = RED.editor.getEditStack() | ||||
|         for (let i = editStack.length - 1; i >= 0; i--) { | ||||
|             if (editStack[i].id) { | ||||
|                 location.node = editStack[i].id | ||||
|                 break | ||||
|             } | ||||
|         } | ||||
|         if (isInWorkspace) { | ||||
|             const chart = $('#red-ui-workspace-chart') | ||||
|             const chartOffset = chart.offset() | ||||
|             const scaleFactor = RED.view.scale() | ||||
|             location.cursor = { | ||||
|                 x: (lastPosition[0] - chartOffset.left + chart.scrollLeft()) / scaleFactor, | ||||
|                 y: (lastPosition[1] - chartOffset.top + chart.scrollTop()) / scaleFactor | ||||
|             } | ||||
|         } | ||||
|         return location | ||||
|     } | ||||
|  | ||||
|     let publishLocationTimeout | ||||
|     let lastPosition = [0,0] | ||||
|     let isInWorkspace = false | ||||
|  | ||||
|     function publishLocation () { | ||||
|         if (!publishLocationTimeout) { | ||||
|             publishLocationTimeout = setTimeout(() => { | ||||
|                 const location = getLocation() | ||||
|                 if (location.workspace !== 0) { | ||||
|                     log('send', 'multiplayer/location', location) | ||||
|                     RED.comms.send('multiplayer/location', location) | ||||
|                 } | ||||
|                 publishLocationTimeout = null | ||||
|             }, 100) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     function revealUser(location, skipWorkspace) { | ||||
|         if (location.node) { | ||||
|             // Need to check if this is a known node, so we can fall back to revealing | ||||
|             // the workspace instead | ||||
|             const node = RED.nodes.node(location.node) | ||||
|             if (node) { | ||||
|                 RED.view.reveal(location.node) | ||||
|             } else if (!skipWorkspace && location.workspace) { | ||||
|                 RED.view.reveal(location.workspace) | ||||
|             } | ||||
|         } else if (!skipWorkspace && location.workspace) { | ||||
|             RED.view.reveal(location.workspace) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     const workspaceTrays = {} | ||||
|     function getWorkspaceTray(workspaceId) { | ||||
|         // console.log('get tray for',workspaceId) | ||||
|         if (!workspaceTrays[workspaceId]) { | ||||
|             const tray = $('<div class="red-ui-multiplayer-users-tray"></div>') | ||||
|             const users = [] | ||||
|             const userIcons = {} | ||||
|  | ||||
|             const userCountIcon = $(`<div class="red-ui-multiplayer-user-location"><span class="red-ui-user-profile red-ui-multiplayer-user-count"><span></span></span></div>`) | ||||
|             const userCountSpan = userCountIcon.find('span span') | ||||
|             userCountIcon.hide() | ||||
|             userCountSpan.text('') | ||||
|             userCountIcon.appendTo(tray) | ||||
|             const userCountTooltip = RED.popover.tooltip(userCountIcon, function () { | ||||
|                     const content = $('<div>') | ||||
|                     users.forEach(sessionId => { | ||||
|                         $('<div>').append($('<a href="#">').text(sessions[sessionId].user.username).on('click', function (evt) { | ||||
|                             evt.preventDefault() | ||||
|                             revealUser(sessions[sessionId].location, true) | ||||
|                             userCountTooltip.close() | ||||
|                         })).appendTo(content) | ||||
|                     }) | ||||
|                     return content | ||||
|                 }, | ||||
|                 null, | ||||
|                 true | ||||
|             ) | ||||
|  | ||||
|             const updateUserCount = function () { | ||||
|                 const maxShown = 2 | ||||
|                 const children = tray.children() | ||||
|                 children.each(function (index, element) { | ||||
|                     const i = users.length - index | ||||
|                     if (i > maxShown) { | ||||
|                         $(this).hide() | ||||
|                     } else if (i >= 0) { | ||||
|                         $(this).show() | ||||
|                     } | ||||
|                 }) | ||||
|                 if (users.length < maxShown + 1) {  | ||||
|                     userCountIcon.hide() | ||||
|                 } else { | ||||
|                     userCountSpan.text('+'+(users.length - maxShown)) | ||||
|                     userCountIcon.show() | ||||
|                 } | ||||
|             } | ||||
|             workspaceTrays[workspaceId] = { | ||||
|                 attached: false, | ||||
|                 tray, | ||||
|                 users, | ||||
|                 userIcons, | ||||
|                 addUser: function (sessionId) { | ||||
|                     if (users.indexOf(sessionId) === -1) { | ||||
|                         // console.log(`addUser ws:${workspaceId} session:${sessionId}`) | ||||
|                         users.push(sessionId) | ||||
|                         const userLocationId = `red-ui-multiplayer-user-location-${sessionId}` | ||||
|                         const userLocationIcon = $(`<div class="red-ui-multiplayer-user-location" id="${userLocationId}"></div>`) | ||||
|                         RED.user.generateUserIcon(sessions[sessionId].user).appendTo(userLocationIcon) | ||||
|                         userLocationIcon.prependTo(tray) | ||||
|                         RED.popover.tooltip(userLocationIcon, sessions[sessionId].user.username) | ||||
|                         userIcons[sessionId] = userLocationIcon | ||||
|                         updateUserCount() | ||||
|                     } | ||||
|                 }, | ||||
|                 removeUser: function (sessionId) { | ||||
|                     // console.log(`removeUser ws:${workspaceId} session:${sessionId}`) | ||||
|                     const userLocationId = `red-ui-multiplayer-user-location-${sessionId}` | ||||
|                     const index = users.indexOf(sessionId) | ||||
|                     if (index > -1) { | ||||
|                         users.splice(index, 1) | ||||
|                         userIcons[sessionId].remove() | ||||
|                         delete userIcons[sessionId] | ||||
|                     } | ||||
|                     updateUserCount() | ||||
|                 }, | ||||
|                 updateUserCount | ||||
|             } | ||||
|         } | ||||
|         const trayDef = workspaceTrays[workspaceId] | ||||
|         if (!trayDef.attached) { | ||||
|             const workspaceTab = $(`#red-ui-tab-${workspaceId}`) | ||||
|             if (workspaceTab.length > 0) { | ||||
|                 trayDef.attached = true | ||||
|                 trayDef.tray.appendTo(workspaceTab) | ||||
|                 trayDef.users.forEach(sessionId => { | ||||
|                     trayDef.userIcons[sessionId].on('click', function (evt) { | ||||
|                         revealUser(sessions[sessionId].location, true) | ||||
|                     }) | ||||
|                 }) | ||||
|             } | ||||
|         } | ||||
|         return workspaceTrays[workspaceId] | ||||
|     } | ||||
|     function attachWorkspaceTrays () { | ||||
|         let viewTouched = false | ||||
|         for (let sessionId of Object.keys(sessions)) { | ||||
|             const location = sessions[sessionId].location | ||||
|             if (location) { | ||||
|                 if (location.workspace) { | ||||
|                     getWorkspaceTray(location.workspace).updateUserCount() | ||||
|                 } | ||||
|                 if (location.node) { | ||||
|                     addUserToNode(sessionId, location.node) | ||||
|                     viewTouched = true | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         if (viewTouched) { | ||||
|             RED.view.redraw() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function addUserToNode(sessionId, nodeId) { | ||||
|         const node = RED.nodes.node(nodeId) | ||||
|         if (node) { | ||||
|             if (!node._multiplayer) { | ||||
|                 node._multiplayer = { | ||||
|                     users: [sessionId] | ||||
|                 } | ||||
|                 node._multiplayer_refresh = true | ||||
|             } else { | ||||
|                 if (node._multiplayer.users.indexOf(sessionId) === -1) { | ||||
|                     node._multiplayer.users.push(sessionId) | ||||
|                     node._multiplayer_refresh = true | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     function removeUserFromNode(sessionId, nodeId) { | ||||
|         const node = RED.nodes.node(nodeId) | ||||
|         if (node && node._multiplayer) { | ||||
|             const i = node._multiplayer.users.indexOf(sessionId) | ||||
|             if (i > -1) { | ||||
|                 node._multiplayer.users.splice(i, 1) | ||||
|             } | ||||
|             if (node._multiplayer.users.length === 0) { | ||||
|                 delete node._multiplayer | ||||
|             } else { | ||||
|                 node._multiplayer_refresh = true | ||||
|             } | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     function removeUserLocation (sessionId) { | ||||
|         updateUserLocation(sessionId, {}) | ||||
|         removeUserCursor(sessionId) | ||||
|     } | ||||
|     function removeUserCursor (sessionId) { | ||||
|         // return | ||||
|         if (sessions[sessionId]?.cursor) { | ||||
|             sessions[sessionId].cursor.parentNode.removeChild(sessions[sessionId].cursor) | ||||
|             delete sessions[sessionId].cursor | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function updateUserLocation (sessionId, location) { | ||||
|         let viewTouched = false | ||||
|         const oldLocation = sessions[sessionId].location | ||||
|         if (location) { | ||||
|             if (oldLocation.workspace !== location.workspace) { | ||||
|                 // console.log('removing', sessionId, oldLocation.workspace) | ||||
|                 workspaceTrays[oldLocation.workspace]?.removeUser(sessionId) | ||||
|             } | ||||
|             if (oldLocation.node !== location.node) { | ||||
|                 removeUserFromNode(sessionId, oldLocation.node) | ||||
|                 viewTouched = true | ||||
|             } | ||||
|             sessions[sessionId].location = location | ||||
|         } else { | ||||
|             location = sessions[sessionId].location | ||||
|         } | ||||
|         // console.log(`updateUserLocation sessionId:${sessionId} oldWS:${oldLocation?.workspace} newWS:${location.workspace}`) | ||||
|         if (location.workspace) { | ||||
|             getWorkspaceTray(location.workspace).addUser(sessionId) | ||||
|             if (location.cursor && location.workspace === RED.workspaces.active()) { | ||||
|                 if (!sessions[sessionId].cursor) { | ||||
|                     const user = sessions[sessionId].user | ||||
|                     const cursorIcon = document.createElementNS("http://www.w3.org/2000/svg","g"); | ||||
|                     cursorIcon.setAttribute("class", "red-ui-multiplayer-annotation") | ||||
|                     cursorIcon.appendChild(createAnnotationUser(user, true)) | ||||
|                     $(cursorIcon).css({ | ||||
|                         transform: `translate( ${location.cursor.x}px, ${location.cursor.y}px)`, | ||||
|                         transition: 'transform 0.1s linear' | ||||
|                     }) | ||||
|                     $("#red-ui-workspace-chart svg").append(cursorIcon) | ||||
|                     sessions[sessionId].cursor = cursorIcon | ||||
|                 } else { | ||||
|                     const cursorIcon = sessions[sessionId].cursor | ||||
|                     $(cursorIcon).css({ | ||||
|                         transform: `translate( ${location.cursor.x}px, ${location.cursor.y}px)` | ||||
|                     }) | ||||
|      | ||||
|                 } | ||||
|             } else if (sessions[sessionId].cursor) { | ||||
|                 removeUserCursor(sessionId) | ||||
|             } | ||||
|         } | ||||
|         if (location.node) { | ||||
|             addUserToNode(sessionId, location.node) | ||||
|             viewTouched = true | ||||
|         } | ||||
|         if (viewTouched) { | ||||
|             RED.view.redraw() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // function refreshUserLocations () { | ||||
|     //     for (const session of Object.keys(sessions)) { | ||||
|     //         if (session !== activeSessionId) { | ||||
|     //             updateUserLocation(session) | ||||
|     //         } | ||||
|     //     } | ||||
|     // } | ||||
|  | ||||
|  | ||||
|     function createAnnotationUser(user, pointer = false) { | ||||
|         const radius = 20 | ||||
|         const halfRadius = radius/2 | ||||
|         const group = document.createElementNS("http://www.w3.org/2000/svg","g"); | ||||
|         const badge = document.createElementNS("http://www.w3.org/2000/svg","path"); | ||||
|         let shapePath | ||||
|         if (!pointer) { | ||||
|             shapePath = `M 0 ${halfRadius} a ${halfRadius} ${halfRadius} 0 1 1 ${radius} 0 a ${halfRadius} ${halfRadius} 0 1 1 -${radius} 0 z` | ||||
|         } else { | ||||
|             shapePath = `M 0 0 h ${halfRadius} a ${halfRadius} ${halfRadius} 0 1 1 -${halfRadius} ${halfRadius} z` | ||||
|         } | ||||
|         badge.setAttribute('d', shapePath) | ||||
|         badge.setAttribute("class", "red-ui-multiplayer-annotation-background") | ||||
|         group.appendChild(badge) | ||||
|         if (user && user.profileColor !== undefined) { | ||||
|             badge.setAttribute("class", "red-ui-multiplayer-annotation-background red-ui-user-profile-color-" + user.profileColor) | ||||
|         } | ||||
|         if (user && user.image) { | ||||
|             const image = document.createElementNS("http://www.w3.org/2000/svg","image"); | ||||
|             image.setAttribute("width", radius) | ||||
|             image.setAttribute("height", radius) | ||||
|             image.setAttribute("href", user.image) | ||||
|             image.setAttribute("clip-path", "circle("+Math.floor(radius/2)+")") | ||||
|             group.appendChild(image) | ||||
|         } else if (user && user.anonymous) { | ||||
|             const anonIconHead = document.createElementNS("http://www.w3.org/2000/svg","circle"); | ||||
|             anonIconHead.setAttribute("cx", radius/2) | ||||
|             anonIconHead.setAttribute("cy", radius/2 - 2) | ||||
|             anonIconHead.setAttribute("r", 2.4) | ||||
|             anonIconHead.setAttribute("class","red-ui-multiplayer-annotation-anon-label"); | ||||
|             group.appendChild(anonIconHead) | ||||
|             const anonIconBody = document.createElementNS("http://www.w3.org/2000/svg","path"); | ||||
|             anonIconBody.setAttribute("class","red-ui-multiplayer-annotation-anon-label"); | ||||
|             // anonIconBody.setAttribute("d",`M ${radius/2 - 4} ${radius/2 + 1} h 8 v4 h -8 z`); | ||||
|             anonIconBody.setAttribute("d",`M ${radius/2} ${radius/2 + 5} h -2.5 c -2 1 -2 -5 0.5 -4.5 c 2 1 2 1 4 0 c 2.5 -0.5  2.5 5.5  0 4.5  z`); | ||||
|             group.appendChild(anonIconBody) | ||||
|         } else { | ||||
|             const labelText = user.username ? user.username.substring(0,2) : user | ||||
|             const label = document.createElementNS("http://www.w3.org/2000/svg","text"); | ||||
|             if (user.username) { | ||||
|                 label.setAttribute("class","red-ui-multiplayer-annotation-label"); | ||||
|                 label.textContent = user.username.substring(0,2) | ||||
|             } else { | ||||
|                 label.setAttribute("class","red-ui-multiplayer-annotation-label red-ui-multiplayer-user-count") | ||||
|                 label.textContent = user | ||||
|             } | ||||
|             label.setAttribute("text-anchor", "middle") | ||||
|             label.setAttribute("x",radius/2); | ||||
|             label.setAttribute("y",radius/2 + 3); | ||||
|             group.appendChild(label) | ||||
|         } | ||||
|         const border = document.createElementNS("http://www.w3.org/2000/svg","path"); | ||||
|         border.setAttribute('d', shapePath) | ||||
|         border.setAttribute("class", "red-ui-multiplayer-annotation-border") | ||||
|         group.appendChild(border) | ||||
|         return group | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         init: function () { | ||||
|  | ||||
|              | ||||
|              | ||||
|             RED.view.annotations.register("red-ui-multiplayer",{ | ||||
|                 type: 'badge', | ||||
|                 align: 'left', | ||||
|                 class: "red-ui-multiplayer-annotation", | ||||
|                 show: "_multiplayer", | ||||
|                 refresh: "_multiplayer_refresh", | ||||
|                 element: function(node) { | ||||
|                     const containerGroup = document.createElementNS("http://www.w3.org/2000/svg","g"); | ||||
|                     containerGroup.setAttribute("transform","translate(0,-4)") | ||||
|                     if (node._multiplayer) { | ||||
|                         let y = 0 | ||||
|                         for (let i = Math.min(1, node._multiplayer.users.length - 1); i >= 0; i--) { | ||||
|                             const user = sessions[node._multiplayer.users[i]].user | ||||
|                             const group = createAnnotationUser(user) | ||||
|                             group.setAttribute("transform","translate("+y+",0)") | ||||
|                             y += 15 | ||||
|                             containerGroup.appendChild(group) | ||||
|                         } | ||||
|                         if (node._multiplayer.users.length > 2) { | ||||
|                             const group = createAnnotationUser('+'+(node._multiplayer.users.length - 2)) | ||||
|                             group.setAttribute("transform","translate("+y+",0)") | ||||
|                             y += 12 | ||||
|                             containerGroup.appendChild(group) | ||||
|                         } | ||||
|  | ||||
|                     } | ||||
|                     return containerGroup; | ||||
|                 }, | ||||
|                 tooltip: node => { return node._multiplayer.users.map(u => sessions[u].user.username).join('\n') } | ||||
|             }); | ||||
|  | ||||
|  | ||||
|             // activeSessionId = RED.settings.getLocal('multiplayer:sessionId') | ||||
|             // if (!activeSessionId) { | ||||
|                 activeSessionId = RED.nodes.id() | ||||
|             //     RED.settings.setLocal('multiplayer:sessionId', activeSessionId) | ||||
|             //     log('Session ID (new)', activeSessionId) | ||||
|             // } else { | ||||
|                 log('Session ID', activeSessionId) | ||||
|             // } | ||||
|              | ||||
|             headerWidget = $('<li><ul id="red-ui-multiplayer-user-list"></ul></li>').prependTo('.red-ui-header-toolbar') | ||||
|  | ||||
|             RED.comms.on('connect', () => { | ||||
|                 const location = getLocation() | ||||
|                 const connectInfo = { | ||||
|                     session: activeSessionId | ||||
|                 } | ||||
|                 if (location.workspace !== 0) { | ||||
|                     connectInfo.location = location | ||||
|                 } | ||||
|                 RED.comms.send('multiplayer/connect', connectInfo) | ||||
|             }) | ||||
|             RED.comms.subscribe('multiplayer/#', (topic, msg) => { | ||||
|                 log('recv', topic, msg) | ||||
|                 if (topic === 'multiplayer/init') { | ||||
|                     // We have just reconnected, runtime has sent state to | ||||
|                     // initialise the world | ||||
|                     sessions = {} | ||||
|                     users = {} | ||||
|                     $('#red-ui-multiplayer-user-list').empty() | ||||
|  | ||||
|                     msg.sessions.forEach(session => { | ||||
|                         addUserSession(session) | ||||
|                     }) | ||||
|                 } else if (topic === 'multiplayer/connection-added') { | ||||
|                     addUserSession(msg) | ||||
|                 } else if (topic === 'multiplayer/connection-removed') { | ||||
|                     removeUserSession(msg.session, msg.disconnected) | ||||
|                 } else if (topic === 'multiplayer/location') { | ||||
|                     const session = msg.session | ||||
|                     delete msg.session | ||||
|                     updateUserLocation(session, msg) | ||||
|                 } | ||||
|             }) | ||||
|  | ||||
|             RED.events.on('workspace:change', (event) => { | ||||
|                 getWorkspaceTray(event.workspace) | ||||
|                 publishLocation() | ||||
|             }) | ||||
|             RED.events.on('editor:open', () => { | ||||
|                 publishLocation() | ||||
|             }) | ||||
|             RED.events.on('editor:close', () => { | ||||
|                 publishLocation() | ||||
|             }) | ||||
|             RED.events.on('editor:change', () => { | ||||
|                 publishLocation() | ||||
|             }) | ||||
|             RED.events.on('login', () => { | ||||
|                 publishLocation() | ||||
|             }) | ||||
|             RED.events.on('flows:loaded', () => { | ||||
|                 attachWorkspaceTrays() | ||||
|             }) | ||||
|             RED.events.on('workspace:close', (event) => { | ||||
|                 // A subflow tab has been closed. Need to mark its tray as detached | ||||
|                 if (workspaceTrays[event.workspace]) { | ||||
|                     workspaceTrays[event.workspace].attached = false | ||||
|                 } | ||||
|             }) | ||||
|             RED.events.on('logout', () => { | ||||
|                 const disconnectInfo = { | ||||
|                     session: activeSessionId | ||||
|                 } | ||||
|                 RED.comms.send('multiplayer/disconnect', disconnectInfo) | ||||
|                 RED.settings.removeLocal('multiplayer:sessionId') | ||||
|             }) | ||||
|              | ||||
|             const chart = $('#red-ui-workspace-chart') | ||||
|             chart.on('mousemove', function (evt) { | ||||
|                 lastPosition[0] = evt.clientX | ||||
|                 lastPosition[1] = evt.clientY | ||||
|                 publishLocation() | ||||
|             }) | ||||
|             chart.on('scroll', function (evt) { | ||||
|                 publishLocation() | ||||
|             }) | ||||
|             chart.on('mouseenter', function () { | ||||
|                 isInWorkspace = true | ||||
|                 publishLocation() | ||||
|             }) | ||||
|             chart.on('mouseleave', function () { | ||||
|                 isInWorkspace = false | ||||
|                 publishLocation() | ||||
|             }) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function log() { | ||||
|         if (RED.multiplayer.DEBUG) { | ||||
|             console.log('[multiplayer]', ...arguments) | ||||
|         } | ||||
|     } | ||||
| })(); | ||||
| @@ -73,7 +73,13 @@ RED.nodes = (function() { | ||||
|  | ||||
|         var exports = { | ||||
|             setModulePendingUpdated: function(module,version) { | ||||
|                 moduleList[module].pending_version = version; | ||||
|                 if (!!RED.plugins.getModule(module)) { | ||||
|                     // The module updated is a plugin | ||||
|                     RED.plugins.getModule(module).pending_version = version; | ||||
|                 } else { | ||||
|                     moduleList[module].pending_version = version; | ||||
|                 } | ||||
|  | ||||
|                 RED.events.emit("registry:module-updated",{module:module,version:version}); | ||||
|             }, | ||||
|             getModule: function(module) { | ||||
| @@ -91,6 +97,31 @@ RED.nodes = (function() { | ||||
|             getNodeTypes: function() { | ||||
|                 return Object.keys(nodeDefinitions); | ||||
|             }, | ||||
|             /** | ||||
|              * Get an array of node definitions | ||||
|              * @param {Object} options - options object | ||||
|              * @param {boolean} [options.configOnly] - if true, only return config nodes | ||||
|              * @param {function} [options.filter] - a filter function to apply to the list of nodes | ||||
|              * @returns array of node definitions | ||||
|              */ | ||||
|             getNodeDefinitions: function(options) { | ||||
|                 const result = [] | ||||
|                 const configOnly = (options && options.configOnly) | ||||
|                 const filter = (options && options.filter) | ||||
|                 const keys = Object.keys(nodeDefinitions) | ||||
|                 for (const key of keys) { | ||||
|                     const def = nodeDefinitions[key] | ||||
|                     if(!def) { continue } | ||||
|                     if (configOnly && def.category !== "config") { | ||||
|                             continue | ||||
|                     } | ||||
|                     if (filter && !filter(nodeDefinitions[key])) { | ||||
|                         continue | ||||
|                     } | ||||
|                     result.push(nodeDefinitions[key]) | ||||
|                 } | ||||
|                 return result | ||||
|             }, | ||||
|             setNodeList: function(list) { | ||||
|                 nodeList = []; | ||||
|                 for(var i=0;i<list.length;i++) { | ||||
| @@ -124,6 +155,8 @@ RED.nodes = (function() { | ||||
|             }, | ||||
|             removeNodeSet: function(id) { | ||||
|                 var ns = nodeSets[id]; | ||||
|                 if (!ns) { return {} } | ||||
|  | ||||
|                 for (var j=0;j<ns.types.length;j++) { | ||||
|                     delete typeToId[ns.types[j]]; | ||||
|                 } | ||||
| @@ -547,12 +580,16 @@ RED.nodes = (function() { | ||||
|              * @param {String} z tab id | ||||
|              */ | ||||
|             checkTabState: function (z) { | ||||
|                 const ws = workspaces[z] | ||||
|                 const ws = workspaces[z] || subflows[z] | ||||
|                 if (ws) { | ||||
|                     const contentsChanged = tabDirtyMap[z].size > 0 || tabDeletedNodesMap[z].size > 0 | ||||
|                     if (Boolean(ws.contentsChanged) !== contentsChanged) { | ||||
|                         ws.contentsChanged = contentsChanged | ||||
|                         RED.events.emit("flows:change", ws); | ||||
|                         if (ws.type === 'tab') { | ||||
|                             RED.events.emit("flows:change", ws); | ||||
|                         } else { | ||||
|                             RED.events.emit("subflows:change", ws); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| @@ -670,12 +707,15 @@ RED.nodes = (function() { | ||||
|             } | ||||
|             n["_"] = RED._; | ||||
|         } | ||||
|  | ||||
|         // Both node and config node can use a config node | ||||
|         updateConfigNodeUsers(newNode, { action: "add" }); | ||||
|  | ||||
|         if (n._def.category == "config") { | ||||
|             configNodes[n.id] = n; | ||||
|             configNodes[n.id] = newNode; | ||||
|         } else { | ||||
|             if (n.wires && (n.wires.length > n.outputs)) { n.outputs = n.wires.length; } | ||||
|             n.dirty = true; | ||||
|             updateConfigNodeUsers(n); | ||||
|             if (n._def.category == "subflows" && typeof n.i === "undefined") { | ||||
|                 var nextId = 0; | ||||
|                 RED.nodes.eachNode(function(node) { | ||||
| @@ -737,9 +777,11 @@ RED.nodes = (function() { | ||||
|         var removedLinks = []; | ||||
|         var removedNodes = []; | ||||
|         var node; | ||||
|  | ||||
|         if (id in configNodes) { | ||||
|             node = configNodes[id]; | ||||
|             delete configNodes[id]; | ||||
|             updateConfigNodeUsers(node, { action: "remove" }); | ||||
|             RED.events.emit('nodes:remove',node); | ||||
|             RED.workspaces.refresh(); | ||||
|         } else if (allNodes.hasNode(id)) { | ||||
| @@ -748,6 +790,9 @@ RED.nodes = (function() { | ||||
|             delete nodeLinks[id]; | ||||
|             removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); }); | ||||
|             removedLinks.forEach(removeLink); | ||||
|             updateConfigNodeUsers(node, { action: "remove" }); | ||||
|  | ||||
|             // TODO: Legacy code for exclusive config node | ||||
|             var updatedConfigNode = false; | ||||
|             for (var d in node._def.defaults) { | ||||
|                 if (node._def.defaults.hasOwnProperty(d)) { | ||||
| @@ -761,10 +806,6 @@ RED.nodes = (function() { | ||||
|                                 if (configNode._def.exclusive) { | ||||
|                                     removeNode(node[d]); | ||||
|                                     removedNodes.push(configNode); | ||||
|                                 } else { | ||||
|                                     var users = configNode.users; | ||||
|                                     users.splice(users.indexOf(node),1); | ||||
|                                     RED.events.emit('nodes:change',configNode) | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
| @@ -1001,23 +1042,34 @@ RED.nodes = (function() { | ||||
|         return {nodes:removedNodes,links:removedLinks, groups: removedGroups, junctions: removedJunctions}; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|       * Add a Subflow to the Workspace | ||||
|       * | ||||
|       * @param {object} sf The Subflow to add. | ||||
|       * @param {boolean|undefined} createNewIds Whether to update the name. | ||||
|       */ | ||||
|     function addSubflow(sf, createNewIds) { | ||||
|         if (createNewIds) { | ||||
|             var subflowNames = Object.keys(subflows).map(function(sfid) { | ||||
|                 return subflows[sfid].name; | ||||
|             }); | ||||
|             // Update the Subflow name to highlight that this is a copy | ||||
|             const subflowNames = Object.keys(subflows).map(function (sfid) { | ||||
|                 return subflows[sfid].name || ""; | ||||
|             }) | ||||
|             subflowNames.sort() | ||||
|  | ||||
|             subflowNames.sort(); | ||||
|             var copyNumber = 1; | ||||
|             var subflowName = sf.name; | ||||
|             let copyNumber = 1; | ||||
|             let subflowName = sf.name; | ||||
|             subflowNames.forEach(function(name) { | ||||
|                 if (subflowName == name) { | ||||
|                     subflowName = sf.name + " (" + copyNumber + ")"; | ||||
|                     copyNumber++; | ||||
|                     subflowName = sf.name+" ("+copyNumber+")"; | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             sf.name = subflowName; | ||||
|         } | ||||
|  | ||||
|         sf.instances = []; | ||||
|  | ||||
|         subflows[sf.id] = sf; | ||||
|         allNodes.addTab(sf.id); | ||||
|         linkTabMap[sf.id] = []; | ||||
| @@ -1025,7 +1077,22 @@ RED.nodes = (function() { | ||||
|         RED.nodes.registerType("subflow:"+sf.id, { | ||||
|             defaults:{ | ||||
|                 name:{value:""}, | ||||
|                 env:{value:[]} | ||||
|                 env:{value:[], validate: function(value) { | ||||
|                     const errors = [] | ||||
|                     if (value) { | ||||
|                         value.forEach(env => { | ||||
|                             const r = RED.utils.validateTypedProperty(env.value, env.type) | ||||
|                             if (r !== true) { | ||||
|                                 errors.push(env.name+': '+r) | ||||
|                             } | ||||
|                         }) | ||||
|                     } | ||||
|                     if (errors.length === 0) { | ||||
|                         return true | ||||
|                     } else { | ||||
|                         return errors | ||||
|                     } | ||||
|                 }} | ||||
|             }, | ||||
|             icon: function() { return sf.icon||"subflow.svg" }, | ||||
|             category: sf.category || "subflows", | ||||
| @@ -1055,7 +1122,7 @@ RED.nodes = (function() { | ||||
|                 module: "node-red" | ||||
|             } | ||||
|         }); | ||||
|         sf.instances = []; | ||||
|  | ||||
|         sf._def = RED.nodes.getType("subflow:"+sf.id); | ||||
|         RED.events.emit("subflows:add",sf); | ||||
|     } | ||||
| @@ -1697,7 +1764,8 @@ RED.nodes = (function() { | ||||
|             // Remove the old subflow definition - but leave the instances in place | ||||
|             var removalResult = RED.subflow.removeSubflow(n.id, true); | ||||
|             // Create the list of nodes for the new subflow def | ||||
|             var subflowNodes = [n].concat(zMap[n.id]); | ||||
|             // Need to sort the list in order to remove missing nodes | ||||
|             var subflowNodes = [n].concat(zMap[n.id]).filter((s) => !!s); | ||||
|             // Import the new subflow - no clashes should occur as we've removed | ||||
|             // the old version | ||||
|             var result = importNodes(subflowNodes); | ||||
| @@ -1734,9 +1802,20 @@ RED.nodes = (function() { | ||||
|         // Replace config nodes | ||||
|         // | ||||
|         configNodeIds.forEach(function(id) { | ||||
|             removedNodes = removedNodes.concat(convertNode(getNode(id))); | ||||
|             const configNode = getNode(id); | ||||
|             const currentUserCount = configNode.users; | ||||
|  | ||||
|             // Add a snapshot of the Config Node | ||||
|             removedNodes = removedNodes.concat(convertNode(configNode)); | ||||
|  | ||||
|             // Remove the Config Node instance | ||||
|             removeNode(id); | ||||
|             importNodes([newConfigNodes[id]]) | ||||
|  | ||||
|             // Import the new one | ||||
|             importNodes([newConfigNodes[id]]); | ||||
|  | ||||
|             // Re-attributes the user count | ||||
|             getNode(id).users = currentUserCount; | ||||
|         }); | ||||
|  | ||||
|         return { | ||||
| @@ -1977,6 +2056,8 @@ RED.nodes = (function() { | ||||
|                 if (matchingSubflow) { | ||||
|                     subflow_denylist[n.id] = matchingSubflow; | ||||
|                 } else { | ||||
|                     const oldId = n.id; | ||||
|  | ||||
|                     subflow_map[n.id] = n; | ||||
|                     if (createNewIds || options.importMap[n.id] === "copy") { | ||||
|                         nid = getID(); | ||||
| @@ -2004,7 +2085,7 @@ RED.nodes = (function() { | ||||
|                         n.status.id = getID(); | ||||
|                     } | ||||
|                     new_subflows.push(n); | ||||
|                     addSubflow(n,createNewIds || options.importMap[n.id] === "copy"); | ||||
|                     addSubflow(n,createNewIds || options.importMap[oldId] === "copy"); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| @@ -2018,6 +2099,8 @@ RED.nodes = (function() { | ||||
|             activeWorkspace = RED.workspaces.active(); | ||||
|         } | ||||
|  | ||||
|         const pendingConfigNodes = [] | ||||
|         const pendingConfigNodeIds = new Set() | ||||
|         // Find all config nodes and add them | ||||
|         for (i=0;i<newNodes.length;i++) { | ||||
|             n = newNodes[i]; | ||||
| @@ -2077,7 +2160,8 @@ RED.nodes = (function() { | ||||
|                         type:n.type, | ||||
|                         info: n.info, | ||||
|                         users:[], | ||||
|                         _config:{} | ||||
|                         _config:{}, | ||||
|                         _configNodeReferences: new Set() | ||||
|                     }; | ||||
|                     if (!n.z) { | ||||
|                         delete configNode.z; | ||||
| @@ -2092,6 +2176,9 @@ RED.nodes = (function() { | ||||
|                         if (def.defaults.hasOwnProperty(d)) { | ||||
|                             configNode[d] = n[d]; | ||||
|                             configNode._config[d] = JSON.stringify(n[d]); | ||||
|                             if (def.defaults[d].type) { | ||||
|                                 configNode._configNodeReferences.add(n[d]) | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     if (def.hasOwnProperty('credentials') && n.hasOwnProperty('credentials')) { | ||||
| @@ -2108,11 +2195,55 @@ RED.nodes = (function() { | ||||
|                         configNode.id = getID(); | ||||
|                     } | ||||
|                     node_map[n.id] = configNode; | ||||
|                     new_nodes.push(configNode); | ||||
|                     pendingConfigNodes.push(configNode); | ||||
|                     pendingConfigNodeIds.add(configNode.id) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // We need to sort new_nodes (which only contains config nodes at this point) | ||||
|         // to ensure they get added in the right order. If NodeA depends on NodeB, then | ||||
|         // NodeB must be added first. | ||||
|          | ||||
|         // Limit us to 5 full iterations of the list - this should be more than | ||||
|         // enough to process the list as config->config node relationships are | ||||
|         // not very common | ||||
|         let iterationLimit = pendingConfigNodes.length * 5 | ||||
|         const handledConfigNodes = new Set() | ||||
|         while (pendingConfigNodes.length > 0 && iterationLimit > 0) { | ||||
|             const node = pendingConfigNodes.shift() | ||||
|             let hasPending = false | ||||
|             // Loop through the nodes referenced by this node to see if anything | ||||
|             // is pending | ||||
|             node._configNodeReferences.forEach(id => { | ||||
|                 if (pendingConfigNodeIds.has(id) && !handledConfigNodes.has(id)) { | ||||
|                     // This reference is for a node we know is in this import, but | ||||
|                     // it isn't added yet - flag as pending | ||||
|                     hasPending = true | ||||
|                 } | ||||
|             }) | ||||
|             if (!hasPending) { | ||||
|                 // This node has no pending config node references - safe to add | ||||
|                 delete node._configNodeReferences | ||||
|                 new_nodes.push(node) | ||||
|                 handledConfigNodes.add(node.id) | ||||
|             } else { | ||||
|                 // This node has pending config node references | ||||
|                 // Put to the back of the queue | ||||
|                 pendingConfigNodes.push(node) | ||||
|             } | ||||
|             iterationLimit-- | ||||
|         } | ||||
|         if (pendingConfigNodes.length > 0) { | ||||
|             // We exceeded the iteration count. Could be due to reference loops | ||||
|             // between the config nodes. At this point, just add the remaining | ||||
|             // nodes as-is | ||||
|             pendingConfigNodes.forEach(node => { | ||||
|                 delete node._configNodeReferences | ||||
|                 new_nodes.push(node) | ||||
|             }) | ||||
|         } | ||||
|  | ||||
|         // Find regular flow nodes and subflow instances | ||||
|         for (i=0;i<newNodes.length;i++) { | ||||
|             n = newNodes[i]; | ||||
| @@ -2124,7 +2255,7 @@ RED.nodes = (function() { | ||||
|                         x:parseFloat(n.x || 0), | ||||
|                         y:parseFloat(n.y || 0), | ||||
|                         z:n.z, | ||||
|                         type:0, | ||||
|                         type: n.type, | ||||
|                         info: n.info, | ||||
|                         changed:false, | ||||
|                         _config:{} | ||||
| @@ -2185,7 +2316,6 @@ RED.nodes = (function() { | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     node.type = n.type; | ||||
|                     node._def = def; | ||||
|                     if (node.type === "group") { | ||||
|                         node._def = RED.group.def; | ||||
| @@ -2215,6 +2345,15 @@ RED.nodes = (function() { | ||||
|                                 outputs: n.outputs|| (n.wires && n.wires.length) || 0, | ||||
|                                 set: registry.getNodeSet("node-red/unknown") | ||||
|                             } | ||||
|                             var orig = {}; | ||||
|                             for (var p in n) { | ||||
|                                 if (n.hasOwnProperty(p) && p!="x" && p!="y" && p!="z" && p!="id" && p!="wires") { | ||||
|                                     orig[p] = n[p]; | ||||
|                                 } | ||||
|                             } | ||||
|                             node._orig = orig; | ||||
|                             node.name = n.type; | ||||
|                             node.type = "unknown"; | ||||
|                         } else { | ||||
|                             if (subflow_denylist[parentId] || createNewIds || options.importMap[n.id] === "copy") { | ||||
|                                 parentId = subflow.id; | ||||
| @@ -2275,29 +2414,31 @@ RED.nodes = (function() { | ||||
|                             node.type = "unknown"; | ||||
|                         } | ||||
|                         if (node._def.category != "config") { | ||||
|                             if (n.hasOwnProperty('inputs')) { | ||||
|                                 node.inputs = n.inputs; | ||||
|                             if (n.hasOwnProperty('inputs') && node._def.defaults.hasOwnProperty("inputs")) { | ||||
|                                 node.inputs = parseInt(n.inputs, 10); | ||||
|                                 node._config.inputs = JSON.stringify(n.inputs); | ||||
|                             } else { | ||||
|                                 node.inputs = node._def.inputs; | ||||
|                             } | ||||
|                             if (n.hasOwnProperty('outputs')) { | ||||
|                                 node.outputs = n.outputs; | ||||
|                             if (n.hasOwnProperty('outputs') && node._def.defaults.hasOwnProperty("outputs")) { | ||||
|                                 node.outputs = parseInt(n.outputs, 10); | ||||
|                                 node._config.outputs = JSON.stringify(n.outputs); | ||||
|                             } else { | ||||
|                                 node.outputs = node._def.outputs; | ||||
|                             } | ||||
|                             if (node.hasOwnProperty('wires') && node.wires.length > node.outputs) { | ||||
|                                 if (!node._def.defaults.hasOwnProperty("outputs") || !isNaN(parseInt(n.outputs))) { | ||||
|                                     // If 'wires' is longer than outputs, clip wires | ||||
|                                     console.log("Warning: node.wires longer than node.outputs - trimming wires:",node.id," wires:",node.wires.length," outputs:",node.outputs); | ||||
|                                     node.wires = node.wires.slice(0,node.outputs); | ||||
|                                 } else { | ||||
|                                     // The node declares outputs in its defaults, but has not got a valid value | ||||
|                                     // Defer to the length of the wires array | ||||
|  | ||||
|                             // The node declares outputs in its defaults, but has not got a valid value | ||||
|                             // Defer to the length of the wires array | ||||
|                             if (node.hasOwnProperty('wires')) { | ||||
|                                 if (isNaN(node.outputs)) { | ||||
|                                     node.outputs = node.wires.length; | ||||
|                                 } else if (node.wires.length > node.outputs) { | ||||
|                                     // If 'wires' is longer than outputs, clip wires | ||||
|                                     console.log("Warning: node.wires longer than node.outputs - trimming wires:", node.id, " wires:", node.wires.length, " outputs:", node.outputs); | ||||
|                                     node.wires = node.wires.slice(0, node.outputs); | ||||
|                                 } | ||||
|                             } | ||||
|  | ||||
|                             for (d in node._def.defaults) { | ||||
|                                 if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') { | ||||
|                                     node[d] = n[d]; | ||||
| @@ -2360,6 +2501,30 @@ RED.nodes = (function() { | ||||
|             } else { | ||||
|                 delete n.g | ||||
|             } | ||||
|             // If importing a link node, ensure both ends of each link are either: | ||||
|             // - not in a subflow | ||||
|             // - both in the same subflow (not for link call node) | ||||
|             if (/^link /.test(n.type) && n.links) { | ||||
|                 n.links = n.links.filter(function(id) { | ||||
|                     const otherNode = node_map[id] || RED.nodes.node(id); | ||||
|                     if (!otherNode) { | ||||
|                         // Cannot find other end - remove the link | ||||
|                         return false | ||||
|                     } | ||||
|                     if (otherNode.z === n.z) { | ||||
|                         // Both ends in the same flow/subflow | ||||
|                         return true | ||||
|                     } else if (n.type === "link call" && !getSubflow(otherNode.z)) { | ||||
|                         // Link call node can call out of a subflow as long as otherNode is | ||||
|                         // not in a subflow | ||||
|                         return true | ||||
|                     } else if (!!getSubflow(n.z) || !!getSubflow(otherNode.z)) { | ||||
|                         // One end is in a subflow - remove the link | ||||
|                         return false | ||||
|                     } | ||||
|                     return true | ||||
|                 }); | ||||
|             } | ||||
|             for (var d3 in n._def.defaults) { | ||||
|                 if (n._def.defaults.hasOwnProperty(d3)) { | ||||
|                     if (n._def.defaults[d3].type) { | ||||
| @@ -2370,11 +2535,6 @@ RED.nodes = (function() { | ||||
|                         nodeList = nodeList.map(function(id) { | ||||
|                             var node = node_map[id]; | ||||
|                             if (node) { | ||||
|                                 if (node._def.category === 'config') { | ||||
|                                     if (node.users.indexOf(n) === -1) { | ||||
|                                         node.users.push(n); | ||||
|                                     } | ||||
|                                 } | ||||
|                                 return node.id; | ||||
|                             } | ||||
|                             return id; | ||||
| @@ -2383,22 +2543,16 @@ RED.nodes = (function() { | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             // If importing into a subflow, ensure an outbound-link doesn't | ||||
|             // get added | ||||
|             if (activeSubflow && /^link /.test(n.type) && n.links) { | ||||
|                 n.links = n.links.filter(function(id) { | ||||
|                     const otherNode = node_map[id] || RED.nodes.node(id); | ||||
|                     return (otherNode && otherNode.z === activeWorkspace) | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|         for (i=0;i<new_subflows.length;i++) { | ||||
|             n = new_subflows[i]; | ||||
|             n.in.forEach(function(input) { | ||||
|                 input.wires.forEach(function(wire) { | ||||
|                     var link = {source:input, sourcePort:0, target:node_map[wire.id]}; | ||||
|                     addLink(link); | ||||
|                     new_links.push(link); | ||||
|                     if (node_map.hasOwnProperty(wire.id)) { | ||||
|                         var link = {source:input, sourcePort:0, target:node_map[wire.id]}; | ||||
|                         addLink(link); | ||||
|                         new_links.push(link); | ||||
|                     } | ||||
|                 }); | ||||
|                 delete input.wires; | ||||
|             }); | ||||
| @@ -2407,11 +2561,13 @@ RED.nodes = (function() { | ||||
|                     var link; | ||||
|                     if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) { | ||||
|                         link = {source:n.in[wire.port], sourcePort:wire.port,target:output}; | ||||
|                     } else { | ||||
|                     } else if (node_map.hasOwnProperty(wire.id) || subflow_map.hasOwnProperty(wire.id)) { | ||||
|                         link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:output}; | ||||
|                     } | ||||
|                     addLink(link); | ||||
|                     new_links.push(link); | ||||
|                     if (link) { | ||||
|                         addLink(link); | ||||
|                         new_links.push(link); | ||||
|                     } | ||||
|                 }); | ||||
|                 delete output.wires; | ||||
|             }); | ||||
| @@ -2420,11 +2576,13 @@ RED.nodes = (function() { | ||||
|                     var link; | ||||
|                     if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) { | ||||
|                         link = {source:n.in[wire.port], sourcePort:wire.port,target:n.status}; | ||||
|                     } else { | ||||
|                     } else if (node_map.hasOwnProperty(wire.id) || subflow_map.hasOwnProperty(wire.id)) { | ||||
|                         link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:n.status}; | ||||
|                     } | ||||
|                     addLink(link); | ||||
|                     new_links.push(link); | ||||
|                     if (link) { | ||||
|                         addLink(link); | ||||
|                         new_links.push(link); | ||||
|                     } | ||||
|                 }); | ||||
|                 delete n.status.wires; | ||||
|             } | ||||
| @@ -2603,25 +2761,79 @@ RED.nodes = (function() { | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     // Update any config nodes referenced by the provided node to ensure their 'users' list is correct | ||||
|     function updateConfigNodeUsers(n) { | ||||
|         for (var d in n._def.defaults) { | ||||
|             if (n._def.defaults.hasOwnProperty(d)) { | ||||
|                 var property = n._def.defaults[d]; | ||||
|     /** | ||||
|      * Update any config nodes referenced by the provided node to ensure | ||||
|      * their 'users' list is correct. | ||||
|      * | ||||
|      * @param {object} node The node in which to check if it contains references | ||||
|      * @param {object} options Options to apply. | ||||
|      * @param {"add" | "remove"} [options.action] Add or remove the node from | ||||
|      * the Config Node users list. Default `add`. | ||||
|      * @param {boolean} [options.emitEvent] Emit the `nodes:changes` event. | ||||
|      * Default true. | ||||
|      */ | ||||
|     function updateConfigNodeUsers(node, options) { | ||||
|         const defaultOptions = { action: "add", emitEvent: true }; | ||||
|         options = Object.assign({}, defaultOptions, options); | ||||
|  | ||||
|         for (var d in node._def.defaults) { | ||||
|             if (node._def.defaults.hasOwnProperty(d)) { | ||||
|                 var property = node._def.defaults[d]; | ||||
|                 if (property.type) { | ||||
|                     var type = registry.getNodeType(property.type); | ||||
|                     // Need to ensure the type is a config node to not treat links nodes | ||||
|                     if (type && type.category == "config") { | ||||
|                         var configNode = configNodes[n[d]]; | ||||
|                         var configNode = configNodes[node[d]]; | ||||
|                         if (configNode) { | ||||
|                             if (configNode.users.indexOf(n) === -1) { | ||||
|                                 configNode.users.push(n); | ||||
|                                 RED.events.emit('nodes:change',configNode) | ||||
|                             if (options.action === "add") { | ||||
|                                 if (configNode.users.indexOf(node) === -1) { | ||||
|                                     configNode.users.push(node); | ||||
|                                     if (options.emitEvent) { | ||||
|                                         RED.events.emit('nodes:change', configNode); | ||||
|                                     } | ||||
|                                 } | ||||
|                             } else if (options.action === "remove") { | ||||
|                                 if (configNode.users.indexOf(node) !== -1) { | ||||
|                                     const users = configNode.users; | ||||
|                                     users.splice(users.indexOf(node), 1); | ||||
|                                     if (options.emitEvent) { | ||||
|                                         RED.events.emit('nodes:change', configNode); | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Subflows can have config node env | ||||
|         if (node.type.indexOf("subflow:") === 0) { | ||||
|             node.env?.forEach((prop) => { | ||||
|                 if (prop.type === "conf-type" && prop.value) { | ||||
|                     // Add the node to the config node users | ||||
|                     const configNode = getNode(prop.value); | ||||
|                     if (configNode) { | ||||
|                         if (options.action === "add") { | ||||
|                             if (configNode.users.indexOf(node) === -1) { | ||||
|                                 configNode.users.push(node); | ||||
|                                 if (options.emitEvent) { | ||||
|                                     RED.events.emit('nodes:change', configNode); | ||||
|                                 } | ||||
|                             } | ||||
|                         } else if (options.action === "remove") { | ||||
|                             if (configNode.users.indexOf(node) !== -1) { | ||||
|                                 const users = configNode.users; | ||||
|                                 users.splice(users.indexOf(node), 1); | ||||
|                                 if (options.emitEvent) { | ||||
|                                     RED.events.emit('nodes:change', configNode); | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function flowVersion(version) { | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| RED.plugins = (function() { | ||||
|     var plugins = {}; | ||||
|     var pluginsByType = {}; | ||||
|     var moduleList = {}; | ||||
|  | ||||
|     function registerPlugin(id,definition) { | ||||
|         plugins[id] = definition; | ||||
| @@ -38,9 +39,43 @@ RED.plugins = (function() { | ||||
|     function getPluginsByType(type) { | ||||
|         return pluginsByType[type] || []; | ||||
|     } | ||||
|  | ||||
|     function setPluginList(list) { | ||||
|         for(let i=0;i<list.length;i++) { | ||||
|             let p = list[i]; | ||||
|             addPlugin(p); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function addPlugin(p) { | ||||
|  | ||||
|         moduleList[p.module] = moduleList[p.module] || { | ||||
|             name:p.module, | ||||
|             version:p.version, | ||||
|             local:p.local, | ||||
|             sets:{}, | ||||
|             plugin: true, | ||||
|             id: p.id | ||||
|         }; | ||||
|         if (p.pending_version) { | ||||
|             moduleList[p.module].pending_version = p.pending_version; | ||||
|         } | ||||
|         moduleList[p.module].sets[p.name] = p; | ||||
|  | ||||
|         RED.events.emit("registry:plugin-module-added",p.module); | ||||
|     } | ||||
|  | ||||
|     function getModule(module) { | ||||
|         return moduleList[module]; | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         registerPlugin: registerPlugin, | ||||
|         getPlugin: getPlugin, | ||||
|         getPluginsByType: getPluginsByType | ||||
|         getPluginsByType: getPluginsByType, | ||||
|  | ||||
|         setPluginList: setPluginList, | ||||
|         addPlugin: addPlugin, | ||||
|         getModule: getModule | ||||
|     } | ||||
| })(); | ||||
|   | ||||
| @@ -25,6 +25,7 @@ var RED = (function() { | ||||
|             cache: false, | ||||
|             url: 'plugins', | ||||
|             success: function(data) { | ||||
|                 RED.plugins.setPluginList(data); | ||||
|                 loader.reportProgress(RED._("event.loadPlugins"), 13) | ||||
|                 RED.i18n.loadPluginCatalogs(function() { | ||||
|                     loadPlugins(function() { | ||||
| @@ -297,6 +298,7 @@ var RED = (function() { | ||||
|                                 RED.workspaces.show(workspaces[0]); | ||||
|                             } | ||||
|                         } | ||||
|                         RED.events.emit('flows:loaded') | ||||
|                     } catch(err) { | ||||
|                         console.warn(err); | ||||
|                         RED.notify( | ||||
| @@ -534,6 +536,41 @@ var RED = (function() { | ||||
|                 RED.view.redrawStatus(node); | ||||
|             } | ||||
|         }); | ||||
|         RED.comms.subscribe("notification/plugin/#",function(topic,msg) { | ||||
|             if (topic == "notification/plugin/added") { | ||||
|                 RED.settings.refreshSettings(function(err, data) { | ||||
|                     let addedPlugins = []; | ||||
|                     msg.forEach(function(m) { | ||||
|                         let id = m.id; | ||||
|                         RED.plugins.addPlugin(m); | ||||
|  | ||||
|                         m.plugins.forEach((p) => { | ||||
|                             addedPlugins.push(p.id); | ||||
|                         }) | ||||
|  | ||||
|                         RED.i18n.loadNodeCatalog(id, function() { | ||||
|                             var lang = localStorage.getItem("editor-language")||RED.i18n.detectLanguage(); | ||||
|                             $.ajax({ | ||||
|                                 headers: { | ||||
|                                     "Accept":"text/html", | ||||
|                                     "Accept-Language": lang | ||||
|                                 }, | ||||
|                                 cache: false, | ||||
|                                 url: 'plugins/'+id, | ||||
|                                 success: function(data) { | ||||
|                                     appendPluginConfig(data); | ||||
|                                 } | ||||
|                             }); | ||||
|                         }); | ||||
|                     }); | ||||
|                     if (addedPlugins.length) { | ||||
|                         let pluginList = "<ul><li>"+addedPlugins.map(RED.utils.sanitize).join("</li><li>")+"</li></ul>"; | ||||
|                         // ToDo: Adapt notification (node -> plugin) | ||||
|                         RED.notify(RED._("palette.event.nodeAdded", {count:addedPlugins.length})+pluginList,"success"); | ||||
|                     } | ||||
|                 }) | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         let pendingNodeRemovedNotifications = [] | ||||
|         let pendingNodeRemovedTimeout | ||||
| @@ -803,6 +840,10 @@ var RED = (function() { | ||||
|  | ||||
|         RED.nodes.init(); | ||||
|         RED.runtime.init() | ||||
|  | ||||
|         if (RED.settings.theme("multiplayer.enabled",false)) { | ||||
|             RED.multiplayer.init() | ||||
|         } | ||||
|         RED.comms.connect(); | ||||
|  | ||||
|         $("#red-ui-main-container").show(); | ||||
|   | ||||
| @@ -205,7 +205,9 @@ RED.actionList = (function() { | ||||
|     } | ||||
|  | ||||
|     function init() { | ||||
|         RED.actions.add("core:show-action-list",show); | ||||
|         if (RED.settings.theme("menu.menu-item-action-list", true)) { | ||||
|             RED.actions.add("core:show-action-list",show); | ||||
|         } | ||||
|  | ||||
|         RED.events.on("editor:open",function() { disabled = true; }); | ||||
|         RED.events.on("editor:close",function() { disabled = false; }); | ||||
|   | ||||
| @@ -26,6 +26,7 @@ RED.clipboard = (function() { | ||||
|     var currentPopoverError; | ||||
|     var activeTab; | ||||
|     var libraryBrowser; | ||||
|     var clipboardTabs; | ||||
|  | ||||
|     var activeLibraries = {}; | ||||
|  | ||||
| @@ -215,6 +216,13 @@ RED.clipboard = (function() { | ||||
|                 open: function( event, ui ) { | ||||
|                     RED.keyboard.disable(); | ||||
|                 }, | ||||
|                 beforeClose: function(e) { | ||||
|                     if (clipboardTabs && activeTab === "red-ui-clipboard-dialog-export-tab-clipboard") { | ||||
|                         const jsonTabIndex = clipboardTabs.getTabIndex('red-ui-clipboard-dialog-export-tab-clipboard-json') | ||||
|                         const activeTabIndex = clipboardTabs.activeIndex() | ||||
|                         RED.settings.set("editor.dialog.export.json-view", activeTabIndex === jsonTabIndex ) | ||||
|                     } | ||||
|                 }, | ||||
|                 close: function(e) { | ||||
|                     RED.keyboard.enable(); | ||||
|                     if (popover) { | ||||
| @@ -228,12 +236,23 @@ RED.clipboard = (function() { | ||||
|  | ||||
|         exportNodesDialog = | ||||
|             '<div class="form-row">'+ | ||||
|                 '<label style="width:auto;margin-right: 10px;" data-i18n="common.label.export"></label>'+ | ||||
|                 '<span id="red-ui-clipboard-dialog-export-rng-group" class="button-group">'+ | ||||
|                     '<a id="red-ui-clipboard-dialog-export-rng-selected" class="red-ui-button toggle" href="#" data-i18n="clipboard.export.selected"></a>'+ | ||||
|                     '<a id="red-ui-clipboard-dialog-export-rng-flow" class="red-ui-button toggle" href="#" data-i18n="clipboard.export.current"></a>'+ | ||||
|                     '<a id="red-ui-clipboard-dialog-export-rng-full" class="red-ui-button toggle" href="#" data-i18n="clipboard.export.all"></a>'+ | ||||
|                 '</span>'+ | ||||
|                 '<div style="display: flex; justify-content: space-between;">'+ | ||||
|                     '<div class="form-row">'+ | ||||
|                         '<label style="width:auto;margin-right: 10px;" data-i18n="common.label.export"></label>'+ | ||||
|                         '<span id="red-ui-clipboard-dialog-export-rng-group" class="button-group">'+ | ||||
|                             '<a id="red-ui-clipboard-dialog-export-rng-selected" class="red-ui-button toggle" href="#" data-i18n="clipboard.export.selected"></a>'+ | ||||
|                             '<a id="red-ui-clipboard-dialog-export-rng-flow" class="red-ui-button toggle" href="#" data-i18n="clipboard.export.current"></a>'+ | ||||
|                             '<a id="red-ui-clipboard-dialog-export-rng-full" class="red-ui-button toggle" href="#" data-i18n="clipboard.export.all"></a>'+ | ||||
|                         '</span>'+ | ||||
|                     '</div>'+ | ||||
|                     '<div class="form-row">'+ | ||||
|                         '<label style="width:auto;margin-right: 10px;" data-i18n="common.label.format"></label>'+ | ||||
|                         '<span id="red-ui-clipboard-dialog-export-fmt-group" class="button-group">'+ | ||||
|                             '<a id="red-ui-clipboard-dialog-export-fmt-mini" class="red-ui-button red-ui-button toggle" href="#" data-i18n="clipboard.export.compact"></a>'+ | ||||
|                             '<a id="red-ui-clipboard-dialog-export-fmt-full" class="red-ui-button red-ui-button toggle" href="#" data-i18n="clipboard.export.formatted"></a>'+ | ||||
|                         '</span>'+ | ||||
|                     '</div>'+ | ||||
|                 '</div>'+ | ||||
|             '</div>'+ | ||||
|             '<div class="red-ui-clipboard-dialog-box">'+ | ||||
|                 '<div class="red-ui-clipboard-dialog-tabs">'+ | ||||
| @@ -248,15 +267,9 @@ RED.clipboard = (function() { | ||||
|                             '<div id="red-ui-clipboard-dialog-export-tab-clipboard-preview-list"></div>'+ | ||||
|                         '</div>'+ | ||||
|                         '<div class="red-ui-clipboard-dialog-export-tab-clipboard-tab" id="red-ui-clipboard-dialog-export-tab-clipboard-json">'+ | ||||
|                             '<div class="form-row" style="height:calc(100% - 40px)">'+ | ||||
|                             '<div class="form-row" style="height:calc(100% - 10px)">'+ | ||||
|                                 '<textarea readonly id="red-ui-clipboard-dialog-export-text"></textarea>'+ | ||||
|                             '</div>'+ | ||||
|                             '<div class="form-row" style="text-align: right;">'+ | ||||
|                                 '<span id="red-ui-clipboard-dialog-export-fmt-group" class="button-group">'+ | ||||
|                                     '<a id="red-ui-clipboard-dialog-export-fmt-mini" class="red-ui-button red-ui-button-small toggle" href="#" data-i18n="clipboard.export.compact"></a>'+ | ||||
|                                     '<a id="red-ui-clipboard-dialog-export-fmt-full" class="red-ui-button red-ui-button-small toggle" href="#" data-i18n="clipboard.export.formatted"></a>'+ | ||||
|                                 '</span>'+ | ||||
|                             '</div>'+ | ||||
|                         '</div>'+ | ||||
|                     '</div>'+ | ||||
|                     '<div class="form-row" id="red-ui-clipboard-dialog-export-tab-library-filename">'+ | ||||
| @@ -321,6 +334,30 @@ RED.clipboard = (function() { | ||||
|         },100); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Validates if the provided string looks like valid flow json | ||||
|      * @param {string} flowString the string to validate | ||||
|      * @returns If valid, returns the node array | ||||
|      */ | ||||
|     function validateFlowString(flowString) { | ||||
|         const res = JSON.parse(flowString) | ||||
|         if (!Array.isArray(res)) { | ||||
|             throw new Error(RED._("clipboard.import.errors.notArray")); | ||||
|         } | ||||
|         for (let i = 0; i < res.length; i++) { | ||||
|             if (typeof res[i] !== "object") { | ||||
|                 throw new Error(RED._("clipboard.import.errors.itemNotObject",{index:i})); | ||||
|             } | ||||
|             if (!Object.hasOwn(res[i], 'id')) { | ||||
|                 throw new Error(RED._("clipboard.import.errors.missingId",{index:i})); | ||||
|             } | ||||
|             if (!Object.hasOwn(res[i], 'type')) { | ||||
|                 throw new Error(RED._("clipboard.import.errors.missingType",{index:i})); | ||||
|             } | ||||
|         } | ||||
|         return res | ||||
|     } | ||||
|  | ||||
|     var validateImportTimeout; | ||||
|     function validateImport() { | ||||
|         if (activeTab === "red-ui-clipboard-dialog-import-tab-clipboard") { | ||||
| @@ -338,21 +375,7 @@ RED.clipboard = (function() { | ||||
|                     return; | ||||
|                 } | ||||
|                 try { | ||||
|                     if (!/^\[[\s\S]*\]$/m.test(v)) { | ||||
|                         throw new Error(RED._("clipboard.import.errors.notArray")); | ||||
|                     } | ||||
|                     var res = JSON.parse(v); | ||||
|                     for (var i=0;i<res.length;i++) { | ||||
|                         if (typeof res[i] !== "object") { | ||||
|                             throw new Error(RED._("clipboard.import.errors.itemNotObject",{index:i})); | ||||
|                         } | ||||
|                         if (!res[i].hasOwnProperty('id')) { | ||||
|                             throw new Error(RED._("clipboard.import.errors.missingId",{index:i})); | ||||
|                         } | ||||
|                         if (!res[i].hasOwnProperty('type')) { | ||||
|                             throw new Error(RED._("clipboard.import.errors.missingType",{index:i})); | ||||
|                         } | ||||
|                     } | ||||
|                     validateFlowString(v) | ||||
|                     currentPopoverError = null; | ||||
|                     popover.close(true); | ||||
|                     importInput.removeClass("input-error"); | ||||
| @@ -569,7 +592,7 @@ RED.clipboard = (function() { | ||||
|  | ||||
|         dialogContainer.empty(); | ||||
|         dialogContainer.append($(exportNodesDialog)); | ||||
|  | ||||
|         clipboardTabs = null | ||||
|         var tabs = RED.tabs.create({ | ||||
|             id: "red-ui-clipboard-dialog-export-tabs", | ||||
|             vertical: true, | ||||
| @@ -630,7 +653,7 @@ RED.clipboard = (function() { | ||||
|         $("#red-ui-clipboard-dialog-tab-library-name").on('paste',function() { setTimeout(validateExportFilename,10)}); | ||||
|         $("#red-ui-clipboard-dialog-export").button("enable"); | ||||
|  | ||||
|         var clipboardTabs = RED.tabs.create({ | ||||
|         clipboardTabs = RED.tabs.create({ | ||||
|             id: "red-ui-clipboard-dialog-export-tab-clipboard-tabs", | ||||
|             onchange: function(tab) { | ||||
|                 $(".red-ui-clipboard-dialog-export-tab-clipboard-tab").hide(); | ||||
| @@ -647,6 +670,9 @@ RED.clipboard = (function() { | ||||
|             id: "red-ui-clipboard-dialog-export-tab-clipboard-json", | ||||
|             label: RED._("editor.types.json") | ||||
|         }); | ||||
|         if (RED.settings.get("editor.dialog.export.json-view") === true) { | ||||
|             clipboardTabs.activateTab("red-ui-clipboard-dialog-export-tab-clipboard-json"); | ||||
|         } | ||||
|  | ||||
|         var previewList = $("#red-ui-clipboard-dialog-export-tab-clipboard-preview-list").css({position:"absolute",top:0,right:0,bottom:0,left:0}).treeList({ | ||||
|             data: [] | ||||
| @@ -982,16 +1008,16 @@ RED.clipboard = (function() { | ||||
|     } | ||||
|  | ||||
|     function importNodes(nodesStr,addFlow) { | ||||
|         var newNodes = nodesStr; | ||||
|         let newNodes = nodesStr; | ||||
|         if (typeof nodesStr === 'string') { | ||||
|             try { | ||||
|                 nodesStr = nodesStr.trim(); | ||||
|                 if (nodesStr.length === 0) { | ||||
|                     return; | ||||
|                 } | ||||
|                 newNodes = JSON.parse(nodesStr); | ||||
|                 newNodes = validateFlowString(nodesStr) | ||||
|             } catch(err) { | ||||
|                 var e = new Error(RED._("clipboard.invalidFlow",{message:err.message})); | ||||
|                 const e = new Error(RED._("clipboard.invalidFlow",{message:err.message})); | ||||
|                 e.code = "NODE_RED"; | ||||
|                 throw e; | ||||
|             } | ||||
| @@ -1326,6 +1352,7 @@ RED.clipboard = (function() { | ||||
|                             } | ||||
|                         } | ||||
|                     } catch(err) { | ||||
|                         console.warn('Import failed: ', err) | ||||
|                         // Ensure any errors throw above doesn't stop the drop target from | ||||
|                         // being hidden. | ||||
|                     } | ||||
|   | ||||
| @@ -174,12 +174,24 @@ | ||||
|                 this.uiContainer.width(m[1]); | ||||
|             } | ||||
|             if (this.options.sortable) { | ||||
|                 var isCanceled = false; // Flag to track if an item has been canceled from being dropped into a different list | ||||
|                 var noDrop = false; // Flag to track if an item is being dragged into a different list | ||||
|                 var handle = (typeof this.options.sortable === 'string')? | ||||
|                                 this.options.sortable : | ||||
|                                 ".red-ui-editableList-item-handle"; | ||||
|                 var sortOptions = { | ||||
|                     axis: "y", | ||||
|                     update: function( event, ui ) { | ||||
|                         // dont trigger update if the item is being canceled | ||||
|                         const targetList = $(event.target); | ||||
|                         const draggedItem = ui.item; | ||||
|                         const draggedItemParent = draggedItem.parent(); | ||||
|                         if (!targetList.is(draggedItemParent) && draggedItem.hasClass("red-ui-editableList-item-constrained")) { | ||||
|                             noDrop = true; | ||||
|                         } | ||||
|                         if (isCanceled || noDrop) { | ||||
|                             return; | ||||
|                         } | ||||
|                         if (that.options.sortItems) { | ||||
|                             that.options.sortItems(that.items()); | ||||
|                         } | ||||
| @@ -189,8 +201,32 @@ | ||||
|                     tolerance: "pointer", | ||||
|                     forcePlaceholderSize:true, | ||||
|                     placeholder: "red-ui-editabelList-item-placeholder", | ||||
|                     start: function(e, ui){ | ||||
|                         ui.placeholder.height(ui.item.height()-4); | ||||
|                     start: function (event, ui) { | ||||
|                         isCanceled = false; | ||||
|                         ui.placeholder.height(ui.item.height() - 4); | ||||
|                         ui.item.css('cursor', 'grabbing'); // TODO: this doesn't seem to work, use a class instead? | ||||
|                     }, | ||||
|                     stop: function (event, ui) { | ||||
|                         ui.item.css('cursor', 'auto'); | ||||
|                     }, | ||||
|                     receive: function (event, ui) { | ||||
|                         if (ui.item.hasClass("red-ui-editableList-item-constrained")) { | ||||
|                             isCanceled = true; | ||||
|                             $(ui.sender).sortable('cancel'); | ||||
|                         } | ||||
|                     }, | ||||
|                     over: function (event, ui) { | ||||
|                         // if the dragged item is constrained, prevent it from being dropped into a different list | ||||
|                         const targetList = $(event.target); | ||||
|                         const draggedItem = ui.item; | ||||
|                         const draggedItemParent = draggedItem.parent(); | ||||
|                         if (!targetList.is(draggedItemParent) && draggedItem.hasClass("red-ui-editableList-item-constrained")) { | ||||
|                             noDrop = true; | ||||
|                             draggedItem.css('cursor', 'no-drop'); // TODO: this doesn't seem to work, use a class instead? | ||||
|                         } else { | ||||
|                             noDrop = false; | ||||
|                             draggedItem.css('cursor', 'grabbing'); // TODO: this doesn't seem to work, use a class instead? | ||||
|                         } | ||||
|                     } | ||||
|                 }; | ||||
|                 if (this.options.connectWith) { | ||||
|   | ||||
| @@ -211,7 +211,7 @@ RED.popover = (function() { | ||||
|                         closePopup(true); | ||||
|                     }); | ||||
|                 } | ||||
|                 if (trigger === 'hover' && options.interactive) { | ||||
|                 if (/*trigger === 'hover' && */options.interactive) { | ||||
|                     div.on('mouseenter', function(e) { | ||||
|                         clearTimeout(timer); | ||||
|                         active = true; | ||||
| @@ -445,9 +445,12 @@ RED.popover = (function() { | ||||
|  | ||||
|     return { | ||||
|         create: createPopover, | ||||
|         tooltip: function(target,content, action) { | ||||
|         tooltip: function(target,content, action, interactive) { | ||||
|             var label = function() { | ||||
|                 var label = content; | ||||
|                 if (typeof content === 'function') { | ||||
|                     label = content() | ||||
|                 } | ||||
|                 if (action) { | ||||
|                     var shortcut = RED.keyboard.getShortcut(action); | ||||
|                     if (shortcut && shortcut.key) { | ||||
| @@ -463,6 +466,7 @@ RED.popover = (function() { | ||||
|                 size: "small", | ||||
|                 direction: "bottom", | ||||
|                 content: label, | ||||
|                 interactive, | ||||
|                 delay: { show: 750, hide: 50 } | ||||
|             }); | ||||
|             popover.setContent = function(newContent) { | ||||
|   | ||||
| @@ -365,7 +365,10 @@ RED.tabs = (function() { | ||||
|  | ||||
|             var thisTabA = thisTab.find("a"); | ||||
|             if (options.onclick) { | ||||
|                 options.onclick(tabs[thisTabA.attr('href').slice(1)]); | ||||
|                 options.onclick(tabs[thisTabA.attr('href').slice(1)], evt); | ||||
|                 if (evt.isDefaultPrevented() && evt.isPropagationStopped()) { | ||||
|                     return false | ||||
|                 } | ||||
|             } | ||||
|             activateTab(thisTabA); | ||||
|             if (fireSelectionChanged) { | ||||
| @@ -548,6 +551,8 @@ RED.tabs = (function() { | ||||
|         ul.find("li.red-ui-tab a") | ||||
|             .on("mousedown", function(evt) { mousedownTab = evt.currentTarget }) | ||||
|             .on("mouseup",onTabClick) | ||||
|             // prevent browser-default middle-click behaviour | ||||
|             .on("auxclick", function(evt) { evt.preventDefault() }) | ||||
|             .on("click", function(evt) {evt.preventDefault(); }) | ||||
|             .on("dblclick", function(evt) {evt.stopPropagation(); evt.preventDefault(); }) | ||||
|  | ||||
| @@ -816,6 +821,8 @@ RED.tabs = (function() { | ||||
|                 } | ||||
|                 link.on("mousedown", function(evt) { mousedownTab = evt.currentTarget }) | ||||
|                 link.on("mouseup",onTabClick); | ||||
|                 // prevent browser-default middle-click behaviour | ||||
|                 link.on("auxclick", function(evt) { evt.preventDefault() }) | ||||
|                 link.on("click", function(evt) { evt.preventDefault(); }) | ||||
|                 link.on("dblclick", function(evt) { evt.stopPropagation(); evt.preventDefault(); }) | ||||
|  | ||||
|   | ||||
| @@ -54,25 +54,26 @@ | ||||
|         return icon; | ||||
|     } | ||||
|  | ||||
|     var autoComplete = function(options) { | ||||
|         function getMatch(value, searchValue) { | ||||
|             const idx = value.toLowerCase().indexOf(searchValue.toLowerCase()); | ||||
|             const len = idx > -1 ? searchValue.length : 0; | ||||
|             return { | ||||
|                 index: idx, | ||||
|                 found: idx > -1, | ||||
|                 pre: value.substring(0,idx), | ||||
|                 match: value.substring(idx,idx+len), | ||||
|                 post: value.substring(idx+len), | ||||
|             } | ||||
|         } | ||||
|         function generateSpans(match) { | ||||
|             const els = []; | ||||
|             if(match.pre) { els.push($('<span/>').text(match.pre)); } | ||||
|             if(match.match) { els.push($('<span/>',{style:"font-weight: bold; color: var(--red-ui-text-color-link);"}).text(match.match)); } | ||||
|             if(match.post) { els.push($('<span/>').text(match.post)); } | ||||
|             return els; | ||||
|     function getMatch(value, searchValue) { | ||||
|         const idx = value.toLowerCase().indexOf(searchValue.toLowerCase()); | ||||
|         const len = idx > -1 ? searchValue.length : 0; | ||||
|         return { | ||||
|             index: idx, | ||||
|             found: idx > -1, | ||||
|             pre: value.substring(0,idx), | ||||
|             match: value.substring(idx,idx+len), | ||||
|             post: value.substring(idx+len), | ||||
|         } | ||||
|     } | ||||
|     function generateSpans(match) { | ||||
|         const els = []; | ||||
|         if(match.pre) { els.push($('<span/>').text(match.pre)); } | ||||
|         if(match.match) { els.push($('<span/>',{style:"font-weight: bold; color: var(--red-ui-text-color-link);"}).text(match.match)); } | ||||
|         if(match.post) { els.push($('<span/>').text(match.post)); } | ||||
|         return els; | ||||
|     } | ||||
|      | ||||
|     const msgAutoComplete = function(options) { | ||||
|         return function(val) { | ||||
|             var matches = []; | ||||
|             options.forEach(opt => { | ||||
| @@ -102,6 +103,197 @@ | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function getEnvVars (obj, envVars = {}) { | ||||
|         contextKnownKeys.env = contextKnownKeys.env || {} | ||||
|         if (contextKnownKeys.env[obj.id]) { | ||||
|             return contextKnownKeys.env[obj.id] | ||||
|         } | ||||
|         let parent | ||||
|         if (obj.type === 'tab' || obj.type === 'subflow') { | ||||
|             RED.nodes.eachConfig(function (conf) { | ||||
|                 if (conf.type === "global-config") { | ||||
|                     parent = conf; | ||||
|                 } | ||||
|             }) | ||||
|         } else if (obj.g) { | ||||
|             parent = RED.nodes.group(obj.g) | ||||
|         } else if (obj.z) { | ||||
|             parent = RED.nodes.workspace(obj.z) || RED.nodes.subflow(obj.z) | ||||
|         } | ||||
|         if (parent) { | ||||
|             getEnvVars(parent, envVars) | ||||
|         } | ||||
|         if (obj.env) { | ||||
|             obj.env.forEach(env => { | ||||
|                 envVars[env.name] = obj | ||||
|             }) | ||||
|         } | ||||
|         contextKnownKeys.env[obj.id] = envVars | ||||
|         return envVars | ||||
|     } | ||||
|  | ||||
|     const envAutoComplete = function (val) { | ||||
|         const editStack = RED.editor.getEditStack() | ||||
|         if (editStack.length === 0) { | ||||
|             done([]) | ||||
|             return | ||||
|         } | ||||
|         const editingNode = editStack.pop() | ||||
|         if (!editingNode) { | ||||
|             return [] | ||||
|         } | ||||
|         const envVarsMap = getEnvVars(editingNode) | ||||
|         const envVars = Object.keys(envVarsMap) | ||||
|         const matches = [] | ||||
|         const i = val.lastIndexOf('${') | ||||
|         let searchKey = val | ||||
|         let isSubkey = false | ||||
|         if (i > -1) { | ||||
|             if (val.lastIndexOf('}') < i) { | ||||
|                 searchKey = val.substring(i+2) | ||||
|                 isSubkey = true | ||||
|             } | ||||
|         } | ||||
|         envVars.forEach(v => { | ||||
|             let valMatch = getMatch(v, searchKey); | ||||
|             if (valMatch.found) { | ||||
|                 const optSrc = envVarsMap[v] | ||||
|                 const element = $('<div>',{style: "display: flex"}); | ||||
|                 const valEl = $('<div/>',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"}); | ||||
|                 valEl.append(generateSpans(valMatch)) | ||||
|                 valEl.appendTo(element) | ||||
|  | ||||
|                 if (optSrc) { | ||||
|                     const optEl = $('<div>').css({ "font-size": "0.8em" }); | ||||
|                     let label | ||||
|                     if (optSrc.type === 'global-config') { | ||||
|                         label = RED._('sidebar.context.global') | ||||
|                     } else if (optSrc.type === 'group') { | ||||
|                         label = RED.utils.getNodeLabel(optSrc) || (RED._('sidebar.info.group') + ': '+optSrc.id) | ||||
|                     } else { | ||||
|                         label = RED.utils.getNodeLabel(optSrc) || optSrc.id | ||||
|                     } | ||||
|  | ||||
|                     optEl.append(generateSpans({ match: label })); | ||||
|                     optEl.appendTo(element); | ||||
|                 } | ||||
|                 matches.push({ | ||||
|                     value: isSubkey ? val + v + '}' : v, | ||||
|                     label: element, | ||||
|                     i: valMatch.index | ||||
|                 }); | ||||
|             } | ||||
|         }) | ||||
|         matches.sort(function(A,B){return A.i-B.i}) | ||||
|         return matches | ||||
|     } | ||||
|  | ||||
|     let contextKnownKeys = {} | ||||
|     let contextCache = {} | ||||
|     if (RED.events) { | ||||
|         RED.events.on("editor:close", function () { | ||||
|             contextCache = {} | ||||
|             contextKnownKeys = {} | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     const contextAutoComplete = function() { | ||||
|         const that = this | ||||
|         const getContextKeysFromRuntime = function(scope, store, searchKey, done) { | ||||
|             contextKnownKeys[scope] = contextKnownKeys[scope] || {} | ||||
|             contextKnownKeys[scope][store] = contextKnownKeys[scope][store] || new Set() | ||||
|             if (searchKey.length > 0) { | ||||
|                 try { | ||||
|                     RED.utils.normalisePropertyExpression(searchKey) | ||||
|                 } catch (err) { | ||||
|                     // Not a valid context key, so don't try looking up | ||||
|                     done() | ||||
|                     return | ||||
|                 } | ||||
|             } | ||||
|             const url = `context/${scope}/${encodeURIComponent(searchKey)}?store=${store}&keysOnly` | ||||
|             if (contextCache[url]) { | ||||
|                 // console.log('CACHED', url) | ||||
|                 done() | ||||
|             } else { | ||||
|                 // console.log('GET', url) | ||||
|                 $.getJSON(url, function(data) { | ||||
|                     // console.log(data) | ||||
|                     contextCache[url] = true | ||||
|                     const result = data[store] || {} | ||||
|                     const keys = result.keys || [] | ||||
|                     const keyPrefix = searchKey + (searchKey.length > 0 ? '.' : '') | ||||
|                     keys.forEach(key => { | ||||
|                         if (/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(key)) { | ||||
|                             contextKnownKeys[scope][store].add(keyPrefix + key) | ||||
|                         } else { | ||||
|                             contextKnownKeys[scope][store].add(searchKey + "[\""+key.replace(/"/,"\\\"")+"\"]") | ||||
|                         }                         | ||||
|                     }) | ||||
|                     done() | ||||
|                 }) | ||||
|             } | ||||
|         } | ||||
|         const getContextKeys = function(key, done) { | ||||
|             const keyParts = key.split('.') | ||||
|             const partialKey = keyParts.pop() | ||||
|             let scope = that.propertyType | ||||
|             if (scope === 'flow') { | ||||
|                 // Get the flow id of the node we're editing | ||||
|                 const editStack = RED.editor.getEditStack() | ||||
|                 if (editStack.length === 0) { | ||||
|                     done([]) | ||||
|                     return | ||||
|                 } | ||||
|                 const editingNode = editStack.pop() | ||||
|                 if (editingNode.z) { | ||||
|                     scope = `${scope}/${editingNode.z}` | ||||
|                 } else { | ||||
|                     done([]) | ||||
|                     return | ||||
|                 } | ||||
|             } | ||||
|             const store = (contextStoreOptions.length === 1) ? contextStoreOptions[0].value : that.optionValue | ||||
|             const searchKey = keyParts.join('.') | ||||
|             | ||||
|             getContextKeysFromRuntime(scope, store, searchKey, function() { | ||||
|                 if (contextKnownKeys[scope][store].has(key) || key.endsWith(']')) { | ||||
|                     getContextKeysFromRuntime(scope, store, key, function() { | ||||
|                         done(contextKnownKeys[scope][store]) | ||||
|                     }) | ||||
|                 } | ||||
|                 done(contextKnownKeys[scope][store]) | ||||
|             }) | ||||
|         } | ||||
|  | ||||
|         return function(val, done) { | ||||
|             getContextKeys(val, function (keys) { | ||||
|                 const matches = [] | ||||
|                 keys.forEach(v => { | ||||
|                     let optVal = v | ||||
|                     let valMatch = getMatch(optVal, val); | ||||
|                     if (!valMatch.found && val.length > 0 && val.endsWith('.')) { | ||||
|                         // Search key ends in '.' - but doesn't match. Check again | ||||
|                         // with [" at the end instead so we match bracket notation | ||||
|                         valMatch = getMatch(optVal, val.substring(0, val.length - 1) + '["') | ||||
|                     } | ||||
|                     if (valMatch.found) { | ||||
|                         const element = $('<div>',{style: "display: flex"}); | ||||
|                         const valEl = $('<div/>',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"}); | ||||
|                         valEl.append(generateSpans(valMatch)) | ||||
|                         valEl.appendTo(element) | ||||
|                         matches.push({ | ||||
|                             value: optVal, | ||||
|                             label: element, | ||||
|                         }); | ||||
|                     } | ||||
|                 }) | ||||
|                 matches.sort(function(a, b) { return a.value.localeCompare(b.value) }); | ||||
|                 done(matches); | ||||
|             }) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // This is a hand-generated list of completions for the core nodes (based on the node help html). | ||||
|     var msgCompletions = [ | ||||
|         { value: "payload" }, | ||||
| @@ -166,68 +358,93 @@ | ||||
|         { value: "_session", source: ["websocket out","tcp out"] }, | ||||
|     ] | ||||
|     var allOptions = { | ||||
|         msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: autoComplete(msgCompletions)}, | ||||
|         flow: {value:"flow",label:"flow.",hasValue:true, | ||||
|             options:[], | ||||
|             validate:RED.utils.validatePropertyExpression, | ||||
|         msg: { value: "msg", label: "msg.", validate: RED.utils.validatePropertyExpression, autoComplete: msgAutoComplete(msgCompletions) }, | ||||
|         flow: { value: "flow", label: "flow.", hasValue: true, | ||||
|             options: [], | ||||
|             validate: RED.utils.validatePropertyExpression, | ||||
|             parse: contextParse, | ||||
|             export: contextExport, | ||||
|             valueLabel: contextLabel | ||||
|             valueLabel: contextLabel, | ||||
|             autoComplete: contextAutoComplete | ||||
|         }, | ||||
|         global: {value:"global",label:"global.",hasValue:true, | ||||
|             options:[], | ||||
|             validate:RED.utils.validatePropertyExpression, | ||||
|         global: { | ||||
|             value: "global", label: "global.", hasValue: true, | ||||
|             options: [], | ||||
|             validate: RED.utils.validatePropertyExpression, | ||||
|             parse: contextParse, | ||||
|             export: contextExport, | ||||
|             valueLabel: contextLabel | ||||
|             valueLabel: contextLabel, | ||||
|             autoComplete: contextAutoComplete | ||||
|         }, | ||||
|         str: {value:"str",label:"string",icon:"red/images/typedInput/az.svg"}, | ||||
|         num: {value:"num",label:"number",icon:"red/images/typedInput/09.svg",validate: function(v) { | ||||
|             return (true === RED.utils.validateTypedProperty(v, "num")); | ||||
|         str: { value: "str", label: "string", icon: "red/images/typedInput/az.svg" }, | ||||
|         num: { value: "num", label: "number", icon: "red/images/typedInput/09.svg", validate: function (v, o) { | ||||
|             return RED.utils.validateTypedProperty(v, "num", o); | ||||
|         } }, | ||||
|         bool: {value:"bool",label:"boolean",icon:"red/images/typedInput/bool.svg",options:["true","false"]}, | ||||
|         bool: { value: "bool", label: "boolean", icon: "red/images/typedInput/bool.svg", options: ["true", "false"] }, | ||||
|         json: { | ||||
|             value:"json", | ||||
|             label:"JSON", | ||||
|             icon:"red/images/typedInput/json.svg", | ||||
|             validate: function(v) { try{JSON.parse(v);return true;}catch(e){return false;}}, | ||||
|             expand: function() { | ||||
|             value: "json", | ||||
|             label: "JSON", | ||||
|             icon: "red/images/typedInput/json.svg", | ||||
|             validate: function (v, o) { | ||||
|                 return RED.utils.validateTypedProperty(v, "json", o); | ||||
|             }, | ||||
|             expand: function () { | ||||
|                 var that = this; | ||||
|                 var value = this.value(); | ||||
|                 try { | ||||
|                     value = JSON.stringify(JSON.parse(value),null,4); | ||||
|                 } catch(err) { | ||||
|                     value = JSON.stringify(JSON.parse(value), null, 4); | ||||
|                 } catch (err) { | ||||
|                 } | ||||
|                 RED.editor.editJSON({ | ||||
|                     value: value, | ||||
|                     stateId: RED.editor.generateViewStateId("typedInput", that, "json"), | ||||
|                     focus: true, | ||||
|                     complete: function(v) { | ||||
|                     complete: function (v) { | ||||
|                         var value = v; | ||||
|                         try { | ||||
|                             value = JSON.stringify(JSON.parse(v)); | ||||
|                         } catch(err) { | ||||
|                         } catch (err) { | ||||
|                         } | ||||
|                         that.value(value); | ||||
|                     } | ||||
|                 }) | ||||
|             } | ||||
|         }, | ||||
|         re: {value:"re",label:"regular expression",icon:"red/images/typedInput/re.svg"}, | ||||
|         date: {value:"date",label:"timestamp",icon:"fa fa-clock-o",hasValue:false}, | ||||
|         re: { value: "re", label: "regular expression", icon: "red/images/typedInput/re.svg" }, | ||||
|         date: { | ||||
|             value: "date", | ||||
|             label: "timestamp", | ||||
|             icon: "fa fa-clock-o", | ||||
|             options: [ | ||||
|                 { | ||||
|                     label: 'milliseconds since epoch', | ||||
|                     value: '' | ||||
|                 }, | ||||
|                 { | ||||
|                     label: 'YYYY-MM-DDTHH:mm:ss.sssZ', | ||||
|                     value: 'iso' | ||||
|                 }, | ||||
|                 { | ||||
|                     label: 'JavaScript Date Object', | ||||
|                     value: 'object' | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         jsonata: { | ||||
|             value: "jsonata", | ||||
|             label: "expression", | ||||
|             icon: "red/images/typedInput/expr.svg", | ||||
|             validate: function(v) { try{jsonata(v);return true;}catch(e){return false;}}, | ||||
|             expand:function() { | ||||
|             validate: function (v, o) { | ||||
|                 return RED.utils.validateTypedProperty(v, "jsonata", o); | ||||
|             }, | ||||
|             expand: function () { | ||||
|                 var that = this; | ||||
|                 RED.editor.editExpression({ | ||||
|                     value: this.value().replace(/\t/g,"\n"), | ||||
|                     value: this.value().replace(/\t/g, "\n"), | ||||
|                     stateId: RED.editor.generateViewStateId("typedInput", that, "jsonata"), | ||||
|                     focus: true, | ||||
|                     complete: function(v) { | ||||
|                         that.value(v.replace(/\n/g,"\t")); | ||||
|                     complete: function (v) { | ||||
|                         that.value(v.replace(/\n/g, "\t")); | ||||
|                     } | ||||
|                 }) | ||||
|             } | ||||
| @@ -236,13 +453,13 @@ | ||||
|             value: "bin", | ||||
|             label: "buffer", | ||||
|             icon: "red/images/typedInput/bin.svg", | ||||
|             expand: function() { | ||||
|             expand: function () { | ||||
|                 var that = this; | ||||
|                 RED.editor.editBuffer({ | ||||
|                     value: this.value(), | ||||
|                     stateId: RED.editor.generateViewStateId("typedInput", that, "bin"), | ||||
|                     focus: true, | ||||
|                     complete: function(v) { | ||||
|                     complete: function (v) { | ||||
|                         that.value(v); | ||||
|                     } | ||||
|                 }) | ||||
| @@ -251,15 +468,16 @@ | ||||
|         env: { | ||||
|             value: "env", | ||||
|             label: "env variable", | ||||
|             icon: "red/images/typedInput/env.svg" | ||||
|             icon: "red/images/typedInput/env.svg", | ||||
|             autoComplete: envAutoComplete | ||||
|         }, | ||||
|         node: { | ||||
|             value: "node", | ||||
|             label: "node", | ||||
|             icon: "red/images/typedInput/target.svg", | ||||
|             valueLabel: function(container,value) { | ||||
|             valueLabel: function (container, value) { | ||||
|                 var node = RED.nodes.node(value); | ||||
|                 var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).css({ | ||||
|                 var nodeDiv = $('<div>', { class: "red-ui-search-result-node" }).css({ | ||||
|                     "margin-top": "2px", | ||||
|                     "margin-left": "3px" | ||||
|                 }).appendTo(container); | ||||
| @@ -268,133 +486,190 @@ | ||||
|                     "margin-left": "6px" | ||||
|                 }).appendTo(container); | ||||
|                 if (node) { | ||||
|                     var colour = RED.utils.getNodeColor(node.type,node._def); | ||||
|                     var icon_url = RED.utils.getNodeIcon(node._def,node); | ||||
|                     var colour = RED.utils.getNodeColor(node.type, node._def); | ||||
|                     var icon_url = RED.utils.getNodeIcon(node._def, node); | ||||
|                     if (node.type === 'tab') { | ||||
|                         colour = "#C0DEED"; | ||||
|                     } | ||||
|                     nodeDiv.css('backgroundColor',colour); | ||||
|                     var iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv); | ||||
|                     nodeDiv.css('backgroundColor', colour); | ||||
|                     var iconContainer = $('<div/>', { class: "red-ui-palette-icon-container" }).appendTo(nodeDiv); | ||||
|                     RED.utils.createIconElement(icon_url, iconContainer, true); | ||||
|                     var l = RED.utils.getNodeLabel(node,node.id); | ||||
|                     var l = RED.utils.getNodeLabel(node, node.id); | ||||
|                     nodeLabel.text(l); | ||||
|                 } else { | ||||
|                     nodeDiv.css({ | ||||
|                         'backgroundColor': '#eee', | ||||
|                         'border-style' : 'dashed' | ||||
|                         'border-style': 'dashed' | ||||
|                     }); | ||||
|  | ||||
|                 } | ||||
|             }, | ||||
|             expand: function() { | ||||
|             expand: function () { | ||||
|                 var that = this; | ||||
|                 RED.tray.hide(); | ||||
|                 RED.view.selectNodes({ | ||||
|                     single: true, | ||||
|                     selected: [that.value()], | ||||
|                     onselect: function(selection) { | ||||
|                     onselect: function (selection) { | ||||
|                         that.value(selection.id); | ||||
|                         RED.tray.show(); | ||||
|                     }, | ||||
|                     oncancel: function() { | ||||
|                     oncancel: function () { | ||||
|                         RED.tray.show(); | ||||
|                     } | ||||
|                 }) | ||||
|             } | ||||
|         }, | ||||
|         cred:{ | ||||
|             value:"cred", | ||||
|             label:"credential", | ||||
|             icon:"fa fa-lock", | ||||
|         cred: { | ||||
|             value: "cred", | ||||
|             label: "credential", | ||||
|             icon: "fa fa-lock", | ||||
|             inputType: "password", | ||||
|             valueLabel: function(container,value) { | ||||
|             valueLabel: function (container, value) { | ||||
|                 var that = this; | ||||
|                 container.css("pointer-events","none"); | ||||
|                 container.css("flex-grow",0); | ||||
|                 container.css("pointer-events", "none"); | ||||
|                 container.css("flex-grow", 0); | ||||
|                 this.elementDiv.hide(); | ||||
|                 var buttons = $('<div>').css({ | ||||
|                     position: "absolute", | ||||
|                     right:"6px", | ||||
|                     right: "6px", | ||||
|                     top: "6px", | ||||
|                     "pointer-events":"all" | ||||
|                     "pointer-events": "all" | ||||
|                 }).appendTo(container); | ||||
|                 var eyeButton = $('<button type="button" class="red-ui-button red-ui-button-small"></button>').css({ | ||||
|                     width:"20px" | ||||
|                 }).appendTo(buttons).on("click", function(evt) { | ||||
|                     width: "20px" | ||||
|                 }).appendTo(buttons).on("click", function (evt) { | ||||
|                     evt.preventDefault(); | ||||
|                     var cursorPosition = that.input[0].selectionStart; | ||||
|                     var currentType = that.input.attr("type"); | ||||
|                     if (currentType === "text") { | ||||
|                         that.input.attr("type","password"); | ||||
|                         that.input.attr("type", "password"); | ||||
|                         eyeCon.removeClass("fa-eye-slash").addClass("fa-eye"); | ||||
|                         setTimeout(function() { | ||||
|                         setTimeout(function () { | ||||
|                             that.input.focus(); | ||||
|                             that.input[0].setSelectionRange(cursorPosition, cursorPosition); | ||||
|                         },50); | ||||
|                         }, 50); | ||||
|                     } else { | ||||
|                         that.input.attr("type","text"); | ||||
|                         that.input.attr("type", "text"); | ||||
|                         eyeCon.removeClass("fa-eye").addClass("fa-eye-slash"); | ||||
|                         setTimeout(function() { | ||||
|                         setTimeout(function () { | ||||
|                             that.input.focus(); | ||||
|                             that.input[0].setSelectionRange(cursorPosition, cursorPosition); | ||||
|                         },50); | ||||
|                         }, 50); | ||||
|                     } | ||||
|                 }).hide(); | ||||
|                 var eyeCon = $('<i class="fa fa-eye"></i>').css("margin-left","-2px").appendTo(eyeButton); | ||||
|                 var eyeCon = $('<i class="fa fa-eye"></i>').css("margin-left", "-2px").appendTo(eyeButton); | ||||
|  | ||||
|                 if (value === "__PWRD__") { | ||||
|                     var innerContainer = $('<div><i class="fa fa-asterisk"></i><i class="fa fa-asterisk"></i><i class="fa fa-asterisk"></i><i class="fa fa-asterisk"></i><i class="fa fa-asterisk"></i></div>').css({ | ||||
|                         padding:"6px 6px", | ||||
|                         borderRadius:"4px" | ||||
|                         padding: "6px 6px", | ||||
|                         borderRadius: "4px" | ||||
|                     }).addClass("red-ui-typedInput-value-label-inactive").appendTo(container); | ||||
|                     var editButton = $('<button type="button" class="red-ui-button red-ui-button-small"><i class="fa fa-pencil"></i></button>').appendTo(buttons).on("click", function(evt) { | ||||
|                     var editButton = $('<button type="button" class="red-ui-button red-ui-button-small"><i class="fa fa-pencil"></i></button>').appendTo(buttons).on("click", function (evt) { | ||||
|                         evt.preventDefault(); | ||||
|                         innerContainer.hide(); | ||||
|                         container.css("background","none"); | ||||
|                         container.css("pointer-events","none"); | ||||
|                         container.css("background", "none"); | ||||
|                         container.css("pointer-events", "none"); | ||||
|                         that.input.val(""); | ||||
|                         that.element.val(""); | ||||
|                         that.elementDiv.show(); | ||||
|                         editButton.hide(); | ||||
|                         cancelButton.show(); | ||||
|                         eyeButton.show(); | ||||
|                         setTimeout(function() { | ||||
|                         setTimeout(function () { | ||||
|                             that.input.focus(); | ||||
|                         },50); | ||||
|                         }, 50); | ||||
|                     }); | ||||
|                     var cancelButton = $('<button type="button" class="red-ui-button red-ui-button-small"><i class="fa fa-times"></i></button>').css("margin-left","3px").appendTo(buttons).on("click", function(evt) { | ||||
|                     var cancelButton = $('<button type="button" class="red-ui-button red-ui-button-small"><i class="fa fa-times"></i></button>').css("margin-left", "3px").appendTo(buttons).on("click", function (evt) { | ||||
|                         evt.preventDefault(); | ||||
|                         innerContainer.show(); | ||||
|                         container.css("background",""); | ||||
|                         container.css("background", ""); | ||||
|                         that.input.val("__PWRD__"); | ||||
|                         that.element.val("__PWRD__"); | ||||
|                         that.elementDiv.hide(); | ||||
|                         editButton.show(); | ||||
|                         cancelButton.hide(); | ||||
|                         eyeButton.hide(); | ||||
|                         that.input.attr("type","password"); | ||||
|                         that.input.attr("type", "password"); | ||||
|                         eyeCon.removeClass("fa-eye-slash").addClass("fa-eye"); | ||||
|  | ||||
|                     }).hide(); | ||||
|                 } else { | ||||
|                     container.css("background","none"); | ||||
|                     container.css("pointer-events","none"); | ||||
|                     container.css("background", "none"); | ||||
|                     container.css("pointer-events", "none"); | ||||
|                     this.elementDiv.show(); | ||||
|                     eyeButton.show(); | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         'conf-types': { | ||||
|             value: "conf-types", | ||||
|             label: "config", | ||||
|             icon: "fa fa-cog", | ||||
|             // hasValue: false, | ||||
|             valueLabel: function (container, value) { | ||||
|                 // get the selected option (for access to the "name" and "module" properties) | ||||
|                 const _options = this._optionsCache || this.typeList.find(opt => opt.value === value)?.options || [] | ||||
|                 const selectedOption = _options.find(opt => opt.value === value) || { | ||||
|                     title: '', | ||||
|                     name: '', | ||||
|                     module: '' | ||||
|                 } | ||||
|                 container.attr("title", selectedOption.title) // set tooltip to the full path/id of the module/node | ||||
|                 container.text(selectedOption.name) // apply the "name" of the selected option | ||||
|                 // set "line-height" such as to make the "name" appear further up, giving room for the "module" to be displayed below the value | ||||
|                 container.css("line-height", "1.4em") | ||||
|                 // add the module name in smaller, lighter font below the value | ||||
|                 $('<div></div>').text(selectedOption.module).css({ | ||||
|                     // "font-family": "var(--red-ui-monospace-font)", | ||||
|                     color: "var(--red-ui-tertiary-text-color)", | ||||
|                     "font-size": "0.8em", | ||||
|                     "line-height": "1em", | ||||
|                     opacity: 0.8 | ||||
|                 }).appendTo(container); | ||||
|             }, | ||||
|             // hasValue: false, | ||||
|             options: function () { | ||||
|                 if (this._optionsCache) { | ||||
|                     return this._optionsCache | ||||
|                 } | ||||
|                 const configNodes = RED.nodes.registry.getNodeDefinitions({configOnly: true, filter: (def) => def.type !== "global-config"}).map((def) => { | ||||
|                     // create a container with with 2 rows (row 1 for the name, row 2 for the module name in smaller, lighter font) | ||||
|                     const container = $('<div style="display: flex; flex-direction: column; justify-content: space-between; row-gap: 1px;">') | ||||
|                     const row1Name = $('<div>').text(def.type) | ||||
|                     const row2Module = $('<div style="font-size: 0.8em; color: var(--red-ui-tertiary-text-color);">').text(def.set.module) | ||||
|                     container.append(row1Name, row2Module) | ||||
|          | ||||
|                     return { | ||||
|                         value: def.type, | ||||
|                         name: def.type, | ||||
|                         enabled: def.set.enabled ?? true, | ||||
|                         local: def.set.local, | ||||
|                         title: def.set.id, // tooltip e.g. "node-red-contrib-foo/bar" | ||||
|                         module: def.set.module, | ||||
|                         icon: container[0].outerHTML.trim(), // the typeInput will interpret this as html text and render it in the anchor | ||||
|                     } | ||||
|                 }) | ||||
|                 this._optionsCache = configNodes | ||||
|                 return configNodes | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|      | ||||
|     // For a type with options, check value is a valid selection | ||||
|     // If !opt.multiple, returns the valid option object | ||||
|     // if opt.multiple, returns an array of valid option objects | ||||
|     // If not valid, returns null; | ||||
|  | ||||
|     function isOptionValueValid(opt, currentVal) { | ||||
|         let _options = opt.options | ||||
|         if (typeof _options === "function") { | ||||
|             _options = _options.call(this) | ||||
|         } | ||||
|         if (!opt.multiple) { | ||||
|             for (var i=0;i<opt.options.length;i++) { | ||||
|                 op = opt.options[i]; | ||||
|             for (var i=0;i<_options.length;i++) { | ||||
|                 op = _options[i]; | ||||
|                 if (typeof op === "string" && op === currentVal) { | ||||
|                     return {value:currentVal} | ||||
|                 } else if (op.value === currentVal) { | ||||
| @@ -411,8 +686,8 @@ | ||||
|                     currentValues[v] = true; | ||||
|                 } | ||||
|             }); | ||||
|             for (var i=0;i<opt.options.length;i++) { | ||||
|                 op = opt.options[i]; | ||||
|             for (var i=0;i<_options.length;i++) { | ||||
|                 op = _options[i]; | ||||
|                 var val = typeof op === "string" ? op : op.value; | ||||
|                 if (currentValues.hasOwnProperty(val)) { | ||||
|                     delete currentValues[val]; | ||||
| @@ -427,6 +702,7 @@ | ||||
|     } | ||||
|  | ||||
|     var nlsd = false; | ||||
|     let contextStoreOptions; | ||||
|  | ||||
|     $.widget( "nodered.typedInput", { | ||||
|         _create: function() { | ||||
| @@ -438,7 +714,7 @@ | ||||
|                     } | ||||
|                 } | ||||
|                 var contextStores = RED.settings.context.stores; | ||||
|                 var contextOptions = contextStores.map(function(store) { | ||||
|                 contextStoreOptions = contextStores.map(function(store) { | ||||
|                     return {value:store,label: store, icon:'<i class="red-ui-typedInput-icon fa fa-database"></i>'} | ||||
|                 }).sort(function(A,B) { | ||||
|                     if (A.value === RED.settings.context.default) { | ||||
| @@ -449,13 +725,17 @@ | ||||
|                         return A.value.localeCompare(B.value); | ||||
|                     } | ||||
|                 }) | ||||
|                 if (contextOptions.length < 2) { | ||||
|                 if (contextStoreOptions.length < 2) { | ||||
|                     allOptions.flow.options = []; | ||||
|                     allOptions.global.options = []; | ||||
|                 } else { | ||||
|                     allOptions.flow.options = contextOptions; | ||||
|                     allOptions.global.options = contextOptions; | ||||
|                     allOptions.flow.options = contextStoreOptions; | ||||
|                     allOptions.global.options = contextStoreOptions; | ||||
|                 } | ||||
|                 // Translate timestamp options | ||||
|                 allOptions.date.options.forEach(opt => { | ||||
|                     opt.label = RED._("typedInput.date.format." + (opt.value || 'timestamp'), {defaultValue: opt.label}) | ||||
|                 }) | ||||
|             } | ||||
|             nlsd = true; | ||||
|             var that = this; | ||||
| @@ -544,7 +824,7 @@ | ||||
|                 that.element.trigger('paste',evt); | ||||
|             }); | ||||
|             this.input.on('keydown', function(evt) { | ||||
|                 if (that.typeMap[that.propertyType].autoComplete) { | ||||
|                 if (that.typeMap[that.propertyType].autoComplete || that.input.hasClass('red-ui-autoComplete')) { | ||||
|                     return | ||||
|                 } | ||||
|                 if (evt.keyCode >= 37 && evt.keyCode <= 40) { | ||||
| @@ -734,12 +1014,12 @@ | ||||
|             } | ||||
|             if (menu.opts.multiple) { | ||||
|                 var selected = {}; | ||||
|                  this.value().split(",").forEach(function(f) { | ||||
|                      selected[f] = true; | ||||
|                  }) | ||||
|                 this.value().split(",").forEach(function(f) { | ||||
|                     selected[f] = true; | ||||
|                 }); | ||||
|                 menu.find('input[type="checkbox"]').each(function() { | ||||
|                     $(this).prop("checked",selected[$(this).data('value')]) | ||||
|                 }) | ||||
|                     $(this).prop("checked", selected[$(this).data('value')] || false); | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|  | ||||
| @@ -830,7 +1110,7 @@ | ||||
|                         this.input.trigger('change',[this.propertyType,this.value()]); | ||||
|                     } | ||||
|                 } else { | ||||
|                     this.optionSelectLabel.text(o.length+" selected"); | ||||
|                     this.optionSelectLabel.text(RED._("typedInput.selected", { count: o.length })); | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
| @@ -838,7 +1118,9 @@ | ||||
|             if (this.optionMenu) { | ||||
|                 this.optionMenu.remove(); | ||||
|             } | ||||
|             this.menu.remove(); | ||||
|             if (this.menu) { | ||||
|                 this.menu.remove(); | ||||
|             } | ||||
|             this.uiSelect.remove(); | ||||
|         }, | ||||
|         types: function(types) { | ||||
| @@ -871,7 +1153,7 @@ | ||||
|             this.menu = this._createMenu(this.typeList,{},function(v) { that.type(v) }); | ||||
|             if (currentType && !this.typeMap.hasOwnProperty(currentType)) { | ||||
|                 if (!firstCall) { | ||||
|                     this.type(this.typeList[0].value); | ||||
|                     this.type(this.typeList[0]?.value || ""); // permit empty typeList | ||||
|                 } | ||||
|             } else { | ||||
|                 this.propertyType = null; | ||||
| @@ -908,6 +1190,11 @@ | ||||
|                 var selectedOption = []; | ||||
|                 var valueToCheck = value; | ||||
|                 if (opt.options) { | ||||
|                     let _options = opt.options | ||||
|                     if (typeof opt.options === "function") { | ||||
|                         _options = opt.options.call(this) | ||||
|                     } | ||||
|  | ||||
|                     if (opt.hasValue && opt.parse) { | ||||
|                         var parts = opt.parse(value); | ||||
|                         if (this.options.debug) { console.log(this.identifier,"new parse",parts) } | ||||
| @@ -921,8 +1208,8 @@ | ||||
|                         checkValues = valueToCheck.split(","); | ||||
|                     } | ||||
|                     checkValues.forEach(function(valueToCheck) { | ||||
|                         for (var i=0;i<opt.options.length;i++) { | ||||
|                             var op = opt.options[i]; | ||||
|                         for (var i=0;i<_options.length;i++) { | ||||
|                             var op = _options[i]; | ||||
|                             if (typeof op === "string") { | ||||
|                                 if (op === valueToCheck || op === ""+valueToCheck) { | ||||
|                                     selectedOption.push(that.activeOptions[op]); | ||||
| @@ -957,7 +1244,7 @@ | ||||
|         }, | ||||
|         type: function(type) { | ||||
|             if (!arguments.length) { | ||||
|                 return this.propertyType; | ||||
|                 return this.propertyType || this.options?.default || ''; | ||||
|             } else { | ||||
|                 var that = this; | ||||
|                 if (this.options.debug) { console.log(this.identifier,"----- SET TYPE -----",type) } | ||||
| @@ -967,6 +1254,9 @@ | ||||
|                     // If previousType is !null, then this is a change of the type, rather than the initialisation | ||||
|                     var previousType = this.typeMap[this.propertyType]; | ||||
|                     previousValue = this.input.val(); | ||||
|                     if (this.input.hasClass('red-ui-autoComplete')) { | ||||
|                         this.input.autoComplete("destroy"); | ||||
|                     } | ||||
|  | ||||
|                     if (previousType && this.typeChanged) { | ||||
|                         if (this.options.debug) { console.log(this.identifier,"typeChanged",{previousType,previousValue}) } | ||||
| @@ -1013,7 +1303,9 @@ | ||||
|                             this.input.val(this.oldValues.hasOwnProperty("_")?this.oldValues["_"]:(opt.default||"")) | ||||
|                         } | ||||
|                         if (previousType.autoComplete) { | ||||
|                             this.input.autoComplete("destroy"); | ||||
|                             if (this.input.hasClass('red-ui-autoComplete')) { | ||||
|                                 this.input.autoComplete("destroy"); | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     this.propertyType = type; | ||||
| @@ -1053,6 +1345,10 @@ | ||||
|                         this.optionMenu = null; | ||||
|                     } | ||||
|                     if (opt.options) { | ||||
|                         let _options = opt.options | ||||
|                         if (typeof _options === "function") { | ||||
|                             _options = opt.options.call(this); | ||||
|                         } | ||||
|                         if (this.optionExpandButton) { | ||||
|                             this.optionExpandButton.hide(); | ||||
|                             this.optionExpandButton.shown = false; | ||||
| @@ -1069,7 +1365,7 @@ | ||||
|                                 this.valueLabelContainer.hide(); | ||||
|                             } | ||||
|                             this.activeOptions = {}; | ||||
|                             opt.options.forEach(function(o) { | ||||
|                             _options.forEach(function(o) { | ||||
|                                 if (typeof o === 'string') { | ||||
|                                     that.activeOptions[o] = {label:o,value:o}; | ||||
|                                 } else { | ||||
| @@ -1089,7 +1385,7 @@ | ||||
|                                     if (validValues) { | ||||
|                                         that._updateOptionSelectLabel(validValues) | ||||
|                                     } else { | ||||
|                                         op = opt.options[0]; | ||||
|                                         op = _options[0] || {value:""}; // permit zero options | ||||
|                                         if (typeof op === "string") { | ||||
|                                             this.value(op); | ||||
|                                             that._updateOptionSelectLabel({value:op}); | ||||
| @@ -1108,7 +1404,7 @@ | ||||
|                                     that._updateOptionSelectLabel(validValues); | ||||
|                                 } | ||||
|                             } else { | ||||
|                                 var selectedOption = this.optionValue||opt.options[0]; | ||||
|                                 var selectedOption = this.optionValue||_options[0]; | ||||
|                                 if (opt.parse) { | ||||
|                                     var selectedOptionObj = typeof selectedOption === "string"?{value:selectedOption}:selectedOption | ||||
|                                     var parts = opt.parse(this.input.val(),selectedOptionObj); | ||||
| @@ -1141,8 +1437,18 @@ | ||||
|                                 } else { | ||||
|                                     this.optionSelectTrigger.hide(); | ||||
|                                 } | ||||
|                                 if (opt.autoComplete) { | ||||
|                                     let searchFunction = opt.autoComplete | ||||
|                                     if (searchFunction.length === 0) { | ||||
|                                         searchFunction = opt.autoComplete.call(this) | ||||
|                                     } | ||||
|                                     this.input.autoComplete({ | ||||
|                                         search: searchFunction, | ||||
|                                         minLength: 0 | ||||
|                                     }) | ||||
|                                 } | ||||
|                             } | ||||
|                             this.optionMenu = this._createMenu(opt.options,opt,function(v){ | ||||
|                             this.optionMenu = this._createMenu(_options,opt,function(v){ | ||||
|                                 if (!opt.multiple) { | ||||
|                                     that._updateOptionSelectLabel(that.activeOptions[v]); | ||||
|                                     if (!opt.hasValue) { | ||||
| @@ -1183,8 +1489,12 @@ | ||||
|                             this.valueLabelContainer.hide(); | ||||
|                             this.elementDiv.show(); | ||||
|                             if (opt.autoComplete) { | ||||
|                                 let searchFunction = opt.autoComplete | ||||
|                                 if (searchFunction.length === 0) { | ||||
|                                     searchFunction = opt.autoComplete.call(this) | ||||
|                                 } | ||||
|                                 this.input.autoComplete({ | ||||
|                                     search: opt.autoComplete, | ||||
|                                     search: searchFunction, | ||||
|                                     minLength: 0 | ||||
|                                 }) | ||||
|                             } | ||||
| @@ -1233,26 +1543,48 @@ | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         validate: function() { | ||||
|             var result; | ||||
|             var value = this.value(); | ||||
|             var type = this.type(); | ||||
|         validate: function(options) { | ||||
|             let valid = true; | ||||
|             const value = this.value(); | ||||
|             const type = this.type(); | ||||
|             if (this.typeMap[type] && this.typeMap[type].validate) { | ||||
|                 var val = this.typeMap[type].validate; | ||||
|                 if (typeof val === 'function') { | ||||
|                     result = val(value); | ||||
|                 const validate = this.typeMap[type].validate; | ||||
|                 if (typeof validate === 'function') { | ||||
|                     valid = validate(value, {}); | ||||
|                 } else { | ||||
|                     result = val.test(value); | ||||
|                     // Regex | ||||
|                     valid = validate.test(value); | ||||
|                     if (!valid) { | ||||
|                         valid = RED._("validator.errors.invalid-regexp"); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             if ((typeof valid === "string") || !valid) { | ||||
|                 this.element.addClass("input-error"); | ||||
|                 this.uiSelect.addClass("input-error"); | ||||
|                 if (typeof valid === "string") { | ||||
|                     let tooltip = this.element.data("tooltip"); | ||||
|                     if (tooltip) { | ||||
|                         tooltip.setContent(valid); | ||||
|                     } else { | ||||
|                         tooltip = RED.popover.tooltip(this.elementDiv, valid); | ||||
|                         this.element.data("tooltip", tooltip); | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|                 result = true; | ||||
|                 this.element.removeClass("input-error"); | ||||
|                 this.uiSelect.removeClass("input-error"); | ||||
|                 const tooltip = this.element.data("tooltip"); | ||||
|                 if (tooltip) { | ||||
|                     this.element.data("tooltip", null); | ||||
|                     tooltip.delete(); | ||||
|                 } | ||||
|             } | ||||
|             if (result) { | ||||
|                 this.uiSelect.removeClass('input-error'); | ||||
|             } else { | ||||
|                 this.uiSelect.addClass('input-error'); | ||||
|             if (options?.returnErrorMessage === true) { | ||||
|                 return valid; | ||||
|             } | ||||
|             return result; | ||||
|             // Must return a boolean for no 3.x validator | ||||
|             return (typeof valid === "string") ? false : valid; | ||||
|         }, | ||||
|         show: function() { | ||||
|             this.uiSelect.show(); | ||||
|   | ||||
| @@ -32,39 +32,44 @@ RED.contextMenu = (function () { | ||||
|             const canRemoveFromGroup = hasSelection && !!selection.nodes[0].g | ||||
|             let hasGroup, isAllGroups = true, hasDisabledNode, hasEnabledNode, hasLabeledNode, hasUnlabeledNode; | ||||
|             if (hasSelection) { | ||||
|                 selection.nodes.forEach(n => { | ||||
|                 const nodes = selection.nodes.slice(); | ||||
|                 while (nodes.length) { | ||||
|                     const n = nodes.shift(); | ||||
|                     if (n.type === 'group') { | ||||
|                         hasGroup = true; | ||||
|                         nodes.push(...n.nodes); | ||||
|                     } else { | ||||
|                         isAllGroups = false; | ||||
|                     } | ||||
|                     if (n.d) { | ||||
|                         hasDisabledNode = true; | ||||
|                     } else { | ||||
|                         hasEnabledNode = true; | ||||
|                         if (n.d) { | ||||
|                             hasDisabledNode = true; | ||||
|                         } else { | ||||
|                             hasEnabledNode = true; | ||||
|                         } | ||||
|                     } | ||||
|                     if (n.l === undefined || n.l) { | ||||
|                         hasLabeledNode = true; | ||||
|                     } else { | ||||
|                         hasUnlabeledNode = true; | ||||
|                     } | ||||
|                 }); | ||||
|                 } | ||||
|             } | ||||
|             const offset = $("#red-ui-workspace-chart").offset() | ||||
|  | ||||
|             let addX = options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft() | ||||
|             let addY = options.y - offset.top + $("#red-ui-workspace-chart").scrollTop() | ||||
|             const scale = RED.view.scale() | ||||
|             const offset = $("#red-ui-workspace-chart").offset() | ||||
|             let addX = (options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft()) / scale | ||||
|             let addY = (options.y - offset.top + $("#red-ui-workspace-chart").scrollTop()) / scale | ||||
|  | ||||
|             if (RED.view.snapGrid) { | ||||
|                 const gridSize = RED.view.gridSize() | ||||
|                 addX = gridSize * Math.floor(addX / gridSize) | ||||
|                 addY = gridSize * Math.floor(addY / gridSize) | ||||
|                 addX = gridSize * Math.round(addX / gridSize) | ||||
|                 addY = gridSize * Math.round(addY / gridSize) | ||||
|             } | ||||
|  | ||||
|             menuItems.push( | ||||
|                 { onselect: 'core:show-action-list', label: RED._("contextMenu.showActionList"), onpostselect: function () { } } | ||||
|             ) | ||||
|  | ||||
|             if (RED.settings.theme("menu.menu-item-action-list", true)) { | ||||
|                 menuItems.push( | ||||
|                     { onselect: 'core:show-action-list', label: RED._("contextMenu.showActionList"), onpostselect: function () { } } | ||||
|                 ) | ||||
|             } | ||||
|             const insertOptions = [] | ||||
|             menuItems.push({ label: RED._("contextMenu.insert"), options: insertOptions }) | ||||
|             insertOptions.push( | ||||
| @@ -82,7 +87,9 @@ RED.contextMenu = (function () { | ||||
|                 }, | ||||
|                 (hasLinks) ? { // has least 1 wire selected | ||||
|                     label: RED._("contextMenu.junction"), | ||||
|                     onselect: 'core:split-wires-with-junctions', | ||||
|                     onselect: function () { | ||||
|                         RED.actions.invoke('core:split-wires-with-junctions', { x: addX, y: addY }) | ||||
|                     }, | ||||
|                     disabled: !canEdit || !hasLinks | ||||
|                 } : { | ||||
|                     label: RED._("contextMenu.junction"), | ||||
| @@ -118,10 +125,16 @@ RED.contextMenu = (function () { | ||||
|                     onselect: 'core:split-wire-with-link-nodes', | ||||
|                     disabled: !canEdit || !hasLinks | ||||
|                 }, | ||||
|                 null, | ||||
|                 { onselect: 'core:show-import-dialog', label: RED._('common.label.import')}, | ||||
|                 { onselect: 'core:show-examples-import-dialog', label: RED._('menu.label.importExample') } | ||||
|                 null | ||||
|             ) | ||||
|             if (RED.settings.theme("menu.menu-item-import-library", true)) { | ||||
|                 insertOptions.push( | ||||
|                     { onselect: 'core:show-import-dialog', label: RED._('common.label.import')}, | ||||
|                     { onselect: 'core:show-examples-import-dialog', label: RED._('menu.label.importExample') } | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
|  | ||||
|             if (hasSelection && canEdit) { | ||||
|                 const nodeOptions = [] | ||||
|                 if (!hasMultipleSelection && !isGroup) { | ||||
| @@ -194,8 +207,14 @@ RED.contextMenu = (function () { | ||||
|                 { onselect: 'core:paste-from-internal-clipboard', label: RED._("keyboard.pasteNode"), disabled: !canEdit || !RED.view.clipboard() }, | ||||
|                 { onselect: 'core:delete-selection', label: RED._('keyboard.deleteSelected'), disabled: !canEdit || !canDelete }, | ||||
|                 { onselect: 'core:delete-selection-and-reconnect', label: RED._('keyboard.deleteReconnect'), disabled: !canEdit || !canDelete }, | ||||
|                 { onselect: 'core:show-export-dialog', label: RED._("menu.label.export") }, | ||||
|                 { onselect: 'core:select-all-nodes', label: RED._("keyboard.selectAll") }, | ||||
|             ) | ||||
|             if (RED.settings.theme("menu.menu-item-export-library", true)) { | ||||
|                 menuItems.push( | ||||
|                     { onselect: 'core:show-export-dialog', label: RED._("menu.label.export") } | ||||
|                 ) | ||||
|             } | ||||
|             menuItems.push( | ||||
|                 { onselect: 'core:select-all-nodes', label: RED._("keyboard.selectAll") } | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -34,6 +34,8 @@ RED.deploy = (function() { | ||||
|  | ||||
|     var currentDiff = null; | ||||
|  | ||||
|     var activeBackgroundDeployNotification; | ||||
|  | ||||
|     function changeDeploymentType(type) { | ||||
|         deploymentType = type; | ||||
|         $("#red-ui-header-button-deploy-icon").attr("src",deploymentTypes[type].img); | ||||
| @@ -61,7 +63,7 @@ RED.deploy = (function() { | ||||
|                  '<img src="red/images/spin.svg"/>'+ | ||||
|                 '</span>'+ | ||||
|               '</a>'+ | ||||
|               '<a id="red-ui-header-button-deploy-options" class="red-ui-deploy-button" href="#"><i class="fa fa-caret-down"></i></a>'+ | ||||
|               '<a id="red-ui-header-button-deploy-options" class="red-ui-deploy-button" href="#"><i class="fa fa-caret-down"></i><i class="fa fa-lock"></i></a>'+ | ||||
|               '</span></li>').prependTo(".red-ui-header-toolbar"); | ||||
|             const mainMenuItems = [ | ||||
|                     {id:"deploymenu-item-full",toggle:"deploy-type",icon:"red/images/deploy-full.svg",label:RED._("deploy.full"),sublabel:RED._("deploy.fullDesc"),selected: true, onselect:function(s) { if(s){changeDeploymentType("full")}}}, | ||||
| @@ -112,53 +114,80 @@ RED.deploy = (function() { | ||||
|             RED.actions.add("core:set-deploy-type-to-modified-nodes",function() { RED.menu.setSelected("deploymenu-item-node",true); }); | ||||
|         } | ||||
|  | ||||
|  | ||||
|         window.addEventListener('beforeunload', function (event) { | ||||
|             if (RED.nodes.dirty()) { | ||||
|                 event.preventDefault(); | ||||
|                 event.stopImmediatePropagation() | ||||
|                 event.returnValue = RED._("deploy.confirm.undeployedChanges"); | ||||
|                 return | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         RED.events.on('workspace:dirty',function(state) { | ||||
|             if (RED.settings.user?.permissions === 'read') { | ||||
|                 return | ||||
|             } | ||||
|             if (state.dirty) { | ||||
|                 window.onbeforeunload = function() { | ||||
|                     return RED._("deploy.confirm.undeployedChanges"); | ||||
|                 } | ||||
|                 // window.onbeforeunload = function() { | ||||
|                 //     return  | ||||
|                 // } | ||||
|                 $("#red-ui-header-button-deploy").removeClass("disabled"); | ||||
|             } else { | ||||
|                 window.onbeforeunload = null; | ||||
|                 // window.onbeforeunload = null; | ||||
|                 $("#red-ui-header-button-deploy").addClass("disabled"); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         var activeNotifyMessage; | ||||
|         RED.comms.subscribe("notification/runtime-deploy",function(topic,msg) { | ||||
|             if (!activeNotifyMessage) { | ||||
|                 var currentRev = RED.nodes.version(); | ||||
|                 if (currentRev === null || deployInflight || currentRev === msg.revision) { | ||||
|                     return; | ||||
|                 } | ||||
|                 var message = $('<p>').text(RED._('deploy.confirm.backgroundUpdate')); | ||||
|                 activeNotifyMessage = RED.notify(message,{ | ||||
|                     modal: true, | ||||
|                     fixed: true, | ||||
|                     buttons: [ | ||||
|                         { | ||||
|                             text: RED._('deploy.confirm.button.ignore'), | ||||
|                             click: function() { | ||||
|                                 activeNotifyMessage.close(); | ||||
|                                 activeNotifyMessage = null; | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             text: RED._('deploy.confirm.button.review'), | ||||
|                             class: "primary", | ||||
|                             click: function() { | ||||
|                                 activeNotifyMessage.close(); | ||||
|                                 var nns = RED.nodes.createCompleteNodeSet(); | ||||
|                                 resolveConflict(nns,false); | ||||
|                                 activeNotifyMessage = null; | ||||
|                             } | ||||
|             var currentRev = RED.nodes.version(); | ||||
|             if (currentRev === null || deployInflight || currentRev === msg.revision) { | ||||
|                 return; | ||||
|             } | ||||
|             if (activeBackgroundDeployNotification?.hidden && !activeBackgroundDeployNotification?.closed) { | ||||
|                 activeBackgroundDeployNotification.showNotification() | ||||
|                 return | ||||
|             } | ||||
|             const message = $('<p>').text(RED._('deploy.confirm.backgroundUpdate')); | ||||
|             const options = { | ||||
|                 id: 'background-update', | ||||
|                 type: 'compact', | ||||
|                 modal: false, | ||||
|                 fixed: true, | ||||
|                 timeout: 10000, | ||||
|                 buttons: [ | ||||
|                     { | ||||
|                         text: RED._('deploy.confirm.button.review'), | ||||
|                         class: "primary", | ||||
|                         click: function() { | ||||
|                             activeBackgroundDeployNotification.hideNotification(); | ||||
|                             var nns = RED.nodes.createCompleteNodeSet(); | ||||
|                             resolveConflict(nns,false); | ||||
|                         } | ||||
|                     ] | ||||
|                 }); | ||||
|                     } | ||||
|                 ] | ||||
|             } | ||||
|             if (!activeBackgroundDeployNotification || activeBackgroundDeployNotification.closed) { | ||||
|                 activeBackgroundDeployNotification = RED.notify(message, options) | ||||
|             } else { | ||||
|                 activeBackgroundDeployNotification.update(message, options) | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|  | ||||
|         updateLockedState() | ||||
|         RED.events.on('login', updateLockedState) | ||||
|     } | ||||
|  | ||||
|     function updateLockedState() { | ||||
|         if (RED.settings.user?.permissions === 'read') { | ||||
|             $(".red-ui-deploy-button-group").addClass("readOnly"); | ||||
|             $("#red-ui-header-button-deploy").addClass("disabled"); | ||||
|         } else { | ||||
|             $(".red-ui-deploy-button-group").removeClass("readOnly"); | ||||
|             if (RED.nodes.dirty()) { | ||||
|                 $("#red-ui-header-button-deploy").removeClass("disabled"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function getNodeInfo(node) { | ||||
| @@ -213,7 +242,11 @@ RED.deploy = (function() { | ||||
|                 class: "primary disabled", | ||||
|                 click: function() { | ||||
|                     if (!$("#red-ui-deploy-dialog-confirm-deploy-review").hasClass('disabled')) { | ||||
|                         RED.diff.showRemoteDiff(); | ||||
|                         RED.diff.showRemoteDiff(null, { | ||||
|                             onmerge: function () { | ||||
|                                 activeBackgroundDeployNotification.close() | ||||
|                             } | ||||
|                         }); | ||||
|                         conflictNotification.close(); | ||||
|                     } | ||||
|                 } | ||||
| @@ -226,6 +259,7 @@ RED.deploy = (function() { | ||||
|                     if (!$("#red-ui-deploy-dialog-confirm-deploy-merge").hasClass('disabled')) { | ||||
|                         RED.diff.mergeDiff(currentDiff); | ||||
|                         conflictNotification.close(); | ||||
|                         activeBackgroundDeployNotification.close() | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| @@ -238,6 +272,7 @@ RED.deploy = (function() { | ||||
|                 click: function() { | ||||
|                     save(true,activeDeploy); | ||||
|                     conflictNotification.close(); | ||||
|                     activeBackgroundDeployNotification.close() | ||||
|                 } | ||||
|             }) | ||||
|         } | ||||
| @@ -248,21 +283,17 @@ RED.deploy = (function() { | ||||
|             buttons: buttons | ||||
|         }); | ||||
|  | ||||
|         var now = Date.now(); | ||||
|         RED.diff.getRemoteDiff(function(diff) { | ||||
|             var ellapsed = Math.max(1000 - (Date.now()-now), 0); | ||||
|             currentDiff = diff; | ||||
|             setTimeout(function() { | ||||
|                 conflictCheck.hide(); | ||||
|                 var d = Object.keys(diff.conflicts); | ||||
|                 if (d.length === 0) { | ||||
|                     conflictAutoMerge.show(); | ||||
|                     $("#red-ui-deploy-dialog-confirm-deploy-merge").removeClass('disabled') | ||||
|                 } else { | ||||
|                     conflictManualMerge.show(); | ||||
|                 } | ||||
|                 $("#red-ui-deploy-dialog-confirm-deploy-review").removeClass('disabled') | ||||
|             },ellapsed); | ||||
|             conflictCheck.hide(); | ||||
|             var d = Object.keys(diff.conflicts); | ||||
|             if (d.length === 0) { | ||||
|                 conflictAutoMerge.show(); | ||||
|                 $("#red-ui-deploy-dialog-confirm-deploy-merge").removeClass('disabled') | ||||
|             } else { | ||||
|                 conflictManualMerge.show(); | ||||
|             } | ||||
|             $("#red-ui-deploy-dialog-confirm-deploy-review").removeClass('disabled') | ||||
|         }) | ||||
|     } | ||||
|     function cropList(list) { | ||||
| @@ -558,7 +589,9 @@ RED.deploy = (function() { | ||||
|                 RED.notify('<p>' + RED._("deploy.successfulDeploy") + '</p>', "success"); | ||||
|             } | ||||
|             const flowsToLock = new Set() | ||||
|             // Node's properties cannot be modified if its workspace is locked. | ||||
|             function ensureUnlocked(id) { | ||||
|                 // TODO: `RED.nodes.subflow` is useless | ||||
|                 const flow = id && (RED.nodes.workspace(id) || RED.nodes.subflow(id) || null); | ||||
|                 const isLocked = flow ? flow.locked : false; | ||||
|                 if (flow && isLocked) { | ||||
| @@ -611,23 +644,34 @@ RED.deploy = (function() { | ||||
|                     delete confNode.credentials; | ||||
|                 } | ||||
|             }); | ||||
|             // Subflow cannot be locked | ||||
|             RED.nodes.eachSubflow(function (subflow) { | ||||
|                 subflow.changed = false; | ||||
|                 if (subflow.changed) { | ||||
|                     subflow.changed = false; | ||||
|                     RED.events.emit("subflows:change", subflow); | ||||
|                 } | ||||
|             }); | ||||
|             RED.nodes.eachWorkspace(function (ws) { | ||||
|                 if (ws.changed || ws.added) { | ||||
|                     ensureUnlocked(ws.z) | ||||
|                     // Ensure the Workspace is unlocked to modify its properties. | ||||
|                     ensureUnlocked(ws.id); | ||||
|                     ws.changed = false; | ||||
|                     delete ws.added | ||||
|                     if (flowsToLock.has(ws)) { | ||||
|                         ws.locked = true; | ||||
|                         flowsToLock.delete(ws); | ||||
|                     } | ||||
|                     RED.events.emit("flows:change", ws) | ||||
|                 } | ||||
|             }); | ||||
|             // Ensures all workspaces to be locked have been locked. | ||||
|             flowsToLock.forEach(flow => { | ||||
|                 flow.locked = true | ||||
|             }) | ||||
|             // Once deployed, cannot undo back to a clean state | ||||
|             RED.history.markAllDirty(); | ||||
|             RED.view.redraw(); | ||||
|             RED.sidebar.config.refresh(); | ||||
|             RED.events.emit("deploy"); | ||||
|         }).fail(function (xhr, textStatus, err) { | ||||
|             RED.nodes.dirty(true); | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| RED.diff = (function() { | ||||
|  | ||||
|     var currentDiff = {}; | ||||
|     var diffVisible = false; | ||||
|     var diffList; | ||||
| @@ -62,12 +61,14 @@ RED.diff = (function() { | ||||
|                         addedCount:0, | ||||
|                         deletedCount:0, | ||||
|                         changedCount:0, | ||||
|                         movedCount:0, | ||||
|                         unchangedCount: 0 | ||||
|                     }, | ||||
|                     remote: { | ||||
|                         addedCount:0, | ||||
|                         deletedCount:0, | ||||
|                         changedCount:0, | ||||
|                         movedCount:0, | ||||
|                         unchangedCount: 0 | ||||
|                     }, | ||||
|                     conflicts: 0 | ||||
| @@ -138,7 +139,7 @@ RED.diff = (function() { | ||||
|                             $(this).parent().toggleClass('collapsed'); | ||||
|                         }); | ||||
|  | ||||
|                         createNodePropertiesTable(def,tab,localTabNode,remoteTabNode,conflicts).appendTo(div); | ||||
|                         createNodePropertiesTable(def,tab,localTabNode,remoteTabNode).appendTo(div); | ||||
|                         selectState = ""; | ||||
|                         if (conflicts[tab.id]) { | ||||
|                             flowStats.conflicts++; | ||||
| @@ -208,19 +209,26 @@ RED.diff = (function() { | ||||
|                         var localStats = $('<span>',{class:"red-ui-diff-list-flow-stats"}).appendTo(localCell); | ||||
|                         $('<span class="red-ui-diff-status"></span>').text(RED._('diff.nodeCount',{count:localNodeCount})).appendTo(localStats); | ||||
|  | ||||
|                         if (flowStats.conflicts + flowStats.local.addedCount + flowStats.local.changedCount + flowStats.local.deletedCount > 0) { | ||||
|                         if (flowStats.conflicts + flowStats.local.addedCount + flowStats.local.changedCount + flowStats.local.movedCount + flowStats.local.deletedCount > 0) { | ||||
|                             $('<span class="red-ui-diff-status"> [ </span>').appendTo(localStats); | ||||
|                             if (flowStats.conflicts > 0) { | ||||
|                                 $('<span class="red-ui-diff-status-conflict"><span class="red-ui-diff-status"><i class="fa fa-exclamation"></i> '+flowStats.conflicts+'</span></span>').appendTo(localStats); | ||||
|                             } | ||||
|                             if (flowStats.local.addedCount > 0) { | ||||
|                                 $('<span class="red-ui-diff-status-added"><span class="red-ui-diff-status"><i class="fa fa-plus-square"></i> '+flowStats.local.addedCount+'</span></span>').appendTo(localStats); | ||||
|                                 const cell = $('<span class="red-ui-diff-status-added"><span class="red-ui-diff-status"><i class="fa fa-plus-square"></i> '+flowStats.local.addedCount+'</span></span>').appendTo(localStats); | ||||
|                                 RED.popover.tooltip(cell, RED._('diff.type.added')) | ||||
|                             } | ||||
|                             if (flowStats.local.changedCount > 0) { | ||||
|                                 $('<span class="red-ui-diff-status-changed"><span class="red-ui-diff-status"><i class="fa fa-square"></i> '+flowStats.local.changedCount+'</span></span>').appendTo(localStats); | ||||
|                                 const cell = $('<span class="red-ui-diff-status-changed"><span class="red-ui-diff-status"><i class="fa fa-square"></i> '+flowStats.local.changedCount+'</span></span>').appendTo(localStats); | ||||
|                                 RED.popover.tooltip(cell, RED._('diff.type.changed')) | ||||
|                             } | ||||
|                             if (flowStats.local.movedCount > 0) { | ||||
|                                 const cell = $('<span class="red-ui-diff-status-moved"><span class="red-ui-diff-status"><i class="fa fa-square"></i> '+flowStats.local.movedCount+'</span></span>').appendTo(localStats); | ||||
|                                 RED.popover.tooltip(cell, RED._('diff.type.moved')) | ||||
|                             } | ||||
|                             if (flowStats.local.deletedCount > 0) { | ||||
|                                 $('<span class="red-ui-diff-status-deleted"><span class="red-ui-diff-status"><i class="fa fa-minus-square"></i> '+flowStats.local.deletedCount+'</span></span>').appendTo(localStats); | ||||
|                                 const cell = $('<span class="red-ui-diff-status-deleted"><span class="red-ui-diff-status"><i class="fa fa-minus-square"></i> '+flowStats.local.deletedCount+'</span></span>').appendTo(localStats); | ||||
|                                 RED.popover.tooltip(cell, RED._('diff.type.deleted')) | ||||
|                             } | ||||
|                             $('<span class="red-ui-diff-status"> ] </span>').appendTo(localStats); | ||||
|                         } | ||||
| @@ -246,19 +254,26 @@ RED.diff = (function() { | ||||
|                             } | ||||
|                             var remoteStats = $('<span>',{class:"red-ui-diff-list-flow-stats"}).appendTo(remoteCell); | ||||
|                             $('<span class="red-ui-diff-status"></span>').text(RED._('diff.nodeCount',{count:remoteNodeCount})).appendTo(remoteStats); | ||||
|                             if (flowStats.conflicts + flowStats.remote.addedCount + flowStats.remote.changedCount + flowStats.remote.deletedCount > 0) { | ||||
|                             if (flowStats.conflicts + flowStats.remote.addedCount + flowStats.remote.changedCount + flowStats.remote.movedCount + flowStats.remote.deletedCount > 0) { | ||||
|                                 $('<span class="red-ui-diff-status"> [ </span>').appendTo(remoteStats); | ||||
|                                 if (flowStats.conflicts > 0) { | ||||
|                                     $('<span class="red-ui-diff-status-conflict"><span class="red-ui-diff-status"><i class="fa fa-exclamation"></i> '+flowStats.conflicts+'</span></span>').appendTo(remoteStats); | ||||
|                                 } | ||||
|                                 if (flowStats.remote.addedCount > 0) { | ||||
|                                     $('<span class="red-ui-diff-status-added"><span class="red-ui-diff-status"><i class="fa fa-plus-square"></i> '+flowStats.remote.addedCount+'</span></span>').appendTo(remoteStats); | ||||
|                                     const cell = $('<span class="red-ui-diff-status-added"><span class="red-ui-diff-status"><i class="fa fa-plus-square"></i> '+flowStats.remote.addedCount+'</span></span>').appendTo(remoteStats); | ||||
|                                     RED.popover.tooltip(cell, RED._('diff.type.added')) | ||||
|                                 } | ||||
|                                 if (flowStats.remote.changedCount > 0) { | ||||
|                                     $('<span class="red-ui-diff-status-changed"><span class="red-ui-diff-status"><i class="fa fa-square"></i> '+flowStats.remote.changedCount+'</span></span>').appendTo(remoteStats); | ||||
|                                     const cell = $('<span class="red-ui-diff-status-changed"><span class="red-ui-diff-status"><i class="fa fa-square"></i> '+flowStats.remote.changedCount+'</span></span>').appendTo(remoteStats); | ||||
|                                     RED.popover.tooltip(cell, RED._('diff.type.changed')) | ||||
|                                 } | ||||
|                                 if (flowStats.remote.movedCount > 0) { | ||||
|                                     const cell = $('<span class="red-ui-diff-status-moved"><span class="red-ui-diff-status"><i class="fa fa-square"></i> '+flowStats.remote.movedCount+'</span></span>').appendTo(remoteStats); | ||||
|                                     RED.popover.tooltip(cell, RED._('diff.type.moved')) | ||||
|                                 } | ||||
|                                 if (flowStats.remote.deletedCount > 0) { | ||||
|                                     $('<span class="red-ui-diff-status-deleted"><span class="red-ui-diff-status"><i class="fa fa-minus-square"></i> '+flowStats.remote.deletedCount+'</span></span>').appendTo(remoteStats); | ||||
|                                     const cell = $('<span class="red-ui-diff-status-deleted"><span class="red-ui-diff-status"><i class="fa fa-minus-square"></i> '+flowStats.remote.deletedCount+'</span></span>').appendTo(remoteStats); | ||||
|                                     RED.popover.tooltip(cell, RED._('diff.type.deleted')) | ||||
|                                 } | ||||
|                                 $('<span class="red-ui-diff-status"> ] </span>').appendTo(remoteStats); | ||||
|                             } | ||||
| @@ -293,7 +308,7 @@ RED.diff = (function() { | ||||
|         if (options.mode === "merge") { | ||||
|             diffPanel.addClass("red-ui-diff-panel-merge"); | ||||
|         } | ||||
|         var diffList = createDiffTable(diffPanel, diff); | ||||
|         var diffList = createDiffTable(diffPanel, diff, options); | ||||
|  | ||||
|         var localDiff = diff.localDiff; | ||||
|         var remoteDiff = diff.remoteDiff; | ||||
| @@ -482,7 +497,7 @@ RED.diff = (function() { | ||||
|             } | ||||
|         }) | ||||
|         if (c === 0) { | ||||
|             result.text("none"); | ||||
|             result.text(RED._("diff.type.none")); | ||||
|         } else { | ||||
|             list.appendTo(result); | ||||
|         } | ||||
| @@ -516,7 +531,6 @@ RED.diff = (function() { | ||||
|  | ||||
|         var hasChanges = false; // exists in original and local/remote but with changes | ||||
|         var unChanged = true; // existing in original,local,remote unchanged | ||||
|         var localChanged = false; | ||||
|  | ||||
|         if (localDiff.added[node.id]) { | ||||
|             stats.local.addedCount++; | ||||
| @@ -535,12 +549,20 @@ RED.diff = (function() { | ||||
|             unChanged = false; | ||||
|         } | ||||
|         if (localDiff.changed[node.id]) { | ||||
|             stats.local.changedCount++; | ||||
|             if (localDiff.positionChanged[node.id]) { | ||||
|                 stats.local.movedCount++ | ||||
|             } else { | ||||
|                 stats.local.changedCount++; | ||||
|             } | ||||
|             hasChanges = true; | ||||
|             unChanged = false; | ||||
|         } | ||||
|         if (remoteDiff && remoteDiff.changed[node.id]) { | ||||
|             stats.remote.changedCount++; | ||||
|             if (remoteDiff.positionChanged[node.id]) { | ||||
|                 stats.remote.movedCount++ | ||||
|             } else { | ||||
|                 stats.remote.changedCount++; | ||||
|             } | ||||
|             hasChanges = true; | ||||
|             unChanged = false; | ||||
|         } | ||||
| @@ -605,27 +627,32 @@ RED.diff = (function() { | ||||
|                     localNodeDiv.addClass("red-ui-diff-status-moved"); | ||||
|                     var localMovedMessage = ""; | ||||
|                     if (node.z === localN.z) { | ||||
|                         localMovedMessage = RED._("diff.type.movedFrom",{id:(localDiff.currentConfig.all[node.id].z||'global')}); | ||||
|                         const movedFromNodeTab = localDiff.currentConfig.all[localDiff.currentConfig.all[node.id].z] | ||||
|                         const movedFromLabel = `'${movedFromNodeTab ? (movedFromNodeTab.label || movedFromNodeTab.id) : 'global'}'` | ||||
|                         localMovedMessage = RED._("diff.type.movedFrom",{id: movedFromLabel}); | ||||
|                     } else { | ||||
|                         localMovedMessage = RED._("diff.type.movedTo",{id:(localN.z||'global')}); | ||||
|                         const movedToNodeTab = localDiff.newConfig.all[localN.z] | ||||
|                         const movedToLabel = `'${movedToNodeTab ? (movedToNodeTab.label || movedToNodeTab.id) : 'global'}'` | ||||
|                         localMovedMessage = RED._("diff.type.movedTo",{id:movedToLabel}); | ||||
|                     } | ||||
|                     $('<span class="red-ui-diff-status"><i class="fa fa-caret-square-o-right"></i> '+localMovedMessage+'</span>').appendTo(localNodeDiv); | ||||
|                 } | ||||
|                 localChanged = true; | ||||
|             } else if (localDiff.deleted[node.z]) { | ||||
|                 localNodeDiv.addClass("red-ui-diff-empty"); | ||||
|                 localChanged = true; | ||||
|             } else if (localDiff.deleted[node.id]) { | ||||
|                 localNodeDiv.addClass("red-ui-diff-status-deleted"); | ||||
|                 $('<span class="red-ui-diff-status"><i class="fa fa-minus-square"></i> <span data-i18n="diff.type.deleted"></span></span>').appendTo(localNodeDiv); | ||||
|                 localChanged = true; | ||||
|             } else if (localDiff.changed[node.id]) { | ||||
|                 if (localDiff.newConfig.all[node.id].z !== node.z) { | ||||
|                     localNodeDiv.addClass("red-ui-diff-empty"); | ||||
|                 } else { | ||||
|                     localNodeDiv.addClass("red-ui-diff-status-changed"); | ||||
|                     $('<span class="red-ui-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.changed"></span></span>').appendTo(localNodeDiv); | ||||
|                     localChanged = true; | ||||
|                     if (localDiff.positionChanged[node.id]) { | ||||
|                         localNodeDiv.addClass("red-ui-diff-status-moved"); | ||||
|                         $('<span class="red-ui-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.moved"></span></span>').appendTo(localNodeDiv); | ||||
|                     } else { | ||||
|                         localNodeDiv.addClass("red-ui-diff-status-changed"); | ||||
|                         $('<span class="red-ui-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.changed"></span></span>').appendTo(localNodeDiv); | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|                 if (localDiff.newConfig.all[node.id].z !== node.z) { | ||||
| @@ -646,9 +673,13 @@ RED.diff = (function() { | ||||
|                         remoteNodeDiv.addClass("red-ui-diff-status-moved"); | ||||
|                         var remoteMovedMessage = ""; | ||||
|                         if (node.z === remoteN.z) { | ||||
|                             remoteMovedMessage = RED._("diff.type.movedFrom",{id:(remoteDiff.currentConfig.all[node.id].z||'global')}); | ||||
|                             const movedFromNodeTab = remoteDiff.currentConfig.all[remoteDiff.currentConfig.all[node.id].z] | ||||
|                             const movedFromLabel = `'${movedFromNodeTab ? (movedFromNodeTab.label || movedFromNodeTab.id) : 'global'}'` | ||||
|                             remoteMovedMessage = RED._("diff.type.movedFrom",{id:movedFromLabel}); | ||||
|                         } else { | ||||
|                             remoteMovedMessage = RED._("diff.type.movedTo",{id:(remoteN.z||'global')}); | ||||
|                             const movedToNodeTab = remoteDiff.newConfig.all[remoteN.z] | ||||
|                             const movedToLabel = `'${movedToNodeTab ? (movedToNodeTab.label || movedToNodeTab.id) : 'global'}'` | ||||
|                             remoteMovedMessage = RED._("diff.type.movedTo",{id:movedToLabel}); | ||||
|                         } | ||||
|                         $('<span class="red-ui-diff-status"><i class="fa fa-caret-square-o-right"></i> '+remoteMovedMessage+'</span>').appendTo(remoteNodeDiv); | ||||
|                     } | ||||
| @@ -661,8 +692,13 @@ RED.diff = (function() { | ||||
|                     if (remoteDiff.newConfig.all[node.id].z !== node.z) { | ||||
|                         remoteNodeDiv.addClass("red-ui-diff-empty"); | ||||
|                     } else { | ||||
|                         remoteNodeDiv.addClass("red-ui-diff-status-changed"); | ||||
|                         $('<span class="red-ui-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.changed"></span></span>').appendTo(remoteNodeDiv); | ||||
|                         if (remoteDiff.positionChanged[node.id]) { | ||||
|                             remoteNodeDiv.addClass("red-ui-diff-status-moved"); | ||||
|                             $('<span class="red-ui-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.moved"></span></span>').appendTo(remoteNodeDiv); | ||||
|                         } else { | ||||
|                             remoteNodeDiv.addClass("red-ui-diff-status-changed"); | ||||
|                             $('<span class="red-ui-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.changed"></span></span>').appendTo(remoteNodeDiv); | ||||
|                         } | ||||
|                     } | ||||
|                 } else { | ||||
|                     if (remoteDiff.newConfig.all[node.id].z !== node.z) { | ||||
| @@ -785,10 +821,10 @@ RED.diff = (function() { | ||||
|                 conflict = true; | ||||
|             } | ||||
|             row = $("<tr>").appendTo(nodePropertiesTableBody); | ||||
|             $("<td>",{class:"red-ui-diff-list-cell-label"}).text("position").appendTo(row); | ||||
|             $("<td>",{class:"red-ui-diff-list-cell-label"}).text(RED._("diff.type.position")).appendTo(row); | ||||
|             localCell = $("<td>",{class:"red-ui-diff-list-cell red-ui-diff-list-node-local"}).appendTo(row); | ||||
|             if (localNode) { | ||||
|                 localCell.addClass("red-ui-diff-status-"+(localChanged?"changed":"unchanged")); | ||||
|                 localCell.addClass("red-ui-diff-status-"+(localChanged?"moved":"unchanged")); | ||||
|                 $('<span class="red-ui-diff-status">'+(localChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(localCell); | ||||
|                 element = $('<span class="red-ui-diff-list-element"></span>').appendTo(localCell); | ||||
|                 var localPosition = {x:localNode.x,y:localNode.y}; | ||||
| @@ -813,7 +849,7 @@ RED.diff = (function() { | ||||
|  | ||||
|             if (remoteNode !== undefined) { | ||||
|                 remoteCell = $("<td>",{class:"red-ui-diff-list-cell red-ui-diff-list-node-remote"}).appendTo(row); | ||||
|                 remoteCell.addClass("red-ui-diff-status-"+(remoteChanged?"changed":"unchanged")); | ||||
|                 remoteCell.addClass("red-ui-diff-status-"+(remoteChanged?"moved":"unchanged")); | ||||
|                 if (remoteNode) { | ||||
|                     $('<span class="red-ui-diff-status">'+(remoteChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(remoteCell); | ||||
|                     element = $('<span class="red-ui-diff-list-element"></span>').appendTo(remoteCell); | ||||
| @@ -863,7 +899,7 @@ RED.diff = (function() { | ||||
|                 conflict = true; | ||||
|             } | ||||
|             row = $("<tr>").appendTo(nodePropertiesTableBody); | ||||
|             $("<td>",{class:"red-ui-diff-list-cell-label"}).text("wires").appendTo(row); | ||||
|             $("<td>",{class:"red-ui-diff-list-cell-label"}).text(RED._("diff.type.wires")).appendTo(row); | ||||
|             localCell = $("<td>",{class:"red-ui-diff-list-cell red-ui-diff-list-node-local"}).appendTo(row); | ||||
|             if (localNode) { | ||||
|                 if (!conflict) { | ||||
| @@ -1099,11 +1135,11 @@ RED.diff = (function() { | ||||
|     //     var diff = generateDiff(originalFlow,nns); | ||||
|     //     showDiff(diff); | ||||
|     // } | ||||
|     function showRemoteDiff(diff) { | ||||
|         if (diff === undefined) { | ||||
|             getRemoteDiff(showRemoteDiff); | ||||
|     function showRemoteDiff(diff, options = {}) { | ||||
|         if (!diff) { | ||||
|             getRemoteDiff((remoteDiff) => showRemoteDiff(remoteDiff, options)); | ||||
|         } else { | ||||
|             showDiff(diff,{mode:'merge'}); | ||||
|             showDiff(diff,{...options, mode:'merge'}); | ||||
|         } | ||||
|     } | ||||
|     function parseNodes(nodeList) { | ||||
| @@ -1144,23 +1180,53 @@ RED.diff = (function() { | ||||
|         } | ||||
|     } | ||||
|     function generateDiff(currentNodes,newNodes) { | ||||
|         var currentConfig = parseNodes(currentNodes); | ||||
|         var newConfig = parseNodes(newNodes); | ||||
|         var added = {}; | ||||
|         var deleted = {}; | ||||
|         var changed = {}; | ||||
|         var moved = {}; | ||||
|         const currentConfig = parseNodes(currentNodes); | ||||
|         const newConfig = parseNodes(newNodes); | ||||
|         const added = {}; | ||||
|         const deleted = {}; | ||||
|         const changed = {}; | ||||
|         const positionChanged = {}; | ||||
|         const moved = {}; | ||||
|  | ||||
|         Object.keys(currentConfig.all).forEach(function(id) { | ||||
|             var node = RED.nodes.workspace(id)||RED.nodes.subflow(id)||RED.nodes.node(id); | ||||
|             const node = RED.nodes.workspace(id)||RED.nodes.subflow(id)||RED.nodes.node(id); | ||||
|             if (!newConfig.all.hasOwnProperty(id)) { | ||||
|                 deleted[id] = true; | ||||
|             } else if (JSON.stringify(currentConfig.all[id]) !== JSON.stringify(newConfig.all[id])) { | ||||
|                 return | ||||
|             } | ||||
|             const currentConfigJSON = JSON.stringify(currentConfig.all[id]) | ||||
|             const newConfigJSON = JSON.stringify(newConfig.all[id]) | ||||
|              | ||||
|             if (currentConfigJSON !== newConfigJSON) { | ||||
|                 changed[id] = true; | ||||
|  | ||||
|                 if (currentConfig.all[id].z !== newConfig.all[id].z) { | ||||
|                     moved[id] = true; | ||||
|                 } else if ( | ||||
|                     currentConfig.all[id].x !== newConfig.all[id].x || | ||||
|                     currentConfig.all[id].y !== newConfig.all[id].y || | ||||
|                     currentConfig.all[id].w !== newConfig.all[id].w || | ||||
|                     currentConfig.all[id].h !== newConfig.all[id].h | ||||
|                 ) { | ||||
|                     // This node's position on its parent has changed. We want to | ||||
|                     // check if this is the *only* change for this given node | ||||
|                     const currentNodeClone = JSON.parse(currentConfigJSON) | ||||
|                     const newNodeClone = JSON.parse(newConfigJSON) | ||||
|  | ||||
|                     delete currentNodeClone.x | ||||
|                     delete currentNodeClone.y | ||||
|                     delete currentNodeClone.w | ||||
|                     delete currentNodeClone.h | ||||
|                     delete newNodeClone.x | ||||
|                     delete newNodeClone.y | ||||
|                     delete newNodeClone.w | ||||
|                     delete newNodeClone.h | ||||
|                      | ||||
|                     if (JSON.stringify(currentNodeClone) === JSON.stringify(newNodeClone)) { | ||||
|                         // Only the position has changed - everything else is the same | ||||
|                         positionChanged[id] = true | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|             } | ||||
|         }); | ||||
|         Object.keys(newConfig.all).forEach(function(id) { | ||||
| @@ -1169,13 +1235,14 @@ RED.diff = (function() { | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         var diff = { | ||||
|             currentConfig: currentConfig, | ||||
|             newConfig: newConfig, | ||||
|             added: added, | ||||
|             deleted: deleted, | ||||
|             changed: changed, | ||||
|             moved: moved | ||||
|         const diff = { | ||||
|             currentConfig, | ||||
|             newConfig, | ||||
|             added, | ||||
|             deleted, | ||||
|             changed, | ||||
|             positionChanged, | ||||
|             moved | ||||
|         }; | ||||
|         return diff; | ||||
|     } | ||||
| @@ -1240,12 +1307,14 @@ RED.diff = (function() { | ||||
|         return diff; | ||||
|     } | ||||
|  | ||||
|     function showDiff(diff,options) { | ||||
|     function showDiff(diff, options) { | ||||
|         if (diffVisible) { | ||||
|             return; | ||||
|         } | ||||
|         options = options || {}; | ||||
|         var mode = options.mode || 'merge'; | ||||
|          | ||||
|         options.hidePositionChanges = true | ||||
|  | ||||
|         var localDiff = diff.localDiff; | ||||
|         var remoteDiff = diff.remoteDiff; | ||||
| @@ -1315,6 +1384,9 @@ RED.diff = (function() { | ||||
|                         if (!$("#red-ui-diff-view-diff-merge").hasClass('disabled')) { | ||||
|                             refreshConflictHeader(diff); | ||||
|                             mergeDiff(diff); | ||||
|                             if (options.onmerge) { | ||||
|                                 options.onmerge() | ||||
|                             } | ||||
|                             RED.tray.close(); | ||||
|                         } | ||||
|                     } | ||||
| @@ -1345,6 +1417,7 @@ RED.diff = (function() { | ||||
|         var newConfig = []; | ||||
|         var node; | ||||
|         var nodeChangedStates = {}; | ||||
|         var nodeMovedStates = {}; | ||||
|         var localChangedStates = {}; | ||||
|         for (id in localDiff.newConfig.all) { | ||||
|             if (localDiff.newConfig.all.hasOwnProperty(id)) { | ||||
| @@ -1352,12 +1425,14 @@ RED.diff = (function() { | ||||
|                 if (resolutions[id] === 'local') { | ||||
|                     if (node) { | ||||
|                         nodeChangedStates[id] = node.changed; | ||||
|                         nodeMovedStates[id] = node.moved; | ||||
|                     } | ||||
|                     newConfig.push(localDiff.newConfig.all[id]); | ||||
|                 } else if (resolutions[id] === 'remote') { | ||||
|                     if (!remoteDiff.deleted[id] && remoteDiff.newConfig.all.hasOwnProperty(id)) { | ||||
|                         if (node) { | ||||
|                             nodeChangedStates[id] = node.changed; | ||||
|                             nodeMovedStates[id] = node.moved; | ||||
|                         } | ||||
|                         localChangedStates[id] = 1; | ||||
|                         newConfig.push(remoteDiff.newConfig.all[id]); | ||||
| @@ -1381,8 +1456,9 @@ RED.diff = (function() { | ||||
|         } | ||||
|         return { | ||||
|             config: newConfig, | ||||
|             nodeChangedStates: nodeChangedStates, | ||||
|             localChangedStates: localChangedStates | ||||
|             nodeChangedStates, | ||||
|             nodeMovedStates, | ||||
|             localChangedStates | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -1393,6 +1469,7 @@ RED.diff = (function() { | ||||
|  | ||||
|         var newConfig = appliedDiff.config; | ||||
|         var nodeChangedStates = appliedDiff.nodeChangedStates; | ||||
|         var nodeMovedStates = appliedDiff.nodeMovedStates; | ||||
|         var localChangedStates = appliedDiff.localChangedStates; | ||||
|  | ||||
|         var isDirty = RED.nodes.dirty(); | ||||
| @@ -1401,33 +1478,56 @@ RED.diff = (function() { | ||||
|             t:"replace", | ||||
|             config: RED.nodes.createCompleteNodeSet(), | ||||
|             changed: nodeChangedStates, | ||||
|             moved: nodeMovedStates, | ||||
|             complete: true, | ||||
|             dirty: isDirty, | ||||
|             rev: RED.nodes.version() | ||||
|         } | ||||
|  | ||||
|         RED.history.push(historyEvent); | ||||
|  | ||||
|         var originalFlow = RED.nodes.originalFlow(); | ||||
|         // originalFlow is what the editor things it loaded | ||||
|         //  - add any newly added nodes from remote diff as they are now part of the record | ||||
|         for (var id in diff.remoteDiff.added) { | ||||
|             if (diff.remoteDiff.added.hasOwnProperty(id)) { | ||||
|                 if (diff.remoteDiff.newConfig.all.hasOwnProperty(id)) { | ||||
|                     originalFlow.push(JSON.parse(JSON.stringify(diff.remoteDiff.newConfig.all[id]))); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         // var originalFlow = RED.nodes.originalFlow(); | ||||
|         // // originalFlow is what the editor thinks it loaded | ||||
|         // //  - add any newly added nodes from remote diff as they are now part of the record | ||||
|         // for (var id in diff.remoteDiff.added) { | ||||
|         //     if (diff.remoteDiff.added.hasOwnProperty(id)) { | ||||
|         //         if (diff.remoteDiff.newConfig.all.hasOwnProperty(id)) { | ||||
|         //             originalFlow.push(JSON.parse(JSON.stringify(diff.remoteDiff.newConfig.all[id]))); | ||||
|         //         } | ||||
|         //     } | ||||
|         // } | ||||
|  | ||||
|         RED.nodes.clear(); | ||||
|         var imported = RED.nodes.import(newConfig); | ||||
|  | ||||
|         // Restore the original flow so subsequent merge resolutions can properly | ||||
|         // identify new-vs-old | ||||
|         RED.nodes.originalFlow(originalFlow); | ||||
|         // // Restore the original flow so subsequent merge resolutions can properly | ||||
|         // // identify new-vs-old | ||||
|         // RED.nodes.originalFlow(originalFlow); | ||||
|  | ||||
|         // Clear all change flags from the import | ||||
|         RED.nodes.dirty(false); | ||||
|          | ||||
|         const flowsToLock = new Set() | ||||
|         function ensureUnlocked(id) { | ||||
|             const flow = id && (RED.nodes.workspace(id) || RED.nodes.subflow(id) || null); | ||||
|             const isLocked = flow ? flow.locked : false; | ||||
|             if (flow && isLocked) { | ||||
|                 flow.locked = false; | ||||
|                 flowsToLock.add(flow) | ||||
|             } | ||||
|         } | ||||
|         imported.nodes.forEach(function(n) { | ||||
|             if (nodeChangedStates[n.id] || localChangedStates[n.id]) { | ||||
|             if (nodeChangedStates[n.id]) { | ||||
|                 ensureUnlocked(n.z) | ||||
|                 n.changed = true; | ||||
|             } | ||||
|             if (nodeMovedStates[n.id]) { | ||||
|                 ensureUnlocked(n.z) | ||||
|                 n.moved = true; | ||||
|             } | ||||
|         }) | ||||
|         flowsToLock.forEach(flow => { | ||||
|             flow.locked = true | ||||
|         }) | ||||
|  | ||||
|         RED.nodes.version(diff.remoteDiff.rev); | ||||
| @@ -1929,15 +2029,14 @@ RED.diff = (function() { | ||||
|                             if (!isSeparator) { | ||||
|                                 var isOurs = /^..<<<<<<</.test(lineText); | ||||
|                                 if (isOurs) { | ||||
|                                     $('<span>').text("<<<<<<< Local Changes").appendTo(line); | ||||
|                                     $('<span>').text("<<<<<<< " + RED._("diff.localChanges")).appendTo(line); | ||||
|                                     hunk.localChangeStart = actualLineNumber; | ||||
|                                 } else { | ||||
|                                     hunk.remoteChangeEnd = actualLineNumber; | ||||
|                                     $('<span>').text(">>>>>>> Remote Changes").appendTo(line); | ||||
|  | ||||
|                                     $('<span>').text(">>>>>>> " + RED._("diff.remoteChanges")).appendTo(line); | ||||
|                                 } | ||||
|                                 diffRow.addClass("mergeHeader-"+(isOurs?"ours":"theirs")); | ||||
|                                 $('<button class="red-ui-button red-ui-button-small" style="float: right; margin-right: 20px;"><i class="fa fa-angle-double-'+(isOurs?"down":"up")+'"></i> use '+(isOurs?"local":"remote")+' changes</button>') | ||||
|                                 $('<button class="red-ui-button red-ui-button-small" style="float: right; margin-right: 20px;"><i class="fa fa-angle-double-'+(isOurs?"down":"up")+'"></i> '+RED._(isOurs?"diff.useLocalChanges":"diff.useRemoteChanges")+'</button>') | ||||
|                                     .appendTo(line) | ||||
|                                     .on("click", function(evt) { | ||||
|                                         evt.preventDefault(); | ||||
| @@ -2019,7 +2118,7 @@ RED.diff = (function() { | ||||
|                 $("<h3>").text(commit.title).appendTo(content); | ||||
|                 $('<div class="commit-body"></div>').text(commit.comment).appendTo(content); | ||||
|                 var summary = $('<div class="commit-summary"></div>').appendTo(content); | ||||
|                 $('<div style="float: right">').text("Commit "+commit.sha).appendTo(summary); | ||||
|                 $('<div style="float: right">').text(RED._('diff.commit')+" "+commit.sha).appendTo(summary); | ||||
|                 $('<div>').text((commit.authorName||commit.author)+" - "+options.date).appendTo(summary); | ||||
|  | ||||
|                 if (commit.files) { | ||||
|   | ||||
| @@ -157,6 +157,12 @@ RED.editor = (function() { | ||||
|             } | ||||
|         } | ||||
|         if (valid && "validate" in definition[property]) { | ||||
|             if (definition[property].hasOwnProperty("required") && | ||||
|                 definition[property].required === false) { | ||||
|                 if (value === "") { | ||||
|                     return true; | ||||
|                 } | ||||
|             } | ||||
|             try { | ||||
|                 var opt = {}; | ||||
|                 if (label) { | ||||
| @@ -183,6 +189,11 @@ RED.editor = (function() { | ||||
|                 }); | ||||
|             } | ||||
|         } else if (valid) { | ||||
|             if (definition[property].hasOwnProperty("required") && definition[property].required === false) { | ||||
|                 if (value === "") { | ||||
|                     return true; | ||||
|                 } | ||||
|             } | ||||
|             // If the validator is not provided in node property => Check if the input has a validator | ||||
|             if ("category" in node._def) { | ||||
|                 const isConfig = node._def.category === "config"; | ||||
| @@ -190,7 +201,10 @@ RED.editor = (function() { | ||||
|                 const input = $("#"+prefix+"-"+property); | ||||
|                 const isTypedInput = input.length > 0 && input.next(".red-ui-typedInput-container").length > 0; | ||||
|                 if (isTypedInput) { | ||||
|                     valid = input.typedInput("validate"); | ||||
|                     valid = input.typedInput("validate", { returnErrorMessage: true }); | ||||
|                     if (typeof valid === "string") { | ||||
|                         return label ? label + ": " + valid : valid; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| @@ -248,6 +262,8 @@ RED.editor = (function() { | ||||
|             var value = input.val(); | ||||
|             if (defaults[property].hasOwnProperty("format") && defaults[property].format !== "" && input[0].nodeName === "DIV") { | ||||
|                 value = input.text(); | ||||
|             } else if (input.attr("type") === "checkbox") { | ||||
|                 value = input.prop("checked"); | ||||
|             } | ||||
|             var valid = validateNodeProperty(node, defaults, property,value); | ||||
|             if (((typeof valid) === "string") || !valid) { | ||||
| @@ -326,49 +342,101 @@ RED.editor = (function() { | ||||
|  | ||||
|     /** | ||||
|      * Create a config-node select box for this property | ||||
|      * @param node - the node being edited | ||||
|      * @param property - the name of the field | ||||
|      * @param type - the type of the config-node | ||||
|      * @param  {Object} node - the node being edited | ||||
|      * @param {String} property - the name of the node property | ||||
|      * @param {String} type - the type of the config-node | ||||
|      * @param {"node-config-input"|"node-input"|"node-input-subflow-env"} prefix - the prefix to use in the input element ids | ||||
|      * @param {Function} [filter] - a function to filter the list of config nodes | ||||
|      * @param {Object} [env] - the environment variable object (only used for subflow env vars) | ||||
|      */ | ||||
|     function prepareConfigNodeSelect(node,property,type,prefix,filter) { | ||||
|         var input = $("#"+prefix+"-"+property); | ||||
|         if (input.length === 0 ) { | ||||
|     function prepareConfigNodeSelect(node, property, type, prefix, filter, env) { | ||||
|         let nodeValue | ||||
|         if (prefix === 'node-input-subflow-env') { | ||||
|             nodeValue = env?.value | ||||
|         } else { | ||||
|             nodeValue = node[property] | ||||
|         } | ||||
|  | ||||
|         const addBtnId = `${prefix}-btn-${property}-add`; | ||||
|         const editBtnId = `${prefix}-btn-${property}-edit`; | ||||
|         const selectId = prefix + '-' + property; | ||||
|         const input = $(`#${selectId}`); | ||||
|         if (input.length === 0) { | ||||
|             return; | ||||
|         } | ||||
|         var newWidth = input.width(); | ||||
|         var attrStyle = input.attr('style'); | ||||
|         var m; | ||||
|         const attrStyle = input.attr('style'); | ||||
|         let newWidth; | ||||
|         let m; | ||||
|         if ((m = /(^|\s|;)width\s*:\s*([^;]+)/i.exec(attrStyle)) !== null) { | ||||
|             newWidth = m[2].trim(); | ||||
|         } else { | ||||
|             newWidth = "70%"; | ||||
|         } | ||||
|         var outerWrap = $("<div></div>").css({ | ||||
|         const outerWrap = $("<div></div>").css({ | ||||
|             width: newWidth, | ||||
|             display:'inline-flex' | ||||
|             display: 'inline-flex' | ||||
|         }); | ||||
|         var select = $('<select id="'+prefix+'-'+property+'"></select>').appendTo(outerWrap); | ||||
|         const select = $('<select id="' + selectId + '"></select>').appendTo(outerWrap); | ||||
|         input.replaceWith(outerWrap); | ||||
|         // set the style attr directly - using width() on FF causes a value of 114%... | ||||
|         select.css({ | ||||
|             'flex-grow': 1 | ||||
|         }); | ||||
|         updateConfigNodeSelect(property,type,node[property],prefix,filter); | ||||
|         $('<a id="'+prefix+'-lookup-'+property+'" class="red-ui-button"><i class="fa fa-pencil"></i></a>') | ||||
|             .css({"margin-left":"10px"}) | ||||
|  | ||||
|         updateConfigNodeSelect(property, type, nodeValue, prefix, filter); | ||||
|  | ||||
|         // create the edit button | ||||
|         const editButton = $('<a id="' + editBtnId + '" class="red-ui-button"><i class="fa fa-pencil"></i></a>') | ||||
|             .css({ "margin-left": "10px" }) | ||||
|             .appendTo(outerWrap); | ||||
|         $('#'+prefix+'-lookup-'+property).on("click", function(e) { | ||||
|             showEditConfigNodeDialog(property,type,select.find(":selected").val(),prefix,node); | ||||
|  | ||||
|         RED.popover.tooltip(editButton, RED._('editor.editConfig', { type })); | ||||
|  | ||||
|         // create the add button | ||||
|         const addButton = $('<a id="' + addBtnId + '" class="red-ui-button"><i class="fa fa-plus"></i></a>') | ||||
|             .css({ "margin-left": "10px" }) | ||||
|             .appendTo(outerWrap); | ||||
|         RED.popover.tooltip(addButton, RED._('editor.addNewConfig', { type })); | ||||
|  | ||||
|         const disableButton = function(button, disabled) { | ||||
|             $(button).prop("disabled", !!disabled) | ||||
|             $(button).toggleClass("disabled", !!disabled) | ||||
|         }; | ||||
|  | ||||
|         // add the click handler | ||||
|         addButton.on("click", function (e) { | ||||
|             if (addButton.prop("disabled")) { return } | ||||
|             showEditConfigNodeDialog(property, type, "_ADD_", prefix, node); | ||||
|             e.preventDefault(); | ||||
|         }); | ||||
|         editButton.on("click", function (e) { | ||||
|             const selectedOpt = select.find(":selected") | ||||
|             if (selectedOpt.data('env')) { return } // don't show the dialog for env vars items (MVP. Future enhancement: lookup the env, if present, show the associated edit dialog) | ||||
|             if (editButton.prop("disabled")) { return } | ||||
|             showEditConfigNodeDialog(property, type, selectedOpt.val(), prefix, node); | ||||
|             e.preventDefault(); | ||||
|         }); | ||||
|         var label = ""; | ||||
|         var configNode = RED.nodes.node(node[property]); | ||||
|         var node_def = RED.nodes.getType(type); | ||||
|  | ||||
|         if (configNode) { | ||||
|             label = RED.utils.getNodeLabel(configNode,configNode.id); | ||||
|         } | ||||
|         input.val(label); | ||||
|         // dont permit the user to click the button if the selected option is an env var | ||||
|         select.on("change", function () { | ||||
|             const selectedOpt = select.find(":selected"); | ||||
|             const optionsLength = select.find("option").length; | ||||
|             if (selectedOpt?.data('env')) { | ||||
|                 disableButton(addButton, true); | ||||
|                 disableButton(editButton, true); | ||||
|             // disable the edit button if no options available or 'none' selected | ||||
|             } else if (optionsLength === 1 || selectedOpt.val() === "_ADD_") { | ||||
|                 disableButton(addButton, false); | ||||
|                 disableButton(editButton, true); | ||||
|             } else { | ||||
|                 disableButton(addButton, false); | ||||
|                 disableButton(editButton, false); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         // If the value is "", 'add new...' option if no config node available or 'none' option | ||||
|         // Otherwise, it's a config node | ||||
|         select.val(nodeValue || '_ADD_'); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -740,10 +808,31 @@ RED.editor = (function() { | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             const oldCreds = {}; | ||||
|             if (editing_node._def.credentials) { | ||||
|                 for (const prop in editing_node._def.credentials) { | ||||
|                     if (Object.prototype.hasOwnProperty.call(editing_node._def.credentials, prop)) { | ||||
|                         if (editing_node._def.credentials[prop].type === 'password') { | ||||
|                             oldCreds['has_' + prop] = editing_node.credentials['has_' + prop]; | ||||
|                         } | ||||
|                         if (prop in editing_node.credentials) { | ||||
|                             oldCreds[prop] = editing_node.credentials[prop]; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             try { | ||||
|                 var rc = editing_node._def.oneditsave.call(editing_node); | ||||
|                 const rc = editing_node._def.oneditsave.call(editing_node); | ||||
|                 if (rc === true) { | ||||
|                     editState.changed = true; | ||||
|                 } else if (typeof rc === 'object' && rc !== null ) { | ||||
|                     if (rc.changed === true) { | ||||
|                         editState.changed = true | ||||
|                     } | ||||
|                     if (Array.isArray(rc.history) && rc.history.length > 0) { | ||||
|                         editState.history = rc.history | ||||
|                     } | ||||
|                 } | ||||
|             } catch(err) { | ||||
|                 console.warn("oneditsave",editing_node.id,editing_node.type,err.toString()); | ||||
| @@ -764,16 +853,32 @@ RED.editor = (function() { | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (editing_node._def.credentials) { | ||||
|                 for (const prop in editing_node._def.credentials) { | ||||
|                     if (Object.prototype.hasOwnProperty.call(editing_node._def.credentials, prop)) { | ||||
|                         if (oldCreds[prop] !== editing_node.credentials[prop]) { | ||||
|                             if (editing_node.credentials[prop] === '__PWRD__') { | ||||
|                                 // The password may not exist in oldCreds | ||||
|                                 // The value '__PWRD__' means the password exists, | ||||
|                                 // so ignore this change | ||||
|                                 continue; | ||||
|                             } | ||||
|                             editState.changes.credentials = editState.changes.credentials || {}; | ||||
|                             editState.changes.credentials['has_' + prop] = oldCreds['has_' + prop]; | ||||
|                             editState.changes.credentials[prop] = oldCreds[prop]; | ||||
|                             editState.changed = true; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function defaultConfigNodeSort(A,B) { | ||||
|         if (A.__label__ < B.__label__) { | ||||
|             return -1; | ||||
|         } else if (A.__label__ > B.__label__) { | ||||
|             return 1; | ||||
|         } | ||||
|         return 0; | ||||
|         // sort case insensitive so that `[env] node-name` items are at the top and | ||||
|         // not mixed inbetween the the lower and upper case items | ||||
|         return (A.__label__ || '').localeCompare((B.__label__ || ''), undefined, {sensitivity: 'base'}) | ||||
|     } | ||||
|  | ||||
|     function updateConfigNodeSelect(name,type,value,prefix,filter) { | ||||
| @@ -788,7 +893,7 @@ RED.editor = (function() { | ||||
|                 } | ||||
|                 $("#"+prefix+"-"+name).val(value); | ||||
|             } else { | ||||
|  | ||||
|                 let inclSubflowEnvvars = false | ||||
|                 var select = $("#"+prefix+"-"+name); | ||||
|                 var node_def = RED.nodes.getType(type); | ||||
|                 select.children().remove(); | ||||
| @@ -796,6 +901,7 @@ RED.editor = (function() { | ||||
|                 var activeWorkspace = RED.nodes.workspace(RED.workspaces.active()); | ||||
|                 if (!activeWorkspace) { | ||||
|                     activeWorkspace = RED.nodes.subflow(RED.workspaces.active()); | ||||
|                     inclSubflowEnvvars = true | ||||
|                 } | ||||
|  | ||||
|                 var configNodes = []; | ||||
| @@ -811,6 +917,31 @@ RED.editor = (function() { | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
|   | ||||
|                 // as includeSubflowEnvvars is true, this is a subflow. | ||||
|                 // include any 'conf-types' env vars as a list of avaiable configs | ||||
|                 // in the config dropdown as `[env] node-name` | ||||
|                 if (inclSubflowEnvvars && activeWorkspace.env) { | ||||
|                     const parentEnv = activeWorkspace.env.filter(env => env.ui?.type === 'conf-types' && env.type === type) | ||||
|                     if (parentEnv && parentEnv.length > 0) { | ||||
|                         const locale = RED.i18n.lang() | ||||
|                         for (let i = 0; i < parentEnv.length; i++) { | ||||
|                             const tenv = parentEnv[i] | ||||
|                             const ui = tenv.ui || {} | ||||
|                             const labels = ui.label || {} | ||||
|                             const labelText = RED.editor.envVarList.lookupLabel(labels, labels["en-US"] || tenv.name, locale) | ||||
|                             const config = { | ||||
|                                 env: tenv, | ||||
|                                 id: '${' + tenv.name + '}', | ||||
|                                 type: type, | ||||
|                                 label: labelText, | ||||
|                                 __label__: `[env] ${labelText}` | ||||
|                             } | ||||
|                             configNodes.push(config) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 var configSortFn = defaultConfigNodeSort; | ||||
|                 if (typeof node_def.sort == "function") { | ||||
|                     configSortFn = node_def.sort; | ||||
| @@ -822,7 +953,10 @@ RED.editor = (function() { | ||||
|                 } | ||||
|  | ||||
|                 configNodes.forEach(function(cn) { | ||||
|                     $('<option value="'+cn.id+'"'+(value==cn.id?" selected":"")+'></option>').text(RED.text.bidi.enforceTextDirectionWithUCC(cn.__label__)).appendTo(select); | ||||
|                     const option = $('<option value="'+cn.id+'"'+(value==cn.id?" selected":"")+'></option>').text(RED.text.bidi.enforceTextDirectionWithUCC(cn.__label__)).appendTo(select); | ||||
|                     if (cn.env) { | ||||
|                         option.data('env', cn.env) // set a data attribute to indicate this is an env var (to inhibit the edit button) | ||||
|                     } | ||||
|                     delete cn.__label__; | ||||
|                 }); | ||||
|  | ||||
| @@ -835,7 +969,14 @@ RED.editor = (function() { | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 select.append('<option value="_ADD_"'+(value===""?" selected":"")+'>'+RED._("editor.addNewType", {type:label})+'</option>'); | ||||
|                 if (!configNodes.length) { | ||||
|                     // Add 'add new...' option | ||||
|                     select.append('<option value="_ADD_" selected>' + RED._("editor.addNewType", { type: label }) + '</option>'); | ||||
|                 } else { | ||||
|                     // Add 'none' option | ||||
|                     select.append('<option value="_ADD_">' + RED._("editor.inputs.none") + '</option>'); | ||||
|                 } | ||||
|  | ||||
|                 window.setTimeout(function() { select.trigger("change");},50); | ||||
|             } | ||||
|         } | ||||
| @@ -914,6 +1055,17 @@ RED.editor = (function() { | ||||
|                             dirty: startDirty | ||||
|                         } | ||||
|  | ||||
|                         if (editing_node.g) { | ||||
|                             const group = RED.nodes.group(editing_node.g); | ||||
|                             // Don't use RED.group.removeFromGroup as that emits | ||||
|                             // a change event on the node - but we're deleting it | ||||
|                             const index = group?.nodes.indexOf(editing_node) ?? -1; | ||||
|                             if (index > -1) { | ||||
|                                 group.nodes.splice(index, 1); | ||||
|                                 RED.group.markDirty(group); | ||||
|                             } | ||||
|                         } | ||||
|  | ||||
|                         RED.nodes.dirty(true); | ||||
|                         RED.view.redraw(true); | ||||
|                         RED.history.push(historyEvent); | ||||
| @@ -1015,7 +1167,7 @@ RED.editor = (function() { | ||||
|                                     } | ||||
|                                 }); | ||||
|                             } | ||||
|                             var historyEvent = { | ||||
|                             let historyEvent = { | ||||
|                                 t:'edit', | ||||
|                                 node:editing_node, | ||||
|                                 changes:editState.changes, | ||||
| @@ -1031,6 +1183,15 @@ RED.editor = (function() { | ||||
|                                     instances:subflowInstances | ||||
|                                 } | ||||
|                             } | ||||
|  | ||||
|                             if (editState.history) { | ||||
|                                 historyEvent = { | ||||
|                                     t: 'multi', | ||||
|                                     events: [ historyEvent, ...editState.history ], | ||||
|                                     dirty: wasDirty | ||||
|                                 } | ||||
|                             } | ||||
|  | ||||
|                             RED.history.push(historyEvent); | ||||
|                         } | ||||
|                         editing_node.dirty = true; | ||||
| @@ -1353,139 +1514,193 @@ RED.editor = (function() { | ||||
|             }, | ||||
|             { | ||||
|                 id: "node-config-dialog-ok", | ||||
|                 text: adding?RED._("editor.configAdd"):RED._("editor.configUpdate"), | ||||
|                 text: adding ? RED._("editor.configAdd") : RED._("editor.configUpdate"), | ||||
|                 class: "primary", | ||||
|                 click: function() { | ||||
|                     var editState = { | ||||
|                     // TODO: Already defined | ||||
|                     const configProperty = name; | ||||
|                     const configType = type; | ||||
|                     const configTypeDef = RED.nodes.getType(configType); | ||||
|  | ||||
|                     const wasChanged = editing_config_node.changed; | ||||
|                     const editState = { | ||||
|                         changes: {}, | ||||
|                         changed: false, | ||||
|                         outputMap: null | ||||
|                     }; | ||||
|                     var configProperty = name; | ||||
|                     var configId = editing_config_node.id; | ||||
|                     var configType = type; | ||||
|                     var configAdding = adding; | ||||
|                     var configTypeDef = RED.nodes.getType(configType); | ||||
|                     var d; | ||||
|                     var input; | ||||
|                      | ||||
|                     // Call `oneditsave` and search for changes | ||||
|                     handleEditSave(editing_config_node, editState); | ||||
|  | ||||
|                     if (configTypeDef.oneditsave) { | ||||
|                         try { | ||||
|                             configTypeDef.oneditsave.call(editing_config_node); | ||||
|                         } catch(err) { | ||||
|                             console.warn("oneditsave",editing_config_node.id,editing_config_node.type,err.toString()); | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     for (d in configTypeDef.defaults) { | ||||
|                         if (configTypeDef.defaults.hasOwnProperty(d)) { | ||||
|                             var newValue; | ||||
|                             input = $("#node-config-input-"+d); | ||||
|                             if (input.attr('type') === "checkbox") { | ||||
|                                 newValue = input.prop('checked'); | ||||
|                             } else if ("format" in configTypeDef.defaults[d] && configTypeDef.defaults[d].format !== "" && input[0].nodeName === "DIV") { | ||||
|                                 newValue = input.text(); | ||||
|                             } else { | ||||
|                                 newValue = input.val(); | ||||
|                             } | ||||
|                             if (newValue != null && newValue !== editing_config_node[d]) { | ||||
|                                 if (editing_config_node._def.defaults[d].type) { | ||||
|                                     if (newValue == "_ADD_") { | ||||
|                                         newValue = ""; | ||||
|                                     } | ||||
|                                     // Change to a related config node | ||||
|                                     var configNode = RED.nodes.node(editing_config_node[d]); | ||||
|                                     if (configNode) { | ||||
|                                         var users = configNode.users; | ||||
|                                         users.splice(users.indexOf(editing_config_node),1); | ||||
|                                         RED.events.emit("nodes:change",configNode); | ||||
|                                     } | ||||
|                                     configNode = RED.nodes.node(newValue); | ||||
|                                     if (configNode) { | ||||
|                                         configNode.users.push(editing_config_node); | ||||
|                                         RED.events.emit("nodes:change",configNode); | ||||
|                                     } | ||||
|                                 } | ||||
|                                 editing_config_node[d] = newValue; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     activeEditPanes.forEach(function(pane) { | ||||
|                     // Search for changes in the edit box (panes) | ||||
|                     activeEditPanes.forEach(function (pane) { | ||||
|                         if (pane.apply) { | ||||
|                             pane.apply.call(pane, editState); | ||||
|                         } | ||||
|                     }) | ||||
|                     }); | ||||
|  | ||||
|                     editing_config_node.label = configTypeDef.label; | ||||
|  | ||||
|                     var scope = $("#red-ui-editor-config-scope").val(); | ||||
|                     editing_config_node.z = scope; | ||||
|                     // TODO: Why? | ||||
|                     editing_config_node.label = configTypeDef.label | ||||
|  | ||||
|                     // Check if disabled has changed | ||||
|                     if ($("#node-config-input-node-disabled").prop('checked')) { | ||||
|                         if (editing_config_node.d !== true) { | ||||
|                             editState.changes.d = editing_config_node.d; | ||||
|                             editState.changed = true; | ||||
|                             editing_config_node.d = true; | ||||
|                         } | ||||
|                     } else { | ||||
|                         if (editing_config_node.d === true) { | ||||
|                             editState.changes.d = editing_config_node.d; | ||||
|                             editState.changed = true; | ||||
|                             delete editing_config_node.d; | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     // NOTE: must be undefined if no scope used | ||||
|                     const scope = $("#red-ui-editor-config-scope").val() || undefined; | ||||
|  | ||||
|                     // Check if the scope has changed | ||||
|                     if (editing_config_node.z !== scope) { | ||||
|                         editState.changes.z = editing_config_node.z; | ||||
|                         editState.changed = true; | ||||
|                         editing_config_node.z = scope; | ||||
|                     } | ||||
|  | ||||
|                     // Search for nodes that use this config node that are no longer | ||||
|                     // in scope, so must be removed | ||||
|                     const historyEvents = []; | ||||
|                     if (scope) { | ||||
|                         // Search for nodes that use this one that are no longer | ||||
|                         // in scope, so must be removed | ||||
|                         editing_config_node.users = editing_config_node.users.filter(function(n) { | ||||
|                             var keep = true; | ||||
|                             for (var d in n._def.defaults) { | ||||
|                                 if (n._def.defaults.hasOwnProperty(d)) { | ||||
|                                     if (n._def.defaults[d].type === editing_config_node.type && | ||||
|                                         n[d] === editing_config_node.id && | ||||
|                                         n.z !== scope) { | ||||
|                                             keep = false; | ||||
|                                             // Remove the reference to this node | ||||
|                                             // and revalidate | ||||
|                                             n[d] = null; | ||||
|                                             n.dirty = true; | ||||
|                                             n.changed = true; | ||||
|                                             validateNode(n); | ||||
|                         const newUsers = editing_config_node.users.filter(function (node) { | ||||
|                             let keepNode = false; | ||||
|                             let nodeModified = null; | ||||
|  | ||||
|                             for (const d in node._def.defaults) { | ||||
|                                 if (node._def.defaults.hasOwnProperty(d)) { | ||||
|                                     if (node._def.defaults[d].type === editing_config_node.type) { | ||||
|                                         if (node[d] === editing_config_node.id) { | ||||
|                                             if (node.z === editing_config_node.z) { | ||||
|                                                 // The node is kept only if at least one property uses | ||||
|                                                 // this config node in the correct scope. | ||||
|                                                 keepNode = true; | ||||
|                                             } else { | ||||
|                                                 if (!nodeModified) { | ||||
|                                                     nodeModified = { | ||||
|                                                         t: "edit", | ||||
|                                                         node: node, | ||||
|                                                         changes: { [d]: node[d] }, | ||||
|                                                         changed: node.changed, | ||||
|                                                         dirty: node.dirty | ||||
|                                                     }; | ||||
|                                                 } else { | ||||
|                                                     nodeModified.changes[d] = node[d]; | ||||
|                                                 } | ||||
|  | ||||
|                                                 // Remove the reference to the config node | ||||
|                                                 node[d] = ""; | ||||
|                                             } | ||||
|                                         } | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|                             return keep; | ||||
|                         }); | ||||
|                     } | ||||
|  | ||||
|                     if (configAdding) { | ||||
|                         RED.nodes.add(editing_config_node); | ||||
|                     } | ||||
|  | ||||
|                     validateNode(editing_config_node); | ||||
|                     var validatedNodes = {}; | ||||
|                     validatedNodes[editing_config_node.id] = true; | ||||
|  | ||||
|                     var userStack = editing_config_node.users.slice(); | ||||
|                     while(userStack.length > 0) { | ||||
|                         var user = userStack.pop(); | ||||
|                         if (!validatedNodes[user.id]) { | ||||
|                             validatedNodes[user.id] = true; | ||||
|                             if (user.users) { | ||||
|                                 userStack = userStack.concat(user.users); | ||||
|                             // Add the node modified to the history | ||||
|                             if (nodeModified) { | ||||
|                                 historyEvents.push(nodeModified); | ||||
|                             } | ||||
|                             validateNode(user); | ||||
|  | ||||
|                             // Mark as changed and revalidate this node | ||||
|                             if (!keepNode) { | ||||
|                                 node.changed = true; | ||||
|                                 node.dirty = true; | ||||
|                                 validateNode(node); | ||||
|                                 RED.events.emit("nodes:change", node); | ||||
|                             } | ||||
|  | ||||
|                             return keepNode; | ||||
|                         }); | ||||
|  | ||||
|                         // Check if users are changed | ||||
|                         if (editing_config_node.users.length !== newUsers.length) { | ||||
|                             editState.changes.users = editing_config_node.users; | ||||
|                             editState.changed = true; | ||||
|                             editing_config_node.users = newUsers; | ||||
|                         } | ||||
|                     } | ||||
|                     RED.nodes.dirty(true); | ||||
|                     RED.view.redraw(true); | ||||
|                     if (!configAdding) { | ||||
|                         RED.events.emit("editor:save",editing_config_node); | ||||
|                         RED.events.emit("nodes:change",editing_config_node); | ||||
|  | ||||
|                     if (editState.changed) { | ||||
|                         // Set the congig node as changed | ||||
|                         editing_config_node.changed = true; | ||||
|                     } | ||||
|  | ||||
|                     // Now, validate the config node | ||||
|                     validateNode(editing_config_node); | ||||
|  | ||||
|                     // And validate nodes using this config node too | ||||
|                     const validatedNodes = new Set(); | ||||
|                     const userStack = editing_config_node.users.slice(); | ||||
|  | ||||
|                     validatedNodes.add(editing_config_node.id); | ||||
|                     while (userStack.length) { | ||||
|                         const node = userStack.pop(); | ||||
|                         if (!validatedNodes.has(node.id)) { | ||||
|                             validatedNodes.add(node.id); | ||||
|                             if (node.users) { | ||||
|                                 userStack.push(...node.users); | ||||
|                             } | ||||
|                             validateNode(node); | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     let historyEvent = { | ||||
|                         t: "edit", | ||||
|                         node: editing_config_node, | ||||
|                         changes: editState.changes, | ||||
|                         changed: wasChanged, | ||||
|                         dirty: RED.nodes.dirty() | ||||
|                     }; | ||||
|  | ||||
|                     if (historyEvents.length) { | ||||
|                         // Need a multi events | ||||
|                         historyEvent = { | ||||
|                             t: "multi", | ||||
|                             events: [historyEvent].concat(historyEvents), | ||||
|                             dirty: historyEvent.dirty | ||||
|                         }; | ||||
|                     } | ||||
|  | ||||
|                     if (!adding) { | ||||
|                         // This event is triggered when the edit box is saved, | ||||
|                         // regardless of whether there are any modifications. | ||||
|                         RED.events.emit("editor:save", editing_config_node); | ||||
|                     } | ||||
|  | ||||
|                     if (editState.changed) { | ||||
|                         if (adding) { | ||||
|                             RED.history.push({ t: "add", nodes: [editing_config_node.id], dirty: RED.nodes.dirty() }); | ||||
|                             // Add the new config node and trigger the `nodes:add` event | ||||
|                             RED.nodes.add(editing_config_node); | ||||
|                         } else { | ||||
|                             RED.history.push(historyEvent); | ||||
|                             RED.events.emit("nodes:change", editing_config_node); | ||||
|                         } | ||||
|  | ||||
|                         RED.nodes.dirty(true); | ||||
|                         RED.view.redraw(true); | ||||
|                     } | ||||
|  | ||||
|                     RED.tray.close(function() { | ||||
|                         var filter = null; | ||||
|                         if (editContext && typeof editContext._def.defaults[configProperty].filter === 'function') { | ||||
|                             filter = function(n) { | ||||
|                                 return editContext._def.defaults[configProperty].filter.call(editContext,n); | ||||
|                         // when editing a config via subflow edit panel, the `configProperty` will not | ||||
|                         // necessarily be a property of the editContext._def.defaults object | ||||
|                         // Also, when editing via dashboard sidebar, editContext can be null | ||||
|                         // so we need to guard both scenarios | ||||
|                         if (editContext?._def) { | ||||
|                             const isSubflow = (editContext._def.type === 'subflow' || /subflow:.*/.test(editContext._def.type)) | ||||
|                             if (editContext && !isSubflow && typeof editContext._def.defaults?.[configProperty]?.filter === 'function') { | ||||
|                                 filter = function(n) { | ||||
|                                     return editContext._def.defaults[configProperty].filter.call(editContext,n); | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                         updateConfigNodeSelect(configProperty,configType,editing_config_node.id,prefix,filter); | ||||
| @@ -1546,7 +1761,7 @@ RED.editor = (function() { | ||||
|                     RED.history.push(historyEvent); | ||||
|                     RED.tray.close(function() { | ||||
|                         var filter = null; | ||||
|                         if (editContext && typeof editContext._def.defaults[configProperty].filter === 'function') { | ||||
|                         if (editContext && typeof editContext._def.defaults[configProperty]?.filter === 'function') { | ||||
|                             filter = function(n) { | ||||
|                                 return editContext._def.defaults[configProperty].filter.call(editContext,n); | ||||
|                             } | ||||
| @@ -1623,8 +1838,8 @@ RED.editor = (function() { | ||||
|                         } | ||||
|  | ||||
|                         if (!isSameObj(old_env, new_env)) { | ||||
|                             editing_node.env = new_env; | ||||
|                             editState.changes.env = editing_node.env; | ||||
|                             editing_node.env = new_env; | ||||
|                             editState.changed = true; | ||||
|                         } | ||||
|  | ||||
| @@ -2087,6 +2302,7 @@ RED.editor = (function() { | ||||
|             } | ||||
|         }, | ||||
|         editBuffer: function(options) { showTypeEditor("_buffer", options) }, | ||||
|         getEditStack: function () { return [...editStack] }, | ||||
|         buildEditForm: buildEditForm, | ||||
|         validateNode: validateNode, | ||||
|         updateNodeProperties: updateNodeProperties, | ||||
| @@ -2131,6 +2347,7 @@ RED.editor = (function() { | ||||
|                 filteredEditPanes[type] = filter | ||||
|             } | ||||
|             editPanes[type] = definition; | ||||
|         } | ||||
|         }, | ||||
|         prepareConfigNodeSelect: prepareConfigNodeSelect, | ||||
|     } | ||||
| })(); | ||||
|   | ||||
| @@ -165,7 +165,13 @@ RED.editor.codeEditor.monaco = (function() { | ||||
|         //Handles orphaned models | ||||
|         //ensure loaded models that are not explicitly destroyed by a call to .destroy() are disposed | ||||
|         RED.events.on("editor:close",function() { | ||||
|             let models = window.monaco ? monaco.editor.getModels() : null; | ||||
|             if (!window.monaco) { return; } | ||||
|             const editors = window.monaco.editor.getEditors() | ||||
|             const orphanEditors = editors.filter(editor => editor && !document.body.contains(editor.getDomNode())) | ||||
|             orphanEditors.forEach(editor => { | ||||
|                 editor.dispose(); | ||||
|             }); | ||||
|             let models = monaco.editor.getModels() | ||||
|             if(models && models.length) { | ||||
|                 console.warn("Cleaning up monaco models left behind. Any node that calls createEditor() should call .destroy().") | ||||
|                 for (let index = 0; index < models.length; index++) { | ||||
| @@ -514,7 +520,7 @@ RED.editor.codeEditor.monaco = (function() { | ||||
|                 _monaco.languages.json.jsonDefaults.setDiagnosticsOptions(diagnosticOptions); | ||||
|                 if(modeConfiguration) { _monaco.languages.json.jsonDefaults.setModeConfiguration(modeConfiguration); } | ||||
|             } catch (error) { | ||||
|                 console.warn("monaco - Error setting up json options", err) | ||||
|                 console.warn("monaco - Error setting up json options", error) | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -526,7 +532,7 @@ RED.editor.codeEditor.monaco = (function() { | ||||
|                 if(htmlDefaults) { _monaco.languages.html.htmlDefaults.setOptions(htmlDefaults); } | ||||
|                 if(handlebarDefaults) { _monaco.languages.html.handlebarDefaults.setOptions(handlebarDefaults); } | ||||
|             } catch (error) { | ||||
|                 console.warn("monaco - Error setting up html options", err) | ||||
|                 console.warn("monaco - Error setting up html options", error) | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -546,7 +552,7 @@ RED.editor.codeEditor.monaco = (function() { | ||||
|                 if(lessDefaults_modeConfiguration) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(lessDefaults_modeConfiguration); } | ||||
|                 if(scssDefaults_modeConfiguration) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(scssDefaults_modeConfiguration); } | ||||
|             } catch (error) { | ||||
|                 console.warn("monaco - Error setting up CSS/SCSS/LESS options", err) | ||||
|                 console.warn("monaco - Error setting up CSS/SCSS/LESS options", error) | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -585,7 +591,7 @@ RED.editor.codeEditor.monaco = (function() { | ||||
|                     createMonacoCompletionItem("set (flow context)", 'flow.set("${1:name}", ${1:value});','Set a value in flow context',range), | ||||
|                     createMonacoCompletionItem("get (global context)", 'global.get("${1:name}");','Get a value from global context',range), | ||||
|                     createMonacoCompletionItem("set (global context)", 'global.set("${1:name}", ${1:value});','Set a value in global context',range), | ||||
|                     createMonacoCompletionItem("get (env)", 'env.get("${1|NR_NODE_ID,NR_NODE_NAME,NR_NODE_PATH,NR_GROUP_ID,NR_GROUP_NAME,NR_FLOW_ID,NR_FLOW_NAME|}");','Get env variable value',range), | ||||
|                     createMonacoCompletionItem("get (env)", 'env.get("${1|NR_NODE_ID,NR_NODE_NAME,NR_NODE_PATH,NR_GROUP_ID,NR_GROUP_NAME,NR_FLOW_ID,NR_FLOW_NAME,NR_SUBFLOW_NAME,NR_SUBFLOW_ID,NR_SUBFLOW_PATH|}");','Get env variable value',range), | ||||
|                     createMonacoCompletionItem("cloneMessage (RED.util)", 'RED.util.cloneMessage(${1:msg});', | ||||
|                         ["```typescript", | ||||
|                         "RED.util.cloneMessage<T extends registry.NodeMessage>(msg: T): T", | ||||
| @@ -1124,6 +1130,7 @@ RED.editor.codeEditor.monaco = (function() { | ||||
|  | ||||
|             $(el).remove(); | ||||
|             $(toolbarRow).remove(); | ||||
|             ed.dispose(); | ||||
|         } | ||||
|  | ||||
|         ed.resize = function resize() { | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| RED.editor.envVarList = (function() { | ||||
|  | ||||
|     var currentLocale = 'en-US'; | ||||
|     var DEFAULT_ENV_TYPE_LIST = ['str','num','bool','json','bin','env']; | ||||
|     var DEFAULT_ENV_TYPE_LIST_INC_CRED = ['str','num','bool','json','bin','env','cred','jsonata']; | ||||
|     const DEFAULT_ENV_TYPE_LIST = ['str','num','bool','json','bin','env']; | ||||
|     const DEFAULT_ENV_TYPE_LIST_INC_CONFTYPES = ['str','num','bool','json','bin','env','conf-types']; | ||||
|     const DEFAULT_ENV_TYPE_LIST_INC_CRED = ['str','num','bool','json','bin','env','cred','jsonata']; | ||||
|  | ||||
|     /** | ||||
|      * Create env var edit interface | ||||
| @@ -10,8 +11,8 @@ RED.editor.envVarList = (function() { | ||||
|      * @param node - subflow node | ||||
|      */ | ||||
|     function buildPropertiesList(envContainer, node) { | ||||
|  | ||||
|         var isTemplateNode = (node.type === "subflow"); | ||||
|         if(RED.editor.envVarList.debug) { console.log('envVarList: buildPropertiesList', envContainer, node) } | ||||
|         const isTemplateNode = (node.type === "subflow"); | ||||
|  | ||||
|         envContainer | ||||
|             .css({ | ||||
| @@ -83,7 +84,14 @@ RED.editor.envVarList = (function() { | ||||
|                         // if `opt.ui` does not exist, then apply defaults. If these | ||||
|                         // defaults do not change then they will get stripped off | ||||
|                         // before saving. | ||||
|                         if (opt.type === 'cred') { | ||||
|                         if (opt.type === 'conf-types') { | ||||
|                             opt.ui = opt.ui || { | ||||
|                                 icon: "fa fa-cog", | ||||
|                                 type: "conf-types", | ||||
|                                 opts: {opts:[]} | ||||
|                             } | ||||
|                             opt.ui.type = "conf-types"; | ||||
|                         } else if (opt.type === 'cred') { | ||||
|                             opt.ui = opt.ui || { | ||||
|                                 icon: "", | ||||
|                                 type: "cred" | ||||
| @@ -119,11 +127,11 @@ RED.editor.envVarList = (function() { | ||||
|                             } | ||||
|                         }); | ||||
|  | ||||
|                         buildEnvEditRow(uiRow, opt.ui, nameField, valueField); | ||||
|                         buildEnvEditRow(uiRow, opt, nameField, valueField); | ||||
|                         nameField.trigger('change'); | ||||
|                     } | ||||
|                 }, | ||||
|                 sortable: ".red-ui-editableList-item-handle", | ||||
|                 sortable: true, | ||||
|                 removable: false | ||||
|             }); | ||||
|         var parentEnv = {}; | ||||
| @@ -181,21 +189,23 @@ RED.editor.envVarList = (function() { | ||||
|      * @param nameField - name field of env var | ||||
|      * @param valueField - value field of env var | ||||
|      */ | ||||
|      function buildEnvEditRow(container, ui, nameField, valueField) { | ||||
|      function buildEnvEditRow(container, opt, nameField, valueField) { | ||||
|         const ui = opt.ui | ||||
|         if(RED.editor.envVarList.debug) { console.log('envVarList: buildEnvEditRow', container, ui, nameField, valueField) } | ||||
|          container.addClass("red-ui-editor-subflow-env-ui-row") | ||||
|          var topRow = $('<div></div>').appendTo(container); | ||||
|          $('<div></div>').appendTo(topRow); | ||||
|          $('<div>').text(RED._("editor.icon")).appendTo(topRow); | ||||
|          $('<div>').text(RED._("editor.label")).appendTo(topRow); | ||||
|          $('<div>').text(RED._("editor.inputType")).appendTo(topRow); | ||||
|          $('<div class="red-env-ui-input-type-col">').text(RED._("editor.inputType")).appendTo(topRow); | ||||
|  | ||||
|          var row = $('<div></div>').appendTo(container); | ||||
|          $('<div><i class="red-ui-editableList-item-handle fa fa-bars"></i></div>').appendTo(row); | ||||
|          var typeOptions = { | ||||
|              'input': {types:DEFAULT_ENV_TYPE_LIST}, | ||||
|              'select': {opts:[]}, | ||||
|              'spinner': {}, | ||||
|              'cred': {} | ||||
|             'input': {types:DEFAULT_ENV_TYPE_LIST_INC_CONFTYPES}, | ||||
|             'select': {opts:[]}, | ||||
|             'spinner': {}, | ||||
|             'cred': {} | ||||
|          }; | ||||
|          if (ui.opts) { | ||||
|              typeOptions[ui.type] = ui.opts; | ||||
| @@ -260,15 +270,16 @@ RED.editor.envVarList = (function() { | ||||
|             labelInput.attr("placeholder",$(this).val()) | ||||
|         }); | ||||
|  | ||||
|         var inputCell = $('<div></div>').appendTo(row); | ||||
|         var inputCellInput = $('<input type="text">').css("width","100%").appendTo(inputCell); | ||||
|         var inputCell = $('<div class="red-env-ui-input-type-col"></div>').appendTo(row); | ||||
|         var uiInputTypeInput = $('<input type="text">').css("width","100%").appendTo(inputCell); | ||||
|         if (ui.type === "input") { | ||||
|             inputCellInput.val(ui.opts.types.join(",")); | ||||
|             uiInputTypeInput.val(ui.opts.types.join(",")); | ||||
|         } | ||||
|         var checkbox; | ||||
|         var selectBox; | ||||
|  | ||||
|         inputCellInput.typedInput({ | ||||
|         // the options presented in the UI section for an "input" type selection | ||||
|         uiInputTypeInput.typedInput({ | ||||
|             types: [ | ||||
|                 { | ||||
|                     value:"input", | ||||
| @@ -429,7 +440,7 @@ RED.editor.envVarList = (function() { | ||||
|                                         } | ||||
|                                     }); | ||||
|                                     ui.opts.opts = vals; | ||||
|                                     inputCellInput.typedInput('value',Date.now()) | ||||
|                                     uiInputTypeInput.typedInput('value',Date.now()) | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
| @@ -496,12 +507,13 @@ RED.editor.envVarList = (function() { | ||||
|                                     } else { | ||||
|                                         delete ui.opts.max; | ||||
|                                     } | ||||
|                                     inputCellInput.typedInput('value',Date.now()) | ||||
|                                     uiInputTypeInput.typedInput('value',Date.now()) | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 }, | ||||
|                 'conf-types', | ||||
|                 { | ||||
|                     value:"none", | ||||
|                     label:RED._("editor.inputs.none"), icon:"fa fa-times",hasValue:false | ||||
| @@ -519,14 +531,20 @@ RED.editor.envVarList = (function() { | ||||
|                 // In the case of 'input' type, the typedInput uses the multiple-option | ||||
|                 // mode. Its value needs to be set to a comma-separately list of the | ||||
|                 // selected options. | ||||
|                 inputCellInput.typedInput('value',ui.opts.types.join(",")) | ||||
|                 uiInputTypeInput.typedInput('value',ui.opts.types.join(",")) | ||||
|             } else if (ui.type === 'conf-types') { | ||||
|                 // In the case of 'conf-types' type, the typedInput will be populated | ||||
|                 // with a list of all config nodes types installed. | ||||
|                 // Restore the value to the last selected type | ||||
|                 uiInputTypeInput.typedInput('value', opt.type) | ||||
|             } else { | ||||
|                 // No other type cares about `value`, but doing this will | ||||
|                 // force a refresh of the label now that `ui.opts` has | ||||
|                 // been updated. | ||||
|                 inputCellInput.typedInput('value',Date.now()) | ||||
|                 uiInputTypeInput.typedInput('value',Date.now()) | ||||
|             } | ||||
|  | ||||
|             if(RED.editor.envVarList.debug) { console.log('envVarList: inputCellInput on:typedinputtypechange. ui.type = ' + ui.type) } | ||||
|             switch (ui.type) { | ||||
|                 case 'input': | ||||
|                     valueField.typedInput('types',ui.opts.types); | ||||
| @@ -544,7 +562,7 @@ RED.editor.envVarList = (function() { | ||||
|                     valueField.typedInput('types',['cred']); | ||||
|                     break; | ||||
|                 default: | ||||
|                     valueField.typedInput('types',DEFAULT_ENV_TYPE_LIST) | ||||
|                     valueField.typedInput('types', DEFAULT_ENV_TYPE_LIST); | ||||
|             } | ||||
|             if (ui.type === 'checkbox') { | ||||
|                 valueField.typedInput('type','bool'); | ||||
| @@ -556,8 +574,46 @@ RED.editor.envVarList = (function() { | ||||
|             } | ||||
|  | ||||
|         }).on("change", function(evt,type) { | ||||
|             if (ui.type === 'input') { | ||||
|                 var types = inputCellInput.typedInput('value'); | ||||
|             const selectedType = $(this).typedInput('type') // the UI typedInput type | ||||
|             if(RED.editor.envVarList.debug) { console.log('envVarList: inputCellInput on:change. selectedType = ' + selectedType) } | ||||
|             if (selectedType === 'conf-types') { | ||||
|                 const selectedConfigType = $(this).typedInput('value') || opt.type | ||||
|                 let activeWorkspace = RED.nodes.workspace(RED.workspaces.active()); | ||||
|                 if (!activeWorkspace) { | ||||
|                     activeWorkspace = RED.nodes.subflow(RED.workspaces.active()); | ||||
|                 } | ||||
|  | ||||
|                 // get a list of all config nodes matching the selectedValue | ||||
|                 const configNodes = []; | ||||
|                 RED.nodes.eachConfig(function(config) { | ||||
|                     if (config.type == selectedConfigType && (!config.z || config.z === activeWorkspace.id)) { | ||||
|                         const modulePath = config._def?.set?.id || '' | ||||
|                         let label = RED.utils.getNodeLabel(config, config.id) || config.id; | ||||
|                         label += config.d ? ' ['+RED._('workspace.disabled')+']' : ''; | ||||
|                         const _config = { | ||||
|                             _type: selectedConfigType, | ||||
|                             value: config.id, | ||||
|                             label: label, | ||||
|                             title: modulePath ? modulePath + ' - ' + label : label, | ||||
|                             enabled: config.d !== true, | ||||
|                             disabled: config.d === true, | ||||
|                         } | ||||
|                         configNodes.push(_config); | ||||
|                     } | ||||
|                 }); | ||||
|                 const tiTypes = { | ||||
|                     value: selectedConfigType, | ||||
|                     label: "config", | ||||
|                     icon: "fa fa-cog", | ||||
|                     options: configNodes, | ||||
|                 } | ||||
|                 valueField.typedInput('types', [tiTypes]); | ||||
|                 valueField.typedInput('type', selectedConfigType); | ||||
|                 valueField.typedInput('value', opt.value); | ||||
|  | ||||
|  | ||||
|             } else if (ui.type === 'input') { | ||||
|                 var types = uiInputTypeInput.typedInput('value'); | ||||
|                 ui.opts.types = (types === "") ? ["str"] : types.split(","); | ||||
|                 valueField.typedInput('types',ui.opts.types); | ||||
|             } | ||||
| @@ -569,7 +625,7 @@ RED.editor.envVarList = (function() { | ||||
|         }) | ||||
|         // Set the input to the right type. This will trigger the 'typedinputtypechange' | ||||
|         // event handler (just above ^^) to update the value if needed | ||||
|         inputCellInput.typedInput('type',ui.type) | ||||
|         uiInputTypeInput.typedInput('type',ui.type) | ||||
|     } | ||||
|  | ||||
|     function setLocale(l, list) { | ||||
|   | ||||
| @@ -11,9 +11,22 @@ RED.editor.mermaid = (function () { | ||||
|              | ||||
|             if (!initializing) { | ||||
|                 initializing = true | ||||
|                 $.getScript( | ||||
|                     'vendor/mermaid/mermaid.min.js', | ||||
|                     function (data, stat, jqxhr) { | ||||
|                 // Find the cache-buster: | ||||
|                 let cacheBuster | ||||
|                 $('script').each(function (i, el) {  | ||||
|                     if (!cacheBuster) { | ||||
|                         const src = el.getAttribute('src') | ||||
|                         const m = /\?v=(.+)$/.exec(src) | ||||
|                         if (m) { | ||||
|                             cacheBuster = m[1] | ||||
|                         } | ||||
|                     } | ||||
|                 }) | ||||
|                 $.ajax({ | ||||
|                     url: `vendor/mermaid/mermaid.min.js?v=${cacheBuster}`, | ||||
|                     dataType: "script", | ||||
|                     cache: true, | ||||
|                     success: function (data, stat, jqxhr) { | ||||
|                         mermaid.initialize({ | ||||
|                             startOnLoad: false, | ||||
|                             theme: RED.settings.get('mermaid', {}).theme | ||||
| @@ -24,7 +37,7 @@ RED.editor.mermaid = (function () { | ||||
|                             render(pending) | ||||
|                         } | ||||
|                     } | ||||
|                 ) | ||||
|                 }); | ||||
|             } | ||||
|         } else { | ||||
|             const nodes = document.querySelectorAll(selector) | ||||
|   | ||||
| @@ -20,10 +20,31 @@ | ||||
|             apply: function(editState) { | ||||
|                 var old_env = node.env; | ||||
|                 var new_env = []; | ||||
|  | ||||
|                 if (/^subflow:/.test(node.type)) { | ||||
|                     // Get the list of environment variables from the node properties | ||||
|                     new_env = RED.subflow.exportSubflowInstanceEnv(node); | ||||
|                 } | ||||
|  | ||||
|                 if (old_env && old_env.length) { | ||||
|                     old_env.forEach(function (prop) { | ||||
|                         if (prop.type === "conf-type" && prop.value) { | ||||
|                             const stillInUse = new_env?.some((p) => p.type === "conf-type" && p.name === prop.name && p.value === prop.value); | ||||
|                             if (!stillInUse) { | ||||
|                                 // Remove the node from the config node users | ||||
|                                 // Only for empty value or modified | ||||
|                                 const configNode = RED.nodes.node(prop.value); | ||||
|                                 if (configNode) { | ||||
|                                     if (configNode.users.indexOf(node) !== -1) { | ||||
|                                         configNode.users.splice(configNode.users.indexOf(node), 1); | ||||
|                                         RED.events.emit('nodes:change', configNode) | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|                 // Get the values from the Properties table tab | ||||
|                 var items = this.list.editableList('items'); | ||||
|                 items.each(function (i,el) { | ||||
| @@ -41,7 +62,6 @@ | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|  | ||||
|                 if (new_env && new_env.length > 0) { | ||||
|                     new_env.forEach(function(prop) { | ||||
|                         if (prop.type === "cred") { | ||||
| @@ -52,6 +72,15 @@ | ||||
|                                 editState.changed = true; | ||||
|                             } | ||||
|                             delete prop.value; | ||||
|                         } else if (prop.type === "conf-type" && prop.value) {   | ||||
|                             const configNode = RED.nodes.node(prop.value); | ||||
|                             if (configNode) { | ||||
|                                 if (configNode.users.indexOf(node) === -1) { | ||||
|                                     // Add the node to the config node users | ||||
|                                     configNode.users.push(node); | ||||
|                                     RED.events.emit('nodes:change', configNode); | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|   | ||||
| @@ -44,6 +44,7 @@ | ||||
|             apply: function(editState) { | ||||
|                 var newValue; | ||||
|                 var d; | ||||
|                 // If the node is a subflow, the node's properties (exepts name) are saved by `envProperties` | ||||
|                 if (node._def.defaults) { | ||||
|                     for (d in node._def.defaults) { | ||||
|                         if (node._def.defaults.hasOwnProperty(d)) { | ||||
| @@ -131,9 +132,16 @@ | ||||
|                     } | ||||
|                 } | ||||
|                 if (node._def.credentials) { | ||||
|                     var credDefinition = node._def.credentials; | ||||
|                     var credsChanged = updateNodeCredentials(node,credDefinition,this.inputClass); | ||||
|                     editState.changed = editState.changed || credsChanged; | ||||
|                     const credDefinition = node._def.credentials; | ||||
|                     const credChanges = updateNodeCredentials(node, credDefinition, this.inputClass); | ||||
|  | ||||
|                     if (Object.keys(credChanges).length) { | ||||
|                         editState.changed = true; | ||||
|                         editState.changes.credentials = { | ||||
|                             ...(editState.changes.credentials || {}), | ||||
|                             ...credChanges | ||||
|                         }; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| @@ -161,10 +169,11 @@ | ||||
|      * @param node - the node containing the credentials | ||||
|      * @param credDefinition - definition of the credentials | ||||
|      * @param prefix - prefix of the input fields | ||||
|      * @return {boolean} whether anything has changed | ||||
|      * @return {object} an object containing the modified properties | ||||
|      */ | ||||
|     function updateNodeCredentials(node, credDefinition, prefix) { | ||||
|         var changed = false; | ||||
|         const changes = {}; | ||||
|  | ||||
|         if (!node.credentials) { | ||||
|             node.credentials = {_:{}}; | ||||
|         } else if (!node.credentials._) { | ||||
| @@ -177,22 +186,33 @@ | ||||
|                 if (input.length > 0) { | ||||
|                     var value = input.val(); | ||||
|                     if (credDefinition[cred].type == 'password') { | ||||
|                         node.credentials['has_' + cred] = (value !== ""); | ||||
|                         if (value == '__PWRD__') { | ||||
|                             continue; | ||||
|                         if (value === '__PWRD__') { | ||||
|                             // A cred value exists - no changes | ||||
|                         } else if (value === '' && node.credentials['has_' + cred] === false) { | ||||
|                             // Empty cred value exists - no changes | ||||
|                         } else if (value === node.credentials[cred]) { | ||||
|                             // A cred value exists locally in the editor - no changes | ||||
|                             // Like the user sets a value, saves the config, | ||||
|                             // reopens the config and save the config again | ||||
|                         } else { | ||||
|                             changes['has_' + cred] = node.credentials['has_' + cred]; | ||||
|                             changes[cred] = node.credentials[cred]; | ||||
|                             node.credentials[cred] = value; | ||||
|                         } | ||||
|                         changed = true; | ||||
|  | ||||
|                     } | ||||
|                     node.credentials[cred] = value; | ||||
|                     if (value != node.credentials._[cred]) { | ||||
|                         changed = true; | ||||
|                         node.credentials['has_' + cred] = (value !== ''); | ||||
|                     } else { | ||||
|                         // Since these creds are loaded by the editor, | ||||
|                         // values can be directly compared | ||||
|                         if (value !== node.credentials[cred]) { | ||||
|                             changes[cred] = node.credentials[cred]; | ||||
|                             node.credentials[cred] = value; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return changed; | ||||
|  | ||||
|         return changes; | ||||
|     } | ||||
|  | ||||
|  | ||||
| })(); | ||||
|   | ||||
| @@ -153,10 +153,6 @@ RED.envVar = (function() { | ||||
|     } | ||||
|  | ||||
|     function init(done) { | ||||
|         if (!RED.user.hasPermission("settings.write")) { | ||||
|             RED.notify(RED._("user.errors.settings"),"error"); | ||||
|             return; | ||||
|         } | ||||
|         RED.userSettings.add({ | ||||
|             id:'envvar', | ||||
|             title: RED._("env-var.environment"), | ||||
|   | ||||
| @@ -245,10 +245,15 @@ RED.library = (function() { | ||||
|                         if (lib.types && lib.types.indexOf(options.url) === -1) { | ||||
|                             return; | ||||
|                         } | ||||
|                         let icon = 'fa fa-hdd-o'; | ||||
|                         if (lib.icon) { | ||||
|                             const fullIcon = RED.utils.separateIconPath(lib.icon); | ||||
|                             icon = (fullIcon.module==="font-awesome"?"fa ":"")+fullIcon.file; | ||||
|                         } | ||||
|                         listing.push({ | ||||
|                             library: lib.id, | ||||
|                             type: options.url, | ||||
|                             icon: lib.icon || 'fa fa-hdd-o', | ||||
|                             icon, | ||||
|                             label: RED._(lib.label||lib.id), | ||||
|                             path: "", | ||||
|                             expanded: true, | ||||
| @@ -303,10 +308,15 @@ RED.library = (function() { | ||||
|                         if (lib.types && lib.types.indexOf(options.url) === -1) { | ||||
|                             return; | ||||
|                         } | ||||
|                         let icon = 'fa fa-hdd-o'; | ||||
|                         if (lib.icon) { | ||||
|                             const fullIcon = RED.utils.separateIconPath(lib.icon); | ||||
|                             icon = (fullIcon.module==="font-awesome"?"fa ":"")+fullIcon.file; | ||||
|                         } | ||||
|                         listing.push({ | ||||
|                             library: lib.id, | ||||
|                             type: options.url, | ||||
|                             icon: lib.icon || 'fa fa-hdd-o', | ||||
|                             icon, | ||||
|                             label: RED._(lib.label||lib.id), | ||||
|                             path: "", | ||||
|                             expanded: true, | ||||
| @@ -839,10 +849,10 @@ RED.library = (function() { | ||||
|                     if (file && file.label && !file.children) { | ||||
|                         $.get("library/"+file.library+"/"+file.type+"/"+file.path, function(data) { | ||||
|                             //TODO: nls + sanitize | ||||
|                             var propRow = $('<tr class="red-ui-help-info-row"><td>Type</td><td></td></tr>').appendTo(table); | ||||
|                             var propRow = $('<tr class="red-ui-help-info-row"><td>'+RED._("library.type")+'</td><td></td></tr>').appendTo(table); | ||||
|                             $(propRow.children()[1]).text(activeLibrary.type); | ||||
|                             if (file.props.hasOwnProperty('name')) { | ||||
|                                 propRow = $('<tr class="red-ui-help-info-row"><td>Name</td><td>'+file.props.name+'</td></tr>').appendTo(table); | ||||
|                                 propRow = $('<tr class="red-ui-help-info-row"><td>'+RED._("library.name")+'</td><td>'+file.props.name+'</td></tr>').appendTo(table); | ||||
|                                 $(propRow.children()[1]).text(file.props.name); | ||||
|                             } | ||||
|                             for (var p in file.props) { | ||||
|   | ||||
| @@ -221,12 +221,12 @@ RED.notifications = (function() { | ||||
|                     if (newType) { | ||||
|                         n.className = "red-ui-notification red-ui-notification-"+newType; | ||||
|                     } | ||||
|  | ||||
|                     newTimeout = newOptions.hasOwnProperty('timeout')?newOptions.timeout:timeout | ||||
|                     if (!fixed || newOptions.fixed === false) { | ||||
|                         newTimeout = (newOptions.hasOwnProperty('timeout')?newOptions.timeout:timeout)||5000; | ||||
|                         newTimeout = newTimeout || 5000 | ||||
|                     } | ||||
|                     if (newOptions.buttons) { | ||||
|                         var buttonSet = $('<div style="margin-top: 20px;" class="ui-dialog-buttonset"></div>').appendTo(nn) | ||||
|                         var buttonSet = $('<div class="ui-dialog-buttonset"></div>').appendTo(nn) | ||||
|                         newOptions.buttons.forEach(function(buttonDef) { | ||||
|                             var b = $('<button>').text(buttonDef.text).on("click", buttonDef.click).appendTo(buttonSet); | ||||
|                             if (buttonDef.id) { | ||||
| @@ -272,6 +272,15 @@ RED.notifications = (function() { | ||||
|                 }; | ||||
|             })()); | ||||
|             n.timeoutid = window.setTimeout(n.close,timeout||5000); | ||||
|         } else if (timeout) { | ||||
|             $(n).on("click.red-ui-notification-close", (function() { | ||||
|                 var nn = n; | ||||
|                 return function() { | ||||
|                     nn.hideNotification(); | ||||
|                     window.clearTimeout(nn.timeoutid); | ||||
|                 }; | ||||
|             })()); | ||||
|             n.timeoutid = window.setTimeout(n.hideNotification,timeout||5000); | ||||
|         } | ||||
|         currentNotifications.push(n); | ||||
|         if (options.id) { | ||||
|   | ||||
| @@ -133,7 +133,7 @@ RED.palette.editor = (function() { | ||||
|         }).done(function(data,textStatus,xhr) { | ||||
|             callback(); | ||||
|         }).fail(function(xhr,textStatus,err) { | ||||
|             callback(xhr); | ||||
|             callback(xhr,textStatus,err); | ||||
|         }); | ||||
|     } | ||||
|     function removeNodeModule(id,callback) { | ||||
| @@ -248,86 +248,106 @@ RED.palette.editor = (function() { | ||||
|             var moduleInfo = nodeEntries[module].info; | ||||
|             var nodeEntry = nodeEntries[module].elements; | ||||
|             if (nodeEntry) { | ||||
|                 var activeTypeCount = 0; | ||||
|                 var typeCount = 0; | ||||
|                 var errorCount = 0; | ||||
|                 nodeEntry.errorList.empty(); | ||||
|                 nodeEntries[module].totalUseCount = 0; | ||||
|                 nodeEntries[module].setUseCount = {}; | ||||
|                 if (moduleInfo.plugin) { | ||||
|                     nodeEntry.enableButton.hide(); | ||||
|                     nodeEntry.removeButton.show(); | ||||
|  | ||||
|                 for (var setName in moduleInfo.sets) { | ||||
|                     if (moduleInfo.sets.hasOwnProperty(setName)) { | ||||
|                         var inUseCount = 0; | ||||
|                         var set = moduleInfo.sets[setName]; | ||||
|                         var setElements = nodeEntry.sets[setName]; | ||||
|                         if (set.err) { | ||||
|                             errorCount++; | ||||
|                             var errMessage = set.err; | ||||
|                             if (set.err.message) { | ||||
|                                 errMessage = set.err.message; | ||||
|                             } else if (set.err.code) { | ||||
|                                 errMessage = set.err.code; | ||||
|                     let pluginCount = 0; | ||||
|                     for (let setName in moduleInfo.sets) { | ||||
|                         if (moduleInfo.sets.hasOwnProperty(setName)) { | ||||
|                             let set = moduleInfo.sets[setName]; | ||||
|                             if (set.plugins) { | ||||
|                                 pluginCount += set.plugins.length; | ||||
|                             } | ||||
|                             $("<li>").text(errMessage).appendTo(nodeEntry.errorList); | ||||
|                         } | ||||
|                         if (set.enabled) { | ||||
|                             activeTypeCount += set.types.length; | ||||
|                         } | ||||
|                         typeCount += set.types.length; | ||||
|                         for (var i=0;i<moduleInfo.sets[setName].types.length;i++) { | ||||
|                             var t = moduleInfo.sets[setName].types[i]; | ||||
|                             inUseCount += (typesInUse[t]||0); | ||||
|                             var swatch = setElements.swatches[t]; | ||||
|                     } | ||||
|                      | ||||
|                     nodeEntry.setCount.text(RED._('palette.editor.pluginCount',{count:pluginCount,label:pluginCount})); | ||||
|  | ||||
|                 } else { | ||||
|                     var activeTypeCount = 0; | ||||
|                     var typeCount = 0; | ||||
|                     var errorCount = 0; | ||||
|                     nodeEntry.errorList.empty(); | ||||
|                     nodeEntries[module].totalUseCount = 0; | ||||
|                     nodeEntries[module].setUseCount = {}; | ||||
|  | ||||
|                     for (var setName in moduleInfo.sets) { | ||||
|                         if (moduleInfo.sets.hasOwnProperty(setName)) { | ||||
|                             var inUseCount = 0; | ||||
|                             const set = moduleInfo.sets[setName]; | ||||
|                             const setElements = nodeEntry.sets[setName] | ||||
|  | ||||
|                             if (set.err) { | ||||
|                                 errorCount++; | ||||
|                                 var errMessage = set.err; | ||||
|                                 if (set.err.message) { | ||||
|                                     errMessage = set.err.message; | ||||
|                                 } else if (set.err.code) { | ||||
|                                     errMessage = set.err.code; | ||||
|                                 } | ||||
|                                 $("<li>").text(errMessage).appendTo(nodeEntry.errorList); | ||||
|                             } | ||||
|                             if (set.enabled) { | ||||
|                                 var def = RED.nodes.getType(t); | ||||
|                                 if (def && def.color) { | ||||
|                                     swatch.css({background:RED.utils.getNodeColor(t,def)}); | ||||
|                                     swatch.css({border: "1px solid "+getContrastingBorder(swatch.css('backgroundColor'))}) | ||||
|                                 activeTypeCount += set.types.length; | ||||
|                             } | ||||
|                             typeCount += set.types.length; | ||||
|                             for (var i=0;i<moduleInfo.sets[setName].types.length;i++) { | ||||
|                                 var t = moduleInfo.sets[setName].types[i]; | ||||
|                                 inUseCount += (typesInUse[t]||0); | ||||
|                                 if (setElements && set.enabled) { | ||||
|                                     var def = RED.nodes.getType(t); | ||||
|                                     if (def && def.color) { | ||||
|                                         setElements.swatches[t].css({background:RED.utils.getNodeColor(t,def)}); | ||||
|                                         setElements.swatches[t].css({border: "1px solid "+getContrastingBorder(setElements.swatches[t].css('backgroundColor'))}) | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                         nodeEntries[module].setUseCount[setName] = inUseCount; | ||||
|                         nodeEntries[module].totalUseCount += inUseCount; | ||||
|                             nodeEntries[module].setUseCount[setName] = inUseCount; | ||||
|                             nodeEntries[module].totalUseCount += inUseCount; | ||||
|  | ||||
|                         if (inUseCount > 0) { | ||||
|                             setElements.enableButton.text(RED._('palette.editor.inuse')); | ||||
|                             setElements.enableButton.addClass('disabled'); | ||||
|                         } else { | ||||
|                             setElements.enableButton.removeClass('disabled'); | ||||
|                             if (set.enabled) { | ||||
|                                 setElements.enableButton.text(RED._('palette.editor.disable')); | ||||
|                             } else { | ||||
|                                 setElements.enableButton.text(RED._('palette.editor.enable')); | ||||
|                             if (setElements) { | ||||
|                                 if (inUseCount > 0) { | ||||
|                                     setElements.enableButton.text(RED._('palette.editor.inuse')); | ||||
|                                     setElements.enableButton.addClass('disabled'); | ||||
|                                 } else { | ||||
|                                     setElements.enableButton.removeClass('disabled'); | ||||
|                                     if (set.enabled) { | ||||
|                                         setElements.enableButton.text(RED._('palette.editor.disable')); | ||||
|                                     } else { | ||||
|                                         setElements.enableButton.text(RED._('palette.editor.enable')); | ||||
|                                     } | ||||
|                                 } | ||||
|                                 setElements.setRow.toggleClass("red-ui-palette-module-set-disabled",!set.enabled); | ||||
|                             } | ||||
|                         } | ||||
|                         setElements.setRow.toggleClass("red-ui-palette-module-set-disabled",!set.enabled); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if (errorCount === 0) { | ||||
|                     nodeEntry.errorRow.hide() | ||||
|                 } else { | ||||
|                     nodeEntry.errorRow.show(); | ||||
|                 } | ||||
|  | ||||
|                 var nodeCount = (activeTypeCount === typeCount)?typeCount:activeTypeCount+" / "+typeCount; | ||||
|                 nodeEntry.setCount.text(RED._('palette.editor.nodeCount',{count:typeCount,label:nodeCount})); | ||||
|  | ||||
|                 if (nodeEntries[module].totalUseCount > 0) { | ||||
|                     nodeEntry.enableButton.text(RED._('palette.editor.inuse')); | ||||
|                     nodeEntry.enableButton.addClass('disabled'); | ||||
|                     nodeEntry.removeButton.hide(); | ||||
|                 } else { | ||||
|                     nodeEntry.enableButton.removeClass('disabled'); | ||||
|                     if (moduleInfo.local) { | ||||
|                         nodeEntry.removeButton.css('display', 'inline-block'); | ||||
|                     } | ||||
|                     if (activeTypeCount === 0) { | ||||
|                         nodeEntry.enableButton.text(RED._('palette.editor.enableall')); | ||||
|                     if (errorCount === 0) { | ||||
|                         nodeEntry.errorRow.hide() | ||||
|                     } else { | ||||
|                         nodeEntry.enableButton.text(RED._('palette.editor.disableall')); | ||||
|                         nodeEntry.errorRow.show(); | ||||
|                     } | ||||
|  | ||||
|                     var nodeCount = (activeTypeCount === typeCount)?typeCount:activeTypeCount+" / "+typeCount; | ||||
|                     nodeEntry.setCount.text(RED._('palette.editor.nodeCount',{count:typeCount,label:nodeCount})); | ||||
|  | ||||
|                     if (nodeEntries[module].totalUseCount > 0) { | ||||
|                         nodeEntry.enableButton.text(RED._('palette.editor.inuse')); | ||||
|                         nodeEntry.enableButton.addClass('disabled'); | ||||
|                         nodeEntry.removeButton.hide(); | ||||
|                     } else { | ||||
|                         nodeEntry.enableButton.removeClass('disabled'); | ||||
|                         if (moduleInfo.local) { | ||||
|                             nodeEntry.removeButton.css('display', 'inline-block'); | ||||
|                         } | ||||
|                         if (activeTypeCount === 0) { | ||||
|                             nodeEntry.enableButton.text(RED._('palette.editor.enableall')); | ||||
|                         } else { | ||||
|                             nodeEntry.enableButton.text(RED._('palette.editor.disableall')); | ||||
|                         } | ||||
|                         nodeEntry.container.toggleClass("disabled",(activeTypeCount === 0)); | ||||
|                     } | ||||
|                     nodeEntry.container.toggleClass("disabled",(activeTypeCount === 0)); | ||||
|                 } | ||||
|             } | ||||
|             if (moduleInfo.pending_version) { | ||||
| @@ -678,6 +698,33 @@ RED.palette.editor = (function() { | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         RED.events.on("registry:plugin-module-added", function(module) { | ||||
|  | ||||
|             if (!nodeEntries.hasOwnProperty(module)) { | ||||
|                 nodeEntries[module] = {info:RED.plugins.getModule(module)}; | ||||
|                 var index = [module]; | ||||
|                 for (var s in nodeEntries[module].info.sets) { | ||||
|                     if (nodeEntries[module].info.sets.hasOwnProperty(s)) { | ||||
|                         index.push(s); | ||||
|                         index = index.concat(nodeEntries[module].info.sets[s].types) | ||||
|                     } | ||||
|                 } | ||||
|                 nodeEntries[module].index = index.join(",").toLowerCase(); | ||||
|                 nodeList.editableList('addItem', nodeEntries[module]); | ||||
|             } else { | ||||
|                 _refreshNodeModule(module); | ||||
|             } | ||||
|  | ||||
|             for (var i=0;i<filteredList.length;i++) { | ||||
|                 if (filteredList[i].info.id === module) { | ||||
|                     var installButton = filteredList[i].elements.installButton; | ||||
|                     installButton.addClass('disabled'); | ||||
|                     installButton.text(RED._('palette.editor.installed')); | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     var settingsPane; | ||||
| @@ -804,6 +851,7 @@ RED.palette.editor = (function() { | ||||
|                         errorRow: errorRow, | ||||
|                         errorList: errorList, | ||||
|                         setCount: setCount, | ||||
|                         setButton: setButton, | ||||
|                         container: container, | ||||
|                         shade: shade, | ||||
|                         versionSpan: versionSpan, | ||||
| @@ -814,49 +862,88 @@ RED.palette.editor = (function() { | ||||
|                         if (container.hasClass('expanded')) { | ||||
|                             container.removeClass('expanded'); | ||||
|                             contentRow.slideUp(); | ||||
|                             setTimeout(() => { | ||||
|                                 contentRow.empty() | ||||
|                             }, 200) | ||||
|                             object.elements.sets = {} | ||||
|                         } else { | ||||
|                             container.addClass('expanded'); | ||||
|                             populateSetList() | ||||
|                             contentRow.slideDown(); | ||||
|                         } | ||||
|                     }) | ||||
|  | ||||
|                     var setList = Object.keys(entry.sets) | ||||
|                     setList.sort(function(A,B) { | ||||
|                         return A.toLowerCase().localeCompare(B.toLowerCase()); | ||||
|                     }); | ||||
|                     setList.forEach(function(setName) { | ||||
|                         var set = entry.sets[setName]; | ||||
|                         var setRow = $('<div>',{class:"red-ui-palette-module-set"}).appendTo(contentRow); | ||||
|                         var buttonGroup = $('<div>',{class:"red-ui-palette-module-set-button-group"}).appendTo(setRow); | ||||
|                         var typeSwatches = {}; | ||||
|                         set.types.forEach(function(t) { | ||||
|                             var typeDiv = $('<div>',{class:"red-ui-palette-module-type"}).appendTo(setRow); | ||||
|                             typeSwatches[t] = $('<span>',{class:"red-ui-palette-module-type-swatch"}).appendTo(typeDiv); | ||||
|                             $('<span>',{class:"red-ui-palette-module-type-node"}).text(t).appendTo(typeDiv); | ||||
|                         }) | ||||
|                         var enableButton = $('<a href="#" class="red-ui-button red-ui-button-small"></a>').appendTo(buttonGroup); | ||||
|                         enableButton.on("click", function(evt) { | ||||
|                             evt.preventDefault(); | ||||
|                             if (object.setUseCount[setName] === 0) { | ||||
|                                 var currentSet = RED.nodes.registry.getNodeSet(set.id); | ||||
|                                 shade.show(); | ||||
|                                 var newState = !currentSet.enabled | ||||
|                                 changeNodeState(set.id,newState,shade,function(xhr){ | ||||
|                                     if (xhr) { | ||||
|                                         if (xhr.responseJSON) { | ||||
|                                             RED.notify(RED._('palette.editor.errors.'+(newState?'enable':'disable')+'Failed',{module: id,message:xhr.responseJSON.message})); | ||||
|                     const populateSetList = function () { | ||||
|                         var setList = Object.keys(entry.sets) | ||||
|                         setList.sort(function(A,B) { | ||||
|                             return A.toLowerCase().localeCompare(B.toLowerCase()); | ||||
|                         }); | ||||
|                         setList.forEach(function(setName) { | ||||
|                             var set = entry.sets[setName]; | ||||
|                             var setRow = $('<div>',{class:"red-ui-palette-module-set"}).appendTo(contentRow); | ||||
|                             var buttonGroup = $('<div>',{class:"red-ui-palette-module-set-button-group"}).appendTo(setRow); | ||||
|                             var typeSwatches = {}; | ||||
|                             let enableButton; | ||||
|                             if (set.types) { | ||||
|                                 set.types.forEach(function(t) { | ||||
|                                     var typeDiv = $('<div>',{class:"red-ui-palette-module-type"}).appendTo(setRow); | ||||
|                                     typeSwatches[t] = $('<span>',{class:"red-ui-palette-module-type-swatch"}).appendTo(typeDiv); | ||||
|                                     if (set.enabled) { | ||||
|                                         var def = RED.nodes.getType(t); | ||||
|                                         if (def && def.color) { | ||||
|                                             typeSwatches[t].css({background:RED.utils.getNodeColor(t,def)}); | ||||
|                                             typeSwatches[t].css({border: "1px solid "+getContrastingBorder(typeSwatches[t].css('backgroundColor'))}) | ||||
|                                         } | ||||
|                                     } | ||||
|                                 }); | ||||
|                             } | ||||
|                         }) | ||||
|                                     $('<span>',{class:"red-ui-palette-module-type-node"}).text(t).appendTo(typeDiv); | ||||
|                                 }) | ||||
|                                 enableButton = $('<a href="#" class="red-ui-button red-ui-button-small"></a>').appendTo(buttonGroup); | ||||
|                                 enableButton.on("click", function(evt) { | ||||
|                                     evt.preventDefault(); | ||||
|                                     if (object.setUseCount[setName] === 0) { | ||||
|                                         var currentSet = RED.nodes.registry.getNodeSet(set.id); | ||||
|                                         shade.show(); | ||||
|                                         var newState = !currentSet.enabled | ||||
|                                         changeNodeState(set.id,newState,shade,function(xhr){ | ||||
|                                             if (xhr) { | ||||
|                                                 if (xhr.responseJSON) { | ||||
|                                                     RED.notify(RED._('palette.editor.errors.'+(newState?'enable':'disable')+'Failed',{module: id,message:xhr.responseJSON.message})); | ||||
|                                                 } | ||||
|                                             } | ||||
|                                         }); | ||||
|                                     } | ||||
|                                 }) | ||||
|  | ||||
|                         object.elements.sets[set.name] = { | ||||
|                             setRow: setRow, | ||||
|                             enableButton: enableButton, | ||||
|                             swatches: typeSwatches | ||||
|                         }; | ||||
|                     }); | ||||
|                                 if (object.setUseCount[setName] > 0) { | ||||
|                                     enableButton.text(RED._('palette.editor.inuse')); | ||||
|                                     enableButton.addClass('disabled'); | ||||
|                                 } else { | ||||
|                                     enableButton.removeClass('disabled'); | ||||
|                                     if (set.enabled) { | ||||
|                                         enableButton.text(RED._('palette.editor.disable')); | ||||
|                                     } else { | ||||
|                                         enableButton.text(RED._('palette.editor.enable')); | ||||
|                                     } | ||||
|                                 } | ||||
|                                 setRow.toggleClass("red-ui-palette-module-set-disabled",!set.enabled); | ||||
|  | ||||
|  | ||||
|                             } | ||||
|                             if (set.plugins) { | ||||
|                                 set.plugins.forEach(function(p) { | ||||
|                                     var typeDiv = $('<div>',{class:"red-ui-palette-module-type"}).appendTo(setRow); | ||||
|                                     // typeSwatches[p.id] = $('<span>',{class:"red-ui-palette-module-type-swatch"}).appendTo(typeDiv); | ||||
|                                     $('<span><i class="fa fa-puzzle-piece" aria-hidden="true"></i>  </span>',{class:"red-ui-palette-module-type-swatch"}).appendTo(typeDiv); | ||||
|                                     $('<span>',{class:"red-ui-palette-module-type-node"}).text(p.id).appendTo(typeDiv); | ||||
|                                 }) | ||||
|                             } | ||||
|  | ||||
|                             object.elements.sets[set.name] = { | ||||
|                                 setRow: setRow, | ||||
|                                 enableButton: enableButton, | ||||
|                                 swatches: typeSwatches | ||||
|                             }; | ||||
|                         }); | ||||
|                     } | ||||
|                     enableButton.on("click", function(evt) { | ||||
|                         evt.preventDefault(); | ||||
|                         if (object.totalUseCount === 0) { | ||||
| @@ -1226,7 +1313,55 @@ RED.palette.editor = (function() { | ||||
|                                                 } | ||||
|                                             } | ||||
|                                         ] | ||||
|                                     });                                } | ||||
|                                     });                                 | ||||
|                                 } | ||||
|                             } else { | ||||
|                                 // dedicated list management for plugins | ||||
|                                 if (entry.plugin) { | ||||
|  | ||||
|                                     let e = nodeEntries[entry.name]; | ||||
|                                     if (e) { | ||||
|                                         nodeList.editableList('removeItem', e); | ||||
|                                         delete nodeEntries[entry.name]; | ||||
|                                     } | ||||
|  | ||||
|                                     // We assume that a plugin that implements onremove | ||||
|                                     // cleans the editor accordingly of its left-overs. | ||||
|                                     let found_onremove = true; | ||||
|  | ||||
|                                     let keys = Object.keys(entry.sets); | ||||
|                                     keys.forEach((key) => { | ||||
|                                         let set = entry.sets[key]; | ||||
|                                         for (let i=0; i<set.plugins?.length; i++) { | ||||
|                                             let plgn = RED.plugins.getPlugin(set.plugins[i].id); | ||||
|                                             if (plgn && plgn.onremove  && typeof plgn.onremove === 'function') { | ||||
|                                                 plgn.onremove(); | ||||
|                                             } else { | ||||
|                                                 if (plgn && plgn.onadd && typeof plgn.onadd === 'function') { | ||||
|                                                     // if there's no 'onadd', there shouldn't be any left-overs | ||||
|                                                     found_onremove = false; | ||||
|                                                 } | ||||
|                                             } | ||||
|                                         } | ||||
|                                     }); | ||||
|  | ||||
|                                     if (!found_onremove) { | ||||
|                                         let removeNotify = RED.notify(RED._("palette.editor.confirm.removePlugin.body",{module:entry.name}),{ | ||||
|                                             modal: true, | ||||
|                                             fixed: true, | ||||
|                                             type: 'warning', | ||||
|                                             buttons: [ | ||||
|                                                 { | ||||
|                                                     text: RED._("palette.editor.confirm.button.understood"), | ||||
|                                                     class:"primary", | ||||
|                                                     click: function(e) { | ||||
|                                                         removeNotify.close(); | ||||
|                                                     } | ||||
|                                                 } | ||||
|                                             ] | ||||
|                                         }); | ||||
|                                     } | ||||
|                                 }         | ||||
|                             } | ||||
|                         }) | ||||
|                         notification.close(); | ||||
| @@ -1270,9 +1405,28 @@ RED.palette.editor = (function() { | ||||
|                     RED.actions.invoke("core:show-event-log"); | ||||
|                 }); | ||||
|                 RED.eventLog.startEvent(RED._("palette.editor.confirm.button.install")+" : "+entry.id+" "+entry.version); | ||||
|                 installNodeModule(entry.id,entry.version,entry.pkg_url,function(xhr) { | ||||
|                 installNodeModule(entry.id,entry.version,entry.pkg_url,function(xhr, textStatus,err) { | ||||
|                     spinner.remove(); | ||||
|                      if (xhr) { | ||||
|                      if (err && xhr.status === 504) { | ||||
|                         var notification = RED.notify(RED._("palette.editor.errors.installTimeout"), { | ||||
|                             modal: true, | ||||
|                             fixed: true, | ||||
|                             buttons: [ | ||||
|                                 { | ||||
|                                     text: RED._("common.label.close"), | ||||
|                                     click: function() { | ||||
|                                         notification.close(); | ||||
|                                     } | ||||
|                                 },{ | ||||
|                                     text: RED._("eventLog.view"), | ||||
|                                     click: function() { | ||||
|                                         notification.close(); | ||||
|                                         RED.actions.invoke("core:show-event-log"); | ||||
|                                     } | ||||
|                                 } | ||||
|                             ] | ||||
|                         }) | ||||
|                      } else if (xhr) { | ||||
|                          if (xhr.responseJSON) { | ||||
|                              var notification = RED.notify(RED._('palette.editor.errors.installFailed',{module: entry.id,message:xhr.responseJSON.message}),{ | ||||
|                                  type: 'error', | ||||
|   | ||||
| @@ -35,6 +35,10 @@ RED.palette = (function() { | ||||
|     var categoryContainers = {}; | ||||
|     var sidebarControls; | ||||
|  | ||||
|     let paletteState = { filter: "", collapsed: [] }; | ||||
|  | ||||
|     let filterRefreshTimeout | ||||
|  | ||||
|     function createCategory(originalCategory,rootCategory,category,ns) { | ||||
|         if ($("#red-ui-palette-base-category-"+rootCategory).length === 0) { | ||||
|             createCategoryContainer(originalCategory,rootCategory, ns+":palette.label."+rootCategory); | ||||
| @@ -60,20 +64,57 @@ RED.palette = (function() { | ||||
|         catDiv.data('label',label); | ||||
|         categoryContainers[category] = { | ||||
|             container: catDiv, | ||||
|             close: function() { | ||||
|             hide: function (instant) { | ||||
|                 if (instant) { | ||||
|                     catDiv.hide() | ||||
|                 } else { | ||||
|                     catDiv.slideUp() | ||||
|                 } | ||||
|             }, | ||||
|             show: function () { | ||||
|                 catDiv.show() | ||||
|             }, | ||||
|             isOpen: function () { | ||||
|                 return !!catDiv.hasClass("red-ui-palette-open") | ||||
|             }, | ||||
|             getNodeCount: function (visibleOnly) { | ||||
|                 const nodes = catDiv.find(".red-ui-palette-node") | ||||
|                 if (visibleOnly) { | ||||
|                     return nodes.filter(function() { return $(this).css('display') !== 'none'}).length | ||||
|                 } else { | ||||
|                     return nodes.length | ||||
|                 } | ||||
|             }, | ||||
|             close: function(instant, skipSaveState) { | ||||
|                 catDiv.removeClass("red-ui-palette-open"); | ||||
|                 catDiv.addClass("red-ui-palette-closed"); | ||||
|                 $("#red-ui-palette-base-category-"+category).slideUp(); | ||||
|                 if (instant) { | ||||
|                     $("#red-ui-palette-base-category-"+category).hide(); | ||||
|                 } else { | ||||
|                     $("#red-ui-palette-base-category-"+category).slideUp(); | ||||
|                 } | ||||
|                 $("#red-ui-palette-header-"+category+" i").removeClass("expanded"); | ||||
|                 if (!skipSaveState) { | ||||
|                     if (!paletteState.collapsed.includes(category)) { | ||||
|                         paletteState.collapsed.push(category); | ||||
|                         savePaletteState(); | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             open: function() { | ||||
|             open: function(skipSaveState) { | ||||
|                 catDiv.addClass("red-ui-palette-open"); | ||||
|                 catDiv.removeClass("red-ui-palette-closed"); | ||||
|                 $("#red-ui-palette-base-category-"+category).slideDown(); | ||||
|                 $("#red-ui-palette-header-"+category+" i").addClass("expanded"); | ||||
|                 if (!skipSaveState) { | ||||
|                     if (paletteState.collapsed.includes(category)) { | ||||
|                         paletteState.collapsed.splice(paletteState.collapsed.indexOf(category), 1); | ||||
|                         savePaletteState(); | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             toggle: function() { | ||||
|                 if (catDiv.hasClass("red-ui-palette-open")) { | ||||
|                 if (categoryContainers[category].isOpen()) { | ||||
|                     categoryContainers[category].close(); | ||||
|                 } else { | ||||
|                     categoryContainers[category].open(); | ||||
| @@ -415,8 +456,16 @@ RED.palette = (function() { | ||||
|  | ||||
|             var categoryNode = $("#red-ui-palette-container-"+rootCategory); | ||||
|             if (categoryNode.find(".red-ui-palette-node").length === 1) { | ||||
|                 categoryContainers[rootCategory].open(); | ||||
|                 if (!paletteState?.collapsed?.includes(rootCategory)) { | ||||
|                     categoryContainers[rootCategory].open(); | ||||
|                 } else { | ||||
|                     categoryContainers[rootCategory].close(true); | ||||
|                 } | ||||
|             } | ||||
|             clearTimeout(filterRefreshTimeout) | ||||
|             filterRefreshTimeout = setTimeout(() => { | ||||
|                 refreshFilter() | ||||
|             }, 200) | ||||
|  | ||||
|         } | ||||
|     } | ||||
| @@ -516,7 +565,8 @@ RED.palette = (function() { | ||||
|         paletteNode.css("backgroundColor", sf.color); | ||||
|     } | ||||
|  | ||||
|     function filterChange(val) { | ||||
|     function refreshFilter() { | ||||
|         const val = $("#red-ui-palette-search input").val() | ||||
|         var re = new RegExp(val.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'),'i'); | ||||
|         $("#red-ui-palette-container .red-ui-palette-node").each(function(i,el) { | ||||
|             var currentLabel = $(el).attr("data-palette-label"); | ||||
| @@ -528,16 +578,26 @@ RED.palette = (function() { | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         for (var category in categoryContainers) { | ||||
|         for (let category in categoryContainers) { | ||||
|             if (categoryContainers.hasOwnProperty(category)) { | ||||
|                 if (categoryContainers[category].container | ||||
|                         .find(".red-ui-palette-node") | ||||
|                         .filter(function() { return $(this).css('display') !== 'none'}).length === 0) { | ||||
|                     categoryContainers[category].close(); | ||||
|                     categoryContainers[category].container.slideUp(); | ||||
|                 const categorySection = categoryContainers[category] | ||||
|                 if (categorySection.getNodeCount(true) === 0) { | ||||
|                     categorySection.hide() | ||||
|                 } else { | ||||
|                     categoryContainers[category].open(); | ||||
|                     categoryContainers[category].container.show(); | ||||
|                     categorySection.show() | ||||
|                     if (val) { | ||||
|                         // There is a filter being applied and it has matched | ||||
|                         // something in this category - show the contents | ||||
|                         categorySection.open(true) | ||||
|                     } else { | ||||
|                         // No filter. Only show the category if it isn't in lastState | ||||
|                         if (!paletteState.collapsed.includes(category)) { | ||||
|                             categorySection.open(true) | ||||
|                         } else if (categorySection.isOpen()) { | ||||
|                             // This section should be collapsed but isn't - so make it so | ||||
|                             categorySection.close(true, true) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| @@ -553,6 +613,9 @@ RED.palette = (function() { | ||||
|  | ||||
|         $("#red-ui-palette > .red-ui-palette-spinner").show(); | ||||
|  | ||||
|         RED.events.on('logout', function () { | ||||
|             RED.settings.removeLocal('palette-state') | ||||
|         }) | ||||
|  | ||||
|         RED.events.on('registry:node-type-added', function(nodeType) { | ||||
|             var def = RED.nodes.getType(nodeType); | ||||
| @@ -596,14 +659,14 @@ RED.palette = (function() { | ||||
|  | ||||
|         RED.events.on("subflows:change",refreshSubflow); | ||||
|  | ||||
|  | ||||
|  | ||||
|         $("#red-ui-palette-search input").searchBox({ | ||||
|             delay: 100, | ||||
|             change: function() { | ||||
|                 filterChange($(this).val()); | ||||
|                 refreshFilter(); | ||||
|                 paletteState.filter = $(this).val(); | ||||
|                 savePaletteState(); | ||||
|             } | ||||
|         }) | ||||
|         }); | ||||
|  | ||||
|         sidebarControls = $('<div class="red-ui-sidebar-control-left"><i class="fa fa-chevron-left"></i></div>').appendTo($("#red-ui-palette")); | ||||
|         RED.popover.tooltip(sidebarControls,RED._("keyboard.togglePalette"),"core:toggle-palette"); | ||||
| @@ -669,7 +732,23 @@ RED.palette = (function() { | ||||
|                 togglePalette(state); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         try { | ||||
|             paletteState = JSON.parse(RED.settings.getLocal("palette-state") || '{"filter":"", "collapsed": []}'); | ||||
|             if (paletteState.filter) { | ||||
|                 // Apply the category filter | ||||
|                 $("#red-ui-palette-search input").searchBox("value", paletteState.filter); | ||||
|             } | ||||
|         } catch (error) { | ||||
|             console.error("Unexpected error loading palette state from localStorage: ", error); | ||||
|         } | ||||
|         setTimeout(() => { | ||||
|             // Lazily tidy up any categories that haven't been reloaded | ||||
|             paletteState.collapsed = paletteState.collapsed.filter(category => !!categoryContainers[category]) | ||||
|             savePaletteState() | ||||
|         }, 10000) | ||||
|     } | ||||
|  | ||||
|     function togglePalette(state) { | ||||
|         if (!state) { | ||||
|             $("#red-ui-main-container").addClass("red-ui-palette-closed"); | ||||
| @@ -689,6 +768,15 @@ RED.palette = (function() { | ||||
|         }) | ||||
|         return categories; | ||||
|     } | ||||
|  | ||||
|     function savePaletteState() { | ||||
|         try { | ||||
|             RED.settings.setLocal("palette-state", JSON.stringify(paletteState)); | ||||
|         } catch (error) { | ||||
|             console.error("Unexpected error saving palette state to localStorage: ", error); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         init: init, | ||||
|         add:addNodeType, | ||||
|   | ||||
| @@ -287,7 +287,7 @@ RED.projects.settings = (function() { | ||||
|         var notInstalledCount = 0; | ||||
|  | ||||
|         for (var m in modulesInUse) { | ||||
|             if (modulesInUse.hasOwnProperty(m)) { | ||||
|             if (modulesInUse.hasOwnProperty(m) && !activeProject.dependencies.hasOwnProperty(m)) { | ||||
|                 depsList.editableList('addItem',{ | ||||
|                     id: modulesInUse[m].module, | ||||
|                     version: modulesInUse[m].version, | ||||
| @@ -307,8 +307,8 @@ RED.projects.settings = (function() { | ||||
|  | ||||
|         if (activeProject.dependencies) { | ||||
|             for (var m in activeProject.dependencies) { | ||||
|                 if (activeProject.dependencies.hasOwnProperty(m) && !modulesInUse.hasOwnProperty(m)) { | ||||
|                     var installed = !!RED.nodes.registry.getModule(m); | ||||
|                 if (activeProject.dependencies.hasOwnProperty(m)) { | ||||
|                     var installed = !!RED.nodes.registry.getModule(m) && activeProject.dependencies[m] === modulesInUse[m]?.version; | ||||
|                     depsList.editableList('addItem',{ | ||||
|                         id: m, | ||||
|                         version: activeProject.dependencies[m], //RED.nodes.registry.getModule(module).version, | ||||
| @@ -1256,7 +1256,7 @@ RED.projects.settings = (function() { | ||||
|                                             notification.close(); | ||||
|                                         } | ||||
|                                     },{ | ||||
|                                         text: 'Delete branch', | ||||
|                                         text: RED._("sidebar.project.projectSettings.deleteBranch"), | ||||
|                                         click: function() { | ||||
|                                             notification.close(); | ||||
|                                             var url = "projects/"+activeProject.name+"/branches/"+entry.name; | ||||
|   | ||||
| @@ -909,17 +909,19 @@ RED.subflow = (function() { | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Create interface for controlling env var UI definition | ||||
|      * Build the edit dialog for a subflow template (creating/modifying a subflow template) | ||||
|      * @param {Object} uiContainer - the jQuery container for the environment variable list | ||||
|      * @param {Object} node - the subflow template node | ||||
|      */ | ||||
|     function buildEnvControl(envList,node) { | ||||
|     function buildEnvControl(uiContainer,node) { | ||||
|         var tabs = RED.tabs.create({ | ||||
|             id: "subflow-env-tabs", | ||||
|             onchange: function(tab) { | ||||
|                 if (tab.id === "subflow-env-tab-preview") { | ||||
|                     var inputContainer = $("#subflow-input-ui"); | ||||
|                     var list = envList.editableList("items"); | ||||
|                     var list = uiContainer.editableList("items"); | ||||
|                     var exportedEnv = exportEnvList(list, true); | ||||
|                     buildEnvUI(inputContainer, exportedEnv,node); | ||||
|                     buildEnvUI(inputContainer, exportedEnv, node); | ||||
|                 } | ||||
|                 $("#subflow-env-tabs-content").children().hide(); | ||||
|                 $("#" + tab.id).show(); | ||||
| @@ -957,12 +959,33 @@ RED.subflow = (function() { | ||||
|         RED.editor.envVarList.setLocale(locale); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     function buildEnvUIRow(row, tenv, ui, node) { | ||||
|     /** | ||||
|      * Build a UI row for a subflow instance environment variable | ||||
|      * Also used to build the UI row for subflow template preview | ||||
|      * @param {JQuery} row - A form row element | ||||
|      * @param {Object} tenv - A template environment variable | ||||
|      * @param {String} tenv.name - The name of the environment variable | ||||
|      * @param {String} tenv.type - The type of the environment variable | ||||
|      * @param {String} tenv.value - The value set for this environment variable | ||||
|      * @param {Object} tenv.parent - The parent environment variable | ||||
|      * @param {String} tenv.parent.value - The value set for the parent environment variable | ||||
|      * @param {String} tenv.parent.type - The type of the parent environment variable | ||||
|      * @param {Object} tenv.ui - The UI configuration for the environment variable | ||||
|      * @param {String} tenv.ui.icon - The icon for the environment variable | ||||
|      * @param {Object} tenv.ui.label - The label for the environment variable | ||||
|      * @param {String} tenv.ui.type - The type of the UI control for the environment variable | ||||
|      * @param {Object} node - The subflow instance node | ||||
|      */ | ||||
|     function buildEnvUIRow(row, tenv, node) { | ||||
|         if(RED.subflow.debug) { console.log("buildEnvUIRow", tenv) } | ||||
|         const ui = tenv.ui || {} | ||||
|         ui.label = ui.label||{}; | ||||
|         if ((tenv.type === "cred" || (tenv.parent && tenv.parent.type === "cred")) && !ui.type) { | ||||
|             ui.type = "cred"; | ||||
|             ui.opts = {}; | ||||
|         } else if (tenv.type === "conf-types") { | ||||
|             ui.type = "conf-types" | ||||
|             ui.opts = { types: ['conf-types'] } | ||||
|         } else if (!ui.type) { | ||||
|             ui.type = "input"; | ||||
|             ui.opts = { types: RED.editor.envVarList.DEFAULT_ENV_TYPE_LIST } | ||||
| @@ -1006,9 +1029,10 @@ RED.subflow = (function() { | ||||
|         if (tenv.hasOwnProperty('type')) { | ||||
|             val.type = tenv.type; | ||||
|         } | ||||
|         const elId = getSubflowEnvPropertyName(tenv.name) | ||||
|         switch(ui.type) { | ||||
|             case "input": | ||||
|                 input = $('<input type="text">').css('width','70%').appendTo(row); | ||||
|                 input = $('<input type="text">').css('width','70%').attr('id', elId).appendTo(row); | ||||
|                 if (ui.opts.types && ui.opts.types.length > 0) { | ||||
|                     var inputType = val.type; | ||||
|                     if (ui.opts.types.indexOf(inputType) === -1) { | ||||
| @@ -1035,7 +1059,7 @@ RED.subflow = (function() { | ||||
|                 } | ||||
|                 break; | ||||
|             case "select": | ||||
|                 input = $('<select>').css('width','70%').appendTo(row); | ||||
|                 input = $('<select>').css('width','70%').attr('id', elId).appendTo(row); | ||||
|                 if (ui.opts.opts) { | ||||
|                     ui.opts.opts.forEach(function(o) { | ||||
|                         $('<option>').val(o.v).text(RED.editor.envVarList.lookupLabel(o.l, o.l['en-US']||o.v, locale)).appendTo(input); | ||||
| @@ -1046,7 +1070,7 @@ RED.subflow = (function() { | ||||
|             case "checkbox": | ||||
|                 label.css("cursor","default"); | ||||
|                 var cblabel = $('<label>').css('width','70%').appendTo(row); | ||||
|                 input = $('<input type="checkbox">').css({ | ||||
|                 input = $('<input type="checkbox">').attr('id', elId).css({ | ||||
|                     marginTop: 0, | ||||
|                     width: 'auto', | ||||
|                     height: '34px' | ||||
| @@ -1064,7 +1088,7 @@ RED.subflow = (function() { | ||||
|                 input.prop("checked",boolVal); | ||||
|                 break; | ||||
|             case "spinner": | ||||
|                 input = $('<input>').css('width','70%').appendTo(row); | ||||
|                 input = $('<input>').css('width','70%').attr('id', elId).appendTo(row); | ||||
|                 var spinnerOpts = {}; | ||||
|                 if (ui.opts.hasOwnProperty('min')) { | ||||
|                     spinnerOpts.min = ui.opts.min; | ||||
| @@ -1076,7 +1100,7 @@ RED.subflow = (function() { | ||||
|                 input.val(val.value); | ||||
|                 break; | ||||
|             case "cred": | ||||
|                 input = $('<input type="password">').css('width','70%').appendTo(row); | ||||
|                 input = $('<input type="password">').css('width','70%').attr('id', elId).appendTo(row); | ||||
|                 if (node.credentials) { | ||||
|                     if (node.credentials[tenv.name]) { | ||||
|                         input.val(node.credentials[tenv.name]); | ||||
| @@ -1093,18 +1117,25 @@ RED.subflow = (function() { | ||||
|                     default: 'cred' | ||||
|                 }) | ||||
|                 break; | ||||
|         } | ||||
|         if (input) { | ||||
|             input.attr('id',getSubflowEnvPropertyName(tenv.name)) | ||||
|             case "conf-types": | ||||
|                 // let clsId = 'config-node-input-' + val.type + '-' + val.value + '-' + Math.floor(Math.random() * 100000); | ||||
|                 // clsId = clsId.replace(/\W/g, '-'); | ||||
|                 // input = $('<input>').css('width','70%').addClass(clsId).attr('id', elId).appendTo(row); | ||||
|                 input = $('<input>').css('width','70%').attr('id', elId).appendTo(row); | ||||
|                 const _type = tenv.parent?.type || tenv.type; | ||||
|                 RED.editor.prepareConfigNodeSelect(node, tenv.name, _type, 'node-input-subflow-env', null, tenv); | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Create environment variable input UI | ||||
|      * Build the edit form for a subflow instance | ||||
|      * Also used to build the preview form in the subflow template edit dialog | ||||
|      * @param uiContainer - container for UI | ||||
|      * @param envList - env var definitions of template | ||||
|      */ | ||||
|     function buildEnvUI(uiContainer, envList, node) { | ||||
|         if(RED.subflow.debug) { console.log("buildEnvUI",envList) } | ||||
|         uiContainer.empty(); | ||||
|         for (var i = 0; i < envList.length; i++) { | ||||
|             var tenv = envList[i]; | ||||
| @@ -1112,7 +1143,7 @@ RED.subflow = (function() { | ||||
|                 continue; | ||||
|             } | ||||
|             var row = $("<div/>", { class: "form-row" }).appendTo(uiContainer); | ||||
|             buildEnvUIRow(row,tenv, tenv.ui || {}, node); | ||||
|             buildEnvUIRow(row, tenv, node); | ||||
|         } | ||||
|     } | ||||
|     // buildEnvUI | ||||
| @@ -1185,6 +1216,9 @@ RED.subflow = (function() { | ||||
|                                         delete ui.opts | ||||
|                                     } | ||||
|                                     break; | ||||
|                                 case "conf-types": | ||||
|                                     delete ui.opts; | ||||
|                                     break; | ||||
|                                 default: | ||||
|                                     delete ui.opts; | ||||
|                             } | ||||
| @@ -1207,8 +1241,9 @@ RED.subflow = (function() { | ||||
|         if (/^subflow:/.test(node.type)) { | ||||
|             var subflowDef = RED.nodes.subflow(node.type.substring(8)); | ||||
|             if (subflowDef.env) { | ||||
|                 subflowDef.env.forEach(function(env) { | ||||
|                 subflowDef.env.forEach(function(env, i) { | ||||
|                     var item = { | ||||
|                         index: i, | ||||
|                         name:env.name, | ||||
|                         parent: { | ||||
|                             type: env.type, | ||||
| @@ -1245,14 +1280,20 @@ RED.subflow = (function() { | ||||
|                     var nodePropValue = nodeProp; | ||||
|                     if (prop.ui && prop.ui.type === "cred") { | ||||
|                         nodePropType = "cred"; | ||||
|                     } else if (prop.ui && prop.ui.type === "conf-types") { | ||||
|                         nodePropType = prop.value.type | ||||
|                     } else { | ||||
|                         switch(typeof nodeProp) { | ||||
|                             case "string": nodePropType = "str"; break; | ||||
|                             case "number": nodePropType = "num"; break; | ||||
|                             case "boolean": nodePropType = "bool"; nodePropValue = nodeProp?"true":"false"; break; | ||||
|                             default: | ||||
|                             nodePropType = nodeProp.type; | ||||
|                             nodePropValue = nodeProp.value; | ||||
|                                 if (nodeProp) { | ||||
|                                     nodePropType = nodeProp.type; | ||||
|                                     nodePropValue = nodeProp.value; | ||||
|                                 } else { | ||||
|                                     nodePropType = 'str' | ||||
|                                 } | ||||
|                         } | ||||
|                     } | ||||
|                     var item = { | ||||
| @@ -1273,6 +1314,7 @@ RED.subflow = (function() { | ||||
|     } | ||||
|  | ||||
|     function exportSubflowInstanceEnv(node) { | ||||
|         if(RED.subflow.debug) { console.log("exportSubflowInstanceEnv",node) } | ||||
|         var env = []; | ||||
|         // First, get the values for the SubflowTemplate defined properties | ||||
|         //  - these are the ones with custom UI elements | ||||
| @@ -1304,7 +1346,7 @@ RED.subflow = (function() { | ||||
|                         } | ||||
|                         break; | ||||
|                     case "cred": | ||||
|                         item.value = input.val(); | ||||
|                         item.value = input.typedInput('value'); | ||||
|                         item.type = 'cred'; | ||||
|                         break; | ||||
|                     case "spinner": | ||||
| @@ -1319,6 +1361,9 @@ RED.subflow = (function() { | ||||
|                         item.type = 'bool'; | ||||
|                         item.value = ""+input.prop("checked"); | ||||
|                         break; | ||||
|                     case "conf-types": | ||||
|                         item.value = input.val() === "_ADD_" ? "" : input.val(); | ||||
|                         item.type = "conf-type" | ||||
|                 } | ||||
|                 if (ui.type === "cred" || item.type !== data.parent.type || item.value !== data.parent.value) { | ||||
|                     env.push(item); | ||||
| @@ -1332,8 +1377,15 @@ RED.subflow = (function() { | ||||
|         return 'node-input-subflow-env-'+name.replace(/[^a-z0-9-_]/ig,"_"); | ||||
|     } | ||||
|  | ||||
|     // Called by subflow.oneditprepare for both instances and templates | ||||
|      | ||||
|     /** | ||||
|      * Build the subflow edit form | ||||
|      * Called by subflow.oneditprepare for both instances and templates | ||||
|      * @param {"subflow"|"subflow-template"} type - the type of subflow being edited | ||||
|      * @param {Object} node - the node being edited | ||||
|      */ | ||||
|     function buildEditForm(type,node) { | ||||
|         if(RED.subflow.debug) { console.log("buildEditForm",type,node) } | ||||
|         if (type === "subflow-template") { | ||||
|             // This is the tabbed UI that offers the env list - with UI options | ||||
|             // plus the preview tab | ||||
|   | ||||
| @@ -382,9 +382,11 @@ RED.sidebar.config = (function() { | ||||
|                 refreshConfigNodeList(); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         RED.popover.tooltip($('#red-ui-sidebar-config-filter-all'), RED._("sidebar.config.showAllConfigNodes")); | ||||
|         RED.popover.tooltip($('#red-ui-sidebar-config-filter-unused'), RED._("sidebar.config.showAllUnusedConfigNodes")); | ||||
|  | ||||
|         RED.popover.tooltip($('#red-ui-sidebar-config-collapse-all'), RED._("palette.actions.collapse-all")); | ||||
|         RED.popover.tooltip($('#red-ui-sidebar-config-expand-all'), RED._("palette.actions.expand-all")); | ||||
|     } | ||||
|  | ||||
|     function flashConfigNode(el) { | ||||
|   | ||||
| @@ -36,7 +36,13 @@ RED.sidebar.help = (function() { | ||||
|         toolbar = $("<div>", {class:"red-ui-sidebar-header red-ui-info-toolbar"}).appendTo(content); | ||||
|         $('<span class="button-group"><a id="red-ui-sidebar-help-show-toc" class="red-ui-button red-ui-button-small selected" href="#"><i class="fa fa-list-ul"></i></a></span>').appendTo(toolbar) | ||||
|         var showTOCButton = toolbar.find('#red-ui-sidebar-help-show-toc') | ||||
|         RED.popover.tooltip(showTOCButton,RED._("sidebar.help.showTopics")); | ||||
|         RED.popover.tooltip(showTOCButton, function () { | ||||
|             if ($(showTOCButton).hasClass('selected')) { | ||||
|                 return RED._("sidebar.help.hideTopics"); | ||||
|             } else { | ||||
|                 return RED._("sidebar.help.showTopics"); | ||||
|             } | ||||
|         }); | ||||
|         showTOCButton.on("click",function(e) { | ||||
|             e.preventDefault(); | ||||
|             if ($(this).hasClass('selected')) { | ||||
| @@ -158,8 +164,10 @@ RED.sidebar.help = (function() { | ||||
|  | ||||
|     function refreshSubflow(sf) { | ||||
|         var item = treeList.treeList('get',"node-type:subflow:"+sf.id); | ||||
|         item.subflowLabel = sf._def.label().toLowerCase(); | ||||
|         item.treeList.replaceElement(getNodeLabel({_def:sf._def,type:sf._def.label()})); | ||||
|         if (item) { | ||||
|             item.subflowLabel = sf._def.label().toLowerCase(); | ||||
|             item.treeList.replaceElement(getNodeLabel({_def:sf._def,type:sf._def.label()})); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function hideTOC() { | ||||
|   | ||||
| @@ -103,7 +103,7 @@ RED.sidebar.info.outliner = (function() { | ||||
|                 evt.stopPropagation(); | ||||
|                 RED.search.show("type:subflow:"+n.id); | ||||
|             }) | ||||
|             // RED.popover.tooltip(userCountBadge,function() { return RED._('editor.nodesUse',{count:n.users.length})}); | ||||
|             RED.popover.tooltip(subflowInstanceBadge,function() { return RED._('subflow.subflowInstances',{count:n.instances.length})}); | ||||
|         } | ||||
|         if (n._def.category === "config" && n.type !== "group") { | ||||
|             var userCountBadge = $('<button type="button" class="red-ui-info-outline-item-control-users red-ui-button red-ui-button-small"><i class="fa fa-toggle-right"></i></button>').text(n.users.length).appendTo(controls).on("click",function(evt) { | ||||
|   | ||||
| @@ -204,7 +204,7 @@ RED.sidebar.info = (function() { | ||||
|  | ||||
|             propertiesPanelHeaderIcon.empty(); | ||||
|             RED.utils.createNodeIcon({type:"_selection_"}).appendTo(propertiesPanelHeaderIcon); | ||||
|             propertiesPanelHeaderLabel.text("Selection"); | ||||
|             propertiesPanelHeaderLabel.text(RED._("sidebar.info.selection")); | ||||
|             propertiesPanelHeaderReveal.hide(); | ||||
|             propertiesPanelHeaderHelp.hide(); | ||||
|             propertiesPanelHeaderCopyLink.hide(); | ||||
|   | ||||
| @@ -435,10 +435,15 @@ RED.tourGuide = (function() { | ||||
|  | ||||
|     function listTour() { | ||||
|         return [ | ||||
|             { | ||||
|                 id: "4_0", | ||||
|                 label: "4.0", | ||||
|                 path: "./tours/welcome.js" | ||||
|             }, | ||||
|             { | ||||
|                 id: "3_1", | ||||
|                 label: "3.1", | ||||
|                 path: "./tours/welcome.js" | ||||
|                 path: "./tours/3.1/welcome.js" | ||||
|             }, | ||||
|             { | ||||
|                 id: "3_0", | ||||
|   | ||||
| @@ -264,6 +264,7 @@ | ||||
|                 setTimeout(function() { | ||||
|                     oldTray.tray.detach(); | ||||
|                     showTray(options); | ||||
|                     RED.events.emit('editor:change') | ||||
|                 },250) | ||||
|             } else { | ||||
|                 if (stack.length > 0) { | ||||
| @@ -333,6 +334,7 @@ | ||||
|                         RED.view.focus(); | ||||
|                     } else { | ||||
|                         stack[stack.length-1].tray.css("z-index", "auto"); | ||||
|                         RED.events.emit('editor:change') | ||||
|                     } | ||||
|                 },250) | ||||
|             } | ||||
|   | ||||
| @@ -279,6 +279,11 @@ RED.typeSearch = (function() { | ||||
|         if ($("#red-ui-main-container").height() - opts.y - 195 < 0) { | ||||
|             opts.y = opts.y - 275; | ||||
|         } | ||||
|         const dialogWidth = dialog.width() || 300 // default is 300 (defined in class .red-ui-search) | ||||
|         const workspaceWidth = $('#red-ui-workspace').width() | ||||
|         if (workspaceWidth > dialogWidth && workspaceWidth - opts.x - dialogWidth < 0) { | ||||
|             opts.x = opts.x - (dialogWidth - RED.view.node_width) | ||||
|         } | ||||
|         dialog.css({left:opts.x+"px",top:opts.y+"px"}).show(); | ||||
|         searchResultsDiv.slideDown(300); | ||||
|         setTimeout(function() { | ||||
| @@ -330,13 +335,25 @@ RED.typeSearch = (function() { | ||||
|         } | ||||
|     } | ||||
|     function applyFilter(filter,type,def) { | ||||
|         return !def || !filter || | ||||
|             ( | ||||
|                 (!filter.spliceMultiple) && | ||||
|                 (!filter.type || type === filter.type) && | ||||
|                 (!filter.input || type === 'junction' || def.inputs > 0) && | ||||
|                 (!filter.output || type === 'junction' || def.outputs > 0) | ||||
|             ) | ||||
|         if (!filter) { | ||||
|             // No filter; allow everything | ||||
|             return true | ||||
|         } | ||||
|         if (type === 'junction') { | ||||
|             // Only allow Junction is there's no specific type filter | ||||
|             return !filter.type | ||||
|         } | ||||
|         if (filter.type) { | ||||
|             // Handle explicit type filter | ||||
|             return filter.type === type | ||||
|         } | ||||
|         if (!def) { | ||||
|             // No node definition available - allow it | ||||
|             return true | ||||
|         } | ||||
|         // Check if the filter is for input/outputs and apply | ||||
|         return (!filter.input || def.inputs > 0) && | ||||
|                 (!filter.output || def.outputs > 0) | ||||
|     } | ||||
|     function refreshTypeList(opts) { | ||||
|         var i; | ||||
| @@ -365,7 +382,7 @@ RED.typeSearch = (function() { | ||||
|         var items = []; | ||||
|         RED.nodes.registry.getNodeTypes().forEach(function(t) { | ||||
|             var def = RED.nodes.getType(t); | ||||
|             if (def.category !== 'config' && t !== 'unknown' && t !== 'tab') { | ||||
|             if (def.set?.enabled !== false && def.category !== 'config' && t !== 'unknown' && t !== 'tab') { | ||||
|                 items.push({type:t,def: def, label:getTypeLabel(t,def)}); | ||||
|             } | ||||
|         }); | ||||
|   | ||||
| @@ -483,6 +483,16 @@ RED.utils = (function() { | ||||
|                 $('<span class="red-ui-debug-msg-type-string-swatch"></span>').css('backgroundColor',obj).appendTo(e); | ||||
|             } | ||||
|  | ||||
|             let n = RED.nodes.node(obj) ?? RED.nodes.workspace(obj); | ||||
|             if (n) { | ||||
|                 if (options.nodeSelector && "function" == typeof options.nodeSelector) { | ||||
|                     e.css('cursor', 'pointer').on("click", function(evt) { | ||||
|                         evt.preventDefault(); | ||||
|                         options.nodeSelector(n.id); | ||||
|                     }) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|         } else if (typeof obj === 'number') { | ||||
|             e = $('<span class="red-ui-debug-msg-type-number"></span>').appendTo(entryObj); | ||||
|  | ||||
| @@ -589,6 +599,7 @@ RED.utils = (function() { | ||||
|                                     exposeApi: exposeApi, | ||||
|                                     // tools: tools // Do not pass tools down as we | ||||
|                                                     // keep them attached to the top-level header | ||||
|                                     nodeSelector: options.nodeSelector, | ||||
|                                 } | ||||
|                             ).appendTo(row); | ||||
|                         } | ||||
| @@ -619,6 +630,7 @@ RED.utils = (function() { | ||||
|                                                 exposeApi: exposeApi, | ||||
|                                                 // tools: tools // Do not pass tools down as we | ||||
|                                                                 // keep them attached to the top-level header | ||||
|                                                 nodeSelector: options.nodeSelector, | ||||
|                                             } | ||||
|                                         ).appendTo(row); | ||||
|                                     } | ||||
| @@ -675,6 +687,7 @@ RED.utils = (function() { | ||||
|                                 exposeApi: exposeApi, | ||||
|                                 // tools: tools // Do not pass tools down as we | ||||
|                                                 // keep them attached to the top-level header | ||||
|                                 nodeSelector: options.nodeSelector, | ||||
|                             } | ||||
|                         ).appendTo(row); | ||||
|                     } | ||||
| @@ -888,11 +901,25 @@ RED.utils = (function() { | ||||
|         return parts; | ||||
|     } | ||||
|  | ||||
|     function validatePropertyExpression(str) { | ||||
|     /** | ||||
|      * Validate a property expression | ||||
|      * @param {*} str - the property value | ||||
|      * @returns {boolean|string} whether the node proprty is valid. `true`: valid `false|String`: invalid | ||||
|      */ | ||||
|     function validatePropertyExpression(str, opt) { | ||||
|         try { | ||||
|             var parts = normalisePropertyExpression(str); | ||||
|             const parts = normalisePropertyExpression(str); | ||||
|             return true; | ||||
|         } catch(err) { | ||||
|             // If the validator has opt, it is a 3.x validator that | ||||
|             // can return a String to mean 'invalid' and provide a reason | ||||
|             if (opt) { | ||||
|                 if (opt.label) { | ||||
|                     return opt.label + ': ' + err.message; | ||||
|                 } | ||||
|                 return err.message; | ||||
|             } | ||||
|             // Otherwise, a 2.x returns a false value | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
| @@ -910,22 +937,24 @@ RED.utils = (function() { | ||||
|             // Allow ${ENV_VAR} value | ||||
|             return true | ||||
|         } | ||||
|         let error | ||||
|         let error; | ||||
|         if (propertyType === 'json') { | ||||
|             try { | ||||
|                 JSON.parse(propertyValue); | ||||
|             } catch(err) { | ||||
|                 error = RED._("validator.errors.invalid-json", { | ||||
|                     error: err.message | ||||
|                 }) | ||||
|                 }); | ||||
|             } | ||||
|         } else if (propertyType === 'msg' || propertyType === 'flow' || propertyType === 'global' ) { | ||||
|             if (!RED.utils.validatePropertyExpression(propertyValue)) { | ||||
|                 error = RED._("validator.errors.invalid-prop") | ||||
|             // To avoid double label | ||||
|             const valid = RED.utils.validatePropertyExpression(propertyValue, opt ? {} : null); | ||||
|             if (valid !== true) { | ||||
|                 error = opt ? valid : RED._("validator.errors.invalid-prop"); | ||||
|             } | ||||
|         } else if (propertyType === 'num') { | ||||
|             if (!/^NaN$|^[+-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$|^[+-]?(0b|0B)[01]+$|^[+-]?(0o|0O)[0-7]+$|^[+-]?(0x|0X)[0-9a-fA-F]+$/.test(propertyValue)) { | ||||
|                 error = RED._("validator.errors.invalid-num") | ||||
|                 error = RED._("validator.errors.invalid-num"); | ||||
|             } | ||||
|         } else if (propertyType === 'jsonata') { | ||||
|             try {  | ||||
| @@ -933,16 +962,16 @@ RED.utils = (function() { | ||||
|             } catch(err) { | ||||
|                 error = RED._("validator.errors.invalid-expr", { | ||||
|                     error: err.message | ||||
|                 }) | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|         if (error) { | ||||
|             if (opt && opt.label) { | ||||
|                 return opt.label+': '+error | ||||
|                 return opt.label + ': ' + error; | ||||
|             } | ||||
|             return error | ||||
|             return error; | ||||
|         } | ||||
|         return true | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     function getMessageProperty(msg,expr) { | ||||
|   | ||||
| @@ -9,14 +9,27 @@ RED.view.annotations = (function() { | ||||
|                     addAnnotation(evt.node.__pendingAnnotation__,evt); | ||||
|                     delete evt.node.__pendingAnnotation__; | ||||
|                 } | ||||
|                 var badgeDX = 0; | ||||
|                 var controlDX = 0; | ||||
|                 for (var i=0,l=evt.el.__annotations__.length;i<l;i++) { | ||||
|                     var annotation = evt.el.__annotations__[i]; | ||||
|                 let badgeRDX = 0; | ||||
|                 let badgeLDX = 0; | ||||
|                 const scale = RED.view.scale() | ||||
|                 for (let i=0,l=evt.el.__annotations__.length;i<l;i++) { | ||||
|                     const annotation = evt.el.__annotations__[i]; | ||||
|                     if (annotations.hasOwnProperty(annotation.id)) { | ||||
|                         var opts = annotations[annotation.id]; | ||||
|                         var showAnnotation = true; | ||||
|                         var isBadge = opts.type === 'badge'; | ||||
|                         const opts = annotations[annotation.id]; | ||||
|                         let showAnnotation = true; | ||||
|                         const isBadge = opts.type === 'badge'; | ||||
|                         if (opts.refresh !== undefined) { | ||||
|                             let refreshAnnotation = false | ||||
|                             if (typeof opts.refresh === "string") { | ||||
|                                 refreshAnnotation = !!evt.node[opts.refresh] | ||||
|                                 delete evt.node[opts.refresh] | ||||
|                             } else if (typeof opts.refresh === "function") { | ||||
|                                 refreshAnnotation = opts.refresh(evnt.node) | ||||
|                             } | ||||
|                             if (refreshAnnotation) { | ||||
|                                 refreshAnnotationElement(annotation.id, annotation.node, annotation.element) | ||||
|                             } | ||||
|                         } | ||||
|                         if (opts.show !== undefined) { | ||||
|                             if (typeof opts.show === "string") { | ||||
|                                 showAnnotation = !!evt.node[opts.show] | ||||
| @@ -29,17 +42,26 @@ RED.view.annotations = (function() { | ||||
|                         } | ||||
|                         if (isBadge) { | ||||
|                             if (showAnnotation) { | ||||
|                                 var rect = annotation.element.getBoundingClientRect(); | ||||
|                                 badgeDX += rect.width; | ||||
|                                 annotation.element.setAttribute("transform", "translate("+(evt.node.w-3-badgeDX)+", -8)"); | ||||
|                                 badgeDX += 4; | ||||
|                             } | ||||
|                         } else { | ||||
|                             if (showAnnotation) { | ||||
|                                 var rect = annotation.element.getBoundingClientRect(); | ||||
|                                 annotation.element.setAttribute("transform", "translate("+(3+controlDX)+", -12)"); | ||||
|                                 controlDX += rect.width + 4; | ||||
|                                 // getBoundingClientRect is in real-world scale so needs to be adjusted according to | ||||
|                                 // the current scale factor | ||||
|                                 const rectWidth = annotation.element.getBoundingClientRect().width / scale; | ||||
|                                 let annotationX | ||||
|                                 if (!opts.align || opts.align === 'right') { | ||||
|                                     annotationX = evt.node.w - 3 - badgeRDX - rectWidth | ||||
|                                     badgeRDX += rectWidth + 4; | ||||
|  | ||||
|                                 } else if (opts.align === 'left') { | ||||
|                                     annotationX = 3 + badgeLDX | ||||
|                                     badgeLDX += rectWidth + 4; | ||||
|                                 } | ||||
|                                 annotation.element.setAttribute("transform", "translate("+annotationX+", -8)"); | ||||
|                             } | ||||
|                         // } else { | ||||
|                         //     if (showAnnotation) { | ||||
|                         //         var rect = annotation.element.getBoundingClientRect(); | ||||
|                         //         annotation.element.setAttribute("transform", "translate("+(3+controlDX)+", -12)"); | ||||
|                         //         controlDX += rect.width + 4; | ||||
|                         //     } | ||||
|                         } | ||||
|                     } else { | ||||
|                         annotation.element.parentNode.removeChild(annotation.element); | ||||
| @@ -95,15 +117,25 @@ RED.view.annotations = (function() { | ||||
|         annotationGroup.setAttribute("class",opts.class || ""); | ||||
|         evt.el.__annotations__.push({ | ||||
|             id:id, | ||||
|             node: evt.node, | ||||
|             element: annotationGroup | ||||
|         }); | ||||
|         var annotation = opts.element(evt.node); | ||||
|         refreshAnnotationElement(id, evt.node, annotationGroup) | ||||
|         evt.el.appendChild(annotationGroup); | ||||
|     } | ||||
|  | ||||
|     function refreshAnnotationElement(id, node, annotationGroup) { | ||||
|         const opts = annotations[id]; | ||||
|         const annotation = opts.element(node); | ||||
|         if (opts.tooltip) { | ||||
|             annotation.addEventListener("mouseenter", getAnnotationMouseEnter(annotation,evt.node,opts.tooltip)); | ||||
|             annotation.addEventListener("mouseenter", getAnnotationMouseEnter(annotation, node, opts.tooltip)); | ||||
|             annotation.addEventListener("mouseleave", annotationMouseLeave); | ||||
|         } | ||||
|         if (annotationGroup.hasChildNodes()) { | ||||
|             annotationGroup.removeChild(annotationGroup.firstChild) | ||||
|         } | ||||
|         annotationGroup.appendChild(annotation); | ||||
|         evt.el.appendChild(annotationGroup); | ||||
|  | ||||
|     } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1102,18 +1102,27 @@ RED.view.tools = (function() { | ||||
|                     const paletteLabel = RED.utils.getPaletteLabel(n.type, nodeDef) | ||||
|                     const defaultNodeNameRE = new RegExp('^'+paletteLabel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')+' (\\d+)$') | ||||
|                     if (!typeIndex.hasOwnProperty(n.type)) { | ||||
|                         const existingNodes = RED.nodes.filterNodes({type: n.type}) | ||||
|                         let maxNameNumber = 0; | ||||
|                         existingNodes.forEach(n => { | ||||
|                             let match = defaultNodeNameRE.exec(n.name) | ||||
|                         const existingNodes = RED.nodes.filterNodes({ type: n.type }); | ||||
|                         const existingIds = existingNodes.reduce((ids, node) => { | ||||
|                             let match = defaultNodeNameRE.exec(node.name); | ||||
|                             if (match) { | ||||
|                                 let nodeNumber = parseInt(match[1]) | ||||
|                                 if (nodeNumber > maxNameNumber) { | ||||
|                                     maxNameNumber = nodeNumber | ||||
|                                 const nodeNumber = parseInt(match[1], 10); | ||||
|                                 if (!ids.includes(nodeNumber)) { | ||||
|                                     ids.push(nodeNumber); | ||||
|                                 } | ||||
|                             } | ||||
|                         }) | ||||
|                         typeIndex[n.type] = maxNameNumber + 1 | ||||
|                             return ids; | ||||
|                         }, []).sort((a, b) => a - b); | ||||
|  | ||||
|                         let availableNameNumber = 1; | ||||
|                         for (let i = 0; i < existingIds.length; i++) { | ||||
|                             if (existingIds[i] !== availableNameNumber) { | ||||
|                                 break; | ||||
|                             } | ||||
|                             availableNameNumber++; | ||||
|                         } | ||||
|  | ||||
|                         typeIndex[n.type] = availableNameNumber; | ||||
|                     } | ||||
|                     if ((options.renameBlank && n.name === '') || (options.renameClash && defaultNodeNameRE.test(n.name))) { | ||||
|                         if (generateHistory) { | ||||
| @@ -1145,11 +1154,11 @@ RED.view.tools = (function() { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function addJunctionsToWires(wires) { | ||||
|     function addJunctionsToWires(options = {}) { | ||||
|         if (RED.workspaces.isLocked()) { | ||||
|             return | ||||
|         } | ||||
|         let wiresToSplit = wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link)); | ||||
|         let wiresToSplit = options.wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link)); | ||||
|         if (!wiresToSplit) { | ||||
|             return | ||||
|         } | ||||
| @@ -1197,21 +1206,26 @@ RED.view.tools = (function() { | ||||
|             if (links.length === 0) { | ||||
|                 return | ||||
|             } | ||||
|             let pointCount = 0 | ||||
|             links.forEach(function(l) { | ||||
|                 if (l._sliceLocation) { | ||||
|                     junction.x += l._sliceLocation.x | ||||
|                     junction.y += l._sliceLocation.y | ||||
|                     delete l._sliceLocation | ||||
|                     pointCount++ | ||||
|                 } else { | ||||
|                     junction.x += l.source.x + l.source.w/2 + l.target.x - l.target.w/2 | ||||
|                     junction.y += l.source.y + l.target.y | ||||
|                     pointCount += 2 | ||||
|                 } | ||||
|             }) | ||||
|             junction.x = Math.round(junction.x/pointCount) | ||||
|             junction.y = Math.round(junction.y/pointCount) | ||||
|             if (addedJunctions.length === 0 && Object.hasOwn(options, 'x') && Object.hasOwn(options, 'y')) { | ||||
|                 junction.x = options.x | ||||
|                 junction.y = options.y | ||||
|             } else { | ||||
|                 let pointCount = 0 | ||||
|                 links.forEach(function(l) { | ||||
|                     if (l._sliceLocation) { | ||||
|                         junction.x += l._sliceLocation.x | ||||
|                         junction.y += l._sliceLocation.y | ||||
|                         delete l._sliceLocation | ||||
|                         pointCount++ | ||||
|                     } else { | ||||
|                         junction.x += l.source.x + l.source.w/2 + l.target.x - l.target.w/2 | ||||
|                         junction.y += l.source.y + l.target.y | ||||
|                         pointCount += 2 | ||||
|                     } | ||||
|                 }) | ||||
|                 junction.x = Math.round(junction.x/pointCount) | ||||
|                 junction.y = Math.round(junction.y/pointCount) | ||||
|             } | ||||
|             if (RED.view.snapGrid) { | ||||
|                 let gridSize = RED.view.gridSize() | ||||
|                 junction.x = (gridSize*Math.round(junction.x/gridSize)); | ||||
| @@ -1401,7 +1415,7 @@ RED.view.tools = (function() { | ||||
|             RED.actions.add("core:wire-multiple-to-node", function() { wireMultipleToNode() }) | ||||
|  | ||||
|             RED.actions.add("core:split-wire-with-link-nodes", function () { splitWiresWithLinkNodes() }); | ||||
|             RED.actions.add("core:split-wires-with-junctions", function () { addJunctionsToWires() }); | ||||
|             RED.actions.add("core:split-wires-with-junctions", function (options) { addJunctionsToWires(options) }); | ||||
|  | ||||
|             RED.actions.add("core:generate-node-names", generateNodeNames ) | ||||
|  | ||||
|   | ||||
| @@ -288,7 +288,7 @@ RED.view = (function() { | ||||
|                 } | ||||
|                 selectedLinks.clearUnselected() | ||||
|             }, | ||||
|             length: () => groups.length, | ||||
|             length: () => groups.size, | ||||
|             forEach: (func) => { groups.forEach(func) }, | ||||
|             toArray: () => [...groups], | ||||
|             clear: function () { | ||||
| @@ -321,8 +321,8 @@ RED.view = (function() { | ||||
|             evt.stopPropagation() | ||||
|             RED.contextMenu.show({ | ||||
|                 type: 'workspace', | ||||
|                 x:evt.clientX-5, | ||||
|                 y:evt.clientY-5 | ||||
|                 x: evt.clientX, | ||||
|                 y: evt.clientY | ||||
|             }) | ||||
|             return false | ||||
|         }) | ||||
| @@ -646,120 +646,128 @@ RED.view = (function() { | ||||
|                 } | ||||
|                 d3.event = event; | ||||
|                 var selected_tool = $(ui.draggable[0]).attr("data-palette-type"); | ||||
|                 var result = createNode(selected_tool); | ||||
|                 if (!result) { | ||||
|                     return; | ||||
|                 } | ||||
|                 var historyEvent = result.historyEvent; | ||||
|                 var nn = RED.nodes.add(result.node); | ||||
|  | ||||
|                 var showLabel = RED.utils.getMessageProperty(RED.settings.get('editor'),"view.view-node-show-label"); | ||||
|                 if (showLabel !== undefined &&  (nn._def.hasOwnProperty("showLabel")?nn._def.showLabel:true) && !nn._def.defaults.hasOwnProperty("l")) { | ||||
|                     nn.l = showLabel; | ||||
|                 } | ||||
|  | ||||
|                 var helperOffset = d3.touches(ui.helper.get(0))[0]||d3.mouse(ui.helper.get(0)); | ||||
|                 var helperWidth = ui.helper.width(); | ||||
|                 var helperHeight = ui.helper.height(); | ||||
|                 var mousePos = d3.touches(this)[0]||d3.mouse(this); | ||||
|  | ||||
|                 try { | ||||
|                     var isLink = (nn.type === "link in" || nn.type === "link out") | ||||
|                     var hideLabel = nn.hasOwnProperty('l')?!nn.l : isLink; | ||||
|  | ||||
|                     var label = RED.utils.getNodeLabel(nn, nn.type); | ||||
|                     var labelParts = getLabelParts(label, "red-ui-flow-node-label"); | ||||
|                     if (hideLabel) { | ||||
|                         nn.w = node_height; | ||||
|                         nn.h = Math.max(node_height,(nn.outputs || 0) * 15); | ||||
|                     } else { | ||||
|                         nn.w = Math.max(node_width,20*(Math.ceil((labelParts.width+50+(nn._def.inputs>0?7:0))/20)) ); | ||||
|                         nn.h = Math.max(6+24*labelParts.lines.length,(nn.outputs || 0) * 15, 30); | ||||
|                     var result = createNode(selected_tool); | ||||
|                     if (!result) { | ||||
|                         return; | ||||
|                     } | ||||
|                 } catch(err) { | ||||
|                 } | ||||
|                     var historyEvent = result.historyEvent; | ||||
|                     var nn = RED.nodes.add(result.node); | ||||
|  | ||||
|                 mousePos[1] += this.scrollTop + ((helperHeight/2)-helperOffset[1]); | ||||
|                 mousePos[0] += this.scrollLeft + ((helperWidth/2)-helperOffset[0]); | ||||
|                 mousePos[1] /= scaleFactor; | ||||
|                 mousePos[0] /= scaleFactor; | ||||
|                     var showLabel = RED.utils.getMessageProperty(RED.settings.get('editor'),"view.view-node-show-label"); | ||||
|                     if (showLabel !== undefined &&  (nn._def.hasOwnProperty("showLabel")?nn._def.showLabel:true) && !nn._def.defaults.hasOwnProperty("l")) { | ||||
|                         nn.l = showLabel; | ||||
|                     } | ||||
|  | ||||
|                 nn.x = mousePos[0]; | ||||
|                 nn.y = mousePos[1]; | ||||
|                     var helperOffset = d3.touches(ui.helper.get(0))[0]||d3.mouse(ui.helper.get(0)); | ||||
|                     var helperWidth = ui.helper.width(); | ||||
|                     var helperHeight = ui.helper.height(); | ||||
|                     var mousePos = d3.touches(this)[0]||d3.mouse(this); | ||||
|  | ||||
|                 var minX = nn.w/2 -5; | ||||
|                 if (nn.x < minX) { | ||||
|                     nn.x = minX; | ||||
|                 } | ||||
|                 var minY = nn.h/2 -5; | ||||
|                 if (nn.y < minY) { | ||||
|                     nn.y = minY; | ||||
|                 } | ||||
|                 var maxX = space_width -nn.w/2 +5; | ||||
|                 if (nn.x > maxX) { | ||||
|                     nn.x = maxX; | ||||
|                 } | ||||
|                 var maxY = space_height -nn.h +5; | ||||
|                 if (nn.y > maxY) { | ||||
|                     nn.y = maxY; | ||||
|                 } | ||||
|                     try { | ||||
|                         var isLink = (nn.type === "link in" || nn.type === "link out") | ||||
|                         var hideLabel = nn.hasOwnProperty('l')?!nn.l : isLink; | ||||
|  | ||||
|                 if (snapGrid) { | ||||
|                     var gridOffset = RED.view.tools.calculateGridSnapOffsets(nn); | ||||
|                     nn.x -= gridOffset.x; | ||||
|                     nn.y -= gridOffset.y; | ||||
|                 } | ||||
|                         var label = RED.utils.getNodeLabel(nn, nn.type); | ||||
|                         var labelParts = getLabelParts(label, "red-ui-flow-node-label"); | ||||
|                         if (hideLabel) { | ||||
|                             nn.w = node_height; | ||||
|                             nn.h = Math.max(node_height,(nn.outputs || 0) * 15); | ||||
|                         } else { | ||||
|                             nn.w = Math.max(node_width,20*(Math.ceil((labelParts.width+50+(nn._def.inputs>0?7:0))/20)) ); | ||||
|                             nn.h = Math.max(6+24*labelParts.lines.length,(nn.outputs || 0) * 15, 30); | ||||
|                         } | ||||
|                     } catch(err) { | ||||
|                     } | ||||
|  | ||||
|                 var linkToSplice = $(ui.helper).data("splice"); | ||||
|                 if (linkToSplice) { | ||||
|                     spliceLink(linkToSplice, nn, historyEvent) | ||||
|                 } | ||||
|                     mousePos[1] += this.scrollTop + ((helperHeight/2)-helperOffset[1]); | ||||
|                     mousePos[0] += this.scrollLeft + ((helperWidth/2)-helperOffset[0]); | ||||
|                     mousePos[1] /= scaleFactor; | ||||
|                     mousePos[0] /= scaleFactor; | ||||
|  | ||||
|                     nn.x = mousePos[0]; | ||||
|                     nn.y = mousePos[1]; | ||||
|  | ||||
|                     var minX = nn.w/2 -5; | ||||
|                     if (nn.x < minX) { | ||||
|                         nn.x = minX; | ||||
|                     } | ||||
|                     var minY = nn.h/2 -5; | ||||
|                     if (nn.y < minY) { | ||||
|                         nn.y = minY; | ||||
|                     } | ||||
|                     var maxX = space_width -nn.w/2 +5; | ||||
|                     if (nn.x > maxX) { | ||||
|                         nn.x = maxX; | ||||
|                     } | ||||
|                     var maxY = space_height -nn.h +5; | ||||
|                     if (nn.y > maxY) { | ||||
|                         nn.y = maxY; | ||||
|                     } | ||||
|  | ||||
|                     if (snapGrid) { | ||||
|                         var gridOffset = RED.view.tools.calculateGridSnapOffsets(nn); | ||||
|                         nn.x -= gridOffset.x; | ||||
|                         nn.y -= gridOffset.y; | ||||
|                     } | ||||
|  | ||||
|                     var linkToSplice = $(ui.helper).data("splice"); | ||||
|                     if (linkToSplice) { | ||||
|                         spliceLink(linkToSplice, nn, historyEvent) | ||||
|                     } | ||||
|  | ||||
|                     var group = $(ui.helper).data("group"); | ||||
|                     if (group) { | ||||
|                         var oldX = group.x;  | ||||
|                         var oldY = group.y;  | ||||
|                         RED.group.addToGroup(group, nn); | ||||
|                         var moveEvent = null; | ||||
|                         if ((group.x !== oldX) || | ||||
|                             (group.y !== oldY)) { | ||||
|                             moveEvent = { | ||||
|                                 t: "move", | ||||
|                                 nodes: [{n: group, | ||||
|                                         ox: oldX, oy: oldY, | ||||
|                                         dx: group.x -oldX, | ||||
|                                         dy: group.y -oldY}], | ||||
|                                 dirty: true | ||||
|                             }; | ||||
|                         } | ||||
|                         historyEvent = { | ||||
|                             t: 'multi', | ||||
|                             events: [historyEvent], | ||||
|  | ||||
|                 var group = $(ui.helper).data("group"); | ||||
|                 if (group) { | ||||
|                     var oldX = group.x;  | ||||
|                     var oldY = group.y;  | ||||
|                     RED.group.addToGroup(group, nn); | ||||
|                     var moveEvent = null; | ||||
|                     if ((group.x !== oldX) || | ||||
|                         (group.y !== oldY)) { | ||||
|                         moveEvent = { | ||||
|                             t: "move", | ||||
|                             nodes: [{n: group, | ||||
|                                      ox: oldX, oy: oldY, | ||||
|                                      dx: group.x -oldX, | ||||
|                                      dy: group.y -oldY}], | ||||
|                             dirty: true | ||||
|                         }; | ||||
|                         if (moveEvent) { | ||||
|                             historyEvent.events.push(moveEvent) | ||||
|                         } | ||||
|                         historyEvent.events.push({ | ||||
|                             t: "addToGroup", | ||||
|                             group: group, | ||||
|                             nodes: nn | ||||
|                         }) | ||||
|                     } | ||||
|                     historyEvent = { | ||||
|                         t: 'multi', | ||||
|                         events: [historyEvent], | ||||
|  | ||||
|                     }; | ||||
|                     if (moveEvent) { | ||||
|                         historyEvent.events.push(moveEvent) | ||||
|                     RED.history.push(historyEvent); | ||||
|                     RED.editor.validateNode(nn); | ||||
|                     RED.nodes.dirty(true); | ||||
|                     // auto select dropped node - so info shows (if visible) | ||||
|                     clearSelection(); | ||||
|                     nn.selected = true; | ||||
|                     movingSet.add(nn); | ||||
|                     updateActiveNodes(); | ||||
|                     updateSelection(); | ||||
|                     redraw(); | ||||
|  | ||||
|                     if (nn._def.autoedit) { | ||||
|                         RED.editor.edit(nn); | ||||
|                     } | ||||
|                 } catch (error) { | ||||
|                     if (error.code != "NODE_RED") { | ||||
|                         RED.notify(RED._("notification.error",{message:error.toString()}),"error"); | ||||
|                     } else { | ||||
|                         RED.notify(RED._("notification.error",{message:error.message}),"error"); | ||||
|                     } | ||||
|                     historyEvent.events.push({ | ||||
|                         t: "addToGroup", | ||||
|                         group: group, | ||||
|                         nodes: nn | ||||
|                     }) | ||||
|                 } | ||||
|  | ||||
|                 RED.history.push(historyEvent); | ||||
|                 RED.editor.validateNode(nn); | ||||
|                 RED.nodes.dirty(true); | ||||
|                 // auto select dropped node - so info shows (if visible) | ||||
|                 clearSelection(); | ||||
|                 nn.selected = true; | ||||
|                 movingSet.add(nn); | ||||
|                 updateActiveNodes(); | ||||
|                 updateSelection(); | ||||
|                 redraw(); | ||||
|  | ||||
|                 if (nn._def.autoedit) { | ||||
|                     RED.editor.edit(nn); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
| @@ -1182,6 +1190,7 @@ RED.view = (function() { | ||||
|  | ||||
|         if (d3.event.button === 1) { | ||||
|             // Middle Click pan | ||||
|             d3.event.preventDefault(); | ||||
|             mouse_mode = RED.state.PANNING; | ||||
|             mouse_position = [d3.event.pageX,d3.event.pageY] | ||||
|             scroll_position = [chart.scrollLeft(),chart.scrollTop()]; | ||||
| @@ -1200,7 +1209,10 @@ RED.view = (function() { | ||||
|             lasso = null; | ||||
|         } | ||||
|         if (d3.event.touches || d3.event.button === 0) { | ||||
|             if ((mouse_mode === 0 || mouse_mode === RED.state.QUICK_JOINING) && isControlPressed(d3.event) && !(d3.event.altKey || d3.event.shiftKey)) { | ||||
|             if ( | ||||
|                 (mouse_mode === 0 && isControlPressed(d3.event) && !(d3.event.altKey || d3.event.shiftKey)) || | ||||
|                 mouse_mode === RED.state.QUICK_JOINING | ||||
|             ) { | ||||
|                 // Trigger quick add dialog | ||||
|                 d3.event.stopPropagation(); | ||||
|                 clearSelection(); | ||||
| @@ -1276,7 +1288,6 @@ RED.view = (function() { | ||||
|         } | ||||
|  | ||||
|         var mainPos = $("#red-ui-main-container").position(); | ||||
|  | ||||
|         if (mouse_mode !== RED.state.QUICK_JOINING) { | ||||
|             mouse_mode = RED.state.QUICK_JOINING; | ||||
|             $(window).on('keyup',disableQuickJoinEventHandler); | ||||
| @@ -2678,22 +2689,21 @@ RED.view = (function() { | ||||
|                 addToRemovedLinks(reconnectResult.removedLinks) | ||||
|             } | ||||
|  | ||||
|             var startDirty = RED.nodes.dirty(); | ||||
|             var startChanged = false; | ||||
|             var selectedGroups = []; | ||||
|             const startDirty = RED.nodes.dirty(); | ||||
|             let movingSelectedGroups = []; | ||||
|             if (movingSet.length() > 0) { | ||||
|  | ||||
|                 for (var i=0;i<movingSet.length();i++) { | ||||
|                     node = movingSet.get(i).n; | ||||
|                     if (node.type === "group") { | ||||
|                         selectedGroups.push(node); | ||||
|                         movingSelectedGroups.push(node); | ||||
|                     } | ||||
|                 } | ||||
|                 // Make sure we have identified all groups about to be deleted | ||||
|                 for (i=0;i<selectedGroups.length;i++) { | ||||
|                     selectedGroups[i].nodes.forEach(function(n) { | ||||
|                         if (n.type === "group" && selectedGroups.indexOf(n) === -1) { | ||||
|                             selectedGroups.push(n); | ||||
|                 for (i=0;i<movingSelectedGroups.length;i++) { | ||||
|                     movingSelectedGroups[i].nodes.forEach(function(n) { | ||||
|                         if (n.type === "group" && movingSelectedGroups.indexOf(n) === -1) { | ||||
|                             movingSelectedGroups.push(n); | ||||
|                         } | ||||
|                     }) | ||||
|                 } | ||||
| @@ -2710,7 +2720,7 @@ RED.view = (function() { | ||||
|                         addToRemovedLinks(removedEntities.links); | ||||
|                         if (node.g) { | ||||
|                             var group = RED.nodes.group(node.g); | ||||
|                             if (selectedGroups.indexOf(group) === -1) { | ||||
|                             if (movingSelectedGroups.indexOf(group) === -1) { | ||||
|                                 // Don't use RED.group.removeFromGroup as that emits | ||||
|                                 // a change event on the node - but we're deleting it | ||||
|                                 var index = group.nodes.indexOf(node); | ||||
| @@ -2724,7 +2734,7 @@ RED.view = (function() { | ||||
|                         removedLinks = removedLinks.concat(result.links); | ||||
|                         if (node.g) { | ||||
|                             var group = RED.nodes.group(node.g); | ||||
|                             if (selectedGroups.indexOf(group) === -1) { | ||||
|                             if (movingSelectedGroups.indexOf(group) === -1) { | ||||
|                                 // Don't use RED.group.removeFromGroup as that emits | ||||
|                                 // a change event on the node - but we're deleting it | ||||
|                                 var index = group.nodes.indexOf(node); | ||||
| @@ -2746,8 +2756,8 @@ RED.view = (function() { | ||||
|  | ||||
|                 // Groups must be removed in the right order - from inner-most | ||||
|                 // to outermost. | ||||
|                 for (i = selectedGroups.length-1; i>=0; i--) { | ||||
|                     var g = selectedGroups[i]; | ||||
|                 for (i = movingSelectedGroups.length-1; i>=0; i--) { | ||||
|                     var g = movingSelectedGroups[i]; | ||||
|                     removedGroups.push(g); | ||||
|                     RED.nodes.removeGroup(g); | ||||
|                 } | ||||
| @@ -3048,8 +3058,8 @@ RED.view = (function() { | ||||
|     } | ||||
|  | ||||
|     function disableQuickJoinEventHandler(evt) { | ||||
|         // Check for ctrl (all browsers), "Meta" (Chrome/FF), keyCode 91 (Safari) | ||||
|         if (evt.keyCode === 17 || evt.key === "Meta" || evt.keyCode === 91) { | ||||
|         // Check for ctrl (all browsers), "Meta" (Chrome/FF), keyCode 91 (Safari), or Escape | ||||
|         if (evt.keyCode === 17 || evt.key === "Meta" || evt.keyCode === 91 || evt.keyCode === 27) { | ||||
|             resetMouseVars(); | ||||
|             hideDragLines(); | ||||
|             redraw(); | ||||
| @@ -3180,27 +3190,59 @@ RED.view = (function() { | ||||
|  | ||||
|             for (i=0;i<drag_lines.length;i++) { | ||||
|                 if (portType != drag_lines[i].portType && mouseup_node !== drag_lines[i].node) { | ||||
|                     var drag_line = drag_lines[i]; | ||||
|                     var src,dst,src_port; | ||||
|                     let drag_line = drag_lines[i]; | ||||
|                     let src,dst,src_port; | ||||
|                     let oldDst; | ||||
|                     let oldSrc; | ||||
|                     if (drag_line.portType === PORT_TYPE_OUTPUT) { | ||||
|                         src = drag_line.node; | ||||
|                         src_port = drag_line.port; | ||||
|                         dst = mouseup_node; | ||||
|                         oldSrc = src; | ||||
|                         if (drag_line.link) { | ||||
|                             oldDst = drag_line.link.target; | ||||
|                         } | ||||
|                     } else if (drag_line.portType === PORT_TYPE_INPUT) { | ||||
|                         src = mouseup_node; | ||||
|                         dst = drag_line.node; | ||||
|                         src_port = portIndex || 0; | ||||
|                         oldSrc = dst; | ||||
|                         if (drag_line.link) { | ||||
|                             oldDst = drag_line.link.source | ||||
|                         } | ||||
|                     } | ||||
|                     var link = {source: src, sourcePort:src_port, target: dst}; | ||||
|                     if (drag_line.virtualLink) { | ||||
|                         if (/^link (in|out)$/.test(src.type) && /^link (in|out)$/.test(dst.type) && src.type !== dst.type) { | ||||
|                             if (src.links.indexOf(dst.id) === -1 && dst.links.indexOf(src.id) === -1) { | ||||
|                                 var oldSrcLinks = $.extend(true,{},{v:src.links}).v | ||||
|                                 var oldDstLinks = $.extend(true,{},{v:dst.links}).v | ||||
|                                 var oldSrcLinks = [...src.links] | ||||
|                                 var oldDstLinks = [...dst.links] | ||||
|  | ||||
|                                 src.links.push(dst.id); | ||||
|                                 dst.links.push(src.id); | ||||
|  | ||||
|                                 if (oldDst) { | ||||
|                                     src.links = src.links.filter(id => id !== oldDst.id) | ||||
|                                     dst.links = dst.links.filter(id => id !== oldDst.id) | ||||
|                                     var oldOldDstLinks = [...oldDst.links] | ||||
|                                     oldDst.links = oldDst.links.filter(id => id !== oldSrc.id) | ||||
|                                     oldDst.dirty = true; | ||||
|                                     modifiedNodes.push(oldDst); | ||||
|                                     linkEditEvents.push({ | ||||
|                                         t:'edit', | ||||
|                                         node: oldDst, | ||||
|                                         dirty: RED.nodes.dirty(), | ||||
|                                         changed: oldDst.changed, | ||||
|                                         changes: { | ||||
|                                             links:oldOldDstLinks | ||||
|                                         } | ||||
|                                     }); | ||||
|                                     oldDst.changed = true; | ||||
|                                 } | ||||
|  | ||||
|                                 src.dirty = true; | ||||
|                                 dst.dirty = true; | ||||
|  | ||||
|                                 modifiedNodes.push(src); | ||||
|                                 modifiedNodes.push(dst); | ||||
|  | ||||
| @@ -3228,6 +3270,7 @@ RED.view = (function() { | ||||
|                                         links:oldDstLinks | ||||
|                                     } | ||||
|                                 }); | ||||
|                                 | ||||
|                                 src.changed = true; | ||||
|                                 dst.changed = true; | ||||
|                             } | ||||
| @@ -5131,8 +5174,8 @@ RED.view = (function() { | ||||
|                                 var delta = Infinity; | ||||
|                                 for (var i = 0; i < lineLength; i++) { | ||||
|                                     var linePos = pathLine.getPointAtLength(i); | ||||
|                                     var posDeltaX = Math.abs(linePos.x-d3.event.offsetX) | ||||
|                                     var posDeltaY = Math.abs(linePos.y-d3.event.offsetY) | ||||
|                                     var posDeltaX = Math.abs(linePos.x-(d3.event.offsetX / scaleFactor)) | ||||
|                                     var posDeltaY = Math.abs(linePos.y-(d3.event.offsetY / scaleFactor)) | ||||
|                                     var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY | ||||
|                                     if (posDelta < delta) { | ||||
|                                         pos = linePos | ||||
| @@ -6063,14 +6106,19 @@ RED.view = (function() { | ||||
|      function createNode(type, x, y, z) { | ||||
|         const wasDirty = RED.nodes.dirty() | ||||
|         var m = /^subflow:(.+)$/.exec(type); | ||||
|         var activeSubflow = z ? RED.nodes.subflow(z) : null; | ||||
|         var activeSubflow = (z || RED.workspaces.active()) ? RED.nodes.subflow(z || RED.workspaces.active()) : null; | ||||
|  | ||||
|         if (activeSubflow && m) { | ||||
|             var subflowId = m[1]; | ||||
|             let err | ||||
|             if (subflowId === activeSubflow.id) { | ||||
|                 throw new Error(RED._("notification.error", { message: RED._("notification.errors.cannotAddSubflowToItself") })) | ||||
|                 err = new Error(RED._("notification.errors.cannotAddSubflowToItself")) | ||||
|             } else if (RED.nodes.subflowContains(m[1], activeSubflow.id)) { | ||||
|                 err = new Error(RED._("notification.errors.cannotAddCircularReference")) | ||||
|             } | ||||
|             if (RED.nodes.subflowContains(m[1], activeSubflow.id)) { | ||||
|                 throw new Error(RED._("notification.error", { message: RED._("notification.errors.cannotAddCircularReference") })) | ||||
|             if (err) { | ||||
|                 err.code = 'NODE_RED' | ||||
|                 throw err | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -6252,6 +6300,10 @@ RED.view = (function() { | ||||
|                             } | ||||
|                         }) | ||||
|                     } | ||||
|                     if (selection.links) { | ||||
|                         selectedLinks.clear(); | ||||
|                         selection.links.forEach(selectedLinks.add); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             updateSelection(); | ||||
|   | ||||
| @@ -183,25 +183,29 @@ RED.workspaces = (function() { | ||||
|             }, | ||||
|             null) | ||||
|         } | ||||
|         menuItems.push( | ||||
|             { | ||||
|                 id:"red-ui-tabs-menu-option-add-flow", | ||||
|                 label: RED._("workspace.addFlow"), | ||||
|                 onselect: "core:add-flow" | ||||
|             } | ||||
|         ) | ||||
|         if (isMenuButton || !!tab) { | ||||
|         if (RED.settings.theme("menu.menu-item-workspace-add", true)) { | ||||
|             menuItems.push( | ||||
|                 { | ||||
|                     id:"red-ui-tabs-menu-option-add-flow-right", | ||||
|                     label: RED._("workspace.addFlowToRight"), | ||||
|                     shortcut: RED.keyboard.getShortcut("core:add-flow-to-right"), | ||||
|                     onselect: function() { | ||||
|                         RED.actions.invoke("core:add-flow-to-right", tab) | ||||
|                     } | ||||
|                 }, | ||||
|                 null | ||||
|                     id:"red-ui-tabs-menu-option-add-flow", | ||||
|                     label: RED._("workspace.addFlow"), | ||||
|                     onselect: "core:add-flow" | ||||
|                 } | ||||
|             ) | ||||
|         } | ||||
|         if (isMenuButton || !!tab) { | ||||
|             if (RED.settings.theme("menu.menu-item-workspace-add", true)) { | ||||
|                 menuItems.push( | ||||
|                     { | ||||
|                         id:"red-ui-tabs-menu-option-add-flow-right", | ||||
|                         label: RED._("workspace.addFlowToRight"), | ||||
|                         shortcut: RED.keyboard.getShortcut("core:add-flow-to-right"), | ||||
|                         onselect: function() { | ||||
|                             RED.actions.invoke("core:add-flow-to-right", tab) | ||||
|                         } | ||||
|                     }, | ||||
|                     null | ||||
|                 ) | ||||
|             } | ||||
|             if (activeWorkspace && activeWorkspace.type === 'tab') { | ||||
|                 menuItems.push( | ||||
|                     isFlowDisabled ? { | ||||
| @@ -255,7 +259,9 @@ RED.workspaces = (function() { | ||||
|                 } | ||||
|             ) | ||||
|         } | ||||
|         menuItems.push(null) | ||||
|         if (menuItems.length > 0) { | ||||
|             menuItems.push(null) | ||||
|         } | ||||
|         if (isMenuButton || !!tab) { | ||||
|             menuItems.push( | ||||
|                 { | ||||
| @@ -299,19 +305,24 @@ RED.workspaces = (function() { | ||||
|             } | ||||
|         ) | ||||
|         if (tab) { | ||||
|             menuItems.push(null) | ||||
|  | ||||
|             if (RED.settings.theme("menu.menu-item-workspace-delete", true)) { | ||||
|                 menuItems.push( | ||||
|                     { | ||||
|                         label: RED._("common.label.delete"), | ||||
|                         onselect: function() { | ||||
|                             if (tab.type === 'tab') { | ||||
|                                 RED.workspaces.delete(tab) | ||||
|                             } else if (tab.type === 'subflow') { | ||||
|                                 RED.subflow.delete(tab.id) | ||||
|                             } | ||||
|                         }, | ||||
|                         disabled: isCurrentLocked || (workspaceTabCount === 1) | ||||
|                     } | ||||
|                 ) | ||||
|             } | ||||
|             menuItems.push( | ||||
|                 null, | ||||
|                 { | ||||
|                     label: RED._("common.label.delete"), | ||||
|                     onselect: function() { | ||||
|                         if (tab.type === 'tab') { | ||||
|                             RED.workspaces.delete(tab) | ||||
|                         } else if (tab.type === 'subflow') { | ||||
|                             RED.subflow.delete(tab.id) | ||||
|                         } | ||||
|                     }, | ||||
|                     disabled: isCurrentLocked || (workspaceTabCount === 1) | ||||
|                 }, | ||||
|                 { | ||||
|                     label: RED._("menu.label.export"), | ||||
|                     shortcut: RED.keyboard.getShortcut("core:show-export-dialog"), | ||||
| @@ -359,11 +370,17 @@ RED.workspaces = (function() { | ||||
|                 RED.sidebar.config.refresh(); | ||||
|                 RED.view.focus(); | ||||
|             }, | ||||
|             onclick: function(tab) { | ||||
|                 if (tab.id !== activeWorkspace) { | ||||
|                     addToViewStack(activeWorkspace); | ||||
|             onclick: function(tab, evt) { | ||||
|                 if(evt.which === 2) { | ||||
|                     evt.preventDefault(); | ||||
|                     evt.stopPropagation(); | ||||
|                     RED.actions.invoke("core:hide-flow", tab) | ||||
|                 } else { | ||||
|                     if (tab.id !== activeWorkspace) { | ||||
|                         addToViewStack(activeWorkspace); | ||||
|                     } | ||||
|                     RED.view.focus(); | ||||
|                 } | ||||
|                 RED.view.focus(); | ||||
|             }, | ||||
|             ondblclick: function(tab) { | ||||
|                 if (tab.type != "subflow") { | ||||
| @@ -401,6 +418,7 @@ RED.workspaces = (function() { | ||||
|                 if (tab.type === "tab") { | ||||
|                     workspaceTabCount--; | ||||
|                 } else { | ||||
|                     RED.events.emit("workspace:close",{workspace: tab.id}) | ||||
|                     hideStack.push(tab.id); | ||||
|                 } | ||||
|                 RED.menu.setDisabled("menu-item-workspace-delete",activeWorkspace === 0 || workspaceTabCount <= 1); | ||||
| @@ -461,7 +479,7 @@ RED.workspaces = (function() { | ||||
|             }, | ||||
|             minimumActiveTabWidth: 150, | ||||
|             scrollable: true, | ||||
|             addButton: "core:add-flow", | ||||
|             addButton: RED.settings.theme("menu.menu-item-workspace-add", true) ? "core:add-flow" : undefined, | ||||
|             addButtonCaption: RED._("workspace.addFlow"), | ||||
|             menu: function() { return getMenuItems(true) }, | ||||
|             contextmenu: function(tab) { return getMenuItems(false, tab) } | ||||
| @@ -491,6 +509,11 @@ RED.workspaces = (function() { | ||||
|         createWorkspaceTabs(); | ||||
|         RED.events.on("sidebar:resize",workspace_tabs.resize); | ||||
|  | ||||
|         RED.events.on("workspace:clear", () => { | ||||
|             // Reset the index used to generate new flow names | ||||
|             workspaceIndex = 0 | ||||
|         }) | ||||
|  | ||||
|         RED.actions.add("core:show-next-tab",function() { | ||||
|             var oldActive = activeWorkspace; | ||||
|             workspace_tabs.nextTab(); | ||||
| @@ -513,19 +536,24 @@ RED.workspaces = (function() { | ||||
|         $(window).on("resize", function() { | ||||
|             workspace_tabs.resize(); | ||||
|         }); | ||||
|  | ||||
|         RED.actions.add("core:add-flow",function(opts) { addWorkspace(undefined,undefined,opts?opts.index:undefined)}); | ||||
|         RED.actions.add("core:add-flow-to-right",function(workspace) { | ||||
|             let index | ||||
|             if (workspace) { | ||||
|                 index = workspace_tabs.getTabIndex(workspace.id)+1 | ||||
|             } else { | ||||
|                 index = workspace_tabs.activeIndex()+1 | ||||
|             } | ||||
|             addWorkspace(undefined,undefined,index) | ||||
|         }); | ||||
|         RED.actions.add("core:edit-flow",editWorkspace); | ||||
|         RED.actions.add("core:remove-flow",removeWorkspace); | ||||
|         if (RED.settings.theme("menu.menu-item-workspace-add", true)) { | ||||
|             RED.actions.add("core:add-flow",function(opts) { addWorkspace(undefined,undefined,opts?opts.index:undefined)}); | ||||
|             RED.actions.add("core:add-flow-to-right",function(workspace) { | ||||
|                 let index | ||||
|                 if (workspace) { | ||||
|                     index = workspace_tabs.getTabIndex(workspace.id)+1 | ||||
|                 } else { | ||||
|                     index = workspace_tabs.activeIndex()+1 | ||||
|                 } | ||||
|                 addWorkspace(undefined,undefined,index) | ||||
|             }); | ||||
|         } | ||||
|         if (RED.settings.theme("menu.menu-item-workspace-edit", true)) { | ||||
|             RED.actions.add("core:edit-flow",editWorkspace); | ||||
|         } | ||||
|         if (RED.settings.theme("menu.menu-item-workspace-delete", true)) { | ||||
|             RED.actions.add("core:remove-flow",removeWorkspace); | ||||
|         } | ||||
|         RED.actions.add("core:enable-flow",enableWorkspace); | ||||
|         RED.actions.add("core:disable-flow",disableWorkspace); | ||||
|         RED.actions.add("core:lock-flow",lockWorkspace); | ||||
| @@ -657,6 +685,9 @@ RED.workspaces = (function() { | ||||
|         RED.events.on("flows:change", (ws) => { | ||||
|             $("#red-ui-tab-"+(ws.id.replace(".","-"))).toggleClass('red-ui-workspace-changed',!!(ws.contentsChanged || ws.changed || ws.added)); | ||||
|         }) | ||||
|         RED.events.on("subflows:change", (ws) => { | ||||
|             $("#red-ui-tab-"+(ws.id.replace(".","-"))).toggleClass('red-ui-workspace-changed',!!(ws.contentsChanged || ws.changed || ws.added)); | ||||
|         }) | ||||
|  | ||||
|         hideWorkspace(); | ||||
|     } | ||||
| @@ -889,6 +920,17 @@ RED.workspaces = (function() { | ||||
|             } | ||||
|         }, | ||||
|         refresh: function() { | ||||
|             var workspace = RED.nodes.workspace(RED.workspaces.active()); | ||||
|             if (workspace) { | ||||
|                 document.title = `${documentTitle} : ${workspace.label}`; | ||||
|             } else { | ||||
|                 var subflow = RED.nodes.subflow(RED.workspaces.active()); | ||||
|                 if (subflow) { | ||||
|                     document.title = `${documentTitle} : ${subflow.name}`; | ||||
|                 } else { | ||||
|                     document.title = documentTitle | ||||
|                 } | ||||
|             } | ||||
|             RED.nodes.eachWorkspace(function(ws) { | ||||
|                 workspace_tabs.renameTab(ws.id,ws.label); | ||||
|                 $("#red-ui-tab-"+(ws.id.replace(".","-"))).attr("flowname",ws.label) | ||||
|   | ||||
| @@ -168,6 +168,37 @@ RED.user = (function() { | ||||
|                     } | ||||
|  | ||||
|  | ||||
|                 } else { | ||||
|                     if (data.prompts) { | ||||
|                         if (data.loginMessage) { | ||||
|                             const sessionMessages = $("<div/>",{class:"form-row",style:"text-align: center"}).appendTo("#node-dialog-login-fields"); | ||||
|                             $('<div>').text(data.loginMessage).appendTo(sessionMessages); | ||||
|                         } | ||||
|  | ||||
|                         i = 0; | ||||
|                         for (;i<data.prompts.length;i++) { | ||||
|                             var field = data.prompts[i]; | ||||
|                             var row = $("<div/>",{class:"form-row",style:"text-align: center"}).appendTo("#node-dialog-login-fields"); | ||||
|                             var loginButton = $('<a href="#" class="red-ui-button"></a>',{style: "padding: 10px"}).appendTo(row).on("click", function() { | ||||
|                                 document.location = field.url; | ||||
|                             }); | ||||
|                             if (field.image) { | ||||
|                                 $("<img>",{src:field.image}).appendTo(loginButton); | ||||
|                             } else if (field.label) { | ||||
|                                 var label = $('<span></span>').text(field.label); | ||||
|                                 if (field.icon) { | ||||
|                                     $('<i></i>',{class: "fa fa-2x "+field.icon, style:"vertical-align: middle"}).appendTo(loginButton); | ||||
|                                     label.css({ | ||||
|                                         "verticalAlign":"middle", | ||||
|                                         "marginLeft":"8px" | ||||
|                                     }); | ||||
|  | ||||
|                                 } | ||||
|                                 label.appendTo(loginButton); | ||||
|                             } | ||||
|                             loginButton.button(); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 if (opts.cancelable) { | ||||
|                     $("#node-dialog-login-cancel").button().on("click", function( event ) { | ||||
| @@ -187,6 +218,7 @@ RED.user = (function() { | ||||
|     } | ||||
|  | ||||
|     function logout() { | ||||
|         RED.events.emit('logout') | ||||
|         var tokens = RED.settings.get("auth-tokens"); | ||||
|         var token = tokens?tokens.access_token:""; | ||||
|         $.ajax({ | ||||
| @@ -211,6 +243,8 @@ RED.user = (function() { | ||||
|  | ||||
|     function updateUserMenu() { | ||||
|         $("#red-ui-header-button-user-submenu li").remove(); | ||||
|         const userMenu = $("#red-ui-header-button-user") | ||||
|         userMenu.empty() | ||||
|         if (RED.settings.user.anonymous) { | ||||
|             RED.menu.addItem("red-ui-header-button-user",{ | ||||
|                 id:"usermenu-item-login", | ||||
| @@ -238,7 +272,8 @@ RED.user = (function() { | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         const userIcon = generateUserIcon(RED.settings.user) | ||||
|         userIcon.appendTo(userMenu); | ||||
|     } | ||||
|  | ||||
|     function init() { | ||||
| @@ -247,14 +282,6 @@ RED.user = (function() { | ||||
|  | ||||
|                 var userMenu = $('<li><a id="red-ui-header-button-user" class="button hide" href="#"></a></li>') | ||||
|                     .prependTo(".red-ui-header-toolbar"); | ||||
|                 if (RED.settings.user.image) { | ||||
|                     $('<span class="user-profile"></span>').css({ | ||||
|                         backgroundImage: "url("+RED.settings.user.image+")", | ||||
|                     }).appendTo(userMenu.find("a")); | ||||
|                 } else { | ||||
|                     $('<i class="fa fa-user"></i>').appendTo(userMenu.find("a")); | ||||
|                 } | ||||
|  | ||||
|                 RED.menu.init({id:"red-ui-header-button-user", | ||||
|                     options: [] | ||||
|                 }); | ||||
| @@ -317,12 +344,30 @@ RED.user = (function() { | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     function generateUserIcon(user) { | ||||
|         const userIcon = $('<span class="red-ui-user-profile"></span>') | ||||
|         if (user.image) { | ||||
|             userIcon.addClass('has_profile_image') | ||||
|             userIcon.css({ | ||||
|                 backgroundImage: "url("+user.image+")", | ||||
|             }) | ||||
|         } else if (user.anonymous) { | ||||
|             $('<i class="fa fa-user"></i>').appendTo(userIcon); | ||||
|         } else { | ||||
|             $('<span>').text(user.username.substring(0,2)).appendTo(userIcon); | ||||
|         } | ||||
|         if (user.profileColor !== undefined) { | ||||
|             userIcon.addClass('red-ui-user-profile-color-' + user.profileColor) | ||||
|         } | ||||
|         return userIcon | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         init: init, | ||||
|         login: login, | ||||
|         logout: logout, | ||||
|         hasPermission: hasPermission | ||||
|         hasPermission: hasPermission, | ||||
|         generateUserIcon | ||||
|     } | ||||
|  | ||||
| })(); | ||||
|   | ||||
| @@ -38,7 +38,7 @@ body { | ||||
| } | ||||
| #red-ui-main-container { | ||||
|     position: absolute; | ||||
|     top:40px; left:0; bottom: 0; right:0; | ||||
|     top: var(--red-ui-header-height); left:0; bottom: 0; right:0; | ||||
|     overflow:hidden; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -259,7 +259,8 @@ $deploy-button-background-disabled-hover: #555; | ||||
|  | ||||
| $header-background: #000; | ||||
| $header-button-background-active: #121212; | ||||
| $header-menu-color: #C7C7C7; | ||||
| $header-accent: #C02020; | ||||
| $header-menu-color: #eee; | ||||
| $header-menu-color-disabled: #666; | ||||
| $header-menu-heading-color: #fff; | ||||
| $header-menu-sublabel-color: #aeaeae; | ||||
| @@ -313,6 +314,16 @@ $spinner-color: #999; | ||||
|  | ||||
| $tab-icon-color: #dedede; | ||||
|  | ||||
| // Anonymous User Colors | ||||
|  | ||||
| $user-profile-colors: ( | ||||
|     1: #822e81, | ||||
|     2: #955e42, | ||||
|     3: #9c914f, | ||||
|     4: #748e54, | ||||
|     5: #06bcc1 | ||||
| ); | ||||
|  | ||||
| // Deprecated | ||||
| $text-color-green: $text-color-success; | ||||
| $info-text-code-color: $text-color-code; | ||||
|   | ||||
| @@ -23,16 +23,20 @@ | ||||
|     top: 0; | ||||
|     left: 0; | ||||
|     width: 100%; | ||||
|     height: 40px; | ||||
|     height: var(--red-ui-header-height); | ||||
|     background: var(--red-ui-header-background); | ||||
|     box-sizing: border-box; | ||||
|     padding: 0px 0px 0px 20px; | ||||
|     color: var(--red-ui-header-menu-color); | ||||
|     font-size: 14px; | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     align-items: center; | ||||
|     border-bottom: 2px solid var(--red-ui-header-accent); | ||||
|     padding-top: 2px; | ||||
|  | ||||
|     span.red-ui-header-logo { | ||||
|         float: left; | ||||
|         margin-top: 5px; | ||||
|         font-size: 30px; | ||||
|         line-height: 30px; | ||||
|         text-decoration: none; | ||||
| @@ -42,7 +46,7 @@ | ||||
|             vertical-align: middle; | ||||
|             font-size: 16px !important; | ||||
|             &:not(:first-child) { | ||||
|                 margin-left: 5px; | ||||
|                 margin-left: 8px; | ||||
|             } | ||||
|         } | ||||
|         img { | ||||
| @@ -59,25 +63,29 @@ | ||||
|     } | ||||
|  | ||||
|     .red-ui-header-toolbar { | ||||
|         display: flex; | ||||
|         align-items: stretch; | ||||
|         padding: 0; | ||||
|         margin: 0; | ||||
|         list-style: none; | ||||
|         float: right; | ||||
|  | ||||
|         > li { | ||||
|             display: inline-block; | ||||
|             display: inline-flex; | ||||
|             align-items: stretch; | ||||
|             padding: 0; | ||||
|             margin: 0; | ||||
|             position: relative; | ||||
|  | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .button { | ||||
|         height: 100%; | ||||
|         display: inline-flex; | ||||
|         align-items: center; | ||||
|         justify-content: center; | ||||
|         min-width: 20px; | ||||
|         text-align: center; | ||||
|         line-height: 40px; | ||||
|         display: inline-block; | ||||
|         font-size: 20px; | ||||
|         padding: 0px 12px; | ||||
|         text-decoration: none; | ||||
| @@ -178,6 +186,20 @@ | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .red-ui-deploy-button-group.readOnly { | ||||
|         .fa-caret-down { display: none; } | ||||
|         .fa-lock { display: inline-block; } | ||||
|     } | ||||
|     .red-ui-deploy-button-group:not(.readOnly) { | ||||
|         .fa-caret-down { display: inline-block; } | ||||
|         .fa-lock { display: none; } | ||||
|     } | ||||
|     .red-ui-deploy-button-group.readOnly { | ||||
|         a { | ||||
|             pointer-events: none; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     li.open .button { | ||||
|         background: var(--red-ui-header-button-background-active); | ||||
|         border-color: var(--red-ui-header-button-background-active); | ||||
| @@ -266,18 +288,44 @@ | ||||
|     #usermenu-item-username > .red-ui-menu-label { | ||||
|         color: var(--red-ui-header-menu-heading-color); | ||||
|     } | ||||
| } | ||||
|  | ||||
|     #red-ui-header-button-user .user-profile { | ||||
|         background-position: center center; | ||||
|         background-repeat: no-repeat; | ||||
|         background-size: contain; | ||||
|         display: inline-block; | ||||
|         width: 40px; | ||||
|         height: 35px; | ||||
|         vertical-align: middle; | ||||
|  | ||||
| .red-ui-user-profile { | ||||
|     background-color: var(--red-ui-header-background); | ||||
|     border: 2px solid var(--red-ui-header-menu-color); | ||||
|     border-radius: 30px; | ||||
|     overflow: hidden; | ||||
|  | ||||
|     background-position: center center; | ||||
|     background-repeat: no-repeat; | ||||
|     background-size: contain; | ||||
|     display: inline-flex; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
|     vertical-align: middle; | ||||
|     width: 30px; | ||||
|     height: 30px; | ||||
|     font-size: 20px; | ||||
|  | ||||
|     &.red-ui-user-profile-color-1 { | ||||
|         background-color: var(--red-ui-user-profile-colors-1); | ||||
|     } | ||||
|     &.red-ui-user-profile-color-2 { | ||||
|         background-color: var(--red-ui-user-profile-colors-2); | ||||
|     } | ||||
|     &.red-ui-user-profile-color-3 { | ||||
|         background-color: var(--red-ui-user-profile-colors-3); | ||||
|     } | ||||
|     &.red-ui-user-profile-color-4 { | ||||
|         background-color: var(--red-ui-user-profile-colors-4); | ||||
|     } | ||||
|     &.red-ui-user-profile-color-5 { | ||||
|         background-color: var(--red-ui-user-profile-colors-5); | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| @media only screen and (max-width: 450px) { | ||||
|     span.red-ui-header-logo > span { | ||||
|         display: none; | ||||
|   | ||||
							
								
								
									
										116
									
								
								packages/node_modules/@node-red/editor-client/src/sass/multiplayer.scss
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,116 @@ | ||||
| #red-ui-multiplayer-user-list { | ||||
|     display: inline-flex; | ||||
|     align-items: center; | ||||
|     margin: 0 5px; | ||||
|     li { | ||||
|         display: inline-flex; | ||||
|         align-items: center; | ||||
|         margin: 0 2px; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| .red-ui-multiplayer-user-icon { | ||||
|     background: none; | ||||
|     border: none; | ||||
|     display: inline-flex; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
|     text-align: center; | ||||
|     box-sizing: border-box; | ||||
|     text-decoration: none; | ||||
|     color: var(--red-ui-header-menu-color); | ||||
|     padding: 0px; | ||||
|     margin: 0px; | ||||
|     vertical-align: middle; | ||||
|  | ||||
|     &:focus { | ||||
|         outline: none; | ||||
|     } | ||||
|  | ||||
|     .red-ui-multiplayer-user.inactive & { | ||||
|         opacity: 0.5; | ||||
|     } | ||||
|     .red-ui-user-profile { | ||||
|         width: 20px; | ||||
|         border-radius: 20px; | ||||
|         height: 20px; | ||||
|         font-size: 12px | ||||
|     } | ||||
| } | ||||
| .red-ui-multiplayer-users-tray { | ||||
|     position: absolute; | ||||
|     top: 5px; | ||||
|     right: 20px; | ||||
|     line-height: normal; | ||||
|     cursor: pointer; | ||||
|     // &:hover { | ||||
|     //     .red-ui-multiplayer-user-location { | ||||
|     //         margin-left: 1px; | ||||
|     //     } | ||||
|     // } | ||||
| } | ||||
| $multiplayer-user-icon-background: var(--red-ui-primary-background); | ||||
| $multiplayer-user-icon-border: var(--red-ui-view-background); | ||||
| $multiplayer-user-icon-text-color: var(--red-ui-header-menu-color); | ||||
| $multiplayer-user-icon-count-text-color: var(--red-ui-primary-color); | ||||
| $multiplayer-user-icon-shadow: 0px 0px 4px var(--red-ui-shadow); | ||||
| .red-ui-multiplayer-user-location { | ||||
|     display: inline-block; | ||||
|     margin-left: -6px; | ||||
|     transition: margin-left 0.2s; | ||||
|     .red-ui-user-profile { | ||||
|         border: 1px solid $multiplayer-user-icon-border; | ||||
|         color: $multiplayer-user-icon-text-color; | ||||
|         width: 18px; | ||||
|         height: 18px; | ||||
|         border-radius: 18px; | ||||
|         font-size: 10px; | ||||
|         font-weight: normal; | ||||
|         box-shadow: $multiplayer-user-icon-shadow; | ||||
|         &.red-ui-multiplayer-user-count { | ||||
|             color: $multiplayer-user-icon-count-text-color; | ||||
|             background-color: $multiplayer-user-icon-background; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| .red-ui-multiplayer-annotation { | ||||
|     .red-ui-multiplayer-annotation-background { | ||||
|         filter: drop-shadow($multiplayer-user-icon-shadow); | ||||
|         fill: $multiplayer-user-icon-background; | ||||
|         &.red-ui-user-profile-color-1 { | ||||
|             fill: var(--red-ui-user-profile-colors-1); | ||||
|         } | ||||
|         &.red-ui-user-profile-color-2 { | ||||
|             fill: var(--red-ui-user-profile-colors-2); | ||||
|         } | ||||
|         &.red-ui-user-profile-color-3 { | ||||
|             fill: var(--red-ui-user-profile-colors-3); | ||||
|         } | ||||
|         &.red-ui-user-profile-color-4 { | ||||
|             fill: var(--red-ui-user-profile-colors-4); | ||||
|         } | ||||
|         &.red-ui-user-profile-color-5 { | ||||
|             fill: var(--red-ui-user-profile-colors-5); | ||||
|         } | ||||
|     } | ||||
|     .red-ui-multiplayer-annotation-border { | ||||
|         stroke: $multiplayer-user-icon-border; | ||||
|         stroke-width: 1px; | ||||
|         fill: none; | ||||
|     } | ||||
|     .red-ui-multiplayer-annotation-anon-label { | ||||
|         fill: $multiplayer-user-icon-text-color; | ||||
|         stroke: none; | ||||
|     } | ||||
|     text { | ||||
|         user-select: none; | ||||
|         fill: $multiplayer-user-icon-text-color; | ||||
|         stroke: none; | ||||
|         font-size: 10px;    | ||||
|         &.red-ui-multiplayer-user-count { | ||||
|             fill: $multiplayer-user-icon-count-text-color; | ||||
|         }      | ||||
|     } | ||||
| } | ||||
							
								
								
									
										17
									
								
								packages/node_modules/@node-red/editor-client/src/sass/sizes.scss
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | ||||
| /** | ||||
|  * Copyright JS Foundation and other contributors, http://js.foundation | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  **/ | ||||
|  | ||||
|  $header-height: 48px; | ||||
| @@ -15,4 +15,5 @@ | ||||
| **/ | ||||
|  | ||||
| @import "colors"; | ||||
| @import "sizes"; | ||||
| @import "variables"; | ||||
| @@ -15,6 +15,7 @@ | ||||
| **/ | ||||
|  | ||||
| @import "colors"; | ||||
| @import "sizes"; | ||||
| @import "variables"; | ||||
| @import "mixins"; | ||||
|  | ||||
| @@ -72,3 +73,5 @@ | ||||
| @import "radialMenu"; | ||||
|  | ||||
| @import "tourGuide"; | ||||
|  | ||||
| @import "multiplayer"; | ||||
|   | ||||
| @@ -37,7 +37,6 @@ ul.red-ui-sidebar-node-config-list { | ||||
|     } | ||||
|     .red-ui-palette-node { | ||||
|         // overflow: hidden; | ||||
|         cursor: default; | ||||
|         &.selected { | ||||
|             border-color: transparent; | ||||
|             box-shadow: 0 0 0 2px var(--red-ui-node-selected-color); | ||||
|   | ||||
| @@ -151,8 +151,9 @@ | ||||
|     &.red-ui-tabs-add { | ||||
|         padding-right: 29px; | ||||
|     } | ||||
|     &.red-ui-tabs-add.red-ui-tabs-scrollable { | ||||
|         padding-right: 53px; | ||||
|     &.red-ui-tabs-add.red-ui-tabs-scrollable, | ||||
|     &.red-ui-tabs-menu.red-ui-tabs-scrollable { | ||||
|             padding-right: 53px; | ||||
|     } | ||||
|     &.red-ui-tabs-add.red-ui-tabs-menu.red-ui-tabs-scrollable, | ||||
|     &.red-ui-tabs-add.red-ui-tabs-search.red-ui-tabs-scrollable { | ||||
| @@ -310,8 +311,9 @@ | ||||
|     } | ||||
|  | ||||
| } | ||||
| .red-ui-tabs.red-ui-tabs-add .red-ui-tab-scroll-right { | ||||
|     right: 32px; | ||||
| .red-ui-tabs.red-ui-tabs-add .red-ui-tab-scroll-right, | ||||
| .red-ui-tabs.red-ui-tabs-menu .red-ui-tab-scroll-right { | ||||
|         right: 32px; | ||||
| } | ||||
|  | ||||
| .red-ui-tabs.red-ui-tabs-add.red-ui-tabs-menu .red-ui-tab-scroll-right, | ||||
|   | ||||
| @@ -16,6 +16,9 @@ | ||||
|  | ||||
|     --red-ui-shadow: #{$shadow}; | ||||
|  | ||||
|     // Header Height | ||||
|     --red-ui-header-height: #{$header-height}; | ||||
|  | ||||
| // Main body text | ||||
|     --red-ui-primary-text-color: #{$primary-text-color}; | ||||
| // UI control label text | ||||
| @@ -240,6 +243,7 @@ | ||||
|  | ||||
|  | ||||
|     --red-ui-header-background: #{$header-background}; | ||||
|     --red-ui-header-accent: #{$header-accent}; | ||||
|     --red-ui-header-button-background-active: #{$header-button-background-active}; | ||||
|     --red-ui-header-menu-color: #{$header-menu-color}; | ||||
|     --red-ui-header-menu-color-disabled: #{$header-menu-color-disabled}; | ||||
| @@ -295,4 +299,7 @@ | ||||
|  | ||||
|     --red-ui-tab-icon-color: #{$tab-icon-color}; | ||||
|  | ||||
|     @each $current-color in 1 2 3 4 5 { | ||||
|         --red-ui-user-profile-colors-#{"" + $current-color}: #{map-get($user-profile-colors, $current-color)}; | ||||
|     } | ||||
| } | ||||
|   | ||||
| Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB | 
| Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB | 
| Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB | 
| Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB | 
| Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 189 KiB | 
| Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB | 
| Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB | 
							
								
								
									
										231
									
								
								packages/node_modules/@node-red/editor-client/src/tours/3.1/welcome.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,231 @@ | ||||
| export default { | ||||
|     version: "3.1.0", | ||||
|     steps: [ | ||||
|         { | ||||
|             titleIcon: "fa fa-map-o", | ||||
|             title: { | ||||
|                 "en-US": "Welcome to Node-RED 3.1!", | ||||
|                 "ja": "Node-RED 3.1へようこそ!", | ||||
|                 "fr": "Bienvenue dans Node-RED 3.1!" | ||||
|             }, | ||||
|             description: { | ||||
|                 "en-US": "<p>Let's take a moment to discover the new features in this release.</p>", | ||||
|                 "ja": "<p>本リリースの新機能を見つけてみましょう。</p>", | ||||
|                 "fr": "<p>Prenons un moment pour découvrir les nouvelles fonctionnalités de cette version.</p>" | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "New ways to work with groups", | ||||
|                 "ja": "グループの新たな操作方法", | ||||
|                 "fr": "De nouvelles façons de travailler avec les groupes" | ||||
|             }, | ||||
|             description: { | ||||
|                 "en-US": `<p>We have changed how you interact with groups in the editor.</p> | ||||
|                 <ul> | ||||
|                     <li>They don't get in the way when clicking on a node</li> | ||||
|                     <li>They can be reordered using the Moving Forwards and Move Backwards actions</li> | ||||
|                     <li>Multiple nodes can be dragged into a group in one go</li> | ||||
|                     <li>Holding <code>Alt</code> when dragging a node will *remove* it from its group</li> | ||||
|                 </ul>`, | ||||
|                 "ja": `<p>エディタ上のグループの操作が変更されました。</p> | ||||
|                 <ul> | ||||
|                     <li>グループ内のノードをクリックする時に、グループが邪魔をすることが無くなりました。</li> | ||||
|                     <li>「前面へ移動」と「背面へ移動」の動作を用いて、複数のグループの表示順序を変えることができます。</li> | ||||
|                     <li>グループ内へ一度に複数のノードをドラッグできるようになりました。</li> | ||||
|                     <li><code>Alt</code> を押したまま、グループ内のノードをドラッグすると、そのグループから *除く* ことができます。</li> | ||||
|                 </ul>`, | ||||
|                 "fr": `<p>Nous avons modifié la façon dont vous interagissez avec les groupes dans l'éditeur.</p> | ||||
|                 <ul> | ||||
|                     <li>Ils ne gênent plus lorsque vous cliquez sur un noeud</li> | ||||
|                     <li>Ils peuvent être réorganisés à l'aide des actions Avancer et Reculer</li> | ||||
|                     <li>Plusieurs noeuds peuvent être glissés dans un groupe en une seule fois</li> | ||||
|                     <li>Maintenir <code>Alt</code> lors du déplacement d'un noeud le *supprimera* de son groupe</li> | ||||
|                 </ul>` | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "Change notification on tabs", | ||||
|                 "ja": "タブ上の変更通知", | ||||
|                 "fr": "Notification de changement sur les onglets" | ||||
|             }, | ||||
|             image: '3.1/images/tab-changes.png', | ||||
|             description: { | ||||
|                 "en-US": `<p>When a tab contains undeployed changes it now shows the | ||||
|                     same style of change icon used by nodes.</p> | ||||
|                     <p>This will make it much easier to track down changes when you're | ||||
|                     working across multiple flows.</p>`, | ||||
|                 "ja": `<p>タブ内にデプロイされていない変更が存在する時は、ノードと同じスタイルで変更の印が表示されるようになりました。</p> | ||||
|                        <p>これによって複数のフローを編集している時に、変更を見つけるのが簡単になりました。</p>`, | ||||
|                 "fr": `<p>Lorsqu'un onglet contient des modifications non déployées, il affiche désormais le | ||||
|                     même style d'icône de changement utilisé par les noeuds.</p> | ||||
|                     <p>Cela facilitera grandement le suivi des modifications lorsque vous | ||||
|                     travaillez sur plusieurs flux.</p>` | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "A bigger canvas to work with", | ||||
|                 "ja": "より広くなった作業キャンバス", | ||||
|                 "fr": "Un canevas plus grand pour travailler" | ||||
|             }, | ||||
|             description: { | ||||
|                 "en-US": `<p>The default canvas size has been increased so you can fit more | ||||
|                 into one flow.</p> | ||||
|                 <p>We still recommend using tools such as subflows and Link Nodes to help | ||||
|                    keep things organised, but now you have more room to work in.</p>`, | ||||
|                 "ja": `<p>標準のキャンバスが広くなったため、1つのフローに沢山のものを含めることができるようになりました。</p> | ||||
|                        <p>引き続き、サブフローやリンクノードなどの方法を用いて整理することをお勧めしますが、作業できる場所が増えました。</p>`, | ||||
|                 "fr": `<p>La taille par défaut du canevas a été augmentée pour que vous puissiez en mettre plus | ||||
|                 sur un seul flux.</p> | ||||
|                 <p>Nous recommandons toujours d'utiliser des outils tels que les sous-flux et les noeuds de lien pour vous aider | ||||
|                    à garder les choses organisées, mais vous avez maintenant plus d'espace pour travailler.</p>` | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "Finding help", | ||||
|                 "ja": "ヘルプを見つける", | ||||
|                 "fr": "Trouver de l'aide" | ||||
|             }, | ||||
|             image: '3.1/images/node-help.png', | ||||
|             description: { | ||||
|                 "en-US": `<p>All node edit dialogs now include a link to that node's help | ||||
|                 in the footer.</p> | ||||
|                 <p>Clicking it will open up the Help sidebar showing the help for that node.</p>`, | ||||
|                 "ja": `<p>全てのノードの編集ダイアログの下に、ノードのヘルプへのリンクが追加されました。</p> | ||||
|                        <p>これをクリックすると、ノードのヘルプサイドバーが表示されます。</p>`, | ||||
|                 "fr": `<p>Toutes les boîtes de dialogue d'édition de noeud incluent désormais un lien vers l'aide de ce noeud | ||||
|                 dans le pied de page.</p> | ||||
|                 <p>Cliquer dessus ouvrira la barre latérale d'aide affichant l'aide pour ce noeud.</p>` | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "Improved Context Menu", | ||||
|                 "ja": "コンテキストメニューの改善", | ||||
|                 "fr": "Menu contextuel amélioré" | ||||
|             }, | ||||
|             image: '3.1/images/context-menu.png', | ||||
|             description: { | ||||
|                 "en-US": `<p>The editor's context menu has been expanded to make lots more of | ||||
|                         the built-in actions available.</p> | ||||
|                         <p>Adding nodes, working with groups and plenty | ||||
|                         of other useful tools are now just a click away.</p> | ||||
|                         <p>The flow tab bar also has its own context menu to make working | ||||
|                         with your flows much easier.</p>`, | ||||
|                 "ja": `<p>より多くの組み込み動作を利用できるように、エディタのコンテキストメニューが拡張されました。</p> | ||||
|                        <p>ノードの追加、グループの操作、その他の便利なツールをクリックするだけで実行できるようになりました。</p> | ||||
|                        <p>フローのタブバーには、フローの操作をより簡単にする独自のコンテキストメニューもあります。</p>`, | ||||
|                 "fr": `<p>Le menu contextuel de l'éditeur a été étendu pour faire beaucoup plus d'actions intégrées disponibles.</p> | ||||
|                 <p>Ajouter des noeuds, travailler avec des groupes et beaucoup d'autres outils utiles sont désormais à portée de clic.</p> | ||||
|                 <p>La barre d'onglets de flux possède également son propre menu contextuel pour faciliter l'utilisation de vos flux.</p>` | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "Hiding Flows", | ||||
|                 "ja": "フローを非表示", | ||||
|                 "fr": "Masquage de flux" | ||||
|             }, | ||||
|             image: '3.1/images/hiding-flows.png', | ||||
|             description: { | ||||
|                 "en-US": `<p>Hiding flows is now done through the flow context menu.</p> | ||||
|                           <p>The 'hide' button in previous releases has been removed from the tabs | ||||
|                              as they were being clicked accidentally too often.</p>`, | ||||
|                 "ja": `<p>フローを非表示にする機能は、フローのコンテキストメニューから実行するようになりました。</p> | ||||
|                        <p>これまでのリリースでタブに存在していた「非表示」ボタンは、よく誤ってクリックされていたため、削除されました。</p>`, | ||||
|                 "fr": `<p>Le masquage des flux s'effectue désormais via le menu contextuel du flux.</p> | ||||
|                 <p>Le bouton "Masquer" des versions précédentes a été supprimé des onglets | ||||
|                    car il était cliqué accidentellement trop souvent.</p>` | ||||
|             }, | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "Locking Flows", | ||||
|                 "ja": "フローを固定", | ||||
|                 "fr": "Verrouillage de flux" | ||||
|             }, | ||||
|             image: '3.1/images/locking-flows.png', | ||||
|             description: { | ||||
|                 "en-US": `<p>Flows can now be locked to prevent accidental changes being made.</p> | ||||
|                           <p>When locked you cannot modify the nodes in any way.</p> | ||||
|                           <p>The flow context menu provides the options to lock and unlock flows, | ||||
|                              as well as in the Info sidebar explorer.</p>`, | ||||
|                 "ja": `<p>誤ってフローに変更が加えられてしまうのを防ぐために、フローを固定できるようになりました。</p> | ||||
|                        <p>固定されている時は、ノードを修正することはできません。</p> | ||||
|                        <p>フローのコンテキストメニューと、情報サイドバーのエクスプローラには、フローの固定や解除をするためのオプションが用意されています。</p>`, | ||||
|                 "fr": `<p>Les flux peuvent désormais être verrouillés pour éviter toute modification accidentelle.</p> | ||||
|                 <p>Lorsqu'il est verrouillé, vous ne pouvez en aucun cas modifier les noeuds.</p> | ||||
|                 <p>Le menu contextuel du flux fournit les options pour verrouiller et déverrouiller les flux, | ||||
|                    ainsi que dans l'explorateur de la barre latérale d'informations.</p>` | ||||
|             }, | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "Adding Images to node/flow descriptions", | ||||
|                 "ja": "ノードやフローの説明へ画像を追加", | ||||
|                 "fr": "Ajout d'images aux descriptions de noeud/flux" | ||||
|             }, | ||||
|             // image: 'images/debug-path-tooltip.png', | ||||
|             description: { | ||||
|                 "en-US": `<p>You can now add images to a node's or flows's description.</p> | ||||
|                           <p>Simply drag the image into the text editor and it will get added inline.</p> | ||||
|                           <p>When the description is shown in the Info sidebar, the image will be displayed.</p>`, | ||||
|                 "ja": `<p>ノードまたはフローの説明に、画像を追加できるようになりました。</p> | ||||
|                        <p>画像をテキストエディタにドラッグするだけで、行内に埋め込まれます。</p> | ||||
|                        <p>情報サイドバーの説明を開くと、その画像が表示されます。</p>`, | ||||
|                 "fr": `<p>Vous pouvez désormais ajouter des images à la description d'un noeud ou d'un flux.</p> | ||||
|                 <p>Faites simplement glisser l'image dans l'éditeur de texte et elle sera ajoutée en ligne.</p> | ||||
|                 <p>Lorsque la description s'affiche dans la barre latérale d'informations, l'image s'affiche.</p>` | ||||
|             }, | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "Adding Mermaid Diagrams", | ||||
|                 "ja": "Mermaid図を追加", | ||||
|                 "fr": "Ajout de diagrammes Mermaid" | ||||
|             }, | ||||
|             image: '3.1/images/mermaid.png', | ||||
|             description: { | ||||
|                 "en-US": `<p>You can also add <a href="https://github.com/mermaid-js/mermaid">Mermaid</a> diagrams directly into your node or flow descriptions.</p> | ||||
|                           <p>This gives you much richer options for documenting your flows.</p>`, | ||||
|                 "ja": `<p>ノードやフローの説明に、<a href="https://github.com/mermaid-js/mermaid">Mermaid</a>図を直接追加することもできます。</p> | ||||
|                        <p>これによって、フローを説明する文書作成の選択肢がより多くなります。</p>`, | ||||
|                 "fr": `<p>Vous pouvez également ajouter des diagrammes <a href="https://github.com/mermaid-js/mermaid">Mermaid</a> directement dans vos descriptions de noeud ou de flux.</p> | ||||
|                 <p>Cela vous offre des options beaucoup plus riches pour documenter vos flux.</p>` | ||||
|             }, | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "Managing Global Environment Variables", | ||||
|                 "ja": "グローバル環境変数の管理", | ||||
|                 "fr": "Gestion des variables d'environnement globales" | ||||
|             }, | ||||
|             image: '3.1/images/global-env-vars.png', | ||||
|             description: { | ||||
|                 "en-US": `<p>You can set environment variables that apply to all nodes and flows in the new | ||||
|                           'Global Environment Variables' section of User Settings.</p>`, | ||||
|                 "ja": `<p>ユーザ設定に新しく追加された「グローバル環境変数」のセクションで、全てのノードとフローに適用される環境変数を登録できます。</p>`, | ||||
|                 "fr": `<p>Vous pouvez définir des variables d'environnement qui s'appliquent à tous les noeuds et flux dans la nouvelle | ||||
|                 section "Global Environment Variables" des paramètres utilisateur.</p>` | ||||
|             }, | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "Node Updates", | ||||
|                 "ja": "ノードの更新", | ||||
|                 "fr": "Mises à jour des noeuds" | ||||
|             }, | ||||
|             // image: "images/", | ||||
|             description: { | ||||
|                 "en-US": `<p>The core nodes have received lots of minor fixes, documentation updates and | ||||
|                           small enhancements. Check the full changelog in the Help sidebar for a full list.</p>`, | ||||
|                 "ja": `<p>コアノードにマイナーな修正、ドキュメント更新、小規模な拡張が数多く追加されています。全ての一覧は、ヘルプサイドバーの全ての更新履歴を確認してください。</p>`, | ||||
|                 "fr": `<p>Les noeuds principaux ont reçu de nombreux correctifs mineurs, mises à jour de la documentation et | ||||
|                 petites améliorations. Consulter le journal des modifications complet dans la barre latérale d'aide.</p>` | ||||
|             } | ||||
|         } | ||||
|     ] | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								packages/node_modules/@node-red/editor-client/src/tours/images/nr4-auto-complete.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 24 KiB | 
							
								
								
									
										
											BIN
										
									
								
								packages/node_modules/@node-red/editor-client/src/tours/images/nr4-background-deploy.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 21 KiB | 
							
								
								
									
										
											BIN
										
									
								
								packages/node_modules/@node-red/editor-client/src/tours/images/nr4-config-select.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 9.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								packages/node_modules/@node-red/editor-client/src/tours/images/nr4-diff-update.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 36 KiB | 
							
								
								
									
										
											BIN
										
									
								
								packages/node_modules/@node-red/editor-client/src/tours/images/nr4-multiplayer-location.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 24 KiB | 
							
								
								
									
										
											BIN
										
									
								
								packages/node_modules/@node-red/editor-client/src/tours/images/nr4-multiplayer.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 36 KiB | 
							
								
								
									
										
											BIN
										
									
								
								packages/node_modules/@node-red/editor-client/src/tours/images/nr4-plugins.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 20 KiB | 
							
								
								
									
										
											BIN
										
									
								
								packages/node_modules/@node-red/editor-client/src/tours/images/nr4-sf-config.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 36 KiB | 
							
								
								
									
										
											BIN
										
									
								
								packages/node_modules/@node-red/editor-client/src/tours/images/nr4-timestamp-formatting.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 15 KiB | 
| @@ -1,12 +1,12 @@ | ||||
| export default { | ||||
|     version: "3.1.0", | ||||
|     version: "4.0.0", | ||||
|     steps: [ | ||||
|         { | ||||
|             titleIcon: "fa fa-map-o", | ||||
|             title: { | ||||
|                 "en-US": "Welcome to Node-RED 3.1!", | ||||
|                 "ja": "Node-RED 3.1へようこそ!", | ||||
|                 "fr": "Bienvenue dans Node-RED 3.1!" | ||||
|                 "en-US": "Welcome to Node-RED 4.0!", | ||||
|                 "ja": "Node-RED 4.0 へようこそ!", | ||||
|                 "fr": "Bienvenue dans Node-RED 4.0!" | ||||
|             }, | ||||
|             description: { | ||||
|                 "en-US": "<p>Let's take a moment to discover the new features in this release.</p>", | ||||
| @@ -16,202 +16,184 @@ export default { | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "New ways to work with groups", | ||||
|                 "ja": "グループの新たな操作方法", | ||||
|                 "fr": "De nouvelles façons de travailler avec les groupes" | ||||
|                 "en-US": "Multiplayer Mode", | ||||
|                 "ja": "複数ユーザ同時利用モード", | ||||
|                 "fr": "Mode Multi-utilisateur" | ||||
|             }, | ||||
|             image: 'images/nr4-multiplayer-location.png', | ||||
|             description: { | ||||
|                 "en-US": `<p>We have changed how you interact with groups in the editor.</p> | ||||
|                 "en-US": `<p>This release includes the first small steps towards making Node-RED easier | ||||
|                 to work with when you have multiple people editing flows at the same time.</p> | ||||
|                 <p>When this feature is enabled, you will now see who else has the editor open and some | ||||
|                 basic information on where they are in the editor.</p> | ||||
|                 <p>Check the release post for details on how to enable this feature in your settings file.</p>`, | ||||
|                 "ja": `<p>本リリースには、複数ユーザが同時にフローを編集する時に、Node-REDをより使いやすくするのための最初の微修正が入っています。</p> | ||||
|                 <p>本機能を有効にすると、誰がエディタを開いているか、その人がエディタ上のどこにいるかの基本的な情報が表示されます。</p> | ||||
|                 <p>設定ファイルで本機能を有効化する方法の詳細は、リリースの投稿を確認してください。</p>`, | ||||
|                 "fr": `<p>Cette version inclut les premières étapes visant à rendre Node-RED plus facile à utiliser | ||||
|                 lorsque plusieurs personnes modifient des flux en même temps.</p> | ||||
|                 <p>Lorsque cette fonctionnalité est activée, vous pourrez désormais voir si d’autres utilisateurs ont | ||||
|                 ouvert l'éditeur. Vous pourrez également savoir où ces utilisateurs se trouvent dans l'éditeur.</p> | ||||
|                 <p>Consultez la note de publication pour plus de détails sur la façon d'activer cette fonctionnalité | ||||
|                 dans votre fichier de paramètres.</p>` | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "Better background deploy handling", | ||||
|                 "ja": "バックグラウンドのデプロイ処理の改善", | ||||
|                 "fr": "Meilleure gestion du déploiement en arrière-plan" | ||||
|             }, | ||||
|             image: 'images/nr4-background-deploy.png', | ||||
|             description: { | ||||
|                 "en-US": `<p>If another user deploys changes whilst you are editing, we now use a more discrete notification | ||||
|                 that doesn't stop you continuing your work - especially if they are being very productive and deploying lots | ||||
|                 of changes.</p>`, | ||||
|                 "ja": `他のユーザが変更をデプロイした時に、特に変更が多い生産的な編集作業を妨げないように通知するようになりました。`, | ||||
|                 "fr": `<p>Si un autre utilisateur déploie des modifications pendant que vous êtes en train de modifier, vous recevrez | ||||
|                 une notification plus discrète qu'auparavant qui ne vous empêche pas de continuer votre travail.</p>` | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "Improved flow diffs", | ||||
|                 "ja": "フローの差分表示の改善", | ||||
|                 "fr": "Amélioration des différences de flux" | ||||
|             }, | ||||
|             image: 'images/nr4-diff-update.png', | ||||
|             description: { | ||||
|                 "en-US": `<p>When viewing changes made to a flow, Node-RED now distinguishes between nodes that have had configuration | ||||
|                 changes and those that have only been moved.<p> | ||||
|                 <p>When faced with a long list of changes to look at, this makes it much easier to focus on more significant items.</p>`, | ||||
|                 "ja": `<p>フローの変更内容を表示する時に、Node-REDは設定が変更されたノードと、移動されただけのノードを区別するようになりました。<p> | ||||
|                 <p>これによって、多くの変更内容を確認する際に、重要な項目に焦点を当てることができます。</p>`, | ||||
|                 "fr": `<p>Lors de l'affichage des modifications apportées à un flux, Node-RED fait désormais la distinction entre les | ||||
|                 noeuds qui ont changé de configuration et ceux qui ont seulement été déplacés.<p> | ||||
|                 <p>Face à une longue liste de changements à examiner, il est beaucoup plus facile de se concentrer sur les éléments les | ||||
|                 plus importants.</p>` | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "Better Configuration Node UX", | ||||
|                 "ja": "設定ノードのUXが向上", | ||||
|                 "fr": "Meilleure expérience utilisateur du noeud de configuration" | ||||
|             }, | ||||
|             image: 'images/nr4-config-select.png', | ||||
|             description: { | ||||
|                 "en-US": `<p>The Configuration node selection UI has had a small update to have a dedicated 'add' button | ||||
|                 next to the select box.</p> | ||||
|                 <p>It's a small change, but should make it easier to work with your config nodes.</p>`, | ||||
|                 "ja": `<p>設定ノードを選択するUIが修正され、選択ボックスの隣に専用の「追加」ボタンが追加されました。</p> | ||||
|                 <p>微修正ですが設定ノードの操作が容易になります。</p>`, | ||||
|                 "fr": `<p>L'interface utilisateur de la sélection du noeud de configuration a fait l'objet d'une petite | ||||
|                 mise à jour afin de disposer d'un bouton « Ajouter » à côté de la zone de sélection.</p> | ||||
|                 <p>C'est un petit changement, mais cela devrait faciliter le travail avec vos noeuds de configuration.</p>` | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "Timestamp formatting options", | ||||
|                 "ja": "タイムスタンプの形式の項目", | ||||
|                 "fr": "Options de formatage de l'horodatage" | ||||
|             }, | ||||
|             image: 'images/nr4-timestamp-formatting.png', | ||||
|             description: { | ||||
|                 "en-US": `<p>Nodes that let you set a timestamp now have options on what format that timestamp should be in.</p> | ||||
|                 <p>We're keeping it simple to begin with by providing three options:<p> | ||||
|                 <ul> | ||||
|                     <li>They don't get in the way when clicking on a node</li> | ||||
|                     <li>They can be reordered using the Moving Forwards and Move Backwards actions</li> | ||||
|                     <li>Multiple nodes can be dragged into a group in one go</li> | ||||
|                     <li>Holding <code>Alt</code> when dragging a node will *remove* it from its group</li> | ||||
|                     <li>Milliseconds since epoch - this is existing behaviour of the timestamp option</li> | ||||
|                     <li>ISO 8601 - a common format used by many systems</li> | ||||
|                     <li>JavaScript Date Object</li> | ||||
|                 </ul>`, | ||||
|                 "ja": `<p>エディタ上のグループの操作が変更されました。</p> | ||||
|                 "ja": `<p>タイムスタンプを設定するノードに、タイムスタンプの形式を指定できる項目が追加されました。</p> | ||||
|                 <p>次の3つの項目を追加したことで、簡単に選択できるようになりました:<p> | ||||
|                 <ul> | ||||
|                     <li>グループ内のノードをクリックする時に、グループが邪魔をすることが無くなりました。</li> | ||||
|                     <li>「前面へ移動」と「背面へ移動」の動作を用いて、複数のグループの表示順序を変えることができます。</li> | ||||
|                     <li>グループ内へ一度に複数のノードをドラッグできるようになりました。</li> | ||||
|                     <li><code>Alt</code> を押したまま、グループ内のノードをドラッグすると、そのグループから *除く* ことができます。</li> | ||||
|                     <li>エポックからのミリ秒 - 従来動作と同じになるタイムスタンプの項目</li> | ||||
|                     <li>ISO 8601 - 多くのシステムで使用されている共通の形式</li> | ||||
|                     <li>JavaScript日付オブジェクト</li> | ||||
|                 </ul>`, | ||||
|                 "fr": `<p>Nous avons modifié la façon dont vous interagissez avec les groupes dans l'éditeur.</p> | ||||
|                 "fr": `<p>Les noeuds qui vous permettent de définir un horodatage disposent désormais d'options sur le format dans lequel cet horodatage peut être défini.</p> | ||||
|                 <p>Nous gardons les choses simples en proposant trois options :<p> | ||||
|                 <ul> | ||||
|                     <li>Ils ne gênent plus lorsque vous cliquez sur un noeud</li> | ||||
|                     <li>Ils peuvent être réorganisés à l'aide des actions Avancer et Reculer</li> | ||||
|                     <li>Plusieurs noeuds peuvent être glissés dans un groupe en une seule fois</li> | ||||
|                     <li>Maintenir <code>Alt</code> lors du déplacement d'un noeud le *supprimera* de son groupe</li> | ||||
|                     <li>Millisecondes depuis l'époque : il s'agit du comportement existant de l'option d'horodatage</li> | ||||
|                     <li>ISO 8601 : un format commun utilisé par de nombreux systèmes</li> | ||||
|                     <li>Objet Date JavaScript</li> | ||||
|                 </ul>` | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "Change notification on tabs", | ||||
|                 "ja": "タブ上の変更通知", | ||||
|                 "fr": "Notification de changement sur les onglets" | ||||
|                 "en-US": "Auto-complete of flow/global and env types", | ||||
|                 "ja": "フロー/グローバル、環境変数の型の自動補完", | ||||
|                 "fr": "Saisie automatique des types de flux/global et env" | ||||
|             }, | ||||
|             image: 'images/tab-changes.png', | ||||
|             image: 'images/nr4-auto-complete.png', | ||||
|             description: { | ||||
|                 "en-US": `<p>When a tab contains undeployed changes it now shows the | ||||
|                     same style of change icon used by nodes.</p> | ||||
|                     <p>This will make it much easier to track down changes when you're | ||||
|                     working across multiple flows.</p>`, | ||||
|                 "ja": `<p>タブ内にデプロイされていない変更が存在する時は、ノードと同じスタイルで変更の印が表示されるようになりました。</p> | ||||
|                        <p>これによって複数のフローを編集している時に、変更を見つけるのが簡単になりました。</p>`, | ||||
|                 "fr": `<p>Lorsqu'un onglet contient des modifications non déployées, il affiche désormais le | ||||
|                     même style d'icône de changement utilisé par les noeuds.</p> | ||||
|                     <p>Cela facilitera grandement le suivi des modifications lorsque vous | ||||
|                     travaillez sur plusieurs flux.</p>` | ||||
|                 "en-US": `<p>The <code>flow</code>/<code>global</code> context inputs and the <code>env</code> input | ||||
|                 now all include auto-complete suggestions based on the live state of your flows.</p> | ||||
|                 `, | ||||
|                 "ja": `<p><code>flow</code>/<code>global</code>コンテキストや<code>env</code>の入力を、現在のフローの状態をもとに自動補完で提案するようになりました。</p> | ||||
|                 `, | ||||
|                 "fr": `<p>Les entrées contextuelles <code>flow</code>/<code>global</code> et l'entrée <code>env</code> | ||||
|                 incluent désormais des suggestions de saisie semi-automatique basées sur l'état actuel de vos flux.</p> | ||||
|                 `, | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "A bigger canvas to work with", | ||||
|                 "ja": "より広くなった作業キャンバス", | ||||
|                 "fr": "Un canevas plus grand pour travailler" | ||||
|                 "en-US": "Config node customisation in Subflows", | ||||
|                 "ja": "サブフローでの設定ノードのカスタマイズ", | ||||
|                 "fr": "Personnalisation du noeud de configuration dans les sous-flux" | ||||
|             }, | ||||
|             image: 'images/nr4-sf-config.png', | ||||
|             description: { | ||||
|                 "en-US": `<p>The default canvas size has been increased so you can fit more | ||||
|                 into one flow.</p> | ||||
|                 <p>We still recommend using tools such as subflows and Link Nodes to help | ||||
|                    keep things organised, but now you have more room to work in.</p>`, | ||||
|                 "ja": `<p>標準のキャンバスが広くなったため、1つのフローに沢山のものを含めることができるようになりました。</p> | ||||
|                        <p>引き続き、サブフローやリンクノードなどの方法を用いて整理することをお勧めしますが、作業できる場所が増えました。</p>`, | ||||
|                 "fr": `<p>La taille par défaut du canevas a été augmentée pour que vous puissiez en mettre plus | ||||
|                 sur un seul flux.</p> | ||||
|                 <p>Nous recommandons toujours d'utiliser des outils tels que les sous-flux et les noeuds de lien pour vous aider | ||||
|                    à garder les choses organisées, mais vous avez maintenant plus d'espace pour travailler.</p>` | ||||
|                 "en-US": `<p>Subflows can now be customised to allow each instance to use a different | ||||
|                 config node of a selected type.</p> | ||||
|                 <p>For example, each instance of a subflow that connects to an MQTT Broker and does some post-processing | ||||
|                 of the messages received can be pointed at a different broker.</p> | ||||
|                 `, | ||||
|                 "ja": `<p>サブフローをカスタマイズして、選択した型の異なる設定ノードを各インスタンスが使用できるようになりました。</p> | ||||
|                 <p>例えば、MQTTブローカへ接続し、メッセージ受信と後処理を行うサブフローの各インスタンスに異なるブローカを指定することも可能です。</p> | ||||
|                 `, | ||||
|                 "fr": `<p>Les sous-flux peuvent désormais être personnalisés pour permettre à chaque instance d'utiliser un | ||||
|                 noeud de configuration d'un type sélectionné.</p> | ||||
|                 <p>Par exemple, chaque instance d'un sous-flux qui se connecte à un courtier MQTT et effectue un post-traitement | ||||
|                 des messages reçus peut être pointée vers un autre courtier.</p> | ||||
|                 ` | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "Finding help", | ||||
|                 "ja": "ヘルプを見つける", | ||||
|                 "fr": "Trouver de l'aide" | ||||
|                 "en-US": "Remembering palette state", | ||||
|                 "ja": "パレットの状態を維持", | ||||
|                 "fr": "Mémorisation de l'état de la palette" | ||||
|             }, | ||||
|             image: 'images/node-help.png', | ||||
|             description: { | ||||
|                 "en-US": `<p>All node edit dialogs now include a link to that node's help | ||||
|                 in the footer.</p> | ||||
|                 <p>Clicking it will open up the Help sidebar showing the help for that node.</p>`, | ||||
|                 "ja": `<p>全てのノードの編集ダイアログの下に、ノードのヘルプへのリンクが追加されました。</p> | ||||
|                        <p>これをクリックすると、ノードのヘルプサイドバーが表示されます。</p>`, | ||||
|                 "fr": `<p>Toutes les boîtes de dialogue d'édition de noeud incluent désormais un lien vers l'aide de ce noeud | ||||
|                 dans le pied de page.</p> | ||||
|                 <p>Cliquer dessus ouvrira la barre latérale d'aide affichant l'aide pour ce noeud.</p>` | ||||
|                 "en-US": `<p>The palette now remembers what categories you have hidden between reloads - as well as any | ||||
|                 filter you have applied.</p>`, | ||||
|                 "ja": `<p>パレット上で非表示にしたカテゴリや適用したフィルタが、リロードしても記憶されるようになりました。</p>`, | ||||
|                 "fr": `<p>La palette se souvient désormais des catégories que vous avez masquées entre les rechargements, | ||||
|                 ainsi que le filtre que vous avez appliqué.</p>` | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "Improved Context Menu", | ||||
|                 "ja": "コンテキストメニューの改善", | ||||
|                 "fr": "Menu contextuel amélioré" | ||||
|                 "en-US": "Plugins shown in the Palette Manager", | ||||
|                 "ja": "パレット管理にプラグインを表示", | ||||
|                 "fr": "Affichage des Plugins dans le gestionnaire de palettes" | ||||
|             }, | ||||
|             image: 'images/context-menu.png', | ||||
|             image: 'images/nr4-plugins.png', | ||||
|             description: { | ||||
|                 "en-US": `<p>The editor's context menu has been expanded to make lots more of | ||||
|                         the built-in actions available.</p> | ||||
|                         <p>Adding nodes, working with groups and plenty | ||||
|                         of other useful tools are now just a click away.</p> | ||||
|                         <p>The flow tab bar also has its own context menu to make working | ||||
|                         with your flows much easier.</p>`, | ||||
|                 "ja": `<p>より多くの組み込み動作を利用できるように、エディタのコンテキストメニューが拡張されました。</p> | ||||
|                        <p>ノードの追加、グループの操作、その他の便利なツールをクリックするだけで実行できるようになりました。</p> | ||||
|                        <p>フローのタブバーには、フローの操作をより簡単にする独自のコンテキストメニューもあります。</p>`, | ||||
|                 "fr": `<p>Le menu contextuel de l'éditeur a été étendu pour faire beaucoup plus d'actions intégrées disponibles.</p> | ||||
|                 <p>Ajouter des noeuds, travailler avec des groupes et beaucoup d'autres outils utiles sont désormais à portée de clic.</p> | ||||
|                 <p>La barre d'onglets de flux possède également son propre menu contextuel pour faciliter l'utilisation de vos flux.</p>` | ||||
|                 "en-US": `<p>The palette manager now shows any plugin modules you have installed, such as | ||||
|                 <code>node-red-debugger</code>. Previously they would only be shown if the plugins include | ||||
|                 nodes for the palette.</p>`, | ||||
|                 "ja": `<p>パレットの管理に <code>node-red-debugger</code> の様なインストールしたプラグインが表示されます。以前はプラグインにパレット向けのノードが含まれている時のみ表示されていました。</p>`, | ||||
|                 "fr": `<p>Le gestionnaire de palettes affiche désormais tous les plugins que vous avez installés, | ||||
|                 tels que <code>node-red-debugger</code>. Auparavant, ils n'étaient affichés que s'ils contenaient | ||||
|                 des noeuds pour la palette.</p>` | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "Hiding Flows", | ||||
|                 "ja": "フローを非表示", | ||||
|                 "fr": "Masquage de flux" | ||||
|             }, | ||||
|             image: 'images/hiding-flows.png', | ||||
|             description: { | ||||
|                 "en-US": `<p>Hiding flows is now done through the flow context menu.</p> | ||||
|                           <p>The 'hide' button in previous releases has been removed from the tabs | ||||
|                              as they were being clicked accidentally too often.</p>`, | ||||
|                 "ja": `<p>フローを非表示にする機能は、フローのコンテキストメニューから実行するようになりました。</p> | ||||
|                        <p>これまでのリリースでタブに存在していた「非表示」ボタンは、よく誤ってクリックされていたため、削除されました。</p>`, | ||||
|                 "fr": `<p>Le masquage des flux s'effectue désormais via le menu contextuel du flux.</p> | ||||
|                 <p>Le bouton "Masquer" des versions précédentes a été supprimé des onglets | ||||
|                    car il était cliqué accidentellement trop souvent.</p>` | ||||
|             }, | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "Locking Flows", | ||||
|                 "ja": "フローを固定", | ||||
|                 "fr": "Verrouillage de flux" | ||||
|             }, | ||||
|             image: 'images/locking-flows.png', | ||||
|             description: { | ||||
|                 "en-US": `<p>Flows can now be locked to prevent accidental changes being made.</p> | ||||
|                           <p>When locked you cannot modify the nodes in any way.</p> | ||||
|                           <p>The flow context menu provides the options to lock and unlock flows, | ||||
|                              as well as in the Info sidebar explorer.</p>`, | ||||
|                 "ja": `<p>誤ってフローに変更が加えられてしまうのを防ぐために、フローを固定できるようになりました。</p> | ||||
|                        <p>固定されている時は、ノードを修正することはできません。</p> | ||||
|                        <p>フローのコンテキストメニューと、情報サイドバーのエクスプローラには、フローの固定や解除をするためのオプションが用意されています。</p>`, | ||||
|                 "fr": `<p>Les flux peuvent désormais être verrouillés pour éviter toute modification accidentelle.</p> | ||||
|                 <p>Lorsqu'il est verrouillé, vous ne pouvez en aucun cas modifier les noeuds.</p> | ||||
|                 <p>Le menu contextuel du flux fournit les options pour verrouiller et déverrouiller les flux, | ||||
|                    ainsi que dans l'explorateur de la barre latérale d'informations.</p>` | ||||
|             }, | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "Adding Images to node/flow descriptions", | ||||
|                 "ja": "ノードやフローの説明へ画像を追加", | ||||
|                 "fr": "Ajout d'images aux descriptions de noeud/flux" | ||||
|             }, | ||||
|             // image: 'images/debug-path-tooltip.png', | ||||
|             description: { | ||||
|                 "en-US": `<p>You can now add images to a node's or flows's description.</p> | ||||
|                           <p>Simply drag the image into the text editor and it will get added inline.</p> | ||||
|                           <p>When the description is shown in the Info sidebar, the image will be displayed.</p>`, | ||||
|                 "ja": `<p>ノードまたはフローの説明に、画像を追加できるようになりました。</p> | ||||
|                        <p>画像をテキストエディタにドラッグするだけで、行内に埋め込まれます。</p> | ||||
|                        <p>情報サイドバーの説明を開くと、その画像が表示されます。</p>`, | ||||
|                 "fr": `<p>Vous pouvez désormais ajouter des images à la description d'un noeud ou d'un flux.</p> | ||||
|                 <p>Faites simplement glisser l'image dans l'éditeur de texte et elle sera ajoutée en ligne.</p> | ||||
|                 <p>Lorsque la description s'affiche dans la barre latérale d'informations, l'image s'affiche.</p>` | ||||
|             }, | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "Adding Mermaid Diagrams", | ||||
|                 "ja": "Mermaid図を追加", | ||||
|                 "fr": "Ajout de diagrammes Mermaid" | ||||
|             }, | ||||
|             image: 'images/mermaid.png', | ||||
|             description: { | ||||
|                 "en-US": `<p>You can also add <a href="https://github.com/mermaid-js/mermaid">Mermaid</a> diagrams directly into your node or flow descriptions.</p> | ||||
|                           <p>This gives you much richer options for documenting your flows.</p>`, | ||||
|                 "ja": `<p>ノードやフローの説明に、<a href="https://github.com/mermaid-js/mermaid">Mermaid</a>図を直接追加することもできます。</p> | ||||
|                        <p>これによって、フローを説明する文書作成の選択肢がより多くなります。</p>`, | ||||
|                 "fr": `<p>Vous pouvez également ajouter des diagrammes <a href="https://github.com/mermaid-js/mermaid">Mermaid</a> directement dans vos descriptions de noeud ou de flux.</p> | ||||
|                 <p>Cela vous offre des options beaucoup plus riches pour documenter vos flux.</p>` | ||||
|             }, | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "Managing Global Environment Variables", | ||||
|                 "ja": "グローバル環境変数の管理", | ||||
|                 "fr": "Gestion des variables d'environnement globales" | ||||
|             }, | ||||
|             image: 'images/global-env-vars.png', | ||||
|             description: { | ||||
|                 "en-US": `<p>You can set environment variables that apply to all nodes and flows in the new | ||||
|                           'Global Environment Variables' section of User Settings.</p>`, | ||||
|                 "ja": `<p>ユーザ設定に新しく追加された「大域環境変数」のセクションで、全てのノードとフローに適用される環境変数を登録できます。</p>`, | ||||
|                 "fr": `<p>Vous pouvez définir des variables d'environnement qui s'appliquent à tous les noeuds et flux dans la nouvelle | ||||
|                 section "Global Environment Variables" des paramètres utilisateur.</p>` | ||||
|             }, | ||||
|         }, | ||||
|         { | ||||
|             title: { | ||||
|                 "en-US": "Node Updates", | ||||
| @@ -221,10 +203,28 @@ export default { | ||||
|             // image: "images/", | ||||
|             description: { | ||||
|                 "en-US": `<p>The core nodes have received lots of minor fixes, documentation updates and | ||||
|                           small enhancements. Check the full changelog in the Help sidebar for a full list.</p>`, | ||||
|                 "ja": `<p>コアノードにマイナーな修正、ドキュメント更新、小規模な拡張が数多く追加されています。全ての一覧は、ヘルプサイドバーの全ての更新履歴を確認してください。</p>`, | ||||
|                 "fr": `<p>Les noeuds principaux ont reçu de nombreux correctifs mineurs, mises à jour de la documentation et | ||||
|                 petites améliorations. Consulter le journal des modifications complet dans la barre latérale d'aide.</p>` | ||||
|                           small enhancements. Check the full changelog in the Help sidebar for a full list.</p> | ||||
|                           <ul> | ||||
|                             <li>A fully RFC4180 compliant CSV mode</li> | ||||
|                             <li>Customisable headers on the WebSocket node</li> | ||||
|                             <li>Split node now can operate on any message property</li> | ||||
|                             <li>and lots more...</li> | ||||
|                           </ul>`, | ||||
|                 "ja": `<p>コアノードには沢山の軽微な修正、ドキュメント更新、小さな機能拡張が入っています。全リストはヘルプサイドバーにある変更履歴を参照してください。</p> | ||||
|                           <ul> | ||||
|                             <li>RFC4180に完全に準拠したCSVモード</li> | ||||
|                             <li>WebSocketノードのカスタマイズ可能なヘッダ</li> | ||||
|                             <li>Splitノードは、メッセージプロパティで操作できるようになりました</li> | ||||
|                             <li>他にも沢山あります...</li> | ||||
|                           </ul>`, | ||||
|                 "fr": `<p>Les noeuds principaux ont reçu de nombreux correctifs mineurs ainsi que des améliorations. La documentation a été mise à jour. | ||||
|                           Consultez le journal des modifications dans la barre latérale d'aide pour une liste complète. Ci-dessous, les changements les plus importants :</p> | ||||
|                           <ul> | ||||
|                             <li>Un mode CSV entièrement conforme à la norme RFC4180</li> | ||||
|                             <li>En-têtes personnalisables pour le noeud WebSocket</li> | ||||
|                             <li>Le noeud Split peut désormais fonctionner sur n'importe quelle propriété de message</li> | ||||
|                             <li>Et bien plus encore...</li> | ||||
|                           </ul>` | ||||
|             } | ||||
|         } | ||||
|     ] | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
|  | ||||
| /* NOTE: Do not edit directly! This file is generated using `npm run update-types` in https://github.com/Steve-Mcl/monaco-editor-esm-i18n */ | ||||
| /* NOTE: Do not edit directly! This file is generated using `npm run update-types` in https://github.com/node-red/nr-monaco-build */ | ||||
|  | ||||
| interface NodeMessage { | ||||
|     topic?: string; | ||||
|     payload?: any; | ||||
|     _msgid?: string; | ||||
|     /** `_msgid` is generated internally. It not something you typically need to set or modify. */ _msgid?: string; | ||||
|     [other: string]: any; //permit other properties | ||||
| } | ||||
|  | ||||
| @@ -19,15 +19,15 @@ declare const promisify:typeof import('util').promisify | ||||
| /** | ||||
|  * @typedef NodeStatus | ||||
|  * @type {object} | ||||
|  * @property {string} [fill] The fill property can be: red, green, yellow, blue or grey. | ||||
|  * @property {string} [shape] The shape property can be: ring or dot. | ||||
|  * @property {string} [text] The text to display | ||||
|  * @property {'red'|'green'|'yellow'|'blue'|'grey'|string} [fill] - The fill property can be: red, green, yellow, blue or grey. | ||||
|  * @property {'ring'|'dot'|string} [shape] The shape property can be: ring or dot. | ||||
|  * @property {string|boolean|number} [text] The text to display | ||||
|  */ | ||||
| interface NodeStatus { | ||||
|     /** The fill property can be: red, green, yellow, blue or grey */ | ||||
|     fill?: string, | ||||
|     fill?: 'red'|'green'|'yellow'|'blue'|'grey'|string, | ||||
|     /** The shape property can be: ring or dot */ | ||||
|     shape?: string, | ||||
|     shape?: 'ring'|'dot'|string, | ||||
|     /** The text to display */ | ||||
|     text?: string|boolean|number | ||||
| } | ||||
| @@ -37,25 +37,24 @@ declare class node { | ||||
|     * Send 1 or more messages asynchronously | ||||
|     * @param {object | object[]} msg  The msg object | ||||
|     * @param {Boolean} [clone=true]  Flag to indicate the `msg` should be cloned. Default = `true` | ||||
|     * @see node-red documentation [writing-functions: sending messages asynchronously](https://nodered.org/docs/user-guide/writing-functions#sending-messages-asynchronously) | ||||
|     * @see Node-RED documentation [writing-functions: sending messages asynchronously](https://nodered.org/docs/user-guide/writing-functions#sending-messages-asynchronously) | ||||
|     */ | ||||
|     static send(msg:object|object[], clone?:Boolean): void; | ||||
|     static send(msg:NodeMessage|NodeMessage[], clone?:Boolean): void; | ||||
|     /** Inform runtime this instance has completed its operation */ | ||||
|     static done(); | ||||
|     /** Send an error to the console and debug side bar. Include `msg` in the 2nd parameter to trigger the catch node.  */ | ||||
|     static error(err:string|Error, msg?:object); | ||||
|     static error(err:string|Error, msg?:NodeMessage); | ||||
|     /** Log a warn message to the console and debug sidebar */ | ||||
|     static warn(warning:string|object); | ||||
|     /** Log an info message to the console (not sent to sidebar)' */ | ||||
|     static log(info:string|object); | ||||
|     /** Sets the status icon and text underneath the node. | ||||
|     * @param {NodeStatus} status - The status object `{fill, shape, text}` | ||||
|     * @see node-red documentation [writing-functions: adding-status](https://nodered.org/docs/user-guide/writing-functions#adding-status) | ||||
|     * @see Node-RED documentation [writing-functions: adding-status](https://nodered.org/docs/user-guide/writing-functions#adding-status) | ||||
|     */ | ||||
|     static status(status:NodeStatus); | ||||
|     /** Sets the status text underneath the node. | ||||
|     * @param {string} status - The status to display | ||||
|     * @see node-red documentation [writing-functions: adding-status](https://nodered.org/docs/user-guide/writing-functions#adding-status) | ||||
|     * @see Node-RED documentation [writing-functions: adding-status](https://nodered.org/docs/user-guide/writing-functions#adding-status) | ||||
|     */ | ||||
|     static status(status:string|boolean|number); | ||||
|     /** the id of this node */ | ||||
| @@ -264,9 +263,12 @@ declare class global { | ||||
|     /** Get an array of the keys in the context store */ | ||||
|     static keys(store: string, callback: Function); | ||||
| } | ||||
|  | ||||
| // (string & {}) is a workaround for offering string type completion without enforcing it. See https://github.com/microsoft/TypeScript/issues/29729#issuecomment-567871939 | ||||
| type NR_ENV_NAME_STRING = 'NR_NODE_ID'|'NR_NODE_NAME'|'NR_NODE_PATH'|'NR_GROUP_ID'|'NR_GROUP_NAME'|'NR_FLOW_ID'|'NR_FLOW_NAME'|'NR_SUBFLOW_ID'|'NR_SUBFLOW_NAME'|'NR_SUBFLOW_PATH' | (string & {}) | ||||
| declare class env { | ||||
|     /**  | ||||
|      * Get an environment variable value   | ||||
|      * Get an environment variable value defined in the OS, or in the global/flow/subflow/group environment variables.   | ||||
|      *  | ||||
|      * Predefined node-red variables...   | ||||
|      *   * `NR_NODE_ID` - the ID of the node | ||||
| @@ -276,9 +278,16 @@ declare class env { | ||||
|      *   * `NR_GROUP_NAME` - the Name of the containing group | ||||
|      *   * `NR_FLOW_ID` - the ID of the flow the node is on | ||||
|      *   * `NR_FLOW_NAME` - the Name of the flow the node is on | ||||
|      * @param name Name of the environment variable to get | ||||
|      *   * `NR_SUBFLOW_ID` - the ID of the subflow the node is in | ||||
|      *   * `NR_SUBFLOW_NAME` - the Name of the subflow the node is in | ||||
|      *   * `NR_SUBFLOW_PATH` - the Path of the subflow the node is in | ||||
|      * @param name - The name of the environment variable | ||||
|      * @example  | ||||
|      * ```const flowName = env.get("NR_FLOW_NAME");``` | ||||
|      * ```const flowName = env.get("NR_FLOW_NAME") // get the name of the flow``` | ||||
|      * @example  | ||||
|      * ```const systemHomeDir = env.get("HOME") // get the user's home directory``` | ||||
|      * @example  | ||||
|      * ```const systemHomeDir = env.get("LABEL1") // get the value of a global/flow/subflow/group defined variable named "LABEL1"``` | ||||
|      */ | ||||
|     static get(name:string) :any; | ||||
|     static get(name:NR_ENV_NAME_STRING) :any; | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
|  | ||||
| /* NOTE: Do not edit directly! This file is generated using `npm run update-types` in https://github.com/Steve-Mcl/monaco-editor-esm-i18n */ | ||||
| /* NOTE: Do not edit directly! This file is generated using `npm run update-types` in https://github.com/node-red/nr-monaco-build */ | ||||
|  | ||||
| /** | ||||
|  * The `assert` module provides a set of assertion functions for verifying | ||||
|  * invariants. | ||||
|  * @see [source](https://github.com/nodejs/node/blob/v16.9.0/lib/assert.js) | ||||
|  * @see [source](https://github.com/nodejs/node/blob/v18.0.0/lib/assert.js) | ||||
|  */ | ||||
| declare module 'assert' { | ||||
|     /** | ||||
| @@ -290,8 +290,8 @@ declare module 'assert' { | ||||
|          * > Stability: 3 - Legacy: Use {@link strictEqual} instead. | ||||
|          * | ||||
|          * Tests shallow, coercive equality between the `actual` and `expected` parameters | ||||
|          * using the [Abstract Equality Comparison](https://tc39.github.io/ecma262/#sec-abstract-equality-comparison) ( `==` ). `NaN` is special handled | ||||
|          * and treated as being identical in case both sides are `NaN`. | ||||
|          * using the [`==` operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Equality). `NaN` is specially handled | ||||
|          * and treated as being identical if both sides are `NaN`. | ||||
|          * | ||||
|          * ```js | ||||
|          * import assert from 'assert'; | ||||
| @@ -323,9 +323,8 @@ declare module 'assert' { | ||||
|          * | ||||
|          * > Stability: 3 - Legacy: Use {@link notStrictEqual} instead. | ||||
|          * | ||||
|          * Tests shallow, coercive inequality with the [Abstract Equality Comparison](https://tc39.github.io/ecma262/#sec-abstract-equality-comparison)(`!=` ). `NaN` is special handled and treated as | ||||
|          * being identical in case both | ||||
|          * sides are `NaN`. | ||||
|          * Tests shallow, coercive inequality with the [`!=` operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Inequality). `NaN` is | ||||
|          * specially handled and treated as being identical if both sides are `NaN`. | ||||
|          * | ||||
|          * ```js | ||||
|          * import assert from 'assert'; | ||||
| @@ -415,7 +414,7 @@ declare module 'assert' { | ||||
|         function notDeepEqual(actual: unknown, expected: unknown, message?: string | Error): void; | ||||
|         /** | ||||
|          * Tests strict equality between the `actual` and `expected` parameters as | ||||
|          * determined by the [SameValue Comparison](https://tc39.github.io/ecma262/#sec-samevalue). | ||||
|          * determined by [`Object.is()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is). | ||||
|          * | ||||
|          * ```js | ||||
|          * import assert from 'assert/strict'; | ||||
| @@ -453,7 +452,7 @@ declare module 'assert' { | ||||
|         function strictEqual<T>(actual: unknown, expected: T, message?: string | Error): asserts actual is T; | ||||
|         /** | ||||
|          * Tests strict inequality between the `actual` and `expected` parameters as | ||||
|          * determined by the [SameValue Comparison](https://tc39.github.io/ecma262/#sec-samevalue). | ||||
|          * determined by [`Object.is()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is). | ||||
|          * | ||||
|          * ```js | ||||
|          * import assert from 'assert/strict'; | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
|  | ||||
| /* NOTE: Do not edit directly! This file is generated using `npm run update-types` in https://github.com/Steve-Mcl/monaco-editor-esm-i18n */ | ||||
| /* NOTE: Do not edit directly! This file is generated using `npm run update-types` in https://github.com/node-red/nr-monaco-build */ | ||||
|  | ||||
| declare module 'assert/strict' { | ||||
|     import { strict } from 'node:assert'; | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
|  | ||||
| /* NOTE: Do not edit directly! This file is generated using `npm run update-types` in https://github.com/Steve-Mcl/monaco-editor-esm-i18n */ | ||||
| /* NOTE: Do not edit directly! This file is generated using `npm run update-types` in https://github.com/node-red/nr-monaco-build */ | ||||
|  | ||||
| /** | ||||
|  * The `async_hooks` module provides an API to track asynchronous resources. It | ||||
| @@ -9,7 +9,7 @@ | ||||
|  * import async_hooks from 'async_hooks'; | ||||
|  * ``` | ||||
|  * @experimental | ||||
|  * @see [source](https://github.com/nodejs/node/blob/v16.9.0/lib/async_hooks.js) | ||||
|  * @see [source](https://github.com/nodejs/node/blob/v18.0.0/lib/async_hooks.js) | ||||
|  */ | ||||
| declare module 'async_hooks' { | ||||
|     /** | ||||
| @@ -367,7 +367,7 @@ declare module 'async_hooks' { | ||||
|      * | ||||
|      * Each instance of `AsyncLocalStorage` maintains an independent storage context. | ||||
|      * Multiple instances can safely exist simultaneously without risk of interfering | ||||
|      * with each other data. | ||||
|      * with each other's data. | ||||
|      * @since v13.10.0, v12.17.0 | ||||
|      */ | ||||
|     class AsyncLocalStorage<T> { | ||||
| @@ -398,8 +398,9 @@ declare module 'async_hooks' { | ||||
|         getStore(): T | undefined; | ||||
|         /** | ||||
|          * Runs a function synchronously within a context and returns its | ||||
|          * return value. The store is not accessible outside of the callback function or | ||||
|          * the asynchronous operations created within the callback. | ||||
|          * return value. The store is not accessible outside of the callback function. | ||||
|          * The store is accessible to any asynchronous operations created within the | ||||
|          * callback. | ||||
|          * | ||||
|          * The optional `args` are passed to the callback function. | ||||
|          * | ||||
| @@ -413,6 +414,9 @@ declare module 'async_hooks' { | ||||
|          * try { | ||||
|          *   asyncLocalStorage.run(store, () => { | ||||
|          *     asyncLocalStorage.getStore(); // Returns the store object | ||||
|          *     setTimeout(() => { | ||||
|          *       asyncLocalStorage.getStore(); // Returns the store object | ||||
|          *     }, 200); | ||||
|          *     throw new Error(); | ||||
|          *   }); | ||||
|          * } catch (e) { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
|  | ||||
| /* NOTE: Do not edit directly! This file is generated using `npm run update-types` in https://github.com/Steve-Mcl/monaco-editor-esm-i18n */ | ||||
| /* NOTE: Do not edit directly! This file is generated using `npm run update-types` in https://github.com/node-red/nr-monaco-build */ | ||||
|  | ||||
| /** | ||||
|  * `Buffer` objects are used to represent a fixed-length sequence of bytes. Many | ||||
| @@ -44,7 +44,7 @@ | ||||
|  * // Creates a Buffer containing the Latin-1 bytes [0x74, 0xe9, 0x73, 0x74]. | ||||
|  * const buf7 = Buffer.from('tést', 'latin1'); | ||||
|  * ``` | ||||
|  * @see [source](https://github.com/nodejs/node/blob/v16.9.0/lib/buffer.js) | ||||
|  * @see [source](https://github.com/nodejs/node/blob/v18.0.0/lib/buffer.js) | ||||
|  */ | ||||
| declare module 'buffer' { | ||||
|     import { BinaryLike } from 'node:crypto'; | ||||
| @@ -117,18 +117,17 @@ declare module 'buffer' { | ||||
|     /** | ||||
|      * A [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) encapsulates immutable, raw data that can be safely shared across | ||||
|      * multiple worker threads. | ||||
|      * @since v15.7.0 | ||||
|      * @experimental | ||||
|      * @since v15.7.0, v14.18.0 | ||||
|      */ | ||||
|     export class Blob { | ||||
|         /** | ||||
|          * The total size of the `Blob` in bytes. | ||||
|          * @since v15.7.0 | ||||
|          * @since v15.7.0, v14.18.0 | ||||
|          */ | ||||
|         readonly size: number; | ||||
|         /** | ||||
|          * The content-type of the `Blob`. | ||||
|          * @since v15.7.0 | ||||
|          * @since v15.7.0, v14.18.0 | ||||
|          */ | ||||
|         readonly type: string; | ||||
|         /** | ||||
| @@ -143,13 +142,13 @@ declare module 'buffer' { | ||||
|         /** | ||||
|          * Returns a promise that fulfills with an [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) containing a copy of | ||||
|          * the `Blob` data. | ||||
|          * @since v15.7.0 | ||||
|          * @since v15.7.0, v14.18.0 | ||||
|          */ | ||||
|         arrayBuffer(): Promise<ArrayBuffer>; | ||||
|         /** | ||||
|          * Creates and returns a new `Blob` containing a subset of this `Blob` objects | ||||
|          * data. The original `Blob` is not altered. | ||||
|          * @since v15.7.0 | ||||
|          * @since v15.7.0, v14.18.0 | ||||
|          * @param start The starting index. | ||||
|          * @param end The ending index. | ||||
|          * @param type The content-type for the new `Blob` | ||||
| @@ -158,7 +157,7 @@ declare module 'buffer' { | ||||
|         /** | ||||
|          * Returns a promise that fulfills with the contents of the `Blob` decoded as a | ||||
|          * UTF-8 string. | ||||
|          * @since v15.7.0 | ||||
|          * @since v15.7.0, v14.18.0 | ||||
|          */ | ||||
|         text(): Promise<string>; | ||||
|         /** | ||||
| @@ -169,6 +168,12 @@ declare module 'buffer' { | ||||
|     } | ||||
|     export import atob = globalThis.atob; | ||||
|     export import btoa = globalThis.btoa; | ||||
|  | ||||
|     import { Blob as NodeBlob } from 'buffer'; | ||||
|     // This conditional type will be the existing global Blob in a browser, or | ||||
|     // the copy below in a Node environment. | ||||
|     type __Blob = typeof globalThis extends { onmessage: any, Blob: infer T } | ||||
|         ? T : NodeBlob; | ||||
|     global { | ||||
|         // Buffer class | ||||
|         type BufferEncoding = 'ascii' | 'utf8' | 'utf-8' | 'utf16le' | 'ucs2' | 'ucs-2' | 'base64' | 'base64url' | 'latin1' | 'binary' | 'hex'; | ||||
| @@ -394,7 +399,7 @@ declare module 'buffer' { | ||||
|              * @since v0.11.13 | ||||
|              * @return Either `-1`, `0`, or `1`, depending on the result of the comparison. See `compare` for details. | ||||
|              */ | ||||
|             compare(buf1: Uint8Array, buf2: Uint8Array): number; | ||||
|             compare(buf1: Uint8Array, buf2: Uint8Array): -1 | 0 | 1; | ||||
|             /** | ||||
|              * Allocates a new `Buffer` of `size` bytes. If `fill` is `undefined`, the`Buffer` will be zero-filled. | ||||
|              * | ||||
| @@ -447,7 +452,7 @@ declare module 'buffer' { | ||||
|              * Allocates a new `Buffer` of `size` bytes. If `size` is larger than {@link constants.MAX_LENGTH} or smaller than 0, `ERR_INVALID_ARG_VALUE` is thrown. | ||||
|              * | ||||
|              * The underlying memory for `Buffer` instances created in this way is _not_ | ||||
|              * _initialized_. The contents of the newly created `Buffer` are unknown and_may contain sensitive data_. Use `Buffer.alloc()` instead to initialize`Buffer` instances with zeroes. | ||||
|              * _initialized_. The contents of the newly created `Buffer` are unknown and _may contain sensitive data_. Use `Buffer.alloc()` instead to initialize`Buffer` instances with zeroes. | ||||
|              * | ||||
|              * ```js | ||||
|              * import { Buffer } from 'buffer'; | ||||
| @@ -485,7 +490,7 @@ declare module 'buffer' { | ||||
|              * if `size` is 0. | ||||
|              * | ||||
|              * The underlying memory for `Buffer` instances created in this way is _not_ | ||||
|              * _initialized_. The contents of the newly created `Buffer` are unknown and_may contain sensitive data_. Use `buf.fill(0)` to initialize | ||||
|              * _initialized_. The contents of the newly created `Buffer` are unknown and _may contain sensitive data_. Use `buf.fill(0)` to initialize | ||||
|              * such `Buffer` instances with zeroes. | ||||
|              * | ||||
|              * When using `Buffer.allocUnsafe()` to allocate new `Buffer` instances, | ||||
| @@ -708,7 +713,7 @@ declare module 'buffer' { | ||||
|              * @param [sourceStart=0] The offset within `buf` at which to begin comparison. | ||||
|              * @param [sourceEnd=buf.length] The offset within `buf` at which to end comparison (not inclusive). | ||||
|              */ | ||||
|             compare(target: Uint8Array, targetStart?: number, targetEnd?: number, sourceStart?: number, sourceEnd?: number): number; | ||||
|             compare(target: Uint8Array, targetStart?: number, targetEnd?: number, sourceStart?: number, sourceEnd?: number): -1 | 0 | 1; | ||||
|             /** | ||||
|              * Copies data from a region of `buf` to a region in `target`, even if the `target`memory region overlaps with `buf`. | ||||
|              * | ||||
| @@ -767,8 +772,6 @@ declare module 'buffer' { | ||||
|              * Returns a new `Buffer` that references the same memory as the original, but | ||||
|              * offset and cropped by the `start` and `end` indices. | ||||
|              * | ||||
|              * This is the same behavior as `buf.subarray()`. | ||||
|              * | ||||
|              * This method is not compatible with the `Uint8Array.prototype.slice()`, | ||||
|              * which is a superclass of `Buffer`. To copy the slice, use`Uint8Array.prototype.slice()`. | ||||
|              * | ||||
| @@ -784,8 +787,17 @@ declare module 'buffer' { | ||||
|              * | ||||
|              * console.log(buf.toString()); | ||||
|              * // Prints: buffer | ||||
|              * | ||||
|              * // With buf.slice(), the original buffer is modified. | ||||
|              * const notReallyCopiedBuf = buf.slice(); | ||||
|              * notReallyCopiedBuf[0]++; | ||||
|              * console.log(notReallyCopiedBuf.toString()); | ||||
|              * // Prints: cuffer | ||||
|              * console.log(buf.toString()); | ||||
|              * // Also prints: cuffer (!) | ||||
|              * ``` | ||||
|              * @since v0.3.0 | ||||
|              * @deprecated Use `subarray` instead. | ||||
|              * @param [start=0] Where the new `Buffer` will start. | ||||
|              * @param [end=buf.length] Where the new `Buffer` will end (not inclusive). | ||||
|              */ | ||||
| @@ -1952,7 +1964,7 @@ declare module 'buffer' { | ||||
|              * | ||||
|              * * a string, `value` is interpreted according to the character encoding in`encoding`. | ||||
|              * * a `Buffer` or [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array), `value` will be used in its entirety. | ||||
|              * To compare a partial `Buffer`, use `buf.slice()`. | ||||
|              * To compare a partial `Buffer`, use `buf.subarray`. | ||||
|              * * a number, `value` will be interpreted as an unsigned 8-bit integer | ||||
|              * value between `0` and `255`. | ||||
|              * | ||||
| @@ -2208,7 +2220,7 @@ declare module 'buffer' { | ||||
|          * **binary data and predate the introduction of typed arrays in JavaScript.** | ||||
|          * **For code running using Node.js APIs, converting between base64-encoded strings** | ||||
|          * **and binary data should be performed using `Buffer.from(str, 'base64')` and`buf.toString('base64')`.** | ||||
|          * @since v15.13.0 | ||||
|          * @since v15.13.0, v14.17.0 | ||||
|          * @deprecated Use `Buffer.from(data, 'base64')` instead. | ||||
|          * @param data The Base64-encoded input string. | ||||
|          */ | ||||
| @@ -2224,11 +2236,24 @@ declare module 'buffer' { | ||||
|          * **binary data and predate the introduction of typed arrays in JavaScript.** | ||||
|          * **For code running using Node.js APIs, converting between base64-encoded strings** | ||||
|          * **and binary data should be performed using `Buffer.from(str, 'base64')` and`buf.toString('base64')`.** | ||||
|          * @since v15.13.0 | ||||
|          * @since v15.13.0, v14.17.0 | ||||
|          * @deprecated Use `buf.toString('base64')` instead. | ||||
|          * @param data An ASCII (Latin1) string. | ||||
|          */ | ||||
|         function btoa(data: string): string; | ||||
|  | ||||
|         interface Blob extends __Blob {} | ||||
|         /** | ||||
|          * `Blob` class is a global reference for `require('node:buffer').Blob` | ||||
|          * https://nodejs.org/api/buffer.html#class-blob | ||||
|          * @since v18.0.0 | ||||
|          */ | ||||
|         var Blob: typeof globalThis extends { | ||||
|             onmessage: any; | ||||
|             Blob: infer T; | ||||
|         } | ||||
|             ? T | ||||
|             : typeof NodeBlob; | ||||
|     } | ||||
| } | ||||
| declare module 'node:buffer' { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
|  | ||||
| /* NOTE: Do not edit directly! This file is generated using `npm run update-types` in https://github.com/Steve-Mcl/monaco-editor-esm-i18n */ | ||||
| /* NOTE: Do not edit directly! This file is generated using `npm run update-types` in https://github.com/node-red/nr-monaco-build */ | ||||
|  | ||||
| /** | ||||
|  * The `child_process` module provides the ability to spawn subprocesses in | ||||
| @@ -31,8 +31,11 @@ | ||||
|  * identical to the behavior of pipes in the shell. Use the `{ stdio: 'ignore' }`option if the output will not be consumed. | ||||
|  * | ||||
|  * The command lookup is performed using the `options.env.PATH` environment | ||||
|  * variable if it is in the `options` object. Otherwise, `process.env.PATH` is | ||||
|  * used. | ||||
|  * variable if `env` is in the `options` object. Otherwise, `process.env.PATH` is | ||||
|  * used. If `options.env` is set without `PATH`, lookup on Unix is performed | ||||
|  * on a default search path search of `/usr/bin:/bin` (see your operating system's | ||||
|  * manual for execvpe/execvp), on Windows the current processes environment | ||||
|  * variable `PATH` is used. | ||||
|  * | ||||
|  * On Windows, environment variables are case-insensitive. Node.js | ||||
|  * lexicographically sorts the `env` keys and uses the first one that | ||||
| @@ -63,7 +66,7 @@ | ||||
|  * For certain use cases, such as automating shell scripts, the `synchronous counterparts` may be more convenient. In many cases, however, | ||||
|  * the synchronous methods can have significant impact on performance due to | ||||
|  * stalling the event loop while spawned processes complete. | ||||
|  * @see [source](https://github.com/nodejs/node/blob/v16.9.0/lib/child_process.js) | ||||
|  * @see [source](https://github.com/nodejs/node/blob/v18.0.0/lib/child_process.js) | ||||
|  */ | ||||
| declare module 'child_process' { | ||||
|     import { ObjectEncodingOptions } from 'node:fs'; | ||||
| @@ -624,7 +627,7 @@ declare module 'child_process' { | ||||
|     } | ||||
|     interface CommonOptions extends ProcessEnvOptions { | ||||
|         /** | ||||
|          * @default false | ||||
|          * @default true | ||||
|          */ | ||||
|         windowsHide?: boolean | undefined; | ||||
|         /** | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
|  | ||||
| /* NOTE: Do not edit directly! This file is generated using `npm run update-types` in https://github.com/Steve-Mcl/monaco-editor-esm-i18n */ | ||||
| /* NOTE: Do not edit directly! This file is generated using `npm run update-types` in https://github.com/node-red/nr-monaco-build */ | ||||
|  | ||||
| /** | ||||
|  * A single instance of Node.js runs in a single thread. To take advantage of | ||||
|  * multi-core systems, the user will sometimes want to launch a cluster of Node.js | ||||
|  * processes to handle the load. | ||||
|  * Clusters of Node.js processes can be used to run multiple instances of Node.js | ||||
|  * that can distribute workloads among their application threads. When process | ||||
|  * isolation is not needed, use the `worker_threads` module instead, which | ||||
|  * allows running multiple application threads within a single Node.js instance. | ||||
|  * | ||||
|  * The cluster module allows easy creation of child processes that all share | ||||
|  * server ports. | ||||
| @@ -52,7 +53,7 @@ | ||||
|  * ``` | ||||
|  * | ||||
|  * On Windows, it is not yet possible to set up a named pipe server in a worker. | ||||
|  * @see [source](https://github.com/nodejs/node/blob/v16.9.0/lib/cluster.js) | ||||
|  * @see [source](https://github.com/nodejs/node/blob/v18.0.0/lib/cluster.js) | ||||
|  */ | ||||
| declare module 'cluster' { | ||||
|     import * as child from 'node:child_process'; | ||||
| @@ -102,9 +103,9 @@ declare module 'cluster' { | ||||
|         /** | ||||
|          * Send a message to a worker or primary, optionally with a handle. | ||||
|          * | ||||
|          * In the primary this sends a message to a specific worker. It is identical to `ChildProcess.send()`. | ||||
|          * In the primary, this sends a message to a specific worker. It is identical to `ChildProcess.send()`. | ||||
|          * | ||||
|          * In a worker this sends a message to the primary. It is identical to`process.send()`. | ||||
|          * In a worker, this sends a message to the primary. It is identical to`process.send()`. | ||||
|          * | ||||
|          * This example will echo back all messages from the primary: | ||||
|          * | ||||
| @@ -126,19 +127,13 @@ declare module 'cluster' { | ||||
|         send(message: child.Serializable, sendHandle: child.SendHandle, callback?: (error: Error | null) => void): boolean; | ||||
|         send(message: child.Serializable, sendHandle: child.SendHandle, options?: child.MessageOptions, callback?: (error: Error | null) => void): boolean; | ||||
|         /** | ||||
|          * This function will kill the worker. In the primary, it does this | ||||
|          * by disconnecting the `worker.process`, and once disconnected, killing | ||||
|          * with `signal`. In the worker, it does it by disconnecting the channel, | ||||
|          * and then exiting with code `0`. | ||||
|          * This function will kill the worker. In the primary worker, it does this by | ||||
|          * disconnecting the `worker.process`, and once disconnected, killing with`signal`. In the worker, it does it by killing the process with `signal`. | ||||
|          * | ||||
|          * Because `kill()` attempts to gracefully disconnect the worker process, it is | ||||
|          * susceptible to waiting indefinitely for the disconnect to complete. For example, | ||||
|          * if the worker enters an infinite loop, a graceful disconnect will never occur. | ||||
|          * If the graceful disconnect behavior is not needed, use `worker.process.kill()`. | ||||
|          * The `kill()` function kills the worker process without waiting for a graceful | ||||
|          * disconnect, it has the same behavior as `worker.process.kill()`. | ||||
|          * | ||||
|          * Causes `.exitedAfterDisconnect` to be set. | ||||
|          * | ||||
|          * This method is aliased as `worker.destroy()` for backward compatibility. | ||||
|          * This method is aliased as `worker.destroy()` for backwards compatibility. | ||||
|          * | ||||
|          * In a worker, `process.kill()` exists, but it is not this function; | ||||
|          * it is `kill()`. | ||||
| @@ -256,7 +251,8 @@ declare module 'cluster' { | ||||
|          */ | ||||
|         isDead(): boolean; | ||||
|         /** | ||||
|          * This property is `true` if the worker exited due to `.kill()` or`.disconnect()`. If the worker exited any other way, it is `false`. If the | ||||
|          * This property is `true` if the worker exited due to `.disconnect()`. | ||||
|          * If the worker exited any other way, it is `false`. If the | ||||
|          * worker has not exited, it is `undefined`. | ||||
|          * | ||||
|          * The boolean `worker.exitedAfterDisconnect` allows distinguishing between | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
|  | ||||
| /* NOTE: Do not edit directly! This file is generated using `npm run update-types` in https://github.com/Steve-Mcl/monaco-editor-esm-i18n */ | ||||
| /* NOTE: Do not edit directly! This file is generated using `npm run update-types` in https://github.com/node-red/nr-monaco-build */ | ||||
|  | ||||
| /** | ||||
|  * The `console` module provides a simple debugging console that is similar to the | ||||
| @@ -56,7 +56,7 @@ | ||||
|  * myConsole.warn(`Danger ${name}! Danger!`); | ||||
|  * // Prints: Danger Will Robinson! Danger!, to err | ||||
|  * ``` | ||||
|  * @see [source](https://github.com/nodejs/node/blob/v16.9.0/lib/console.js) | ||||
|  * @see [source](https://github.com/nodejs/node/blob/v18.0.0/lib/console.js) | ||||
|  */ | ||||
| declare module 'console' { | ||||
|     import console = require('node:console'); | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
|  | ||||
| /* NOTE: Do not edit directly! This file is generated using `npm run update-types` in https://github.com/Steve-Mcl/monaco-editor-esm-i18n */ | ||||
| /* NOTE: Do not edit directly! This file is generated using `npm run update-types` in https://github.com/node-red/nr-monaco-build */ | ||||
|  | ||||
| /** | ||||
|  * The `crypto` module provides cryptographic functionality that includes a set of | ||||
| @@ -16,12 +16,64 @@ | ||||
|  * // Prints: | ||||
|  * //   c0fa1bc00531bd78ef38c628449c5102aeabd49b5dc3a2a516ea6ea959d6658e | ||||
|  * ``` | ||||
|  * @see [source](https://github.com/nodejs/node/blob/v16.9.0/lib/crypto.js) | ||||
|  * @see [source](https://github.com/nodejs/node/blob/v18.0.0/lib/crypto.js) | ||||
|  */ | ||||
| declare module 'crypto' { | ||||
|     import * as stream from 'node:stream'; | ||||
|     import { PeerCertificate } from 'node:tls'; | ||||
|     interface Certificate { | ||||
|     /** | ||||
|      * SPKAC is a Certificate Signing Request mechanism originally implemented by | ||||
|      * Netscape and was specified formally as part of [HTML5's `keygen` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/keygen). | ||||
|      * | ||||
|      * `<keygen>` is deprecated since [HTML 5.2](https://www.w3.org/TR/html52/changes.html#features-removed) and new projects | ||||
|      * should not use this element anymore. | ||||
|      * | ||||
|      * The `crypto` module provides the `Certificate` class for working with SPKAC | ||||
|      * data. The most common usage is handling output generated by the HTML5`<keygen>` element. Node.js uses [OpenSSL's SPKAC | ||||
|      * implementation](https://www.openssl.org/docs/man1.1.0/apps/openssl-spkac.html) internally. | ||||
|      * @since v0.11.8 | ||||
|      */ | ||||
|     class Certificate { | ||||
|         /** | ||||
|          * ```js | ||||
|          * const { Certificate } = await import('crypto'); | ||||
|          * const spkac = getSpkacSomehow(); | ||||
|          * const challenge = Certificate.exportChallenge(spkac); | ||||
|          * console.log(challenge.toString('utf8')); | ||||
|          * // Prints: the challenge as a UTF8 string | ||||
|          * ``` | ||||
|          * @since v9.0.0 | ||||
|          * @param encoding The `encoding` of the `spkac` string. | ||||
|          * @return The challenge component of the `spkac` data structure, which includes a public key and a challenge. | ||||
|          */ | ||||
|         static exportChallenge(spkac: BinaryLike): Buffer; | ||||
|         /** | ||||
|          * ```js | ||||
|          * const { Certificate } = await import('crypto'); | ||||
|          * const spkac = getSpkacSomehow(); | ||||
|          * const publicKey = Certificate.exportPublicKey(spkac); | ||||
|          * console.log(publicKey); | ||||
|          * // Prints: the public key as <Buffer ...> | ||||
|          * ``` | ||||
|          * @since v9.0.0 | ||||
|          * @param encoding The `encoding` of the `spkac` string. | ||||
|          * @return The public key component of the `spkac` data structure, which includes a public key and a challenge. | ||||
|          */ | ||||
|         static exportPublicKey(spkac: BinaryLike, encoding?: string): Buffer; | ||||
|         /** | ||||
|          * ```js | ||||
|          * import { Buffer } from 'buffer'; | ||||
|          * const { Certificate } = await import('crypto'); | ||||
|          * | ||||
|          * const spkac = getSpkacSomehow(); | ||||
|          * console.log(Certificate.verifySpkac(Buffer.from(spkac))); | ||||
|          * // Prints: true or false | ||||
|          * ``` | ||||
|          * @since v9.0.0 | ||||
|          * @param encoding The `encoding` of the `spkac` string. | ||||
|          * @return `true` if the given `spkac` data structure is valid, `false` otherwise. | ||||
|          */ | ||||
|         static verifySpkac(spkac: NodeJS.ArrayBufferView): boolean; | ||||
|         /** | ||||
|          * @deprecated | ||||
|          * @param spkac | ||||
| @@ -45,31 +97,6 @@ declare module 'crypto' { | ||||
|          */ | ||||
|         verifySpkac(spkac: NodeJS.ArrayBufferView): boolean; | ||||
|     } | ||||
|     const Certificate: Certificate & { | ||||
|         /** @deprecated since v14.9.0 - Use static methods of `crypto.Certificate` instead. */ | ||||
|         new (): Certificate; | ||||
|         /** @deprecated since v14.9.0 - Use static methods of `crypto.Certificate` instead. */ | ||||
|         (): Certificate; | ||||
|         /** | ||||
|          * @param spkac | ||||
|          * @returns The challenge component of the `spkac` data structure, | ||||
|          * which includes a public key and a challenge. | ||||
|          */ | ||||
|         exportChallenge(spkac: BinaryLike): Buffer; | ||||
|         /** | ||||
|          * @param spkac | ||||
|          * @param encoding The encoding of the spkac string. | ||||
|          * @returns The public key component of the `spkac` data structure, | ||||
|          * which includes a public key and a challenge. | ||||
|          */ | ||||
|         exportPublicKey(spkac: BinaryLike, encoding?: string): Buffer; | ||||
|         /** | ||||
|          * @param spkac | ||||
|          * @returns `true` if the given `spkac` data structure is valid, | ||||
|          * `false` otherwise. | ||||
|          */ | ||||
|         verifySpkac(spkac: NodeJS.ArrayBufferView): boolean; | ||||
|     }; | ||||
|     namespace constants { | ||||
|         // https://nodejs.org/dist/latest-v10.x/docs/api/crypto.html#crypto_crypto_constants | ||||
|         const OPENSSL_VERSION_NUMBER: number; | ||||
| @@ -175,7 +202,7 @@ declare module 'crypto' { | ||||
|      * | ||||
|      * The `algorithm` is dependent on the available algorithms supported by the | ||||
|      * version of OpenSSL on the platform. Examples are `'sha256'`, `'sha512'`, etc. | ||||
|      * On recent releases of OpenSSL, `openssl list -digest-algorithms`(`openssl list-message-digest-algorithms` for older versions of OpenSSL) will | ||||
|      * On recent releases of OpenSSL, `openssl list -digest-algorithms` will | ||||
|      * display the available digest algorithms. | ||||
|      * | ||||
|      * Example: generating the sha256 sum of a file | ||||
| @@ -215,7 +242,7 @@ declare module 'crypto' { | ||||
|      * | ||||
|      * The `algorithm` is dependent on the available algorithms supported by the | ||||
|      * version of OpenSSL on the platform. Examples are `'sha256'`, `'sha512'`, etc. | ||||
|      * On recent releases of OpenSSL, `openssl list -digest-algorithms`(`openssl list-message-digest-algorithms` for older versions of OpenSSL) will | ||||
|      * On recent releases of OpenSSL, `openssl list -digest-algorithms` will | ||||
|      * display the available digest algorithms. | ||||
|      * | ||||
|      * The `key` is the HMAC key used to generate the cryptographic HMAC hash. If it is | ||||
| @@ -253,7 +280,7 @@ declare module 'crypto' { | ||||
|      */ | ||||
|     function createHmac(algorithm: string, key: BinaryLike | KeyObject, options?: stream.TransformOptions): Hmac; | ||||
|     // https://nodejs.org/api/buffer.html#buffer_buffers_and_character_encodings | ||||
|     type BinaryToTextEncoding = 'base64' | 'base64url' | 'hex'; | ||||
|     type BinaryToTextEncoding = 'base64' | 'base64url' | 'hex' | 'binary'; | ||||
|     type CharacterEncoding = 'utf8' | 'utf-8' | 'utf16le' | 'latin1'; | ||||
|     type LegacyCharacterEncoding = 'ascii' | 'binary' | 'ucs2' | 'ucs-2'; | ||||
|     type Encoding = BinaryToTextEncoding | CharacterEncoding | LegacyCharacterEncoding; | ||||
| @@ -662,12 +689,13 @@ declare module 'crypto' { | ||||
|      * Creates and returns a `Cipher` object that uses the given `algorithm` and`password`. | ||||
|      * | ||||
|      * The `options` argument controls stream behavior and is optional except when a | ||||
|      * cipher in CCM or OCB mode is used (e.g. `'aes-128-ccm'`). In that case, the`authTagLength` option is required and specifies the length of the | ||||
|      * cipher in CCM or OCB mode (e.g. `'aes-128-ccm'`) is used. In that case, the`authTagLength` option is required and specifies the length of the | ||||
|      * authentication tag in bytes, see `CCM mode`. In GCM mode, the `authTagLength`option is not required but can be used to set the length of the authentication | ||||
|      * tag that will be returned by `getAuthTag()` and defaults to 16 bytes. | ||||
|      * For `chacha20-poly1305`, the `authTagLength` option defaults to 16 bytes. | ||||
|      * | ||||
|      * The `algorithm` is dependent on OpenSSL, examples are `'aes192'`, etc. On | ||||
|      * recent OpenSSL releases, `openssl list -cipher-algorithms`(`openssl list-cipher-algorithms` for older versions of OpenSSL) will | ||||
|      * recent OpenSSL releases, `openssl list -cipher-algorithms` will | ||||
|      * display the available cipher algorithms. | ||||
|      * | ||||
|      * The `password` is used to derive the cipher key and initialization vector (IV). | ||||
| @@ -700,12 +728,13 @@ declare module 'crypto' { | ||||
|      * initialization vector (`iv`). | ||||
|      * | ||||
|      * The `options` argument controls stream behavior and is optional except when a | ||||
|      * cipher in CCM or OCB mode is used (e.g. `'aes-128-ccm'`). In that case, the`authTagLength` option is required and specifies the length of the | ||||
|      * cipher in CCM or OCB mode (e.g. `'aes-128-ccm'`) is used. In that case, the`authTagLength` option is required and specifies the length of the | ||||
|      * authentication tag in bytes, see `CCM mode`. In GCM mode, the `authTagLength`option is not required but can be used to set the length of the authentication | ||||
|      * tag that will be returned by `getAuthTag()` and defaults to 16 bytes. | ||||
|      * For `chacha20-poly1305`, the `authTagLength` option defaults to 16 bytes. | ||||
|      * | ||||
|      * The `algorithm` is dependent on OpenSSL, examples are `'aes192'`, etc. On | ||||
|      * recent OpenSSL releases, `openssl list -cipher-algorithms`(`openssl list-cipher-algorithms` for older versions of OpenSSL) will | ||||
|      * recent OpenSSL releases, `openssl list -cipher-algorithms` will | ||||
|      * display the available cipher algorithms. | ||||
|      * | ||||
|      * The `key` is the raw key used by the `algorithm` and `iv` is an [initialization vector](https://en.wikipedia.org/wiki/Initialization_vector). Both arguments must be `'utf8'` encoded | ||||
| @@ -925,8 +954,9 @@ declare module 'crypto' { | ||||
|      * Creates and returns a `Decipher` object that uses the given `algorithm` and`password` (key). | ||||
|      * | ||||
|      * The `options` argument controls stream behavior and is optional except when a | ||||
|      * cipher in CCM or OCB mode is used (e.g. `'aes-128-ccm'`). In that case, the`authTagLength` option is required and specifies the length of the | ||||
|      * cipher in CCM or OCB mode (e.g. `'aes-128-ccm'`) is used. In that case, the`authTagLength` option is required and specifies the length of the | ||||
|      * authentication tag in bytes, see `CCM mode`. | ||||
|      * For `chacha20-poly1305`, the `authTagLength` option defaults to 16 bytes. | ||||
|      * | ||||
|      * The implementation of `crypto.createDecipher()` derives keys using the OpenSSL | ||||
|      * function [`EVP_BytesToKey`](https://www.openssl.org/docs/man1.1.0/crypto/EVP_BytesToKey.html) with the digest algorithm set to MD5, one | ||||
| @@ -951,12 +981,13 @@ declare module 'crypto' { | ||||
|      * Creates and returns a `Decipher` object that uses the given `algorithm`, `key`and initialization vector (`iv`). | ||||
|      * | ||||
|      * The `options` argument controls stream behavior and is optional except when a | ||||
|      * cipher in CCM or OCB mode is used (e.g. `'aes-128-ccm'`). In that case, the`authTagLength` option is required and specifies the length of the | ||||
|      * cipher in CCM or OCB mode (e.g. `'aes-128-ccm'`) is used. In that case, the`authTagLength` option is required and specifies the length of the | ||||
|      * authentication tag in bytes, see `CCM mode`. In GCM mode, the `authTagLength`option is not required but can be used to restrict accepted authentication tags | ||||
|      * to those with the specified length. | ||||
|      * For `chacha20-poly1305`, the `authTagLength` option defaults to 16 bytes. | ||||
|      * | ||||
|      * The `algorithm` is dependent on OpenSSL, examples are `'aes192'`, etc. On | ||||
|      * recent OpenSSL releases, `openssl list -cipher-algorithms`(`openssl list-cipher-algorithms` for older versions of OpenSSL) will | ||||
|      * recent OpenSSL releases, `openssl list -cipher-algorithms` will | ||||
|      * display the available cipher algorithms. | ||||
|      * | ||||
|      * The `key` is the raw key used by the `algorithm` and `iv` is an [initialization vector](https://en.wikipedia.org/wiki/Initialization_vector). Both arguments must be `'utf8'` encoded | ||||
| @@ -1162,13 +1193,11 @@ declare module 'crypto' { | ||||
|         format?: KeyFormat | undefined; | ||||
|         type?: 'pkcs1' | 'pkcs8' | 'sec1' | undefined; | ||||
|         passphrase?: string | Buffer | undefined; | ||||
|         encoding?: string | undefined; | ||||
|     } | ||||
|     interface PublicKeyInput { | ||||
|         key: string | Buffer; | ||||
|         format?: KeyFormat | undefined; | ||||
|         type?: 'pkcs1' | 'spki' | undefined; | ||||
|         encoding?: string | undefined; | ||||
|     } | ||||
|     /** | ||||
|      * Asynchronously generates a new random secret key of the given `length`. The`type` will determine which validations will be performed on the `length`. | ||||
| @@ -1279,7 +1308,6 @@ declare module 'crypto' { | ||||
|     interface VerifyKeyObjectInput extends SigningOptions { | ||||
|         key: KeyObject; | ||||
|     } | ||||
|     interface VerifyJsonWebKeyInput extends JsonWebKeyInput, SigningOptions {} | ||||
|     type KeyLike = string | Buffer | KeyObject; | ||||
|     /** | ||||
|      * The `Sign` class is a utility for generating signatures. It can be used in one | ||||
| @@ -1434,8 +1462,8 @@ declare module 'crypto' { | ||||
|          * be passed instead of a public key. | ||||
|          * @since v0.1.92 | ||||
|          */ | ||||
|         verify(object: KeyLike | VerifyKeyObjectInput | VerifyPublicKeyInput | VerifyJsonWebKeyInput, signature: NodeJS.ArrayBufferView): boolean; | ||||
|         verify(object: KeyLike | VerifyKeyObjectInput | VerifyPublicKeyInput | VerifyJsonWebKeyInput, signature: string, signature_format?: BinaryToTextEncoding): boolean; | ||||
|         verify(object: KeyLike | VerifyKeyObjectInput | VerifyPublicKeyInput, signature: NodeJS.ArrayBufferView): boolean; | ||||
|         verify(object: KeyLike | VerifyKeyObjectInput | VerifyPublicKeyInput, signature: string, signature_format?: BinaryToTextEncoding): boolean; | ||||
|     } | ||||
|     /** | ||||
|      * Creates a `DiffieHellman` key exchange object using the supplied `prime` and an | ||||
| @@ -1453,10 +1481,10 @@ declare module 'crypto' { | ||||
|      * @param [generator=2] | ||||
|      * @param generatorEncoding The `encoding` of the `generator` string. | ||||
|      */ | ||||
|     function createDiffieHellman(primeLength: number, generator?: number | NodeJS.ArrayBufferView): DiffieHellman; | ||||
|     function createDiffieHellman(prime: NodeJS.ArrayBufferView): DiffieHellman; | ||||
|     function createDiffieHellman(prime: string, primeEncoding: BinaryToTextEncoding): DiffieHellman; | ||||
|     function createDiffieHellman(prime: string, primeEncoding: BinaryToTextEncoding, generator: number | NodeJS.ArrayBufferView): DiffieHellman; | ||||
|     function createDiffieHellman(primeLength: number, generator?: number): DiffieHellman; | ||||
|     function createDiffieHellman(prime: ArrayBuffer | NodeJS.ArrayBufferView, generator?: number | ArrayBuffer | NodeJS.ArrayBufferView): DiffieHellman; | ||||
|     function createDiffieHellman(prime: ArrayBuffer | NodeJS.ArrayBufferView, generator: string, generatorEncoding: BinaryToTextEncoding): DiffieHellman; | ||||
|     function createDiffieHellman(prime: string, primeEncoding: BinaryToTextEncoding, generator?: number | ArrayBuffer | NodeJS.ArrayBufferView): DiffieHellman; | ||||
|     function createDiffieHellman(prime: string, primeEncoding: BinaryToTextEncoding, generator: string, generatorEncoding: BinaryToTextEncoding): DiffieHellman; | ||||
|     /** | ||||
|      * The `DiffieHellman` class is a utility for creating Diffie-Hellman key | ||||
| @@ -1805,7 +1833,7 @@ declare module 'crypto' { | ||||
|      * Return a random integer `n` such that `min <= n < max`.  This | ||||
|      * implementation avoids [modulo bias](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#Modulo_bias). | ||||
|      * | ||||
|      * The range (`max - min`) must be less than `2**48`. `min` and `max` must | ||||
|      * The range (`max - min`) must be less than 2^48. `min` and `max` must | ||||
|      * be [safe integers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isSafeInteger). | ||||
|      * | ||||
|      * If the `callback` function is not provided, the random integer is | ||||
| @@ -2281,11 +2309,11 @@ declare module 'crypto' { | ||||
|          * If `encoding` is specified, a string is returned; otherwise a `Buffer` is | ||||
|          * returned. | ||||
|          * @since v0.11.14 | ||||
|          * @param encoding The `encoding` of the return value. | ||||
|          * @param [encoding] The `encoding` of the return value. | ||||
|          * @param [format='uncompressed'] | ||||
|          * @return The EC Diffie-Hellman public key in the specified `encoding` and `format`. | ||||
|          */ | ||||
|         getPublicKey(): Buffer; | ||||
|         getPublicKey(encoding?: null, format?: ECDHKeyFormat): Buffer; | ||||
|         getPublicKey(encoding: BinaryToTextEncoding, format?: ECDHKeyFormat): string; | ||||
|         /** | ||||
|          * Sets the EC Diffie-Hellman private key. | ||||
| @@ -2316,7 +2344,8 @@ declare module 'crypto' { | ||||
|      * comparing HMAC digests or secret values like authentication cookies or [capability urls](https://www.w3.org/TR/capability-urls/). | ||||
|      * | ||||
|      * `a` and `b` must both be `Buffer`s, `TypedArray`s, or `DataView`s, and they | ||||
|      * must have the same byte length. | ||||
|      * must have the same byte length. An error is thrown if `a` and `b` have | ||||
|      * different byte lengths. | ||||
|      * | ||||
|      * If at least one of `a` and `b` is a `TypedArray` with more than one byte per | ||||
|      * entry, such as `Uint16Array`, the result will be computed using the platform | ||||
| @@ -2331,7 +2360,7 @@ declare module 'crypto' { | ||||
|     /** @deprecated since v10.0.0 */ | ||||
|     const DEFAULT_ENCODING: BufferEncoding; | ||||
|     type KeyType = 'rsa' | 'rsa-pss' | 'dsa' | 'ec' | 'ed25519' | 'ed448' | 'x25519' | 'x448'; | ||||
|     type KeyFormat = 'pem' | 'der' | 'jwk'; | ||||
|     type KeyFormat = 'pem' | 'der'; | ||||
|     interface BasePrivateKeyEncodingOptions<T extends KeyFormat> { | ||||
|         format: T; | ||||
|         cipher?: string | undefined; | ||||
| @@ -2942,16 +2971,11 @@ declare module 'crypto' { | ||||
|      * If the `callback` function is provided this function uses libuv's threadpool. | ||||
|      * @since v12.0.0 | ||||
|      */ | ||||
|     function verify(algorithm: string | null | undefined, data: NodeJS.ArrayBufferView, key: KeyLike | VerifyKeyObjectInput | VerifyPublicKeyInput, signature: NodeJS.ArrayBufferView): boolean; | ||||
|     function verify( | ||||
|         algorithm: string | null | undefined, | ||||
|         data: NodeJS.ArrayBufferView, | ||||
|         key: KeyLike | VerifyKeyObjectInput | VerifyPublicKeyInput | VerifyJsonWebKeyInput, | ||||
|         signature: NodeJS.ArrayBufferView | ||||
|     ): boolean; | ||||
|     function verify( | ||||
|         algorithm: string | null | undefined, | ||||
|         data: NodeJS.ArrayBufferView, | ||||
|         key: KeyLike | VerifyKeyObjectInput | VerifyPublicKeyInput | VerifyJsonWebKeyInput, | ||||
|         key: KeyLike | VerifyKeyObjectInput | VerifyPublicKeyInput, | ||||
|         signature: NodeJS.ArrayBufferView, | ||||
|         callback: (error: Error | null, result: boolean) => void | ||||
|     ): void; | ||||
| @@ -3100,34 +3124,33 @@ declare module 'crypto' { | ||||
|          */ | ||||
|         disableEntropyCache?: boolean | undefined; | ||||
|     } | ||||
|     type UUID = `${string}-${string}-${string}-${string}-${string}`; | ||||
|     /** | ||||
|      * Generates a random [RFC 4122](https://www.rfc-editor.org/rfc/rfc4122.txt) version 4 UUID. The UUID is generated using a | ||||
|      * cryptographic pseudorandom number generator. | ||||
|      * @since v15.6.0 | ||||
|      * @since v15.6.0, v14.17.0 | ||||
|      */ | ||||
|     function randomUUID(options?: RandomUUIDOptions): UUID; | ||||
|     function randomUUID(options?: RandomUUIDOptions): string; | ||||
|     interface X509CheckOptions { | ||||
|         /** | ||||
|          * @default 'always' | ||||
|          */ | ||||
|         subject?: 'always' | 'default' | 'never'; | ||||
|         subject: 'always' | 'never'; | ||||
|         /** | ||||
|          * @default true | ||||
|          */ | ||||
|         wildcards?: boolean; | ||||
|         wildcards: boolean; | ||||
|         /** | ||||
|          * @default true | ||||
|          */ | ||||
|         partialWildcards?: boolean; | ||||
|         partialWildcards: boolean; | ||||
|         /** | ||||
|          * @default false | ||||
|          */ | ||||
|         multiLabelWildcards?: boolean; | ||||
|         multiLabelWildcards: boolean; | ||||
|         /** | ||||
|          * @default false | ||||
|          */ | ||||
|         singleLabelSubdomains?: boolean; | ||||
|         singleLabelSubdomains: boolean; | ||||
|     } | ||||
|     /** | ||||
|      * Encapsulates an X509 certificate and provides read-only access to | ||||
| @@ -3144,12 +3167,16 @@ declare module 'crypto' { | ||||
|      */ | ||||
|     class X509Certificate { | ||||
|         /** | ||||
|          * Will be \`true\` if this is a Certificate Authority (ca) certificate. | ||||
|          * Will be \`true\` if this is a Certificate Authority (CA) certificate. | ||||
|          * @since v15.6.0 | ||||
|          */ | ||||
|         readonly ca: boolean; | ||||
|         /** | ||||
|          * The SHA-1 fingerprint of this certificate. | ||||
|          * | ||||
|          * Because SHA-1 is cryptographically broken and because the security of SHA-1 is | ||||
|          * significantly worse than that of algorithms that are commonly used to sign | ||||
|          * certificates, consider using `x509.fingerprint256` instead. | ||||
|          * @since v15.6.0 | ||||
|          */ | ||||
|         readonly fingerprint: string; | ||||
| @@ -3208,6 +3235,10 @@ declare module 'crypto' { | ||||
|         readonly raw: Buffer; | ||||
|         /** | ||||
|          * The serial number of this certificate. | ||||
|          * | ||||
|          * Serial numbers are assigned by certificate authorities and do not uniquely | ||||
|          * identify certificates. Consider using `x509.fingerprint256` as a unique | ||||
|          * identifier instead. | ||||
|          * @since v15.6.0 | ||||
|          */ | ||||
|         readonly serialNumber: string; | ||||
| @@ -3224,18 +3255,50 @@ declare module 'crypto' { | ||||
|         constructor(buffer: BinaryLike); | ||||
|         /** | ||||
|          * Checks whether the certificate matches the given email address. | ||||
|          * | ||||
|          * If the `'subject'` option is undefined or set to `'default'`, the certificate | ||||
|          * subject is only considered if the subject alternative name extension either does | ||||
|          * not exist or does not contain any email addresses. | ||||
|          * | ||||
|          * If the `'subject'` option is set to `'always'` and if the subject alternative | ||||
|          * name extension either does not exist or does not contain a matching email | ||||
|          * address, the certificate subject is considered. | ||||
|          * | ||||
|          * If the `'subject'` option is set to `'never'`, the certificate subject is never | ||||
|          * considered, even if the certificate contains no subject alternative names. | ||||
|          * @since v15.6.0 | ||||
|          * @return Returns `email` if the certificate matches, `undefined` if it does not. | ||||
|          */ | ||||
|         checkEmail(email: string, options?: Pick<X509CheckOptions, 'subject'>): string | undefined; | ||||
|         /** | ||||
|          * Checks whether the certificate matches the given host name. | ||||
|          * | ||||
|          * If the certificate matches the given host name, the matching subject name is | ||||
|          * returned. The returned name might be an exact match (e.g., `foo.example.com`) | ||||
|          * or it might contain wildcards (e.g., `*.example.com`). Because host name | ||||
|          * comparisons are case-insensitive, the returned subject name might also differ | ||||
|          * from the given `name` in capitalization. | ||||
|          * | ||||
|          * If the `'subject'` option is undefined or set to `'default'`, the certificate | ||||
|          * subject is only considered if the subject alternative name extension either does | ||||
|          * not exist or does not contain any DNS names. This behavior is consistent with [RFC 2818](https://www.rfc-editor.org/rfc/rfc2818.txt) ("HTTP Over TLS"). | ||||
|          * | ||||
|          * If the `'subject'` option is set to `'always'` and if the subject alternative | ||||
|          * name extension either does not exist or does not contain a matching DNS name, | ||||
|          * the certificate subject is considered. | ||||
|          * | ||||
|          * If the `'subject'` option is set to `'never'`, the certificate subject is never | ||||
|          * considered, even if the certificate contains no subject alternative names. | ||||
|          * @since v15.6.0 | ||||
|          * @return Returns `name` if the certificate matches, `undefined` if it does not. | ||||
|          * @return Returns a subject name that matches `name`, or `undefined` if no subject name matches `name`. | ||||
|          */ | ||||
|         checkHost(name: string, options?: X509CheckOptions): string | undefined; | ||||
|         /** | ||||
|          * Checks whether the certificate matches the given IP address (IPv4 or IPv6). | ||||
|          * | ||||
|          * Only [RFC 5280](https://www.rfc-editor.org/rfc/rfc5280.txt) `iPAddress` subject alternative names are considered, and they | ||||
|          * must match the given `ip` address exactly. Other subject alternative names as | ||||
|          * well as the subject field of the certificate are ignored. | ||||
|          * @since v15.6.0 | ||||
|          * @return Returns `ip` if the certificate matches, `undefined` if it does not. | ||||
|          */ | ||||
| @@ -3408,6 +3471,19 @@ declare module 'crypto' { | ||||
|      * @param [flags=crypto.constants.ENGINE_METHOD_ALL] | ||||
|      */ | ||||
|     function setEngine(engine: string, flags?: number): void; | ||||
|     /** | ||||
|      * A convenient alias for `crypto.webcrypto.getRandomValues()`. | ||||
|      * This implementation is not compliant with the Web Crypto spec, | ||||
|      * to write web-compatible code use `crypto.webcrypto.getRandomValues()` instead. | ||||
|      * @since v17.4.0 | ||||
|      * @returns Returns `typedArray`. | ||||
|      */ | ||||
|     function getRandomValues<T extends webcrypto.BufferSource>(typedArray: T): T; | ||||
|     /** | ||||
|      * A convenient alias for `crypto.webcrypto.subtle`. | ||||
|      * @since v17.4.0 | ||||
|      */ | ||||
|     const subtle: webcrypto.SubtleCrypto; | ||||
|     /** | ||||
|      * An implementation of the Web Crypto API standard. | ||||
|      * | ||||
| @@ -3565,7 +3641,7 @@ declare module 'crypto' { | ||||
|              * The UUID is generated using a cryptographic pseudorandom number generator. | ||||
|              * @since v16.7.0 | ||||
|              */ | ||||
|             randomUUID(): UUID; | ||||
|             randomUUID(): string; | ||||
|             CryptoKey: CryptoKeyConstructor; | ||||
|         } | ||||
|         // This constructor throws ILLEGAL_CONSTRUCTOR so it should not be newable. | ||||
| @@ -3650,17 +3726,22 @@ declare module 'crypto' { | ||||
|             /** | ||||
|              * Using the method and parameters specified in `algorithm` and the keying material provided by `baseKey`, | ||||
|              * `subtle.deriveBits()` attempts to generate `length` bits. | ||||
|              * The Node.js implementation requires that `length` is a multiple of `8`. | ||||
|              * The Node.js implementation requires that when `length` is a number it must be multiple of `8`. | ||||
|              * When `length` is `null` the maximum number of bits for a given algorithm is generated. This is allowed | ||||
|              * for the `'ECDH'`, `'X25519'`, and `'X448'` algorithms. | ||||
|              * If successful, the returned promise will be resolved with an `<ArrayBuffer>` containing the generated data. | ||||
|              * | ||||
|              * The algorithms currently supported include: | ||||
|              * | ||||
|              * - `'ECDH'` | ||||
|              * - `'X25519'` | ||||
|              * - `'X448'` | ||||
|              * - `'HKDF'` | ||||
|              * - `'PBKDF2'` | ||||
|              * @since v15.0.0 | ||||
|              */ | ||||
|             deriveBits(algorithm: AlgorithmIdentifier | EcdhKeyDeriveParams | HkdfParams | Pbkdf2Params, baseKey: CryptoKey, length: number): Promise<ArrayBuffer>; | ||||
|             deriveBits(algorithm: EcdhKeyDeriveParams, baseKey: CryptoKey, length: number | null): Promise<ArrayBuffer>; | ||||
|             deriveBits(algorithm: AlgorithmIdentifier | HkdfParams | Pbkdf2Params, baseKey: CryptoKey, length: number): Promise<ArrayBuffer>; | ||||
|             /** | ||||
|              * Using the method and parameters specified in `algorithm`, and the keying material provided by `baseKey`, | ||||
|              * `subtle.deriveKey()` attempts to generate a new <CryptoKey>` based on the method and parameters in `derivedKeyAlgorithm`. | ||||
| @@ -3671,6 +3752,8 @@ declare module 'crypto' { | ||||
|              * The algorithms currently supported include: | ||||
|              * | ||||
|              * - `'ECDH'` | ||||
|              * - `'X25519'` | ||||
|              * - `'X448'` | ||||
|              * - `'HKDF'` | ||||
|              * - `'PBKDF2'` | ||||
|              * @param keyUsages See {@link https://nodejs.org/docs/latest/api/webcrypto.html#cryptokeyusages Key usages}. | ||||
| @@ -3739,7 +3822,11 @@ declare module 'crypto' { | ||||
|              * - `'RSA-PSS'` | ||||
|              * - `'RSA-OAEP'` | ||||
|              * - `'ECDSA'` | ||||
|              * - `'Ed25519'` | ||||
|              * - `'Ed448'` | ||||
|              * - `'ECDH'` | ||||
|              * - `'X25519'` | ||||
|              * - `'X448'` | ||||
|              * The `<CryptoKey>` (secret key) generating algorithms supported include: | ||||
|              * | ||||
|              * - `'HMAC'` | ||||
| @@ -3787,6 +3874,8 @@ declare module 'crypto' { | ||||
|              * - `'RSASSA-PKCS1-v1_5'` | ||||
|              * - `'RSA-PSS'` | ||||
|              * - `'ECDSA'` | ||||
|              * - `'Ed25519'` | ||||
|              * - `'Ed448'` | ||||
|              * - `'HMAC'` | ||||
|              * @since v15.0.0 | ||||
|              */ | ||||
| @@ -3812,7 +3901,11 @@ declare module 'crypto' { | ||||
|              * - `'RSA-PSS'` | ||||
|              * - `'RSA-OAEP'` | ||||
|              * - `'ECDSA'` | ||||
|              * - `'Ed25519'` | ||||
|              * - `'Ed448'` | ||||
|              * - `'ECDH'` | ||||
|              * - `'X25519'` | ||||
|              * - `'X448'` | ||||
|              * - `'HMAC'` | ||||
|              * - `'AES-CTR'` | ||||
|              * - `'AES-CBC'` | ||||
| @@ -3841,6 +3934,8 @@ declare module 'crypto' { | ||||
|              * - `'RSASSA-PKCS1-v1_5'` | ||||
|              * - `'RSA-PSS'` | ||||
|              * - `'ECDSA'` | ||||
|              * - `'Ed25519'` | ||||
|              * - `'Ed448'` | ||||
|              * - `'HMAC'` | ||||
|              * @since v15.0.0 | ||||
|              */ | ||||
|   | ||||