Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2dcae9060f | ||
|
|
f4525cc62a | ||
|
|
d4aa70ddbf | ||
|
|
4c2aca3495 | ||
|
|
36c9ef0e95 | ||
|
|
7511b64523 | ||
|
|
7b4b4ebeeb | ||
|
|
c7ba025d39 | ||
|
|
b5e9b25319 | ||
|
|
82a254d22c | ||
|
|
0d7fd34297 | ||
|
|
1beb3d490d | ||
|
|
1fb5e8eb37 | ||
|
|
78dd29ba78 | ||
|
|
ba7b61082d | ||
|
|
1a9231809e | ||
|
|
df5c975869 | ||
|
|
2728dac873 | ||
|
|
88f55b9d06 | ||
|
|
301675052f | ||
|
|
45c434271d | ||
|
|
f523c798c3 | ||
|
|
41fd618884 | ||
|
|
f368242cb0 | ||
|
|
636e4e5b4a | ||
|
|
49710eb62d | ||
|
|
25c12f2b18 | ||
|
|
3ba8858260 | ||
|
|
81014001ac | ||
|
|
691d6f277e | ||
|
|
e7453a9305 | ||
|
|
005b616adb | ||
|
|
0ae1781d28 | ||
|
|
fb94840016 | ||
|
|
6864c044dc | ||
|
|
b30ba3e76b | ||
|
|
a4583f2db0 | ||
|
|
a161670b86 | ||
|
|
e6cf5a8d25 | ||
|
|
7ebc26fc7f | ||
|
|
c9dd933cec | ||
|
|
716eba621f | ||
|
|
982d507ae3 | ||
|
|
27a12951e3 | ||
|
|
4c8e4ee1b2 | ||
|
|
a15fdc401e | ||
|
|
7019e39318 | ||
|
|
9ca57193b4 | ||
|
|
175eb9853a | ||
|
|
e859d3354e | ||
|
|
adc6424211 | ||
|
|
74442096ba | ||
|
|
9f84a580fb | ||
|
|
2f5d3dfc38 | ||
|
|
9b18a45d35 | ||
|
|
6a30de37ca | ||
|
|
31075a165f | ||
|
|
53b7ee0cb2 | ||
|
|
ebe6972dd7 | ||
|
|
2acea64589 | ||
|
|
ffa3ff9c10 | ||
|
|
0ac3ff4511 | ||
|
|
3429a7a2a6 | ||
|
|
56fb2c2a3a | ||
|
|
77b4bb310e | ||
|
|
47ba03ec63 | ||
|
|
e331e1bb21 | ||
|
|
40f285f295 | ||
|
|
2d29346cbf | ||
|
|
f70ea2fe71 | ||
|
|
29aaa72c60 | ||
|
|
12c7d30894 | ||
|
|
38cb1e207b | ||
|
|
4c879c614b | ||
|
|
8e012f760a | ||
|
|
8c09befd74 | ||
|
|
6af49cf1fd | ||
|
|
4ea604f9fb | ||
|
|
9d97bf7ea1 | ||
|
|
eb015d712b | ||
|
|
1d04f4a42c | ||
|
|
11ee49a57a | ||
|
|
609731ff99 | ||
|
|
d6cb32ed25 | ||
|
|
3668a7426f | ||
|
|
cc82dcaafe | ||
|
|
ea03bdecc5 | ||
|
|
51071b463b | ||
|
|
ac0bd10212 | ||
|
|
1b73e355b7 | ||
|
|
b661580c60 | ||
|
|
dc4e07a692 | ||
|
|
f2313b31f1 | ||
|
|
f8810ee7e9 | ||
|
|
8ab4af8397 | ||
|
|
2382a559a5 | ||
|
|
5d889d824c | ||
|
|
bed21122ce | ||
|
|
ea8cf71101 | ||
|
|
f1a26e132e | ||
|
|
1db6984de3 | ||
|
|
636ff237dd | ||
|
|
02963ab6d5 | ||
|
|
d40f61292a | ||
|
|
7d94cb2ae4 | ||
|
|
bb80476767 | ||
|
|
1635bc9c45 | ||
|
|
f031b4eb89 | ||
|
|
93d4ab65ac | ||
|
|
ce921a032a | ||
|
|
322f904b4b | ||
|
|
982eeac58c | ||
|
|
0ab9717556 | ||
|
|
24a85de8e1 | ||
|
|
65f0b979a4 | ||
|
|
947f34a0ad | ||
|
|
0f60fd7181 | ||
|
|
251062e42e | ||
|
|
05a328976e | ||
|
|
503cb08b2f | ||
|
|
f71be20476 | ||
|
|
e6397cecc1 | ||
|
|
80ce80ece4 | ||
|
|
9546fcd631 | ||
|
|
b79aaf4327 | ||
|
|
76f9198f73 | ||
|
|
d60029deae | ||
|
|
d26f7d2511 | ||
|
|
33efe09b50 | ||
|
|
e0aef9bf00 | ||
|
|
02d1a2e0b1 | ||
|
|
4e3154e929 | ||
|
|
32c0713256 | ||
|
|
defe15bcec | ||
|
|
6f8e471f16 | ||
|
|
dc35abfd85 | ||
|
|
04a8c5d52a | ||
|
|
2cb370053d | ||
|
|
1707f3802a | ||
|
|
c87ba660ca | ||
|
|
ef8a2dbc38 | ||
|
|
40d04a53c4 | ||
|
|
d5ebb01ce3 | ||
|
|
ec66631a33 | ||
|
|
d66eb5dfc2 | ||
|
|
731b589ed8 | ||
|
|
fe92d5fc36 | ||
|
|
a335b400f1 | ||
|
|
f63e507ddf | ||
|
|
991cc0567d | ||
|
|
52d58036a4 | ||
|
|
26bff85792 | ||
|
|
2ccf0d1100 | ||
|
|
3be33186bc | ||
|
|
1a7f950ae2 | ||
|
|
8924dc8ab1 | ||
|
|
2c2901b537 | ||
|
|
2cae7ea638 | ||
|
|
66e98a96cb | ||
|
|
0b0db1c543 | ||
|
|
80039f4876 | ||
|
|
326c26fddf | ||
|
|
e7b2ead0e2 | ||
|
|
a89657b0b8 | ||
|
|
4ee15e1b6e | ||
|
|
ed40df6295 | ||
|
|
6d590c050d | ||
|
|
47f9a54f97 | ||
|
|
17b0d72fbf | ||
|
|
0269c2421d | ||
|
|
5b260e4915 | ||
|
|
5290410a17 | ||
|
|
dc9041aaec | ||
|
|
b0dc441d68 | ||
|
|
b0fc3eb5af | ||
|
|
d9980e866d | ||
|
|
52ae3404ee | ||
|
|
eecb4f4f53 | ||
|
|
744345af81 | ||
|
|
7694d1b0fb | ||
|
|
d8010d4c0c | ||
|
|
a57b49c936 | ||
|
|
a84ba41ea9 | ||
|
|
dc200646f7 | ||
|
|
e273e870ef | ||
|
|
6fc34aca00 | ||
|
|
f97ac70678 | ||
|
|
6ceb681463 | ||
|
|
716ee6eba0 | ||
|
|
f6b0f6b0cd | ||
|
|
737a652ff0 | ||
|
|
2995e5c9dc | ||
|
|
338c7a2e41 | ||
|
|
ad27358deb | ||
|
|
495754816c | ||
|
|
96f2518af9 | ||
|
|
280bdeeb1b | ||
|
|
46cdeb0bab | ||
|
|
d41f88f8bf | ||
|
|
815d6e97a8 | ||
|
|
4e35aefe41 | ||
|
|
ca84915e43 | ||
|
|
31cc7e9e4d | ||
|
|
4ec107705e | ||
|
|
7740d2ac86 | ||
|
|
8c2aeacc85 | ||
|
|
8b8e8aec15 | ||
|
|
6ac3f660e9 | ||
|
|
7ee576a9c1 | ||
|
|
f4c8699aaf | ||
|
|
4273ca6e5c | ||
|
|
4ba043b77e | ||
|
|
0169415ea2 | ||
|
|
86791dbaca | ||
|
|
d5b58a3cbd | ||
|
|
3d8281d27e | ||
|
|
b1a014c7bc | ||
|
|
505d0af2b3 | ||
|
|
ece6bd1d78 | ||
|
|
8dc18b155a | ||
|
|
aecd597687 | ||
|
|
c99f106bae | ||
|
|
d2fc7ae6fe | ||
|
|
c1c1d0055a | ||
|
|
f37ec82d39 | ||
|
|
6e9e30e0f0 | ||
|
|
990ddb5cd3 | ||
|
|
c6ebbb48bb | ||
|
|
95cc0b1c91 | ||
|
|
81b282f1f1 | ||
|
|
8c5b357dd2 | ||
|
|
a13b2cbfd2 | ||
|
|
19617c2285 | ||
|
|
f9794a973f | ||
|
|
65efe62d19 | ||
|
|
683ddcaf53 | ||
|
|
113e0bbc3c | ||
|
|
2af97e40bf | ||
|
|
83cbe3aece | ||
|
|
ceac1ba614 | ||
|
|
765fdbdf9d | ||
|
|
43ae73c5f3 | ||
|
|
e57a0de8a9 | ||
|
|
3de491105b | ||
|
|
2fe690e962 | ||
|
|
b826d37086 | ||
|
|
bfbfd55fe7 | ||
|
|
15f38efd52 | ||
|
|
5d25681485 | ||
|
|
d26df376f8 | ||
|
|
cff3fb3bad | ||
|
|
fb3c49aa8b | ||
|
|
af1657e226 | ||
|
|
d7c7302c12 | ||
|
|
26ce95f7c1 | ||
|
|
4924df2ef3 | ||
|
|
2e141a5d52 | ||
|
|
cb1978bcaf | ||
|
|
387f5122f1 | ||
|
|
db5887de21 | ||
|
|
fe24d9bcbd | ||
|
|
6c6b1cbf46 | ||
|
|
4d65c9c999 | ||
|
|
4417279275 | ||
|
|
365f12996d | ||
|
|
ef1015f33b | ||
|
|
b43dfb5cf4 | ||
|
|
225e693d1f | ||
|
|
fda91770be | ||
|
|
d512876e0b | ||
|
|
2ba4b5383e | ||
|
|
ecc3184609 | ||
|
|
712b384848 | ||
|
|
26cdd73331 | ||
|
|
919aa6dcfe | ||
|
|
3bb3093b49 | ||
|
|
9c12250558 | ||
|
|
6c843f64fb | ||
|
|
3fda7dc5b0 | ||
|
|
22ec7f0340 | ||
|
|
378461dcbb | ||
|
|
905c7b9d3c | ||
|
|
670ec2c71c | ||
|
|
b419cd5078 | ||
|
|
b1db4ea590 | ||
|
|
822ddb1274 | ||
|
|
672dd6d4f1 | ||
|
|
b3ac73d903 | ||
|
|
f8554dd5e7 | ||
|
|
88679c0c9a | ||
|
|
a08a717ca8 | ||
|
|
c8b6fdbfdb | ||
|
|
b495c41626 | ||
|
|
7f4be474c6 | ||
|
|
7f6f68fcbb | ||
|
|
03fd7db033 | ||
|
|
9dc5c66f7b | ||
|
|
aa1b08632e | ||
|
|
6bbe354c9e | ||
|
|
8ff429793b | ||
|
|
70d010127a | ||
|
|
696717c509 | ||
|
|
71c308d052 | ||
|
|
bed5b320ad | ||
|
|
b0e827412e | ||
|
|
63e9af402e | ||
|
|
fd55105f62 | ||
|
|
c2e3b362e7 | ||
|
|
5d2d60d040 | ||
|
|
0e59f5406f | ||
|
|
e7cb471c0b | ||
|
|
0465089207 | ||
|
|
4f2f59907c | ||
|
|
7d85328d41 | ||
|
|
f2a9329196 | ||
|
|
7a8231615c | ||
|
|
0db6b25327 | ||
|
|
e91f6f01a6 | ||
|
|
0b4042af60 | ||
|
|
519d8953b5 | ||
|
|
14a616fc1b | ||
|
|
bfe5a8626f | ||
|
|
4ecd186e5c | ||
|
|
8c60473c15 | ||
|
|
bb126eba5b | ||
|
|
d1998422d2 | ||
|
|
69f687a85f | ||
|
|
2aa19d1dca | ||
|
|
e44a25dcc3 | ||
|
|
efb5ffa906 | ||
|
|
b4c586a859 | ||
|
|
e874143a95 | ||
|
|
e4dbab8b16 | ||
|
|
f6d7d6f90c | ||
|
|
063c90caf4 | ||
|
|
2011c2eee7 | ||
|
|
24545f2acd | ||
|
|
a7c81245ff | ||
|
|
4f2c993b2d | ||
|
|
e5f8e500c1 | ||
|
|
a61e2de140 | ||
|
|
881d2b060e | ||
|
|
4830c6d219 | ||
|
|
4c3c9ac88c | ||
|
|
7e1c610a4d | ||
|
|
8139f495ce | ||
|
|
89ab296151 | ||
|
|
a0dc92c403 | ||
|
|
86134f478a | ||
|
|
9a2addc18e | ||
|
|
9264ce2a60 | ||
|
|
cbd69df708 | ||
|
|
ca7caa25b6 | ||
|
|
c10330627f | ||
|
|
d80c59afce | ||
|
|
abdbb67471 | ||
|
|
037a97b6ff | ||
|
|
a0a529ad38 | ||
|
|
72239558af | ||
|
|
d2097ad6dd | ||
|
|
1bd542cddb | ||
|
|
fb1c1ee874 | ||
|
|
624a7a8f37 | ||
|
|
381378f532 | ||
|
|
af05486c0e | ||
|
|
4b558948d9 | ||
|
|
d9cff42f5c | ||
|
|
2a3d2b8d0d | ||
|
|
d0f453d291 | ||
|
|
8e65dd0eae | ||
|
|
da4d0b28c7 | ||
|
|
6c96ef7fb0 | ||
|
|
997f09adde | ||
|
|
0e8503ed2b | ||
|
|
97dfcd79b1 | ||
|
|
9a12a2e41f | ||
|
|
5c114e562b | ||
|
|
ae696a0be0 | ||
|
|
22d46a5dcc | ||
|
|
f3125ff464 | ||
|
|
9682b548e6 | ||
|
|
ec11a74265 | ||
|
|
4be0614604 | ||
|
|
27cbff2bc5 | ||
|
|
c41c8963f2 | ||
|
|
7249d4fa29 | ||
|
|
2e0c60cc0d | ||
|
|
7c5e47f5e4 | ||
|
|
f9decbd366 | ||
|
|
1dc69341ca | ||
|
|
75ae19fa8e | ||
|
|
9de04e9a0c | ||
|
|
983f59cd51 | ||
|
|
7b5e7ae184 | ||
|
|
6e96b1daec | ||
|
|
9604661f3b | ||
|
|
4bd215491f | ||
|
|
6a7dd451b5 | ||
|
|
841201adab | ||
|
|
e013e62776 | ||
|
|
6c171a6394 | ||
|
|
a377f6e8d6 | ||
|
|
0337318e0b | ||
|
|
2844c70f95 | ||
|
|
7e11c8f375 | ||
|
|
23cbbce964 | ||
|
|
c63fcdd465 | ||
|
|
82412b2659 | ||
|
|
eef3832e40 | ||
|
|
63b75a1c61 | ||
|
|
d1f0879f0a | ||
|
|
36416aab2e | ||
|
|
7f3577c3ef | ||
|
|
69f883034f | ||
|
|
1c56af01d0 | ||
|
|
43c0f73c21 | ||
|
|
e8cd3d63da | ||
|
|
b7f564cb21 | ||
|
|
3f5cb1e2de | ||
|
|
7d8c0472f0 | ||
|
|
b8b516ba77 | ||
|
|
83b3d775b6 | ||
|
|
aea252ccc6 | ||
|
|
d8908f9c7f | ||
|
|
95289aa952 | ||
|
|
149e318a87 | ||
|
|
8657826e60 | ||
|
|
a45f5bb3b2 | ||
|
|
ae9a91a7a1 | ||
|
|
3638bde633 | ||
|
|
3a2361f42c | ||
|
|
1bd0eef913 | ||
|
|
85aa3b38b7 | ||
|
|
678859a68d | ||
|
|
e531acee3f | ||
|
|
4a45ebfe3b | ||
|
|
8fb9a344f2 | ||
|
|
8829b60220 | ||
|
|
4e64f28732 | ||
|
|
a0749de700 | ||
|
|
f9623e046e | ||
|
|
bce1fd57ef | ||
|
|
c6a1c53b46 | ||
|
|
e36229e4c4 | ||
|
|
853c1bc23e | ||
|
|
3b6f6853bc | ||
|
|
b415437662 | ||
|
|
24df878abe | ||
|
|
f474673a14 | ||
|
|
c53ab85107 | ||
|
|
5c4dd7c9e3 | ||
|
|
217220ffaa | ||
|
|
c89a3049fa | ||
|
|
bb12bff9db | ||
|
|
30d5e51d0b | ||
|
|
cd3bcc0ee3 | ||
|
|
ef1ba824c3 | ||
|
|
bc45e92138 | ||
|
|
c2f36f5419 | ||
|
|
31a31612fc | ||
|
|
f9c2491ab6 | ||
|
|
3de095fe77 | ||
|
|
e4e04a193f | ||
|
|
a1a90519c5 | ||
|
|
522979835c | ||
|
|
fe049256b5 | ||
|
|
31d1870198 | ||
|
|
6edf0dfb31 | ||
|
|
3bf3dceaa2 | ||
|
|
ce503653cd | ||
|
|
caa9854ff0 | ||
|
|
20c3a338c5 | ||
|
|
6547461637 | ||
|
|
bbb725c64c | ||
|
|
403d08c86a | ||
|
|
f66d9f511a | ||
|
|
dd2138ac5e | ||
|
|
74e9979c67 | ||
|
|
ddc22e5200 | ||
|
|
4218da91cd | ||
|
|
857b94f44f | ||
|
|
a3ce367950 | ||
|
|
75dac54591 | ||
|
|
a156c0111b | ||
|
|
e5d93fd7d6 | ||
|
|
67436f4ff9 | ||
|
|
b712f851a2 | ||
|
|
e2c0b8da86 | ||
|
|
1b4560061d | ||
|
|
d94df7f0eb | ||
|
|
fa04d7bf4e | ||
|
|
4d4cabb465 | ||
|
|
d4fee791a7 | ||
|
|
82c285e348 | ||
|
|
4d276657b3 | ||
|
|
cffeab813d | ||
|
|
18363bbbeb | ||
|
|
6df7d8d2ba | ||
|
|
5deba44d2b | ||
|
|
0a8b1e764c | ||
|
|
fc52e8f41a | ||
|
|
efcdc694dd | ||
|
|
eb7c44333c | ||
|
|
3c62ff6913 | ||
|
|
f4f589af94 | ||
|
|
3521579dea | ||
|
|
e97a6f52da | ||
|
|
3584aa35c7 | ||
|
|
b553520abe | ||
|
|
e32d44b1bc | ||
|
|
105da1503f | ||
|
|
29298072e5 | ||
|
|
154c10fee5 | ||
|
|
7853f7cb6a | ||
|
|
e54ecc8a35 | ||
|
|
d8b95c5739 | ||
|
|
2f8d0b764a | ||
|
|
d7732ed206 | ||
|
|
6e631cd9a5 | ||
|
|
0a9e5dd834 | ||
|
|
d14a4eaadd | ||
|
|
5f7c7fcc7b | ||
|
|
978f152ab6 | ||
|
|
68a2a606f3 | ||
|
|
bad43135d7 | ||
|
|
110ad9a6e9 | ||
|
|
e98f288302 | ||
|
|
9a79f37bf1 | ||
|
|
85909cfc1e | ||
|
|
4330da520f | ||
|
|
27907ed00f | ||
|
|
31c90f3190 | ||
|
|
35844e0cf1 | ||
|
|
afa7e380aa | ||
|
|
cb9a5f0eb4 | ||
|
|
da522c557d | ||
|
|
2c0afd0268 | ||
|
|
20255c69c2 | ||
|
|
74f1c5416b | ||
|
|
feed7f1ded | ||
|
|
967698e4ce | ||
|
|
71bd81ae47 | ||
|
|
89a5153bb3 | ||
|
|
c11bd9a2c5 | ||
|
|
59d46a9926 | ||
|
|
39c3e5411f | ||
|
|
aad419c55d | ||
|
|
ee58fd5b4b | ||
|
|
5564aea9c2 | ||
|
|
aedd2c451b | ||
|
|
6d597611b6 | ||
|
|
9ffe5efe5d | ||
|
|
2715950d86 | ||
|
|
77a7190f4d | ||
|
|
cc65890fea | ||
|
|
93758efb27 | ||
|
|
c0dc9191e3 | ||
|
|
485783a782 | ||
|
|
ee07bef2b8 | ||
|
|
0de8481314 | ||
|
|
ec0f539a33 | ||
|
|
173ee2daff | ||
|
|
5c0091b5e8 | ||
|
|
84dce7f6e8 | ||
|
|
e8a943a35a | ||
|
|
b3171a8125 | ||
|
|
d77f2eb674 | ||
|
|
5474fad9cc | ||
|
|
3dd85cc60b | ||
|
|
510e412ff2 | ||
|
|
3dfdccbc6b | ||
|
|
a55661548a | ||
|
|
69363b37e9 | ||
|
|
2d6a753921 | ||
|
|
dc989e5668 | ||
|
|
227ac480d5 | ||
|
|
c5e4650327 | ||
|
|
56cf271e77 | ||
|
|
fa8396f039 | ||
|
|
904ca30f91 | ||
|
|
549f35766b | ||
|
|
1589c799ec | ||
|
|
604c2e70bd | ||
|
|
297a84d3a4 | ||
|
|
aaad9f7207 | ||
|
|
a4028562ce | ||
|
|
5ab3f98855 | ||
|
|
eba21e96b4 | ||
|
|
9ad7ae47b5 | ||
|
|
cf54e4c8f3 | ||
|
|
57aa6fa0fc | ||
|
|
4a01f3d490 | ||
|
|
80a0db951d | ||
|
|
3202cb8e08 | ||
|
|
ddf36230bf | ||
|
|
b67c2be968 | ||
|
|
dc2c8da92b | ||
|
|
5d6c3701d0 | ||
|
|
b024e263b4 | ||
|
|
b706864e40 | ||
|
|
60cc666b67 | ||
|
|
efff4dd553 | ||
|
|
2b1dad8e48 | ||
|
|
8c7de5aad7 | ||
|
|
1db8bc2573 | ||
|
|
c60dd65a16 | ||
|
|
f7f59a4b0a | ||
|
|
ec7615e7e3 | ||
|
|
f411c33754 | ||
|
|
79121168e4 | ||
|
|
ef81e33f70 | ||
|
|
96066c66cb | ||
|
|
b1819b776d | ||
|
|
bd7a392ffc | ||
|
|
7997b77158 | ||
|
|
d1966bcdc5 | ||
|
|
7b5bbe024d | ||
|
|
4d84f15aca | ||
|
|
928937daab | ||
|
|
3bd8d247e5 | ||
|
|
cf6c9dd5f2 | ||
|
|
ff55cbbaad | ||
|
|
3eda4e2c58 | ||
|
|
02a8a1c565 | ||
|
|
8769841f08 | ||
|
|
7208fe1c3b | ||
|
|
801c8c6a9f | ||
|
|
51ebd25aa3 | ||
|
|
c6595c1f61 | ||
|
|
a267666e99 | ||
|
|
b27fea5b91 | ||
|
|
f8a14d4bde | ||
|
|
633e35338d | ||
|
|
8003da94e6 | ||
|
|
c3af97ef57 | ||
|
|
31d4e5c727 | ||
|
|
fbbcf90956 | ||
|
|
f522038b8d | ||
|
|
ace599b39f | ||
|
|
963562621d | ||
|
|
5171d07441 | ||
|
|
9a0b9573d5 | ||
|
|
4733311ef3 | ||
|
|
432450a15b | ||
|
|
47638becc0 | ||
|
|
bdd3a95685 | ||
|
|
38601346a5 | ||
|
|
dc3a6ae6a1 | ||
|
|
f668b1e7f7 | ||
|
|
eb1c09d790 | ||
|
|
9bf324c74b | ||
|
|
f13e909478 | ||
|
|
6a0fe287dd | ||
|
|
5a48f2e6e3 | ||
|
|
6565834ad4 | ||
|
|
837821fd01 | ||
|
|
cff5989559 | ||
|
|
60510a4d68 | ||
|
|
2b54034973 | ||
|
|
347b0f9065 | ||
|
|
eac0104d7a | ||
|
|
1c8e4604de | ||
|
|
e9624635ed | ||
|
|
f9beda1ddb | ||
|
|
53400de110 | ||
|
|
cf37ddab22 | ||
|
|
625af199f4 | ||
|
|
56e9217f4b | ||
|
|
86f1af7926 | ||
|
|
7737f8455d | ||
|
|
b0f1677fcc | ||
|
|
4406e0d4b4 | ||
|
|
cf19381060 | ||
|
|
802d8a3b08 | ||
|
|
2a852496e9 | ||
|
|
4653097225 | ||
|
|
8afee29e02 | ||
|
|
233eca39ef | ||
|
|
1a96bb1233 | ||
|
|
93702c63dc | ||
|
|
df065562d1 | ||
|
|
802c99adf9 | ||
|
|
482c7455f5 | ||
|
|
731b96dccc | ||
|
|
9550ae4d11 | ||
|
|
2d6e747db9 | ||
|
|
fd94945466 | ||
|
|
d3b0c09332 | ||
|
|
01915a2db9 | ||
|
|
51b043b6d2 | ||
|
|
c144fd8087 | ||
|
|
12b15df78c | ||
|
|
d6ecd12cea | ||
|
|
1c3d498da6 | ||
|
|
d75e10aef5 | ||
|
|
f0f9d85558 | ||
|
|
86f3411896 | ||
|
|
6daef39268 | ||
|
|
7d1720a360 | ||
|
|
b11a4884b4 | ||
|
|
50696bb926 | ||
|
|
d67d4e2f86 | ||
|
|
2f01c4d251 | ||
|
|
9ffa30ab74 | ||
|
|
5d93346b52 | ||
|
|
39762b36c6 | ||
|
|
e40ebdff05 | ||
|
|
8d12d9e51e | ||
|
|
23708c9aae | ||
|
|
c86fba9dc7 | ||
|
|
e747e6e4e7 | ||
|
|
5aec024242 | ||
|
|
b473b198a6 | ||
|
|
516e5a2062 | ||
|
|
fb3abbe491 | ||
|
|
3e009f3037 | ||
|
|
e63d304953 | ||
|
|
3cd90c2d47 | ||
|
|
a485c3b787 | ||
|
|
2702bb3c84 | ||
|
|
56264ecc69 | ||
|
|
cc4535245c | ||
|
|
6ae31de1dd | ||
|
|
2d4d068413 | ||
|
|
271543a446 | ||
|
|
8f796aec36 | ||
|
|
6eb1aa85d2 | ||
|
|
ac59b77ad5 | ||
|
|
3efccc4cf3 | ||
|
|
8cbcd18f4b | ||
|
|
6bb2378790 | ||
|
|
e7d0ffe8c0 | ||
|
|
ab89600c73 | ||
|
|
830e3816a0 | ||
|
|
dadac4f13f | ||
|
|
1de48a49af | ||
|
|
687a1e1cb5 | ||
|
|
665a04f5c5 | ||
|
|
fdb18d242b | ||
|
|
ec173da4ce | ||
|
|
bb35cc15d2 | ||
|
|
181f52eaea | ||
|
|
fc7c8101ed | ||
|
|
a347603fe0 | ||
|
|
09022c995f | ||
|
|
3609943f30 | ||
|
|
a1d308ea07 | ||
|
|
5579db0eaf | ||
|
|
3a3e488b23 | ||
|
|
0a82ec0a70 | ||
|
|
bc1d89218e | ||
|
|
817b9adeac | ||
|
|
a7aa42a59b | ||
|
|
34f239036d | ||
|
|
91f128c2fe | ||
|
|
a00ebea692 | ||
|
|
c27cdab3fc | ||
|
|
d7da2d7890 | ||
|
|
76f0fd4232 | ||
|
|
008b45ad17 | ||
|
|
4d49263816 | ||
|
|
6a2482e5c6 | ||
|
|
b5c3fb2af4 | ||
|
|
fa2c8eb839 | ||
|
|
f3e82b4ee2 | ||
|
|
ba67248155 | ||
|
|
a6d962bfaa | ||
|
|
95cddb52d4 | ||
|
|
5a5118d775 | ||
|
|
1b1cdd7a4b | ||
|
|
a9ddf7f7dd | ||
|
|
b54b3f1778 | ||
|
|
349a63c591 | ||
|
|
293d33912f | ||
|
|
357b9849e7 | ||
|
|
0c8a9eef32 | ||
|
|
112b1d0da4 | ||
|
|
a05007416a | ||
|
|
9f7dd55583 | ||
|
|
11b06f6416 | ||
|
|
adb7eb7755 | ||
|
|
b776b80728 | ||
|
|
73a149ea7b | ||
|
|
ef8784093b | ||
|
|
b30c788e3d | ||
|
|
23899bdff3 | ||
|
|
8bd0dd22c2 | ||
|
|
c55b00c74f | ||
|
|
c895f509b0 | ||
|
|
5968915331 | ||
|
|
44ffc082f6 | ||
|
|
b716ae5675 | ||
|
|
01a0b59494 | ||
|
|
8ebc3da0bb | ||
|
|
133577a4fa | ||
|
|
a19615db41 | ||
|
|
32b212a6bf | ||
|
|
00ec4529cd | ||
|
|
102d03888f | ||
|
|
9f8247320e | ||
|
|
ef871de30e | ||
|
|
b3a15ce26b | ||
|
|
270695aec2 | ||
|
|
ad7392a326 | ||
|
|
92a50a42e2 | ||
|
|
a3a8856d8c | ||
|
|
150c19b1b0 | ||
|
|
f04c51ee4e | ||
|
|
c4338bf957 | ||
|
|
5b06f4ede8 | ||
|
|
e7ab2cc956 | ||
|
|
5ebf8a66f7 | ||
|
|
3d4e886b9b | ||
|
|
a00d31caf3 | ||
|
|
40fe707287 | ||
|
|
8296be01ba | ||
|
|
dbeb003292 | ||
|
|
f048501c48 | ||
|
|
498350b3f3 | ||
|
|
91e82c9c65 | ||
|
|
24067ea82b | ||
|
|
aa7deddba0 | ||
|
|
b2d13203d1 | ||
|
|
19c1092b5b | ||
|
|
94ab949df8 | ||
|
|
9eb2451c2f | ||
|
|
6e521bfa3e | ||
|
|
239720fe0c | ||
|
|
8571e01e44 | ||
|
|
6f482ec6d9 | ||
|
|
5e30dc0f89 | ||
|
|
3d76d12c86 | ||
|
|
93f1b81d70 | ||
|
|
e2b65ddd82 | ||
|
|
b64bbe91d4 | ||
|
|
1f2ce55f24 | ||
|
|
921157da9e | ||
|
|
413acf7d05 | ||
|
|
725f75aa74 | ||
|
|
c64ab8a577 | ||
|
|
1154c99e0a | ||
|
|
5bcdef77b8 | ||
|
|
e4a0089102 | ||
|
|
ba022d8a35 | ||
|
|
c8ac859d2e | ||
|
|
d6fd72e880 | ||
|
|
61cc360c85 | ||
|
|
08e39c4fca | ||
|
|
8c2ee441fc | ||
|
|
0a7d23c763 | ||
|
|
7b54fff26e | ||
|
|
cd3e0f614b | ||
|
|
81c0aec202 | ||
|
|
3193bdd720 | ||
|
|
b9a12454e1 | ||
|
|
01a857f7c6 | ||
|
|
2bb2eee80e | ||
|
|
c054ea500d | ||
|
|
d7f8201246 | ||
|
|
622cfcc6fe | ||
|
|
303240e4d2 | ||
|
|
a8b849aede | ||
|
|
ca281b70c9 | ||
|
|
e056ece223 | ||
|
|
0ae35214fb | ||
|
|
34ffac39cb | ||
|
|
52d4b3eefc | ||
|
|
908d2761f2 | ||
|
|
7c98ad8c5b | ||
|
|
5bb55e6484 | ||
|
|
e48e47bc63 | ||
|
|
2a3f4f755c | ||
|
|
ddfae9227c | ||
|
|
c74378423d | ||
|
|
0e020623a5 | ||
|
|
eeb4000efe | ||
|
|
2053c858ac | ||
|
|
4c2c5d5526 | ||
|
|
7ccc745eb2 | ||
|
|
e93367fb42 | ||
|
|
250fb1e8cf | ||
|
|
1c2b87940d | ||
|
|
25fed203c6 | ||
|
|
bba2a6cc9d | ||
|
|
b036cc2abe | ||
|
|
218b15918d | ||
|
|
19489a68a7 | ||
|
|
7d7061a78a | ||
|
|
1aceb297ba | ||
|
|
cdee7480c2 | ||
|
|
bb2fb235aa | ||
|
|
b2f88e203a | ||
|
|
c2f1af99ef | ||
|
|
00e9dff1d9 | ||
|
|
82d74e6fee | ||
|
|
f1e2770574 | ||
|
|
5ce1701657 | ||
|
|
2f79487aea | ||
|
|
fefdfaf17b | ||
|
|
c1e6f71430 | ||
|
|
a8310fa63f | ||
|
|
e28ac814b8 | ||
|
|
45fed59a65 | ||
|
|
310399112b | ||
|
|
01238fc8a9 | ||
|
|
365e46c568 | ||
|
|
8d336e5b46 | ||
|
|
54fe7013b1 | ||
|
|
8dc40bdd4e | ||
|
|
d88ab8bf58 | ||
|
|
67030ab033 |
@@ -0,0 +1,606 @@
|
||||
---
|
||||
name: handsontable
|
||||
description: >
|
||||
Use this skill whenever the user asks about Handsontable — a JavaScript data grid component with
|
||||
spreadsheet-like UX. Triggers include: mentions of "handsontable", "HotTable", "data grid",
|
||||
"@handsontable/react-wrapper", spreadsheet component in React/Angular/Vue, HyperFormula formulas
|
||||
inside a grid, cell types (dropdown, checkbox, date, numeric) in a grid context, or questions
|
||||
about Handsontable theming, plugins, hooks, configuration options, or column/row features. Also
|
||||
trigger when the user wants to add an editable spreadsheet-like table to a web app, or asks about
|
||||
copy/paste, sorting, filtering, frozen rows/columns, merged cells, or context menus in a grid.
|
||||
This skill covers Handsontable used with any framework (React, Angular, Vue 3, vanilla JS) and
|
||||
HyperFormula as the Formulas plugin engine inside Handsontable. Do NOT use this skill for
|
||||
headless/standalone HyperFormula usage without Handsontable — that is covered by the separate
|
||||
"hyperformula" skill.
|
||||
---
|
||||
|
||||
# Handsontable
|
||||
|
||||
Handsontable is a JavaScript **data grid component** (not a full spreadsheet application) that
|
||||
brings spreadsheet-like UX to web apps: cell editing, copy/paste, sorting, filtering, formulas,
|
||||
keyboard navigation, context menus, merged cells, frozen rows/columns, conditional formatting, data
|
||||
validation, pagination, and 400+ built-in formulas via HyperFormula.
|
||||
|
||||
- **Latest version:** 17.1.0 (May 2026)
|
||||
- **Frameworks:** Vanilla JS/TS, React (`@handsontable/react-wrapper`), Angular (`@handsontable/angular-wrapper`), Vue 3 (`@handsontable/vue3`)
|
||||
- **React wrapper requires:** React 18+
|
||||
- **License:** Dual — free for non-commercial use (`licenseKey: 'non-commercial-and-evaluation'`), paid for commercial. Per-developer annual license, offline validation (no server connection). Tiers: Hobby (free, non-commercial), Trial (free 45 days), Standard (from $999/yr), Priority (from $1,299/yr), Enterprise (custom). See [Pricing](https://handsontable.com/pricing).
|
||||
|
||||
Always check `references/docs-map.md` (in this skill folder) for the full organized link directory
|
||||
when you need to point the user to specific documentation or need to look up more info.
|
||||
|
||||
### What this skill does NOT cover
|
||||
|
||||
- **Standalone HyperFormula** (headless formula engine without a grid UI) — use the "hyperformula" skill instead.
|
||||
- **Other data grid libraries** (AG Grid, TanStack Table, etc.).
|
||||
- **Full spreadsheet applications** — Handsontable is a component you embed, not a standalone app like Google Sheets.
|
||||
|
||||
---
|
||||
|
||||
## React Quick Start (Recommended)
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
npm install handsontable @handsontable/react-wrapper
|
||||
```
|
||||
|
||||
### Minimal working example
|
||||
|
||||
```jsx
|
||||
import { HotTable } from '@handsontable/react-wrapper';
|
||||
import { registerAllModules } from 'handsontable/registry';
|
||||
import 'handsontable/styles/handsontable.min.css';
|
||||
import 'handsontable/styles/ht-theme-main.min.css';
|
||||
|
||||
registerAllModules();
|
||||
|
||||
const MyGrid = () => (
|
||||
<HotTable
|
||||
themeName="ht-theme-main"
|
||||
data={[
|
||||
['', 'Tesla', 'Volvo', 'Toyota', 'Ford'],
|
||||
['2019', 10, 11, 12, 13],
|
||||
['2020', 20, 11, 14, 13],
|
||||
['2021', 30, 15, 12, 13],
|
||||
]}
|
||||
colHeaders={true}
|
||||
rowHeaders={true}
|
||||
height="auto"
|
||||
autoWrapRow={true}
|
||||
autoWrapCol={true}
|
||||
licenseKey="non-commercial-and-evaluation"
|
||||
/>
|
||||
);
|
||||
|
||||
export default MyGrid;
|
||||
```
|
||||
|
||||
Key points:
|
||||
- `registerAllModules()` registers every built-in plugin. To reduce bundle size, import only what
|
||||
you need — see [Modules guide](https://handsontable.com/docs/react-data-grid/modules/).
|
||||
- `height` is required (or the grid won't render). Use `"auto"`, a pixel number, or a CSS string.
|
||||
- All Handsontable configuration options are passed as props on `<HotTable>`.
|
||||
- Full installation guide: https://handsontable.com/docs/react-data-grid/installation/
|
||||
|
||||
---
|
||||
|
||||
## Vanilla JS Quick Start
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
npm install handsontable
|
||||
```
|
||||
|
||||
### CDN (jsDelivr)
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/handsontable/dist/handsontable.full.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/handsontable/styles/handsontable.min.css" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/handsontable/styles/ht-theme-main.min.css" />
|
||||
```
|
||||
|
||||
To pin a specific version, add `@17.1` after `handsontable` in the URL (e.g.,
|
||||
`handsontable@17.1/dist/handsontable.full.min.js`).
|
||||
|
||||
### Minimal working example
|
||||
|
||||
```js
|
||||
import Handsontable from 'handsontable';
|
||||
import 'handsontable/styles/handsontable.min.css';
|
||||
import 'handsontable/styles/ht-theme-main.min.css';
|
||||
|
||||
const container = document.getElementById('grid');
|
||||
|
||||
new Handsontable(container, {
|
||||
data: [
|
||||
['', 'Tesla', 'Volvo', 'Toyota', 'Ford'],
|
||||
['2019', 10, 11, 12, 13],
|
||||
['2020', 20, 11, 14, 13],
|
||||
['2021', 30, 15, 12, 13],
|
||||
],
|
||||
colHeaders: true,
|
||||
rowHeaders: true,
|
||||
height: 'auto',
|
||||
themeName: 'ht-theme-main',
|
||||
autoWrapRow: true,
|
||||
autoWrapCol: true,
|
||||
licenseKey: 'non-commercial-and-evaluation',
|
||||
});
|
||||
```
|
||||
|
||||
Full JS docs: https://handsontable.com/docs/javascript-data-grid/installation/
|
||||
|
||||
---
|
||||
|
||||
## Other Frameworks
|
||||
|
||||
### Angular (v17–19)
|
||||
|
||||
```bash
|
||||
npm install handsontable @handsontable/angular-wrapper
|
||||
```
|
||||
|
||||
The Angular wrapper was modernized in Handsontable v17.1 to align with Angular 17–19, simplifying setup and reducing dependencies. Earlier Angular versions are no longer the target — upgrade Angular first if you're below v17.
|
||||
|
||||
Docs: https://handsontable.com/docs/angular-data-grid/installation/
|
||||
|
||||
### Vue 3
|
||||
|
||||
```bash
|
||||
npm install handsontable @handsontable/vue3
|
||||
```
|
||||
|
||||
Docs: https://handsontable.com/docs/react-data-grid/vue3-installation/
|
||||
|
||||
All framework wrappers share the same version number as the core library and expose the same
|
||||
configuration options. The React examples in this skill translate directly — just use the
|
||||
framework's component syntax. Note: Vue 2 (`@handsontable/vue`) is deprecated — use Vue 3.
|
||||
|
||||
---
|
||||
|
||||
## Theming: Light / Dark / Auto
|
||||
|
||||
Handsontable v15+ includes a built-in theme system. Three themes ship out of the box: **main**
|
||||
(default, spreadsheet-like), **horizon** (clean, analytics-focused), **classic** (legacy
|
||||
replacement).
|
||||
|
||||
### CSS file approach (simplest)
|
||||
|
||||
Import the base stylesheet plus a theme:
|
||||
|
||||
```js
|
||||
import 'handsontable/styles/handsontable.min.css';
|
||||
import 'handsontable/styles/ht-theme-main.min.css';
|
||||
```
|
||||
|
||||
Then set the theme on the grid:
|
||||
|
||||
```jsx
|
||||
<HotTable themeName="ht-theme-main" /* ... */ />
|
||||
```
|
||||
|
||||
**Dark mode:**
|
||||
- System preference (auto): `themeName="ht-theme-main-dark-auto"`
|
||||
- Forced dark: `themeName="ht-theme-main-dark"`
|
||||
|
||||
Replace `main` with `horizon` or `classic` for other themes.
|
||||
|
||||
### Theme API approach (runtime switching)
|
||||
|
||||
```js
|
||||
import { mainTheme, registerTheme } from 'handsontable/themes';
|
||||
|
||||
const theme = registerTheme(mainTheme)
|
||||
.setColorScheme('auto') // 'light' | 'dark' | 'auto'
|
||||
.setDensityType('comfortable'); // 'compact' | 'default' | 'comfortable'
|
||||
```
|
||||
|
||||
Then pass it as a prop:
|
||||
|
||||
```jsx
|
||||
<HotTable theme={theme} /* ... */ />
|
||||
```
|
||||
|
||||
### CDN theme files
|
||||
|
||||
```html
|
||||
<!-- Theme CSS (includes light and dark mode support) -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/handsontable/styles/ht-theme-main.min.css" />
|
||||
```
|
||||
|
||||
In v17, dark mode is controlled via `themeName` or the Theme API — there are no separate `-dark` or
|
||||
`-dark-auto` CSS files. Load the base theme CSS above and set the mode at runtime.
|
||||
|
||||
For CSS variable customization, the Theme Builder, Figma design system, and 200+ design tokens, see:
|
||||
- [Themes guide](https://handsontable.com/docs/react-data-grid/themes/)
|
||||
- [Theme customization](https://handsontable.com/docs/react-data-grid/theme-customization/)
|
||||
- [Theme Builder tool](https://handsontable.com/theme-builder)
|
||||
|
||||
---
|
||||
|
||||
## Common Configuration Patterns
|
||||
|
||||
All options below are passed as props on `<HotTable>` (React) or in the config object (vanilla JS).
|
||||
For the full options reference: https://handsontable.com/docs/react-data-grid/api/options/
|
||||
|
||||
### Data binding
|
||||
|
||||
```jsx
|
||||
// Array of arrays
|
||||
data={[['A1', 'B1'], ['A2', 'B2']]}
|
||||
|
||||
// Array of objects
|
||||
data={[{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]}
|
||||
columns={[{ data: 'id' }, { data: 'name' }]}
|
||||
```
|
||||
|
||||
Docs: https://handsontable.com/docs/react-data-grid/binding-to-data/
|
||||
|
||||
### Column & row headers
|
||||
|
||||
```jsx
|
||||
colHeaders={['ID', 'Name', 'Price']} // custom labels, or {true} for A, B, C...
|
||||
rowHeaders={true} // 1, 2, 3... or pass an array
|
||||
```
|
||||
|
||||
### Column types (cell types)
|
||||
|
||||
Set via the `columns` array or `cells` function. Built-in types:
|
||||
|
||||
- `text` (default), `numeric`, `date`, `time`, `checkbox`, `select`, `dropdown`,
|
||||
`autocomplete`, `password`, `handsontable` (nested grid), `multiselect` (v17+)
|
||||
|
||||
```jsx
|
||||
columns={[
|
||||
{ data: 'name', type: 'text' },
|
||||
{ data: 'price', type: 'numeric', numericFormat: { pattern: '$0,0.00' } },
|
||||
{ data: 'active', type: 'checkbox' },
|
||||
{ data: 'category', type: 'dropdown', source: ['A', 'B', 'C'] },
|
||||
]}
|
||||
```
|
||||
|
||||
Full cell types reference: https://handsontable.com/docs/react-data-grid/cell-type/
|
||||
|
||||
### Sorting & filtering
|
||||
|
||||
```jsx
|
||||
columnSorting={true} // single-column sort
|
||||
// or
|
||||
multiColumnSorting={true} // multi-column sort
|
||||
|
||||
filters={true} // enable column filters
|
||||
dropdownMenu={true} // column header menu with filter UI
|
||||
```
|
||||
|
||||
### Frozen rows/columns
|
||||
|
||||
```jsx
|
||||
fixedRowsTop={1}
|
||||
fixedRowsBottom={1}
|
||||
fixedColumnsStart={2}
|
||||
```
|
||||
|
||||
### Other common options
|
||||
|
||||
```jsx
|
||||
readOnly={true} // entire grid read-only (or per-cell/column)
|
||||
contextMenu={true} // right-click menu
|
||||
mergeCells={[{ row: 0, col: 0, rowspan: 2, colspan: 2 }]}
|
||||
manualColumnResize={true}
|
||||
manualRowResize={true}
|
||||
stretchH="all" // stretch columns to fill width: 'none' | 'last' | 'all'
|
||||
```
|
||||
|
||||
### Per-column config with HotColumn (React)
|
||||
|
||||
```jsx
|
||||
import { HotTable, HotColumn } from '@handsontable/react-wrapper';
|
||||
|
||||
<HotTable data={data} licenseKey="non-commercial-and-evaluation">
|
||||
<HotColumn title="Name" data="name" />
|
||||
<HotColumn title="Active" data="active" type="checkbox" />
|
||||
</HotTable>
|
||||
```
|
||||
|
||||
Docs: https://handsontable.com/docs/react-data-grid/hot-column/
|
||||
|
||||
---
|
||||
|
||||
## HyperFormula Integration (Formulas Plugin)
|
||||
|
||||
HyperFormula powers Handsontable's Formulas plugin, providing 400+ spreadsheet functions (SUM,
|
||||
AVERAGE, IF, VLOOKUP, etc.). This section covers using HyperFormula **inside** Handsontable only.
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
npm install hyperformula
|
||||
```
|
||||
|
||||
> **Note:** Starting in Handsontable v18, HyperFormula will no longer be bundled. Install it
|
||||
> separately and pass it to the Formulas plugin.
|
||||
|
||||
### CDN
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/hyperformula/dist/hyperformula.full.min.js"></script>
|
||||
```
|
||||
|
||||
### Simple setup (auto-created instance)
|
||||
|
||||
Pass the `HyperFormula` class directly — Handsontable creates an instance automatically:
|
||||
|
||||
```jsx
|
||||
import { HyperFormula } from 'hyperformula';
|
||||
|
||||
<HotTable
|
||||
data={[
|
||||
['10', '20', '=SUM(A1:B1)'],
|
||||
['30', '40', '=SUM(A2:B2)'],
|
||||
]}
|
||||
formulas={{ engine: HyperFormula }}
|
||||
colHeaders={true}
|
||||
licenseKey="non-commercial-and-evaluation"
|
||||
/>
|
||||
```
|
||||
|
||||
### External instance (for multi-sheet or shared engine)
|
||||
|
||||
Create a HyperFormula instance with the `'internal-use-in-handsontable'` license key:
|
||||
|
||||
```jsx
|
||||
import { HyperFormula } from 'hyperformula';
|
||||
|
||||
const hfInstance = HyperFormula.buildEmpty({
|
||||
licenseKey: 'internal-use-in-handsontable',
|
||||
});
|
||||
|
||||
// Sheet 1
|
||||
<HotTable
|
||||
data={data1}
|
||||
formulas={{ engine: hfInstance, sheetName: 'Sheet1' }}
|
||||
licenseKey="non-commercial-and-evaluation"
|
||||
/>
|
||||
|
||||
// Sheet 2 — shares the same engine, enabling cross-sheet references
|
||||
<HotTable
|
||||
data={data2}
|
||||
formulas={{ engine: hfInstance, sheetName: 'Sheet2' }}
|
||||
licenseKey="non-commercial-and-evaluation"
|
||||
/>
|
||||
```
|
||||
|
||||
Cross-sheet formula example: `=SUM(Sheet1!A:A)`
|
||||
|
||||
### Named expressions
|
||||
|
||||
```jsx
|
||||
formulas={{
|
||||
engine: HyperFormula,
|
||||
namedExpressions: [
|
||||
{ name: 'TAX_RATE', expression: 0.21 },
|
||||
],
|
||||
}}
|
||||
```
|
||||
|
||||
Use in cells: `=A1 * TAX_RATE`
|
||||
|
||||
### Listening for formula changes
|
||||
|
||||
```jsx
|
||||
<HotTable
|
||||
formulas={{ engine: HyperFormula }}
|
||||
afterFormulasValuesUpdate={(changes) => {
|
||||
changes.forEach((c) => console.log(c.address, c.newValue));
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Known limitations
|
||||
|
||||
- Formulas don't work with nested object data.
|
||||
- Moving rows/columns with formulas requires the ManualRowMove / ManualColumnMove plugins (not IndexMapper).
|
||||
- `getSourceData()` operates on physical indexes; formulas use visual indexes.
|
||||
|
||||
Full guide: https://handsontable.com/docs/react-data-grid/formula-calculation/
|
||||
HyperFormula functions list: https://hyperformula.handsontable.com/docs/guide/built-in-functions.html
|
||||
HyperFormula version compatibility table: https://handsontable.com/docs/react-data-grid/formula-calculation/#hyperformula-version-support
|
||||
|
||||
---
|
||||
|
||||
## Server-side data with DataProvider (v17.1+)
|
||||
|
||||
The **DataProvider plugin** (new in v17.1) wires the grid up to a remote data source so rows are fetched, sorted, and mutated server-side instead of held in memory. Use it for datasets too large to load up front, or when the source of truth lives in a backend.
|
||||
|
||||
The `dataProvider` option is an object with five required keys: a row-id resolver, a paginated `fetchRows` callback, and three mutation callbacks. For a read-only grid, stub the mutation callbacks with `async () => {}` — they must still be present.
|
||||
|
||||
```jsx
|
||||
<HotTable
|
||||
colHeaders={['ID', 'Name', 'Email']}
|
||||
pagination={{ pageSize: 25 }}
|
||||
columnSorting={true}
|
||||
emptyDataState={true}
|
||||
height={360}
|
||||
licenseKey="non-commercial-and-evaluation"
|
||||
dataProvider={{
|
||||
// Required: how to extract a stable id from each row
|
||||
rowId: 'id',
|
||||
|
||||
// Required: paginated fetch. `sort` is null when no column is sorted.
|
||||
// Second arg carries an AbortSignal — pass it to fetch() so the plugin
|
||||
// can cancel superseded requests when the user pages/sorts quickly.
|
||||
async fetchRows({ page, pageSize, sort }, { signal }) {
|
||||
const params = new URLSearchParams({ page, pageSize });
|
||||
if (sort) {
|
||||
// sort = { prop: string, order: 'asc' | 'desc' }
|
||||
params.set('sortBy', sort.prop);
|
||||
params.set('order', sort.order);
|
||||
}
|
||||
const res = await fetch(`/api/users?${params}`, { signal });
|
||||
const { rows, totalRows } = await res.json();
|
||||
return { rows, totalRows };
|
||||
},
|
||||
|
||||
// Required. Stub with async () => {} if your grid is read-only.
|
||||
// The plugin auto-refetches the current page after each callback resolves.
|
||||
// See the plugin API ref for the exact payload shapes.
|
||||
onRowsCreate: async () => {},
|
||||
onRowsUpdate: async () => {},
|
||||
onRowsRemove: async () => {},
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Loading and error UI
|
||||
|
||||
The plugin fires three hooks you can wire up as `<HotTable>` props for loading indicators and error surfaces:
|
||||
|
||||
- `beforeDataProviderFetch(params)` — fires before each fetch. `params.skipLoading` is set when the plugin wants to suppress your loading indicator (e.g., during a quick refetch).
|
||||
- `afterDataProviderFetch(result)` — fires after a successful fetch.
|
||||
- `afterDataProviderFetchError(error)` — fires when `fetchRows` throws or returns a rejected promise.
|
||||
|
||||
### Companion options
|
||||
|
||||
The plugin is built to pair with `pagination` (paginates server-side), `columnSorting` (single-column server-side sort), and `emptyDataState` (loading + empty state UI). Enable all three when you use DataProvider.
|
||||
|
||||
### Authoritative references
|
||||
|
||||
- Guide: https://handsontable.com/docs/react-data-grid/server-side-data-fetching/
|
||||
- Recipe (REST API): https://handsontable.com/docs/react-data-grid/recipes/data-management/load-data-rest-api/
|
||||
- Plugin API: https://handsontable.com/docs/react-data-grid/api/data-provider/
|
||||
|
||||
## Notifications (v17.1+)
|
||||
|
||||
The **Notification plugin** (new in v17.1) shows non-blocking toast notifications anchored to the grid — useful for confirming saves, surfacing validation errors, or signaling background sync state. Enable with `notifications: true` and trigger via `hot.getPlugin('notifications').showMessage(...)`. See the plugin guide for placement, severity levels, and auto-dismiss timing: https://handsontable.com/docs/react-data-grid/notification/
|
||||
|
||||
---
|
||||
|
||||
## Events & Hooks
|
||||
|
||||
Handsontable hooks are passed as props on `<HotTable>`:
|
||||
|
||||
```jsx
|
||||
<HotTable
|
||||
afterChange={(changes, source) => {
|
||||
if (source !== 'loadData') {
|
||||
console.log('Cell changed:', changes);
|
||||
}
|
||||
}}
|
||||
beforeChange={(changes, source) => {
|
||||
// Return false to cancel the edit
|
||||
}}
|
||||
afterSelection={(row, col, row2, col2) => {
|
||||
console.log('Selected:', row, col, 'to', row2, col2);
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
There are 100+ hooks available. Full reference: https://handsontable.com/docs/react-data-grid/api/hooks/
|
||||
|
||||
Guide: https://handsontable.com/docs/react-data-grid/events-and-hooks/
|
||||
|
||||
---
|
||||
|
||||
## Accessing the Instance (Ref)
|
||||
|
||||
Use a ref to call Handsontable's core methods:
|
||||
|
||||
```jsx
|
||||
import { useRef } from 'react';
|
||||
|
||||
const MyGrid = () => {
|
||||
const hotRef = useRef(null);
|
||||
|
||||
const handleClick = () => {
|
||||
const hot = hotRef.current?.hotInstance;
|
||||
if (hot) {
|
||||
console.log(hot.getData()); // get all data
|
||||
hot.selectCell(0, 0); // select a cell
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<HotTable ref={hotRef} /* ...options */ />
|
||||
<button onClick={handleClick}>Get Data</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Core API reference: https://handsontable.com/docs/react-data-grid/api/core/
|
||||
|
||||
---
|
||||
|
||||
## Performance & Large Datasets
|
||||
|
||||
Handsontable virtualizes rendering automatically — only visible rows and columns are in the DOM, so
|
||||
grids with 10k–100k+ rows perform well out of the box. For bulk programmatic updates, wrap mutations
|
||||
in `batch()` to defer re-rendering until all changes are applied:
|
||||
|
||||
```jsx
|
||||
hotRef.current.hotInstance.batch(() => {
|
||||
// set many cells, add rows, etc.
|
||||
});
|
||||
```
|
||||
|
||||
For further tuning (disabling auto-size, reducing plugin overhead), see:
|
||||
- [Performance guide](https://handsontable.com/docs/react-data-grid/performance/)
|
||||
- [Batch operations](https://handsontable.com/docs/react-data-grid/batch-operations/)
|
||||
- [Bundle size optimization](https://handsontable.com/docs/react-data-grid/bundle-size/)
|
||||
|
||||
---
|
||||
|
||||
## Recipes (v17+)
|
||||
|
||||
Handsontable v17 introduced a Recipes section in the docs — ready-made patterns for common use cases
|
||||
(data validation workflows, dynamic column generation, etc.). Check the recipes index before building
|
||||
something from scratch: https://handsontable.com/docs/react-data-grid/recipes/
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Forgetting `height`**: The grid won't render without a `height` prop. Use `"auto"`, a pixel value, or a CSS string.
|
||||
- **Not filtering `loadData` in `afterChange`**: The `afterChange` hook fires on initial data load with `source === 'loadData'`. Always check the source to avoid infinite loops when syncing changes back to state.
|
||||
- **Using the old wrapper packages**: v17 removed `@handsontable/react` and `@handsontable/angular`. Use `@handsontable/react-wrapper` and `@handsontable/angular-wrapper`.
|
||||
- **Using legacy CSS imports**: `handsontable.full.min.css` was removed in v17. Use `handsontable/styles/handsontable.min.css` plus a theme file.
|
||||
- **Formulas with nested object data**: HyperFormula formulas don't work when `data` is an array of nested objects — use flat objects or arrays of arrays.
|
||||
- **ExportFile `columnHeaders` renamed**: In v17.1 the ExportFile plugin's `columnHeaders` option was renamed to `colHeaders` to match the table-level option. Update any `exportAsString` / `exportAsBlob` / `downloadFile` calls that pass `columnHeaders: ...`.
|
||||
|
||||
---
|
||||
|
||||
## Version Awareness
|
||||
|
||||
Handsontable docs are versioned. The latest docs live at `/docs/react-data-grid/` (which redirects
|
||||
to the current version). To link to a specific version, use `/docs/17.0/react-data-grid/`.
|
||||
|
||||
When helping a user, check which version they are on — breaking changes between major versions are
|
||||
common. Point them to the relevant migration guide if they're upgrading.
|
||||
|
||||
For the full organized directory of documentation links, read `references/docs-map.md` in this
|
||||
skill's folder.
|
||||
|
||||
### v17.1 changes (latest, May 2026)
|
||||
|
||||
- **New plugins:** DataProvider (server-side row loading via `dataProvider` option), Notification (toast notifications).
|
||||
- **NestedHeaders rowspan:** column headers can now span multiple header rows.
|
||||
- **ExportFile:** XLSX export added; the `columnHeaders` option was renamed to `colHeaders` (see Common Pitfalls).
|
||||
- **Angular wrapper:** modernized for Angular 17–19; simpler setup, fewer deps.
|
||||
- **Touch:** long-press now opens the context menu on touch devices.
|
||||
- **TypeScript:** `dateFormat` option now accepts `Intl.DateTimeFormatOptions`.
|
||||
- No removals or deprecations in v17.1.
|
||||
|
||||
### v17.0 Breaking Changes
|
||||
|
||||
- Removed legacy wrapper packages (`@handsontable/react`, `@handsontable/angular`). Use
|
||||
`@handsontable/react-wrapper` and `@handsontable/angular-wrapper`.
|
||||
- Removed legacy CSS (`handsontable.full.min.css`). Use `handsontable/styles/` imports.
|
||||
- Removed core-js from dependencies.
|
||||
- Removed the PersistentState plugin.
|
||||
- Deprecated bundled HyperFormula (will require separate install in v18).
|
||||
- Deprecated numbro.js, Pikaday, moment.js, DOMPurify — use native alternatives.
|
||||
|
||||
Migration guide: https://handsontable.com/docs/react-data-grid/migration-from-16.2-to-17.0/
|
||||
@@ -0,0 +1,231 @@
|
||||
# Handsontable Documentation Map
|
||||
|
||||
> Last verified: May 2026 · Aligned with Handsontable 17.1.0
|
||||
|
||||
Organized link directory for all Handsontable and HyperFormula (in-grid) documentation. Use these
|
||||
to point users to the right page. Links default to the React docs; replace `react-data-grid` with
|
||||
`javascript-data-grid`, `angular-data-grid`, or prefix with `vue3-` for other frameworks.
|
||||
|
||||
## Getting Started
|
||||
- Installation: https://handsontable.com/docs/react-data-grid/installation/
|
||||
- Demo: https://handsontable.com/docs/react-data-grid/demo/
|
||||
- Binding to data: https://handsontable.com/docs/react-data-grid/binding-to-data/
|
||||
- Server-side data fetching (DataProvider, v17.1+): https://handsontable.com/docs/react-data-grid/server-side-data-fetching/
|
||||
- Saving data: https://handsontable.com/docs/react-data-grid/saving-data/
|
||||
- Configuration options guide: https://handsontable.com/docs/react-data-grid/configuration-options/
|
||||
- Grid size: https://handsontable.com/docs/react-data-grid/grid-size/
|
||||
- Instance methods: https://handsontable.com/docs/react-data-grid/instance-methods/
|
||||
- Events and hooks: https://handsontable.com/docs/react-data-grid/events-and-hooks/
|
||||
- License key: https://handsontable.com/docs/react-data-grid/license-key/
|
||||
- Redux integration: https://handsontable.com/docs/react-data-grid/redux/
|
||||
|
||||
## Styling & Themes
|
||||
- Themes guide: https://handsontable.com/docs/react-data-grid/themes/
|
||||
- Design system (Figma): https://handsontable.com/docs/react-data-grid/handsontable-design-system/
|
||||
- Theme customization (CSS vars, tokens): https://handsontable.com/docs/react-data-grid/theme-customization/
|
||||
- Legacy style (pre-v15): https://handsontable.com/docs/react-data-grid/legacy-style/
|
||||
- Theme Builder tool: https://handsontable.com/theme-builder
|
||||
- Figma Theme Generator: https://github.com/handsontable/handsontable-figma
|
||||
|
||||
## Columns
|
||||
- HotColumn component (React): https://handsontable.com/docs/react-data-grid/hot-column/
|
||||
- Column headers: https://handsontable.com/docs/react-data-grid/column-header/
|
||||
- Column groups (nested headers): https://handsontable.com/docs/react-data-grid/column-groups/
|
||||
- Column hiding: https://handsontable.com/docs/react-data-grid/column-hiding/
|
||||
- Column moving: https://handsontable.com/docs/react-data-grid/column-moving/
|
||||
- Column freezing: https://handsontable.com/docs/react-data-grid/column-freezing/
|
||||
- Column widths: https://handsontable.com/docs/react-data-grid/column-width/
|
||||
- Column summary: https://handsontable.com/docs/react-data-grid/column-summary/
|
||||
- Column virtualization: https://handsontable.com/docs/react-data-grid/column-virtualization/
|
||||
- Column menu (dropdown): https://handsontable.com/docs/react-data-grid/column-menu/
|
||||
- Column filter: https://handsontable.com/docs/react-data-grid/column-filter/
|
||||
|
||||
## Rows
|
||||
- Row headers: https://handsontable.com/docs/react-data-grid/row-header/
|
||||
- Row parent-child (nested rows): https://handsontable.com/docs/react-data-grid/row-parent-child/
|
||||
- Row hiding: https://handsontable.com/docs/react-data-grid/row-hiding/
|
||||
- Row moving: https://handsontable.com/docs/react-data-grid/row-moving/
|
||||
- Row freezing: https://handsontable.com/docs/react-data-grid/row-freezing/
|
||||
- Row heights: https://handsontable.com/docs/react-data-grid/row-height/
|
||||
- Row virtualization: https://handsontable.com/docs/react-data-grid/row-virtualization/
|
||||
- Rows sorting: https://handsontable.com/docs/react-data-grid/rows-sorting/
|
||||
- Rows pagination: https://handsontable.com/docs/react-data-grid/rows-pagination/
|
||||
- Row trimming: https://handsontable.com/docs/react-data-grid/row-trimming/
|
||||
- Row pre-populating: https://handsontable.com/docs/react-data-grid/row-prepopulating/
|
||||
|
||||
## Cell Features
|
||||
- Clipboard (copy/paste): https://handsontable.com/docs/react-data-grid/basic-clipboard/
|
||||
- Selection: https://handsontable.com/docs/react-data-grid/selection/
|
||||
- Merge cells: https://handsontable.com/docs/react-data-grid/merge-cells/
|
||||
- Conditional formatting: https://handsontable.com/docs/react-data-grid/conditional-formatting/
|
||||
- Text alignment: https://handsontable.com/docs/react-data-grid/text-alignment/
|
||||
- Disabled cells: https://handsontable.com/docs/react-data-grid/disabled-cells/
|
||||
- Comments: https://handsontable.com/docs/react-data-grid/comments/
|
||||
- Autofill values: https://handsontable.com/docs/react-data-grid/autofill-values/
|
||||
- Formatting cells: https://handsontable.com/docs/react-data-grid/formatting-cells/
|
||||
|
||||
## Cell Functions
|
||||
- Cell functions overview: https://handsontable.com/docs/react-data-grid/cell-function/
|
||||
- Cell renderer: https://handsontable.com/docs/react-data-grid/cell-renderer/
|
||||
- Cell editor: https://handsontable.com/docs/react-data-grid/cell-editor/
|
||||
- Cell validator: https://handsontable.com/docs/react-data-grid/cell-validator/
|
||||
- Custom cells (v17+): https://handsontable.com/docs/react-data-grid/custom-cells/
|
||||
|
||||
## Cell Types
|
||||
- Cell type overview: https://handsontable.com/docs/react-data-grid/cell-type/
|
||||
- Numeric: https://handsontable.com/docs/react-data-grid/numeric-cell-type/
|
||||
- Date: https://handsontable.com/docs/react-data-grid/date-cell-type/
|
||||
- Time: https://handsontable.com/docs/react-data-grid/time-cell-type/
|
||||
- Checkbox: https://handsontable.com/docs/react-data-grid/checkbox-cell-type/
|
||||
- Select: https://handsontable.com/docs/react-data-grid/select-cell-type/
|
||||
- Dropdown: https://handsontable.com/docs/react-data-grid/dropdown-cell-type/
|
||||
- Autocomplete: https://handsontable.com/docs/react-data-grid/autocomplete-cell-type/
|
||||
- MultiSelect (v17+): https://handsontable.com/docs/react-data-grid/multiselect-cell-type/
|
||||
- Password: https://handsontable.com/docs/react-data-grid/password-cell-type/
|
||||
- Handsontable (nested grid): https://handsontable.com/docs/react-data-grid/handsontable-cell-type/
|
||||
|
||||
## Formulas (HyperFormula in Handsontable)
|
||||
- Formula calculation guide: https://handsontable.com/docs/react-data-grid/formula-calculation/
|
||||
- Formulas plugin API: https://handsontable.com/docs/react-data-grid/api/formulas/
|
||||
- HyperFormula built-in functions list: https://hyperformula.handsontable.com/docs/guide/built-in-functions.html
|
||||
- HyperFormula configuration options: https://hyperformula.handsontable.com/docs/guide/configuration-options.html
|
||||
- HyperFormula custom functions: https://hyperformula.handsontable.com/docs/guide/custom-functions.html
|
||||
- HyperFormula named expressions: https://hyperformula.handsontable.com/docs/guide/cell-references.html#relative-references
|
||||
- HyperFormula license key: https://hyperformula.handsontable.com/docs/guide/license-key.html
|
||||
|
||||
## Navigation
|
||||
- Keyboard shortcuts: https://handsontable.com/docs/react-data-grid/keyboard-shortcuts/
|
||||
- Custom shortcuts: https://handsontable.com/docs/react-data-grid/custom-shortcuts/
|
||||
- Focus scopes: https://handsontable.com/docs/react-data-grid/focus-scopes/
|
||||
- Searching values: https://handsontable.com/docs/react-data-grid/searching-values/
|
||||
|
||||
## Accessibility
|
||||
- Accessibility guide: https://handsontable.com/docs/react-data-grid/accessibility/
|
||||
|
||||
## Menus & Accessories
|
||||
- Context menu: https://handsontable.com/docs/react-data-grid/context-menu/
|
||||
- Undo and redo: https://handsontable.com/docs/react-data-grid/undo-redo/
|
||||
- Icon pack: https://handsontable.com/docs/react-data-grid/icon-pack/
|
||||
- Export to CSV: https://handsontable.com/docs/react-data-grid/export-to-csv/
|
||||
- Export to Excel/XLSX (v17.1+): https://handsontable.com/docs/react-data-grid/export-to-excel/
|
||||
- Notification (v17.1+): https://handsontable.com/docs/react-data-grid/notification/
|
||||
- Empty data state: https://handsontable.com/docs/react-data-grid/empty-data-state/
|
||||
|
||||
## Dialog & Loading
|
||||
- Dialog: https://handsontable.com/docs/react-data-grid/dialog/
|
||||
- Loading indicator: https://handsontable.com/docs/react-data-grid/loading/
|
||||
|
||||
## Internationalization
|
||||
- Language: https://handsontable.com/docs/react-data-grid/language/
|
||||
- Locale: https://handsontable.com/docs/react-data-grid/locale/
|
||||
- Layout direction (RTL/LTR): https://handsontable.com/docs/react-data-grid/layout-direction/
|
||||
- IME support: https://handsontable.com/docs/react-data-grid/ime-support/
|
||||
|
||||
## Tools & Building
|
||||
- Modules (tree-shaking): https://handsontable.com/docs/react-data-grid/modules/
|
||||
- Custom plugins: https://handsontable.com/docs/react-data-grid/custom-plugins/
|
||||
- Custom builds: https://handsontable.com/docs/react-data-grid/custom-builds/
|
||||
- Testing: https://handsontable.com/docs/react-data-grid/testing/
|
||||
|
||||
## Optimization
|
||||
- Batch operations: https://handsontable.com/docs/react-data-grid/batch-operations/
|
||||
- Performance: https://handsontable.com/docs/react-data-grid/performance/
|
||||
- Bundle size: https://handsontable.com/docs/react-data-grid/bundle-size/
|
||||
|
||||
## Security
|
||||
- Security guide: https://handsontable.com/docs/react-data-grid/security/
|
||||
|
||||
## API Reference
|
||||
- API overview: https://handsontable.com/docs/react-data-grid/api/
|
||||
- Core (instance methods): https://handsontable.com/docs/react-data-grid/api/core/
|
||||
- Hooks (all events): https://handsontable.com/docs/react-data-grid/api/hooks/
|
||||
- Configuration options (all props): https://handsontable.com/docs/react-data-grid/api/options/
|
||||
- Plugins index: https://handsontable.com/docs/react-data-grid/api/plugins/
|
||||
|
||||
### Individual Plugin APIs
|
||||
- AutoColumnSize: https://handsontable.com/docs/react-data-grid/api/auto-column-size/
|
||||
- AutoRowSize: https://handsontable.com/docs/react-data-grid/api/auto-row-size/
|
||||
- Autofill: https://handsontable.com/docs/react-data-grid/api/autofill/
|
||||
- BindRowsWithHeaders: https://handsontable.com/docs/react-data-grid/api/bind-rows-with-headers/
|
||||
- CollapsibleColumns: https://handsontable.com/docs/react-data-grid/api/collapsible-columns/
|
||||
- ColumnSorting: https://handsontable.com/docs/react-data-grid/api/column-sorting/
|
||||
- ColumnSummary: https://handsontable.com/docs/react-data-grid/api/column-summary/
|
||||
- Comments: https://handsontable.com/docs/react-data-grid/api/comments/
|
||||
- ContextMenu: https://handsontable.com/docs/react-data-grid/api/context-menu/
|
||||
- CopyPaste: https://handsontable.com/docs/react-data-grid/api/copy-paste/
|
||||
- CustomBorders: https://handsontable.com/docs/react-data-grid/api/custom-borders/
|
||||
- DataProvider (v17.1+): https://handsontable.com/docs/react-data-grid/api/data-provider/
|
||||
- Dialog: https://handsontable.com/docs/react-data-grid/api/dialog/
|
||||
- DragToScroll: https://handsontable.com/docs/react-data-grid/api/drag-to-scroll/
|
||||
- DropdownMenu: https://handsontable.com/docs/react-data-grid/api/dropdown-menu/
|
||||
- EmptyDataState: https://handsontable.com/docs/react-data-grid/api/empty-data-state/
|
||||
- ExportFile: https://handsontable.com/docs/react-data-grid/api/export-file/
|
||||
- Filters: https://handsontable.com/docs/react-data-grid/api/filters/
|
||||
- Formulas: https://handsontable.com/docs/react-data-grid/api/formulas/
|
||||
- HiddenColumns: https://handsontable.com/docs/react-data-grid/api/hidden-columns/
|
||||
- HiddenRows: https://handsontable.com/docs/react-data-grid/api/hidden-rows/
|
||||
- Loading: https://handsontable.com/docs/react-data-grid/api/loading/
|
||||
- ManualColumnFreeze: https://handsontable.com/docs/react-data-grid/api/manual-column-freeze/
|
||||
- ManualColumnMove: https://handsontable.com/docs/react-data-grid/api/manual-column-move/
|
||||
- ManualColumnResize: https://handsontable.com/docs/react-data-grid/api/manual-column-resize/
|
||||
- ManualRowMove: https://handsontable.com/docs/react-data-grid/api/manual-row-move/
|
||||
- ManualRowResize: https://handsontable.com/docs/react-data-grid/api/manual-row-resize/
|
||||
- MergeCells: https://handsontable.com/docs/react-data-grid/api/merge-cells/
|
||||
- MultiColumnSorting: https://handsontable.com/docs/react-data-grid/api/multi-column-sorting/
|
||||
- NestedHeaders: https://handsontable.com/docs/react-data-grid/api/nested-headers/
|
||||
- NestedRows: https://handsontable.com/docs/react-data-grid/api/nested-rows/
|
||||
- Notification (v17.1+): https://handsontable.com/docs/react-data-grid/api/notification/
|
||||
- Pagination: https://handsontable.com/docs/react-data-grid/api/pagination/
|
||||
- Search: https://handsontable.com/docs/react-data-grid/api/search/
|
||||
- StretchColumns: https://handsontable.com/docs/react-data-grid/api/stretch-columns/
|
||||
- TrimRows: https://handsontable.com/docs/react-data-grid/api/trim-rows/
|
||||
- UndoRedo: https://handsontable.com/docs/react-data-grid/api/undo-redo/
|
||||
|
||||
## Recipes (new in v17)
|
||||
- Recipes index: https://handsontable.com/docs/react-data-grid/recipes/
|
||||
- Load data from a REST API (DataProvider, v17.1+): https://handsontable.com/docs/react-data-grid/recipes/data-management/load-data-rest-api/
|
||||
|
||||
## Upgrade & Migration
|
||||
- Changelog: https://handsontable.com/docs/react-data-grid/changelog/
|
||||
- Versioning policy: https://handsontable.com/docs/react-data-grid/versioning-policy/
|
||||
- Deprecation policy: https://handsontable.com/docs/react-data-grid/deprecation-policy/
|
||||
- Long Term Support (LTS): https://handsontable.com/docs/react-data-grid/long-term-support/
|
||||
- 16.2 → 17.0 migration: https://handsontable.com/docs/react-data-grid/migration-from-16.2-to-17.0/
|
||||
- 16.0 → 16.1 migration: https://handsontable.com/docs/react-data-grid/migration-from-16.0-to-16.1/
|
||||
- 15.3 → 16.0 migration: https://handsontable.com/docs/react-data-grid/migration-from-15.3-to-16.0/
|
||||
- 14.6 → 15.0 migration: https://handsontable.com/docs/react-data-grid/migration-from-14.6-to-15.0/
|
||||
|
||||
## Other Framework Installation
|
||||
- JavaScript: https://handsontable.com/docs/javascript-data-grid/installation/
|
||||
- Angular: https://handsontable.com/docs/angular-data-grid/installation/
|
||||
- Vue 3: https://handsontable.com/docs/react-data-grid/vue3-installation/
|
||||
|
||||
## SSR Examples (CodeSandbox)
|
||||
- Next.js: https://codesandbox.io/p/sandbox/kwnjph?file=https://handsontable.com/codesandbox-vm?example-dir=next.js&handsontable-version=17.1preview=true
|
||||
- Astro: https://codesandbox.io/p/sandbox/gnqcwn?file=https://handsontable.com/codesandbox-vm?example-dir=astro&handsontable-version=17.1preview=true
|
||||
- Remix: https://codesandbox.io/p/sandbox/njcjlq?file=https://handsontable.com/codesandbox-vm?example-dir=remix&handsontable-version=17.1preview=true
|
||||
- Nuxt: https://codesandbox.io/p/sandbox/r7qsjc?file=https://handsontable.com/codesandbox-vm?example-dir=nuxt&handsontable-version=17.1preview=true
|
||||
|
||||
## CDN Links (jsDelivr) — latest
|
||||
- JS (full bundle): https://cdn.jsdelivr.net/npm/handsontable/dist/handsontable.full.min.js
|
||||
- Base CSS: https://cdn.jsdelivr.net/npm/handsontable/styles/handsontable.min.css
|
||||
- Theme Main: https://cdn.jsdelivr.net/npm/handsontable/styles/ht-theme-main.min.css
|
||||
- Theme Horizon: https://cdn.jsdelivr.net/npm/handsontable/styles/ht-theme-horizon.min.css
|
||||
- Theme Classic: https://cdn.jsdelivr.net/npm/handsontable/styles/ht-theme-classic.min.css
|
||||
- React wrapper: https://cdn.jsdelivr.net/npm/@handsontable/react-wrapper/dist/react-handsontable.min.js
|
||||
- HyperFormula: https://cdn.jsdelivr.net/npm/hyperformula/dist/hyperformula.full.min.js
|
||||
|
||||
Pin versions by adding `@17.1` after the package name (e.g., `handsontable@17.1`).
|
||||
|
||||
## Package Registries
|
||||
- npm: https://www.npmjs.com/package/handsontable
|
||||
- NuGet: https://www.nuget.org/packages/handsontable
|
||||
|
||||
## Community & Support
|
||||
- Developer Forum: https://forum.handsontable.com/
|
||||
- GitHub Issues: https://github.com/handsontable/handsontable/issues
|
||||
- Stack Overflow: https://stackoverflow.com/tags/handsontable
|
||||
- Commercial support: support@handsontable.com
|
||||
- Contact form: https://handsontable.com/contact
|
||||
- Blog / Release Notes: https://handsontable.com/blog/categories/release-notes
|
||||
- Download page: https://handsontable.com/download
|
||||
@@ -0,0 +1,85 @@
|
||||
# @handsontable/hyperformula-skill
|
||||
|
||||
An agent skill for [**HyperFormula**](https://hyperformula.handsontable.com) — the headless, open-source TypeScript spreadsheet calculation engine. It gives any AI coding agent deep, task-oriented knowledge of HyperFormula so the agent can help you integrate, configure, and debug the engine faster.
|
||||
|
||||
This package is the npm distribution of the `hyperformula` skill maintained in [`handsontable/handsontable-skills`](https://github.com/handsontable/handsontable-skills). The package version tracks the HyperFormula product version the skill targets (e.g. `3.3.0` targets HyperFormula 3.3.0).
|
||||
|
||||
## What's in the package
|
||||
|
||||
A self-contained skill — plain markdown, no runtime dependencies:
|
||||
|
||||
```
|
||||
SKILL.md ← entry point: concepts, routing, and a docs map (YAML frontmatter + body)
|
||||
references/ ← task-oriented deep-dives the agent reads on demand
|
||||
```
|
||||
|
||||
`SKILL.md` follows the [Agent Skills](https://claude.com/blog/skills) layout (a YAML frontmatter block with `name`/`description`, followed by markdown instructions). Any agent that can load a skill — or simply read a folder of markdown instructions — can use it.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install @handsontable/hyperformula-skill
|
||||
```
|
||||
|
||||
The installed package lives at `node_modules/@handsontable/hyperformula-skill`. Resolve that path programmatically with:
|
||||
|
||||
```bash
|
||||
node -p "require.resolve('@handsontable/hyperformula-skill/SKILL.md')"
|
||||
```
|
||||
|
||||
Then make the skill available to your agent using whichever of the following matches it:
|
||||
|
||||
### Claude Code
|
||||
|
||||
Copy the package into a skills directory. User scope (available in every project):
|
||||
|
||||
```bash
|
||||
cp -r node_modules/@handsontable/hyperformula-skill ~/.claude/skills/hyperformula
|
||||
```
|
||||
|
||||
Or project scope, from your project root:
|
||||
|
||||
```bash
|
||||
cp -r node_modules/@handsontable/hyperformula-skill .claude/skills/hyperformula
|
||||
```
|
||||
|
||||
### Claude API
|
||||
|
||||
Upload the package contents (`SKILL.md` + `references/`) to the [Skills API](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview).
|
||||
|
||||
### OpenAI Codex
|
||||
|
||||
Codex discovers [skills](https://developers.openai.com/codex/skills) as folders containing a `SKILL.md`, under `~/.agents/skills` (available in every repo) or `.agents/skills` in a repo (checked in for your team). Copy the package into one of those locations:
|
||||
|
||||
```bash
|
||||
# User scope — available in every repo
|
||||
cp -r node_modules/@handsontable/hyperformula-skill ~/.agents/skills/hyperformula
|
||||
|
||||
# Repo scope — from your project root
|
||||
cp -r node_modules/@handsontable/hyperformula-skill .agents/skills/hyperformula
|
||||
```
|
||||
|
||||
Codex also follows symlinked skill folders, so you can instead link the installed package and let `npm install` keep it current:
|
||||
|
||||
```bash
|
||||
ln -s "$PWD/node_modules/@handsontable/hyperformula-skill" ~/.agents/skills/hyperformula
|
||||
```
|
||||
|
||||
### Any other agent
|
||||
|
||||
Most coding agents support a skills, rules, or knowledge directory. Copy (or symlink) the package folder into it, for example:
|
||||
|
||||
```bash
|
||||
# Replace <agent-skills-dir> with your agent's skills/knowledge directory
|
||||
cp -r node_modules/@handsontable/hyperformula-skill <agent-skills-dir>/hyperformula
|
||||
```
|
||||
|
||||
If your agent reads a single instruction file rather than a folder, point it at `SKILL.md`; it links the files under `references/` so the agent can pull them in as needed. For RAG / vector-store setups, ingest `SKILL.md` together with every file under `references/` (the repository also publishes a pre-flattened `hyperformula-rag.md` for this case).
|
||||
|
||||
## Other distribution formats
|
||||
|
||||
The same skill source is also shipped as a drag-and-drop `.zip` (Cowork / Claude.ai web), a flattened `-rag.md` doc (RAG / vector stores), and a Claude Code plugin marketplace entry. See the [repository README](https://github.com/handsontable/handsontable-skills#readme) for details.
|
||||
|
||||
## License
|
||||
|
||||
MIT — see the [repository](https://github.com/handsontable/handsontable-skills/blob/main/LICENSE.txt). HyperFormula itself is separately licensed (GPLv3 or commercial); see the [licensing guide](https://hyperformula.handsontable.com/docs/guide/licensing.html).
|
||||
@@ -0,0 +1,134 @@
|
||||
---
|
||||
name: hyperformula
|
||||
description: >
|
||||
Use this skill when the user works with HyperFormula (HF) — a headless,
|
||||
open-source TypeScript spreadsheet calculation engine. Use it when the user:
|
||||
mentions HyperFormula or HF; wants to integrate Excel-like formulas into a
|
||||
JS/TS app; evaluate formulas programmatically in a browser or Node.js;
|
||||
simulate spreadsheet or Excel calculation behavior server-side; run
|
||||
calculations over data parsed from XLSX files (e.g. via SheetJS) in code;
|
||||
build a pricing engine, financial model, or what-if analysis in code; define
|
||||
custom spreadsheet functions or named expressions outside of a UI context. Do
|
||||
NOT use this skill for: general Excel questions unrelated to HF; general
|
||||
Google Sheets questions; generic spreadsheet formula-syntax questions outside
|
||||
an HF context; formulas used inside Handsontable (use the "handsontable"
|
||||
skill instead).
|
||||
---
|
||||
|
||||
# HyperFormula
|
||||
|
||||
HyperFormula is a **headless, open-source TypeScript spreadsheet calculation engine** for embedding spreadsheet logic in any JavaScript/TypeScript application (browser or Node.js). ~420 built-in functions, dependency graph, undo/redo, i18n (17 languages). Dual-licensed: GPLv3 or commercial.
|
||||
|
||||
- Docs: https://hyperformula.handsontable.com/
|
||||
- npm: https://www.npmjs.com/package/hyperformula
|
||||
- GitHub: https://github.com/handsontable/hyperformula
|
||||
|
||||
## Where to go next
|
||||
|
||||
Task-oriented references in `references/` (open the one that matches what the user is doing):
|
||||
|
||||
- [`getting-started.md`](references/getting-started.md) — how to install, create an instance (`buildFromArray` / `buildFromSheets` / `buildEmpty`), and work with cell addresses. Open this for onboarding questions and first-run errors.
|
||||
- [`api-quickref.md`](references/api-quickref.md) — runnable examples for CRUD, rows/columns, sheets, exporting data, batching, named expressions, events, undo/redo, clipboard. Open this when implementing or debugging HF API calls.
|
||||
- [`custom-functions.md`](references/custom-functions.md) — extending HyperFormula via `FunctionPlugin`: minimal plugin, argument types, volatile functions, range arguments, returning arrays, error handling, aliases, localized names. Open this when adding or debugging custom spreadsheet functions.
|
||||
- [`configuration.md`](references/configuration.md) — `ConfigParams` options, Excel-compatibility preset, Google-Sheets preset, and config-related pitfalls (separator collisions, Node ICU, `precisionRounding` change in v3). Open this when tuning behavior or diagnosing locale issues.
|
||||
- [`vue3.md`](references/vue3.md) — Vue 3 integration: `markRaw` requirement, Composition API, Pinia/Vuex stores, `destroy()` on unmount. Open this when embedding HF in a Vue 3 app.
|
||||
- [`error-handling.md`](references/error-handling.md) — inspecting `CellError`, `ErrorType` enum, `getCellValueDetailedType`, common error causes (`#NAME?`, `#CYCLE!`, `#REF!`, …). Open this when a cell returns an error or you need to branch on result type.
|
||||
- [`general-pitfalls.md`](references/general-pitfalls.md) — cross-cutting gotchas: `destroy()` lifecycle, forcing literal strings, Excel-parity caveats, hard limits. Open this when results look wrong or memory grows unboundedly.
|
||||
|
||||
Below this section is the **Documentation map** — the canonical directory of links to the official HyperFormula docs. Use it when pointing the user to authoritative material.
|
||||
|
||||
## Documentation map
|
||||
|
||||
All links resolve to `hyperformula.handsontable.com` unless noted.
|
||||
|
||||
### Introduction
|
||||
- Welcome / Homepage: https://hyperformula.handsontable.com/
|
||||
- Demo: https://hyperformula.handsontable.com/docs/guide/demo.html
|
||||
|
||||
### Overview
|
||||
- Quality (test coverage, CI): https://hyperformula.handsontable.com/docs/guide/quality.html
|
||||
- Supported browsers: https://hyperformula.handsontable.com/docs/guide/supported-browsers.html
|
||||
- Dependencies: https://hyperformula.handsontable.com/docs/guide/dependencies.html
|
||||
- Licensing (GPLv3 vs commercial): https://hyperformula.handsontable.com/docs/guide/licensing.html
|
||||
- Support: https://hyperformula.handsontable.com/docs/guide/support.html
|
||||
|
||||
### Getting Started
|
||||
- Client-side installation: https://hyperformula.handsontable.com/docs/guide/client-side-installation.html
|
||||
- Server-side installation (Node.js): https://hyperformula.handsontable.com/docs/guide/server-side-installation.html
|
||||
- Basic usage: https://hyperformula.handsontable.com/docs/guide/basic-usage.html
|
||||
- Advanced usage: https://hyperformula.handsontable.com/docs/guide/advanced-usage.html
|
||||
- Configuration options guide: https://hyperformula.handsontable.com/docs/guide/configuration-options.html
|
||||
- License key setup: https://hyperformula.handsontable.com/docs/guide/license-key.html
|
||||
|
||||
### Framework Integration
|
||||
- React: https://hyperformula.handsontable.com/docs/guide/integration-with-react.html
|
||||
- Vue: https://hyperformula.handsontable.com/docs/guide/integration-with-vue.html
|
||||
- Angular: https://hyperformula.handsontable.com/docs/guide/integration-with-angular.html
|
||||
- Svelte: https://hyperformula.handsontable.com/docs/guide/integration-with-svelte.html
|
||||
|
||||
### Data Operations
|
||||
- Basic operations (CRUD): https://hyperformula.handsontable.com/docs/guide/basic-operations.html
|
||||
- Batch operations: https://hyperformula.handsontable.com/docs/guide/batch-operations.html
|
||||
- Clipboard operations: https://hyperformula.handsontable.com/docs/guide/clipboard-operations.html
|
||||
- Undo-redo: https://hyperformula.handsontable.com/docs/guide/undo-redo.html
|
||||
- Sorting data: https://hyperformula.handsontable.com/docs/guide/sorting-data.html
|
||||
|
||||
### Formulas
|
||||
- Specifications and limits: https://hyperformula.handsontable.com/docs/guide/specifications-and-limits.html
|
||||
- Cell references (relative, absolute, mixed, cross-sheet): https://hyperformula.handsontable.com/docs/guide/cell-references.html
|
||||
- Types of values: https://hyperformula.handsontable.com/docs/guide/types-of-values.html
|
||||
- Types of errors: https://hyperformula.handsontable.com/docs/guide/types-of-errors.html
|
||||
- Types of operators: https://hyperformula.handsontable.com/docs/guide/types-of-operators.html
|
||||
- Order of precedence: https://hyperformula.handsontable.com/docs/guide/order-of-precendece.html
|
||||
- Built-in functions (full list, ~400): https://hyperformula.handsontable.com/docs/guide/built-in-functions.html
|
||||
- Volatile functions: https://hyperformula.handsontable.com/docs/guide/volatile-functions.html
|
||||
- Named expressions: https://hyperformula.handsontable.com/docs/guide/named-expressions.html
|
||||
- Array formulas / ARRAYFORMULA: https://hyperformula.handsontable.com/docs/guide/arrays.html
|
||||
|
||||
### Internationalization
|
||||
- i18n features (17 languages): https://hyperformula.handsontable.com/docs/guide/i18n-features.html
|
||||
- Localizing function names: https://hyperformula.handsontable.com/docs/guide/localizing-functions.html
|
||||
- Date and time handling: https://hyperformula.handsontable.com/docs/guide/date-and-time-handling.html
|
||||
|
||||
### Compatibility
|
||||
- Microsoft Excel compatibility: https://hyperformula.handsontable.com/docs/guide/compatibility-with-microsoft-excel.html
|
||||
- Google Sheets compatibility: https://hyperformula.handsontable.com/docs/guide/compatibility-with-google-sheets.html
|
||||
- Runtime differences (Excel & Sheets): https://hyperformula.handsontable.com/docs/guide/list-of-differences.html
|
||||
|
||||
### Advanced Topics
|
||||
- Key concepts (AST, dependency graph, evaluation): https://hyperformula.handsontable.com/docs/guide/key-concepts.html
|
||||
- Dependency graph: https://hyperformula.handsontable.com/docs/guide/dependency-graph.html
|
||||
- Building & testing from source: https://hyperformula.handsontable.com/docs/guide/building.html
|
||||
- Custom functions (FunctionPlugin): https://hyperformula.handsontable.com/docs/guide/custom-functions.html
|
||||
- Performance: https://hyperformula.handsontable.com/docs/guide/performance.html
|
||||
- Known limitations: https://hyperformula.handsontable.com/docs/guide/known-limitations.html
|
||||
- File import: https://hyperformula.handsontable.com/docs/guide/file-import.html
|
||||
|
||||
### Upgrade & Migration
|
||||
- Release notes / Changelog: https://hyperformula.handsontable.com/docs/guide/release-notes.html
|
||||
- 0.6 → 1.0 migration: https://hyperformula.handsontable.com/docs/guide/migration-from-0.6-to-1.0.html
|
||||
- 1.x → 2.0 migration: https://hyperformula.handsontable.com/docs/guide/migration-from-1.x-to-2.0.html
|
||||
- 2.x → 3.0 migration: https://hyperformula.handsontable.com/docs/guide/migration-from-2.x-to-3.0.html
|
||||
|
||||
### API Reference
|
||||
- API overview: https://hyperformula.handsontable.com/docs/api/
|
||||
- HyperFormula class (all methods): https://hyperformula.handsontable.com/docs/api/classes/hyperformula.html
|
||||
- ConfigParams interface (all options): https://hyperformula.handsontable.com/docs/api/interfaces/configparams.html
|
||||
- Listeners interface (all events): https://hyperformula.handsontable.com/docs/api/interfaces/listeners.html
|
||||
|
||||
### npm
|
||||
- npm page: https://www.npmjs.com/package/hyperformula
|
||||
|
||||
### Community & Support
|
||||
- GitHub repo: https://github.com/handsontable/hyperformula
|
||||
- GitHub Issues: https://github.com/handsontable/hyperformula/issues
|
||||
- GitHub Discussions: https://github.com/handsontable/hyperformula/discussions
|
||||
- Developer Forum: https://forum.handsontable.com/
|
||||
- Commercial support: support@handsontable.com
|
||||
- Sales (commercial license): sales@handsontable.com
|
||||
- Contact form: https://handsontable.com/contact
|
||||
|
||||
---
|
||||
|
||||
> **Last updated: 2026-05-20 · Aligned with HyperFormula 3.3.0.**
|
||||
> If the user is on a newer release, confirm API shape against the latest docs (see the **Release notes** link above) before relying on this file.
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@handsontable/hyperformula-skill",
|
||||
"version": "3.3.0",
|
||||
"description": "Agent skill for HyperFormula — the headless, open-source TypeScript spreadsheet calculation engine. Teaches AI coding agents how to integrate, configure, and debug HyperFormula.",
|
||||
"license": "MIT",
|
||||
"author": "Handsontable <hello@handsontable.com> (https://handsontable.com)",
|
||||
"homepage": "https://github.com/handsontable/handsontable-skills#readme",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/handsontable/handsontable-skills.git",
|
||||
"directory": "skills/hyperformula"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/handsontable/handsontable-skills/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"agent-skill",
|
||||
"claude-skill",
|
||||
"claude",
|
||||
"anthropic",
|
||||
"ai",
|
||||
"hyperformula",
|
||||
"spreadsheet",
|
||||
"formula-engine",
|
||||
"handsontable"
|
||||
],
|
||||
"files": [
|
||||
"SKILL.md",
|
||||
"references/"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
# API Quick Reference
|
||||
|
||||
Runnable examples for the most-used HyperFormula APIs. Authoritative docs:
|
||||
- Basic operations: https://hyperformula.handsontable.com/docs/guide/basic-operations.html
|
||||
- Batch operations: https://hyperformula.handsontable.com/docs/guide/batch-operations.html
|
||||
- Named expressions: https://hyperformula.handsontable.com/docs/guide/named-expressions.html
|
||||
- Clipboard operations: https://hyperformula.handsontable.com/docs/guide/clipboard-operations.html
|
||||
- Undo-redo: https://hyperformula.handsontable.com/docs/guide/undo-redo.html
|
||||
- Sorting data: https://hyperformula.handsontable.com/docs/guide/sorting-data.html
|
||||
- HyperFormula class (all methods): https://hyperformula.handsontable.com/docs/api/classes/hyperformula.html
|
||||
- Listeners (all events): https://hyperformula.handsontable.com/docs/api/interfaces/listeners.html
|
||||
|
||||
## CRUD
|
||||
|
||||
```ts
|
||||
// Write: string, number, or formula
|
||||
hf.setCellContents({ sheet: 0, col: 0, row: 0 }, 'Hello');
|
||||
hf.setCellContents({ sheet: 0, col: 1, row: 0 }, '=A1 & " World"');
|
||||
|
||||
// Read
|
||||
hf.getCellValue({ sheet: 0, col: 1, row: 0 }); // computed result
|
||||
hf.getCellFormula({ sheet: 0, col: 1, row: 0 }); // '=A1 & " World"'
|
||||
hf.getCellType({ sheet: 0, col: 0, row: 0 }); // CellType.FORMULA | VALUE | EMPTY
|
||||
```
|
||||
|
||||
## Rows and columns
|
||||
|
||||
Second arg is `[startIndex, count]`, not a list of indexes.
|
||||
|
||||
```ts
|
||||
hf.addRows(sheetId, [rowIndex, numberOfRows]);
|
||||
hf.removeRows(sheetId, [rowIndex, numberOfRows]);
|
||||
hf.addColumns(sheetId, [colIndex, numberOfColumns]);
|
||||
hf.removeColumns(sheetId, [colIndex, numberOfColumns]);
|
||||
```
|
||||
|
||||
## Sheets
|
||||
|
||||
```ts
|
||||
hf.addSheet('NewSheet');
|
||||
hf.removeSheet(sheetId);
|
||||
hf.renameSheet(sheetId, 'BetterName');
|
||||
hf.countSheets();
|
||||
hf.getSheetName(sheetId);
|
||||
hf.getSheetId('SheetName');
|
||||
hf.getSheetDimensions(sheetId); // → { width, height }
|
||||
```
|
||||
|
||||
## Export data
|
||||
|
||||
```ts
|
||||
hf.getSheetValues(sheetId); // computed values as 2D array
|
||||
hf.getSheetSerialized(sheetId); // formulas/raw values as 2D array
|
||||
hf.getAllSheetsValues(); // { SheetName: values[][] }
|
||||
hf.getAllSheetsSerialized(); // { SheetName: formulas[][] }
|
||||
```
|
||||
|
||||
## Batch operations
|
||||
|
||||
Every `setCellContents` call triggers a full dependency-graph recalculation. Always batch when writing more than one cell.
|
||||
|
||||
```ts
|
||||
// Preferred: batch() — returns ExportedChange[]
|
||||
const changes = hf.batch(() => {
|
||||
hf.setCellContents({ sheet: 0, col: 0, row: 0 }, '100');
|
||||
hf.setCellContents({ sheet: 0, col: 0, row: 1 }, '200');
|
||||
hf.addRows(0, [2, 1]);
|
||||
});
|
||||
// `changes` contains all cells that were recalculated.
|
||||
|
||||
// For bulk imports (value writes only):
|
||||
hf.suspendEvaluation();
|
||||
// ... hundreds of setCellContents calls ...
|
||||
hf.resumeEvaluation(); // single recalc
|
||||
|
||||
// PITFALL: structural ops (addRows/moveRows/...) during suspendEvaluation
|
||||
// have degraded performance. Use batch() when mixing structural + value writes.
|
||||
```
|
||||
|
||||
## Sorting
|
||||
|
||||
`setRowOrder` takes a **permutation array**, not a comparator. Compute the new index sequence externally, then pass it in.
|
||||
|
||||
```ts
|
||||
// Sort rows by the value in column 0
|
||||
const rowCount = hf.getSheetDimensions(0).height;
|
||||
const indexes = Array.from({ length: rowCount }, (_, i) => i);
|
||||
indexes.sort((a, b) => {
|
||||
const va = hf.getCellValue({ sheet: 0, col: 0, row: a }) as number;
|
||||
const vb = hf.getCellValue({ sheet: 0, col: 0, row: b }) as number;
|
||||
return va - vb;
|
||||
});
|
||||
hf.setRowOrder(0, indexes); // e.g. [2, 0, 1, 3]
|
||||
```
|
||||
|
||||
## Named expressions
|
||||
|
||||
Reusable names for values or formulas. Can be global or scoped to a sheet (local shadows global).
|
||||
|
||||
```ts
|
||||
hf.addNamedExpression('TAX_RATE', '0.21');
|
||||
hf.addNamedExpression('TOTAL_REVENUE', '=SUM(Revenue!A:A)');
|
||||
|
||||
hf.setCellContents({ sheet: 0, col: 0, row: 0 }, '=1000 * TAX_RATE');
|
||||
|
||||
hf.listNamedExpressions(); // all registered names
|
||||
```
|
||||
|
||||
## Custom functions
|
||||
|
||||
See [custom-functions.md](custom-functions.md) — `FunctionPlugin`, argument types, volatile functions, range arguments, returning arrays, error handling, aliases, localized names.
|
||||
|
||||
## Events
|
||||
|
||||
Subscribe via `.on()` on the instance. Full list: https://hyperformula.handsontable.com/docs/api/interfaces/listeners.html
|
||||
|
||||
```ts
|
||||
hf.on('valuesUpdated', (changes) => {
|
||||
changes.forEach((c) => console.log(c.address, c.newValue));
|
||||
});
|
||||
|
||||
hf.on('sheetAdded', (addedSheetName) => { /* ... */ });
|
||||
hf.on('sheetRemoved', (removedSheetName, removedSheetData) => { /* ... */ });
|
||||
hf.on('sheetRenamed', (oldName, newName) => { /* ... */ });
|
||||
```
|
||||
|
||||
## Undo / redo
|
||||
|
||||
```ts
|
||||
hf.undo();
|
||||
hf.redo();
|
||||
hf.isThereSomethingToUndo();
|
||||
hf.isThereSomethingToRedo();
|
||||
hf.clearUndoStack();
|
||||
```
|
||||
|
||||
Configure depth via `undoLimit` in `ConfigParams` (see [configuration.md](configuration.md)).
|
||||
|
||||
## Clipboard
|
||||
|
||||
```ts
|
||||
hf.copy({ start: { sheet: 0, col: 0, row: 0 }, end: { sheet: 0, col: 1, row: 1 } });
|
||||
hf.cut ({ start: { sheet: 0, col: 0, row: 0 }, end: { sheet: 0, col: 1, row: 1 } });
|
||||
hf.paste({ sheet: 0, col: 5, row: 5 });
|
||||
hf.isClipboardEmpty();
|
||||
hf.clearClipboard();
|
||||
```
|
||||
@@ -0,0 +1,92 @@
|
||||
# Configuration
|
||||
|
||||
`ConfigParams` + compatibility presets + config-specific pitfalls. Authoritative docs:
|
||||
- Configuration options guide: https://hyperformula.handsontable.com/docs/guide/configuration-options.html
|
||||
- ConfigParams (all options): https://hyperformula.handsontable.com/docs/api/interfaces/configparams.html
|
||||
- Excel compatibility: https://hyperformula.handsontable.com/docs/guide/compatibility-with-microsoft-excel.html
|
||||
- Google Sheets compatibility: https://hyperformula.handsontable.com/docs/guide/compatibility-with-google-sheets.html
|
||||
|
||||
## ConfigParams highlights
|
||||
|
||||
Pass as the second argument to any factory method.
|
||||
|
||||
```ts
|
||||
const hf = HyperFormula.buildEmpty({
|
||||
licenseKey: 'gpl-v3', // required — 'gpl-v3' for open source
|
||||
precisionRounding: 10, // decimal rounding precision (default 10; was 14 before v3)
|
||||
functionArgSeparator: ',', // separator between function arguments
|
||||
dateFormats: ['DD/MM/YYYY', 'DD-MM-YYYY'],
|
||||
nullDate: { year: 1900, month: 1, day: 1 },
|
||||
leapYear1900: false, // Excel bug compat: treat 1900 as leap year
|
||||
maxRows: 40000, // max rows per sheet (default 40000)
|
||||
maxColumns: 18278, // max columns per sheet (default 18278 = 'ZZZ')
|
||||
undoLimit: 20, // undo history depth
|
||||
maxPendingLazyTransformations: 1000, // v3.3+ — cap accumulated lazy transformations before cleanup
|
||||
});
|
||||
```
|
||||
|
||||
## Excel compatibility preset
|
||||
|
||||
```ts
|
||||
const hf = HyperFormula.buildEmpty({
|
||||
licenseKey: 'gpl-v3',
|
||||
useArrayArithmetic: true,
|
||||
useWildcards: true,
|
||||
evaluateNullToZero: true,
|
||||
leapYear1900: true, // Excel's intentional 1900 leap-year bug
|
||||
ignoreWhiteSpace: 'any',
|
||||
caseSensitive: false,
|
||||
accentSensitive: true,
|
||||
});
|
||||
```
|
||||
|
||||
## Google Sheets compatibility preset
|
||||
|
||||
Defaults already match Google Sheets behavior (`leapYear1900: false`, `useArrayArithmetic: false`).
|
||||
|
||||
```ts
|
||||
const hf = HyperFormula.buildEmpty({ licenseKey: 'gpl-v3' });
|
||||
```
|
||||
|
||||
## Config-specific pitfalls
|
||||
|
||||
### Vue 3
|
||||
|
||||
Vue 3 requires wrapping the instance with `markRaw` — see [vue3.md](vue3.md) for the full integration guide (React / Angular / Svelte need no special handling).
|
||||
|
||||
### `functionArgSeparator` vs `thousandSeparator` collision
|
||||
|
||||
These two options **cannot share a character**. European locales that use `,` as thousand separator must change the argument separator:
|
||||
|
||||
```ts
|
||||
const hf = HyperFormula.buildEmpty({
|
||||
licenseKey: 'gpl-v3',
|
||||
thousandSeparator: ',',
|
||||
decimalSeparator: '.',
|
||||
functionArgSeparator: ';', // must differ from thousandSeparator
|
||||
});
|
||||
```
|
||||
|
||||
### Node.js must have full ICU
|
||||
|
||||
String comparisons (used in `MATCH`, `VLOOKUP`, sorting) can silently produce wrong results on Node.js < 13 or builds without full ICU. Verify at startup:
|
||||
|
||||
```ts
|
||||
// Must report 'full' (or a detailed ICU version), not 'small'.
|
||||
console.log(process.versions.icu);
|
||||
```
|
||||
|
||||
### Tuning `maxPendingLazyTransformations` (v3.3+)
|
||||
|
||||
HyperFormula defers some structural transformations (row/column insertions, moves) and applies them lazily. In long-lived instances with heavy mutation throughput — bulk imports, frequent undo/redo, scripted batch edits — the pending queue can grow before cleanup. v3.3 fixed the unbounded-growth leak; this option lets you cap the queue explicitly. Lower values trade memory for more frequent flush work; the default is suitable for typical UI workloads.
|
||||
|
||||
### `precisionRounding` default changed in v3
|
||||
|
||||
Before v3 the default was `14`. It is now `10`. Calculations relying on the old precision need an explicit override:
|
||||
|
||||
```ts
|
||||
const hf = HyperFormula.buildEmpty({
|
||||
licenseKey: 'gpl-v3',
|
||||
precisionRounding: 14, // pre-v3 behavior
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,174 @@
|
||||
# Custom Functions
|
||||
|
||||
Extend HyperFormula with your own functions via `FunctionPlugin`. Authoritative doc:
|
||||
- Custom functions guide: https://hyperformula.handsontable.com/docs/guide/custom-functions.html
|
||||
|
||||
## Minimal plugin
|
||||
|
||||
Register **before** creating any instance — functions registered after `buildFromArray` / `buildFromSheets` / `buildEmpty` won't be available.
|
||||
|
||||
```ts
|
||||
import { HyperFormula, FunctionPlugin, FunctionArgumentType } from 'hyperformula';
|
||||
|
||||
class MyPlugin extends FunctionPlugin {
|
||||
greet(ast, state) {
|
||||
return this.runFunction(
|
||||
ast.args,
|
||||
state,
|
||||
this.metadata('GREET'),
|
||||
(name) => `Hello, ${name}!`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MyPlugin.implementedFunctions = {
|
||||
GREET: {
|
||||
method: 'greet',
|
||||
parameters: [{ argumentType: FunctionArgumentType.STRING }],
|
||||
},
|
||||
};
|
||||
|
||||
const translations = { enGB: { GREET: 'GREET' }, enUS: { GREET: 'GREET' } };
|
||||
|
||||
// PITFALL: registration MUST happen before any instance is built.
|
||||
HyperFormula.registerFunctionPlugin(MyPlugin, translations);
|
||||
|
||||
const hf = HyperFormula.buildFromArray(
|
||||
[['World', '=GREET(A1)']],
|
||||
{ licenseKey: 'gpl-v3' }
|
||||
);
|
||||
console.log(hf.getCellValue({ sheet: 0, col: 1, row: 0 })); // "Hello, World!"
|
||||
```
|
||||
|
||||
## Volatile functions
|
||||
|
||||
Mark a function as volatile so it recalculates on every change (like `RAND` or `NOW`). Volatile functions are expensive in large sheets — use sparingly.
|
||||
|
||||
```ts
|
||||
MyPlugin.implementedFunctions = {
|
||||
RAND_ID: {
|
||||
method: 'randId',
|
||||
parameters: [],
|
||||
isVolatile: true,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Argument types
|
||||
|
||||
```ts
|
||||
import { FunctionArgumentType } from 'hyperformula';
|
||||
|
||||
// FunctionArgumentType.NUMBER
|
||||
// FunctionArgumentType.STRING
|
||||
// FunctionArgumentType.BOOLEAN
|
||||
// FunctionArgumentType.NOERROR
|
||||
// FunctionArgumentType.SCALAR
|
||||
// FunctionArgumentType.RANGE // accepts a cell range
|
||||
// FunctionArgumentType.ANY
|
||||
```
|
||||
|
||||
Use `optionalArg: true`, `defaultValue`, and `minValue` / `maxValue` to validate arguments declaratively:
|
||||
|
||||
```ts
|
||||
MyPlugin.implementedFunctions = {
|
||||
CLAMP: {
|
||||
method: 'clamp',
|
||||
parameters: [
|
||||
{ argumentType: FunctionArgumentType.NUMBER },
|
||||
{ argumentType: FunctionArgumentType.NUMBER, optionalArg: true, defaultValue: 0 },
|
||||
{ argumentType: FunctionArgumentType.NUMBER, optionalArg: true, defaultValue: 100 },
|
||||
],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Range arguments
|
||||
|
||||
Use `RANGE` argument type when the function takes a cell area. The callback receives the range as a 2D array.
|
||||
|
||||
```ts
|
||||
class SumPositivesPlugin extends FunctionPlugin {
|
||||
sumPositives(ast, state) {
|
||||
return this.runFunction(
|
||||
ast.args,
|
||||
state,
|
||||
this.metadata('SUM_POSITIVES'),
|
||||
(range) => {
|
||||
let total = 0;
|
||||
for (const row of range.data) {
|
||||
for (const v of row) if (typeof v === 'number' && v > 0) total += v;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SumPositivesPlugin.implementedFunctions = {
|
||||
SUM_POSITIVES: {
|
||||
method: 'sumPositives',
|
||||
parameters: [{ argumentType: FunctionArgumentType.RANGE }],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Returning arrays
|
||||
|
||||
Custom functions can return 2D arrays. **PITFALL:** result arrays don't auto-resize when upstream dependencies change — the footprint is fixed at first evaluation.
|
||||
|
||||
```ts
|
||||
class SplitPlugin extends FunctionPlugin {
|
||||
splitToRow(ast, state) {
|
||||
return this.runFunction(
|
||||
ast.args,
|
||||
state,
|
||||
this.metadata('SPLIT_ROW'),
|
||||
(text, sep) => [text.split(sep)] // 2D array: one row, N cols
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error handling
|
||||
|
||||
Return a `CellError` to signal a formula error:
|
||||
|
||||
```ts
|
||||
import { CellError, ErrorType } from 'hyperformula';
|
||||
|
||||
class DividePlugin extends FunctionPlugin {
|
||||
safeDivide(ast, state) {
|
||||
return this.runFunction(
|
||||
ast.args,
|
||||
state,
|
||||
this.metadata('SAFE_DIVIDE'),
|
||||
(a, b) => (b === 0 ? new CellError(ErrorType.DIV_BY_ZERO) : a / b)
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Aliases and localized names
|
||||
|
||||
Expose one method under multiple function names, or localize:
|
||||
|
||||
```ts
|
||||
MyPlugin.aliases = {
|
||||
HELLO: 'GREET', // calling =HELLO(x) runs the greet() method
|
||||
};
|
||||
|
||||
const translations = {
|
||||
enGB: { GREET: 'GREET' },
|
||||
plPL: { GREET: 'POWITAJ' }, // =POWITAJ("Świat") in Polish
|
||||
};
|
||||
HyperFormula.registerFunctionPlugin(MyPlugin, translations);
|
||||
```
|
||||
|
||||
## Unregister / introspect
|
||||
|
||||
```ts
|
||||
HyperFormula.unregisterFunctionPlugin(MyPlugin);
|
||||
HyperFormula.unregisterFunction('GREET');
|
||||
HyperFormula.getRegisteredFunctionNames('enGB');
|
||||
```
|
||||
@@ -0,0 +1,69 @@
|
||||
# Error Handling
|
||||
|
||||
Inspect cell values that may be errors, tell error types apart, and check value shape. Authoritative docs:
|
||||
- Types of errors: https://hyperformula.handsontable.com/docs/guide/types-of-errors.html
|
||||
- Types of values: https://hyperformula.handsontable.com/docs/guide/types-of-values.html
|
||||
|
||||
## Always check `CellError` before using a result
|
||||
|
||||
Cell values can be errors (`#DIV/0!`, `#VALUE!`, `#REF!`, etc.). Always test before using a result.
|
||||
|
||||
```ts
|
||||
import { CellError, ErrorType } from 'hyperformula';
|
||||
|
||||
const value = hf.getCellValue({ sheet: 0, col: 0, row: 0 });
|
||||
|
||||
if (value instanceof CellError) {
|
||||
// ErrorType enum: DIV_BY_ZERO, VALUE, REF, NAME, NUM, NA, CYCLE, ERROR
|
||||
switch (value.type) {
|
||||
case ErrorType.CYCLE:
|
||||
console.log('Circular reference');
|
||||
break;
|
||||
case ErrorType.NAME:
|
||||
// Usually: function not registered (check plugin registration or i18n language)
|
||||
console.log('Unknown name:', value.message);
|
||||
break;
|
||||
default:
|
||||
console.log('Error:', value.type, value.message);
|
||||
}
|
||||
} else {
|
||||
console.log('Value:', value);
|
||||
}
|
||||
```
|
||||
|
||||
## `#CYCLE!` is HyperFormula-specific
|
||||
|
||||
Standard spreadsheet apps report cycles differently. HyperFormula's `IF` also reports cycles for all branches, even unreachable ones — this can produce `#CYCLE!` in formulas that Excel or Sheets would evaluate.
|
||||
|
||||
## Inspect value shape without catching errors
|
||||
|
||||
```ts
|
||||
import { CellValueDetailedType } from 'hyperformula';
|
||||
|
||||
hf.getCellValueDetailedType({ sheet: 0, col: 0, row: 0 });
|
||||
// → CellValueDetailedType.NUMBER | STRING | BOOLEAN | ERROR | EMPTY | ...
|
||||
```
|
||||
|
||||
Use the detailed type when you need to distinguish e.g. number vs empty without touching the value.
|
||||
|
||||
## Returning errors from custom functions
|
||||
|
||||
Custom `FunctionPlugin` methods can return a `CellError` to surface a formula error. See [custom-functions.md](custom-functions.md) for the full pattern.
|
||||
|
||||
```ts
|
||||
import { CellError, ErrorType } from 'hyperformula';
|
||||
|
||||
return new CellError(ErrorType.DIV_BY_ZERO);
|
||||
```
|
||||
|
||||
## Common causes
|
||||
|
||||
| Error | Common cause |
|
||||
|---|---|
|
||||
| `#NAME?` | Function not registered (check `registerFunctionPlugin` order or `language` config) |
|
||||
| `#CYCLE!` | Circular reference — or an `IF` branch that *could* produce one |
|
||||
| `#REF!` | Deleted row/column broke a formula reference |
|
||||
| `#VALUE!` | Type mismatch — e.g. passing a string where a number is expected |
|
||||
| `#DIV/0!` | Division by zero, including empty cells coerced to 0 |
|
||||
| `#NUM!` | Out-of-range numeric result (e.g. `SQRT(-1)`) |
|
||||
| `#N/A` | Lookup miss (`MATCH`, `VLOOKUP`) or propagated from an upstream `#N/A` |
|
||||
@@ -0,0 +1,48 @@
|
||||
# General Pitfalls
|
||||
|
||||
Cross-cutting gotchas that aren't tied to a specific API or config option. Authoritative docs:
|
||||
- Known limitations: https://hyperformula.handsontable.com/docs/guide/known-limitations.html
|
||||
- Built-in functions (full list): https://hyperformula.handsontable.com/docs/guide/built-in-functions.html
|
||||
- Runtime differences vs Excel/Sheets: https://hyperformula.handsontable.com/docs/guide/list-of-differences.html
|
||||
|
||||
## Error handling
|
||||
|
||||
See [error-handling.md](error-handling.md) — checking `CellError`, `ErrorType` enum, `getCellValueDetailedType`, common error causes.
|
||||
|
||||
## Always call `destroy()` in long-running apps
|
||||
|
||||
HyperFormula maintains internal data structures (dependency graph, address mapping) that are **not** garbage-collected until `destroy()` is called. Leaking instances in servers or SPAs accumulates memory.
|
||||
|
||||
```ts
|
||||
hf.destroy();
|
||||
// After destroy() the instance is unusable — create a new one if needed.
|
||||
```
|
||||
|
||||
v3.3 fixed two longstanding leak sources inside live instances — pending lazy transformations and undo/redo history were not being trimmed. If you maintain very long-lived instances with heavy mutation throughput, also see `maxPendingLazyTransformations` in [configuration.md](configuration.md) to bound the lazy-transformation queue. `destroy()` is still mandatory at teardown.
|
||||
|
||||
## Force a string that looks like a formula
|
||||
|
||||
Prefix with `'` (apostrophe) to store the literal text instead of evaluating.
|
||||
|
||||
```ts
|
||||
// Stored as the literal string "=SUM(1,2)", not a formula:
|
||||
hf.setCellContents({ sheet: 0, col: 0, row: 0 }, "'=SUM(1,2)");
|
||||
```
|
||||
|
||||
## Don't assume Excel parity
|
||||
|
||||
~68% of Excel functions are covered. Runtime differences exist even for implemented functions. Before relying on behavior, check:
|
||||
|
||||
- Full built-in list: https://hyperformula.handsontable.com/docs/guide/built-in-functions.html
|
||||
- Runtime differences: https://hyperformula.handsontable.com/docs/guide/list-of-differences.html
|
||||
|
||||
## `licenseKey` is always required
|
||||
|
||||
Every factory method (`buildFromArray`, `buildFromSheets`, `buildEmpty`) requires `licenseKey`. Use `'gpl-v3'` for open-source use or your commercial key.
|
||||
|
||||
## Known hard limits
|
||||
|
||||
- **Single workbook per instance** — no multi-workbook support.
|
||||
- No 3D references, dynamic arrays, async functions, structured references ("Tables"), or relative named expressions.
|
||||
- `IF` reports cycles for all branches, even unreachable ones.
|
||||
- Custom function result arrays don't auto-resize when dependencies change.
|
||||
@@ -0,0 +1,82 @@
|
||||
# Getting Started
|
||||
|
||||
Install, create an instance, read/write cells. Authoritative docs:
|
||||
- Client-side install: https://hyperformula.handsontable.com/docs/guide/client-side-installation.html
|
||||
- Server-side install: https://hyperformula.handsontable.com/docs/guide/server-side-installation.html
|
||||
- Basic usage: https://hyperformula.handsontable.com/docs/guide/basic-usage.html
|
||||
|
||||
## Install
|
||||
|
||||
### npm
|
||||
|
||||
```bash
|
||||
npm install hyperformula
|
||||
```
|
||||
|
||||
### CDN (jsDelivr, pinned version)
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/hyperformula@3.2.0/dist/hyperformula.full.min.js"></script>
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
Same npm install. Requires Node.js 13+ with full ICU for locale-aware string comparison.
|
||||
|
||||
## Create an instance
|
||||
|
||||
Every factory method requires `licenseKey`. Use `'gpl-v3'` for open-source use or your commercial key.
|
||||
|
||||
### `buildFromArray` — single sheet from a 2D array
|
||||
|
||||
```ts
|
||||
import { HyperFormula } from 'hyperformula';
|
||||
|
||||
const hf = HyperFormula.buildFromArray(
|
||||
[
|
||||
['10', '20', '=SUM(A1:B1)'],
|
||||
['30', '40', '=SUM(A2:B2)'],
|
||||
],
|
||||
{ licenseKey: 'gpl-v3' }
|
||||
);
|
||||
|
||||
console.log(hf.getCellValue({ sheet: 0, col: 2, row: 0 })); // 30
|
||||
```
|
||||
|
||||
### `buildFromSheets` — multi-sheet workbook
|
||||
|
||||
```ts
|
||||
const hf = HyperFormula.buildFromSheets(
|
||||
{
|
||||
Revenue: [['100', '200', '=SUM(A1:B1)']],
|
||||
Expenses: [['50', '=Revenue!C1 - A1']],
|
||||
},
|
||||
{ licenseKey: 'gpl-v3' }
|
||||
);
|
||||
```
|
||||
|
||||
Cross-sheet references use `SheetName!CellRef` syntax.
|
||||
|
||||
### `buildEmpty` — start empty, add sheets later
|
||||
|
||||
```ts
|
||||
const hf = HyperFormula.buildEmpty({ licenseKey: 'gpl-v3' });
|
||||
const sheetName = hf.addSheet('Data');
|
||||
const sheetId = hf.getSheetId(sheetName);
|
||||
|
||||
hf.setCellContents({ sheet: sheetId, col: 0, row: 0 }, [
|
||||
['10', '20', '=SUM(A1:B1)'],
|
||||
]);
|
||||
```
|
||||
|
||||
## Cell addresses
|
||||
|
||||
All addresses use zero-indexed `{ sheet, col, row }`. Convert to/from A1 notation with built-in helpers:
|
||||
|
||||
```ts
|
||||
hf.simpleCellAddressFromString('B3', 0);
|
||||
// → { sheet: 0, col: 1, row: 2 }
|
||||
|
||||
hf.simpleCellAddressToString({ sheet: 0, col: 1, row: 2 }, 0);
|
||||
// → 'B3'
|
||||
```
|
||||
@@ -0,0 +1,72 @@
|
||||
# Vue 3
|
||||
|
||||
HyperFormula + Vue 3 integration. Authoritative doc:
|
||||
- Vue integration guide: https://hyperformula.handsontable.com/docs/guide/integration-with-vue.html
|
||||
|
||||
## Always wrap the instance with `markRaw`
|
||||
|
||||
Vue 3's Composition API wraps objects in a reactive Proxy that intercepts property access and **corrupts HyperFormula's internal state**, causing silent data corruption or crashes. React, Angular, and Svelte need no special handling.
|
||||
|
||||
```ts
|
||||
import { markRaw } from 'vue';
|
||||
import { HyperFormula } from 'hyperformula';
|
||||
|
||||
const hf = markRaw(
|
||||
HyperFormula.buildEmpty({ licenseKey: 'gpl-v3' })
|
||||
);
|
||||
```
|
||||
|
||||
Use the same wrapper no matter where the instance is held — `ref`, `reactive`, component state, a store (Pinia/Vuex), or a composable. If the raw HyperFormula instance ever reaches Vue's reactivity system, it must be marked.
|
||||
|
||||
## Composition API example
|
||||
|
||||
```ts
|
||||
import { onBeforeUnmount, ref } from 'vue';
|
||||
import { markRaw } from 'vue';
|
||||
import { HyperFormula } from 'hyperformula';
|
||||
|
||||
export function useHyperFormula() {
|
||||
const hf = markRaw(
|
||||
HyperFormula.buildEmpty({ licenseKey: 'gpl-v3' })
|
||||
);
|
||||
|
||||
// Free internal data structures when the component unmounts.
|
||||
onBeforeUnmount(() => hf.destroy());
|
||||
|
||||
const result = ref<unknown>(null);
|
||||
|
||||
function evaluate(formula: string) {
|
||||
hf.setCellContents({ sheet: 0, col: 0, row: 0 }, formula);
|
||||
result.value = hf.getCellValue({ sheet: 0, col: 0, row: 0 });
|
||||
}
|
||||
|
||||
return { evaluate, result };
|
||||
}
|
||||
```
|
||||
|
||||
## Pinia / Vuex store
|
||||
|
||||
```ts
|
||||
import { defineStore } from 'pinia';
|
||||
import { markRaw } from 'vue';
|
||||
import { HyperFormula } from 'hyperformula';
|
||||
|
||||
export const useCalcStore = defineStore('calc', {
|
||||
state: () => ({
|
||||
hf: markRaw(
|
||||
HyperFormula.buildEmpty({ licenseKey: 'gpl-v3' })
|
||||
),
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
## Cleanup
|
||||
|
||||
HyperFormula's internal graph is **not** garbage-collected until `destroy()` is called. In SPAs this accumulates memory over time.
|
||||
|
||||
```ts
|
||||
import { onBeforeUnmount } from 'vue';
|
||||
|
||||
onBeforeUnmount(() => hf.destroy());
|
||||
// After destroy() the instance is unusable — create a new one if needed.
|
||||
```
|
||||
+19
-7
@@ -1,11 +1,23 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Avoid commits to the master branch
|
||||
BRANCH=`git rev-parse --abbrev-ref HEAD`
|
||||
REGEX="^(master|development)$"
|
||||
# Using `--silent` helps for showing any errs in the first line of the response
|
||||
# The first line is picked up by the VS Code GIT UI popup when rc is not 0
|
||||
|
||||
if [[ "$BRANCH" =~ $REGEX ]]; then
|
||||
echo "You are on branch $BRANCH. Are you sure you want to commit to this branch?"
|
||||
echo "If so, commit with -n to bypass the pre-commit hook."
|
||||
exit 1
|
||||
if npm run --silent lint:check:silent ; then
|
||||
exit 0
|
||||
else
|
||||
npm run --silent lint:fix:silent
|
||||
echo "❌ Prettier check failed! We ran lint:fix for you. Please add & commit again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
## Avoid large commits
|
||||
# https://www.backblaze.com/blog/how-many-bytes-are-in-a-megabyte-really/
|
||||
size_limit=$((2 * 2**20)) # 2mbs
|
||||
# https://git-scm.com/docs/git-rev-list#Documentation/git-rev-list.txt---disk-usage
|
||||
commit_size=$(git rev-list --disk-usage HEAD^..HEAD)
|
||||
test "$commit_size" -lt "$size_limit" || (
|
||||
echo "Commit size is too large: $commit_size > $size_limit"
|
||||
echo "Force commit using --no-verify"
|
||||
exit 1
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
+144
-20
@@ -1,38 +1,162 @@
|
||||
name: Build
|
||||
run-name: Building and testing DC
|
||||
run-name: Running Lint Check and Licence checker on Pull Request
|
||||
on: [pull_request]
|
||||
|
||||
env:
|
||||
NODE_VERSION: '24.15.0'
|
||||
|
||||
jobs:
|
||||
Build-and-ng-test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install Google Chrome
|
||||
run: |
|
||||
apt-get update
|
||||
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||
apt install -y ./google-chrome*.deb
|
||||
|
||||
- name: Write .npmrc file
|
||||
run: echo "$NPMRC" > client/.npmrc
|
||||
run: echo "$NPMRC" >> client/.npmrc
|
||||
shell: bash
|
||||
env:
|
||||
NPMRC: ${{ secrets.NPMRC}}
|
||||
|
||||
- run: apt-get update
|
||||
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||
- run: apt install -y ./google-chrome*.deb;
|
||||
- run: export CHROME_BIN=/usr/bin/google-chrome
|
||||
- run: npm run lint:check
|
||||
# Install dependencies~
|
||||
- run: npm ci --legacy-peer-deps
|
||||
# Audit should fail and stop the CI if critical vulnerability found
|
||||
- run: npm audit --audit-level=critical
|
||||
- run: |
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd client
|
||||
# Decrypt and Install sheet
|
||||
echo "${{ secrets.SHEET_PWD }}" | \
|
||||
gpg --batch --yes --passphrase-fd 0 \
|
||||
--output ./libraries/sheet-crypto.tgz \
|
||||
--decrypt ./libraries/sheet-crypto.tgz.gpg
|
||||
npm ci
|
||||
|
||||
- name: Check audit
|
||||
# Audit should fail and stop the CI if critical vulnerability found
|
||||
run: |
|
||||
npm audit --audit-level=critical --omit=dev
|
||||
cd ./sas
|
||||
npm audit --audit-level=critical
|
||||
- run: |
|
||||
npm audit --audit-level=critical --omit=dev
|
||||
cd ../client
|
||||
npm audit --audit-level=critical --omit=dev
|
||||
|
||||
- name: Lint check
|
||||
run: npm run lint:check
|
||||
|
||||
- name: Licence checker
|
||||
run: |
|
||||
cd client
|
||||
npm run license-checker
|
||||
|
||||
- name: Angular Tests
|
||||
run: |
|
||||
cd client
|
||||
npm run test:headless
|
||||
|
||||
- name: Production Build
|
||||
run: |
|
||||
cd client
|
||||
npm run build
|
||||
|
||||
Build-and-test-development:
|
||||
runs-on: ubuntu-latest
|
||||
needs: Build-and-ng-test
|
||||
env:
|
||||
CHROME_BIN: /usr/bin/google-chrome
|
||||
# Pin OS locale
|
||||
LANG: en_GB.UTF-8
|
||||
LC_ALL: en_GB.UTF-8
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Write .npmrc file
|
||||
run: |
|
||||
touch client/.npmrc
|
||||
echo '${{ secrets.NPMRC}}' > client/.npmrc
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||
apt install -y ./google-chrome*.deb
|
||||
apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb jq zip locales
|
||||
# Generate the en_GB.UTF-8 locale referenced by LANG/LC_ALL
|
||||
locale-gen en_GB.UTF-8
|
||||
update-locale LANG=en_GB.UTF-8 LC_ALL=en_GB.UTF-8
|
||||
|
||||
- name: Write cypress credentials
|
||||
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
|
||||
shell: bash
|
||||
env:
|
||||
CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd client
|
||||
# Decrypt and Install sheet
|
||||
echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
|
||||
npm ci
|
||||
|
||||
- name: Setup and start SASjs server
|
||||
run: |
|
||||
npm i -g pm2
|
||||
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
||||
unzip linux.zip
|
||||
touch .env
|
||||
echo RUN_TIMES=js >> .env
|
||||
echo NODE_PATH=node >> .env
|
||||
echo CORS=enable >> .env
|
||||
echo WHITELIST=http://localhost:4200 >> .env
|
||||
cat .env
|
||||
pm2 start api-linux --wait-ready
|
||||
|
||||
- name: Deploy mocked services
|
||||
run: |
|
||||
cd ./sas/mocks/sasjs
|
||||
npm install -g @sasjs/cli
|
||||
npm install -g replace-in-files-cli
|
||||
sasjs cbd -t server-ci
|
||||
# sasjs request services/admin/makedata -t server-ci -d ./deploy/makeData4GL.json -c ./deploy/requestConfig.json -o ./output.json
|
||||
|
||||
- name: Prepare and run frontend and cypress
|
||||
timeout-minutes: 35
|
||||
run: |
|
||||
cd ./client
|
||||
npm audit --audit-level=critical
|
||||
npm test -- --no-watch --no-progress --browsers=ChromeHeadlessCI
|
||||
mv ./cypress.env.example.json ./cypress.env.json
|
||||
replace-in-files --regex='"username".*' --replacement='"username":"'${{ secrets.CYPRESS_USERNAME_SASJS }}'",' ./cypress.env.json
|
||||
replace-in-files --regex='"password".*' --replacement='"password":"'${{ secrets.CYPRESS_PWD_SASJS }}'" ' ./cypress.env.json
|
||||
cat ./cypress.env.json
|
||||
npm run postinstall
|
||||
npm run build
|
||||
# Prepare index.html to SASJS local
|
||||
replace-in-files --regex='serverUrl=".*?"' --replacement='serverUrl="http://localhost:5000"' ./src/index.html
|
||||
replace-in-files --regex='appLoc=".*?"' --replacement='appLoc="/Public/app/devtest"' ./src/index.html
|
||||
replace-in-files --regex='serverType=".*?"' --replacement='serverType="SASJS"' ./src/index.html
|
||||
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
|
||||
cat ./cypress.config.ts
|
||||
# Start frontend and run cypress
|
||||
# timeout 1800: SIGTERM after 30 min so Cypress can flush video/screenshots
|
||||
# before the outer timeout-minutes hard-kills the step (avoids silent multi-hour hangs)
|
||||
npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && timeout 1800 npx cypress run --browser chrome --spec "cypress/e2e/csv-limited.cy.ts,cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts,cypress/e2e/viewer-labels.cy.ts"
|
||||
|
||||
- name: Zip Cypress videos
|
||||
if: always()
|
||||
run: |
|
||||
mkdir -p ./client/cypress/videos
|
||||
zip -r cypress-videos ./client/cypress/videos
|
||||
|
||||
- name: Add cypress videos artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: cypress-videos.zip
|
||||
path: cypress-videos.zip
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
name: Test
|
||||
run-name: Building and testing development branch
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- development
|
||||
|
||||
jobs:
|
||||
Build-and-test-development:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Write .npmrc file
|
||||
run: echo "$NPMRC" > client/.npmrc
|
||||
shell: bash
|
||||
env:
|
||||
NPMRC: ${{ secrets.NPMRC}}
|
||||
|
||||
- run: apt-get update
|
||||
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||
- run: apt install -y ./google-chrome*.deb;
|
||||
- run: export CHROME_BIN=/usr/bin/google-chrome
|
||||
- run: apt-get update -y
|
||||
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
|
||||
- run: apt -y install jq
|
||||
# - run: 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
|
||||
# - run: eval $(ssh-agent -s)
|
||||
# - run: echo "$ssh_key" | tr -d '\r' | ssh-add -
|
||||
- name: Write cypress credentials
|
||||
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
|
||||
shell: bash
|
||||
env:
|
||||
CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }}
|
||||
|
||||
# - run: mkdir -p ~/.ssh
|
||||
# - run: chmod 700 ~/.ssh
|
||||
- run: npm ci --legacy-peer-deps
|
||||
# Install pm2 and prepare SASJS server
|
||||
- run: npm i -g pm2
|
||||
- run: curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
||||
- run: unzip linux.zip
|
||||
- run: touch .env
|
||||
- run: echo RUN_TIMES=js >> .env
|
||||
- run: echo NODE_PATH=node >> .env
|
||||
- run: echo CORS=enable >> .env
|
||||
- run: echo WHITELIST=http://localhost:4200 >> .env
|
||||
# - run: echo "SERVER_URL=$server_url" >> .env
|
||||
# - run: echo "SERVER_TYPE=$server_type" >> .env
|
||||
# - run: echo "CLIENT=$client_sasjs" >> .env
|
||||
# - run: echo "ACCESS_TOKEN=$access_token_sasjs" >> .env
|
||||
# - run: echo "REFRESH_TOKEN=$refresh_token_sasjs" >> .env
|
||||
# - run: cat .env
|
||||
- run: pm2 start api-linux --wait-ready
|
||||
|
||||
- name: Deploy mocked services
|
||||
run: |
|
||||
cd ./sas/mocks/sasjs
|
||||
npm install --legacy-peer-deps -g @sasjs/cli
|
||||
npm install --legacy-peer-deps -g replace-in-files-cli
|
||||
sasjs cbd -t server-ci
|
||||
# sasjs request services/admin/makedata -t server-ci -d ./deploy/makeData4GL.json -c ./deploy/requestConfig.json -o ./output.json
|
||||
|
||||
- name: Prepare frontend
|
||||
run: |
|
||||
cd ./client
|
||||
# mv ./cypress.env.example.json ./cypress.env.json
|
||||
# replace-in-files --regex='"username".*' --replacement='"username":"'$cypress_username_sasjs'",' ./cypress.env.json
|
||||
# replace-in-files --regex='"password".*' --replacement='"password":"'$cypress_pwd_sasjs'" ' ./cypress.env.json
|
||||
cat ./cypress.env.json
|
||||
npm run postinstall
|
||||
# Prepare index.html to SASJS local
|
||||
replace-in-files --regex='serverUrl=".*?"' --replacement='serverUrl="http://localhost:5000"' ./src/index.html
|
||||
replace-in-files --regex='appLoc=".*?"' --replacement='appLoc="/Public/app/devtest"' ./src/index.html
|
||||
replace-in-files --regex='serverType=".*?"' --replacement='serverType="SASJS"' ./src/index.html
|
||||
# Prepare and deploy SASJS version
|
||||
# replace-in-files --regex='serverurl=".*?"' --replacement='serverUrl="http://localhost:5000"' ./dist/index.html
|
||||
# replace-in-files --regex='apploc=".*?"' --replacement='appLoc="/30.SASApps/app/devtest"' ./dist/index.html
|
||||
# replace-in-files --regex='servertype=".*?"' --replacement='serverType="SASJS"' ./dist/index.html
|
||||
# scp -o stricthostkeychecking=no -r ./dist/* dcgitlab@sas.4gl.io:/var/www/html/dcviya/development/newadapter
|
||||
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
|
||||
# replace-in-files --regex='"appLocation".*' --replacement='appLocation:"/dcviya/development/newadapter",' ./cypress.config.ts
|
||||
cat ./cypress.config.ts
|
||||
# Start frontend and run cypress
|
||||
npm start & npx wait-on http://localhost:4200 && ./run-cypress-tests.sh
|
||||
|
||||
Build-and-test-development-latest-adapter:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Write .npmrc file
|
||||
run: echo "$NPMRC" > client/.npmrc
|
||||
shell: bash
|
||||
env:
|
||||
NPMRC: ${{ secrets.NPMRC}}
|
||||
|
||||
- run: apt-get update
|
||||
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||
- run: apt install -y ./google-chrome*.deb;
|
||||
- run: export CHROME_BIN=/usr/bin/google-chrome
|
||||
- run: apt-get update -y
|
||||
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
|
||||
- run: apt -y install jq
|
||||
# - run: 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
|
||||
# - run: eval $(ssh-agent -s)
|
||||
# - run: echo "$ssh_key" | tr -d '\r' | ssh-add -
|
||||
- name: Write cypress credentials
|
||||
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
|
||||
shell: bash
|
||||
env:
|
||||
CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }}
|
||||
|
||||
# - run: mkdir -p ~/.ssh
|
||||
# - run: chmod 700 ~/.ssh
|
||||
- run: npm ci --legacy-peer-deps
|
||||
# Install pm2 and prepare SASJS server
|
||||
- run: npm i -g pm2
|
||||
- run: curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
||||
- run: unzip linux.zip
|
||||
- run: touch .env
|
||||
- run: echo RUN_TIMES=js >> .env
|
||||
- run: echo NODE_PATH=node >> .env
|
||||
- run: echo CORS=enable >> .env
|
||||
- run: echo WHITELIST=http://localhost:4200 >> .env
|
||||
# - run: echo "SERVER_URL=$server_url" >> .env
|
||||
# - run: echo "SERVER_TYPE=$server_type" >> .env
|
||||
# - run: echo "CLIENT=$client_sasjs" >> .env
|
||||
# - run: echo "ACCESS_TOKEN=$access_token_sasjs" >> .env
|
||||
# - run: echo "REFRESH_TOKEN=$refresh_token_sasjs" >> .env
|
||||
# - run: cat .env
|
||||
- run: pm2 start api-linux --wait-ready
|
||||
|
||||
- name: Deploy mocked services
|
||||
run: |
|
||||
cd ./sas/mocks/sasjs
|
||||
npm install --legacy-peer-deps -g @sasjs/cli
|
||||
npm install --legacy-peer-deps -g replace-in-files-cli
|
||||
sasjs cbd -t server-ci
|
||||
# sasjs request services/admin/makedata -t server-ci -d ./deploy/makeData4GL.json -c ./deploy/requestConfig.json -o ./output.json
|
||||
|
||||
- name: Prepare frontend
|
||||
run: |
|
||||
cd ./client
|
||||
# mv ./cypress.env.example.json ./cypress.env.json
|
||||
# replace-in-files --regex='"username".*' --replacement='"username":"'$cypress_username_sasjs'",' ./cypress.env.json
|
||||
# replace-in-files --regex='"password".*' --replacement='"password":"'$cypress_pwd_sasjs'" ' ./cypress.env.json
|
||||
cat ./cypress.env.json
|
||||
npm run postinstall
|
||||
npm install --legacy-peer-deps @sasjs/adapter@latest
|
||||
# Prepare index.html to SASJS local
|
||||
replace-in-files --regex='serverUrl=".*?"' --replacement='serverUrl="http://localhost:5000"' ./src/index.html
|
||||
replace-in-files --regex='appLoc=".*?"' --replacement='appLoc="/Public/app/devtest"' ./src/index.html
|
||||
replace-in-files --regex='serverType=".*?"' --replacement='serverType="SASJS"' ./src/index.html
|
||||
# Prepare and deploy SASJS version
|
||||
# replace-in-files --regex='serverurl=".*?"' --replacement='serverUrl="http://localhost:5000"' ./dist/index.html
|
||||
# replace-in-files --regex='apploc=".*?"' --replacement='appLoc="/30.SASApps/app/devtest"' ./dist/index.html
|
||||
# replace-in-files --regex='servertype=".*?"' --replacement='serverType="SASJS"' ./dist/index.html
|
||||
# scp -o stricthostkeychecking=no -r ./dist/* dcgitlab@sas.4gl.io:/var/www/html/dcviya/development/newadapter
|
||||
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
|
||||
# replace-in-files --regex='"appLocation".*' --replacement='appLocation:"/dcviya/development/newadapter",' ./cypress.config.ts
|
||||
cat ./cypress.config.ts
|
||||
# Start frontend and run cypress
|
||||
npm start & npx wait-on http://localhost:4200 && ./run-cypress-tests.sh
|
||||
@@ -0,0 +1,91 @@
|
||||
name: Lighthouse Checks
|
||||
run-name: Running Lighthouse Performance and Accessibility Checks on Pull Request
|
||||
on: [pull_request]
|
||||
|
||||
env:
|
||||
NODE_VERSION: '24.15.0'
|
||||
|
||||
jobs:
|
||||
lighthouse:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install Google Chrome
|
||||
run: |
|
||||
apt-get update
|
||||
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||
apt install -y ./google-chrome*.deb
|
||||
|
||||
- name: Install global packages
|
||||
run: npm i -g pm2 @sasjs/cli wait-on
|
||||
|
||||
- name: Setup and start SASjs server
|
||||
run: |
|
||||
touch .env
|
||||
echo RUN_TIMES=js >> .env
|
||||
echo NODE_PATH=node >> .env
|
||||
echo CORS=enable >> .env
|
||||
echo WHITELIST=http://localhost:4200 >> .env
|
||||
cat .env
|
||||
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
||||
unzip linux.zip
|
||||
pm2 start api-linux --wait-ready
|
||||
|
||||
- name: Write .npmrc file
|
||||
run: echo "$NPMRC" > client/.npmrc
|
||||
shell: bash
|
||||
env:
|
||||
NPMRC: ${{ secrets.NPMRC}}
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: |
|
||||
cd client
|
||||
# Decrypt and Install sheet
|
||||
echo "${{ secrets.SHEET_PWD }}" | \
|
||||
gpg --batch --yes --passphrase-fd 0 \
|
||||
--output ./libraries/sheet-crypto.tgz \
|
||||
--decrypt ./libraries/sheet-crypto.tgz.gpg
|
||||
npm ci
|
||||
npm install -g replace-in-files-cli
|
||||
|
||||
- name: Update appLoc in index.html
|
||||
run: |
|
||||
cd client
|
||||
replace-in-files --regex='appLoc=".*?"' --replacement='appLoc="/proj/sasjs/genesis-mocks"' ./src/index.html
|
||||
|
||||
- name: Build Frontend
|
||||
run: |
|
||||
cd client
|
||||
npm run build
|
||||
|
||||
- name: Deploy JS mocked services and frontend to the local SASjs Server instance
|
||||
run: |
|
||||
cd sas/mocks
|
||||
npm ci
|
||||
sasjs cbd -t server-ci
|
||||
|
||||
- name: Start frontend server
|
||||
run: |
|
||||
cd client
|
||||
npx ng serve --host 0.0.0.0 --port 4200 &
|
||||
wait-on http://localhost:4200
|
||||
|
||||
- name: Run Lighthouse CI
|
||||
run: |
|
||||
cd client
|
||||
npx lhci autorun
|
||||
|
||||
- name: Lighthouse Result Artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Lighthouse results
|
||||
path: client/lighthouse-reports
|
||||
include-hidden-files: true
|
||||
+313
-13
@@ -1,18 +1,182 @@
|
||||
name: Release
|
||||
run-name: Releasing DC
|
||||
run-name: Testing and Releasing DC
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
NODE_VERSION: '24.5.0'
|
||||
|
||||
jobs:
|
||||
Build-production-and-ng-test:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CHROME_BIN: /usr/bin/google-chrome
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Write .npmrc file
|
||||
run: |
|
||||
touch client/.npmrc
|
||||
echo '${{ secrets.NPMRC}}' > client/.npmrc
|
||||
|
||||
- name: Install Chrome for Angular tests
|
||||
run: |
|
||||
apt-get update
|
||||
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||
apt install -y ./google-chrome*.deb
|
||||
|
||||
- name: Write cypress credentials
|
||||
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
|
||||
shell: bash
|
||||
env:
|
||||
CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd client
|
||||
# Decrypt and Install sheet
|
||||
echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
|
||||
npm ci
|
||||
|
||||
- name: Check audit
|
||||
# Audit should fail and stop the CI if critical vulnerability found
|
||||
run: |
|
||||
npm audit --audit-level=critical --omit=dev
|
||||
cd ./sas
|
||||
npm audit --audit-level=critical --omit=dev
|
||||
cd ../client
|
||||
npm audit --audit-level=critical --omit=dev
|
||||
|
||||
- name: Angular Tests
|
||||
run: |
|
||||
cd client
|
||||
npm run test:headless
|
||||
|
||||
- name: Angular Production Build
|
||||
run: |
|
||||
cd client
|
||||
npm run postinstall
|
||||
npm run build
|
||||
|
||||
Build-and-test-development:
|
||||
runs-on: ubuntu-latest
|
||||
needs: Build-production-and-ng-test
|
||||
env:
|
||||
CHROME_BIN: /usr/bin/google-chrome
|
||||
# Pin OS locale
|
||||
LANG: en_GB.UTF-8
|
||||
LC_ALL: en_GB.UTF-8
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Write .npmrc file
|
||||
run: |
|
||||
touch client/.npmrc
|
||||
echo '${{ secrets.NPMRC}}' > client/.npmrc
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||
apt install -y ./google-chrome*.deb
|
||||
apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb jq zip locales
|
||||
# Generate the en_GB.UTF-8 locale referenced by LANG/LC_ALL
|
||||
locale-gen en_GB.UTF-8
|
||||
update-locale LANG=en_GB.UTF-8 LC_ALL=en_GB.UTF-8
|
||||
|
||||
- name: Write cypress credentials
|
||||
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
|
||||
shell: bash
|
||||
env:
|
||||
CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd client
|
||||
# Decrypt and Install sheet
|
||||
echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
|
||||
npm ci
|
||||
|
||||
- name: Setup and start SASjs server
|
||||
run: |
|
||||
npm i -g pm2
|
||||
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
||||
unzip linux.zip
|
||||
touch .env
|
||||
echo RUN_TIMES=js >> .env
|
||||
echo NODE_PATH=node >> .env
|
||||
echo CORS=enable >> .env
|
||||
echo WHITELIST=http://localhost:4200 >> .env
|
||||
cat .env
|
||||
pm2 start api-linux --wait-ready
|
||||
|
||||
- name: Deploy mocked services
|
||||
run: |
|
||||
cd ./sas/mocks/sasjs
|
||||
npm install -g @sasjs/cli
|
||||
npm install -g replace-in-files-cli
|
||||
sasjs cbd -t server-ci
|
||||
# sasjs request services/admin/makedata -t server-ci -d ./deploy/makeData4GL.json -c ./deploy/requestConfig.json -o ./output.json
|
||||
|
||||
- name: Prepare and run frontend and cypress
|
||||
run: |
|
||||
cd ./client
|
||||
mv ./cypress.env.example.json ./cypress.env.json
|
||||
replace-in-files --regex='"username".*' --replacement='"username":"'${{ secrets.CYPRESS_USERNAME_SASJS }}'",' ./cypress.env.json
|
||||
replace-in-files --regex='"password".*' --replacement='"password":"'${{ secrets.CYPRESS_PWD_SASJS }}'" ' ./cypress.env.json
|
||||
cat ./cypress.env.json
|
||||
npm run postinstall
|
||||
# Prepare index.html to SASJS local
|
||||
replace-in-files --regex='serverUrl=".*?"' --replacement='serverUrl="http://localhost:5000"' ./src/index.html
|
||||
replace-in-files --regex='appLoc=".*?"' --replacement='appLoc="/Public/app/devtest"' ./src/index.html
|
||||
replace-in-files --regex='serverType=".*?"' --replacement='serverType="SASJS"' ./src/index.html
|
||||
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
|
||||
cat ./cypress.config.ts
|
||||
# Start frontend and run cypress
|
||||
npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && npx cypress run --browser chrome --spec "cypress/e2e/csv-limited.cy.ts,cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts,cypress/e2e/viewer-labels.cy.ts"
|
||||
|
||||
- name: Zip Cypress videos
|
||||
if: always()
|
||||
run: |
|
||||
mkdir -p ./client/cypress/videos
|
||||
zip -r cypress-videos ./client/cypress/videos
|
||||
|
||||
- name: Add cypress videos artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: cypress-videos.zip
|
||||
path: cypress-videos.zip
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [Build-production-and-ng-test, Build-and-test-development]
|
||||
# The semantic-release step below uses the per-job token injected by Gitea
|
||||
# Actions (secrets.GITEA_TOKEN). That token is scoped to THIS repository
|
||||
# and its permissions are derived from this block.
|
||||
# contents: write -> push CHANGELOG.md / package.json commit + tag, and
|
||||
# create the Release via the Gitea API.
|
||||
# The TSDOC_TOKEN and CODE_DATACONTROLLER_IO secrets continue to be used
|
||||
# because they target *different* servers (webdoc.datacontroller.io and
|
||||
# code.datacontroller.io), not the Gitea API.
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Write .npmrc file
|
||||
run: |
|
||||
@@ -22,29 +186,165 @@ jobs:
|
||||
env:
|
||||
NPMRC: ${{ secrets.NPMRC}}
|
||||
|
||||
- name: Install ZIP
|
||||
- name: Install system packages
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install zip
|
||||
apt-get install -y zip jq doxygen
|
||||
npm i -g @sasjs/cli
|
||||
|
||||
- name: release-build
|
||||
- name: Frontend Preliminary Build
|
||||
description: We want to prevent creating empty release if frontend fails
|
||||
run: |
|
||||
cd client
|
||||
# Decrypt and Install sheet
|
||||
echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
|
||||
npm ci
|
||||
npm i webpack
|
||||
npm run build
|
||||
zip -r dist.zip ./dist
|
||||
|
||||
- name: Install Semantic Release and plugins
|
||||
- name: Create Empty Release (assets are posted later)
|
||||
env:
|
||||
# Use the ephemeral per-job token injected by Gitea Actions instead
|
||||
# of a manually-managed PAT. This token always reports push=true on
|
||||
# its own repo via the Gitea API, which is what the
|
||||
# @saithodev/semantic-release-gitea verifyConditions step checks.
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_URL: ${{ gitea.server_url }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm i
|
||||
npm i -g semantic-release
|
||||
|
||||
- name: Release
|
||||
# We do a semantic-release DRY RUN to make the job fail if there are
|
||||
# no changes to release. The previous implementation piped --dry-run
|
||||
# into `grep`, which swallowed semantic-release's non-zero exit codes
|
||||
# (e.g. auth/verifyConditions failures) — the real error then only
|
||||
# surfaced from the second invocation. Capture output and inspect it
|
||||
# so authentication failures fail the step cleanly with context.
|
||||
echo "::group::semantic-release --dry-run"
|
||||
DRY_EXIT=0
|
||||
DRY_OUTPUT=$(semantic-release --dry-run 2>&1) || DRY_EXIT=$?
|
||||
echo "$DRY_OUTPUT"
|
||||
echo "::endgroup::"
|
||||
|
||||
if [ "$DRY_EXIT" -ne 0 ]; then
|
||||
echo "::error::semantic-release dry-run failed (exit $DRY_EXIT). See log above; common cause is the Gitea token lacking push permission on this repo (verifyConditions of @saithodev/semantic-release-gitea)."
|
||||
exit "$DRY_EXIT"
|
||||
fi
|
||||
|
||||
if echo "$DRY_OUTPUT" | grep -q "There are no relevant changes, so no new version is released."; then
|
||||
echo "::error::No releasable changes since last tag - aborting release job."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
semantic-release
|
||||
|
||||
- name: Frontend Build
|
||||
description: Must be created AFTER the release as the version (git tag) is used in the interface
|
||||
run: |
|
||||
GITEA_TOKEN=${{ secrets.RELEASE_TOKEN }} GITEA_URL=https://git.datacontroller.io semantic-release
|
||||
cd client
|
||||
npm run build
|
||||
|
||||
- name: Build SAS9 EBI Release
|
||||
description: Compile SAS 9 services, remove tests & create deployment program
|
||||
run: |
|
||||
cd sas
|
||||
npm i
|
||||
sasjs c -t sas9
|
||||
rm -rf sasjsbuild/tests
|
||||
sasjs b -t sas9
|
||||
cp sasjsbuild/mysas9deploy.sas ./demostream_sas9.sas
|
||||
#
|
||||
# remove streamed component and rebuild SAS 9 services
|
||||
#
|
||||
rm -rf sasjsbuild/services/web9
|
||||
rm sasjsbuild/services/clickme.sas
|
||||
sasjs b -t sas9
|
||||
cp sasjsbuild/mysas9deploy.sas ./sas9.sas
|
||||
|
||||
- name: Build SASjs Server Release
|
||||
description: Compile Base (SASjs) services, remove tests & create deployment JSON
|
||||
run: |
|
||||
cd sas
|
||||
cp sasjs/utils/favicon.ico ../client/dist/favicon.ico
|
||||
sasjs c -t server
|
||||
rm -rf sasjsbuild/tests
|
||||
server_apploc="/Public/app/dc"
|
||||
sed -i "s|apploc=\"[^\"]*\"|apploc=\"${server_apploc}\"|g" sasjsbuild/services/web/index.html
|
||||
sasjs b -t server
|
||||
cp sasjsbuild/server.json.zip ./sasjs_server.json.zip
|
||||
|
||||
- name: Build Viya Release
|
||||
description: compile Viya Streaming Deploy (without tests)
|
||||
run: |
|
||||
cd sas
|
||||
sasjs c -t viya
|
||||
rm -rf sasjsbuild/tests
|
||||
sed -i -e 's/servertype="SASJS"/servertype="SASVIYA"/g' sasjsbuild/services/DC.html
|
||||
sasjs b -t viya
|
||||
cp sasjsbuild/viya.sas ./viya.sas
|
||||
# compile Viya Full deploy (without web)
|
||||
rm -rf sasjsbuild/services/web
|
||||
rm sasjsbuild/services/DC.html
|
||||
sasjs b -t viya
|
||||
cp sasjsbuild/viya.sas ./viya_noweb.sas
|
||||
cp sasjsbuild/viya.json ./viya_noweb.json
|
||||
|
||||
- name: Zip Frontend (including viya.json for full viya deploy)
|
||||
run: |
|
||||
cd sas
|
||||
cp sasjsbuild/viya.json ../client/dist/viya.json
|
||||
cd ..
|
||||
zip -r frontend.zip ./client/dist
|
||||
|
||||
- name: Release Typedoc
|
||||
run: |
|
||||
cd client
|
||||
npm run typedoc
|
||||
# deploy docs
|
||||
npm -g install cloudron-surfer
|
||||
npm run compodoc:build
|
||||
surfer put --token ${{ secrets.TSDOC_TOKEN }} --server webdoc.datacontroller.io documentation/* /
|
||||
|
||||
- name: Release code.datacontroller.io
|
||||
run: |
|
||||
cd sas
|
||||
sasjs doc
|
||||
surfer put --token ${{ secrets.CODE_DATACONTROLLER_IO }} --server code.datacontroller.io sasjsbuild/sasdocs/* /
|
||||
|
||||
- name: Upload assets to release
|
||||
env:
|
||||
# Use the same ephemeral per-job Gitea Actions token that the
|
||||
# "Create Empty Release" step uses for semantic-release. It is scoped
|
||||
# to this repo and is granted write access via the workflow's
|
||||
# `permissions:` block at the top of the file (see contents: write).
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Send the token via Authorization header rather than ?access_token=
|
||||
# (the query-string form is deprecated and leaks into access logs).
|
||||
AUTH_HEADER="Authorization: token ${GITEA_TOKEN}"
|
||||
BASE="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
||||
|
||||
RELEASE_JSON=$(curl -k --fail-with-body -sS -H "$AUTH_HEADER" "$BASE/releases/latest")
|
||||
RELEASE_ID=$(echo "$RELEASE_JSON" | jq -r '.id')
|
||||
RELEASE_BODY=$(echo "$RELEASE_JSON" | jq -r '.body')
|
||||
|
||||
# Update body (also confirms the token has contents:write on this repo)
|
||||
curl -k --fail-with-body -sS -X PATCH \
|
||||
-H "$AUTH_HEADER" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data "$(jq -n --arg body "$RELEASE_BODY"$'\n\nFor installation instructions, please visit https://docs.datacontroller.io/' \
|
||||
'{draft:false, body:$body}')" \
|
||||
"$BASE/releases/$RELEASE_ID"
|
||||
|
||||
# Upload assets
|
||||
URL="$BASE/releases/$RELEASE_ID/assets"
|
||||
for f in frontend.zip \
|
||||
sas/demostream_sas9.sas \
|
||||
sas/viya.sas \
|
||||
sas/sasjs_server.json.zip \
|
||||
sas/sas9.sas \
|
||||
sas/viya_noweb.sas \
|
||||
sas/viya_noweb.json; do
|
||||
echo "Uploading $f ..."
|
||||
curl -k --fail-with-body -sS -H "$AUTH_HEADER" "$URL" -F "attachment=@$f"
|
||||
done
|
||||
|
||||
@@ -10,6 +10,11 @@ client/src/environments/version.ts
|
||||
client/cypress/screenshots
|
||||
client/cypress/results
|
||||
client/cypress/videos
|
||||
client/documentation
|
||||
client/**/sheet-crypto.tgz
|
||||
client/.nx
|
||||
client/libraries/sheet-crypto.tgz
|
||||
client/lighthouse-reports
|
||||
cypress.env.json
|
||||
sasjsbuild
|
||||
sasjsresults
|
||||
@@ -17,3 +22,4 @@ sasjsresults
|
||||
.sasjsrc
|
||||
client/.npmrc
|
||||
*~
|
||||
.lighthouseci
|
||||
@@ -1 +1,4 @@
|
||||
legacy-peer-deps=false
|
||||
legacy-peer-deps=true
|
||||
ignore-scripts=true
|
||||
save-exact=true
|
||||
fund=false
|
||||
+37
-13
@@ -1,22 +1,46 @@
|
||||
{
|
||||
"branches": ["main"],
|
||||
"branches": [
|
||||
"main"
|
||||
],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/changelog",
|
||||
"@semantic-release/npm",
|
||||
[
|
||||
"@semantic-release/git",
|
||||
{
|
||||
"assets": [
|
||||
"CHANGELOG.md"
|
||||
]
|
||||
}
|
||||
"@semantic-release/git",
|
||||
{
|
||||
"assets": [
|
||||
"CHANGELOG.md",
|
||||
"package.json"
|
||||
]
|
||||
}
|
||||
],
|
||||
["@saithodev/semantic-release-gitea", {
|
||||
"giteaUrl": "https://git.datacontroller.io",
|
||||
"assets": [
|
||||
{"path": "client/dist.zip"}
|
||||
]
|
||||
}]
|
||||
[
|
||||
"@saithodev/semantic-release-gitea",
|
||||
{
|
||||
"giteaUrl": "https://git.datacontroller.io",
|
||||
"assets": [
|
||||
{
|
||||
"path": "sas/demostream_sas9.sas"
|
||||
},
|
||||
{
|
||||
"path": "sas/demostream_viya.sas"
|
||||
},
|
||||
{
|
||||
"path": "sas/frontend.zip"
|
||||
},
|
||||
{
|
||||
"path": "sas/sasjs_server.json.zip"
|
||||
},
|
||||
{
|
||||
"path": "sas/sas9.sas"
|
||||
},
|
||||
{
|
||||
"path": "sas/viya.sas"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
Vendored
+19
-17
@@ -1,18 +1,20 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"SYSERRORTEXT",
|
||||
"SYSWARNINGTEXT"
|
||||
],
|
||||
"editor.rulers": [
|
||||
80
|
||||
],
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"[markdown]": {
|
||||
"files.trimTrailingWhitespace": false
|
||||
},
|
||||
"workbench.colorCustomizations": {
|
||||
"titleBar.activeForeground": "#ebe8e8",
|
||||
"titleBar.activeBackground": "#95ff0053",
|
||||
},
|
||||
"terminal.integrated.wordSeparators": " ()[]{}',\"`─‘’"
|
||||
}
|
||||
"cSpell.words": [
|
||||
"Handsontable",
|
||||
"Licence",
|
||||
"SYSERRORTEXT",
|
||||
"SYSWARNINGTEXT",
|
||||
"xlmaprules",
|
||||
"xlmaps"
|
||||
],
|
||||
"editor.rulers": [80],
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"[markdown]": {
|
||||
"files.trimTrailingWhitespace": false
|
||||
},
|
||||
"workbench.colorCustomizations": {
|
||||
"titleBar.activeForeground": "#ebe8e8",
|
||||
"titleBar.activeBackground": "#95ff0053"
|
||||
},
|
||||
"terminal.integrated.wordSeparators": " ()[]{}',\"`─‘’"
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
pipeline:
|
||||
build:
|
||||
image: debian
|
||||
commands:
|
||||
- echo "This is the build step"
|
||||
a-test-step:
|
||||
image: debian
|
||||
commands:
|
||||
- echo "Testing.."
|
||||
+926
@@ -1,3 +1,929 @@
|
||||
## [7.10.1](https://git.datacontroller.io/dc/dc/compare/v7.10.0...v7.10.1) (2026-07-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **editor:** order validation rules by COLHEADERS, not cols[].VARNUM ([4c2aca3](https://git.datacontroller.io/dc/dc/commit/4c2aca34951d6d5c5cf2df69d5bf4256a9290d53))
|
||||
|
||||
# [7.10.0](https://git.datacontroller.io/dc/dc/compare/v7.9.1...v7.10.0) (2026-07-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adapt column validation to vertical-array COLTYPE ([#253](https://git.datacontroller.io/dc/dc/issues/253)) ([3016750](https://git.datacontroller.io/dc/dc/commit/301675052fb5cbcb11f54c370ad7eda1d60461fd))
|
||||
* backend updates to bring back labels under [#240](https://git.datacontroller.io/dc/dc/issues/240) ([fb94840](https://git.datacontroller.io/dc/dc/commit/fb94840016f067fa3523a3539dd0616d8970eed0))
|
||||
* **editor:** normalise unpadded time strings on spreadsheet import ([7b4b4eb](https://git.datacontroller.io/dc/dc/commit/7b4b4ebeeb0e34ec5fcd5c485741d7bbddb43b7e))
|
||||
* **hot:** row rendering on resize, resolve blank whitespace ([c7ba025](https://git.datacontroller.io/dc/dc/commit/c7ba025d39aa9681ad51f239503e884fb26613e2))
|
||||
* including datadictionary descs in labels, [#240](https://git.datacontroller.io/dc/dc/issues/240) ([e7453a9](https://git.datacontroller.io/dc/dc/commit/e7453a9305263faf4d4f288d50977121353040c7))
|
||||
* pass variable formats to front-end in a vertical array instead of horizontal list ([691d6f2](https://git.datacontroller.io/dc/dc/commit/691d6f277ea87051f539681b34da2f39cade8fc3))
|
||||
* remove coltype from sasparams table, add it to cols table. ([8101400](https://git.datacontroller.io/dc/dc/commit/81014001ac4da6b8d262c72c65e0c657f186ec84))
|
||||
* trim leading/trailing whitespace from Excel header cells on upload ([2728dac](https://git.datacontroller.io/dc/dc/commit/2728dac873ab8c9fc44413f873e4730515ca0e21))
|
||||
* **va:** date filters refactor, support more dates ([df5c975](https://git.datacontroller.io/dc/dc/commit/df5c9758697d7481e67d20ed7ff81e9ce4e92ec0))
|
||||
* **va:** enable readOnly and hold filtering while editing ([82a254d](https://git.datacontroller.io/dc/dc/commit/82a254d22cb7aaf75aae7b332f0b07fc8bda3e44))
|
||||
* **va:** enable Upload button in va mode ([1beb3d4](https://git.datacontroller.io/dc/dc/commit/1beb3d490d0e7b8d5fa7985f121495ed62740cfe))
|
||||
* **va:** guard deferred contextMenu toggle against destroyed instance ([b5e9b25](https://git.datacontroller.io/dc/dc/commit/b5e9b2531924bec1a23c67d3ad0ca3f512a75655))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* adding DC_MAXOBS_WEBVIEW config item, closes [#258](https://git.datacontroller.io/dc/dc/issues/258) ([ba7b610](https://git.datacontroller.io/dc/dc/commit/ba7b61082d39767f9539a84de6742a1704573e81))
|
||||
* viewer/editor column label display toggle (?labels=true) ([25c12f2](https://git.datacontroller.io/dc/dc/commit/25c12f2b18adda09e6edc93eba263bd5ea0f7409))
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* enable upload button in va mode ([0d7fd34](https://git.datacontroller.io/dc/dc/commit/0d7fd342970744489a2e6c6fb08f239fe853cb81))
|
||||
|
||||
## [7.9.1](https://git.datacontroller.io/dc/dc/compare/v7.9.0...v7.9.1) (2026-06-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* workflow file upload using action PAT ([a161670](https://git.datacontroller.io/dc/dc/commit/a161670b8656614f61d42ee673064ba8f010b71c))
|
||||
|
||||
# [7.9.0](https://git.datacontroller.io/dc/dc/compare/v7.8.2...v7.9.0) (2026-06-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow large retained key values. Closes [#248](https://git.datacontroller.io/dc/dc/issues/248). ([6a30de3](https://git.datacontroller.io/dc/dc/commit/6a30de37cad21b7b94bf550ce7afbc804e6f0f90))
|
||||
* build issues ([#245](https://git.datacontroller.io/dc/dc/issues/245)) ([77b4bb3](https://git.datacontroller.io/dc/dc/commit/77b4bb310e62adb450805488fc69a132435d0eb7))
|
||||
* **ci:** bump Lighthouse to 13.4.0 ([47ba03e](https://git.datacontroller.io/dc/dc/commit/47ba03ec637113d8867372d64484927de9b645ff))
|
||||
* **dc-validator:** display date/time/datetime cells as raw ISO, locale-independent ([9ca5719](https://git.datacontroller.io/dc/dc/commit/9ca57193b4aafa1e7bf216301cdf197cea6ec4c9))
|
||||
* **editor:** await dynamic validation on paste; defer spinner ([ea03bde](https://git.datacontroller.io/dc/dc/commit/ea03bdecc5aaf917052add2c7e6fdc192b9974ce))
|
||||
* **editor:** cancelEdit will reset cell's valid state ([51071b4](https://git.datacontroller.io/dc/dc/commit/51071b463bb311c6db48c301daa8cc7dfd92144a))
|
||||
* **editor:** preserve commas in dropdown source values ([6af49cf](https://git.datacontroller.io/dc/dc/commit/6af49cf1fd8aa27cbf99f9514cca537339cdbf7e))
|
||||
* **editor:** retry initSetup when the Handsontable instance isn't ready ([53b7ee0](https://git.datacontroller.io/dc/dc/commit/53b7ee0cb23329fa4f9d6211ffe6b102053fade9))
|
||||
* **handsontable:** add exceljs ([38cb1e2](https://git.datacontroller.io/dc/dc/commit/38cb1e207b58822d71ef803e770a93bc7265bc1c))
|
||||
* **handsontable:** horizontal scrollbar in dropdown ([ac0bd10](https://git.datacontroller.io/dc/dc/commit/ac0bd102127ced6b6a908a7244f35002f22cb669))
|
||||
* **handsontable:** restore dark mode for v17 ([9d97bf7](https://git.datacontroller.io/dc/dc/commit/9d97bf7ea13199ab9dab10c5fc47e07b4751fb9a))
|
||||
* **handsontable:** update licenseChecker exclude buffers - exceljs dep ([12c7d30](https://git.datacontroller.io/dc/dc/commit/12c7d30894b5c3709b683189da6e52255ad0a3e7))
|
||||
* incrementing rk val ([7444209](https://git.datacontroller.io/dc/dc/commit/74442096bac1481a4992e5c97dac47c8d277cc3f))
|
||||
* **licensing:** add protocol info ([3668a74](https://git.datacontroller.io/dc/dc/commit/3668a7426fec078ad7939b5bc815d7ad920c0d62)), closes [#178](https://git.datacontroller.io/dc/dc/issues/178)
|
||||
* migrate handsontables to v17 ([1b73e35](https://git.datacontroller.io/dc/dc/commit/1b73e355b78a0039aa48f7a137095eb5470c08e2))
|
||||
* **mocks:** return dynamic cell values as OBJ to match new contract ([a15fdc4](https://git.datacontroller.io/dc/dc/commit/a15fdc401e6cdcf934a5d599f8b7c4d197e9d2d4))
|
||||
* removing display_value in SAS code ([9b18a45](https://git.datacontroller.io/dc/dc/commit/9b18a45d354ad617e590d62758ef761b01b3095a))
|
||||
* rule_type ([adc6424](https://git.datacontroller.io/dc/dc/commit/adc6424211a83fb03b267bf8fa6b6d9fa72f185b))
|
||||
* sending dynamic_values and dynamic_extended_values back as OBJ instead of ARR ([e859d33](https://git.datacontroller.io/dc/dc/commit/e859d3354e82f0faad53a0b8aa14ed66974cbffa))
|
||||
* upcasing RAW_VALUE, bumping core, fixing tests ([982d507](https://git.datacontroller.io/dc/dc/commit/982d507ae3ad2ccc08e65678956199cd54b696f2))
|
||||
* validate pasted values ([b661580](https://git.datacontroller.io/dc/dc/commit/b661580c604c2923b930911a1881aec7b06ecf23))
|
||||
* viewLibs now fires only once, libPromise shared between the calls ([dc4e07a](https://git.datacontroller.io/dc/dc/commit/dc4e07a692a5e839986abb69b17e51024c4cdc03))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **edit-record:** use native date/time/datetime pickers ([2d29346](https://git.datacontroller.io/dc/dc/commit/2d29346cbf09ad057743e6f5ebea12f3257f246e))
|
||||
* **editor:** add READONLY, HIDDEN, ROUND and NUMBER_FORMAT validation rules ([4ea604f](https://git.datacontroller.io/dc/dc/commit/4ea604f9fbdd429108fe063b306ea376e756c107))
|
||||
* **editor:** migrate date/time/datetime to HOT 17 Intl cell types ([f70ea2f](https://git.datacontroller.io/dc/dc/commit/f70ea2fe712ff9a42bf47387f9f0aaa6d1a1f511))
|
||||
* **editor:** paste-validation overlay with cancel and confirm ([609731f](https://git.datacontroller.io/dc/dc/commit/609731ff99185a162614df13bde7afb1a2c3bbca))
|
||||
* **editor:** SAS VA data-driven content embed mode (?embed=va) ([ffa3ff9](https://git.datacontroller.io/dc/dc/commit/ffa3ff9c103d6d80677f4f67e7d53418f201f491))
|
||||
* **editor:** VA embed filter modes with pending UX ([ebe6972](https://git.datacontroller.io/dc/dc/commit/ebe6972dd7d38c5742ed7eb446e0448d98b2c8a7))
|
||||
* **editor:** validate autofilled cells; fix paste validation lag ([11ee49a](https://git.datacontroller.io/dc/dc/commit/11ee49a57a0bc7ccd59e91d5fe37bb5db1bf7d69))
|
||||
* **handsontable:** enable export plugin, add export menu items ([29aaa72](https://git.datacontroller.io/dc/dc/commit/29aaa72c6047bb309d325887ab602a9369a1ad74))
|
||||
* migration script for adding new validation rules under [#241](https://git.datacontroller.io/dc/dc/issues/241) ([9f84a58](https://git.datacontroller.io/dc/dc/commit/9f84a580fb762da8c1c796a35eb4f531df1caf13))
|
||||
* new validation rules in mpe_selectbox. [#241](https://git.datacontroller.io/dc/dc/issues/241) ([2f5d3df](https://git.datacontroller.io/dc/dc/commit/2f5d3dfc387f02353601aaa844aaeabde3e9f3e5))
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* eager-load feature modules instead of lazy-loading ([3429a7a](https://git.datacontroller.io/dc/dc/commit/3429a7a2a6ecfa6115394e1c842061cc68bf2be3))
|
||||
* **editor:** DISPLAY_VALUE change ([eb015d7](https://git.datacontroller.io/dc/dc/commit/eb015d712b01687f36ac92e38ab576a7c114b1ba))
|
||||
|
||||
## [7.8.2](https://git.datacontroller.io/dc/dc/compare/v7.8.1...v7.8.2) (2026-05-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bumping ws package ([2382a55](https://git.datacontroller.io/dc/dc/commit/2382a559a5ac32b0f815776a90207650d5809ba6))
|
||||
* enabling version restore for non admin users ([5d889d8](https://git.datacontroller.io/dc/dc/commit/5d889d824cc2f8e4ea089cbb578453125dc4ba6c))
|
||||
|
||||
## [7.8.1](https://git.datacontroller.io/dc/dc/compare/v7.8.0...v7.8.1) (2026-05-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **sasjs:** enable runAsTask ([f1a26e1](https://git.datacontroller.io/dc/dc/commit/f1a26e132eba7fa2ac64754940b52ea46c6619b3))
|
||||
|
||||
# [7.8.0](https://git.datacontroller.io/dc/dc/compare/v7.7.3...v7.8.0) (2026-05-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* enabling DSN=*ALL* in MPE_SECURITY ([7d94cb2](https://git.datacontroller.io/dc/dc/commit/7d94cb2ae4a3f6c1fa1011ae0fced7083a2f2793))
|
||||
* providing default values for RULE_ACTIVE on MPE_VALIDATIONS ([f031b4e](https://git.datacontroller.io/dc/dc/commit/f031b4eb8925397e60dcc739a721cfbbb6da8dff))
|
||||
* switch away from api usage for CASLIB metadata ([ce921a0](https://git.datacontroller.io/dc/dc/commit/ce921a032a8970b8078a463a41da884e1fa71bc3))
|
||||
* use correct debug param for runAsTask ([bb80476](https://git.datacontroller.io/dc/dc/commit/bb8047676749814d3b86eea666726dbe4bf5f270))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add runAsTask config attribute parser ([1635bc9](https://git.datacontroller.io/dc/dc/commit/1635bc9c451bc221f386241007f594096f114b4f))
|
||||
* enabling *ALL* option by default in MPE_SECURITY (DSN col) ([93d4ab6](https://git.datacontroller.io/dc/dc/commit/93d4ab65acce7b5b35e448146f9893964ad2cca3))
|
||||
|
||||
## [7.7.3](https://git.datacontroller.io/dc/dc/compare/v7.7.2...v7.7.3) (2026-05-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* move cas session assign to settings.sas and abort when lib is unassigned ([65f0b97](https://git.datacontroller.io/dc/dc/commit/65f0b979a401277b3e070d409659ae3fae2ff8c0))
|
||||
|
||||
## [7.7.2](https://git.datacontroller.io/dc/dc/compare/v7.7.1...v7.7.2) (2026-05-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **client:** bundle Metropolis font locally to satisfy CSP ([9546fcd](https://git.datacontroller.io/dc/dc/commit/9546fcd6312f3e81f746ef6e32ef398810ed434a))
|
||||
* **client:** clear angular build cache on font strip to avoid stale dist ([503cb08](https://git.datacontroller.io/dc/dc/commit/503cb08b2fa40397434189f9c20eff3358eb7010))
|
||||
* **client:** postinstall removal of Metropolis [@font-face](https://git.datacontroller.io/font-face) from @clr/ui ([e6397ce](https://git.datacontroller.io/dc/dc/commit/e6397cecc13afe2a9238bdfb2b4b9b81f38d055c))
|
||||
* **client:** serve text-security-disc font locally ([80ce80e](https://git.datacontroller.io/dc/dc/commit/80ce80ece40012e59c7cd0340b4aa9a9aca46443))
|
||||
* **editor:** preserve numeric type for SAS num cols with static SOFTSELECT/HARDSELECT ([05a3289](https://git.datacontroller.io/dc/dc/commit/05a328976ea3d1d6ef7559850369aa580f0d067f))
|
||||
|
||||
## [7.7.1](https://git.datacontroller.io/dc/dc/compare/v7.7.0...v7.7.1) (2026-05-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **client:** bump adapter ([d26f7d2](https://git.datacontroller.io/dc/dc/commit/d26f7d2511008634124c7d6fde115abb43db9c43))
|
||||
* **sas:** bump cli ([d60029d](https://git.datacontroller.io/dc/dc/commit/d60029deae0ec21f3b8570461e2a4ca041d58f72))
|
||||
|
||||
# [7.7.0](https://git.datacontroller.io/dc/dc/compare/v7.6.0...v7.7.0) (2026-05-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bump adapter to 4.16.6 ([1707f38](https://git.datacontroller.io/dc/dc/commit/1707f3802a97de8c659f1a88c92fc917e8a30615))
|
||||
* remove data:image/svg+xml CSP violation, use class instead changing style directly ([d66eb5d](https://git.datacontroller.io/dc/dc/commit/d66eb5dfc2dbb01f1e6c0c7d15fc2ad2a39dd829))
|
||||
* remove WORK, SASUSER and CASUSER as library options. [#224](https://git.datacontroller.io/dc/dc/issues/224) ([ec66631](https://git.datacontroller.io/dc/dc/commit/ec66631a33aabb8ab2f92fe22c15440127085782))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* auto-save CAS tables [#224](https://git.datacontroller.io/dc/dc/issues/224) ([40d04a5](https://git.datacontroller.io/dc/dc/commit/40d04a53c4c00183116bdbd08397e0f2ffb1f578))
|
||||
* autoload CAS tables. [#224](https://git.datacontroller.io/dc/dc/issues/224) ([d5ebb01](https://git.datacontroller.io/dc/dc/commit/d5ebb01ce381f5f4ec06de041f3ab9e632c02e43))
|
||||
|
||||
# [7.6.0](https://git.datacontroller.io/dc/dc/compare/v7.5.0...v7.6.0) (2026-04-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add label and tooltip for libref download, sanitise input ([52d5803](https://git.datacontroller.io/dc/dc/commit/52d58036a40e25847e900f9b04a77dbcc409c12b))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* configurable email alerts. Closes [#217](https://git.datacontroller.io/dc/dc/issues/217) ([2ccf0d1](https://git.datacontroller.io/dc/dc/commit/2ccf0d11000129629a0665421135b7530af9892f))
|
||||
|
||||
# [7.5.0](https://git.datacontroller.io/dc/dc/compare/v7.4.1...v7.5.0) (2026-04-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add workflow audits, update deps ([66e98a9](https://git.datacontroller.io/dc/dc/commit/66e98a96cbd092e762b94a04660f8e17ca003ceb))
|
||||
* allow CSV uploads with licence row limit ([5b260e4](https://git.datacontroller.io/dc/dc/commit/5b260e49153dd85bc0023ad94d8a5f57b8ffa6dc)), closes [#213](https://git.datacontroller.io/dc/dc/issues/213)
|
||||
* bumping cli and pinning versions in .npmrc ([80039f4](https://git.datacontroller.io/dc/dc/commit/80039f4876c8e09dc477678e1eff58329094c9e9))
|
||||
* guard CSV upload with fileUpload licence flag ([ed40df6](https://git.datacontroller.io/dc/dc/commit/ed40df62953c3055770b5cbf50738f4a48b943cd))
|
||||
* parse embed param from window.location.hash for hash router compatibility ([0269c24](https://git.datacontroller.io/dc/dc/commit/0269c2421db245f7f5405678605cb4d4587e2a67))
|
||||
* quote CSV char values. Closes [#215](https://git.datacontroller.io/dc/dc/issues/215) ([d9980e8](https://git.datacontroller.io/dc/dc/commit/d9980e866d1a2fe7a731ff279d73accd35003e67))
|
||||
* resolve outer promise in parseCsvFile for non-WLATIN1 path ([4ee15e1](https://git.datacontroller.io/dc/dc/commit/4ee15e1b6e83f27f279fc345e6998452a8f64d7e))
|
||||
* use XLSX for CSV row truncation to handle new lines in values ([6d590c0](https://git.datacontroller.io/dc/dc/commit/6d590c050dcd593a73464fae5604f774f016b10d))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add embed URL parameter to hide header and back button ([b0dc441](https://git.datacontroller.io/dc/dc/commit/b0dc441d681369e06eee58288dbdbb236f930bdc)), closes [#214](https://git.datacontroller.io/dc/dc/issues/214)
|
||||
* add target libref input to config download ([a89657b](https://git.datacontroller.io/dc/dc/commit/a89657b0b81b9c531f64c0dda2714b4eb16c4bc9)), closes [#212](https://git.datacontroller.io/dc/dc/issues/212)
|
||||
* export config service to allow dclib swapping. Closes [#212](https://git.datacontroller.io/dc/dc/issues/212) ([326c26f](https://git.datacontroller.io/dc/dc/commit/326c26fddfa88a0dc4ca79d3bd0c77c4d807f37c))
|
||||
|
||||
## [7.4.1](https://git.datacontroller.io/dc/dc/compare/v7.4.0...v7.4.1) (2026-03-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* support for SASIOSNF engine (SNOW alias) plus meta assignment ([7694d1b](https://git.datacontroller.io/dc/dc/commit/7694d1b0fb2bd0407c8598147fbae87a00d889a8))
|
||||
|
||||
# [7.4.0](https://git.datacontroller.io/dc/dc/compare/v7.3.0...v7.4.0) (2026-02-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* cli bump for mf_getscheme support ([a84ba41](https://git.datacontroller.io/dc/dc/commit/a84ba41ea9f0c97ae24f0a572b8cf5ec200f2132))
|
||||
* missing upcase on SNOW section, plus local sasjs target ([dc20064](https://git.datacontroller.io/dc/dc/commit/dc200646f7df2fd1910841f392c314532aae7581))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* SAS code changes for snowflake support ([e273e87](https://git.datacontroller.io/dc/dc/commit/e273e870efbf7875db869b760f2c7b1f39d571ae))
|
||||
|
||||
# [7.3.0](https://git.datacontroller.io/dc/dc/compare/v7.2.8...v7.3.0) (2026-02-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bump xlsx, add crypto-shim ([8dc18b1](https://git.datacontroller.io/dc/dc/commit/8dc18b155abfc20fd0b043e0d70bbbc17e6b6811))
|
||||
* correctly applying deletes on viya, also ([46cdeb0](https://git.datacontroller.io/dc/dc/commit/46cdeb0babee6870553a41877cbe85204e7099c4))
|
||||
* crypto module requirement for sheetjs/crypto package ([505d0af](https://git.datacontroller.io/dc/dc/commit/505d0af2b3b3c1c79c65045dcaffc263e0f8e796))
|
||||
* disable parsing excel in web worker beacuse it breaks in the stream apps ([280bdee](https://git.datacontroller.io/dc/dc/commit/280bdeeb1b82f00689f46c68a3cde3f2d24bc18f))
|
||||
* Display all contexts when installing DC on Viya ([d41f88f](https://git.datacontroller.io/dc/dc/commit/d41f88f8bf5bb2c725ee3edba085e8961ab8c727))
|
||||
* **edit:** use cellValidation keys and hotDataSchema to fill in defaults on add row ([4957548](https://git.datacontroller.io/dc/dc/commit/495754816c0e757b8f8b1c0ad51246dc7b65d957))
|
||||
* enabling closeouts for UPDATE in CAS tables ([8b8e8ae](https://git.datacontroller.io/dc/dc/commit/8b8e8aec159ff2f50cfa4683bcd7a25aabb75bf8))
|
||||
* enabling rollback when the table has formatted values ([815d6e9](https://git.datacontroller.io/dc/dc/commit/815d6e97a8e304d79d48cc949ba126e02a318dc1))
|
||||
* improvements to validations ([6ceb681](https://git.datacontroller.io/dc/dc/commit/6ceb6814633691b6d4ac2cb898cfb75e9d609102))
|
||||
* remove IE checks and conditions ([ece6bd1](https://git.datacontroller.io/dc/dc/commit/ece6bd1d787d722531334fc4f1396a94cf6d92ec))
|
||||
* updates to demodata to enable auto CAS promote ([7740d2a](https://git.datacontroller.io/dc/dc/commit/7740d2ac8694295b33b40a30603d8239818896f5))
|
||||
* upgrade angular core and compiler ([aecd597](https://git.datacontroller.io/dc/dc/commit/aecd5976875a7c01189248c5f5aa3478b28c1ab2))
|
||||
* using fcopy instead of binary copy for file upload, for Viya 2026 compatibility ([716ee6e](https://git.datacontroller.io/dc/dc/commit/716ee6eba0a28f4f0a7a96b0719caf15da9b6e78))
|
||||
* **viewer:** search causing blank Handsontable ([338c7a2](https://git.datacontroller.io/dc/dc/commit/338c7a2e418c47e34331bd04718cd816f978837c)), closes [#206](https://git.datacontroller.io/dc/dc/issues/206)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* adding demo data job ([8c2aeac](https://git.datacontroller.io/dc/dc/commit/8c2aeacc85da5c106c356709cefcb412ed0a71db))
|
||||
* **dq rules:** notnull validation when invalid cell, will auto populate a default value ([96f2518](https://git.datacontroller.io/dc/dc/commit/96f2518af9e547956be5862a1322d9ab8e07369b))
|
||||
|
||||
## [7.2.8](https://git.datacontroller.io/dc/dc/compare/v7.2.7...v7.2.8) (2026-02-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bump adapter version ([f4c8699](https://git.datacontroller.io/dc/dc/commit/f4c8699aaf0b1e01b447296978a4f6dedc8903f9))
|
||||
|
||||
## [7.2.7](https://git.datacontroller.io/dc/dc/compare/v7.2.6...v7.2.7) (2026-02-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* dclib not found error in getchangeinfo job ([86791db](https://git.datacontroller.io/dc/dc/commit/86791dbaca39034a19bf8f34efbddf898c57f2f7))
|
||||
|
||||
## [7.2.6](https://git.datacontroller.io/dc/dc/compare/v7.2.5...v7.2.6) (2026-01-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update angular and moment ([8c5b357](https://git.datacontroller.io/dc/dc/commit/8c5b357dd286db331a6dcdeb3fd499fe3b634288))
|
||||
|
||||
## [7.2.5](https://git.datacontroller.io/dc/dc/compare/v7.2.4...v7.2.5) (2025-12-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* (build) rebuilt package-lock files ([bfbfd55](https://git.datacontroller.io/dc/dc/commit/bfbfd55fe7e2dff3ce707763a2c7939ff365318b))
|
||||
* (deps) bump @sasjs/cli and @sasjs/core ([d7c7302](https://git.datacontroller.io/dc/dc/commit/d7c7302c12ac60f355ab9b3b1b461fcf7d0719b8))
|
||||
* (deps) bumped @sasjs/core, @sasjs/cli, @sasjs/utils and @sasjs/adapter ([af1657e](https://git.datacontroller.io/dc/dc/commit/af1657e226a4efd22cc87401a3850c4a665c2680))
|
||||
* configurable audit table on restore check ([26ce95f](https://git.datacontroller.io/dc/dc/commit/26ce95f7c1d2260f81c240cd6b058db154d997e4)), closes [#193](https://git.datacontroller.io/dc/dc/issues/193)
|
||||
* improved testing ([fb3c49a](https://git.datacontroller.io/dc/dc/commit/fb3c49aa8bfdc6acf2ae3034b885010dcdce32a6))
|
||||
* output values to intended macro variables ([43ae73c](https://git.datacontroller.io/dc/dc/commit/43ae73c5f3ad919394201f54984b61bb2a52fcfe))
|
||||
|
||||
## [7.2.4](https://git.datacontroller.io/dc/dc/compare/v7.2.3...v7.2.4) (2025-10-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ensure reload after applying licence key ([cb1978b](https://git.datacontroller.io/dc/dc/commit/cb1978bcaf23b0bf45b5d3b78b9707fd4e48a5f4))
|
||||
* snyk report security patches ([387f512](https://git.datacontroller.io/dc/dc/commit/387f5122f1ea6dff55d23c9223f17737283a94d3))
|
||||
|
||||
## [7.2.3](https://git.datacontroller.io/dc/dc/compare/v7.2.2...v7.2.3) (2025-10-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* opening second table in viewer throws an error ([6c6b1cb](https://git.datacontroller.io/dc/dc/commit/6c6b1cbf460e5291ec746af017e764b894fff8d5))
|
||||
|
||||
## [7.2.2](https://git.datacontroller.io/dc/dc/compare/v7.2.1...v7.2.2) (2025-09-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* jsrsasign, @sasjs/cli bump ([365f129](https://git.datacontroller.io/dc/dc/commit/365f12996db3ef50a4f4f099d5af15696c43bb42))
|
||||
|
||||
## [7.2.1](https://git.datacontroller.io/dc/dc/compare/v7.2.0...v7.2.1) (2025-08-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* removing localhost from index.html ([225e693](https://git.datacontroller.io/dc/dc/commit/225e693d1fd4381f2b8ce42fecb508f0a9e9dad8))
|
||||
|
||||
# [7.2.0](https://git.datacontroller.io/dc/dc/compare/v7.1.1...v7.2.0) (2025-08-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ci:** cypress dependency package not available anymore ([26cdd73](https://git.datacontroller.io/dc/dc/commit/26cdd733315ef8babe9498ce93f6eb29c587dabd))
|
||||
* **hot v16 migration:** multi dataset fixed issues, and cypress tests adapted ([712b384](https://git.datacontroller.io/dc/dc/commit/712b3848480a8769d149e00b0d2de91396022b66))
|
||||
* obsolete cypress deps ([2ba4b53](https://git.datacontroller.io/dc/dc/commit/2ba4b5383e23bff8dfeb82b0ef473e5871c94709))
|
||||
* remaining hot migrations - handsontable/angular-wrapper ([b419cd5](https://git.datacontroller.io/dc/dc/commit/b419cd507837e846e9dfcc6b729254d56cc196e6))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* lighthouse accessibility check pipeline ([670ec2c](https://git.datacontroller.io/dc/dc/commit/670ec2c71cb2d24e9d79e297a8cbc6136aa315c8))
|
||||
|
||||
## [7.1.1](https://git.datacontroller.io/dc/dc/compare/v7.1.0...v7.1.1) (2025-07-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **viewboxes:** hot v16 fails to load because of relative height `100%` ([672dd6d](https://git.datacontroller.io/dc/dc/commit/672dd6d4f1fda27e3706dd7caa42b45922319497))
|
||||
|
||||
# [7.1.0](https://git.datacontroller.io/dc/dc/compare/v7.0.3...v7.1.0) (2025-07-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adapter bump ([b495c41](https://git.datacontroller.io/dc/dc/commit/b495c41626c85b7c4141d9361e4d3a826efd6c05))
|
||||
* bumping CLI to 4.12.10 ([a08a717](https://git.datacontroller.io/dc/dc/commit/a08a717ca8d49e8a7d63f3fd91c6a7d42a1d6d8b))
|
||||
* bumping sasjs/core and sasjs/cli ([63e9af4](https://git.datacontroller.io/dc/dc/commit/63e9af402ed65f6be4426e76ee1376a40e6ed097))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* improving accessibility score up to 100, hot update to v16.0.1 ([71c308d](https://git.datacontroller.io/dc/dc/commit/71c308d052400ecedc03f8020a5a69471ac6b116))
|
||||
|
||||
## [7.0.3](https://git.datacontroller.io/dc/dc/compare/v7.0.2...v7.0.3) (2025-06-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* makedata vars ([e7cb471](https://git.datacontroller.io/dc/dc/commit/e7cb471c0b60058b03fe8cbed5e3e2e70dd72e26))
|
||||
* viya deploy makedata missing params ([7a82316](https://git.datacontroller.io/dc/dc/commit/7a8231615cb56710351fae5868e8fdeed54d180c))
|
||||
|
||||
## [7.0.2](https://git.datacontroller.io/dc/dc/compare/v7.0.1...v7.0.2) (2025-06-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **viya deploy:** run makedata in new window to ensure logs are available for the user ([0b4042a](https://git.datacontroller.io/dc/dc/commit/0b4042af6011fdc65cfaaa5d4b1d8f48cd67f3b3))
|
||||
|
||||
## [7.0.1](https://git.datacontroller.io/dc/dc/compare/v7.0.0...v7.0.1) (2025-06-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* refresh process ([4ecd186](https://git.datacontroller.io/dc/dc/commit/4ecd186e5cb22dd436f2d7f1200956f4e3f27425))
|
||||
|
||||
# [7.0.0](https://git.datacontroller.io/dc/dc/compare/v6.16.2...v7.0.0) (2025-06-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bumping adapter to re-enable JES API method ([e874143](https://git.datacontroller.io/dc/dc/commit/e874143a95d0ac2e56c0793e04b979c27f96d74b))
|
||||
* commit git hooks checking lint ([69f687a](https://git.datacontroller.io/dc/dc/commit/69f687a85f1cc562346b6167813d617cb9bd3404))
|
||||
* ensuring apploc is not case sensitive. Closes [#171](https://git.datacontroller.io/dc/dc/issues/171) ([24545f2](https://git.datacontroller.io/dc/dc/commit/24545f2acdd5bd73cbe062526f2bd043269cc6a3))
|
||||
* export unregistered formats ([f6d7d6f](https://git.datacontroller.io/dc/dc/commit/f6d7d6f90c978ac8c071471dfb67a60834424de5)), closes [#158](https://git.datacontroller.io/dc/dc/issues/158)
|
||||
* reload startupservice after user approves the MPE_TABLES page ([e5f8e50](https://git.datacontroller.io/dc/dc/commit/e5f8e500c125ee233c6f7af5ad0077c0ed6abfcb))
|
||||
* showing catalog_cnt in libinfo ([e44a25d](https://git.datacontroller.io/dc/dc/commit/e44a25dcc39ba4b9714257c60da84c2dfa613a85)), closes [#160](https://git.datacontroller.io/dc/dc/issues/160)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* adding 4 new tables for catalogs ([e4dbab8](https://git.datacontroller.io/dc/dc/commit/e4dbab8b1654b24e610e4b0603d1cf2b02a451e2))
|
||||
* capturing catalog specific information, closes [#159](https://git.datacontroller.io/dc/dc/issues/159) ([b4c586a](https://git.datacontroller.io/dc/dc/commit/b4c586a859929e0122cd46449e43d4ca597b8b2b))
|
||||
* viewer added catalog_cnt ([2aa19d1](https://git.datacontroller.io/dc/dc/commit/2aa19d1dca747f41274a032cde78d8ba73d66224))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Introduction of 4 new tables for capturing information related to catalogs and their objects. Migration script prepared and available in the DB folder (usual place)
|
||||
|
||||
## [6.16.2](https://git.datacontroller.io/dc/dc/compare/v6.16.1...v6.16.2) (2025-06-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* streaming viya deploy `isStreaming` function stability fix ([4830c6d](https://git.datacontroller.io/dc/dc/commit/4830c6d2191cb47abcc7919bc1d49e55595e6121))
|
||||
|
||||
## [6.16.1](https://git.datacontroller.io/dc/dc/compare/v6.16.0...v6.16.1) (2025-06-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* viya deploy updating index html based on URL ([86134f4](https://git.datacontroller.io/dc/dc/commit/86134f478ae0b9426e01bfcc9ca4ee597ca733f7))
|
||||
* viya streamed app deploy page flow fix ([89ab296](https://git.datacontroller.io/dc/dc/commit/89ab2961513b245eeea48d1867c6496d3261761e))
|
||||
|
||||
# [6.16.0](https://git.datacontroller.io/dc/dc/compare/v6.15.2...v6.16.0) (2025-06-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adapter bump ([ca7caa2](https://git.datacontroller.io/dc/dc/commit/ca7caa25b6eea1bd4579fb8b67ec9b211a893079))
|
||||
* automatic viya deploy timing issue ([037a97b](https://git.datacontroller.io/dc/dc/commit/037a97b6ffa27b40891531ae6812ebe5b5e71e34))
|
||||
* bump core to ensure ff works on viya streaming deploy ([cbd69df](https://git.datacontroller.io/dc/dc/commit/cbd69df708edf3a8446115ca7315fac3557dcf97)), closes [#156](https://git.datacontroller.io/dc/dc/issues/156)
|
||||
* viya deploy load data timing ([abdbb67](https://git.datacontroller.io/dc/dc/commit/abdbb674713796e5308eb4272197a5c253868a85))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* viya deploy, update the index.html contextname ([7223955](https://git.datacontroller.io/dc/dc/commit/72239558af2ee50cdfc71b7e185e6661ab568ba1))
|
||||
|
||||
## [6.15.2](https://git.datacontroller.io/dc/dc/compare/v6.15.1...v6.15.2) (2025-06-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* pipeline updates for DC.html ([624a7a8](https://git.datacontroller.io/dc/dc/commit/624a7a8f37f0265cf576da310ac330c75aa417cf))
|
||||
|
||||
## [6.15.1](https://git.datacontroller.io/dc/dc/compare/v6.15.0...v6.15.1) (2025-06-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* updating pipeline to default to streaming on viya ([4b55894](https://git.datacontroller.io/dc/dc/commit/4b558948d997f456ff25a12a58827fe0d2075493))
|
||||
|
||||
# [6.15.0](https://git.datacontroller.io/dc/dc/compare/v6.14.10...v6.15.0) (2025-06-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* makedata with context name ([da4d0b2](https://git.datacontroller.io/dc/dc/commit/da4d0b28c7109afd6f96455e1e0e80a40d25a942))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* viya deploy context ([6c96ef7](https://git.datacontroller.io/dc/dc/commit/6c96ef7fb0a55754a84ff0a8bbab838b78c1acaf))
|
||||
|
||||
## [6.14.10](https://git.datacontroller.io/dc/dc/compare/v6.14.9...v6.14.10) (2025-06-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bump core ([0e8503e](https://git.datacontroller.io/dc/dc/commit/0e8503ed2bb22a0fc3924ac929e7f19626772e0a))
|
||||
* default to home directory for SAS Drive in Viya ([9682b54](https://git.datacontroller.io/dc/dc/commit/9682b548e6106d99d97dcc023a35d93addfd5170))
|
||||
|
||||
## [6.14.9](https://git.datacontroller.io/dc/dc/compare/v6.14.8...v6.14.9) (2025-06-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* default DC path for viya ([f3125ff](https://git.datacontroller.io/dc/dc/commit/f3125ff4641e47e33cb203228f5b1014ea3343bc))
|
||||
|
||||
## [6.14.8](https://git.datacontroller.io/dc/dc/compare/v6.14.7...v6.14.8) (2025-05-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* CSP issues, clarity local library build, fixed some style issues ([841201a](https://git.datacontroller.io/dc/dc/commit/841201adab582149b1cca3a42e75f7cac75167f9))
|
||||
* deploy page, makedata error handling, added local build of clarity, to address clr-stack-view CSP issues (inline styles) ([7b5e7ae](https://git.datacontroller.io/dc/dc/commit/7b5e7ae18414152f9b9d8f2d94fc94de43152003))
|
||||
* improved deploy flow for Viya ([9604661](https://git.datacontroller.io/dc/dc/commit/9604661f3b76111387bc9474cc26348d73ab112e))
|
||||
* requests modal causing VIYA CSP errors ([1dc6934](https://git.datacontroller.io/dc/dc/commit/1dc69341cadb837e1f11624d5cf35788bbb98d96))
|
||||
* sas viya service init timing issue ([9de04e9](https://git.datacontroller.io/dc/dc/commit/9de04e9a0ce016e1a9fb8b19c656077079ddcf2f))
|
||||
* scss of components transferred to the global styles.scss so we do not cause CSP (inline styles) issues when streaming to Viya ([6c171a6](https://git.datacontroller.io/dc/dc/commit/6c171a6394aba8104fe0f50aa8a4e6b9fa8023a2))
|
||||
* viya deploy page improved flow ([4bd2154](https://git.datacontroller.io/dc/dc/commit/4bd215491f8cdc68f78bade68e7cb98e07edc81e))
|
||||
|
||||
## [6.14.7](https://git.datacontroller.io/dc/dc/compare/v6.14.6...v6.14.7) (2025-05-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* updated hot, clarity and improved accessibility score. ([2844c70](https://git.datacontroller.io/dc/dc/commit/2844c70f9507036216b8b621900c2bb9010c1d34))
|
||||
|
||||
## [6.14.6](https://git.datacontroller.io/dc/dc/compare/v6.14.5...v6.14.6) (2025-04-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* history table modal links styling ([c63fcdd](https://git.datacontroller.io/dc/dc/commit/c63fcdd465950ada439d7d69622a3886e8f3a783))
|
||||
|
||||
## [6.14.5](https://git.datacontroller.io/dc/dc/compare/v6.14.4...v6.14.5) (2025-03-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improving accessibility lighthouse score ([7f3577c](https://git.datacontroller.io/dc/dc/commit/7f3577c3ef9f44e55a58bc64fbf89a3a64006dd4))
|
||||
* prevent errors when using sqlrc in a DI job in a HOOK ([d1f0879](https://git.datacontroller.io/dc/dc/commit/d1f0879f0acf7e816c80f7635fd02f4f284214ed))
|
||||
* user profile style fix, new select library and table icons ([69f8830](https://git.datacontroller.io/dc/dc/commit/69f883034fabbed31aa5d832e20561c4ae3042db))
|
||||
|
||||
## [6.14.4](https://git.datacontroller.io/dc/dc/compare/v6.14.3...v6.14.4) (2025-03-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* removing cli dependency warnings2 ([43c0f73](https://git.datacontroller.io/dc/dc/commit/43c0f73c2189ff762986a964caae6b0b108164fc))
|
||||
|
||||
## [6.14.3](https://git.datacontroller.io/dc/dc/compare/v6.14.2...v6.14.3) (2025-03-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* NLDAT & NLDATM formats are now being staged ([3f5cb1e](https://git.datacontroller.io/dc/dc/commit/3f5cb1e2defe390220e904e4bf04a165cb31fec4))
|
||||
|
||||
## [6.14.2](https://git.datacontroller.io/dc/dc/compare/v6.14.1...v6.14.2) (2025-03-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improving instructions for setup ([83b3d77](https://git.datacontroller.io/dc/dc/commit/83b3d775b6e33653b087ca9f4eb3ad5b0dbbd479))
|
||||
|
||||
## [6.14.1](https://git.datacontroller.io/dc/dc/compare/v6.14.0...v6.14.1) (2025-03-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* handle national language datetime formats ([149e318](https://git.datacontroller.io/dc/dc/commit/149e318a8787be0109f25aeec3a1270ea75a97b2))
|
||||
* updating logic to use NLDAT formats ([95289aa](https://git.datacontroller.io/dc/dc/commit/95289aa9524d3cb2b1c248cfb84f6b0d0a490c32))
|
||||
|
||||
# [6.14.0](https://git.datacontroller.io/dc/dc/compare/v6.13.2...v6.14.0) (2025-02-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* uses SORTSEQ=LINGUISTIC for the services/metanav/metadetails service ([a45f5bb](https://git.datacontroller.io/dc/dc/commit/a45f5bb3b27a86da5f55ff28c9c7669956484ddf))
|
||||
|
||||
## [6.13.2](https://git.datacontroller.io/dc/dc/compare/v6.13.1...v6.13.2) (2025-02-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* get metadata email if exists ([1bd0eef](https://git.datacontroller.io/dc/dc/commit/1bd0eef913593579771c647d80010ce628c00819))
|
||||
|
||||
## [6.13.1](https://git.datacontroller.io/dc/dc/compare/v6.13.0...v6.13.1) (2025-02-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Avoiding LATIN1 unprintables in various UI locations ([bce1fd5](https://git.datacontroller.io/dc/dc/commit/bce1fd57ef397cfdd030552c3f424a8407174729))
|
||||
* updated @sasjs/adapter, crypto-browserify ([4e64f28](https://git.datacontroller.io/dc/dc/commit/4e64f28732868b07ec2ab403912bd384c62c7e1d))
|
||||
|
||||
# [6.13.0](https://git.datacontroller.io/dc/dc/compare/v6.12.3...v6.13.0) (2025-01-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* editor page csv upload ([217220f](https://git.datacontroller.io/dc/dc/commit/217220ffaaf688133321cc68d770aaf1e50590a9))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* csv test ([c53ab85](https://git.datacontroller.io/dc/dc/commit/c53ab85107f10c023117a099cc06321afc3e1f03))
|
||||
|
||||
## [6.12.3](https://git.datacontroller.io/dc/dc/compare/v6.12.2...v6.12.3) (2025-01-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adding missing=STRING to three services ([30d5e51](https://git.datacontroller.io/dc/dc/commit/30d5e51d0b9cf27774038476bd90559600952304))
|
||||
|
||||
## [6.12.2](https://git.datacontroller.io/dc/dc/compare/v6.12.1...v6.12.2) (2025-01-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* unnecessary zeros when adding new row (data schema default values) ([a1a9051](https://git.datacontroller.io/dc/dc/commit/a1a90519c535ca25e00822b4d3358c991ac9662e))
|
||||
|
||||
## [6.12.1](https://git.datacontroller.io/dc/dc/compare/v6.12.0...v6.12.1) (2024-12-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* no upcase of pk fields in MPE_TABLES in delete scenario ([3de095f](https://git.datacontroller.io/dc/dc/commit/3de095fe7797cde60f0e232c188305fe423c27eb)), closes [#134](https://git.datacontroller.io/dc/dc/issues/134)
|
||||
* reduce length of tmp table names. Closes [#130](https://git.datacontroller.io/dc/dc/issues/130) ([f9c2491](https://git.datacontroller.io/dc/dc/commit/f9c2491ab6e7b528b7ffc011fd9e45c963c5f6bf))
|
||||
|
||||
# [6.12.0](https://git.datacontroller.io/dc/dc/compare/v6.11.1...v6.12.0) (2024-09-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added appLoc to the system page ([dd2138a](https://git.datacontroller.io/dc/dc/commit/dd2138ac5e6067de310e83d16fccc9b9764ba3ff))
|
||||
* bumping core for passthrough fix, [#124](https://git.datacontroller.io/dc/dc/issues/124) ([caa9854](https://git.datacontroller.io/dc/dc/commit/caa9854ff0431ccbb6ff1d6d3509dc877362cceb))
|
||||
* excel with password flow, introducing web worker for XLSX.read ([a3ce367](https://git.datacontroller.io/dc/dc/commit/a3ce36795007a4e3b6ac3499ffd119dc3758f387))
|
||||
* implemented the new request wrapper usage, added XLSX read with a Web Worker, multi load preview data, full height ([4218da9](https://git.datacontroller.io/dc/dc/commit/4218da91cd193aa45346ad7e34ccc00ca89df4fb))
|
||||
* **multi load:** xlsx read file ahead of time, while user choose datasets ([6547461](https://git.datacontroller.io/dc/dc/commit/65474616379e1dacc1329b3bdc5eb14f34428bb1))
|
||||
* refactored adapter request wrapper function to return job log as well ([67436f4](https://git.datacontroller.io/dc/dc/commit/67436f4ff9bb4d77d5f897f47a3e3d472981f275))
|
||||
* using temporary names for temporary tables ([ce50365](https://git.datacontroller.io/dc/dc/commit/ce503653cd9fc36f72fb172bd14816e07c792e14)), closes [#124](https://git.datacontroller.io/dc/dc/issues/124)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* searching data in excel files using new algorithm (massive performance improvement) ([bbb725c](https://git.datacontroller.io/dc/dc/commit/bbb725c64cc23ed701b189623992408c42fdde8f))
|
||||
|
||||
## [6.11.1](https://git.datacontroller.io/dc/dc/compare/v6.11.0...v6.11.1) (2024-07-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adding SYSSITE, part of [#116](https://git.datacontroller.io/dc/dc/issues/116) ([a156c01](https://git.datacontroller.io/dc/dc/commit/a156c0111b3de5e3744e38d377d6e9aa09915803))
|
||||
* ensuring review_reason_txt in output. Closes [#117](https://git.datacontroller.io/dc/dc/issues/117) ([e5d93fd](https://git.datacontroller.io/dc/dc/commit/e5d93fd7d6d86bc47ff56664bd812b4d9d0749a5))
|
||||
|
||||
# [6.11.0](https://git.datacontroller.io/dc/dc/compare/v6.10.1...v6.11.0) (2024-06-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* addressing PR comments ([d94df7f](https://git.datacontroller.io/dc/dc/commit/d94df7f0ebae8feab5e1d5cf8011af8c8be2ca18))
|
||||
* **multi load:** fixed parsing algorithm reused for the multi load, the fix affects the normal upload as well. ([d4fee79](https://git.datacontroller.io/dc/dc/commit/d4fee791a72021e449cf9680c3e3a525dce41ac1))
|
||||
* **multi load:** label rename ([fa04d7b](https://git.datacontroller.io/dc/dc/commit/fa04d7bf4e5ba337146bdaa926c60488f8851449))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **multi load:** added HOT for user datasets input ([18363bb](https://git.datacontroller.io/dc/dc/commit/18363bbbeb9cf96183ba4841da8134b2f66f735c))
|
||||
* **multi load:** implemented matching libds and parsing of the multiple sheets ([efcdc69](https://git.datacontroller.io/dc/dc/commit/efcdc694dd275cdb9a4e19f26e5522b8dadc5fd9))
|
||||
* **multi load:** licence submit limits ([cffeab8](https://git.datacontroller.io/dc/dc/commit/cffeab813d8d4b324f82710dfd73953d4cbf8ffe))
|
||||
* **multi load:** multiple csv files ([4d27665](https://git.datacontroller.io/dc/dc/commit/4d276657b35a147a2233a03afcb1716348555f52))
|
||||
* **multi load:** refactored range find function, unlocking excel with password is reusable ([eb7c443](https://git.datacontroller.io/dc/dc/commit/eb7c44333c865e7f7bbfb54dd7f73bfc110f86a7))
|
||||
* **multi load:** submitting multiple found tables at once ([5deba44](https://git.datacontroller.io/dc/dc/commit/5deba44d2b7352866d821b70dbbfbbf54955dc47))
|
||||
|
||||
## [6.10.1](https://git.datacontroller.io/dc/dc/compare/v6.10.0...v6.10.1) (2024-06-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adding 60 more colours to crayons table. Closes [#112](https://git.datacontroller.io/dc/dc/issues/112) ([3521579](https://git.datacontroller.io/dc/dc/commit/3521579dead089eebf62455686be3aee88bde687))
|
||||
* terms and conditions colours, editor on smaller screens show only icons ([e32d44b](https://git.datacontroller.io/dc/dc/commit/e32d44b1bcdfeea43d19b21ec0ddf4af1ce3992a))
|
||||
|
||||
# [6.10.0](https://git.datacontroller.io/dc/dc/compare/v6.9.0...v6.10.0) (2024-06-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* updated handsontable to v14 ([2f8d0b7](https://git.datacontroller.io/dc/dc/commit/2f8d0b764a957ad8c11cd1088fad5e0670aa1731))
|
||||
|
||||
# [6.9.0](https://git.datacontroller.io/dc/dc/compare/v6.8.5...v6.9.0) (2024-05-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added colors.scss file, start of a refactor ([110ad9a](https://git.datacontroller.io/dc/dc/commit/110ad9a6e9ed39bd5591ae65c2d0005ba47ca758))
|
||||
* added stealFocus directive ([9a79f37](https://git.datacontroller.io/dc/dc/commit/9a79f37bf143a1e05df7407358e2687c678e3e68))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* added app settings service to handle theme persistance, fix: optimised dark mode contrast ([35844e0](https://git.datacontroller.io/dc/dc/commit/35844e0cf1a639553269f2ab0f8666a56ab5cc47))
|
||||
* **dark mode:** clarity optimizations ([afa7e38](https://git.datacontroller.io/dc/dc/commit/afa7e380aa3bdabd380c038522b9d73d9a8a3b91))
|
||||
* **dark mode:** lineage and metadata ([27907ed](https://git.datacontroller.io/dc/dc/commit/27907ed00fe81f4c752ffe99d2fb029d5c884f0a))
|
||||
* **dark mode:** refactoring clarity to enable dark mode, added toggle button ([5564aea](https://git.datacontroller.io/dc/dc/commit/5564aea9c25f8e81ff85afa8352325b9992e4043))
|
||||
* **dark mode:** removing custom css rules so clarity can handle dark/light modes. Handsontable css for dark mode ([2c0afd0](https://git.datacontroller.io/dc/dc/commit/2c0afd02684cdf3bda374731b0359665e00ed95d))
|
||||
|
||||
## [6.8.5](https://git.datacontroller.io/dc/dc/compare/v6.8.4...v6.8.5) (2024-05-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bitemporal load issue [#105](https://git.datacontroller.io/dc/dc/issues/105) ([967698e](https://git.datacontroller.io/dc/dc/commit/967698e4ce1e0abcbc6f0aff8a4be6c512dee93c))
|
||||
|
||||
## [6.8.4](https://git.datacontroller.io/dc/dc/compare/v6.8.3...v6.8.4) (2024-05-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* new approach to fixing [#105](https://git.datacontroller.io/dc/dc/issues/105) ([c11bd9a](https://git.datacontroller.io/dc/dc/commit/c11bd9a2c55e49f10451962cb2e222c21206bce5))
|
||||
|
||||
## [6.8.3](https://git.datacontroller.io/dc/dc/compare/v6.8.2...v6.8.3) (2024-05-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* updating core to increase filename length, closes [#103](https://git.datacontroller.io/dc/dc/issues/103) ([ee58fd5](https://git.datacontroller.io/dc/dc/commit/ee58fd5b4bc0dd3e3f232c4f26bb85b2e7fe2b54))
|
||||
|
||||
## [6.8.2](https://git.datacontroller.io/dc/dc/compare/v6.8.1...v6.8.2) (2024-05-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* dc_request_logs option feature ([93758ef](https://git.datacontroller.io/dc/dc/commit/93758efb275966c181f1ee8b6c752010909a0282))
|
||||
* release process ([c0dc919](https://git.datacontroller.io/dc/dc/commit/c0dc9191e3b95ea6f7e5021fc0bdbcab0af4cc64))
|
||||
|
||||
## [6.8.1](https://git.datacontroller.io/dc/dc/compare/v6.8.0...v6.8.1) (2024-05-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* hide approve button when table revertable ([ec0f539](https://git.datacontroller.io/dc/dc/commit/ec0f539a337b176c83a661ff520a6892d47efa02))
|
||||
|
||||
# [6.8.0](https://git.datacontroller.io/dc/dc/compare/v6.7.0...v6.8.0) (2024-05-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ci sheet lib, submit message auto focus ([c5e4650](https://git.datacontroller.io/dc/dc/commit/c5e46503272f3f3d9cd83ac04225babf79d4de44))
|
||||
* **clarity:** new version style issues ([8c7de5a](https://git.datacontroller.io/dc/dc/commit/8c7de5aad7e7e32a64769696af9b93eb9a6225d3))
|
||||
* cypress tests ([3dd85cc](https://git.datacontroller.io/dc/dc/commit/3dd85cc60bd5ac99bc930b6b9c89a8e707e4d51d))
|
||||
* ensuring that only restorable versions are restorable ([a402856](https://git.datacontroller.io/dc/dc/commit/a4028562ce91b32ff971ab9821328b97cd23f381))
|
||||
* ensuring version history only includes loaded versions ([51ebd25](https://git.datacontroller.io/dc/dc/commit/51ebd25aa362aa8e66c83b29b2c64aa0f206f5bd))
|
||||
* final testing on restore feature ([297a84d](https://git.datacontroller.io/dc/dc/commit/297a84d3a4ebb47bef7f3ca9758978d727afed8d))
|
||||
* issue with multiple adds/deletes, [#84](https://git.datacontroller.io/dc/dc/issues/84) ([904ca30](https://git.datacontroller.io/dc/dc/commit/904ca30f918da085fa05dae066367b512933d1a9))
|
||||
* load_ref var ([aaad9f7](https://git.datacontroller.io/dc/dc/commit/aaad9f7207115599a006980fff099d59738dd2cd))
|
||||
* removing alerts dummy data, closes [#93](https://git.datacontroller.io/dc/dc/issues/93) ([eba21e9](https://git.datacontroller.io/dc/dc/commit/eba21e96b4fa34e63b4477281f47d9a01d621f2e))
|
||||
* restore table version improvement ([549f357](https://git.datacontroller.io/dc/dc/commit/549f35766ba7b5bbe55694845e85bfefc4193375))
|
||||
* **sas:** viewer versions fix ([c6595c1](https://git.datacontroller.io/dc/dc/commit/c6595c1f618803d9202cba1a1fe76986449cf2e2))
|
||||
* stage and approve buttons renaming ([ef81e33](https://git.datacontroller.io/dc/dc/commit/ef81e33f704d0b4f99405d96498e1d29ac817982))
|
||||
* supporting SCD2 data reversions ([fa8396f](https://git.datacontroller.io/dc/dc/commit/fa8396f0394cbddb6dbacb4b355de078fad49980))
|
||||
* table info modal, versions - column names ([801c8c6](https://git.datacontroller.io/dc/dc/commit/801c8c6a9fb95388a06a6c6284fec4dc25bb77c5))
|
||||
* **updates:** angular, clarity, resolved legacy-peer-deps ([c60dd65](https://git.datacontroller.io/dc/dc/commit/c60dd65a1637333f11a0c39ef697c7292a6ede07))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* backend to show in getchangeinfo whether a user is allowed to restore ([8769841](https://git.datacontroller.io/dc/dc/commit/8769841f08694f672ef7ae1a17beacd0dbedda52))
|
||||
* list versions of target tables (backend) ([f8a14d4](https://git.datacontroller.io/dc/dc/commit/f8a14d4bdef055b99930491d1f6fabe55a50a497))
|
||||
* restore ([604c2e7](https://git.datacontroller.io/dc/dc/commit/604c2e70bdeeeb1aa5bb18b94f525ebd049397fa))
|
||||
* SAS services & tests for RESTORE, [#84](https://git.datacontroller.io/dc/dc/issues/84) ([9ad7ae4](https://git.datacontroller.io/dc/dc/commit/9ad7ae47b5e793ce68ab21c9eeb8dee6cb85e496))
|
||||
* staging page, restore buttons ([02a8a1c](https://git.datacontroller.io/dc/dc/commit/02a8a1c5654350cafc53b749cceb686fc6848c33))
|
||||
* table metadata modal, versions tab (and link) ([b27fea5](https://git.datacontroller.io/dc/dc/commit/b27fea5b91e33b4673a3a991aedae558e366ca29))
|
||||
* **versions:** getting list of versions (plus test) ([8003da9](https://git.datacontroller.io/dc/dc/commit/8003da94e615463ed3ddfd60b0cbf2e58615eab1))
|
||||
|
||||
# [6.7.0](https://git.datacontroller.io/dc/dc/compare/v6.6.4...v6.7.0) (2024-04-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* numeric values in hot dropdown aligned right ([9635626](https://git.datacontroller.io/dc/dc/commit/963562621ddf0e8d24a29a8481c5e6da1b040708))
|
||||
|
||||
## [6.6.4](https://git.datacontroller.io/dc/dc/compare/v6.6.3...v6.6.4) (2024-04-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ordering SOFTSELECT numerically in dropdown ([f522038](https://git.datacontroller.io/dc/dc/commit/f522038b8ddb1da14b8adbf8346d0a4539a94cc8)), closes [#85](https://git.datacontroller.io/dc/dc/issues/85)
|
||||
* reverting col ([fbbcf90](https://git.datacontroller.io/dc/dc/commit/fbbcf90956bf538b032b0107c07b8576d20353b9))
|
||||
* typo ([31d4e5c](https://git.datacontroller.io/dc/dc/commit/31d4e5c727f790d428fb2ea8da60dca929561805))
|
||||
|
||||
## [6.6.3](https://git.datacontroller.io/dc/dc/compare/v6.6.2...v6.6.3) (2024-02-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow empty clause value when NE or CONTAINS ([432450a](https://git.datacontroller.io/dc/dc/commit/432450a15b51a269821ba1d430854f5d1dd04703))
|
||||
|
||||
## [6.6.2](https://git.datacontroller.io/dc/dc/compare/v6.6.1...v6.6.2) (2024-02-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* excel with commas getting wrapped in quotes ([3860134](https://git.datacontroller.io/dc/dc/commit/38601346a529cfe3787bb286a639e0293c365020))
|
||||
|
||||
## [6.6.1](https://git.datacontroller.io/dc/dc/compare/v6.6.0...v6.6.1) (2024-02-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **client:** bumped @sasjs/adapter with fixed redirected login ([eb1c09d](https://git.datacontroller.io/dc/dc/commit/eb1c09d7909ba07faf763da261545dc1efaec1b3))
|
||||
|
||||
# [6.6.0](https://git.datacontroller.io/dc/dc/compare/v6.5.2...v6.6.0) (2024-02-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adjust the col numbers in extracted data ([cff5989](https://git.datacontroller.io/dc/dc/commit/cff598955930d2581349e5c6e8b2dd3f9ac96b4c))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* extra table metadata for [#75](https://git.datacontroller.io/dc/dc/issues/75) ([837821f](https://git.datacontroller.io/dc/dc/commit/837821fd01477d340524dfdaf8dd3d3758cf3095))
|
||||
* show dsnote on hover title ([6565834](https://git.datacontroller.io/dc/dc/commit/6565834ad4089ecf2de39967e6ed6f217ee4a0a5))
|
||||
|
||||
## [6.5.2](https://git.datacontroller.io/dc/dc/compare/v6.5.1...v6.5.2) (2024-02-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ordering mpe_selectbox data by the data values after selectbox_order ([2b54034](https://git.datacontroller.io/dc/dc/commit/2b5403497317632a4be8a00f21455c036f1e6461))
|
||||
|
||||
## [6.5.1](https://git.datacontroller.io/dc/dc/compare/v6.5.0...v6.5.1) (2024-02-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ensuring submitter email can be pulled from mpe_emails ([eac0104](https://git.datacontroller.io/dc/dc/commit/eac0104d7aebaf98ff1d1c504c1ce3b25d4a0ce8))
|
||||
|
||||
# [6.5.0](https://git.datacontroller.io/dc/dc/compare/v6.4.0...v6.5.0) (2024-01-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* filtering by reference to Variables as well as Values ([6eb1aa8](https://git.datacontroller.io/dc/dc/commit/6eb1aa85d29294d63e6af377e622fbed7fd1fab8))
|
||||
|
||||
# [6.4.0](https://git.datacontroller.io/dc/dc/compare/v6.3.1...v6.4.0) (2024-01-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add dcLib to globals ([5d93346](https://git.datacontroller.io/dc/dc/commit/5d93346b52eda27c2829770e96686a713296d373))
|
||||
* add service to get xlmap rules and fixed interface name ([9ffa30a](https://git.datacontroller.io/dc/dc/commit/9ffa30ab747f5b62acbd452431a5e6e440afcb80))
|
||||
* increasing length of mpe_excel_map cols to ([2d4d068](https://git.datacontroller.io/dc/dc/commit/2d4d068413dcdac98581f08939e74bde65b73428))
|
||||
* providing info on mapids to FE ([fd94945](https://git.datacontroller.io/dc/dc/commit/fd94945466c1a797ddc89815258a65624a9cb0cf))
|
||||
* removing tables from EDIT menu that are in xlmaps ([9550ae4](https://git.datacontroller.io/dc/dc/commit/9550ae4d1154a0272f8a2427ac9d2afdfd699c96))
|
||||
* removing XLMAP_TARGETLIBDS from mpe_xlmaps_rules table ([93702c6](https://git.datacontroller.io/dc/dc/commit/93702c63dc280cdba1e46f0fd8fe0deaec879611))
|
||||
* renaming TABLE macvar to LOAD_REF in postdata.sas ([01915a2](https://git.datacontroller.io/dc/dc/commit/01915a2db9a4dfb94e4e8213e2c32181da36d349))
|
||||
* reverting xlmap in getdata change ([2d6e747](https://git.datacontroller.io/dc/dc/commit/2d6e747db9b84e9fb0dfcf9102a2f7dd2cb51891))
|
||||
* update edit tab to load ([516e5a2](https://git.datacontroller.io/dc/dc/commit/516e5a206216f79ab1dce9f4eab0d31115743160))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* adding ability to define the target table for excel maps ([c86fba9](https://git.datacontroller.io/dc/dc/commit/c86fba9dc75ddc6033132f469ad1c31b9131b12e))
|
||||
* adding ismap attribute to getdata response (and fixing test) ([2702bb3](https://git.datacontroller.io/dc/dc/commit/2702bb3c84c45903def1aa2b8cc20a6dd080281b))
|
||||
* Complex Excel Uploads ([cf19381](https://git.datacontroller.io/dc/dc/commit/cf193810606f287b8d6f864c4eb64d43c5ab5f3c)), closes [#69](https://git.datacontroller.io/dc/dc/issues/69)
|
||||
* Create Tables / Files dropdown under load tab ([b473b19](https://git.datacontroller.io/dc/dc/commit/b473b198a61f468dff74cd8e64692e7847084a80))
|
||||
* display list of maps in sidebar ([5aec024](https://git.datacontroller.io/dc/dc/commit/5aec0242429942f8a989b5fb79f8d3865e9de01a))
|
||||
* implemented the logic for xlmap component ([50696bb](https://git.datacontroller.io/dc/dc/commit/50696bb926dd00472db65a008771a4b6352871be))
|
||||
* model changes for [#69](https://git.datacontroller.io/dc/dc/issues/69) ([271543a](https://git.datacontroller.io/dc/dc/commit/271543a446a2116718f99f0540e3cd911f9f5fe7))
|
||||
* new getxlmaps service to return rules for a particular xlmap_id ([56264ec](https://git.datacontroller.io/dc/dc/commit/56264ecc6908bf6c8e3e666dfeba7068d6195df8))
|
||||
* validating the excel map after stage (adding load-ref) ([a485c3b](https://git.datacontroller.io/dc/dc/commit/a485c3b78724a36f7bacb264fb02140cc62d6512))
|
||||
|
||||
## [6.3.1](https://git.datacontroller.io/dc/dc/compare/v6.3.0...v6.3.1) (2024-01-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* enabling excel uploads to tables with retained keys, also adding more validation to MPE_TABLES updates ([3efccc4](https://git.datacontroller.io/dc/dc/commit/3efccc4cf3752763d049836724f2491c287f65db))
|
||||
|
||||
# [6.3.0](https://git.datacontroller.io/dc/dc/compare/v6.2.8...v6.3.0) (2023-12-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* viewer row handle ([dadac4f](https://git.datacontroller.io/dc/dc/commit/dadac4f13f85b5446198b6340cad28844defc94d))
|
||||
|
||||
## [6.2.8](https://git.datacontroller.io/dc/dc/compare/v6.2.7...v6.2.8) (2023-12-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bumping sasjs/core to fix mp_loadformat issue ([a1d308e](https://git.datacontroller.io/dc/dc/commit/a1d308ea078786b27bf7ec940d018fc657d4c398))
|
||||
* new logic for -fc suffix. Closes [#63](https://git.datacontroller.io/dc/dc/issues/63) ([5579db0](https://git.datacontroller.io/dc/dc/commit/5579db0eafc668b1bc310099b7cc3062e0598fc4))
|
||||
|
||||
## [6.2.7](https://git.datacontroller.io/dc/dc/compare/v6.2.6...v6.2.7) (2023-11-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **audit:** updated crypto-js (hashing rows in dynamic cell validation) ([a7aa42a](https://git.datacontroller.io/dc/dc/commit/a7aa42a59b71597399924b8d2d06010c806321f3))
|
||||
* missing dependency and avoiding label length limit issue ([91f128c](https://git.datacontroller.io/dc/dc/commit/91f128c2fead1e4f72267d689e67f49ec9a2ab35))
|
||||
|
||||
## [6.2.6](https://git.datacontroller.io/dc/dc/compare/v6.2.5...v6.2.6) (2023-10-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bumping core to address mm_assigndirectlib issue ([c27cdab](https://git.datacontroller.io/dc/dc/commit/c27cdab3fccbde814a29424d0344173a73ea816c))
|
||||
|
||||
## [6.2.5](https://git.datacontroller.io/dc/dc/compare/v6.2.4...v6.2.5) (2023-10-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* enabling AUTHDOMAIN in MM_ASSIGNDIRECTLIB ([008b45a](https://git.datacontroller.io/dc/dc/commit/008b45ad175ec0e6026f5ef3bc210470226e328f))
|
||||
|
||||
## [6.2.4](https://git.datacontroller.io/dc/dc/compare/v6.2.3...v6.2.4) (2023-10-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Enable display of metadata-only tables. Closes [#56](https://git.datacontroller.io/dc/dc/issues/56) ([f3e82b4](https://git.datacontroller.io/dc/dc/commit/f3e82b4ee2a9c1c851f812ac60e9eaf05f91a0f9))
|
||||
|
||||
## [6.2.3](https://git.datacontroller.io/dc/dc/compare/v6.2.2...v6.2.3) (2023-10-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bumping core library to avoid non-ascii char in mp_validatecols.sas. [#50](https://git.datacontroller.io/dc/dc/issues/50) ([11b06f6](https://git.datacontroller.io/dc/dc/commit/11b06f6416300b6d70b1570c415d5a5c004976db))
|
||||
* removing copyright symbol from mpe_alerts macro. [#50](https://git.datacontroller.io/dc/dc/issues/50) ([adb7eb7](https://git.datacontroller.io/dc/dc/commit/adb7eb77550c68a2dab15a6ff358129820e9b612))
|
||||
|
||||
## [6.2.2](https://git.datacontroller.io/dc/dc/compare/v6.2.1...v6.2.2) (2023-10-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* updated SheetJS (crypto) to the latest ([8bd0dd2](https://git.datacontroller.io/dc/dc/commit/8bd0dd22c258911672303869e4df893a98e93575))
|
||||
|
||||
## [6.2.1](https://git.datacontroller.io/dc/dc/compare/v6.2.0...v6.2.1) (2023-10-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* approve, history and submit pages grouped in review module ([e056ece](https://git.datacontroller.io/dc/dc/commit/e056ece2234ef6aab050f6a5b1f8de633b163d91))
|
||||
* closes [#39](https://git.datacontroller.io/dc/dc/issues/39) upcase issue in MPE_SECURITY ([a00d31c](https://git.datacontroller.io/dc/dc/commit/a00d31caf3c5634cd61a4700fb175e76856edbb6))
|
||||
* handsontable v13 ([6f482ec](https://git.datacontroller.io/dc/dc/commit/6f482ec6d909907a304ef9975262889e2370035f))
|
||||
* latest adapter ([5e30dc0](https://git.datacontroller.io/dc/dc/commit/5e30dc0f892fab2af41f4ea56e30f27ec3b3912e))
|
||||
* sasjs/cli and sasjs/core updated to the latest ([8571e01](https://git.datacontroller.io/dc/dc/commit/8571e01e44a8cb6df9d150d271c34bb75bffdf31))
|
||||
* updating editors/stagedata to address issues in particular viya configurations as described in issue [#33](https://git.datacontroller.io/dc/dc/issues/33) ([94ab949](https://git.datacontroller.io/dc/dc/commit/94ab949df8c75072525751a2156b7a32c2e641dc))
|
||||
* updating logic for REPLACE loadtype ([1f2ce55](https://git.datacontroller.io/dc/dc/commit/1f2ce55f249f4af56f0cacdec47e69246cd47431))
|
||||
|
||||
# [6.2.0](https://git.datacontroller.io/dc/dc/compare/v6.1.0...v6.2.0) (2023-08-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* re-enabling full REPLACE uploads ([08e39c4](https://git.datacontroller.io/dc/dc/commit/08e39c4fca570406f9aad3d907cb04596421d074))
|
||||
|
||||
### Features
|
||||
|
||||
* support for European numeric formats ([e48e47b](https://git.datacontroller.io/dc/dc/commit/e48e47bc635452b59e107b235e597c26e748875e))
|
||||
|
||||
# [6.1.0](https://git.datacontroller.io/dc/dc/compare/v6.0.0...v6.1.0) (2023-07-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* missing mf_existds dependency in bitemporal_dataloader ([5ce1701](https://git.datacontroller.io/dc/dc/commit/5ce1701657136f2cf792441412230513ff52e7e8))
|
||||
* reducing audit data volumes. Closes [#4](https://git.datacontroller.io/dc/dc/issues/4) ([54fe701](https://git.datacontroller.io/dc/dc/commit/54fe7013b1a25be228eeb2aba3553f6219952de6))
|
||||
* release script, excel upload duplicate primary keys, cypress fix ([2f79487](https://git.datacontroller.io/dc/dc/commit/2f79487aeaf6268b027a8fa52fcdaae2b449de3d))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* full format deletion, closes [#2](https://git.datacontroller.io/dc/dc/issues/2) ([8dc40bd](https://git.datacontroller.io/dc/dc/commit/8dc40bdd4e3a7ad5c1e6582b4130f24bc445eb77))
|
||||
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# Data Controller
|
||||
# Contributing
|
||||
|
||||
## Workflow guidelines
|
||||
[Wiki Page](https://git.datacontroller.io/dc/dc/wiki/Git-Workflow)
|
||||
|
||||
## Dependencies that requires licences
|
||||
|
||||
[SheetJS Pro Version](https://www.npmjs.com/package/sheetjs)
|
||||
@@ -50,6 +53,17 @@ npm run lint:fix
|
||||
Typedoc is used for generating typescript documentation based on the code.
|
||||
That part is automated and beign done as a part of CI job.
|
||||
|
||||
# Release
|
||||
Release is automated as a part of CI job. Workflow file: `.gitea/workflows/release.yaml`.
|
||||
It will run automatically when branch merged to the `main` branch.
|
||||
IMPORTANT!
|
||||
If release job fails, after it has been created empty release and a tag, we must not re-run the relase job until we removed the newly create GIT TAG and RELEASE.
|
||||
To remove the git tag run:
|
||||
```
|
||||
git push -d origin vX.X.X
|
||||
```
|
||||
To remove the release, you need to do it with repo administration over at [https://git.datacontroller.io/dc/dc](https://git.datacontroller.io/dc/dc)
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
## Makedata service "could not create directory" error
|
||||
|
||||
+6
-16
@@ -3,25 +3,15 @@ Licence Agreement for Data Controller for SAS®
|
||||
|
||||
Copyright (c) Bowe IO Ltd
|
||||
|
||||
Data Controller is a software distributed by 4GL Apps, a brand owned by Bowe IO Ltd, a UK Limited Company headquarted in 29 Oldfield Rd, Cumbria, registered by companies house under number 08777171, VAT number: 203914240
|
||||
Data Controller software is distributed by 4GL Apps, a brand owned by Bowe IO Ltd, a UK Limited Company headquarted in 29 Oldfield Rd, Cumbria, registered at Companies House with company number 08777171, VAT number: 203914240
|
||||
|
||||
This software is protected by applicable copyright laws, including international treaties, and dual-
|
||||
licensed – depending on whether your use for commercial purposes, meaning intended for or
|
||||
resulting in commercial advantage or monetary compensation, or not.
|
||||
This software is protected by applicable copyright laws, including international treaties, and dual-licensed – depending on whether your use for commercial purposes, meaning intended for or resulting in commercial advantage or monetary compensation, or not.
|
||||
|
||||
If your use is strictly personal or solely for evaluation purposes, meaning for the purposes of testing
|
||||
the suitability, performance, and usefulness of this software outside the production environment,
|
||||
you agree to be bound by the terms included in the "licence-non-commercial-datacontroller.md" file.
|
||||
If your use is strictly personal or solely for evaluation purposes, meaning for the purposes of testing the suitability, performance, and usefulness of this software outside the production environment, you agree to be bound by the terms included in the "licence-non-commercial-datacontroller.md" file available here: https://git.datacontroller.io/dc/dc/src/branch/main/licence-non-commercial-datacontroller.md
|
||||
|
||||
Your use of this software for commercial purposes is subject to the terms included in an applicable
|
||||
license agreement.
|
||||
Your use of this software for commercial purposes is subject to the terms included in an applicable license agreement.
|
||||
|
||||
In any case, you must not make any such use of this software as to develop software which may be
|
||||
considered competitive with this software.
|
||||
In any case, you must not make any such use of this software as to develop software which may be considered competitive with this software.
|
||||
|
||||
UNLESS EXPRESSLY AGREED OTHERWISE, 4GL APPS PROVIDES THIS SOFTWARE ON AN "AS IS"
|
||||
BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, AND IN NO EVENT AND UNDER NO
|
||||
LEGAL THEORY, SHALL 4GL APPS BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY DIRECT,
|
||||
INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER ARISING FROM
|
||||
USE OR INABILITY TO USE THIS SOFTWARE.
|
||||
UNLESS EXPRESSLY AGREED OTHERWISE, 4GL APPS PROVIDES THIS SOFTWARE ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, AND IN NO EVENT AND UNDER NO LEGAL THEORY, SHALL 4GL APPS BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER ARISING FROM USE OR INABILITY TO USE THIS SOFTWARE.
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# Data Controller for SAS
|
||||
|
||||
**Control your manual data modifications!**
|
||||
|
||||
_Alternatives to Data Controller include:_
|
||||
|
||||
* 💾 Developing / testing / deploying / scheduling overnight batch jobs to load files from shared drives
|
||||
* 🔒 Opening (and locking) datasets in Enterprise Guide or SAS® Table Viewer to perform direct updates
|
||||
* ❓ Asking a #DBA to run validated code after a change management process
|
||||
* 🌐 Building & maintaining your own custom web application
|
||||
* 🏃 Running #SAS or #SQL updates in production
|
||||
|
||||
_Problems with the above include:_
|
||||
|
||||
* Legacy 'black box' solutions with little to no testing, documentation or support
|
||||
* End users requiring direct write access to critical data sources in production
|
||||
* Upload routines that must be manually modified when the data model changes
|
||||
* Breaches due to unnecessary parties having access to the data
|
||||
* Inability to trace who made a change, when, and why
|
||||
* Reliance on key individuals to perform updates
|
||||
* Building bespoke ETL for every new data source
|
||||
* High risk of manual error / data corruption
|
||||
|
||||
Data Controller for SAS® solves all these issues in a simple-to-install, user-friendly, secure, documented, battle-tested web application. Available on Viya, SAS 9 EBI, and [SASjs Server](https://server.sasjs.io).
|
||||
|
||||
An individual Viya deploy can be done in just 2 lines of #SAS code!
|
||||
|
||||
```sas
|
||||
filename dc url "https://git.datacontroller.io/dc/dc/releases/download/latest/viya.sas";
|
||||
%inc dc;
|
||||
```
|
||||
|
||||
For a multi-user deploy, using a shared system account, please see [deploy docs](https://docs.datacontroller.io/deploy-viya/).
|
||||
|
||||
For further information:
|
||||
|
||||
* Main site: https://datacontroller.io
|
||||
* Docs: https://docs.datacontroller.io
|
||||
* Code: https://code.datacontroller.io
|
||||
|
||||
For support, contact support@4gl.io or reach out on [Matrix](https://matrix.to/#/#dc:4gl.io)!
|
||||
|
||||
## Development
|
||||
|
||||
### Lighthouse CI
|
||||
|
||||
This project includes automated Lighthouse performance and accessibility checks that run on pull requests. The checks ensure:
|
||||
|
||||
- **Accessibility Score**: Minimum 1.0 (100%) median score across all tested pages
|
||||
|
||||
The Lighthouse CI workflow:
|
||||
1. Sets up the development environment with SASjs server and mocked services
|
||||
2. Builds and serves the Angular frontend
|
||||
3. Installs Chrome and runs `lhci autorun` (Lighthouse CI) against key pages
|
||||
4. Uploads results as artifacts for review
|
||||
|
||||
To run Lighthouse checks locally:
|
||||
```bash
|
||||
cd client
|
||||
npm install
|
||||
npm run lighthouse
|
||||
```
|
||||
|
||||
Configuration is in `client/lighthouserc.js` (URL list, `desktop` preset, Chrome flags, assertions).
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Auto-generated at build time (node ./src/version.ts) — gitignored, never linted.
|
||||
# Formerly skipped via root .gitignore when prettier ran from the repo root;
|
||||
# now that lint runs from client/, prettier reads this CWD-local ignore instead.
|
||||
src/environments/version.ts
|
||||
+31
-39
@@ -25,6 +25,7 @@
|
||||
"options": {
|
||||
"allowedCommonJsDependencies": [
|
||||
"handsontable",
|
||||
"exceljs",
|
||||
"core-js",
|
||||
"pikaday",
|
||||
"querystring",
|
||||
@@ -41,35 +42,42 @@
|
||||
"zone.js",
|
||||
"text-encoding",
|
||||
"crypto-js/md5",
|
||||
"crypto-js/sha1",
|
||||
"crypto-js/sha512",
|
||||
"buffer",
|
||||
"numbro",
|
||||
"@clr/icons",
|
||||
"@sasjs/adapter",
|
||||
"@sasjs/utils/types/serverType",
|
||||
"@sasjs/utils/input/validators",
|
||||
"@sasjs/utils/utils/bytesToSize",
|
||||
"base64-arraybuffer",
|
||||
"@handsontable/formulajs"
|
||||
],
|
||||
"polyfills": [
|
||||
"src/polyfills.ts",
|
||||
"zone.js"
|
||||
],
|
||||
"polyfills": ["src/polyfills.ts", "zone.js"],
|
||||
"outputPath": "dist",
|
||||
"resourcesOutputPath": "images",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
"src/images"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "src/images",
|
||||
"output": "images",
|
||||
"ignore": ["spinner.svg", "caret.svg"]
|
||||
}
|
||||
],
|
||||
"styles": ["src/styles.scss"],
|
||||
"scripts": [
|
||||
"node_modules/@clr/icons/clr-icons.min.js",
|
||||
"node_modules/marked/marked.min.js"
|
||||
]
|
||||
"node_modules/marked/marked.min.js",
|
||||
{
|
||||
"input": "src/assets/va-early.js",
|
||||
"bundleName": "va-early",
|
||||
"inject": true
|
||||
}
|
||||
],
|
||||
"webWorkerTsConfig": "tsconfig.worker.json",
|
||||
"main": "src/main.ts"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
@@ -102,9 +110,7 @@
|
||||
}
|
||||
},
|
||||
"development": {
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"buildOptimizer": false,
|
||||
"sourceMap": true,
|
||||
"optimization": false,
|
||||
"namedChunks": true
|
||||
@@ -116,10 +122,10 @@
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "datacontroller:build:production"
|
||||
"buildTarget": "datacontroller:build:production"
|
||||
},
|
||||
"development": {
|
||||
"browserTarget": "datacontroller:build:development"
|
||||
"buildTarget": "datacontroller:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
@@ -127,40 +133,26 @@
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "datacontroller:build"
|
||||
"buildTarget": "datacontroller:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"polyfills": ["src/polyfills.ts", "zone.js", "zone.js/testing"],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"codeCoverage": true,
|
||||
"polyfills": [
|
||||
"src/polyfills.ts",
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": [
|
||||
|
||||
],
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"karmaConfig": "karma.conf.js"
|
||||
"assets": ["src/favicon.ico", "src/assets"],
|
||||
"styles": ["src/styles.scss"],
|
||||
"scripts": [],
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"webWorkerTsConfig": "tsconfig.worker.json"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.html"
|
||||
]
|
||||
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+29
-10
@@ -2,27 +2,46 @@ import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
reporter: 'mochawesome',
|
||||
|
||||
reporterOptions: {
|
||||
reportDir: 'cypress/results',
|
||||
overwrite: false,
|
||||
html: true,
|
||||
json: false,
|
||||
json: false
|
||||
},
|
||||
viewportHeight: 900,
|
||||
viewportWidth: 1600,
|
||||
|
||||
chromeWebSecurity: false,
|
||||
defaultCommandTimeout: 30000,
|
||||
|
||||
env: {
|
||||
hosturl:"http://localhost:4200",
|
||||
appLocation: "",
|
||||
site_id_SAS9: "70221618",
|
||||
site_id_SASVIYA: "70253615",
|
||||
site_id_SASJS: "123",
|
||||
serverType: "SASJS",
|
||||
libraryToOpenIncludes_SASVIYA: "viya",
|
||||
libraryToOpenIncludes_SAS9: "dc",
|
||||
libraryToOpenIncludes_SASJS: "dc",
|
||||
hosturl: 'http://localhost:4200',
|
||||
appLocation: '',
|
||||
site_id_SAS9: '70221618',
|
||||
site_id_SASVIYA: '70253615',
|
||||
site_id_SASJS: '123',
|
||||
serverType: 'SASJS',
|
||||
libraryToOpenIncludes_SASVIYA: 'viya',
|
||||
libraryToOpenIncludes_SAS9: 'dc',
|
||||
libraryToOpenIncludes_SASJS: 'dc',
|
||||
debug: false,
|
||||
screenshotOnRunFailure: false,
|
||||
longerCommandTimeout: 50000,
|
||||
testLicenceUserLimits: false
|
||||
},
|
||||
|
||||
e2e: {
|
||||
video: true,
|
||||
setupNodeEvents(on, config) {
|
||||
// Pin the browser locale so locale-formatted cells (intl-date/time/datetime)
|
||||
// render deterministically regardless of the runner's system locale.
|
||||
on('before:browser:launch', (browser, launchOptions) => {
|
||||
if (browser.family === 'chromium' && browser.name !== 'electron') {
|
||||
launchOptions.args.push('--lang=en-GB')
|
||||
}
|
||||
return launchOptions
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
// Marks this file as an ES module (rather than a global script) so its
|
||||
// top-level consts don't collide, under the TS type-checker, with the same
|
||||
// names declared in other spec files — see viewer-labels.cy.ts for the same
|
||||
// pattern.
|
||||
export {}
|
||||
|
||||
const username = Cypress.env('username')
|
||||
const password = Cypress.env('password')
|
||||
const hostUrl = Cypress.env('hosturl')
|
||||
const appLocation = Cypress.env('appLocation')
|
||||
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
||||
const serverType = Cypress.env('serverType')
|
||||
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
|
||||
|
||||
// Regression coverage for issue #253's vertical-array COLTYPE fix — see
|
||||
// getdata.sas now sends one COLTYPE JSON-object string per cols[] row (via
|
||||
// a LEFT JOIN keyed off the real dataset's columns), instead of one
|
||||
// comma-joined sasparams.COLTYPE string covering every column. Two things
|
||||
// are specifically at risk from that change and aren't covered elsewhere:
|
||||
// 1. `_____DELETE__THIS__RECORD_____` (the delete checkbox column) is a
|
||||
// client-side-only concept — %mp_getcols never has a row for it, so
|
||||
// it can no longer travel via cols[].COLTYPE at all. Its Yes/No
|
||||
// dropdown rule is now hardcoded client-side (deleteRecordColumnRule.ts)
|
||||
// — test 1 proves that hardcode actually renders end to end.
|
||||
// 2. Ordinary columns (e.g. SOME_DROPDOWN) still get their rule via the
|
||||
// new per-row cols[].COLTYPE path — test 2 proves the migration didn't
|
||||
// silently drop real columns' rules either.
|
||||
// Column positions below come from COLHEADERS in
|
||||
// sas/mocks/sasjs/services/editors/getdata.js:
|
||||
// "_____DELETE__THIS__RECORD_____,PRIMARY_KEY_FIELD,SOME_CHAR,SOME_DROPDOWN,..."
|
||||
// — childNodes[0] on a body <tr> is the row-header <th>, so childNodes[N+1]
|
||||
// is the Nth (0-indexed) column in that list.
|
||||
context('coltype vertical-array regression tests: ', function () {
|
||||
this.beforeAll(() => {
|
||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
cy.loginAndUpdateValidKey(true)
|
||||
})
|
||||
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
visitPage('home')
|
||||
})
|
||||
|
||||
it('1 | delete-record column renders its hardcoded Yes/No dropdown', () => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
clickOnEdit()
|
||||
|
||||
cy.get('.ht_master tbody tr', { timeout: longerCommandTimeout }).then(
|
||||
(rows: any) => {
|
||||
// childNodes[1] = _____DELETE__THIS__RECORD_____ (1st column)
|
||||
cy.get(rows[1].childNodes[1])
|
||||
.click({ force: true })
|
||||
.then(($td) => {
|
||||
cy.get('.htAutocompleteArrow', { withinSubject: $td })
|
||||
.should('exist')
|
||||
.click({ force: true })
|
||||
|
||||
cy.get('.autocompleteEditor .htCore tbody td').should(
|
||||
($choices) => {
|
||||
const texts = [...$choices].map((el) => el.innerText.trim())
|
||||
expect(texts).to.deep.equal(['No', 'Yes'])
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('2 | an ordinary dropdown column (SOME_DROPDOWN) still gets its rule from cols[].COLTYPE', () => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
clickOnEdit()
|
||||
|
||||
cy.get('.ht_master tbody tr', { timeout: longerCommandTimeout }).then(
|
||||
(rows: any) => {
|
||||
// childNodes[4] = SOME_DROPDOWN (4th column, 0-indexed as 3). This
|
||||
// only holds if DcValidator emits rules in VARNUM order — see the
|
||||
// "orders rules by VARNUM" regression test in dc-validator.spec.ts.
|
||||
cy.get(rows[1].childNodes[4])
|
||||
.click({ force: true })
|
||||
.then(($td) => {
|
||||
cy.get('.htAutocompleteArrow', { withinSubject: $td }).should(
|
||||
'exist'
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const clickOnEdit = (callback?: any) => {
|
||||
cy.get('.btnCtrl button.btn-primary', { timeout: longerCommandTimeout })
|
||||
.click()
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
}
|
||||
|
||||
const openTableFromTree = (libNameIncludes: string, tablename: string) => {
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout })
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then((treeNodes: any) => {
|
||||
let viyaLib
|
||||
|
||||
for (let node of treeNodes) {
|
||||
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
|
||||
viyaLib = node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cy.get(viyaLib).within(() => {
|
||||
cy.get('.clr-tree-node-content-container > button').click()
|
||||
|
||||
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
||||
for (let innerNode of innerNodes) {
|
||||
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
||||
innerNode.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const visitPage = (url: string) => {
|
||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
const username = Cypress.env('username')
|
||||
const password = Cypress.env('password')
|
||||
const hostUrl = Cypress.env('hosturl')
|
||||
const appLocation = Cypress.env('appLocation')
|
||||
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
||||
const serverType = Cypress.env('serverType')
|
||||
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
|
||||
const fixturePath = 'csvs/'
|
||||
|
||||
context('csv file upload restriction (free tier): ', function () {
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
|
||||
cy.get('body').then(($body) => {
|
||||
const usernameInput = $body.find('input.username')[0]
|
||||
|
||||
if (usernameInput && !Cypress.dom.isHidden(usernameInput)) {
|
||||
cy.get('input.username').type(username)
|
||||
cy.get('input.password').type(password)
|
||||
cy.get('.login-group button').click()
|
||||
}
|
||||
})
|
||||
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout }).should(
|
||||
'not.exist'
|
||||
)
|
||||
|
||||
// Skip licensing page if presented - continue with free tier
|
||||
cy.url().then((url) => {
|
||||
if (url.includes('licensing')) {
|
||||
cy.get('button').contains('Continue with free tier').click()
|
||||
}
|
||||
})
|
||||
|
||||
visitPage('home')
|
||||
})
|
||||
|
||||
it('1 | File upload is restricted on free tier', () => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
// Click upload button - should show feature locked modal
|
||||
cy.get('.buttonBar button:last-child').should('exist').click()
|
||||
|
||||
cy.get('.modal-title').should('contain', 'Locked Feature (File Upload)')
|
||||
})
|
||||
})
|
||||
|
||||
const openTableFromTree = (libNameIncludes: string, tablename: string) => {
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout })
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then((treeNodes: any) => {
|
||||
let targetLib
|
||||
|
||||
for (let node of treeNodes) {
|
||||
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
|
||||
targetLib = node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cy.get(targetLib).within(() => {
|
||||
cy.get('.clr-tree-node-content-container > button').click()
|
||||
|
||||
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
||||
for (let innerNode of innerNodes) {
|
||||
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
||||
innerNode.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const attachFile = (filename: string, callback?: any) => {
|
||||
cy.get('.buttonBar button:last-child')
|
||||
.should('exist')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('input[type="file"]#file-upload')
|
||||
.attachFile(`/${fixturePath}/${filename}`)
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const visitPage = (url: string) => {
|
||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Callbacks } from 'cypress/types/jquery/index'
|
||||
|
||||
const username = Cypress.env('username')
|
||||
const password = Cypress.env('password')
|
||||
const hostUrl = Cypress.env('hosturl')
|
||||
const appLocation = Cypress.env('appLocation')
|
||||
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
||||
const serverType = Cypress.env('serverType')
|
||||
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
|
||||
const fixturePath = 'csvs/'
|
||||
|
||||
context('excel tests: ', function () {
|
||||
this.beforeAll(() => {
|
||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
cy.loginAndUpdateValidKey(true)
|
||||
})
|
||||
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
|
||||
visitPage('home')
|
||||
|
||||
colorLog(
|
||||
`TEST START ---> ${
|
||||
Cypress.mocha.getRunner().suite.ctx.currentTest.title
|
||||
}`,
|
||||
'#3498DB'
|
||||
)
|
||||
})
|
||||
|
||||
it('1 | Uploads regular csv file', () => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
attachExcelFile('regular.csv', () => {
|
||||
cy.get('#approval-btn', { timeout: 60000 }).should('be.visible')
|
||||
// .then(() => {
|
||||
// cy.get('#hotInstance', { timeout: 30000 })
|
||||
// .find('div.ht_master.handsontable')
|
||||
// .find('div.wtHolder')
|
||||
// .find('div.wtHider')
|
||||
// .find('div.wtSpreader')
|
||||
// .find('table.htCore')
|
||||
// .find('tbody')
|
||||
// .then((data) => {
|
||||
// let cell: any = data[0].children[0].children[1]
|
||||
// expect(cell.innerText).to.equal('0')
|
||||
// cell = data[0].children[0].children[2]
|
||||
// expect(cell.innerText).to.equal('44')
|
||||
// cell = data[0].children[0].children[3]
|
||||
// expect(cell.innerText).to.equal('abc')
|
||||
// cell = data[0].children[0].children[6]
|
||||
// expect(cell.innerText).to.equal('Option abc')
|
||||
// })
|
||||
// })
|
||||
})
|
||||
})
|
||||
|
||||
this.afterEach(() => {
|
||||
colorLog(`TEST END -------------`, '#3498DB')
|
||||
})
|
||||
})
|
||||
|
||||
const openTableFromTree = (libNameIncludes: string, tablename: string) => {
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout })
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then((treeNodes: any) => {
|
||||
let viyaLib
|
||||
|
||||
for (let node of treeNodes) {
|
||||
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
|
||||
viyaLib = node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cy.get(viyaLib).within(() => {
|
||||
cy.get('.clr-tree-node-content-container > button').click()
|
||||
|
||||
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
||||
for (let innerNode of innerNodes) {
|
||||
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
||||
innerNode.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const attachExcelFile = (excelFilename: string, callback?: any) => {
|
||||
cy.get('.buttonBar button:last-child')
|
||||
.should('exist')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('input[type="file"]#file-upload')
|
||||
.attachFile(`/${fixturePath}/${excelFilename}`)
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const visitPage = (url: string) => {
|
||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
||||
}
|
||||
|
||||
const colorLog = (msg: string, color: string) => {
|
||||
console.log('%c' + msg, 'color:' + color + ';font-weight:bold;')
|
||||
}
|
||||
@@ -5,7 +5,6 @@ const appLocation = Cypress.env('appLocation')
|
||||
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
||||
const serverType = Cypress.env('serverType')
|
||||
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
|
||||
const fixturePath = 'excels_general/'
|
||||
const downloadsFolder = Cypress.config('downloadsFolder')
|
||||
|
||||
import { deleteDownloadsFolder } from '../util/deleteDownloadFolder'
|
||||
@@ -169,10 +168,6 @@ context('download files test: ', function () {
|
||||
this.afterEach(() => {
|
||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
})
|
||||
|
||||
this.afterAll(() => {
|
||||
cy.visit(`https://sas.4gl.io/mihmed/cypress_finish`)
|
||||
})
|
||||
})
|
||||
|
||||
const visitPage = (url: string) => {
|
||||
@@ -15,9 +15,6 @@ context('editor tests: ', function () {
|
||||
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
// cy.get('input.username').type(username)
|
||||
// cy.get('input.password').type(password)
|
||||
// cy.get('.login-group button').click()
|
||||
|
||||
visitPage('home')
|
||||
})
|
||||
@@ -118,10 +115,6 @@ context('editor tests: ', function () {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
this.afterEach(() => {
|
||||
// cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
})
|
||||
})
|
||||
|
||||
const clickOnEdit = (callback?: any) => {
|
||||
@@ -221,14 +214,10 @@ const submitExcel = (callback?: any) => {
|
||||
|
||||
const rejectExcel = (callback?: any) => {
|
||||
cy.get('button', { timeout: longerCommandTimeout })
|
||||
.should('contain', 'Go to approvals screen')
|
||||
.should('contain', 'Approve')
|
||||
.then((allButtons: any) => {
|
||||
for (let approvalButton of allButtons) {
|
||||
if (
|
||||
approvalButton.innerText
|
||||
.toLowerCase()
|
||||
.includes('go to approvals screen')
|
||||
) {
|
||||
if (approvalButton.innerText.toLowerCase().includes('approve')) {
|
||||
approvalButton.click()
|
||||
break
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
const hostUrl = Cypress.env('hosturl')
|
||||
const appLocation = Cypress.env('appLocation')
|
||||
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
||||
const serverType = Cypress.env('serverType')
|
||||
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
|
||||
|
||||
// SAS Visual Analytics data-driven content embed mode (`?embed=va`):
|
||||
// - chrome is hidden (same as embed=true),
|
||||
// - the editor opens in edit mode immediately (not read-only),
|
||||
// - the only action button is a single Submit below the grid (stubbed in v1),
|
||||
// - the top button bar and Add Record are hidden.
|
||||
//
|
||||
// The detailed PK+label data-merge logic is covered by the VaMessagingService
|
||||
// unit spec; here we only assert the deterministic UI layout and that pushing a
|
||||
// VA postMessage does not break the page.
|
||||
context('embed=va (VA data-driven content) tests: ', function () {
|
||||
this.beforeAll(() => {
|
||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
cy.loginAndUpdateValidKey()
|
||||
})
|
||||
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
visitPage('home')
|
||||
})
|
||||
|
||||
it('1 | opens the editor in VA mode: chrome hidden, editable, single bottom Submit', () => {
|
||||
openEditorInVaMode('mpe_x_test', () => {
|
||||
// Chrome hidden (same as embed=true)
|
||||
cy.get('header.app-header').should('not.exist')
|
||||
|
||||
// Grid present and in edit mode (read-only Filter/Edit/Upload bar absent)
|
||||
cy.get('#hotTable', { timeout: longerCommandTimeout }).should('exist')
|
||||
cy.get('.btnCtrl button').contains('Edit').should('not.exist')
|
||||
cy.get('.btnCtrl button').contains('Filter').should('not.exist')
|
||||
|
||||
// Top edit-mode buttons hidden in VA mode
|
||||
cy.get('.btnCtrl button.btn-outline-danger').should('not.exist') // Cancel
|
||||
cy.get('.btnCtrl').contains('Add Row').should('not.exist')
|
||||
|
||||
// Bottom Add Record hidden
|
||||
cy.contains('button', 'Add Record').should('not.exist')
|
||||
|
||||
// Exactly one Submit button, below the grid, disabled in v1
|
||||
cy.get('button').filter(':contains("Submit")').should('have.length', 1)
|
||||
cy.get('button').contains('Submit').should('be.disabled')
|
||||
})
|
||||
})
|
||||
|
||||
it('2 | accepts a VA postMessage and stays in VA mode', () => {
|
||||
// A VA message drives column visibility + filtering; an empty/unmatched
|
||||
// message reloads the editor (unfiltered) while preserving ?embed=va.
|
||||
openEditorInVaMode('mpe_x_test', () => {
|
||||
cy.window().then((win) => {
|
||||
win.postMessage(
|
||||
{
|
||||
version: '1',
|
||||
resultName: 'dd1',
|
||||
rowCount: 0,
|
||||
availableRowCount: 0,
|
||||
data: [],
|
||||
columns: [],
|
||||
parameters: []
|
||||
},
|
||||
'*'
|
||||
)
|
||||
})
|
||||
|
||||
// Editor still loads in VA mode after the message (chrome hidden, grid up)
|
||||
cy.get('#hotTable', { timeout: longerCommandTimeout }).should('exist')
|
||||
cy.get('header.app-header').should('not.exist')
|
||||
cy.get('button').contains('Submit').should('exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Opens a table from the tree (which routes to #/editor/LIB.TABLE), then
|
||||
// re-visits the same route with ?embed=va so the app parses VA embed mode.
|
||||
const openEditorInVaMode = (tablename: string, callback?: any) => {
|
||||
openTableFromTree(libraryToOpenIncludes, tablename)
|
||||
|
||||
cy.get('#hotTable', { timeout: longerCommandTimeout })
|
||||
.should('exist')
|
||||
.then(() => {
|
||||
cy.url().then((url) => {
|
||||
const separator = url.includes('?') ? '&' : '?'
|
||||
cy.visit(`${url}${separator}embed=va`)
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout }).should(
|
||||
'not.exist'
|
||||
)
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const openTableFromTree = (libNameIncludes: string, tablename: string) => {
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout })
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then((treeNodes: any) => {
|
||||
let viyaLib
|
||||
|
||||
for (let node of treeNodes) {
|
||||
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
|
||||
viyaLib = node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cy.get(viyaLib).within(() => {
|
||||
cy.get('.clr-tree-node-content-container > button').click()
|
||||
|
||||
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
||||
for (let innerNode of innerNodes) {
|
||||
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
||||
innerNode.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const visitPage = (url: string) => {
|
||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
import { Callbacks } from 'cypress/types/jquery/index'
|
||||
|
||||
const username = Cypress.env('username')
|
||||
const password = Cypress.env('password')
|
||||
const hostUrl = Cypress.env('hosturl')
|
||||
const appLocation = Cypress.env('appLocation')
|
||||
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
||||
const serverType = Cypress.env('serverType')
|
||||
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
|
||||
const fixturePath = 'excels_multi_load/'
|
||||
|
||||
const library = 'DC996664'
|
||||
const mpeXTestTable = 'MPE_X_TEST'
|
||||
const mpeTablesTable = 'MPE_TABLES'
|
||||
|
||||
context('excel multi load tests: ', function () {
|
||||
this.beforeAll(() => {
|
||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
cy.loginAndUpdateValidKey(true)
|
||||
})
|
||||
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
|
||||
visitPage('home/multi-load')
|
||||
|
||||
colorLog(
|
||||
`TEST START ---> ${
|
||||
Cypress.mocha.getRunner().suite.ctx.currentTest.title
|
||||
}`,
|
||||
'#3498DB'
|
||||
)
|
||||
})
|
||||
|
||||
it('1 | Uploads Excel file with multiple sheets, 3 sheets including data, 2 sheets matched with dataset', (done) => {
|
||||
attachExcelFile('multi_load_test_2.xlsx', () => {
|
||||
checkHotUserDatasetTable(
|
||||
'hotTableUserDataset',
|
||||
[
|
||||
[library, mpeXTestTable],
|
||||
[library, mpeTablesTable]
|
||||
],
|
||||
() => {
|
||||
cy.get('#continue-btn')
|
||||
.trigger('click')
|
||||
.then(() => {
|
||||
checkIfTreeHasTables(
|
||||
[`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`],
|
||||
undefined,
|
||||
(includes: boolean) => {
|
||||
if (includes) {
|
||||
// MPE_TABLES sheet does not have data so 1 error image must be shown
|
||||
hasErrorTables(1, (valid: boolean) => {
|
||||
if (valid) done()
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('2 | Uploads Excel file with multiple sheets, 2 sheets matched with dataset, 1 matched sheet does not have data', (done) => {
|
||||
attachExcelFile('multi_load_test_1.xlsx', () => {
|
||||
checkHotUserDatasetTable(
|
||||
'hotTableUserDataset',
|
||||
[
|
||||
[library, mpeXTestTable],
|
||||
[library, mpeTablesTable]
|
||||
],
|
||||
() => {
|
||||
cy.get('#continue-btn')
|
||||
.trigger('click')
|
||||
.then(() => {
|
||||
checkIfTreeHasTables(
|
||||
[`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`],
|
||||
`${library}.${mpeXTestTable}`,
|
||||
(includes: boolean) => {
|
||||
if (includes) {
|
||||
cy.get('#hotTable')
|
||||
.should('be.visible')
|
||||
.then(() => {
|
||||
checkHotUserDatasetTable(
|
||||
'hotTable',
|
||||
[
|
||||
['No', '1', 'more dummy data'],
|
||||
[
|
||||
'No',
|
||||
'1',
|
||||
'It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told:'
|
||||
],
|
||||
[
|
||||
'No',
|
||||
'1',
|
||||
'if you can fill the unforgiving minute'
|
||||
]
|
||||
],
|
||||
() => {
|
||||
submitTables()
|
||||
|
||||
hasSuccessSubmits(2, (valid: boolean) => {
|
||||
if (valid) done()
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('3 | Uploads Excel file with multiple sheets, 1 sheets has 2 tables', (done) => {
|
||||
attachExcelFile('multi_load_test_1.xlsx', () => {
|
||||
checkHotUserDatasetTable(
|
||||
'hotTableUserDataset',
|
||||
[
|
||||
[library, mpeXTestTable],
|
||||
[library, mpeTablesTable]
|
||||
],
|
||||
() => {
|
||||
cy.get('#continue-btn')
|
||||
.trigger('click')
|
||||
.then(() => {
|
||||
checkIfTreeHasTables(
|
||||
[`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`],
|
||||
`${library}.${mpeXTestTable}`,
|
||||
(includes: boolean) => {
|
||||
if (includes) {
|
||||
cy.get('#hotTable')
|
||||
.should('be.visible')
|
||||
.then(() => {
|
||||
checkHotUserDatasetTable(
|
||||
'hotTable',
|
||||
[
|
||||
['No', '1', 'more dummy data'],
|
||||
[
|
||||
'No',
|
||||
'1',
|
||||
'It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told:'
|
||||
],
|
||||
[
|
||||
'No',
|
||||
'1',
|
||||
'if you can fill the unforgiving minute'
|
||||
]
|
||||
],
|
||||
() => {
|
||||
clickOnTreeNode('DC996664.MPE_TABLES', () => {
|
||||
cy.wait(1000).then(() => {
|
||||
cy.get('#hotTable')
|
||||
.should('be.visible')
|
||||
.then(() => {
|
||||
checkHotUserDatasetTable(
|
||||
'hotTable',
|
||||
[
|
||||
[
|
||||
'No',
|
||||
'DC914286',
|
||||
'MPE_COLUMN_LEVEL_SECURITY'
|
||||
],
|
||||
['No', 'DC914286', 'MPE_XLMAP_INFO'],
|
||||
['No', 'DC914286', 'MPE_XLMAP_RULES']
|
||||
],
|
||||
() => {
|
||||
submitTables()
|
||||
|
||||
hasSuccessSubmits(
|
||||
2,
|
||||
(valid: boolean) => {
|
||||
if (valid) done()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
this.afterEach(() => {
|
||||
colorLog(`TEST END -------------`, '#3498DB')
|
||||
})
|
||||
})
|
||||
|
||||
const attachExcelFile = (excelFilename: string, callback?: any) => {
|
||||
cy.get('#browse-file')
|
||||
.should('exist')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('input[type="file"]#file-upload')
|
||||
.attachFile(`/${fixturePath}/${excelFilename}`)
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const checkHotUserDatasetTable = (
|
||||
hotId: string,
|
||||
dataToContain: any[][],
|
||||
callback?: () => void
|
||||
) => {
|
||||
cy.get(`#${hotId}`, { timeout: longerCommandTimeout })
|
||||
.find('div.ht_master.handsontable')
|
||||
.find('div.wtHolder')
|
||||
.find('div.wtHider')
|
||||
.find('div.wtSpreader')
|
||||
.find('table.htCore')
|
||||
.find('tbody')
|
||||
.then((data) => {
|
||||
cy.wait(2000).then(() => {
|
||||
for (let rowI = 0; rowI < dataToContain.length; rowI++) {
|
||||
for (let colI = 0; colI < dataToContain[rowI].length; colI++) {
|
||||
expect(data[0].children[rowI].children[colI]).to.contain(
|
||||
dataToContain[rowI][colI]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const clickOnTreeNode = (clickOnNode: string, callback?: () => void) => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node').then((treeNodes: any) => {
|
||||
for (let node of treeNodes) {
|
||||
if (node.innerText.toUpperCase().trim().includes(clickOnNode)) {
|
||||
cy.get(node).trigger('click')
|
||||
if (callback) callback()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const checkIfTreeHasTables = (
|
||||
tables: string[],
|
||||
clickOnNode?: string,
|
||||
callback?: (includes: boolean) => void
|
||||
) => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node').then((treeNodes: any) => {
|
||||
let datasets = tables
|
||||
let nodesCorrect = true
|
||||
let nodeToClick
|
||||
|
||||
for (let node of treeNodes) {
|
||||
if (!datasets.includes(node.innerText.toUpperCase().trim())) {
|
||||
nodesCorrect = false
|
||||
}
|
||||
|
||||
if (clickOnNode) {
|
||||
if (node.innerText.toUpperCase().trim().includes(clickOnNode)) {
|
||||
nodeToClick = node
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeToClick) {
|
||||
cy.wait(1000)
|
||||
cy.get(nodeToClick).trigger('click')
|
||||
}
|
||||
|
||||
if (callback) callback(nodesCorrect)
|
||||
})
|
||||
}
|
||||
|
||||
const submitTables = () => {
|
||||
cy.get('#submit-all').trigger('click')
|
||||
cy.get('#submit-tables').trigger('click')
|
||||
cy.wait(1000)
|
||||
}
|
||||
|
||||
const hasSuccessSubmits = (
|
||||
expectedNoOfSubmits: number,
|
||||
callback: (valid: boolean) => void
|
||||
) => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node cds-icon[status="success"]')
|
||||
.should('be.visible')
|
||||
.then(($nodes) => {
|
||||
callback(expectedNoOfSubmits === $nodes.length)
|
||||
})
|
||||
}
|
||||
|
||||
const hasErrorTables = (
|
||||
expectedNoOfErrors: number,
|
||||
callback: (valid: boolean) => void
|
||||
) => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node cds-icon[status="danger"]')
|
||||
.should('be.visible')
|
||||
.then(($nodes) => {
|
||||
callback(expectedNoOfErrors === $nodes.length)
|
||||
})
|
||||
}
|
||||
|
||||
const visitPage = (url: string) => {
|
||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
||||
}
|
||||
|
||||
const colorLog = (msg: string, color: string) => {
|
||||
console.log('%c' + msg, 'color:' + color + ';font-weight:bold;')
|
||||
}
|
||||
@@ -9,19 +9,14 @@ const serverType = Cypress.env('serverType')
|
||||
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
|
||||
const fixturePath = 'excels/'
|
||||
|
||||
// TODO: 4 and 9 failing
|
||||
|
||||
context('excel tests: ', function () {
|
||||
this.beforeAll(() => {
|
||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
cy.loginAndUpdateValidKey()
|
||||
cy.loginAndUpdateValidKey(true)
|
||||
})
|
||||
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
// cy.get('input.username').type(username)
|
||||
// cy.get('input.password').type(password)
|
||||
// cy.get('.login-group button').click()
|
||||
|
||||
visitPage('home')
|
||||
|
||||
@@ -114,32 +109,27 @@ context('excel tests: ', function () {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
attachExcelFile('duplicate_column_excel.xlsx', () => {
|
||||
cy.get('.abortMsg', { timeout: longerCommandTimeout })
|
||||
.should('exist')
|
||||
.then((elements: any) => {
|
||||
if (elements[0]) {
|
||||
if (elements[0].innerText.toLowerCase().includes('missing')) done()
|
||||
}
|
||||
})
|
||||
submitExcel()
|
||||
rejectExcel(done)
|
||||
})
|
||||
})
|
||||
|
||||
// it('9 | Uploads Excel with a duplicate row', (done) => {
|
||||
// openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
it('9 | Uploads Excel with a duplicate row', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
// attachExcelFile('duplicate_row_excel.xlsx', () => {
|
||||
// submitExcel(() => {
|
||||
// cy.get('.abortMsg', { timeout: longerCommandTimeout })
|
||||
// .should('exist')
|
||||
// .then((elements: any) => {
|
||||
// if (elements[0]) {
|
||||
// if (elements[0].innerText.toLowerCase().includes('duplicates'))
|
||||
// done()
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
attachExcelFile('duplicate_row_excel.xlsx', () => {
|
||||
submitExcel(() => {
|
||||
cy.get('.duplicate-keys-modal', { timeout: longerCommandTimeout })
|
||||
.should('exist')
|
||||
.then((elements: any) => {
|
||||
if (elements[0]) {
|
||||
if (elements[0].innerText.toLowerCase().includes('duplicates'))
|
||||
done()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('10 | Uploads Excel with a mixed content', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
@@ -244,14 +234,14 @@ context('excel tests: ', function () {
|
||||
cy.get('.btn-upload-preview', { timeout: 60000 })
|
||||
.should('be.visible')
|
||||
.then(() => {
|
||||
cy.get('#hotInstance', { timeout: 30000 })
|
||||
cy.get('#hotTable', { timeout: 30000 })
|
||||
.find('div.ht_master.handsontable')
|
||||
.find('div.wtHolder')
|
||||
.find('div.wtHider')
|
||||
.find('div.wtSpreader')
|
||||
.find('table.htCore')
|
||||
.find('tbody')
|
||||
.should((data) => {
|
||||
.then((data) => {
|
||||
let allEmpty = true
|
||||
|
||||
for (let col = 0; col < data[0].children.length; col++) {
|
||||
@@ -293,14 +283,14 @@ context('excel tests: ', function () {
|
||||
cy.get('.btn-upload-preview', { timeout: 60000 })
|
||||
.should('be.visible')
|
||||
.then(() => {
|
||||
cy.get('#hotInstance', { timeout: 30000 })
|
||||
cy.get('#hotTable', { timeout: 30000 })
|
||||
.find('div.ht_master.handsontable')
|
||||
.find('div.wtHolder')
|
||||
.find('div.wtHider')
|
||||
.find('div.wtSpreader')
|
||||
.find('table.htCore')
|
||||
.find('tbody')
|
||||
.should((data: JQuery<HTMLTableSectionElement>) => {
|
||||
.then((data: JQuery<HTMLTableSectionElement>) => {
|
||||
const firstRowFirstCol: Partial<HTMLElement> =
|
||||
data[0].children[0].children[1]
|
||||
|
||||
@@ -319,6 +309,96 @@ context('excel tests: ', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('22 | Uploads password protected Excel and unlocks with correct password', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
cy.get('.buttonBar button:last-child')
|
||||
.should('exist')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('input[type="file"]#file-upload')
|
||||
.attachFile(`/${fixturePath}/regular_excel_password.xlsx`)
|
||||
.then(() => {
|
||||
// Wait for password modal to appear
|
||||
cy.get('#filePasswordInput', { timeout: 10000 })
|
||||
.should('be.visible')
|
||||
.type('123123')
|
||||
|
||||
// Click Unlock button
|
||||
cy.get('.btn.btn-success-outline').should('not.be.disabled').click()
|
||||
|
||||
// Click away the overlay
|
||||
cy.get('.modal-footer .btn.btn-primary', { timeout: 5000 }).click()
|
||||
|
||||
// Verify file loads successfully
|
||||
cy.get('.btn-upload-preview', { timeout: 60000 })
|
||||
.should('be.visible')
|
||||
.then(() => {
|
||||
submitExcel()
|
||||
rejectExcel(done)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('23 | Uploads password protected Excel and handles wrong password', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
cy.get('.buttonBar button:last-child')
|
||||
.should('exist')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('input[type="file"]#file-upload')
|
||||
.attachFile(`/${fixturePath}/regular_excel_password.xlsx`)
|
||||
.then(() => {
|
||||
// First attempt: Enter wrong password
|
||||
cy.get('#filePasswordInput', { timeout: 10000 })
|
||||
.should('be.visible')
|
||||
.type('wrongpassword')
|
||||
|
||||
cy.get('.btn.btn-success-outline').should('not.be.disabled').click()
|
||||
|
||||
// Verify error message appears
|
||||
cy.get('.modal-footer .color-red', { timeout: 10000 })
|
||||
.should('be.visible')
|
||||
.should('contain', "Sorry that didn't work, try again.")
|
||||
|
||||
// Modal should still be open for retry
|
||||
cy.get('#filePasswordInput')
|
||||
.should('be.visible')
|
||||
.clear()
|
||||
.type('123123')
|
||||
|
||||
// Second attempt: Enter correct password
|
||||
cy.get('.btn.btn-success-outline').should('not.be.disabled').click()
|
||||
|
||||
// Click away the overlay
|
||||
cy.get('.modal-footer .btn.btn-primary', { timeout: 5000 }).click()
|
||||
|
||||
// Verify file loads successfully
|
||||
cy.get('.btn-upload-preview', { timeout: 60000 })
|
||||
.should('be.visible')
|
||||
.then(() => {
|
||||
submitExcel()
|
||||
rejectExcel(done)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('24 | Uploads Excel with leading whitespace in header row (should still succeed)', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
// Every header except the first has a leading space, matching a
|
||||
// real-world defect from copy-pasting between spreadsheets/systems.
|
||||
// Header matching must tolerate
|
||||
// this rather than reporting the columns missing and aborting.
|
||||
attachExcelFile('leading_whitespace_header_excel.xlsx', () => {
|
||||
submitExcel()
|
||||
rejectExcel(done)
|
||||
})
|
||||
})
|
||||
|
||||
// Large files break Cypress
|
||||
|
||||
// it ('? | Uploads Excel with size of 5MB', (done) => {
|
||||
@@ -339,7 +419,6 @@ context('excel tests: ', function () {
|
||||
|
||||
this.afterEach(() => {
|
||||
colorLog(`TEST END -------------`, '#3498DB')
|
||||
// cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -407,14 +486,10 @@ const submitExcel = (callback?: any) => {
|
||||
|
||||
const rejectExcel = (callback?: any) => {
|
||||
cy.get('button', { timeout: longerCommandTimeout })
|
||||
.should('contain', 'Go to approvals screen')
|
||||
.should('contain', 'Approve')
|
||||
.then((allButtons: any) => {
|
||||
for (let approvalButton of allButtons) {
|
||||
if (
|
||||
approvalButton.innerText
|
||||
.toLowerCase()
|
||||
.includes('go to approvals screen')
|
||||
) {
|
||||
if (approvalButton.innerText.toLowerCase().includes('approve')) {
|
||||
approvalButton.click()
|
||||
break
|
||||
}
|
||||
@@ -440,14 +515,10 @@ const rejectExcel = (callback?: any) => {
|
||||
|
||||
const acceptExcel = (callback?: any) => {
|
||||
cy.get('button', { timeout: longerCommandTimeout })
|
||||
.should('contain', 'Go to approvals screen')
|
||||
.should('contain', 'Approve')
|
||||
.then((allButtons: any) => {
|
||||
for (let approvalButton of allButtons) {
|
||||
if (
|
||||
approvalButton.innerText
|
||||
.toLowerCase()
|
||||
.includes('go to approvals screen')
|
||||
) {
|
||||
if (approvalButton.innerText.toLowerCase().includes('approve')) {
|
||||
approvalButton.click()
|
||||
break
|
||||
}
|
||||
@@ -466,14 +537,14 @@ const acceptExcel = (callback?: any) => {
|
||||
}
|
||||
|
||||
const checkResultOfFormulaUpload = (callback?: any) => {
|
||||
cy.get('#hotInstance', { timeout: longerCommandTimeout })
|
||||
cy.get('#hotTable', { timeout: longerCommandTimeout })
|
||||
.find('div.ht_master.handsontable')
|
||||
.find('div.wtHolder')
|
||||
.find('div.wtHider')
|
||||
.find('div.wtSpreader')
|
||||
.find('table.htCore')
|
||||
.find('tbody')
|
||||
.should((data) => {
|
||||
.then((data) => {
|
||||
const cell: any = data[0].children[0].children[5]
|
||||
expect(cell.innerText).to.equal('=1+1')
|
||||
if (callback) callback()
|
||||
@@ -481,15 +552,17 @@ const checkResultOfFormulaUpload = (callback?: any) => {
|
||||
}
|
||||
|
||||
const checkResultOfXLSUpload = (callback?: any) => {
|
||||
cy.viewport(1280, 720)
|
||||
cy.get('#hotInstance', { timeout: 30000 })
|
||||
// Config-default width — wide enough that the date/datetime/time columns are
|
||||
// not virtualized away (the old 1280 width hid the trailing time columns).
|
||||
cy.viewport(1600, 900)
|
||||
cy.get('#hotTable', { timeout: 30000 })
|
||||
.find('div.ht_master.handsontable')
|
||||
.find('div.wtHolder')
|
||||
.find('div.wtHider')
|
||||
.find('div.wtSpreader')
|
||||
.find('table.htCore')
|
||||
.find('tbody')
|
||||
.should((data) => {
|
||||
.then((data) => {
|
||||
let cell: any = data[0].children[0].children[2]
|
||||
|
||||
expect(cell.innerText).to.equal('0')
|
||||
@@ -499,19 +572,20 @@ const checkResultOfXLSUpload = (callback?: any) => {
|
||||
expect(cell.innerText).to.equal('▼\nOption 1')
|
||||
cell = data[0].children[0].children[5]
|
||||
expect(cell.innerText).to.equal('42')
|
||||
// Cells render the raw stored ISO string verbatim (locale-independent;
|
||||
// see registerIntlCellTypes). date -> YYYY-MM-DD, datetime ->
|
||||
// YYYY-MM-DD HH:mm:ss, time -> HH:mm:ss.
|
||||
cell = data[0].children[0].children[6]
|
||||
expect(cell.innerText).to.equal('▼\n1960-02-12')
|
||||
expect(cell.innerText).to.equal('1960-02-12')
|
||||
cell = data[0].children[0].children[7]
|
||||
expect(cell.innerText).to.equal('▼\n1960-01-01 00:00:42')
|
||||
expect(cell.innerText).to.equal('1960-01-01 00:00:42')
|
||||
cell = data[0].children[0].children[8]
|
||||
expect(cell.innerText).to.equal('00:00:42')
|
||||
cell = data[0].children[0].children[9]
|
||||
expect(cell.innerText).to.equal('3')
|
||||
|
||||
if (callback) callback()
|
||||
})
|
||||
|
||||
cy.get('#hotInstance', { timeout: 30000 })
|
||||
cy.get('#hotTable', { timeout: 30000 })
|
||||
.find('div.ht_master.handsontable')
|
||||
.find('div.wtHolder')
|
||||
.scrollTo('right')
|
||||
@@ -519,7 +593,7 @@ const checkResultOfXLSUpload = (callback?: any) => {
|
||||
.find('div.wtSpreader')
|
||||
.find('table.htCore')
|
||||
.find('tbody')
|
||||
.should((data) => {
|
||||
.then((data) => {
|
||||
let cell: any = data[0].children[0].children[1]
|
||||
|
||||
cell = data[0].children[0].children[9]
|
||||
+23
-27
@@ -15,9 +15,6 @@ context('filtering tests: ', function () {
|
||||
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation, { timeout: longerCommandTimeout })
|
||||
// cy.get('input.username').type(username)
|
||||
// cy.get('input.password').type(password)
|
||||
// cy.get('.login-group button').click()
|
||||
|
||||
visitPage('home')
|
||||
})
|
||||
@@ -49,7 +46,7 @@ context('filtering tests: ', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it.only('3 | filter time field', (done) => {
|
||||
it('3 | filter time field', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
openFilterPopup(() => {
|
||||
@@ -159,24 +156,21 @@ context('filtering tests: ', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('7 | filter bestnum field BETWEEN', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
// TODO: fix
|
||||
// it('7 | filter bestnum field BETWEEN', (done) => {
|
||||
// openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
openFilterPopup(() => {
|
||||
setFilterWithValue('SOME_BESTNUM', '0-10', 'between', () => {
|
||||
checkInfoBarIncludes(
|
||||
`AND,AND,0,SOME_BESTNUM,BETWEEN,0 AND 10`,
|
||||
(includes: boolean) => {
|
||||
if (includes) done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
this.afterEach(() => {
|
||||
// cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
})
|
||||
// openFilterPopup(() => {
|
||||
// setFilterWithValue('SOME_BESTNUM', '0-10', 'between', () => {
|
||||
// checkInfoBarIncludes(
|
||||
// `AND,AND,0,SOME_BESTNUM,BETWEEN,0 AND 10`,
|
||||
// (includes: boolean) => {
|
||||
// if (includes) done()
|
||||
// }
|
||||
// )
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
})
|
||||
|
||||
const checkInfoBarIncludes = (text: string, callback: any) => {
|
||||
@@ -304,14 +298,16 @@ const setFilterWithValue = (
|
||||
cy.get('.no-values')
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('clr-checkbox-wrapper input').then((inputs: any) => {
|
||||
inputs[0].click()
|
||||
cy.get('.in-values-modal .modal-footer button').click()
|
||||
cy.get('.in-values-modal clr-checkbox-wrapper input').then(
|
||||
(inputs: any) => {
|
||||
inputs[0].click()
|
||||
cy.get('.in-values-modal .modal-footer button').click()
|
||||
|
||||
cy.get('.modal-footer .btn-success-outline').click()
|
||||
cy.get('.modal-footer .btn-success-outline').click()
|
||||
|
||||
if (callback) callback()
|
||||
})
|
||||
if (callback) callback()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,15 +23,11 @@ interface EditConfigTableCells {
|
||||
|
||||
context('licensing tests: ', function () {
|
||||
this.beforeAll(() => {
|
||||
// cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
cy.loginAndUpdateValidKey()
|
||||
})
|
||||
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
// cy.get('input.username').type(username)
|
||||
// cy.get('input.password').type(password)
|
||||
// cy.get('.login-group button').click()
|
||||
|
||||
visitPage('home')
|
||||
})
|
||||
@@ -374,10 +370,6 @@ context('licensing tests: ', function () {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.afterEach(() => {
|
||||
// cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
})
|
||||
})
|
||||
|
||||
const logout = (callback?: any) => {
|
||||
@@ -699,14 +691,10 @@ const submitTable = (callback?: any) => {
|
||||
|
||||
const approveTable = (callback?: any) => {
|
||||
cy.get('button', { timeout: longerCommandTimeout })
|
||||
.should('contain', 'Go to approvals screen')
|
||||
.should('contain', 'Approve')
|
||||
.then((allButtons: any) => {
|
||||
for (let approvalButton of allButtons) {
|
||||
if (
|
||||
approvalButton.innerText
|
||||
.toLowerCase()
|
||||
.includes('go to approvals screen')
|
||||
) {
|
||||
if (approvalButton.innerText.toLowerCase().includes('approve')) {
|
||||
approvalButton.click()
|
||||
break
|
||||
}
|
||||
@@ -18,10 +18,6 @@ context('liveness tests: ', function () {
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
|
||||
// cy.get('input.username').type(username)
|
||||
// cy.get('input.password').type(password)
|
||||
// cy.get('.login-group button').click()
|
||||
|
||||
visitPage('home')
|
||||
})
|
||||
|
||||
@@ -125,14 +121,10 @@ const submitExcel = (callback?: any) => {
|
||||
|
||||
const rejectExcel = (callback?: any) => {
|
||||
cy.get('button', { timeout: longerCommandTimeout })
|
||||
.should('contain', 'Go to approvals screen')
|
||||
.should('contain', 'Approve')
|
||||
.then((allButtons: any) => {
|
||||
for (let approvalButton of allButtons) {
|
||||
if (
|
||||
approvalButton.innerText
|
||||
.toLowerCase()
|
||||
.includes('go to approvals screen')
|
||||
) {
|
||||
if (approvalButton.innerText.toLowerCase().includes('approve')) {
|
||||
approvalButton.click()
|
||||
break
|
||||
}
|
||||
@@ -7,17 +7,15 @@ const appLocation = Cypress.env('appLocation')
|
||||
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
||||
const serverType = Cypress.env('serverType')
|
||||
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
|
||||
const fixturePath = 'excels_general/'
|
||||
|
||||
context('editor tests: ', function () {
|
||||
this.beforeAll(() => {
|
||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
cy.loginAndUpdateValidKey()
|
||||
cy.loginAndUpdateValidKey(true)
|
||||
})
|
||||
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
|
||||
cy.wait(2000)
|
||||
|
||||
cy.get('body').then(($body) => {
|
||||
@@ -80,7 +78,6 @@ context('editor tests: ', function () {
|
||||
libraryToOpenIncludes,
|
||||
viewboxes.map((viewbox) => viewbox.viewbox_table)
|
||||
)
|
||||
|
||||
cy.get('.open-viewbox').then((viewboxNodes: any) => {
|
||||
let found = 0
|
||||
|
||||
@@ -95,32 +92,34 @@ context('editor tests: ', function () {
|
||||
|
||||
if (found < viewboxes.length) return
|
||||
|
||||
cy.get('.viewboxes-container .viewbox').then((viewboxNodes: any) => {
|
||||
for (let viewboxNode of viewboxNodes) {
|
||||
cy.get(viewboxNode).within(() => {
|
||||
cy.get('.table-title').then((tableTitle) => {
|
||||
const title = tableTitle[0].innerText
|
||||
const viewbox = viewboxes.find((vb) =>
|
||||
title.toLowerCase().includes(vb.viewbox_table)
|
||||
)
|
||||
|
||||
if (viewbox) {
|
||||
cy.get('.ht_master.handsontable .htCore thead tr').then(
|
||||
(viewboxColNodes: any) => {
|
||||
let allColsHtml = viewboxColNodes[0].innerHTML
|
||||
|
||||
for (let col of viewbox?.columns) {
|
||||
if (!allColsHtml.includes(col)) return
|
||||
}
|
||||
|
||||
done()
|
||||
}
|
||||
cy.get('.viewboxes-container .viewbox', { withinSubject: null }).then(
|
||||
(viewboxNodes: any) => {
|
||||
for (let viewboxNode of viewboxNodes) {
|
||||
cy.get(viewboxNode).within(() => {
|
||||
cy.get('.table-title').then((tableTitle) => {
|
||||
const title = tableTitle[0].innerText
|
||||
const viewbox = viewboxes.find((vb) =>
|
||||
title.toLowerCase().includes(vb.viewbox_table)
|
||||
)
|
||||
}
|
||||
|
||||
if (viewbox) {
|
||||
cy.get('.ht_master.handsontable .htCore thead tr').then(
|
||||
(viewboxColNodes: any) => {
|
||||
let allColsHtml = viewboxColNodes[0].innerHTML
|
||||
|
||||
for (let col of viewbox?.columns) {
|
||||
if (!allColsHtml.includes(col)) return
|
||||
}
|
||||
|
||||
done()
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -142,6 +141,8 @@ context('editor tests: ', function () {
|
||||
|
||||
openViewboxConfig(viewbox_table)
|
||||
|
||||
removeAllColumns()
|
||||
|
||||
addColumns(additionalColumns)
|
||||
|
||||
checkColumns([...columns, ...additionalColumns], () => {
|
||||
@@ -169,6 +170,8 @@ context('editor tests: ', function () {
|
||||
|
||||
openViewboxConfig(viewbox_table)
|
||||
|
||||
removeAllColumns()
|
||||
|
||||
addColumns(additionalColumns, () => {
|
||||
cy.wait(1000)
|
||||
//reorder
|
||||
@@ -209,6 +212,8 @@ context('editor tests: ', function () {
|
||||
|
||||
viewboxNode.click()
|
||||
|
||||
removeAllColumns()
|
||||
|
||||
addColumns(additionalColumns, () => {
|
||||
cy.wait(1000)
|
||||
//reorder
|
||||
@@ -281,6 +286,8 @@ context('editor tests: ', function () {
|
||||
|
||||
cy.wait(500)
|
||||
|
||||
removeAllColumns()
|
||||
|
||||
addColumns(viewboxes[0].additionalColumns, () => {
|
||||
cy.wait(1000)
|
||||
|
||||
@@ -297,6 +304,7 @@ context('editor tests: ', function () {
|
||||
cy.wait(1000)
|
||||
|
||||
openViewboxConfig(viewboxes[1].viewbox_table)
|
||||
|
||||
addColumns(viewboxes[1].additionalColumns, () => {
|
||||
cy.wait(1000).reload()
|
||||
|
||||
@@ -325,73 +333,80 @@ context('editor tests: ', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('7 | Add viewboxes and filter', () => {
|
||||
const viewboxes = ['mpe_x_test', 'mpe_validations']
|
||||
// We will enable this test when we figure out how to mock filtering
|
||||
// it('7 | Add viewboxes and filter', () => {
|
||||
// const viewboxes = ['mpe_x_test', 'mpe_validations']
|
||||
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
// openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
cy.get('.viewbox-open').click()
|
||||
openTableFromViewboxTree(libraryToOpenIncludes, viewboxes)
|
||||
// cy.get('.viewbox-open').click()
|
||||
// openTableFromViewboxTree(libraryToOpenIncludes, viewboxes)
|
||||
|
||||
cy.wait(1000)
|
||||
// cy.wait(1000)
|
||||
|
||||
closeViewboxModal()
|
||||
// closeViewboxModal()
|
||||
|
||||
cy.get('.viewboxes-container .viewbox', { withinSubject: null }).then(
|
||||
(viewboxNodes: any) => {
|
||||
for (let viewboxNode of viewboxNodes) {
|
||||
cy.get(viewboxNode).within(() => {
|
||||
cy.get('.table-title').then((title: any) => {
|
||||
cy.get('.hot-spinner')
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('clr-icon[shape="filter"]').then((filterButton) => {
|
||||
filterButton[0].click()
|
||||
})
|
||||
// cy.get('.viewboxes-container .viewbox', { withinSubject: null }).then(
|
||||
// (viewboxNodes: any) => {
|
||||
// for (let viewboxNode of viewboxNodes) {
|
||||
// cy.get(viewboxNode).within(() => {
|
||||
// cy.get('.table-title').then((title: any) => {
|
||||
// cy.get('.hot-spinner')
|
||||
// .should('not.exist')
|
||||
// .then(() => {
|
||||
// cy.get('clr-icon[shape="filter"]').then((filterButton) => {
|
||||
// filterButton[0].click()
|
||||
// })
|
||||
|
||||
if (title[0].innerText.includes('MPE_X_TEST')) {
|
||||
setFilterWithValue(
|
||||
'SOME_CHAR',
|
||||
'this is dummy data',
|
||||
'value',
|
||||
() => {
|
||||
cy.get('app-query', { withinSubject: null })
|
||||
.should('not.exist')
|
||||
.get('.ht_master.handsontable tbody tr')
|
||||
.then((rowNodes) => {
|
||||
const tr = rowNodes[0]
|
||||
// if (title[0].innerText.includes('MPE_X_TEST')) {
|
||||
// setFilterWithValue(
|
||||
// 'SOME_CHAR',
|
||||
// 'this is dummy data',
|
||||
// 'value',
|
||||
// () => {
|
||||
// cy.get('app-query', { withinSubject: null })
|
||||
// .should('not.exist')
|
||||
// .get('.ht_master.handsontable tbody tr')
|
||||
// .then((rowNodes) => {
|
||||
// const tr = rowNodes[0]
|
||||
|
||||
expect(rowNodes).to.have.length(1)
|
||||
expect(tr.innerText).to.equal('0')
|
||||
})
|
||||
}
|
||||
)
|
||||
} else if (title[0].innerText.includes('MPE_VALIDATIONS')) {
|
||||
setFilterWithValue('BASE_COL', 'ALERT_LIB', 'value', () => {
|
||||
cy.get('app-query', { withinSubject: null })
|
||||
.should('not.exist')
|
||||
.get('.ht_master.handsontable tbody tr')
|
||||
.then((rowNodes) => {
|
||||
const tr = rowNodes[0]
|
||||
// expect(rowNodes).to.have.length(1)
|
||||
// expect(tr.innerText).to.equal('0')
|
||||
// })
|
||||
// }
|
||||
// )
|
||||
// } else if (title[0].innerText.includes('MPE_VALIDATIONS')) {
|
||||
// setFilterWithValue('BASE_COL', 'ALERT_LIB', 'value', () => {
|
||||
// cy.get('app-query', { withinSubject: null })
|
||||
// .should('not.exist')
|
||||
// .get('.ht_master.handsontable tbody tr')
|
||||
// .then((rowNodes) => {
|
||||
// const tr = rowNodes[0]
|
||||
|
||||
expect(rowNodes).to.have.length(1)
|
||||
expect(tr.innerText).to.contain('ALERT_LIB')
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
this.afterEach(() => {
|
||||
// cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
})
|
||||
// expect(rowNodes).to.have.length(1)
|
||||
// expect(tr.innerText).to.contain('ALERT_LIB')
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
// })
|
||||
})
|
||||
|
||||
const removeAllColumns = () => {
|
||||
cy.get('.configuration-wrapper clr-icon[shape="trash"]').then(
|
||||
(removeNodes) => {
|
||||
for (let removeNode of removeNodes) {
|
||||
removeNode.click()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const checkColumns = (columns: string[], callback: () => void) => {
|
||||
cy.get('.viewboxes-container .viewbox', { withinSubject: null }).then(
|
||||
(viewboxNodes: any) => {
|
||||
@@ -399,8 +414,10 @@ const checkColumns = (columns: string[], callback: () => void) => {
|
||||
cy.get(viewboxNode).within(() => {
|
||||
cy.get('.ht_master.handsontable thead tr th').then(
|
||||
(viewboxColNodes: any) => {
|
||||
console.log('viewboxColNode', viewboxColNodes)
|
||||
console.log('columns', columns)
|
||||
for (let i = 0; i < viewboxColNodes.length; i++) {
|
||||
const col = columns[i]
|
||||
const col = columns[i] || ''
|
||||
const colNode = viewboxColNodes[i]
|
||||
|
||||
if (
|
||||
@@ -610,9 +627,10 @@ const openTableFromViewboxTree = (
|
||||
cy.get('p')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
||||
cy.get('.clr-treenode-link').then(async (innerNodes: any) => {
|
||||
for (let innerNode of innerNodes) {
|
||||
for (let tablename of tablenames) {
|
||||
await pause(300)
|
||||
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
||||
innerNode.click()
|
||||
}
|
||||
@@ -624,6 +642,14 @@ const openTableFromViewboxTree = (
|
||||
})
|
||||
}
|
||||
|
||||
const pause = (ms: number) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve(null)
|
||||
}, ms)
|
||||
})
|
||||
}
|
||||
|
||||
const visitPage = (url: string) => {
|
||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
// Marks this file as an ES module (rather than a global script) so its
|
||||
// top-level consts don't collide, under the TS type-checker, with the same
|
||||
// names declared in other spec files — see e.g. download.cy.ts, which gets
|
||||
// this for free via a real import.
|
||||
export {}
|
||||
|
||||
const username = Cypress.env('username')
|
||||
const password = Cypress.env('password')
|
||||
const hostUrl = Cypress.env('hosturl')
|
||||
const appLocation = Cypress.env('appLocation')
|
||||
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
||||
const serverType = Cypress.env('serverType')
|
||||
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
|
||||
|
||||
// Viewer column-label display toggle (`?labels=true`): clients surfacing DC in
|
||||
// SAS Visual Analytics want column LABELs shown instead of NAMEs. Default
|
||||
// behavior (no param) must stay unchanged; the right-click context menu's
|
||||
// "Show labels"/"Show names" item is the in-app affordance, and it drives the
|
||||
// same URL param so state is shareable/refreshable. See mock data in
|
||||
// sas/mocks/sasjs/services/public/viewdata.js: MPE_X_TEST's SOME_CHAR/SOME_DATE
|
||||
// have LABELs that differ from NAME, and SOME_NUM has a blank LABEL (fallback
|
||||
// to NAME) — this spec exercises exactly that fixture.
|
||||
context('viewer column labels toggle tests: ', function () {
|
||||
this.beforeAll(() => {
|
||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
// forceLicenceKey=true: without it, loginAndUpdateValidKey() only
|
||||
// generates key data and logs back out if no valid license already
|
||||
// exists — it doesn't actually apply one (see commands.ts). Several
|
||||
// specs call it with no argument and rely on an *earlier* spec in the
|
||||
// same CI run having already forced a valid license onto the shared
|
||||
// server first (csv.cy.ts, excel.cy.ts, etc.). This spec shouldn't
|
||||
// depend on run order, so force it explicitly.
|
||||
cy.loginAndUpdateValidKey(true)
|
||||
})
|
||||
|
||||
this.beforeEach(() => {
|
||||
// No re-login here: beforeAll's loginAndUpdateValidKey() already
|
||||
// authenticated the session, and it persists across tests in this file
|
||||
// (see filtering.cy.ts/csv.cy.ts for the same pattern). Re-typing into
|
||||
// the login form would fail — it's hidden once already logged in.
|
||||
cy.visit(hostUrl + appLocation)
|
||||
|
||||
// Visit 'home' first, matching filtering.cy.ts/csv.cy.ts/download.cy.ts —
|
||||
// going straight to 'view/data' skips whatever startup-data fetch 'home'
|
||||
// triggers, which passed locally (fast round-trips) but left the
|
||||
// nav-tree empty in CI's slower environment.
|
||||
visitPage('home')
|
||||
visitPage('view/data')
|
||||
})
|
||||
|
||||
it('1 | default (no param): headers show NAMEs', () => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
cy.get('#hotTable .ht_clone_top .htCore thead tr', {
|
||||
timeout: longerCommandTimeout
|
||||
}).should(($headerRow) => {
|
||||
expect($headerRow[0].innerHTML).to.include('SOME_CHAR')
|
||||
expect($headerRow[0].innerHTML).to.not.include('Some Character Column')
|
||||
})
|
||||
})
|
||||
|
||||
it('2 | ?labels=true: headers show LABELs, blank LABEL falls back to NAME', () => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
appendUrlParam('labels=true')
|
||||
|
||||
cy.get('#hotTable .ht_clone_top .htCore thead tr', {
|
||||
timeout: longerCommandTimeout
|
||||
}).should(($headerRow) => {
|
||||
const html = $headerRow[0].innerHTML
|
||||
expect(html).to.include('Some Character Column')
|
||||
expect(html).to.include('Some Date')
|
||||
// SOME_NUM has a blank LABEL in the mock -> falls back to NAME
|
||||
expect(html).to.include('SOME_NUM')
|
||||
})
|
||||
})
|
||||
|
||||
it('3 | context menu "Show labels"/"Show names" toggles headers and the URL', () => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
toggleColumnLabelsFromContextMenu('Show labels')
|
||||
|
||||
cy.url().should('include', 'labels=true')
|
||||
cy.get('#hotTable .ht_clone_top .htCore thead tr', {
|
||||
timeout: longerCommandTimeout
|
||||
}).should(($headerRow) => {
|
||||
expect($headerRow[0].innerHTML).to.include('Some Character Column')
|
||||
})
|
||||
|
||||
toggleColumnLabelsFromContextMenu('Show names')
|
||||
|
||||
cy.url().should('not.include', 'labels=true')
|
||||
cy.get('#hotTable .ht_clone_top .htCore thead tr', {
|
||||
timeout: longerCommandTimeout
|
||||
}).should(($headerRow) => {
|
||||
expect($headerRow[0].innerHTML).to.include('SOME_CHAR')
|
||||
})
|
||||
})
|
||||
|
||||
it('4 | ?embed=va&labels=true: labels shown with embed chrome hidden', () => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
appendUrlParam('embed=va&labels=true')
|
||||
|
||||
cy.get('header.app-header').should('not.exist')
|
||||
cy.get('#hotTable .ht_clone_top .htCore thead tr', {
|
||||
timeout: longerCommandTimeout
|
||||
}).should(($headerRow) => {
|
||||
expect($headerRow[0].innerHTML).to.include('Some Character Column')
|
||||
})
|
||||
})
|
||||
|
||||
it('5 | info dropdown: first line is NAME (in both toggle states)', () => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
// No assertion on the item's label text here: `info` has a custom
|
||||
// `renderer`, so Handsontable never renders its `name` ("test info") as
|
||||
// text at all — the renderer's own output (checked below) is the only
|
||||
// real content.
|
||||
openColumnDropdown('SOME_CHAR')
|
||||
cy.get('.htDropdownMenu').should(($menu) => {
|
||||
expect($menu.text()).to.match(/NAME: SOME_CHAR/)
|
||||
})
|
||||
cy.get('body').click(0, 0) // close menu
|
||||
|
||||
appendUrlParam('labels=true')
|
||||
|
||||
openColumnDropdown('Some Character Column')
|
||||
cy.get('.htDropdownMenu').should(($menu) => {
|
||||
// NAME is always shown first, even while headers display as LABEL
|
||||
expect($menu.text()).to.match(/NAME: SOME_CHAR/)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const visitPage = (url: string) => {
|
||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
||||
}
|
||||
|
||||
// Re-visits the current route with extra hash-query params appended, then
|
||||
// forces a real reload. Under hash routing (`useHash: true`), changing only
|
||||
// the URL's hash fragment is a same-document navigation — Angular Router
|
||||
// still reacts to it (which is enough for `useLabels`, read reactively from
|
||||
// `route.queryParams`), but `embed` is parsed once from `window.location.hash`
|
||||
// at app bootstrap (app.component.ts), so `?embed=va` only takes effect after
|
||||
// an actual reload.
|
||||
const appendUrlParam = (param: string) => {
|
||||
cy.url().then((url) => {
|
||||
const separator = url.includes('?') ? '&' : '?'
|
||||
cy.visit(`${url}${separator}${param}`)
|
||||
cy.reload()
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout }).should(
|
||||
'not.exist'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const openColumnDropdown = (headerText: string) => {
|
||||
// Handsontable renders the sticky/frozen header via a separate clone pane
|
||||
// (.ht_clone_top) for scroll behavior; the header row inside the main
|
||||
// .ht_master pane is kept visibility:hidden (replaced visually by the
|
||||
// clone). .ht_clone_top is the real, interactive one — confirmed via
|
||||
// `document.querySelector('#hotTable .ht_clone_top thead').innerText` in
|
||||
// a live browser session.
|
||||
cy.get('#hotTable .ht_clone_top .htCore thead button.changeType', {
|
||||
timeout: longerCommandTimeout
|
||||
})
|
||||
.parents('th')
|
||||
.filter((_, th) => Cypress.$(th).text().includes(headerText))
|
||||
.last()
|
||||
.as('targetHeader')
|
||||
|
||||
// Click the header text first to select the column — the `info` item's
|
||||
// renderer reads hot.getSelected() to decide which column to describe,
|
||||
// so without an active selection it always shows "No info found".
|
||||
cy.get('@targetHeader').click()
|
||||
cy.get('@targetHeader').find('button.changeType').click({ force: true })
|
||||
}
|
||||
|
||||
const toggleColumnLabelsFromContextMenu = (
|
||||
menuItemText: 'Show labels' | 'Show names'
|
||||
) => {
|
||||
cy.get('#hotTable .ht_master.handsontable .htCore tbody tr td', {
|
||||
timeout: longerCommandTimeout
|
||||
})
|
||||
.first()
|
||||
// force: true — the first body row can sit under an overlay clone's
|
||||
// header/sort-icon layer, which fails Cypress's actionability check
|
||||
// even though the cell is the real rightclick target underneath.
|
||||
.rightclick({ force: true })
|
||||
|
||||
cy.get('.htContextMenu').contains(menuItemText).click()
|
||||
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout }).should('not.exist')
|
||||
}
|
||||
|
||||
const openTableFromTree = (libNameIncludes: string, tablename: string) => {
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout })
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then((treeNodes: any) => {
|
||||
let viyaLib
|
||||
|
||||
for (let node of treeNodes) {
|
||||
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
|
||||
viyaLib = node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cy.get(viyaLib).within(() => {
|
||||
// Small settle wait: right after a fresh visit/reload, the tree
|
||||
// component can still be re-rendering, causing this node to be
|
||||
// found then swapped out mid-click ("disappeared from the page").
|
||||
cy.wait(300)
|
||||
|
||||
cy.get(
|
||||
'.clr-tree-node-content-container .clr-treenode-content p'
|
||||
).click()
|
||||
|
||||
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
||||
for (let innerNode of innerNodes) {
|
||||
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
||||
innerNode.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Selecting the table triggers async SPA routing + a viewdata fetch;
|
||||
// wait for the grid to actually render before any subsequent action
|
||||
// (reading the URL, right-clicking, opening a header dropdown) — without
|
||||
// this, callers can act while still on the intermediate library-only
|
||||
// route or a not-yet-rendered grid. Waiting specifically for a header's
|
||||
// dropdown button (not just the <tr> shell) confirms Handsontable has
|
||||
// finished populating header cell contents, not just their DOM rows.
|
||||
cy.get('#hotTable .ht_clone_top .htCore thead button.changeType', {
|
||||
timeout: longerCommandTimeout
|
||||
}).should('exist')
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
PRIMARY_KEY_FIELD,SOME_CHAR,SOME_DROPDOWN,SOME_NUM,SOME_DATE,SOME_DATETIME,SOME_TIME,SOME_SHORTNUM,SOME_BESTNUM
|
||||
0,abc,Option abc,42,12FEB1960,01JAN1960:00:00:42,0:00:42,3,44
|
||||
1,more dummy data,Option 2,42,12FEB1960,01JAN1960:00:00:42,0:07:02,3,44
|
||||
2,even more dummy data,Option 3,42,12FEB1960,01JAN1960:00:00:42,0:02:22,3,44
|
||||
3,"It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told:",Option 2,1613.001,27FEB1961,01JAN1960:00:07:03,0:00:44,3,44
|
||||
4,if you can fill the unforgiving minute,Option 1,1613.0011235,02AUG1971,29MAY1973:06:12:03,0:06:52,3,44
|
||||
1010,"10 bottles of beer
|
||||
|
||||
|
||||
|
||||
on the wall",Option 1,0.9153696885,04MAR1962,01JAN1960:12:47:55,0:01:40,92,76
|
||||
1011,11 bottles of beer on the wall,Option 1,0.3531217558,29MAR1960,01JAN1960:03:33:24,0:01:03,80,29
|
||||
1012,12 bottles of beer on the wall,Option 1,0.6743748717,02AUG1962,01JAN1960:07:25:59,0:00:10,16,98
|
||||
1013,13 bottles of beer on the wall,Option 1,0.1305445992,11SEP1960,01JAN1960:13:51:32,0:00:35,73,15
|
||||
1014,14 bottles of beer on the wall,Option 1,0.7409067949,26JUL1960,01JAN1960:05:18:10,0:00:41,30,89
|
||||
1015,15 bottles of beer on the wall,Option 1,0.0869016028,28FEB1961,01JAN1960:13:23:45,0:00:44,80,3
|
||||
1016,16 bottles of beer on the wall,Option 1,0.0462121419,09AUG1962,01JAN1960:07:42:38,0:01:17,62,2
|
||||
1017,17 bottles of beer on the wall,Option 1,0.7501918947,14MAY1962,01JAN1960:04:40:20,0:00:15,53,65
|
||||
1018,18 bottles of beer on the wall,Option 1,0.7300173294,03AUG1962,01JAN1960:03:20:41,0:00:41,21,60
|
||||
1019,19 bottles of beer on the wall,Option 1,0.6960950437,01JUN1960,01JAN1960:01:58:52,0:01:08,38,5
|
||||
1020,20 bottles of beer on the wall,Option 1,0.6190566065,30MAY1961,01JAN1960:09:04:20,0:01:07,92,23
|
||||
1021,21 bottles of beer on the wall,Option 1,0.5173368238,07JAN1961,01JAN1960:07:52:34,0:00:52,57,21
|
||||
1022,22 bottles of beer on the wall,Option 1,0.4720626452,07NOV1960,01JAN1960:12:12:00,0:00:26,53,32
|
||||
1023,23 bottles of beer on the wall,Option 1,0.2856596393,08AUG1960,01JAN1960:06:09:25,0:00:28,40,12
|
||||
1024,24 bottles of beer on the wall,Option 1,0.5160869418,02JUN1960,01JAN1960:06:36:06,0:01:10,41,81
|
||||
1025,25 bottles of beer on the wall,Option 1,0.1683158517,05JAN1961,01JAN1960:08:14:35,0:00:06,18,53
|
||||
1026,26 bottles of beer on the wall,Option 1,0.8951142248,28NOV1961,01JAN1960:03:31:17,0:00:58,79,54
|
||||
1027,27 bottles of beer on the wall,Option 1,0.7037817481,01SEP1961,01JAN1960:05:48:34,0:00:29,50,15
|
||||
1028,28 bottles of beer on the wall,Option 1,0.6193826714,31MAR1962,01JAN1960:02:49:39,0:00:24,78,87
|
||||
1029,29 bottles of beer on the wall,Option 1,0.9339028457,06DEC1961,01JAN1960:02:57:57,0:00:24,73,64
|
||||
1030,30 bottles of beer on the wall,Option 1,0.5647351339,10AUG1960,01JAN1960:11:02:59,0:00:55,39,28
|
||||
1031,31 bottles of beer on the wall,Option 1,0.1218988607,19JUN1961,01JAN1960:04:19:32,0:00:58,51,32
|
||||
1032,32 bottles of beer on the wall,Option 1,0.3459929113,14MAY1962,01JAN1960:05:42:48,0:00:54,96,46
|
||||
1033,33 bottles of beer on the wall,Option 1,0.092664999,31AUG1962,01JAN1960:00:08:34,0:00:51,69,90
|
||||
1034,34 bottles of beer on the wall,Option 1,0.9793458097,08FEB1960,01JAN1960:01:55:23,0:00:42,45,28
|
||||
1035,35 bottles of beer on the wall,Option 1,0.8964386624,18DEC1961,01JAN1960:04:42:45,0:00:07,49,97
|
||||
1036,36 bottles of beer on the wall,Option 1,0.0961652911,13NOV1960,01JAN1960:03:44:53,0:01:25,62,59
|
||||
1037,37 bottles of beer on the wall,Option 1,0.3475089201,16JAN1962,01JAN1960:01:35:19,0:00:15,23,50
|
||||
1038,38 bottles of beer on the wall,Option 1,0.3096271312,21MAY1960,01JAN1960:09:51:33,0:00:15,2,71
|
||||
1039,39 bottles of beer on the wall,Option 1,0.9445223114,28AUG1962,01JAN1960:07:09:31,0:00:12,30,31
|
||||
1040,40 bottles of beer on the wall,Option 1,0.5626084667,06NOV1960,01JAN1960:01:42:16,0:01:14,18,97
|
||||
1041,41 bottles of beer on the wall,Option 1,0.9432962513,01JUN1962,01JAN1960:03:30:04,0:00:11,20,34
|
||||
1042,42 bottles of beer on the wall,Option 1,0.5802429382,08JUL1961,01JAN1960:08:12:43,0:01:26,18,5
|
||||
1043,43 bottles of beer on the wall,Option 1,0.1970176255,27MAR1961,01JAN1960:00:19:45,0:01:29,13,76
|
||||
1044,44 bottles of beer on the wall,Option 1,0.4980671608,05JAN1961,01JAN1960:13:36:08,0:00:56,4,36
|
||||
1045,45 bottles of beer on the wall,Option 1,0.2486515531,05MAY1962,01JAN1960:08:47:09,0:00:42,2,23
|
||||
1046,46 bottles of beer on the wall,Option 1,0.4097825794,20JUN1960,01JAN1960:03:33:26,0:00:31,98,71
|
||||
1047,47 bottles of beer on the wall,Option 1,0.138754441,28JAN1960,01JAN1960:00:57:41,0:00:18,80,32
|
||||
1048,48 bottles of beer on the wall,Option 1,0.0249874415,03MAR1960,01JAN1960:11:33:53,0:00:04,96,76
|
||||
1049,49 bottles of beer on the wall,Option 1,0.8395310011,06NOV1961,01JAN1960:09:54:04,0:00:52,28,45
|
||||
1050,50 bottles of beer on the wall,Option 1,0.0942291618,14APR1962,01JAN1960:08:09:30,0:01:36,37,86
|
||||
1051,51 bottles of beer on the wall,Option 1,0.1670458001,13NOV1961,01JAN1960:01:05:55,0:00:25,42,83
|
||||
1052,52 bottles of beer on the wall,Option 1,0.3122402715,04JUN1960,01JAN1960:03:47:47,0:01:01,18,78
|
||||
1053,53 bottles of beer on the wall,Option 1,0.3854694261,14JUN1960,01JAN1960:02:43:08,0:00:06,22,67
|
||||
1054,54 bottles of beer on the wall,Option 1,0.1950434345,14NOV1961,01JAN1960:02:46:34,0:00:55,42,0
|
||||
1055,55 bottles of beer on the wall,Option 1,0.4948673586,29MAR1962,01JAN1960:00:48:06,0:01:04,28,4
|
||||
1056,56 bottles of beer on the wall,Option 1,0.6464513832,06SEP1962,01JAN1960:10:08:36,0:01:02,43,82
|
||||
1057,57 bottles of beer on the wall,Option 1,0.0724864798,20JUN1961,01JAN1960:12:22:51,0:01:27,82,53
|
||||
1058,58 bottles of beer on the wall,Option 1,0.8114467793,20MAR1962,01JAN1960:06:11:33,0:01:29,40,89
|
||||
1059,59 bottles of beer on the wall,Option 1,0.6348024321,28JUN1962,01JAN1960:05:21:21,0:01:37,55,41
|
||||
1060,60 bottles of beer on the wall,Option 1,0.8019492933,08APR1961,01JAN1960:12:37:00,0:01:29,49,88
|
||||
1061,61 bottles of beer on the wall,Option 1,0.4695742002,29JAN1962,01JAN1960:08:54:24,0:00:15,40,91
|
||||
1062,62 bottles of beer on the wall,Option 1,0.902706475,15JUN1961,01JAN1960:09:46:49,0:00:23,74,70
|
||||
1063,63 bottles of beer on the wall,Option 1,0.4557614594,16JUL1961,01JAN1960:02:06:05,0:01:09,7,3
|
||||
1064,64 bottles of beer on the wall,Option 1,0.6632444466,20MAY1961,01JAN1960:02:44:44,0:00:20,42,100
|
||||
1065,65 bottles of beer on the wall,Option 1,0.3901674,31AUG1961,01JAN1960:07:56:49,0:00:32,98,3
|
||||
1066,66 bottles of beer on the wall,Option 1,0.8453234848,30JUN1962,01JAN1960:04:51:54,0:01:02,51,22
|
||||
1067,67 bottles of beer on the wall,Option 1,0.9370150906,26APR1960,01JAN1960:04:05:08,0:01:39,28,86
|
||||
1068,68 bottles of beer on the wall,Option 1,0.8854161277,22MAR1962,01JAN1960:10:38:49,0:01:07,60,85
|
||||
1069,69 bottles of beer on the wall,Option 1,0.1327841906,24MAY1960,01JAN1960:01:18:46,0:01:15,88,85
|
||||
1070,70 bottles of beer on the wall,Option 1,0.5846563226,27JUL1962,01JAN1960:03:52:31,0:00:09,20,8
|
||||
1071,71 bottles of beer on the wall,Option 1,0.0257193684,18FEB1961,01JAN1960:03:25:01,0:00:29,1,62
|
||||
1072,72 bottles of beer on the wall,Option 1,0.9471486034,01JUN1962,01JAN1960:04:05:25,0:01:22,65,20
|
||||
1073,73 bottles of beer on the wall,Option 1,0.3037446282,16MAY1962,01JAN1960:05:10:19,0:00:01,14,34
|
||||
1074,74 bottles of beer on the wall,Option 1,0.2508690675,01NOV1961,01JAN1960:11:26:03,0:00:15,8,74
|
||||
1075,75 bottles of beer on the wall,Option 1,0.814380363,17SEP1960,01JAN1960:09:00:38,0:00:25,95,1
|
||||
1076,76 bottles of beer on the wall,Option 1,0.3761493621,16AUG1961,01JAN1960:01:48:17,0:00:52,5,38
|
||||
1077,77 bottles of beer on the wall,Option 1,0.3621215761,25JUL1961,01JAN1960:11:48:47,0:01:20,86,90
|
||||
1078,78 bottles of beer on the wall,Option 1,0.0268799584,20MAY1961,01JAN1960:12:43:34,0:01:00,70,96
|
||||
1079,79 bottles of beer on the wall,Option 1,0.4112483945,27JUL1962,01JAN1960:01:20:24,0:01:26,66,20
|
||||
1080,80 bottles of beer on the wall,Option 1,0.9501868011,15APR1961,01JAN1960:09:58:20,0:00:51,93,79
|
||||
1081,81 bottles of beer on the wall,Option 1,0.9866548018,13SEP1961,01JAN1960:05:20:04,0:00:14,28,97
|
||||
1082,82 bottles of beer on the wall,Option 1,0.9907830073,22FEB1962,01JAN1960:03:29:03,0:00:17,16,91
|
||||
1083,83 bottles of beer on the wall,Option 1,0.8927816567,11MAR1960,01JAN1960:05:52:48,0:01:26,54,14
|
||||
1084,84 bottles of beer on the wall,Option 1,0.12871663,02FEB1961,01JAN1960:10:34:37,0:00:44,52,90
|
||||
1085,85 bottles of beer on the wall,Option 1,0.5490252802,02JAN1960,01JAN1960:06:11:58,0:00:27,4,98
|
||||
1086,86 bottles of beer on the wall,Option 1,0.5432773864,11FEB1960,01JAN1960:08:40:00,0:01:15,79,19
|
||||
1087,87 bottles of beer on the wall,Option 1,0.8223943137,01OCT1960,01JAN1960:08:11:33,0:01:19,2,86
|
||||
1088,88 bottles of beer on the wall,Option 1,0.8496777699,09FEB1962,01JAN1960:03:10:35,0:00:15,95,6
|
||||
1089,89 bottles of beer on the wall,Option 1,0.9308730536,27MAY1962,01JAN1960:11:57:53,0:01:18,86,90
|
||||
1090,90 bottles of beer on the wall,Option 1,0.3072653344,23FEB1962,01JAN1960:04:52:38,0:00:25,85,17
|
||||
1091,91 bottles of beer on the wall,Option 1,0.7687679575,12FEB1960,01JAN1960:08:47:11,0:01:20,8,7
|
||||
1092,92 bottles of beer on the wall,Option 1,0.1873595105,29SEP1961,01JAN1960:04:29:58,0:00:29,17,78
|
||||
1093,93 bottles of beer on the wall,Option 1,0.0495966631,03OCT1961,01JAN1960:03:18:50,0:00:39,89,56
|
||||
1094,94 bottles of beer on the wall,Option 1,0.2607690526,19SEP1960,01JAN1960:03:22:28,0:00:29,81,16
|
||||
1095,95 bottles of beer on the wall,Option 1,0.549640266,07JUN1962,01JAN1960:06:15:32,0:00:04,57,70
|
||||
1096,96 bottles of beer on the wall,Option 1,0.9993291092,08MAR1961,01JAN1960:13:49:08,0:00:33,37,28
|
||||
1097,97 bottles of beer on the wall,Option 1,0.9517237963,02SEP1960,01JAN1960:05:16:03,0:00:40,77,61
|
||||
1098,98 bottles of beer on the wall,Option 1,0.5952155588,14FEB1962,01JAN1960:05:05:11,0:01:29,63,83
|
||||
1099,99 bottles of beer on the wall,Option 1,0.7526210732,05MAY1961,01JAN1960:06:58:36,0:00:02,95,1
|
||||
10100,100 bottles of beer on the wall,Option 1,0.307558153,17MAY1961,01JAN1960:06:13:01,0:01:37,68,7
|
||||
10101,101 bottles of beer on the wall,Option 1,0.6596710829,15APR1962,01JAN1960:08:34:02,0:00:43,66,43
|
||||
10102,102 bottles of beer on the wall,Option 1,0.0202811998,31AUG1961,01JAN1960:07:22:35,0:01:31,57,35
|
||||
10103,103 bottles of beer on the wall,Option 1,0.6699061034,02MAY1962,01JAN1960:05:13:17,0:00:36,30,23
|
||||
10104,104 bottles of beer on the wall,Option 1,0.330972748,04JUN1961,01JAN1960:06:47:20,0:01:05,69,82
|
||||
10105,105 bottles of beer on the wall,Option 1,0.2274839176,25JAN1961,01JAN1960:05:34:51,0:00:56,63,68
|
||||
10106,106 bottles of beer on the wall,Option 1,0.5612243989,27JUN1962,01JAN1960:04:32:03,0:01:15,19,73
|
||||
10107,107 bottles of beer on the wall,Option 1,0.7398902111,03SEP1962,01JAN1960:08:34:07,0:00:17,90,6
|
||||
10108,108 bottles of beer on the wall,Option 1,0.6124899791,08AUG1960,01JAN1960:04:59:34,0:00:25,56,12
|
||||
10109,109 bottles of beer on the wall,Option 1,0.882404773,26JAN1961,01JAN1960:01:29:15,0:01:26,36,4
|
||||
10110,110 bottles of beer on the wall,Option 1,0.4427004733,27FEB1961,01JAN1960:06:16:49,0:01:40,97,84
|
||||
10111,111 bottles of beer on the wall,Option 1,0.3609524622,10JAN1962,01JAN1960:09:48:37,0:01:11,87,62
|
||||
10112,112 bottles of beer on the wall,Option 1,0.9408929562,03AUG1960,01JAN1960:06:54:26,0:00:08,19,33
|
||||
10113,113 bottles of beer on the wall,Option 1,0.3149107319,10AUG1962,01JAN1960:13:01:00,0:00:04,75,60
|
||||
10114,114 bottles of beer on the wall,Option 1,0.0525069181,17APR1962,01JAN1960:13:00:52,0:00:35,9,23
|
||||
10115,115 bottles of beer on the wall,Option 1,0.145448105,14FEB1962,01JAN1960:04:06:08,0:00:26,45,91
|
||||
10116,116 bottles of beer on the wall,Option 1,0.2444279959,10OCT1961,01JAN1960:08:03:12,0:01:37,12,41
|
||||
10117,117 bottles of beer on the wall,Option 1,0.4619846043,30JUL1960,01JAN1960:09:40:16,0:00:50,2,88
|
||||
10118,118 bottles of beer on the wall,Option 1,0.0316203502,13JUL1961,01JAN1960:08:31:39,0:01:05,60,94
|
||||
10119,119 bottles of beer on the wall,Option 1,0.4738720574,13AUG1960,01JAN1960:01:31:05,0:01:17,43,79
|
||||
10120,120 bottles of beer on the wall,Option 1,0.8058761856,11JUN1960,01JAN1960:12:56:35,0:00:08,92,36
|
||||
10121,121 bottles of beer on the wall,Option 1,0.2955600979,08JUL1962,01JAN1960:06:09:22,0:00:03,94,80
|
||||
10122,122 bottles of beer on the wall,Option 1,0.0064115427,18SEP1962,01JAN1960:00:06:24,0:00:13,72,75
|
||||
10123,123 bottles of beer on the wall,Option 1,0.5678159327,21APR1960,01JAN1960:10:54:21,0:00:16,75,67
|
||||
10124,124 bottles of beer on the wall,Option 1,0.1431510994,10JAN1962,01JAN1960:01:57:00,0:00:12,48,31
|
||||
10125,125 bottles of beer on the wall,Option 1,0.3805634409,26JAN1962,01JAN1960:03:03:19,0:01:29,83,52
|
||||
10126,126 bottles of beer on the wall,Option 1,0.3833517993,26APR1960,01JAN1960:11:27:41,0:00:44,99,36
|
||||
10127,127 bottles of beer on the wall,Option 1,0.5669089111,04MAR1961,01JAN1960:05:36:22,0:01:18,43,27
|
||||
10128,128 bottles of beer on the wall,Option 1,0.1514211843,01NOV1960,01JAN1960:07:45:50,0:01:02,22,12
|
||||
10129,129 bottles of beer on the wall,Option 1,0.0446588583,05JAN1961,01JAN1960:02:13:55,0:00:42,27,46
|
||||
10130,130 bottles of beer on the wall,Option 1,0.7892141611,22APR1962,01JAN1960:04:17:54,0:01:05,75,84
|
||||
10131,131 bottles of beer on the wall,Option 1,0.5012088001,24DEC1960,01JAN1960:13:03:23,0:01:22,87,82
|
||||
10132,132 bottles of beer on the wall,Option 1,0.2327582944,07APR1961,01JAN1960:01:33:15,0:01:14,18,46
|
||||
10133,133 bottles of beer on the wall,Option 1,0.2234651173,20MAR1961,01JAN1960:13:52:02,0:01:06,42,58
|
||||
10134,134 bottles of beer on the wall,Option 1,0.4954405918,10FEB1961,01JAN1960:13:51:14,0:01:36,35,11
|
||||
10135,135 bottles of beer on the wall,Option 1,0.7874922891,15AUG1960,01JAN1960:00:21:57,0:00:52,45,36
|
||||
10136,136 bottles of beer on the wall,Option 1,0.3992494891,06SEP1961,01JAN1960:09:51:46,0:01:25,26,9
|
||||
10137,137 bottles of beer on the wall,Option 1,0.3964866136,25MAY1960,01JAN1960:03:19:48,0:00:28,44,3
|
||||
10138,138 bottles of beer on the wall,Option 1,0.9466173323,06APR1962,01JAN1960:13:19:18,0:01:27,78,51
|
||||
10139,139 bottles of beer on the wall,Option 1,0.6525219277,09APR1960,01JAN1960:05:43:49,0:00:21,63,6
|
||||
10140,140 bottles of beer on the wall,Option 1,0.4684071925,29MAY1961,01JAN1960:02:53:36,0:00:46,68,4
|
||||
10141,141 bottles of beer on the wall,Option 1,0.8581724013,16MAY1960,01JAN1960:01:45:44,0:01:32,31,85
|
||||
10142,142 bottles of beer on the wall,Option 1,0.825792401,23APR1961,01JAN1960:12:03:13,0:00:49,36,45
|
||||
10143,143 bottles of beer on the wall,Option 1,0.3172852538,20FEB1962,01JAN1960:12:38:31,0:01:34,51,78
|
||||
10144,144 bottles of beer on the wall,Option 1,0.670397946,27JAN1962,01JAN1960:04:59:37,0:00:39,38,99
|
||||
10145,145 bottles of beer on the wall,Option 1,0.3304372441,04JUN1960,01JAN1960:00:39:12,0:00:29,88,76
|
||||
10146,146 bottles of beer on the wall,Option 1,0.845151971,31JUL1962,01JAN1960:05:03:34,0:00:13,2,80
|
||||
10147,147 bottles of beer on the wall,Option 1,0.7957223709,02FEB1961,01JAN1960:00:03:07,0:01:11,29,99
|
||||
10148,148 bottles of beer on the wall,Option 1,0.323337108,29FEB1960,01JAN1960:01:58:05,0:01:17,23,65
|
||||
10149,149 bottles of beer on the wall,Option 1,0.1813316611,29JUN1960,01JAN1960:02:18:08,0:00:40,45,52
|
||||
10150,150 bottles of beer on the wall,Option 1,0.7860426655,05MAR1962,01JAN1960:01:57:15,0:00:26,31,91
|
||||
10151,151 bottles of beer on the wall,Option 1,0.3305453571,09APR1960,01JAN1960:07:08:32,0:01:30,72,15
|
||||
10152,152 bottles of beer on the wall,Option 1,0.9367212513,18AUG1962,01JAN1960:10:36:03,0:01:26,85,81
|
||||
10153,153 bottles of beer on the wall,Option 1,0.3385623458,19MAR1962,01JAN1960:04:21:46,0:01:29,11,54
|
||||
10154,154 bottles of beer on the wall,Option 1,0.9756794413,17JUN1961,01JAN1960:08:35:40,0:00:34,3,72
|
||||
10155,155 bottles of beer on the wall,Option 1,0.6385958868,21OCT1961,01JAN1960:08:51:00,0:00:50,1,6
|
||||
10156,156 bottles of beer on the wall,Option 1,0.3569769959,14AUG1960,01JAN1960:10:57:16,0:00:05,5,94
|
||||
10157,157 bottles of beer on the wall,Option 1,0.8559997239,23MAR1962,01JAN1960:12:03:38,0:00:08,96,68
|
||||
10158,158 bottles of beer on the wall,Option 1,0.2293701918,13AUG1960,01JAN1960:06:36:47,0:00:07,87,87
|
||||
10159,159 bottles of beer on the wall,Option 1,0.0007910165,20SEP1962,01JAN1960:11:49:02,0:00:55,18,69
|
||||
10160,160 bottles of beer on the wall,Option 1,0.5876370373,08JAN1960,01JAN1960:00:59:15,0:01:26,27,36
|
||||
10161,161 bottles of beer on the wall,Option 1,0.2354667514,11OCT1961,01JAN1960:01:27:14,0:01:04,90,28
|
||||
10162,162 bottles of beer on the wall,Option 1,0.0144103263,11AUG1961,01JAN1960:02:37:41,0:01:39,92,8
|
||||
10163,163 bottles of beer on the wall,Option 1,0.7087855668,03JUL1962,01JAN1960:02:07:23,0:01:35,51,33
|
||||
10164,164 bottles of beer on the wall,Option 1,0.7251478106,20MAY1960,01JAN1960:12:15:23,0:01:28,58,7
|
||||
10165,165 bottles of beer on the wall,Option 1,0.9629398403,06APR1962,01JAN1960:01:05:24,0:01:39,84,6
|
||||
10166,166 bottles of beer on the wall,Option 1,0.5155049164,14OCT1960,01JAN1960:04:02:06,0:00:30,63,96
|
||||
10167,167 bottles of beer on the wall,Option 1,0.1016342775,11MAY1960,01JAN1960:05:55:03,0:00:01,31,44
|
||||
10168,168 bottles of beer on the wall,Option 1,0.3690353596,12NOV1961,01JAN1960:12:49:02,0:00:03,2,79
|
||||
10169,169 bottles of beer on the wall,Option 1,0.5573803501,02SEP1962,01JAN1960:12:59:56,0:00:31,7,95
|
||||
10170,170 bottles of beer on the wall,Option 1,0.2008119497,10JUN1961,01JAN1960:05:59:06,0:01:29,88,4
|
||||
10171,171 bottles of beer on the wall,Option 1,0.6939068505,25MAY1962,01JAN1960:07:20:06,0:01:08,87,8
|
||||
10172,172 bottles of beer on the wall,Option 1,0.7013406594,14JUL1960,01JAN1960:04:04:24,0:00:11,44,96
|
||||
10173,173 bottles of beer on the wall,Option 1,0.83506724,30APR1961,01JAN1960:12:44:40,0:00:10,74,70
|
||||
10174,174 bottles of beer on the wall,Option 1,0.9339991943,26JAN1962,01JAN1960:04:59:32,0:01:09,13,66
|
||||
10175,175 bottles of beer on the wall,Option 1,0.8333402787,18FEB1961,01JAN1960:07:25:44,0:00:28,47,2
|
||||
10176,176 bottles of beer on the wall,Option 1,0.5998844433,03MAR1962,01JAN1960:03:45:33,0:00:52,2,61
|
||||
10177,177 bottles of beer on the wall,Option 1,0.6161394634,18DEC1960,01JAN1960:05:35:25,0:00:50,70,22
|
||||
10178,178 bottles of beer on the wall,Option 1,0.0821002392,21APR1960,01JAN1960:10:08:56,0:01:40,28,9
|
||||
10179,179 bottles of beer on the wall,Option 1,0.6845213462,23MAY1960,01JAN1960:13:15:46,0:00:03,89,13
|
||||
10180,180 bottles of beer on the wall,Option 1,0.3839034477,14MAY1960,01JAN1960:03:22:17,0:01:11,15,38
|
||||
10181,181 bottles of beer on the wall,Option 1,0.7949567609,21AUG1962,01JAN1960:02:41:01,0:00:57,90,93
|
||||
10182,182 bottles of beer on the wall,Option 1,0.5079025419,23SEP1962,01JAN1960:02:22:31,0:01:07,83,32
|
||||
10183,183 bottles of beer on the wall,Option 1,0.3215162574,26DEC1961,01JAN1960:09:03:00,0:01:38,46,94
|
||||
10184,184 bottles of beer on the wall,Option 1,0.3322958058,12MAY1961,01JAN1960:02:48:05,0:00:46,80,54
|
||||
10185,185 bottles of beer on the wall,Option 1,0.6510801453,07SEP1960,01JAN1960:11:49:02,0:00:59,51,47
|
||||
10186,186 bottles of beer on the wall,Option 1,0.060995535,15AUG1960,01JAN1960:02:21:08,0:01:40,5,61
|
||||
10187,187 bottles of beer on the wall,Option 1,0.8541180551,14SEP1960,01JAN1960:13:29:33,0:01:23,17,14
|
||||
10188,188 bottles of beer on the wall,Option 1,0.9427926219,23JUL1960,01JAN1960:05:19:05,0:01:13,22,97
|
||||
10189,189 bottles of beer on the wall,Option 1,0.2325015186,01FEB1960,01JAN1960:11:50:07,0:01:22,6,53
|
||||
10190,190 bottles of beer on the wall,Option 1,0.3687101493,21FEB1962,01JAN1960:06:44:23,0:00:13,16,30
|
||||
10191,191 bottles of beer on the wall,Option 1,0.7647511232,09JAN1960,01JAN1960:13:06:29,0:01:35,6,97
|
||||
10192,192 bottles of beer on the wall,Option 1,0.4105463565,17AUG1961,01JAN1960:11:04:32,0:01:14,38,33
|
||||
10193,193 bottles of beer on the wall,Option 1,0.8785403831,12JUL1962,01JAN1960:04:11:05,0:00:29,19,82
|
||||
10194,194 bottles of beer on the wall,Option 1,0.9304303433,11JUL1961,01JAN1960:12:36:57,0:01:02,20,35
|
||||
10195,195 bottles of beer on the wall,Option 1,0.7302505256,01MAR1961,01JAN1960:01:38:35,0:00:42,16,35
|
||||
10196,196 bottles of beer on the wall,Option 1,0.2536906177,04SEP1962,01JAN1960:05:18:23,0:01:18,91,50
|
||||
10197,197 bottles of beer on the wall,Option 1,0.1181504503,08AUG1961,01JAN1960:09:27:54,0:01:26,20,13
|
||||
10198,198 bottles of beer on the wall,Option 1,0.9275614228,17JUL1961,01JAN1960:01:52:18,0:00:06,52,73
|
||||
10199,199 bottles of beer on the wall,Option 1,0.7495222128,04APR1961,01JAN1960:09:28:04,0:00:42,30,41
|
||||
10200,200 bottles of beer on the wall,Option 1,0.925741082,02FEB1962,01JAN1960:12:23:10,0:00:07,79,17
|
||||
10201,201 bottles of beer on the wall,Option 1,0.2591843359,04DEC1960,01JAN1960:12:46:41,0:00:00,53,58
|
||||
10202,202 bottles of beer on the wall,Option 1,0.4289995704,17NOV1961,01JAN1960:02:20:52,0:00:35,41,25
|
||||
10203,203 bottles of beer on the wall,Option 1,0.4625803807,24JAN1960,01JAN1960:08:20:44,0:01:11,84,66
|
||||
10204,204 bottles of beer on the wall,Option 1,0.858440102,31AUG1962,01JAN1960:08:51:40,0:00:12,18,51
|
||||
10205,205 bottles of beer on the wall,Option 1,0.8964499016,01SEP1962,01JAN1960:05:33:47,0:00:23,34,77
|
||||
10206,206 bottles of beer on the wall,Option 1,0.5742789063,24OCT1961,01JAN1960:02:31:04,0:01:08,27,66
|
||||
10207,207 bottles of beer on the wall,Option 1,0.4864150954,29SEP1960,01JAN1960:09:27:46,0:01:28,31,26
|
||||
10208,208 bottles of beer on the wall,Option 1,0.4511992249,04DEC1960,01JAN1960:09:39:26,0:00:42,49,98
|
||||
10209,209 bottles of beer on the wall,Option 1,0.4218624157,13SEP1961,01JAN1960:01:40:55,0:01:39,35,50
|
||||
10210,210 bottles of beer on the wall,Option 1,0.1572868331,15FEB1960,01JAN1960:07:01:15,0:00:51,43,1
|
||||
10211,211 bottles of beer on the wall,Option 1,0.713915177,23MAR1960,01JAN1960:11:08:53,0:00:15,18,61
|
||||
10212,212 bottles of beer on the wall,Option 1,0.5677882165,19MAY1960,01JAN1960:01:27:23,0:01:02,34,89
|
||||
10213,213 bottles of beer on the wall,Option 1,0.7552938581,12SEP1961,01JAN1960:11:47:33,0:00:38,44,46
|
||||
10214,214 bottles of beer on the wall,Option 1,0.6071256071,28DEC1961,01JAN1960:05:28:18,0:01:23,84,66
|
||||
10215,215 bottles of beer on the wall,Option 1,0.7717189266,12MAR1960,01JAN1960:01:21:26,0:01:00,28,22
|
||||
10216,216 bottles of beer on the wall,Option 1,0.8985594329,24MAR1961,01JAN1960:10:48:58,0:01:31,93,2
|
||||
10217,217 bottles of beer on the wall,Option 1,0.3156879904,13AUG1960,01JAN1960:07:10:46,0:01:18,100,54
|
||||
10218,218 bottles of beer on the wall,Option 1,0.3408455315,08JUN1961,01JAN1960:02:26:49,0:00:05,65,82
|
||||
10219,219 bottles of beer on the wall,Option 1,0.6263580553,08JUN1962,01JAN1960:05:59:46,0:01:03,76,88
|
||||
10220,220 bottles of beer on the wall,Option 1,0.2878925355,19DEC1961,01JAN1960:08:23:41,0:00:00,92,1
|
||||
10221,221 bottles of beer on the wall,Option 1,0.0901017348,19JUL1962,01JAN1960:09:50:47,0:00:43,21,84
|
||||
10222,222 bottles of beer on the wall,Option 1,0.8967759362,14SEP1960,01JAN1960:12:25:58,0:01:22,34,50
|
||||
10223,223 bottles of beer on the wall,Option 1,0.9878171943,03DEC1961,01JAN1960:03:43:09,0:00:17,11,84
|
||||
10224,224 bottles of beer on the wall,Option 1,0.5275036886,13DEC1961,01JAN1960:03:12:56,0:01:36,85,49
|
||||
10225,225 bottles of beer on the wall,Option 1,0.442012436,12JUN1960,01JAN1960:11:40:23,0:01:40,76,87
|
||||
10226,226 bottles of beer on the wall,Option 1,0.582103689,10FEB1961,01JAN1960:01:50:49,0:00:59,53,29
|
||||
10227,227 bottles of beer on the wall,Option 1,0.5757669842,01NOV1960,01JAN1960:13:47:33,0:00:43,55,6
|
||||
10228,228 bottles of beer on the wall,Option 1,0.4786617507,07JAN1960,01JAN1960:13:36:24,0:01:22,91,53
|
||||
10229,229 bottles of beer on the wall,Option 1,0.1386274957,06APR1962,01JAN1960:03:48:29,0:01:27,36,48
|
||||
10230,230 bottles of beer on the wall,Option 1,0.4188394893,31MAY1962,01JAN1960:10:30:51,0:00:54,5,87
|
||||
10231,231 bottles of beer on the wall,Option 1,0.9250617777,18OCT1960,01JAN1960:04:29:52,0:00:38,34,94
|
||||
10232,232 bottles of beer on the wall,Option 1,0.3077528124,05FEB1960,01JAN1960:09:37:42,0:01:13,58,75
|
||||
10233,233 bottles of beer on the wall,Option 1,0.7316332277,29NOV1960,01JAN1960:08:56:57,0:01:13,34,53
|
||||
10234,234 bottles of beer on the wall,Option 1,0.5666298352,21NOV1960,01JAN1960:07:51:09,0:01:08,97,71
|
||||
10235,235 bottles of beer on the wall,Option 1,0.5736639409,03JUL1962,01JAN1960:11:57:25,0:00:51,15,49
|
||||
10236,236 bottles of beer on the wall,Option 1,0.6785667616,11FEB1962,01JAN1960:09:47:20,0:00:50,65,21
|
||||
10237,237 bottles of beer on the wall,Option 1,0.3721726869,05JUL1962,01JAN1960:11:58:22,0:01:32,82,21
|
||||
10238,238 bottles of beer on the wall,Option 1,0.0332283876,17AUG1961,01JAN1960:13:11:34,0:00:54,83,30
|
||||
10239,239 bottles of beer on the wall,Option 1,0.9734656848,02JAN1961,01JAN1960:00:36:43,0:00:19,31,54
|
||||
10240,240 bottles of beer on the wall,Option 1,0.3022106021,16FEB1961,01JAN1960:13:50:38,0:00:40,22,66
|
||||
10241,241 bottles of beer on the wall,Option 1,0.7546903294,06JUL1961,01JAN1960:12:36:17,0:01:29,16,85
|
||||
10242,242 bottles of beer on the wall,Option 1,0.2509871834,07MAR1962,01JAN1960:10:38:28,0:00:39,7,8
|
||||
10243,243 bottles of beer on the wall,Option 1,0.9526996668,15JAN1960,01JAN1960:04:24:42,0:01:01,69,80
|
||||
10244,244 bottles of beer on the wall,Option 1,0.1816610122,06FEB1962,01JAN1960:08:46:51,0:00:54,89,91
|
||||
10245,245 bottles of beer on the wall,Option 1,0.3928658876,21JUL1962,01JAN1960:12:59:42,0:00:38,24,27
|
||||
10246,246 bottles of beer on the wall,Option 1,0.3774878524,18FEB1961,01JAN1960:07:40:49,0:01:31,88,93
|
||||
10247,247 bottles of beer on the wall,Option 1,0.6063659362,01NOV1960,01JAN1960:01:19:07,0:00:05,82,73
|
||||
10248,248 bottles of beer on the wall,Option 1,0.119603098,14JUN1960,01JAN1960:04:29:22,0:00:58,87,47
|
||||
10249,249 bottles of beer on the wall,Option 1,0.4833748445,03JUL1960,01JAN1960:01:53:54,0:00:37,34,33
|
||||
10250,250 bottles of beer on the wall,Option 1,0.2244539946,10AUG1961,01JAN1960:06:19:01,0:01:15,87,97
|
||||
10251,251 bottles of beer on the wall,Option 1,0.9368193191,11JUN1962,01JAN1960:06:37:14,0:00:46,94,39
|
||||
10252,252 bottles of beer on the wall,Option 1,0.1791427751,10NOV1961,01JAN1960:00:49:22,0:00:47,96,21
|
||||
10253,253 bottles of beer on the wall,Option 1,0.5836302874,06JUN1961,01JAN1960:08:39:34,0:01:01,78,49
|
||||
10254,254 bottles of beer on the wall,Option 1,0.1289398275,28DEC1960,01JAN1960:12:25:05,0:00:43,67,99
|
||||
10255,255 bottles of beer on the wall,Option 1,0.7833669785,05SEP1962,01JAN1960:02:47:35,0:00:20,25,2
|
||||
10256,256 bottles of beer on the wall,Option 1,0.4945342483,29JAN1960,01JAN1960:00:54:13,0:01:13,72,56
|
||||
10257,257 bottles of beer on the wall,Option 1,0.0635836129,05JAN1961,01JAN1960:08:10:04,0:00:52,11,10
|
||||
10258,258 bottles of beer on the wall,Option 1,0.8188241654,09FEB1962,01JAN1960:06:33:00,0:01:21,41,96
|
||||
10259,259 bottles of beer on the wall,Option 1,0.3398916076,11FEB1960,01JAN1960:07:12:29,0:00:56,18,76
|
||||
10260,260 bottles of beer on the wall,Option 1,0.0814064155,21MAY1961,01JAN1960:11:03:51,0:01:18,78,29
|
||||
10261,261 bottles of beer on the wall,Option 1,0.6653245542,20JAN1962,01JAN1960:08:03:31,0:00:18,39,95
|
||||
10262,262 bottles of beer on the wall,Option 1,0.4036777021,04AUG1962,01JAN1960:12:32:27,0:00:08,57,63
|
||||
10263,263 bottles of beer on the wall,Option 1,0.8931138603,07JAN1962,01JAN1960:09:04:24,0:00:32,6,27
|
||||
10264,264 bottles of beer on the wall,Option 1,0.528584433,06APR1962,01JAN1960:09:43:19,0:01:00,24,41
|
||||
10265,265 bottles of beer on the wall,Option 1,0.8267822945,29JUL1960,01JAN1960:00:48:11,0:00:01,81,78
|
||||
10266,266 bottles of beer on the wall,Option 1,0.7218411401,17FEB1960,01JAN1960:07:30:38,0:00:08,35,81
|
||||
10267,267 bottles of beer on the wall,Option 1,0.1475262773,11NOV1960,01JAN1960:13:44:20,0:00:57,36,68
|
||||
10268,268 bottles of beer on the wall,Option 1,0.9412727286,30DEC1960,01JAN1960:02:46:30,0:01:19,5,92
|
||||
10269,269 bottles of beer on the wall,Option 1,0.3038877548,27NOV1960,01JAN1960:10:50:10,0:01:21,43,95
|
||||
10270,270 bottles of beer on the wall,Option 1,0.2756435532,15APR1962,01JAN1960:09:05:28,0:01:34,11,14
|
||||
10271,271 bottles of beer on the wall,Option 1,0.7056001121,31AUG1960,01JAN1960:08:48:52,0:00:02,9,51
|
||||
10272,272 bottles of beer on the wall,Option 1,0.5273708508,21SEP1962,01JAN1960:12:58:13,0:00:28,97,69
|
||||
10273,273 bottles of beer on the wall,Option 1,0.6002807215,03MAY1960,01JAN1960:10:14:48,0:00:40,52,32
|
||||
10274,274 bottles of beer on the wall,Option 1,0.6100557971,20JUN1960,01JAN1960:08:11:55,0:00:27,90,14
|
||||
10275,275 bottles of beer on the wall,Option 1,0.4197408638,07JUN1961,01JAN1960:12:07:18,0:00:26,64,100
|
||||
10276,276 bottles of beer on the wall,Option 1,0.4903712498,19JAN1960,01JAN1960:01:06:26,0:00:03,35,24
|
||||
10277,277 bottles of beer on the wall,Option 1,0.6658435406,04NOV1960,01JAN1960:00:04:17,0:00:37,7,84
|
||||
10278,278 bottles of beer on the wall,Option 1,0.5491365942,14JAN1961,01JAN1960:04:12:49,0:00:27,99,47
|
||||
10279,279 bottles of beer on the wall,Option 1,0.4473488622,13MAY1961,01JAN1960:12:06:34,0:01:16,19,20
|
||||
10280,280 bottles of beer on the wall,Option 1,0.4511988663,06JUL1962,01JAN1960:10:05:51,0:00:56,76,34
|
||||
10281,281 bottles of beer on the wall,Option 1,0.0783031066,11JUN1961,01JAN1960:09:58:43,0:01:05,9,63
|
||||
10282,282 bottles of beer on the wall,Option 1,0.776985302,20JUL1962,01JAN1960:10:44:29,0:01:00,59,10
|
||||
10283,283 bottles of beer on the wall,Option 1,0.468099362,31AUG1962,01JAN1960:05:26:33,0:00:20,35,52
|
||||
10284,284 bottles of beer on the wall,Option 1,0.4040679696,20FEB1962,01JAN1960:06:27:25,0:00:04,76,30
|
||||
10285,285 bottles of beer on the wall,Option 1,0.4549995947,20FEB1962,01JAN1960:10:36:57,0:00:34,2,43
|
||||
10286,286 bottles of beer on the wall,Option 1,0.7455339361,16SEP1961,01JAN1960:08:39:35,0:01:00,42,44
|
||||
10287,287 bottles of beer on the wall,Option 1,0.0209561712,04JAN1960,01JAN1960:05:52:58,0:00:24,32,7
|
||||
10288,288 bottles of beer on the wall,Option 1,0.4955981842,04JAN1962,01JAN1960:02:56:03,0:00:30,85,31
|
||||
10289,289 bottles of beer on the wall,Option 1,0.4131368219,10FEB1960,01JAN1960:11:57:31,0:00:16,37,88
|
||||
10290,290 bottles of beer on the wall,Option 1,0.3282186721,17OCT1960,01JAN1960:10:54:04,0:00:56,72,28
|
||||
10291,291 bottles of beer on the wall,Option 1,0.2116929005,18JAN1962,01JAN1960:06:56:27,0:00:11,87,82
|
||||
10292,292 bottles of beer on the wall,Option 1,0.8483731937,12FEB1962,01JAN1960:05:05:41,0:01:36,12,83
|
||||
10293,293 bottles of beer on the wall,Option 1,0.1560111345,13NOV1960,01JAN1960:10:04:22,0:00:03,94,4
|
||||
10294,294 bottles of beer on the wall,Option 1,0.7046207808,12APR1962,01JAN1960:13:50:47,0:00:32,31,97
|
||||
10295,295 bottles of beer on the wall,Option 1,0.2716620403,04AUG1961,01JAN1960:01:52:29,0:00:57,99,44
|
||||
10296,296 bottles of beer on the wall,Option 1,0.5543203496,12SEP1960,01JAN1960:13:43:54,0:00:44,49,1
|
||||
10297,297 bottles of beer on the wall,Option 1,0.983109036,31JUL1962,01JAN1960:01:07:33,0:00:36,4,10
|
||||
10298,298 bottles of beer on the wall,Option 1,0.8123072115,14SEP1962,01JAN1960:06:16:12,0:01:25,88,96
|
||||
10299,299 bottles of beer on the wall,Option 1,0.4276896559,05OCT1960,01JAN1960:02:55:07,0:00:58,83,76
|
||||
10300,300 bottles of beer on the wall,Option 1,0.8921809042,19JAN1962,01JAN1960:02:05:38,0:00:12,80,13
|
||||
10301,301 bottles of beer on the wall,Option 1,0.6041374279,10DEC1961,01JAN1960:01:06:29,0:01:27,62,9
|
||||
10302,302 bottles of beer on the wall,Option 1,0.0460550185,31MAY1962,01JAN1960:03:03:56,0:00:05,33,88
|
||||
10303,303 bottles of beer on the wall,Option 1,0.1868385622,12APR1962,01JAN1960:12:42:44,0:01:05,65,18
|
||||
10304,304 bottles of beer on the wall,Option 1,0.3386632657,28SEP1961,01JAN1960:11:24:06,0:00:42,2,93
|
||||
10305,305 bottles of beer on the wall,Option 1,0.6400271019,01JUN1960,01JAN1960:13:33:07,0:01:30,60,72
|
||||
10306,306 bottles of beer on the wall,Option 1,0.9534907304,18NOV1961,01JAN1960:02:02:51,0:00:54,7,57
|
||||
10307,307 bottles of beer on the wall,Option 1,0.6663103745,06SEP1961,01JAN1960:05:36:49,0:00:43,88,2
|
||||
10308,308 bottles of beer on the wall,Option 1,0.5392553073,13FEB1962,01JAN1960:11:28:18,0:01:08,16,8
|
||||
10309,309 bottles of beer on the wall,Option 1,0.0747909025,17OCT1961,01JAN1960:08:36:12,0:00:41,49,42
|
||||
10310,310 bottles of beer on the wall,Option 1,0.3249381847,30SEP1960,01JAN1960:08:12:54,0:00:09,96,89
|
||||
10311,311 bottles of beer on the wall,Option 1,0.9231011951,19MAY1962,01JAN1960:05:10:33,0:00:50,30,9
|
||||
10312,312 bottles of beer on the wall,Option 1,0.4658221637,21MAY1961,01JAN1960:12:55:25,0:01:39,16,20
|
||||
10313,313 bottles of beer on the wall,Option 1,0.7215524673,21FEB1960,01JAN1960:02:00:07,0:01:40,95,94
|
||||
10314,314 bottles of beer on the wall,Option 1,0.7328679942,28OCT1961,01JAN1960:09:07:00,0:00:25,42,71
|
||||
10315,315 bottles of beer on the wall,Option 1,0.1276036776,12JUN1960,01JAN1960:01:54:08,0:00:56,57,42
|
||||
10316,316 bottles of beer on the wall,Option 1,0.1270824723,15SEP1960,01JAN1960:03:19:25,0:00:21,85,9
|
||||
10317,317 bottles of beer on the wall,Option 1,0.3750520117,13JUN1961,01JAN1960:04:33:09,0:01:15,24,20
|
||||
10318,318 bottles of beer on the wall,Option 1,0.5777822102,10DEC1960,01JAN1960:13:32:14,0:00:09,98,28
|
||||
10319,319 bottles of beer on the wall,Option 1,0.140476402,27AUG1962,01JAN1960:08:52:46,0:01:08,64,83
|
||||
10320,320 bottles of beer on the wall,Option 1,0.2589205551,31MAY1961,01JAN1960:08:33:06,0:00:53,28,98
|
||||
10321,321 bottles of beer on the wall,Option 1,0.7350722825,16SEP1962,01JAN1960:05:47:44,0:01:17,79,95
|
||||
10322,322 bottles of beer on the wall,Option 1,0.1476364542,15JAN1960,01JAN1960:12:21:20,0:00:20,86,62
|
||||
10323,323 bottles of beer on the wall,Option 1,0.8700561099,15MAY1962,01JAN1960:00:47:05,0:00:20,90,15
|
||||
10324,324 bottles of beer on the wall,Option 1,0.6408788802,12SEP1962,01JAN1960:11:50:31,0:00:53,41,72
|
||||
10325,325 bottles of beer on the wall,Option 1,0.6961101623,27NOV1960,01JAN1960:00:10:49,0:01:17,28,72
|
||||
10326,326 bottles of beer on the wall,Option 1,0.1467710059,24FEB1961,01JAN1960:01:13:38,0:00:33,14,5
|
||||
10327,327 bottles of beer on the wall,Option 1,0.0215573572,09JUN1961,01JAN1960:11:47:17,0:00:21,57,10
|
||||
10328,328 bottles of beer on the wall,Option 1,0.4173900054,25JUL1962,01JAN1960:12:28:20,0:00:23,73,90
|
||||
10329,329 bottles of beer on the wall,Option 1,0.6395625713,02NOV1961,01JAN1960:08:49:34,0:00:37,77,79
|
||||
10330,330 bottles of beer on the wall,Option 1,0.0091438908,18MAY1962,01JAN1960:05:10:05,0:00:41,15,31
|
||||
10331,331 bottles of beer on the wall,Option 1,0.1024675197,11DEC1960,01JAN1960:13:12:57,0:00:23,50,13
|
||||
10332,332 bottles of beer on the wall,Option 1,0.057470562,11MAY1961,01JAN1960:03:43:04,0:00:17,48,14
|
||||
10333,333 bottles of beer on the wall,Option 1,0.8478633872,21JUL1961,01JAN1960:03:45:42,0:01:31,22,40
|
||||
10334,334 bottles of beer on the wall,Option 1,0.3442252541,24JUN1960,01JAN1960:01:19:31,0:00:48,82,25
|
||||
10335,335 bottles of beer on the wall,Option 1,0.7338460184,06JUN1962,01JAN1960:03:32:34,0:01:04,6,31
|
||||
10336,336 bottles of beer on the wall,Option 1,0.6217917342,09MAR1961,01JAN1960:06:37:39,0:00:50,70,84
|
||||
10337,337 bottles of beer on the wall,Option 1,0.6684890807,10OCT1961,01JAN1960:05:34:24,0:01:20,66,18
|
||||
10338,338 bottles of beer on the wall,Option 1,0.3695247562,05SEP1962,01JAN1960:00:25:21,0:01:18,48,37
|
||||
10339,339 bottles of beer on the wall,Option 1,0.9429265987,06DEC1960,01JAN1960:07:11:17,0:00:38,59,1
|
||||
10340,340 bottles of beer on the wall,Option 1,0.9266307265,17JUL1960,01JAN1960:06:33:59,0:00:21,12,13
|
||||
10341,341 bottles of beer on the wall,Option 1,0.7280535543,23FEB1961,01JAN1960:05:01:10,0:00:34,73,25
|
||||
10342,342 bottles of beer on the wall,Option 1,0.488654495,15AUG1962,01JAN1960:01:24:33,0:00:56,59,25
|
||||
10343,343 bottles of beer on the wall,Option 1,0.9526806548,28DEC1960,01JAN1960:07:26:17,0:00:58,97,61
|
||||
10344,344 bottles of beer on the wall,Option 1,0.526025336,14JAN1960,01JAN1960:10:02:08,0:00:55,11,77
|
||||
10345,345 bottles of beer on the wall,Option 1,0.807215352,03JUL1961,01JAN1960:12:49:47,0:00:01,40,7
|
||||
10346,346 bottles of beer on the wall,Option 1,0.9305162979,28FEB1960,01JAN1960:09:46:40,0:00:59,56,28
|
||||
10347,347 bottles of beer on the wall,Option 1,0.7591318552,18FEB1962,01JAN1960:13:25:32,0:01:10,41,9
|
||||
10348,348 bottles of beer on the wall,Option 1,0.4177664911,11SEP1961,01JAN1960:09:55:17,0:01:39,76,82
|
||||
10349,349 bottles of beer on the wall,Option 1,0.4690050443,05DEC1961,01JAN1960:11:05:15,0:01:09,63,40
|
||||
10350,350 bottles of beer on the wall,Option 1,0.7541399979,31AUG1961,01JAN1960:12:30:45,0:00:33,57,12
|
||||
10351,351 bottles of beer on the wall,Option 1,0.1392844325,17MAR1962,01JAN1960:08:20:38,0:00:41,85,45
|
||||
10352,352 bottles of beer on the wall,Option 1,0.1020530235,23DEC1961,01JAN1960:09:46:20,0:00:01,55,56
|
||||
10353,353 bottles of beer on the wall,Option 1,0.0257998794,04DEC1961,01JAN1960:09:47:10,0:00:31,100,2
|
||||
10354,354 bottles of beer on the wall,Option 1,0.1238113316,20MAR1962,01JAN1960:09:15:30,0:00:01,74,11
|
||||
10355,355 bottles of beer on the wall,Option 1,0.4214245292,24NOV1960,01JAN1960:04:24:09,0:01:01,79,83
|
||||
10356,356 bottles of beer on the wall,Option 1,0.3243381057,12FEB1961,01JAN1960:00:55:59,0:00:50,30,52
|
||||
10357,357 bottles of beer on the wall,Option 1,0.9735697345,24NOV1960,01JAN1960:07:10:56,0:01:33,64,2
|
||||
10358,358 bottles of beer on the wall,Option 1,0.4796259461,28JAN1961,01JAN1960:11:51:29,0:01:03,19,29
|
||||
10359,359 bottles of beer on the wall,Option 1,0.003359966,01SEP1960,01JAN1960:13:24:25,0:00:09,66,60
|
||||
10360,360 bottles of beer on the wall,Option 1,0.5773700334,21JAN1960,01JAN1960:10:15:32,0:00:40,9,21
|
||||
10361,361 bottles of beer on the wall,Option 1,0.0792848342,25JAN1962,01JAN1960:06:00:35,0:01:25,73,73
|
||||
10362,362 bottles of beer on the wall,Option 1,0.9339359365,30MAY1961,01JAN1960:09:08:13,0:00:56,12,56
|
||||
10363,363 bottles of beer on the wall,Option 1,0.3517632132,12FEB1961,01JAN1960:12:07:19,0:00:01,74,69
|
||||
10364,364 bottles of beer on the wall,Option 1,0.4088954895,17MAR1961,01JAN1960:08:04:51,0:01:01,70,66
|
||||
10365,365 bottles of beer on the wall,Option 1,0.1254259623,30DEC1961,01JAN1960:06:47:12,0:00:01,79,43
|
||||
10366,366 bottles of beer on the wall,Option 1,0.9190132958,28MAY1961,01JAN1960:06:35:42,0:00:07,19,31
|
||||
10367,367 bottles of beer on the wall,Option 1,0.3033860015,17MAY1962,01JAN1960:05:32:03,0:00:32,57,73
|
||||
10368,368 bottles of beer on the wall,Option 1,0.1463442846,02SEP1962,01JAN1960:01:24:29,0:00:53,19,64
|
||||
10369,369 bottles of beer on the wall,Option 1,0.5516236343,18JUN1962,01JAN1960:10:52:23,0:00:11,51,40
|
||||
10370,370 bottles of beer on the wall,Option 1,0.5205378246,19JAN1960,01JAN1960:06:54:14,0:00:58,2,72
|
||||
10371,371 bottles of beer on the wall,Option 1,0.9941610768,28MAR1962,01JAN1960:04:55:58,0:00:22,46,65
|
||||
10372,372 bottles of beer on the wall,Option 1,0.065678,07MAY1961,01JAN1960:10:17:35,0:00:10,54,100
|
||||
10373,373 bottles of beer on the wall,Option 1,0.7222646138,17JUL1961,01JAN1960:01:47:32,0:00:51,26,96
|
||||
10374,374 bottles of beer on the wall,Option 1,0.8772228294,23JUL1960,01JAN1960:00:30:51,0:00:40,18,45
|
||||
10375,375 bottles of beer on the wall,Option 1,0.3651081847,11DEC1961,01JAN1960:00:46:15,0:00:15,14,90
|
||||
10376,376 bottles of beer on the wall,Option 1,0.3800431529,15AUG1960,01JAN1960:05:30:55,0:00:19,13,74
|
||||
10377,377 bottles of beer on the wall,Option 1,0.1077003503,26FEB1960,01JAN1960:04:48:40,0:00:08,51,53
|
||||
10378,378 bottles of beer on the wall,Option 1,0.7945664035,06MAR1961,01JAN1960:05:36:47,0:00:45,65,39
|
||||
10379,379 bottles of beer on the wall,Option 1,0.8754883054,06JUN1960,01JAN1960:06:09:33,0:00:04,3,3
|
||||
10380,380 bottles of beer on the wall,Option 1,0.9975561108,10AUG1960,01JAN1960:10:34:33,0:00:28,92,29
|
||||
10381,381 bottles of beer on the wall,Option 1,0.9817031599,07JUL1960,01JAN1960:01:40:00,0:00:39,63,45
|
||||
10382,382 bottles of beer on the wall,Option 1,0.4427802341,07JAN1962,01JAN1960:01:21:32,0:01:31,7,54
|
||||
10383,383 bottles of beer on the wall,Option 1,0.03180474,17JUL1962,01JAN1960:07:15:54,0:01:08,72,60
|
||||
10384,384 bottles of beer on the wall,Option 1,0.1031627707,10MAY1962,01JAN1960:02:52:58,0:01:31,6,64
|
||||
10385,385 bottles of beer on the wall,Option 1,0.911744344,01MAY1960,01JAN1960:03:51:16,0:01:04,1,54
|
||||
10386,386 bottles of beer on the wall,Option 1,0.4912374353,13FEB1961,01JAN1960:07:22:49,0:01:21,70,71
|
||||
10387,387 bottles of beer on the wall,Option 1,0.8803869509,04JUL1960,01JAN1960:12:14:05,0:00:18,78,88
|
||||
10388,388 bottles of beer on the wall,Option 1,0.0596609544,17DEC1960,01JAN1960:08:29:20,0:00:53,13,84
|
||||
10389,389 bottles of beer on the wall,Option 1,0.6625022132,12JAN1961,01JAN1960:11:15:39,0:01:05,10,31
|
||||
10390,390 bottles of beer on the wall,Option 1,0.2669677358,05OCT1961,01JAN1960:00:48:03,0:01:33,29,31
|
||||
10391,391 bottles of beer on the wall,Option 1,0.8095563454,04DEC1961,01JAN1960:07:54:13,0:01:15,95,2
|
||||
10392,392 bottles of beer on the wall,Option 1,0.6406704796,27JAN1961,01JAN1960:03:35:55,0:00:36,92,47
|
||||
10393,393 bottles of beer on the wall,Option 1,0.2188341847,02MAR1960,01JAN1960:04:05:21,0:00:11,28,34
|
||||
10394,394 bottles of beer on the wall,Option 1,0.7052043424,09JUN1962,01JAN1960:07:00:21,0:01:14,84,19
|
||||
10395,395 bottles of beer on the wall,Option 1,0.9843204464,18APR1960,01JAN1960:04:54:31,0:01:29,46,46
|
||||
10396,396 bottles of beer on the wall,Option 1,0.329249541,10SEP1961,01JAN1960:13:21:04,0:00:21,70,11
|
||||
10397,397 bottles of beer on the wall,Option 1,0.2073628675,13JUL1962,01JAN1960:11:16:26,0:01:23,17,97
|
||||
10398,398 bottles of beer on the wall,Option 1,0.7881676665,29JUN1962,01JAN1960:11:36:47,0:00:50,39,72
|
||||
10399,399 bottles of beer on the wall,Option 1,0.4347905537,31AUG1962,01JAN1960:02:30:30,0:00:21,86,61
|
||||
10400,400 bottles of beer on the wall,Option 1,0.2937454084,05APR1962,01JAN1960:02:44:06,0:00:32,27,81
|
||||
10401,401 bottles of beer on the wall,Option 1,0.6659902565,10APR1961,01JAN1960:11:53:59,0:00:01,80,17
|
||||
10402,402 bottles of beer on the wall,Option 1,0.7637229686,07APR1962,01JAN1960:05:45:25,0:01:28,11,17
|
||||
10403,403 bottles of beer on the wall,Option 1,0.3325480941,19MAR1961,01JAN1960:09:57:05,0:00:27,9,99
|
||||
10404,404 bottles of beer on the wall,Option 1,0.580015553,10AUG1960,01JAN1960:06:15:44,0:00:25,27,99
|
||||
10405,405 bottles of beer on the wall,Option 1,0.2514696071,08APR1961,01JAN1960:10:37:45,0:00:17,29,59
|
||||
10406,406 bottles of beer on the wall,Option 1,0.3792107284,19MAR1962,01JAN1960:04:49:30,0:00:07,44,38
|
||||
10407,407 bottles of beer on the wall,Option 1,0.4702913125,13DEC1961,01JAN1960:09:03:31,0:00:38,95,90
|
||||
10408,408 bottles of beer on the wall,Option 1,0.4207190659,03FEB1961,01JAN1960:00:37:47,0:00:38,37,65
|
||||
10409,409 bottles of beer on the wall,Option 1,0.2485069117,08DEC1961,01JAN1960:07:18:28,0:01:39,91,64
|
||||
10410,410 bottles of beer on the wall,Option 1,0.3257893502,27NOV1961,01JAN1960:11:33:02,0:00:54,41,7
|
||||
10411,411 bottles of beer on the wall,Option 1,0.0967283533,11AUG1962,01JAN1960:07:23:15,0:00:21,90,1
|
||||
10412,412 bottles of beer on the wall,Option 1,0.532676542,01SEP1960,01JAN1960:02:46:29,0:00:45,31,48
|
||||
10413,413 bottles of beer on the wall,Option 1,0.2564602151,15JAN1961,01JAN1960:01:09:11,0:01:21,16,89
|
||||
10414,414 bottles of beer on the wall,Option 1,0.0865634024,20MAR1962,01JAN1960:12:56:45,0:00:05,75,51
|
||||
10415,415 bottles of beer on the wall,Option 1,0.2926342321,07FEB1960,01JAN1960:06:48:55,0:00:36,66,33
|
||||
10416,416 bottles of beer on the wall,Option 1,0.0678594029,06OCT1961,01JAN1960:07:14:58,0:01:26,50,16
|
||||
10417,417 bottles of beer on the wall,Option 1,0.6376664767,09JUN1960,01JAN1960:05:00:13,0:01:37,7,92
|
||||
10418,418 bottles of beer on the wall,Option 1,0.4795483525,14JUN1962,01JAN1960:00:00:50,0:00:50,33,74
|
||||
10419,419 bottles of beer on the wall,Option 1,0.3492934342,22MAR1960,01JAN1960:08:54:52,0:01:26,61,72
|
||||
10420,420 bottles of beer on the wall,Option 1,0.5085071356,14MAR1960,01JAN1960:09:47:51,0:00:56,36,63
|
||||
10421,421 bottles of beer on the wall,Option 1,0.414953564,25MAR1961,01JAN1960:03:35:19,0:00:30,47,30
|
||||
10422,422 bottles of beer on the wall,Option 1,0.2431976652,25SEP1960,01JAN1960:08:47:25,0:00:17,6,42
|
||||
10423,423 bottles of beer on the wall,Option 1,0.2370444998,04MAR1962,01JAN1960:00:16:44,0:01:36,54,100
|
||||
10424,424 bottles of beer on the wall,Option 1,0.8687893324,12OCT1961,01JAN1960:03:16:33,0:00:56,97,25
|
||||
10425,425 bottles of beer on the wall,Option 1,0.0470510335,10MAY1960,01JAN1960:02:44:07,0:01:05,83,59
|
||||
10426,426 bottles of beer on the wall,Option 1,0.7114342887,12APR1960,01JAN1960:02:17:40,0:00:39,57,97
|
||||
10427,427 bottles of beer on the wall,Option 1,0.6176668283,05JUL1962,01JAN1960:09:09:12,0:01:03,92,38
|
||||
10428,428 bottles of beer on the wall,Option 1,0.0657907743,01JUL1962,01JAN1960:11:03:37,0:01:35,97,63
|
||||
10429,429 bottles of beer on the wall,Option 1,0.280104912,13MAR1962,01JAN1960:10:35:08,0:01:07,42,35
|
||||
10430,430 bottles of beer on the wall,Option 1,0.3639827088,02APR1960,01JAN1960:12:00:50,0:00:40,91,84
|
||||
10431,431 bottles of beer on the wall,Option 1,0.2130561137,25AUG1961,01JAN1960:12:07:22,0:01:37,70,9
|
||||
10432,432 bottles of beer on the wall,Option 1,0.9656705889,23DEC1960,01JAN1960:02:36:48,0:01:28,23,92
|
||||
10433,433 bottles of beer on the wall,Option 1,0.5824601052,22JAN1961,01JAN1960:01:19:48,0:00:32,73,6
|
||||
10434,434 bottles of beer on the wall,Option 1,0.5207124942,23APR1961,01JAN1960:10:45:16,0:01:08,45,85
|
||||
10435,435 bottles of beer on the wall,Option 1,0.9280530661,01NOV1960,01JAN1960:12:46:59,0:01:36,57,4
|
||||
10436,436 bottles of beer on the wall,Option 1,0.8982810149,29NOV1961,01JAN1960:09:23:13,0:01:15,100,68
|
||||
10437,437 bottles of beer on the wall,Option 1,0.3268259467,04MAR1960,01JAN1960:01:22:42,0:00:04,99,61
|
||||
10438,438 bottles of beer on the wall,Option 1,0.2038002704,12FEB1962,01JAN1960:09:03:24,0:00:40,97,78
|
||||
10439,439 bottles of beer on the wall,Option 1,0.0798850833,11MAY1960,01JAN1960:00:32:50,0:00:37,7,49
|
||||
10440,440 bottles of beer on the wall,Option 1,0.6697000566,01SEP1960,01JAN1960:08:04:03,0:00:30,7,68
|
||||
10441,441 bottles of beer on the wall,Option 1,0.550699767,11JAN1960,01JAN1960:06:33:15,0:01:09,19,10
|
||||
10442,442 bottles of beer on the wall,Option 1,0.9514032798,07FEB1962,01JAN1960:06:03:16,0:01:13,65,87
|
||||
10443,443 bottles of beer on the wall,Option 1,0.1182718324,08FEB1960,01JAN1960:11:28:52,0:00:46,61,59
|
||||
10444,444 bottles of beer on the wall,Option 1,0.5772118874,11FEB1962,01JAN1960:01:14:29,0:01:07,16,22
|
||||
10445,445 bottles of beer on the wall,Option 1,0.9828714463,06DEC1960,01JAN1960:05:47:08,0:01:40,66,4
|
||||
10446,446 bottles of beer on the wall,Option 1,0.1229167269,02JUN1960,01JAN1960:09:43:51,0:01:34,29,24
|
||||
10447,447 bottles of beer on the wall,Option 1,0.2102257522,09MAR1961,01JAN1960:04:06:09,0:01:27,53,78
|
||||
10448,448 bottles of beer on the wall,Option 1,0.9958884935,20FEB1962,01JAN1960:06:43:53,0:00:07,42,65
|
||||
10449,449 bottles of beer on the wall,Option 1,0.3530408085,04MAY1961,01JAN1960:11:26:59,0:01:37,76,35
|
||||
10450,450 bottles of beer on the wall,Option 1,0.2848354258,03SEP1960,01JAN1960:13:26:36,0:00:25,69,38
|
||||
10451,451 bottles of beer on the wall,Option 1,0.1461860818,10JUN1960,01JAN1960:01:24:25,0:00:02,61,23
|
||||
10452,452 bottles of beer on the wall,Option 1,0.2033735766,29AUG1960,01JAN1960:12:30:17,0:00:19,32,75
|
||||
10453,453 bottles of beer on the wall,Option 1,0.7973149958,26JUN1961,01JAN1960:04:46:06,0:00:21,94,14
|
||||
10454,454 bottles of beer on the wall,Option 1,0.6812086425,07NOV1961,01JAN1960:01:59:51,0:00:12,89,59
|
||||
10455,455 bottles of beer on the wall,Option 1,0.6111464974,24FEB1962,01JAN1960:07:42:56,0:00:15,23,92
|
||||
10456,456 bottles of beer on the wall,Option 1,0.9542484209,09APR1961,01JAN1960:09:09:17,0:01:26,87,93
|
||||
10457,457 bottles of beer on the wall,Option 1,0.9818577077,18AUG1960,01JAN1960:12:24:47,0:01:08,18,20
|
||||
10458,458 bottles of beer on the wall,Option 1,0.9331120881,06JUL1961,01JAN1960:10:10:02,0:01:40,78,36
|
||||
10459,459 bottles of beer on the wall,Option 1,0.8953045587,22APR1960,01JAN1960:04:47:34,0:00:37,13,46
|
||||
10460,460 bottles of beer on the wall,Option 1,0.1584420461,08JAN1961,01JAN1960:07:47:49,0:00:05,85,61
|
||||
10461,461 bottles of beer on the wall,Option 1,0.9324573814,24DEC1960,01JAN1960:02:13:36,0:00:22,55,94
|
||||
10462,462 bottles of beer on the wall,Option 1,0.0813189871,21AUG1961,01JAN1960:09:18:28,0:00:36,42,49
|
||||
10463,463 bottles of beer on the wall,Option 1,0.8736312594,17MAY1962,01JAN1960:12:52:58,0:00:30,45,7
|
||||
10464,464 bottles of beer on the wall,Option 1,0.1894013794,06NOV1960,01JAN1960:12:56:13,0:01:30,11,50
|
||||
10465,465 bottles of beer on the wall,Option 1,0.9149773772,07JUN1960,01JAN1960:08:53:28,0:00:22,23,17
|
||||
10466,466 bottles of beer on the wall,Option 1,0.6329479598,27JUN1962,01JAN1960:07:38:23,0:00:06,68,14
|
||||
10467,467 bottles of beer on the wall,Option 1,0.1897066413,21AUG1961,01JAN1960:10:15:28,0:01:12,59,46
|
||||
10468,468 bottles of beer on the wall,Option 1,0.8352205767,16MAR1961,01JAN1960:06:48:50,0:00:44,60,37
|
||||
10469,469 bottles of beer on the wall,Option 1,0.3339443432,09OCT1960,01JAN1960:00:52:53,0:00:27,70,44
|
||||
10470,470 bottles of beer on the wall,Option 1,0.1096137567,09JUL1962,01JAN1960:08:28:10,0:01:15,88,81
|
||||
10471,471 bottles of beer on the wall,Option 1,0.1481936733,10JAN1962,01JAN1960:06:38:41,0:00:56,21,77
|
||||
10472,472 bottles of beer on the wall,Option 1,0.5630390432,01MAR1960,01JAN1960:06:35:09,0:01:08,70,32
|
||||
10473,473 bottles of beer on the wall,Option 1,0.0604085713,12APR1962,01JAN1960:02:41:26,0:00:46,36,17
|
||||
10474,474 bottles of beer on the wall,Option 1,0.6011423606,25DEC1961,01JAN1960:02:57:45,0:01:11,36,18
|
||||
10475,475 bottles of beer on the wall,Option 1,0.7698335782,31JUL1962,01JAN1960:07:46:29,0:01:34,72,17
|
||||
10476,476 bottles of beer on the wall,Option 1,0.3823641224,17MAR1962,01JAN1960:03:34:37,0:00:09,27,86
|
||||
10477,477 bottles of beer on the wall,Option 1,0.9225378446,12JAN1962,01JAN1960:08:41:25,0:01:06,45,20
|
||||
10478,478 bottles of beer on the wall,Option 1,0.2979750453,23JUN1962,01JAN1960:10:06:36,0:01:28,88,7
|
||||
10479,479 bottles of beer on the wall,Option 1,0.4988665942,15JUL1961,01JAN1960:02:33:06,0:01:33,85,1
|
||||
10480,480 bottles of beer on the wall,Option 1,0.5243853585,29JUL1960,01JAN1960:00:26:18,0:00:31,48,77
|
||||
10481,481 bottles of beer on the wall,Option 1,0.6049248826,12JUL1962,01JAN1960:10:01:41,0:00:16,66,45
|
||||
10482,482 bottles of beer on the wall,Option 1,0.6312438024,01OCT1961,01JAN1960:05:18:12,0:00:04,43,27
|
||||
10483,483 bottles of beer on the wall,Option 1,0.3366865974,17MAY1962,01JAN1960:01:44:17,0:00:01,31,25
|
||||
10484,484 bottles of beer on the wall,Option 1,0.0813266188,08AUG1962,01JAN1960:06:18:27,0:00:08,71,63
|
||||
10485,485 bottles of beer on the wall,Option 1,0.3601163455,23JAN1962,01JAN1960:11:26:01,0:00:37,41,88
|
||||
10486,486 bottles of beer on the wall,Option 1,0.9033777345,13AUG1961,01JAN1960:00:16:14,0:00:56,82,33
|
||||
10487,487 bottles of beer on the wall,Option 1,0.1716986667,23DEC1960,01JAN1960:12:06:34,0:01:13,2,32
|
||||
10488,488 bottles of beer on the wall,Option 1,0.9734818912,11SEP1961,01JAN1960:00:34:41,0:00:28,17,7
|
||||
10489,489 bottles of beer on the wall,Option 1,0.0618140223,17DEC1961,01JAN1960:06:26:30,0:00:38,94,40
|
||||
10490,490 bottles of beer on the wall,Option 1,0.0204490144,22AUG1960,01JAN1960:01:50:18,0:00:19,40,56
|
||||
10491,491 bottles of beer on the wall,Option 1,0.8719323929,23MAY1960,01JAN1960:12:36:06,0:00:08,49,44
|
||||
10492,492 bottles of beer on the wall,Option 1,0.2656292181,28JUN1962,01JAN1960:05:32:50,0:01:35,15,39
|
||||
10493,493 bottles of beer on the wall,Option 1,0.37794835,23JUL1962,01JAN1960:13:15:43,0:00:56,6,86
|
||||
10494,494 bottles of beer on the wall,Option 1,0.6395865733,17JAN1961,01JAN1960:12:20:12,0:01:31,22,87
|
||||
10495,495 bottles of beer on the wall,Option 1,0.4408595098,05JUL1960,01JAN1960:12:21:53,0:01:40,25,27
|
||||
10496,496 bottles of beer on the wall,Option 1,0.4846261169,02DEC1961,01JAN1960:12:41:08,0:00:06,7,63
|
||||
10497,497 bottles of beer on the wall,Option 1,0.7903671478,14MAY1962,01JAN1960:05:09:21,0:01:03,98,25
|
||||
10498,498 bottles of beer on the wall,Option 1,0.60474184,13OCT1961,01JAN1960:03:52:59,0:01:22,98,98
|
||||
10499,499 bottles of beer on the wall,Option 1,0.1213259218,23APR1962,01JAN1960:06:29:47,0:01:35,38,70
|
||||
10500,500 bottles of beer on the wall,Option 1,0.7882370487,17FEB1962,01JAN1960:02:00:25,0:00:12,1,74
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -23,21 +23,33 @@
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
import 'cypress-file-upload';
|
||||
import 'cypress-file-upload'
|
||||
|
||||
import { arrayBufferToBase64 } from './../util/helper-functions'
|
||||
import * as moment from 'moment'
|
||||
import moment from 'moment'
|
||||
|
||||
const username = Cypress.env('username');
|
||||
const password = Cypress.env('password');
|
||||
const hostUrl = Cypress.env('hosturl');
|
||||
const appLocation = Cypress.env('appLocation');
|
||||
// These custom commands were added via Cypress.Commands.add() below but never
|
||||
// had a type augmentation, so every spec calling cy.loginAndUpdateValidKey()/
|
||||
// cy.isLoggedIn() had an unresolved-property error.
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
isLoggedIn(callback: (exist: boolean) => void): Chainable<void>
|
||||
loginAndUpdateValidKey(forceLicenceKey?: boolean): Chainable<void>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const username = Cypress.env('username')
|
||||
const password = Cypress.env('password')
|
||||
const hostUrl = Cypress.env('hosturl')
|
||||
const appLocation = Cypress.env('appLocation')
|
||||
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
||||
const site_id_SASJS = Cypress.env('site_id_SASJS')
|
||||
|
||||
Cypress.Commands.add('isLoggedIn', (callback: (exist: boolean) => void) => {
|
||||
cy.get('body').then($body => {
|
||||
if ($body.find(".nav-tree").length > 0) {
|
||||
cy.get('body').then(($body) => {
|
||||
if ($body.find('.nav-tree').length > 0) {
|
||||
if (callback) callback(true)
|
||||
} else {
|
||||
if (callback) callback(false)
|
||||
@@ -45,76 +57,92 @@ Cypress.Commands.add('isLoggedIn', (callback: (exist: boolean) => void) => {
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add('loginAndUpdateValidKey', (forceLicenceKey: boolean = false) => {
|
||||
cy.visit(hostUrl + appLocation);
|
||||
Cypress.Commands.add(
|
||||
'loginAndUpdateValidKey',
|
||||
(forceLicenceKey: boolean = false) => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
|
||||
cy.wait(2000)
|
||||
cy.wait(2000)
|
||||
|
||||
cy.get('body').then($body =>{
|
||||
const usernameInput = $body.find("input.username")[0]
|
||||
cy.get('body').then(($body) => {
|
||||
const usernameInput = $body.find('input.username')[0]
|
||||
|
||||
if (usernameInput && !Cypress.dom.isHidden(usernameInput)) {
|
||||
cy.get('input.username').type(username);
|
||||
cy.get('input.password').type(password);
|
||||
if (usernameInput && !Cypress.dom.isHidden(usernameInput)) {
|
||||
cy.get('input.username').type(username)
|
||||
cy.get('input.password').type(password)
|
||||
|
||||
cy.get('.login-group button').click()
|
||||
}
|
||||
cy.get('.login-group button').click()
|
||||
}
|
||||
|
||||
cy.get('.app-loading', {timeout: longerCommandTimeout}).should('not.exist').then(() => {
|
||||
cy.wait(2000)
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout })
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.wait(2000)
|
||||
|
||||
if ($body.find(".nav-tree").length > 0) {
|
||||
/**
|
||||
* If licence key is already working, then skip rest of the function
|
||||
*/
|
||||
return logout(() => {
|
||||
return
|
||||
})
|
||||
} else{
|
||||
const keyData = {
|
||||
valid_until: moment().add(20, 'day').format('YYYY-MM-DD'),
|
||||
number_of_users: 10,
|
||||
hot_license_key: '',
|
||||
site_id: '',
|
||||
demo: false
|
||||
}
|
||||
|
||||
return generateKeys(keyData.valid_until, keyData.number_of_users, keyData.hot_license_key, keyData.demo, (keysGen: any) => {
|
||||
return acceptTermsIfPresented((result: boolean) => {
|
||||
if (result) {
|
||||
cy.wait(20000)
|
||||
if ($body.find('.nav-tree').length > 0) {
|
||||
/**
|
||||
* If licence key is already working, then skip rest of the function
|
||||
*/
|
||||
return logout(() => {
|
||||
return
|
||||
})
|
||||
} else {
|
||||
const keyData = {
|
||||
valid_until: moment().add(20, 'day').format('YYYY-MM-DD'),
|
||||
number_of_users: 10,
|
||||
hot_license_key: '',
|
||||
site_id: '',
|
||||
demo: false
|
||||
}
|
||||
|
||||
if (!forceLicenceKey) {
|
||||
return logout(() => {
|
||||
return
|
||||
})
|
||||
} else {
|
||||
return updateLicenseKeyQuick(keysGen, () => {
|
||||
cy.wait(25000)
|
||||
return generateKeys(
|
||||
keyData.valid_until,
|
||||
keyData.number_of_users,
|
||||
keyData.hot_license_key,
|
||||
keyData.demo,
|
||||
(keysGen: any) => {
|
||||
return acceptTermsIfPresented((result: boolean) => {
|
||||
if (result) {
|
||||
cy.wait(20000)
|
||||
}
|
||||
return logout(() => {
|
||||
return
|
||||
})
|
||||
|
||||
if (!forceLicenceKey) {
|
||||
return logout(() => {
|
||||
return
|
||||
})
|
||||
} else {
|
||||
return updateLicenseKeyQuick(keysGen, () => {
|
||||
cy.wait(25000)
|
||||
return acceptTermsIfPresented((result: boolean) => {
|
||||
if (result) {
|
||||
cy.wait(20000)
|
||||
}
|
||||
return logout(() => {
|
||||
return
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
)
|
||||
|
||||
const logout = (callback?: any) => {
|
||||
cy.get('.header-actions .dropdown-toggle').click().then(() => {
|
||||
cy.get('.header-actions .dropdown-menu > .separator').next().click().then(() => {
|
||||
if (callback) callback()
|
||||
cy.get('.header-actions .dropdown-toggle')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('.header-actions .dropdown-menu > .separator')
|
||||
.next()
|
||||
.click()
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const updateLicenseKeyQuick = (keys: any, callback: any) => {
|
||||
@@ -132,11 +160,15 @@ const updateLicenseKeyQuick = (keys: any, callback: any) => {
|
||||
const acceptTermsIfPresented = (callback?: any) => {
|
||||
cy.url().then((url: string) => {
|
||||
if (url.includes('licensing/register')) {
|
||||
cy.get('.card-block').scrollTo('bottom').then(() => {
|
||||
cy.get('#checkbox1').click().then(() => {
|
||||
if (callback) callback(true)
|
||||
cy.get('.card-block')
|
||||
.scrollTo('bottom')
|
||||
.then(() => {
|
||||
cy.get('#checkbox1')
|
||||
.click()
|
||||
.then(() => {
|
||||
if (callback) callback(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
} else {
|
||||
if (callback) callback(false)
|
||||
}
|
||||
@@ -151,23 +183,36 @@ const isLicensingPage = (callback: any) => {
|
||||
|
||||
const inputLicenseKeyPage = (licenseKey: string, activationKey: string) => {
|
||||
cy.get('button').contains('Paste licence').click()
|
||||
cy.get('.license-key-form textarea', {timeout: longerCommandTimeout}).invoke('val', licenseKey).trigger('input').should('not.be.undefined')
|
||||
cy.get('.activation-key-form textarea', {timeout: longerCommandTimeout}).invoke('val', activationKey).trigger('input').should('not.be.undefined')
|
||||
cy.get('.license-key-form textarea', { timeout: longerCommandTimeout })
|
||||
.invoke('val', licenseKey)
|
||||
.trigger('input')
|
||||
.should('not.be.undefined')
|
||||
cy.get('.activation-key-form textarea', { timeout: longerCommandTimeout })
|
||||
.invoke('val', activationKey)
|
||||
.trigger('input')
|
||||
.should('not.be.undefined')
|
||||
cy.get('button.apply-keys').click()
|
||||
}
|
||||
|
||||
const visitPage = (url: string) => {
|
||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`);
|
||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
||||
}
|
||||
const generateKeys = async (valid_until: string, users_allowed: number, hot_license_key: string, demo: boolean, resultCallback?: any) => {
|
||||
let keyPair = await window.crypto.subtle.generateKey({
|
||||
name: "RSA-OAEP",
|
||||
modulusLength: 2024,
|
||||
publicExponent: new Uint8Array([1, 0, 1]),
|
||||
hash: "SHA-256"
|
||||
const generateKeys = async (
|
||||
valid_until: string,
|
||||
users_allowed: number,
|
||||
hot_license_key: string,
|
||||
demo: boolean,
|
||||
resultCallback?: any
|
||||
) => {
|
||||
let keyPair = await window.crypto.subtle.generateKey(
|
||||
{
|
||||
name: 'RSA-OAEP',
|
||||
modulusLength: 2024,
|
||||
publicExponent: new Uint8Array([1, 0, 1]),
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
true,
|
||||
["encrypt", "decrypt"]
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
|
||||
let licenseData = {
|
||||
@@ -184,30 +229,39 @@ const generateKeys = async (valid_until: string, users_allowed: number, hot_lice
|
||||
|
||||
console.log(encoded)
|
||||
|
||||
let cipher = await window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: "RSA-OAEP"
|
||||
},
|
||||
keyPair.publicKey,
|
||||
encoded
|
||||
).then((value) => {
|
||||
return value
|
||||
}, (err) => {
|
||||
console.log('Encrpyt error', err)
|
||||
})
|
||||
let cipher = await window.crypto.subtle
|
||||
.encrypt(
|
||||
{
|
||||
name: 'RSA-OAEP'
|
||||
},
|
||||
keyPair.publicKey,
|
||||
encoded
|
||||
)
|
||||
.then(
|
||||
(value) => {
|
||||
return value
|
||||
},
|
||||
(err) => {
|
||||
console.log('Encrpyt error', err)
|
||||
}
|
||||
)
|
||||
|
||||
if (!cipher) {
|
||||
alert('Encryptin keys failed')
|
||||
throw new Error('Encryptin keys failed')
|
||||
}
|
||||
|
||||
let privateKeyBytes = await window.crypto.subtle.exportKey('pkcs8', keyPair.privateKey)
|
||||
let privateKeyBytes = await window.crypto.subtle.exportKey(
|
||||
'pkcs8',
|
||||
keyPair.privateKey
|
||||
)
|
||||
|
||||
let activationKey = await arrayBufferToBase64(privateKeyBytes)
|
||||
let licenseKey = await arrayBufferToBase64(cipher)
|
||||
|
||||
if (resultCallback) resultCallback({
|
||||
activationKey,
|
||||
licenseKey
|
||||
})
|
||||
}
|
||||
if (resultCallback)
|
||||
resultCallback({
|
||||
activationKey,
|
||||
licenseKey
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// ***********************************************************
|
||||
// This example support/e2e.ts is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
|
||||
import 'cypress-plugin-tab'
|
||||
import 'cypress-real-events'
|
||||
|
||||
// Pin the locale
|
||||
Cypress.on('window:before:load', (win) => {
|
||||
Object.defineProperty(win.navigator, 'language', {
|
||||
value: 'en-GB',
|
||||
configurable: true
|
||||
})
|
||||
Object.defineProperty(win.navigator, 'languages', {
|
||||
value: ['en-GB'],
|
||||
configurable: true
|
||||
})
|
||||
})
|
||||
@@ -6,5 +6,8 @@
|
||||
"lib": ["es2019", "dom"],
|
||||
"types": ["cypress", "cypress-real-events"]
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"../cypress.config.ts"
|
||||
]
|
||||
}
|
||||
@@ -42,4 +42,4 @@ module.exports = function (config) {
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -10,7 +10,7 @@ const check = (cwd) => {
|
||||
onlyAllow:
|
||||
'AFLv2.1;Apache 2.0;Apache-2.0;Apache*;Artistic-2.0;0BSD;BSD*;BSD-2-Clause;BSD-3-Clause;CC0-1.0;CC-BY-3.0;CC-BY-4.0;ISC;MIT;MPL-2.0;ODC-By-1.0;Python-2.0;Unlicense;',
|
||||
excludePackages:
|
||||
'@cds/city@1.1.0;@handsontable/angular@13.0.0;handsontable@13.0.0;hyperformula@2.5.0;jackspeak@2.2.0;path-scurry@1.7.0'
|
||||
'@cds/city@1.1.0;@handsontable/angular-wrapper@16.0.1;@handsontable/angular-wrapper@17.1.0;@handsontable/angular-wrapper@18.0.0;handsontable@^16.0.1;handsontable@16.2.0;handsontable@17.1.0;handsontable@18.0.0;hyperformula@2.7.1;hyperformula@3.0.0;hyperformula@3.1.0;hyperformula@3.2.0;hyperformula@3.3.0;jackspeak@3.4.3;path-scurry@1.11.1;package-json-from-dist@1.0.1;buffers@0.1.1'
|
||||
},
|
||||
(error, json) => {
|
||||
if (error) {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
module.exports = {
|
||||
ci: {
|
||||
collect: {
|
||||
settings: {
|
||||
preset: 'desktop',
|
||||
chromeFlags: '--no-sandbox --disable-dev-shm-usage'
|
||||
},
|
||||
url: [
|
||||
'http://localhost:5000/AppStream/clickme/#/home/tables',
|
||||
'http://localhost:5000/AppStream/clickme/#/editor/DC996664.MPE_X_TEST',
|
||||
'http://localhost:5000/AppStream/clickme/#/view/data',
|
||||
'http://localhost:5000/AppStream/clickme/#/view/data/DC996664',
|
||||
'http://localhost:5000/AppStream/clickme/#/view/data/DC996664.MPE_X_TEST',
|
||||
'http://localhost:5000/AppStream/clickme/#/view/usernav/groups',
|
||||
'http://localhost:5000/AppStream/clickme/#/view/usernav/groups',
|
||||
'http://localhost:5000/AppStream/clickme/#/view/usernav/groups/1',
|
||||
'http://localhost:5000/AppStream/clickme/#/view/usernav/users/1',
|
||||
'http://localhost:5000/AppStream/clickme/#/home/excel-maps',
|
||||
'http://localhost:5000/AppStream/clickme/#/home/excel-maps/BASEL-CR2',
|
||||
'http://localhost:5000/AppStream/clickme/#/home/multi-load',
|
||||
'http://localhost:5000/AppStream/clickme/#/review/submitted',
|
||||
'http://localhost:5000/AppStream/clickme/#/review/approve',
|
||||
'http://localhost:5000/AppStream/clickme/#/review/history',
|
||||
'http://localhost:5000/AppStream/clickme/#/stage/DC20221006T142649516_059582_7169',
|
||||
'http://localhost:5000/AppStream/clickme/#/review/submitted/DC20221006T142649516_059582_7169',
|
||||
'http://localhost:5000/AppStream/clickme/#/system'
|
||||
]
|
||||
},
|
||||
assert: {
|
||||
assertions: {
|
||||
'categories:accessibility': [
|
||||
'error',
|
||||
{ minScore: 1, aggregationMethod: 'median' }
|
||||
],
|
||||
'categories:performance': [
|
||||
'error',
|
||||
{ minScore: 0.4, aggregationMethod: 'median' }
|
||||
]
|
||||
}
|
||||
},
|
||||
upload: {
|
||||
target: 'filesystem',
|
||||
outputDir: './lighthouse-reports'
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+16549
-20905
File diff suppressed because it is too large
Load Diff
+74
-55
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dc-client",
|
||||
"description": "dc-client",
|
||||
"name": "data_controller-client",
|
||||
"description": "DataController Client",
|
||||
"angular-cli": {},
|
||||
"scripts": {
|
||||
"start": "node --max_old_space_size=4096 node_modules/@angular/cli/bin/ng serve",
|
||||
@@ -17,59 +17,67 @@
|
||||
"deploy_viya": "rsync -avhe ssh ./dist/* --delete ${npm_config_account}@sas.4gl.io:/var/www/html/${npm_config_account}/dc/dev",
|
||||
"deploy_sasjs": "rsync -avhe ssh ./dist/* --delete root@${npm_config_account}.4gl.io:/var/www/html/dc/dev",
|
||||
"viyabuild": "cd build; ./viyabuild.sh",
|
||||
"lint": "cd .. && npm run lint",
|
||||
"test": "ng test",
|
||||
"test:headless": "ng test --browsers ChromeHeadless",
|
||||
"lint": "npm run lint:fix",
|
||||
"lint:fix": "npx prettier --write \"{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\" \"cypress/e2e/*.cy.ts\"",
|
||||
"lint:fix:silent": "npx prettier --log-level silent --write \"{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\" \"cypress/e2e/*.cy.ts\"",
|
||||
"lint:check": "npx prettier --check \"{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\" \"cypress/e2e/*.cy.ts\"",
|
||||
"lint:check:silent": "npx prettier --log-level silent --check \"{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\" \"cypress/e2e/*.cy.ts\"",
|
||||
"test": "npx ng test",
|
||||
"test:headless": "npx ng test --no-watch --no-progress --browsers ChromeHeadlessCI",
|
||||
"watch": "ng test watch=true",
|
||||
"pree2e": "webdriver-manager update",
|
||||
"e2e": "protractor protractor.config.js",
|
||||
"postinstall": "node ./src/version.ts && npm run add-githook",
|
||||
"postinstall": "node ./src/version.ts && npm run add-githook && node ./scripts/strip-clr-base64-fonts.mjs && node ./scripts/gen-hot-icons.mjs",
|
||||
"add-githook": "[ -d ../.git ] && git config core.hooksPath ./.git-hooks || true",
|
||||
"cypress": "cypress open",
|
||||
"cy:run": "cypress run",
|
||||
"audit:prod": "npm audit --omit=dev",
|
||||
"sasdocs": "sasjs doc && ./sasjs/utils/deploydocs.sh",
|
||||
"typedoc": "typedoc --options typedoc.json && cd ../dc-devdocs"
|
||||
"compodoc:build": "compodoc -p tsconfig.doc.json --name 'Data Controller Client'",
|
||||
"compodoc:build-and-serve": "compodoc -p tsconfig.doc.json -s --name 'Data Controller Client'",
|
||||
"compodoc:serve": "compodoc -s --name 'Data Controller Client'",
|
||||
"lighthouse": "lhci autorun",
|
||||
"ng": "ng"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^16.1.2",
|
||||
"@angular/cdk": "^15.2.0",
|
||||
"@angular/common": "^16.1.2",
|
||||
"@angular/compiler": "^16.1.2",
|
||||
"@angular/core": "^16.1.2",
|
||||
"@angular/forms": "^16.1.2",
|
||||
"@angular/platform-browser": "^16.1.2",
|
||||
"@angular/platform-browser-dynamic": "^16.1.2",
|
||||
"@angular/router": "^16.1.2",
|
||||
"@cds/core": "^6.4.2",
|
||||
"@clr/angular": "^13.17.0",
|
||||
"@angular/animations": "^19.2.20",
|
||||
"@angular/cdk": "^19.2.19",
|
||||
"@angular/common": "^19.2.20",
|
||||
"@angular/compiler": "^19.2.20",
|
||||
"@angular/core": "^19.2.20",
|
||||
"@angular/forms": "^19.2.20",
|
||||
"@angular/platform-browser": "^19.2.20",
|
||||
"@angular/platform-browser-dynamic": "^19.2.20",
|
||||
"@angular/router": "^19.2.20",
|
||||
"@cds/core": "^6.15.1",
|
||||
"@clr/angular": "file:libraries/clr-angular-17.9.0.tgz",
|
||||
"@clr/icons": "^13.0.2",
|
||||
"@clr/ui": "^13.17.0",
|
||||
"@handsontable/angular": "^13.0.0",
|
||||
"@sasjs/adapter": "4.3.6",
|
||||
"@sasjs/utils": "^3.3.0",
|
||||
"@sheet/crypto": "1.20211122.1",
|
||||
"@clr/ui": "file:libraries/clr-ui-17.9.0.tgz",
|
||||
"@handsontable/angular-wrapper": "^18.0.0",
|
||||
"@sasjs/adapter": "^4.17.0",
|
||||
"@sasjs/utils": "^3.5.3",
|
||||
"@sheet/crypto": "file:libraries/sheet-crypto.tgz",
|
||||
"@types/d3-graphviz": "^2.6.7",
|
||||
"@types/text-encoding": "0.0.35",
|
||||
"base64-arraybuffer": "^0.2.0",
|
||||
"buffer": "^5.4.3",
|
||||
"crypto-browserify": "3.12.0",
|
||||
"crypto-js": "^3.3.0",
|
||||
"crypto-browserify": "^3.12.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"d3-graphviz": "^5.0.2",
|
||||
"exceljs": "^4.4.0",
|
||||
"fs-extra": "^7.0.1",
|
||||
"handsontable": "^13.0.0",
|
||||
"handsontable": "^18.0.0",
|
||||
"https-browserify": "1.0.0",
|
||||
"hyperformula": "^2.5.0",
|
||||
"iconv-lite": "^0.5.0",
|
||||
"jquery-datetimepicker": "^2.5.21",
|
||||
"jsrsasign": "^10.2.0",
|
||||
"jsrsasign": "11.1.1",
|
||||
"marked": "^5.0.0",
|
||||
"moment": "^2.26.0",
|
||||
"moment": "^2.30.1",
|
||||
"ngx-clipboard": "^16.0.0",
|
||||
"ngx-json-viewer": "file:libraries/ngx-json-viewer-3.2.1.tgz",
|
||||
"nodejs": "0.0.0",
|
||||
"numbro": "^2.1.1",
|
||||
"os-browserify": "0.3.0",
|
||||
"rxjs": "^7.8.0",
|
||||
"save-svg-as-png": "^1.4.17",
|
||||
@@ -77,52 +85,63 @@
|
||||
"stream-http": "3.2.0",
|
||||
"text-encoding": "^0.7.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.13.0"
|
||||
"vm": "^0.1.0",
|
||||
"webpack": "^5.91.0",
|
||||
"xlsx": "file:libraries/xlsx-0.20.3.tgz",
|
||||
"zone.js": "~0.15.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^16.1.0",
|
||||
"@angular-eslint/builder": "16.0.3",
|
||||
"@angular-eslint/eslint-plugin": "16.0.3",
|
||||
"@angular-eslint/eslint-plugin-template": "16.0.3",
|
||||
"@angular-eslint/schematics": "16.0.3",
|
||||
"@angular-eslint/template-parser": "16.0.3",
|
||||
"@angular/cli": "^16.1.0",
|
||||
"@angular/compiler-cli": "^16.1.2",
|
||||
"@cypress/webpack-preprocessor": "^5.11.1",
|
||||
"@angular-devkit/build-angular": "^19.2.24",
|
||||
"@angular-eslint/builder": "19.8.1",
|
||||
"@angular-eslint/eslint-plugin": "19.8.1",
|
||||
"@angular-eslint/eslint-plugin-template": "19.8.1",
|
||||
"@angular-eslint/schematics": "19.8.1",
|
||||
"@angular-eslint/template-parser": "19.8.1",
|
||||
"@angular/cli": "^19.2.24",
|
||||
"@angular/compiler-cli": "^19.2.20",
|
||||
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
||||
"@compodoc/compodoc": "^1.2.1",
|
||||
"@cypress/webpack-preprocessor": "^5.17.1",
|
||||
"@lhci/cli": "^0.15.1",
|
||||
"@types/core-js": "^2.5.5",
|
||||
"@types/crypto-js": "^4.0.1",
|
||||
"@types/crypto-js": "^4.2.1",
|
||||
"@types/es6-shim": "^0.31.39",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@types/jasmine": "~5.1.4",
|
||||
"@types/lodash-es": "^4.17.3",
|
||||
"@types/marked": "^4.3.0",
|
||||
"@types/node": "12.20.50",
|
||||
"@typescript-eslint/eslint-plugin": "^5.29.0",
|
||||
"@typescript-eslint/parser": "^5.29.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.31.1",
|
||||
"@typescript-eslint/parser": "8.31.1",
|
||||
"core-js": "^2.5.4",
|
||||
"cypress": "^9.5.3",
|
||||
"cypress": "^15.14.2",
|
||||
"cypress-file-upload": "^5.0.8",
|
||||
"cypress-plugin-tab": "^1.0.5",
|
||||
"cypress-real-events": "^1.7.6",
|
||||
"cypress-real-events": "^1.8.1",
|
||||
"es6-shim": "^0.35.5",
|
||||
"eslint": "^8.33.0",
|
||||
"eslint": "8.57.1",
|
||||
"git-describe": "^4.0.4",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"karma": "~6.3.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage": "~2.1.0",
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "~1.7.0",
|
||||
"jasmine-core": "~5.1.2",
|
||||
"karma": "~6.4.3",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.1",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"license-checker": "25.0.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mochawesome": "^7.1.3",
|
||||
"mutationobserver-shim": "^0.3.3",
|
||||
"prettier": "3.8.4",
|
||||
"replace-in-file": "^6.3.5",
|
||||
"rimraf": "3.0.2",
|
||||
"ts-loader": "^9.2.8",
|
||||
"ts-node": "^3.3.0",
|
||||
"typedoc": "^0.23.24",
|
||||
"typescript": "~4.9.4",
|
||||
"typescript": "~5.8.3",
|
||||
"wait-on": "^6.0.1",
|
||||
"watch": "^1.0.2"
|
||||
},
|
||||
"overrides": {
|
||||
"ajv": "8.18.0",
|
||||
"uuid": "11.1.1",
|
||||
"lighthouse": "13.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
npm run cy:run -- --browser chrome --spec "cypress/integration/liveness.tests.ts"
|
||||
npm run cy:run -- --browser chrome --spec "cypress/integration/editor.tests.ts"
|
||||
npm run cy:run -- --browser chrome --spec "cypress/integration/excel.tests.ts"
|
||||
npm run cy:run -- --browser chrome --spec "cypress/integration/filtering.tests.ts"
|
||||
npm run cy:run -- --browser chrome --spec "cypress/integration/licensing.tests.ts"
|
||||
@@ -0,0 +1,69 @@
|
||||
import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from 'fs'
|
||||
import { resolve, join } from 'path'
|
||||
import { createRequire } from 'module'
|
||||
|
||||
/**
|
||||
* Generate static SVG assets + an SCSS partial that re-applies HOT v17 classic
|
||||
* theme icons via real URLs (not data: URIs).
|
||||
*
|
||||
* Why: deployed app runs under CSP `img-src 'self'`. HOT v17's classic theme
|
||||
* embeds icons as `data:image/svg+xml,...` in `-webkit-mask-image` rules, which
|
||||
* the CSP blocks. We switch to `ht-theme-classic-no-icons.min.css` and re-add
|
||||
* the icon rules pointing at same-origin SVG files emitted from this script.
|
||||
*
|
||||
* Inputs (HOT's own modules, so semantic names + selector list track upstream):
|
||||
* handsontable/themes/theme/classic → { classicTheme: { icons } }
|
||||
* handsontable/themes/static/variables/helpers/iconsMap → iconsMap(icons, themePrefix)
|
||||
*
|
||||
* Outputs:
|
||||
* client/src/assets/hot-icons/<kebab-name>.svg
|
||||
* client/src/_hot-icons.scss
|
||||
*
|
||||
* Idempotent: clears the output dir and rewrites both outputs each run.
|
||||
* Skips silently if handsontable isn't installed yet (pre-install runs).
|
||||
*/
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
const ASSETS_DIR = resolve('src/assets/hot-icons')
|
||||
const SCSS_OUT = resolve('src/_hot-icons.scss')
|
||||
const ASSET_URL_PREFIX = './assets/hot-icons/'
|
||||
|
||||
const themePath = resolve('node_modules/handsontable/themes/theme/classic.js')
|
||||
const mapPath = resolve('node_modules/handsontable/themes/static/variables/helpers/iconsMap.js')
|
||||
|
||||
if (!existsSync(themePath) || !existsSync(mapPath)) {
|
||||
console.log('skip: handsontable theme modules not found (likely pre-install run)')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const { classicTheme } = require(themePath)
|
||||
const { iconsMap } = require(mapPath)
|
||||
|
||||
const icons = classicTheme.icons
|
||||
const cssTemplate = iconsMap(icons, 'ht-theme-classic')
|
||||
|
||||
const kebab = (s) => s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
|
||||
|
||||
rmSync(ASSETS_DIR, { recursive: true, force: true })
|
||||
mkdirSync(ASSETS_DIR, { recursive: true })
|
||||
|
||||
const writeMap = {}
|
||||
for (const [name, dataUri] of Object.entries(icons)) {
|
||||
if (typeof dataUri !== 'string' || !dataUri.startsWith('data:image/svg+xml')) continue
|
||||
const decoded = decodeURIComponent(dataUri.replace(/^data:image\/svg\+xml(;charset=utf-8)?,/, ''))
|
||||
const fname = kebab(name) + '.svg'
|
||||
writeFileSync(join(ASSETS_DIR, fname), decoded)
|
||||
writeMap[dataUri] = ASSET_URL_PREFIX + fname
|
||||
}
|
||||
|
||||
let scss = cssTemplate
|
||||
for (const [uri, url] of Object.entries(writeMap)) {
|
||||
scss = scss.split(`url("${uri}")`).join(`url("${url}")`)
|
||||
}
|
||||
|
||||
const header = '/* Auto-generated by scripts/gen-hot-icons.mjs — do not edit by hand.\n' +
|
||||
' Regenerated on postinstall; rerun manually via `node scripts/gen-hot-icons.mjs`. */\n\n'
|
||||
|
||||
writeFileSync(SCSS_OUT, header + scss + '\n')
|
||||
|
||||
console.log(`hot-icons: wrote ${Object.keys(writeMap).length} SVGs + ${SCSS_OUT}`)
|
||||
@@ -0,0 +1,59 @@
|
||||
import { readFileSync, writeFileSync, statSync, rmSync, existsSync } from 'fs'
|
||||
import { resolve } from 'path'
|
||||
|
||||
/**
|
||||
* Remove Clarity's Metropolis @font-face blocks from clr-ui.min.css.
|
||||
*
|
||||
* Why: Clarity ships Metropolis as base64 data: URLs. The deployed app
|
||||
* runs under CSP `default-src 'self'` (no data: font-src), so every page
|
||||
* logs a font-load failure for each weight. Firefox preemptively
|
||||
* validates every parsed src against CSP even when a later @font-face
|
||||
* supersedes the rule at render time, so the only way to silence the
|
||||
* console is to remove the offending blocks from the parsed CSS.
|
||||
*
|
||||
* Our styles.scss declares the same family/weight/style with same-origin
|
||||
* .woff files, so removing Clarity's blocks entirely is safe and leaves
|
||||
* Metropolis fully functional.
|
||||
*
|
||||
* Idempotent: matches by font-family, so works on a fresh install or a
|
||||
* file that's already been stripped on a previous run.
|
||||
*/
|
||||
const target = resolve('node_modules/@clr/ui/clr-ui.min.css')
|
||||
|
||||
let css
|
||||
try {
|
||||
css = readFileSync(target, 'utf8')
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
console.log(`skip: ${target} not found (likely pre-install run)`)
|
||||
process.exit(0)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
const sizeBefore = statSync(target).size
|
||||
const blockRe = /@font-face\{[^}]*Metropolis[^}]*\}/g
|
||||
const matches = css.match(blockRe) ?? []
|
||||
|
||||
if (matches.length === 0) {
|
||||
console.log(`already stripped: ${target}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const stripped = css.replace(blockRe, '')
|
||||
writeFileSync(target, stripped)
|
||||
const sizeAfter = Buffer.byteLength(stripped)
|
||||
console.log(
|
||||
`removed ${matches.length} Metropolis @font-face block(s) from clr-ui.min.css ` +
|
||||
`(${sizeBefore} -> ${sizeAfter} bytes, saved ${sizeBefore - sizeAfter})`
|
||||
)
|
||||
|
||||
// Webpack 5's persistent cache treats node_modules as immutable
|
||||
// (snapshot.module.managedPaths default), so in-place edits don't
|
||||
// invalidate cached entries. Drop the Angular build cache so the next
|
||||
// build re-reads our stripped clr-ui.min.css.
|
||||
const cacheDir = resolve('.angular/cache')
|
||||
if (existsSync(cacheDir)) {
|
||||
rmSync(cacheDir, { recursive: true, force: true })
|
||||
console.log(`cleared ${cacheDir} (webpack persistent cache)`)
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/* Auto-generated by scripts/gen-hot-icons.mjs — do not edit by hand.
|
||||
Regenerated on postinstall; rerun manually via `node scripts/gen-hot-icons.mjs`. */
|
||||
|
||||
[class*=ht-theme-classic] .htDropdownMenu table tbody tr td.htSubmenu .htItemWrapper::after,
|
||||
[class*=ht-theme-classic] .htContextMenu table tbody tr td.htSubmenu .htItemWrapper::after,
|
||||
[class*=ht-theme-classic] .htFiltersConditionsMenu table tbody tr td.htSubmenu .htItemWrapper::after,
|
||||
[class*=ht-theme-classic] .pika-single .pika-next {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/arrow-right.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] .pika-single .pika-prev {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/arrow-left.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] .ht-page-size-section__select-wrapper::after {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/arrow-down.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] .changeType::before {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/select-arrow.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] .htUISelectCaption::after,
|
||||
.htAutocompleteArrow::after {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/select-arrow.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] .columnSorting.sortAction.ascending::before {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/arrow-narrow-up.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] .columnSorting.sortAction.descending::before {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/arrow-narrow-down.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] .ht-page-navigation-section .ht-page-first::before {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/arrow-left-with-bar.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] [dir="rtl"] .ht-page-navigation-section .ht-page-first::before {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/arrow-right-with-bar.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] .ht-page-navigation-section .ht-page-prev::before {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/arrow-left.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] [dir="rtl"] .ht-page-navigation-section .ht-page-prev::before {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/arrow-right.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] .ht-page-navigation-section .ht-page-next::before {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/arrow-right.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] [dir="rtl"] .ht-page-navigation-section .ht-page-next::before {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/arrow-left.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] .ht-page-navigation-section .ht-page-last::before {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/arrow-right-with-bar.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] [dir="rtl"] .ht-page-navigation-section .ht-page-last::before {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/arrow-left-with-bar.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] .htDropdownMenu table tbody tr td .htItemWrapper span.selected::after,
|
||||
[class*=ht-theme-classic] .htContextMenu table tbody tr td .htItemWrapper span.selected::after,
|
||||
[class*=ht-theme-classic] .htFiltersConditionsMenu table tbody tr td .htItemWrapper span.selected::after {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/check.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] .htCheckboxRendererInput {
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] .htCheckboxRendererInput::after {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/checkbox.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] th.beforeHiddenColumn::after {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/caret-hidden-left.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] th.afterHiddenColumn::before {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/caret-hidden-right.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] th.beforeHiddenRow::after {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/caret-hidden-up.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] th.afterHiddenRow::before {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/caret-hidden-down.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] .collapsibleIndicator::before,
|
||||
[class*=ht-theme-classic] .ht_nestingButton::before {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/collapse-off.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] .collapsibleIndicator.collapsed::before,
|
||||
[class*=ht-theme-classic] .ht_nestingButton.ht_nestingExpand::before {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/collapse-on.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] .htUIRadio > input[type="radio"]::after {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/radio.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] .ht-multi-select-chip-remove::before {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/chip-close.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] .ht-notification__close::before {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/chip-close.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] .ht-multi-select-editor-search-icon {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/search.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[class*=ht-theme-classic] .ht-multi-select-editor-item-selected input::after {
|
||||
width: var(--ht-icon-size);
|
||||
height: var(--ht-icon-size);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: url("./assets/hot-icons/checkbox.svg");
|
||||
background-color: currentColor;
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import { QueryClause } from './models/TableData'
|
||||
|
||||
/**
|
||||
* Filtering cache info, to be reused when filtering modal is re-open
|
||||
*/
|
||||
interface FilterCache {
|
||||
cols: any[]
|
||||
vals: any[]
|
||||
@@ -10,12 +13,18 @@ interface FilterCache {
|
||||
query: QueryClause[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtering cache info in the open viewboxes, to be reused when filtering modal is re-open
|
||||
*/
|
||||
interface ViewboxCache {
|
||||
[key: number]: {
|
||||
filter: FilterCache
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial values when no cached values stored
|
||||
*/
|
||||
export const initFilter: { filter: FilterCache } = {
|
||||
filter: {
|
||||
cols: <any[]>[],
|
||||
@@ -28,8 +37,33 @@ export const initFilter: { filter: FilterCache } = {
|
||||
}
|
||||
}
|
||||
|
||||
export interface XLMapListItem {
|
||||
id: string
|
||||
description: string
|
||||
targetDS: string
|
||||
}
|
||||
|
||||
export interface HandsontableStaticConfig {
|
||||
darkTableHeaderClass: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached filtering values across whole app (editor, viewer, viewboxes)
|
||||
* Cached lineage libraries, tables
|
||||
* Cached metadata tree
|
||||
* Cached usernav tree
|
||||
* Cached viyaApi collections, search and selected endpoint
|
||||
*/
|
||||
export const globals: {
|
||||
embed: boolean | 'va'
|
||||
/**
|
||||
* VA embed filter behavior: 'live' = auto-apply, 'confirm' = manual Apply.
|
||||
* Defaults to 'live'; toggled by the Auto-apply checkbox in the editor.
|
||||
*/
|
||||
vaApplyMode: 'live' | 'confirm'
|
||||
rootParam: string
|
||||
dcLib: string
|
||||
xlmaps: XLMapListItem[]
|
||||
editor: any
|
||||
viewer: any
|
||||
viewboxes: ViewboxCache
|
||||
@@ -38,14 +72,19 @@ export const globals: {
|
||||
viyaApi: any
|
||||
usernav: any
|
||||
operators: any
|
||||
handsontable: HandsontableStaticConfig
|
||||
[key: string]: any
|
||||
} = {
|
||||
embed: false,
|
||||
vaApplyMode: 'live',
|
||||
rootParam: <string>'',
|
||||
dcLib: '',
|
||||
xlmaps: [],
|
||||
editor: {
|
||||
startupSet: <boolean>false,
|
||||
treeNodeLibraries: <any[] | null>[],
|
||||
libsAndTables: <any[]>[],
|
||||
libraries: <String[] | undefined>[],
|
||||
libraries: <string[] | undefined>[],
|
||||
library: <string>'',
|
||||
table: <string>'',
|
||||
filter: <FilterCache>{
|
||||
@@ -114,5 +153,11 @@ export const globals: {
|
||||
operators: {
|
||||
numOperators: ['=', '<', '>', '<=', '>=', 'BETWEEN', 'IN', 'NOT IN', 'NE'],
|
||||
charOperators: ['=', '<', '>', '<=', '>=', 'CONTAINS', 'IN', 'NOT IN', 'NE']
|
||||
},
|
||||
handsontable: {
|
||||
darkTableHeaderClass: 'darkTH'
|
||||
},
|
||||
userDropdownConfig: {
|
||||
closeOnDebugClick: false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
<div class="content-area">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex flex-column justify-content-center">
|
||||
<h3 class="text-center">
|
||||
You succesfully edited table
|
||||
<span class="color-blue font-weight-700">{{ libds }}</span>
|
||||
</h3>
|
||||
<p class="text-center">
|
||||
<b>Please choose from the following actions</b>
|
||||
</p>
|
||||
<div class="row d-flex justify-content-center mt-20">
|
||||
<button
|
||||
class="btn btn-sm btn-outline text-center"
|
||||
(click)="submittedTableScreen()"
|
||||
>
|
||||
Go to submitted table screen
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-outline text-center"
|
||||
(click)="viewerTableScreen()"
|
||||
>
|
||||
Go to base table screen
|
||||
</button>
|
||||
<button
|
||||
id="approvalBtn"
|
||||
class="btn btn-sm btn-success-outline text-center"
|
||||
(click)="approveTableScreen()"
|
||||
>
|
||||
Go to approvals screen
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-info-outline text-center"
|
||||
(click)="goBack()"
|
||||
>
|
||||
Go back to editor
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,50 +0,0 @@
|
||||
import { AfterViewInit, Component, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
|
||||
@Component({
|
||||
selector: 'app-actions',
|
||||
templateUrl: './actions.component.html',
|
||||
styleUrls: ['./actions.component.scss'],
|
||||
host: {
|
||||
class: 'content-container'
|
||||
}
|
||||
})
|
||||
export class ActionsComponent implements OnInit, AfterViewInit {
|
||||
public dsid: any
|
||||
public libds: string | undefined
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
public submittedTableScreen() {
|
||||
this.router.navigateByUrl('/stage/' + this.dsid)
|
||||
}
|
||||
|
||||
public approveTableScreen() {
|
||||
this.router.navigateByUrl('/approve/approveDet/' + this.dsid)
|
||||
}
|
||||
|
||||
public viewerTableScreen() {
|
||||
this.router.navigateByUrl('/view/data/' + this.libds)
|
||||
}
|
||||
|
||||
public goBack() {
|
||||
this.router.navigateByUrl('/editor/' + this.libds)
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.dsid = this.route.snapshot.params['dsid']
|
||||
this.libds = this.route.snapshot.params['libds']
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => {
|
||||
let approvalBtn: any = window.document.getElementById('approvalBtn')
|
||||
if (!!approvalBtn) {
|
||||
approvalBtn.focus()
|
||||
}
|
||||
}, 700)
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<clr-icon class="mt-2" shape="warning-standard"></clr-icon>
|
||||
<cds-icon class="alert-icon" shape="warning-standard"></cds-icon>
|
||||
</div>
|
||||
<div class="alert-text">
|
||||
Data Controller (FREE Tier) - to upgrade contact
|
||||
@@ -30,7 +30,7 @@
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<clr-icon class="mt-2" shape="warning-standard"></clr-icon>
|
||||
<cds-icon class="alert-icon" shape="warning-standard"></cds-icon>
|
||||
</div>
|
||||
<div class="alert-text">
|
||||
Data Controller (FREE Tier) - Problem with licence
|
||||
@@ -55,7 +55,7 @@
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<clr-icon class="mt-2" shape="warning-standard"></clr-icon>
|
||||
<cds-icon class="alert-icon" shape="warning-standard"></cds-icon>
|
||||
</div>
|
||||
|
||||
<div class="alert-text">
|
||||
@@ -85,7 +85,7 @@
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<clr-icon class="mt-2" shape="warning-standard"></clr-icon>
|
||||
<cds-icon class="alert-icon" shape="warning-standard"></cds-icon>
|
||||
</div>
|
||||
|
||||
<div class="alert-text">
|
||||
@@ -107,7 +107,7 @@
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<header class="app-header">
|
||||
<header class="app-header" *ngIf="!embed">
|
||||
<!-- <button
|
||||
*ngIf="
|
||||
isMainRoute('view') ||
|
||||
@@ -127,9 +127,10 @@
|
||||
"
|
||||
(click)="toggleSidebar()"
|
||||
type="button"
|
||||
aria-label="Toggle sidebar"
|
||||
class="cursor-pointer select-none ml-10 d-flex clr-justify-content-center clr-align-items-center"
|
||||
>
|
||||
<clr-icon size="24" shape="tree-view"></clr-icon>
|
||||
<clr-icon size="24" shape="tree-view" aria-hidden="true"></clr-icon>
|
||||
</div>
|
||||
|
||||
<div class="logo d-flex clr-align-items-center">
|
||||
@@ -139,10 +140,15 @@
|
||||
[routerLink]="['/']"
|
||||
class="nav-link"
|
||||
>
|
||||
<img class="without-text d-block d-md-none" src="images/dc-logo.svg" />
|
||||
<img
|
||||
class="without-text d-block d-md-none"
|
||||
src="images/dc-logo.svg"
|
||||
alt="datacontroller logo without text"
|
||||
/>
|
||||
<img
|
||||
class="with-text d-none d-md-block"
|
||||
src="images/datacontroller.svg"
|
||||
alt="datacontroller logo"
|
||||
/>
|
||||
</a>
|
||||
|
||||
@@ -168,8 +174,8 @@
|
||||
</button>
|
||||
<clr-dropdown-menu *clrIfOpen clrPosition="bottom-left">
|
||||
<a [routerLink]="['/view']" clrDropdownItem>VIEW</a>
|
||||
<a [routerLink]="['/home']" clrDropdownItem>EDIT</a>
|
||||
<a [routerLink]="['/submitted']" clrDropdownItem>REVIEW</a>
|
||||
<a [routerLink]="['/home']" clrDropdownItem>LOAD</a>
|
||||
<a [routerLink]="['/review/submitted']" clrDropdownItem>REVIEW</a>
|
||||
</clr-dropdown-menu>
|
||||
</clr-dropdown>
|
||||
</div>
|
||||
@@ -189,10 +195,10 @@
|
||||
router.url.includes('edit-record') ||
|
||||
router.url.includes('home')
|
||||
"
|
||||
>EDIT</a
|
||||
>LOAD</a
|
||||
>
|
||||
<a
|
||||
[routerLink]="['/submitted']"
|
||||
[routerLink]="['/review/submitted']"
|
||||
[class.active]="
|
||||
router.url.includes('submitted') ||
|
||||
router.url.includes('approve') ||
|
||||
@@ -204,27 +210,21 @@
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div class="header-actions">
|
||||
<div class="nav-text">
|
||||
<app-loading-indicator></app-loading-indicator>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<app-user-nav-dropdown></app-user-nav-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<app-header-actions></app-header-actions>
|
||||
</header>
|
||||
<nav
|
||||
*ngIf="
|
||||
router.url.includes('submitted') ||
|
||||
router.url.includes('approve') ||
|
||||
router.url.includes('history')
|
||||
!embed &&
|
||||
(router.url.includes('submitted') ||
|
||||
router.url.includes('approve') ||
|
||||
router.url.includes('history'))
|
||||
"
|
||||
class="subnav"
|
||||
>
|
||||
<ul class="nav">
|
||||
<li class="nav-item">
|
||||
<a
|
||||
[routerLink]="['/submitted']"
|
||||
[routerLink]="['/review/submitted']"
|
||||
class="nav-link nav-text"
|
||||
routerLinkActive="active"
|
||||
>SUBMIT</a
|
||||
@@ -232,15 +232,16 @@
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
[routerLink]="['/approve']"
|
||||
[routerLink]="['/review/approve']"
|
||||
class="nav-link nav-text"
|
||||
[class.active]="router.url.includes('approve')"
|
||||
routerLinkActive="active"
|
||||
>APPROVE</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
[routerLink]="['/history']"
|
||||
[routerLink]="['/review/history']"
|
||||
class="nav-link nav-text"
|
||||
routerLinkActive="active"
|
||||
>HISTORY</a
|
||||
@@ -251,9 +252,30 @@
|
||||
|
||||
<app-alerts *ngIf="!errTop"></app-alerts>
|
||||
<app-requests-modal [(opened)]="requestsModal"></app-requests-modal>
|
||||
<app-excel-password-modal></app-excel-password-modal>
|
||||
|
||||
<!-- <app-terms *ngIf="showRegistration"></app-terms> -->
|
||||
|
||||
<!-- VA embed, back to Editor button -->
|
||||
<div
|
||||
*ngIf="embed === 'va' && vaEditorLibds && !isMainRoute('/editor')"
|
||||
class="va-back-bar"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
(click)="backToEditor()"
|
||||
>
|
||||
<clr-icon
|
||||
aria-hidden="true"
|
||||
shape="caret"
|
||||
dir="left"
|
||||
size="16"
|
||||
></clr-icon>
|
||||
Back to Edit table
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<router-outlet *ngIf="startupDataLoaded"></router-outlet>
|
||||
|
||||
<app-login></app-login>
|
||||
@@ -288,7 +310,11 @@
|
||||
|
||||
<!-- App Loading Page -->
|
||||
<div *ngIf="!startupDataLoaded" class="app-loading">
|
||||
<img class="loading-logo" src="images/datacontroller.svg" />
|
||||
<img
|
||||
class="loading-logo"
|
||||
src="images/datacontroller.svg"
|
||||
alt="datacontroller logo"
|
||||
/>
|
||||
|
||||
<div *ngIf="appActive === null" class="slider">
|
||||
<div class="line"></div>
|
||||
|
||||
@@ -1,461 +1,10 @@
|
||||
// Copyright (c) 2016 VMware, Inc. All Rights Reserved.
|
||||
// This software is released under MIT license.
|
||||
// The full license information can be found in LICENSE in the root directory of this project.
|
||||
app-requests-modal {
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
header.app-header {
|
||||
background: #314351 !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.logo img.without-text {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.logo img.with-text {
|
||||
width: 210px;
|
||||
}
|
||||
|
||||
.header-hamburger-trigger {
|
||||
display: block;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.demo-expired-notice {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
.va-back-bar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh !important;
|
||||
width: 100vw !important;
|
||||
z-index: 105;
|
||||
background: rgba(33, 33, 33, .5);
|
||||
|
||||
.expired-details {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 30px;
|
||||
z-index: 110;
|
||||
background: #314351;
|
||||
|
||||
.expired-notice {
|
||||
color: #e0e0e0;
|
||||
font-size: 16px;
|
||||
|
||||
.mailto {
|
||||
color: #8dc53e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-container .update-key {
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: white;
|
||||
padding: 0px 10px;
|
||||
background: #00000026;
|
||||
justify-content: flex-start;
|
||||
padding: 0.35rem 0.6rem;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.alert-icon-wrapper {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
padding-left: 10px;
|
||||
|
||||
clr-icon {
|
||||
cursor: pointer;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
.header-actions {
|
||||
.dropdown {
|
||||
position: unset; //without it, when opening user dropdown scrollbar was displaying without reason
|
||||
}
|
||||
}
|
||||
|
||||
.nav
|
||||
.nav-link {
|
||||
color: #fafafa;
|
||||
opacity: .9;
|
||||
line-height: 1.45rem;
|
||||
}
|
||||
|
||||
.nav .nav-link:hover {
|
||||
box-shadow: inset 0 -3px 0 transparent;
|
||||
transition: box-shadow .2s ease-in;
|
||||
}
|
||||
|
||||
.nav
|
||||
.nav-link:hover {
|
||||
color: #fafafa;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nav .nav-link.active {
|
||||
background: #61717D;
|
||||
opacity: 1;
|
||||
box-shadow: inset 0 -3px transparent;
|
||||
// padding: 0 1rem 0 1rem;
|
||||
}
|
||||
|
||||
.nav .nav-item {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.notf {
|
||||
background: #16a57a;
|
||||
color: #fffcfc;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn.btn-success {
|
||||
border-color: #62a420;
|
||||
background-color: #16a57a!important;
|
||||
color: #fff;
|
||||
}
|
||||
.btn.btn-success:hover {
|
||||
background-color: #2add39;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.toggle-switch input[type=checkbox]:checked+label:before {
|
||||
border-color: #61717D;
|
||||
background-color: #61717D;
|
||||
transition: .15s ease-in;
|
||||
transition-property: border-color,background-color;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
min-height: 100vh !important;
|
||||
}
|
||||
|
||||
.main-container .content-container .content-area {
|
||||
padding: 0rem 1rem 1rem 1rem;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
z-index: 0!important;
|
||||
}
|
||||
|
||||
.navBarResp {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background: #495A67;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.navBarResp {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
background: #495A67;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.main-container .sub-nav.clr-nav-level-1 .nav .nav-link, .main-container .sub-nav.clr-nav-level-2 .nav .nav-link, .main-container .subnav.clr-nav-level-1 .nav .nav-link, .main-container .subnav.clr-nav-level-2 .nav .nav-link {
|
||||
padding: 0 .5rem 0 1rem;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
border-radius: .125rem 0 0 .125rem;
|
||||
color: #95c84b;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.card-block, .card-footer {
|
||||
padding: 10px 0px 0px 0px;
|
||||
}
|
||||
|
||||
.main-container[_ngcontent-c0] .content-container[_ngcontent-c0] .content-area[_ngcontent-c0] {
|
||||
padding: 0rem 0rem 0rem 0rem;
|
||||
}
|
||||
|
||||
}
|
||||
::ng-deep {
|
||||
.htInvalid {
|
||||
background: black!important;
|
||||
}
|
||||
|
||||
@media screen and (max-width:480px) {
|
||||
h2 {
|
||||
font-size: .7rem!important;
|
||||
|
||||
}
|
||||
h3 {
|
||||
font-size: .7rem;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
padding: 0rem 1rem 0rem 1rem;
|
||||
}
|
||||
|
||||
.btn-primary .btn, .btn.btn-primary {
|
||||
border-color: #314351;
|
||||
background-color: #314351;
|
||||
color: #fff;
|
||||
}
|
||||
.btn {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
-webkit-appearance: none!important;
|
||||
border-radius: .125rem;
|
||||
border: 1px solid;
|
||||
min-width: 3rem;
|
||||
max-width: 15rem;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
vertical-align: middle;
|
||||
line-height: 1.5rem;
|
||||
letter-spacing: .12em;
|
||||
font-size: .5rem;
|
||||
font-weight: 500;
|
||||
height: 1.5rem;
|
||||
padding: 0 .5rem;
|
||||
border-color: #314351;
|
||||
background-color: transparent;
|
||||
color: #314351;
|
||||
}
|
||||
|
||||
.btn.btn-outline {
|
||||
border-color: #314351;
|
||||
background-color: transparent;
|
||||
color: #314351;
|
||||
}
|
||||
|
||||
.btn.btn-outline:hover {
|
||||
border-color: #314351;
|
||||
background-color: #495A67;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn.btn-success-outline:hover {
|
||||
background-color: #5ea71f;
|
||||
color: #fff7f7;
|
||||
border-color: #9a9696;
|
||||
}
|
||||
// .btn.btn-success-outline {
|
||||
// border-color: #266900;
|
||||
// background-color: transparent;
|
||||
// color: #318700;
|
||||
// }
|
||||
// .wtSpreader {
|
||||
|
||||
// }
|
||||
|
||||
.htMobileEditorContainer .inputs textarea {
|
||||
font-size: 13pt;
|
||||
border: 2px solid #485967;
|
||||
border-radius: 4px;
|
||||
-webkit-appearance: none;
|
||||
box-shadow: none;
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
right: 0px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
padding: 7pt;
|
||||
width: 290px;
|
||||
}
|
||||
|
||||
.htMobileEditorContainer .positionControls {
|
||||
width: 333px;
|
||||
position: absolute;
|
||||
right: 5pt;
|
||||
top: 50px;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.htMobileEditorContainer.active {
|
||||
display: block;
|
||||
height: 120px;
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
.handsontable {
|
||||
background-color: #ffffff;
|
||||
// border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.handsontable th {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
/* Left and right */
|
||||
.ht_clone_left th {
|
||||
border-right: 1px solid #ccc;
|
||||
border-left: 1px solid #ccc;
|
||||
}
|
||||
|
||||
/* Column headers */
|
||||
.ht_clone_top th {
|
||||
border-top: 1px solid #ccc;
|
||||
border-right: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.ht_clone_top_left_corner th {
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.ht_master tr:nth-of-type(odd) > td {
|
||||
background-color: #f3f3f3;
|
||||
border: 1px solid rgb(197, 197, 197);
|
||||
border-bottom: 1px solid rgb(236, 235, 235);
|
||||
// padding: 1px 1px;
|
||||
}
|
||||
|
||||
.ht_master tr:nth-of-type(even) > td {
|
||||
background-color: white;
|
||||
border: 1px solid rgb(197, 197, 197);
|
||||
border-bottom: 1px solid rgb(236, 235, 235);
|
||||
// padding: 1px 1px;
|
||||
}
|
||||
|
||||
.wtBorder {
|
||||
background-color: #495A67!important;
|
||||
}
|
||||
|
||||
.handsontable .handsontable.ht_clone_top .wtHider {
|
||||
padding: 0 0 0px 0!important;
|
||||
margin: 0px;
|
||||
border-bottom: 3px solid #d6d3d3;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
background: #F5F6FF;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: 0 0.125rem 0 0 #d7d7d7;
|
||||
border-radius: .0rem;
|
||||
border: 1px solid transparent;
|
||||
// min-height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
.datagrid-compact, .datagrid-history{
|
||||
.datagrid {
|
||||
border-collapse: separate;
|
||||
border: 1px solid transparent;
|
||||
border-radius: .125rem;
|
||||
background-color: #fff;
|
||||
color: #565656;
|
||||
margin: 0;
|
||||
margin-top: 1rem;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
padding: 15px 15px 50px 15px;
|
||||
}
|
||||
.datagrid-foot {
|
||||
-webkit-box-pack: end;
|
||||
-ms-flex-pack: end;
|
||||
justify-content: flex-end;
|
||||
height: 1.5rem;
|
||||
padding: 0 .5rem;
|
||||
line-height: calc(1.5rem - 3px);
|
||||
font-size: .45833rem;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #ccc;
|
||||
border-radius: 0px;
|
||||
// border-radius: 0 0 .125rem .125rem;
|
||||
}
|
||||
.datagrid-footer {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 2px;
|
||||
}
|
||||
.datagrid .datagrid-head {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
margin-top: .083333rem;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
background: #f5f6ff;
|
||||
padding: .5rem 0;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 0 1px 0.125rem hsla(0,0%,45%,.25);
|
||||
min-width: 5rem;
|
||||
max-width: 15rem;
|
||||
border-radius: .125rem;
|
||||
visibility: hidden;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.table {
|
||||
border-collapse: separate;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0px;
|
||||
background-color: #fff;
|
||||
color: #565656;
|
||||
margin: 0;
|
||||
margin-top: 1rem;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.table th {
|
||||
font-size: .45833rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: .03em;
|
||||
background-color: #fff;
|
||||
vertical-align: bottom;
|
||||
border-bottom: 1px solid #ccc;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
border-bottom: 2px solid #e4e4e4;
|
||||
padding: 0 0 .5rem 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.main-container .content-container {
|
||||
min-height: 0px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.app-loading {
|
||||
.loading-logo {
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
+117
-22
@@ -1,4 +1,9 @@
|
||||
import { ChangeDetectorRef, Component, ElementRef } from '@angular/core'
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { VERSION } from '../environments/version'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
@@ -6,18 +11,40 @@ import { Location } from '@angular/common'
|
||||
import '@clr/icons'
|
||||
import '@clr/icons/shapes/all-shapes'
|
||||
import { globals } from './_globals'
|
||||
import * as moment from 'moment'
|
||||
import { parseEmbedParam } from './shared/utils/parse-embed-param'
|
||||
import moment from 'moment'
|
||||
import { EventService } from './services/event.service'
|
||||
import { AppService } from './services/app.service'
|
||||
import { InfoModal } from './models/InfoModal'
|
||||
import { DcAdapterSettings } from './models/DcAdapterSettings'
|
||||
import { AppStoreService } from './services/app-store.service'
|
||||
import { LicenceService } from './services/licence.service'
|
||||
import '@cds/core/icon/register.js'
|
||||
import {
|
||||
ClarityIcons,
|
||||
exclamationTriangleIcon,
|
||||
moonIcon,
|
||||
processOnVmIcon,
|
||||
sunIcon,
|
||||
tableIcon,
|
||||
trashIcon
|
||||
} from '@cds/core/icon'
|
||||
|
||||
ClarityIcons.addIcons(
|
||||
moonIcon,
|
||||
sunIcon,
|
||||
exclamationTriangleIcon,
|
||||
tableIcon,
|
||||
trashIcon,
|
||||
processOnVmIcon
|
||||
)
|
||||
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
styleUrls: ['./app.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: false
|
||||
})
|
||||
export class AppComponent {
|
||||
private dcAdapterSettings: DcAdapterSettings | undefined
|
||||
@@ -44,6 +71,7 @@ export class AppComponent {
|
||||
|
||||
public syssite = this.appService.syssite
|
||||
public licenceState = this.licenceService.licenceState
|
||||
public embed = globals.embed
|
||||
|
||||
constructor(
|
||||
private appService: AppService,
|
||||
@@ -57,24 +85,15 @@ export class AppComponent {
|
||||
private elementRef: ElementRef
|
||||
) {
|
||||
this.parseDcAdapterSettings()
|
||||
|
||||
/**
|
||||
* Prints app info in the console such as:
|
||||
* - Adapter versions
|
||||
* - App version
|
||||
* - Build timestamp
|
||||
*
|
||||
*/
|
||||
;(window as any).appinfo = () => {
|
||||
const licenseKeyData = this.licenceService.getLicenseKeyData()
|
||||
|
||||
if (licenseKeyData) {
|
||||
const expiry_date = moment(
|
||||
licenseKeyData.valid_until,
|
||||
'YYYY-MM-DD'
|
||||
).startOf('day')
|
||||
const current_date = moment().startOf('day')
|
||||
const daysToExpiry = expiry_date.diff(current_date, 'days')
|
||||
|
||||
licenseKeyData.valid_until += ` (${daysToExpiry} ${
|
||||
daysToExpiry === 1 ? 'day' : 'days'
|
||||
} remaining)`
|
||||
|
||||
if (isNaN(daysToExpiry)) licenseKeyData.valid_until = 'Unlimited'
|
||||
}
|
||||
|
||||
console.table({
|
||||
'Adapter version': VERSION.adapterVersion || 'n/a',
|
||||
'App version': (VERSION.tag || '').replace('v', ''),
|
||||
@@ -87,7 +106,12 @@ export class AppComponent {
|
||||
|
||||
this.subscribeToLicenseEvents()
|
||||
|
||||
/**
|
||||
* Fetches git tag ang git hash from `version.ts` file
|
||||
* It's placed in the user drop down.
|
||||
*/
|
||||
this.commitVer = (VERSION.tag || '').replace('v', '') + '.' + VERSION.hash
|
||||
|
||||
router.events.subscribe((val) => {
|
||||
this.routeUrl = this.router.url
|
||||
|
||||
@@ -121,14 +145,23 @@ export class AppComponent {
|
||||
}
|
||||
})
|
||||
|
||||
const hashQuery = window.location.hash.split('?')[1]
|
||||
if (hashQuery && new URLSearchParams(hashQuery).get('embed') !== null) {
|
||||
const embed = parseEmbedParam(hashQuery)
|
||||
globals.embed = embed
|
||||
this.embed = embed
|
||||
}
|
||||
|
||||
this.subscribeToShowAbortModal()
|
||||
this.subscribeToRequestsModal()
|
||||
this.subscribeToStartupData()
|
||||
this.subscribeToAppActive()
|
||||
this.subscribeToDemoLimitModal()
|
||||
|
||||
/* In Viya streaming apps, content is served within an iframe. This code
|
||||
makes that iframe "full screen" so it looks like a regular window. */
|
||||
/**
|
||||
* In Viya streaming apps, content is served within an iframe. This code
|
||||
* makes that iframe "full screen" so it looks like a regular window.
|
||||
*/
|
||||
if (window.frameElement) {
|
||||
window.frameElement.setAttribute(
|
||||
'style',
|
||||
@@ -143,6 +176,9 @@ export class AppComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses adapter settings that are found in the <sasjs> tag inside index.html
|
||||
*/
|
||||
private parseDcAdapterSettings() {
|
||||
const sasjsElement = document.querySelector('sasjs')
|
||||
|
||||
@@ -171,6 +207,7 @@ export class AppComponent {
|
||||
dcPath: getAppAttribute('dcPath') || '',
|
||||
debug: getAppAttribute('debug') === 'true' || false,
|
||||
useComputeApi: this.parseComputeApi(getAppAttribute('useComputeApi')),
|
||||
runAsTask: getAppAttribute('runAsTask') === 'true' || false,
|
||||
contextName: getAppAttribute('contextName') || '',
|
||||
hotLicenceKey: getAppAttribute('hotLicenceKey') || ''
|
||||
}
|
||||
@@ -180,9 +217,14 @@ export class AppComponent {
|
||||
this.appService.sasServiceInit()
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens licence page with the active licence problem
|
||||
* Problem details are encoded in the url
|
||||
*/
|
||||
public licenceProblemDetails(url: string) {
|
||||
this.router.navigateByUrl(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on string provided we return true, false or null
|
||||
* True -> Compute API
|
||||
@@ -199,6 +241,12 @@ export class AppComponent {
|
||||
return value === 'true' || false
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens for an `demo limit` event that will show the `Feature locked modal`
|
||||
* For exmaple when in editor upload feature is not enabled
|
||||
* When user tries to upload the excel, editor component will trgger this event
|
||||
* And stop the execution of file upload code.
|
||||
*/
|
||||
public subscribeToDemoLimitModal() {
|
||||
this.eventService.onDemoLimitModalShow.subscribe((featureName: string) => {
|
||||
this.demoLimitNotice = {
|
||||
@@ -208,6 +256,10 @@ export class AppComponent {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens for licence events so banner can be displayed.
|
||||
* App is free tier, licence will expire, is expired or is invalid
|
||||
*/
|
||||
public subscribeToLicenseEvents() {
|
||||
this.licenceService.isAppFreeTier.subscribe((isAppFreeTier: boolean) => {
|
||||
this.freeTierBanner = isAppFreeTier
|
||||
@@ -227,6 +279,10 @@ export class AppComponent {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens for an event that will activate od deactivate full application.
|
||||
* Based on licence key prcoessing result
|
||||
*/
|
||||
public subscribeToAppActive() {
|
||||
this.licenceService.isAppActivated.subscribe((value: any) => {
|
||||
this.appActive = value
|
||||
@@ -248,32 +304,71 @@ export class AppComponent {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* When startupservice request is finished with valid response, this event will
|
||||
* make sure loading screen is gone.
|
||||
*/
|
||||
public subscribeToStartupData() {
|
||||
this.eventService.onStartupDataLoaded.subscribe(() => {
|
||||
this.startupDataLoaded = true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens requests modal when requested from event service
|
||||
*/
|
||||
public subscribeToRequestsModal() {
|
||||
this.eventService.onRequestsModalOpen.subscribe((value: boolean) => {
|
||||
this.requestsModal = true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes abort modal with matching ID (there could be multiple abort modals open)
|
||||
*/
|
||||
public closeAbortModal(abortId: number) {
|
||||
let abortIndex = this.sasjsAborts.findIndex((abort) => abort.id === abortId)
|
||||
this.sasjsAborts.splice(abortIndex, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles sidebar when requested from event service
|
||||
*/
|
||||
public toggleSidebar() {
|
||||
this.eventService.toggleSidebar()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not current route includes the route from param
|
||||
* @param route route to check
|
||||
*/
|
||||
public isMainRoute(route: string): boolean {
|
||||
return this.router.url.includes(route)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a page for updating the licence.
|
||||
*/
|
||||
public openLicencingPage() {
|
||||
this.router.navigateByUrl('/licensing/update')
|
||||
}
|
||||
|
||||
/** "lib.table" of the last-opened editor table (for the VA embed back button). */
|
||||
get vaEditorLibds(): string {
|
||||
const e = globals.editor
|
||||
return e?.library && e?.table ? `${e.library}.${e.table}` : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* VA embed only: returns to the editor for the last-opened table. Lets users
|
||||
* who land on the stage/approve/review pages get back without knowing DC's
|
||||
* page paths. `globals.embed` (set at app init) keeps the editor in VA mode.
|
||||
*/
|
||||
public backToEditor(): void {
|
||||
if (this.vaEditorLibds) {
|
||||
this.router.navigate(['/editor/' + this.vaEditorLibds], {
|
||||
queryParams: { embed: 'va' }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
-1
@@ -1,5 +1,4 @@
|
||||
declare module 'save-svg-as-png'
|
||||
|
||||
declare interface Navigator {
|
||||
msSaveBlob: (blob: any, defaultName?: string) => boolean
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HttpClientModule } from '@angular/common/http'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
@@ -11,80 +11,51 @@ import { NotFoundComponent } from './not-found/not-found.component'
|
||||
|
||||
import { SasStoreService } from './services/sas-store.service'
|
||||
import { SharedModule } from './shared/shared.module'
|
||||
// import { EditorComponent } from './editor/editor.component'
|
||||
import { ActionsComponent } from './actions/actions.component'
|
||||
|
||||
import { AppSharedModule } from './app-shared.module'
|
||||
import { ApproveDetailsComponent } from './approve-details/approve-details.component'
|
||||
import { ApproveComponent } from './approve/approve.component'
|
||||
import { DeployComponent } from './deploy/deploy.component'
|
||||
import { AutomaticComponent } from './deploy/sections/automatic/automatic.component'
|
||||
import { ManualComponent } from './deploy/sections/manual/manual.component'
|
||||
import { SasjsConfiguratorComponent } from './deploy/sections/sasjs-configurator/sasjs-configurator.component'
|
||||
import { GroupComponent } from './group/group.component'
|
||||
import { HistoryComponent } from './history/history.component'
|
||||
import { LicensingComponent } from './licensing/licensing.component'
|
||||
import { LineageComponent } from './lineage/lineage.component'
|
||||
import { MetadataComponent } from './metadata/metadata.component'
|
||||
import { PipesModule } from './pipes/pipes.module'
|
||||
import { RoleComponent } from './role/role.component'
|
||||
import { ApproveRouteComponent } from './routes/approve-route/approve-route.component'
|
||||
import { HistoryRouteComponent } from './routes/history-route/history-route.component'
|
||||
import { ReviewRouteComponent } from './routes/review-route/review-route.component'
|
||||
import { LicensingGuard } from './routes/licensing.guard'
|
||||
import { UsernavRouteComponent } from './routes/usernav-route/usernav-route.component'
|
||||
import { AppService } from './services/app.service'
|
||||
import { InfoModalComponent } from './shared/abort-modal/info-modal.component'
|
||||
import { RequestsModalComponent } from './shared/requests-modal/requests-modal.component'
|
||||
import { SubmitterComponent } from './submitter/submitter.component'
|
||||
import { UserComponent } from './user/user.component'
|
||||
import { HomeModule } from './home/home.module'
|
||||
import { SystemComponent } from './system/system.component'
|
||||
import { DirectivesModule } from './directives/directives.module'
|
||||
import { ViyaApiExplorerComponent } from './viya-api-explorer/viya-api-explorer.component'
|
||||
import { NgxJsonViewerModule } from 'ngx-json-viewer'
|
||||
import { AppSettingsService } from './services/app-settings.service'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
NotFoundComponent,
|
||||
ApproveComponent,
|
||||
ApproveDetailsComponent,
|
||||
ActionsComponent,
|
||||
HistoryComponent,
|
||||
LineageComponent,
|
||||
SubmitterComponent,
|
||||
ApproveRouteComponent,
|
||||
HistoryRouteComponent,
|
||||
MetadataComponent,
|
||||
ReviewRouteComponent,
|
||||
ReviewRouteComponent,
|
||||
UsernavRouteComponent,
|
||||
UserComponent,
|
||||
GroupComponent,
|
||||
RoleComponent,
|
||||
RequestsModalComponent,
|
||||
DeployComponent,
|
||||
InfoModalComponent,
|
||||
LicensingComponent,
|
||||
ManualComponent,
|
||||
AutomaticComponent,
|
||||
SasjsConfiguratorComponent,
|
||||
SystemComponent,
|
||||
ViyaApiExplorerComponent
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [
|
||||
BrowserAnimationsModule,
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
HttpClientModule,
|
||||
ROUTING,
|
||||
SharedModule,
|
||||
ClarityModule,
|
||||
AppSharedModule,
|
||||
HomeModule,
|
||||
PipesModule,
|
||||
DirectivesModule,
|
||||
NgxJsonViewerModule
|
||||
],
|
||||
providers: [AppService, SasStoreService, ApproveComponent, LicensingGuard],
|
||||
bootstrap: [AppComponent]
|
||||
providers: [
|
||||
AppService,
|
||||
SasStoreService,
|
||||
LicensingGuard,
|
||||
AppSettingsService,
|
||||
provideHttpClient(withInterceptorsFromDi())
|
||||
]
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -4,25 +4,23 @@
|
||||
* The full license information can be found in LICENSE in the root directory of this project.
|
||||
*/
|
||||
import { ModuleWithProviders } from '@angular/core'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
|
||||
import { HomeComponent } from './home/home.component'
|
||||
import { ApproveComponent } from './approve/approve.component'
|
||||
import { ApproveDetailsComponent } from './approve-details/approve-details.component'
|
||||
import { ActionsComponent } from './actions/actions.component'
|
||||
import { HistoryComponent } from './history/history.component'
|
||||
import { NotFoundComponent } from './not-found/not-found.component'
|
||||
import { SubmitterComponent } from './submitter/submitter.component'
|
||||
|
||||
import { ApproveRouteComponent } from './routes/approve-route/approve-route.component'
|
||||
import { DeployComponent } from './deploy/deploy.component'
|
||||
import { LicensingComponent } from './licensing/licensing.component'
|
||||
import { LicensingGuard } from './routes/licensing.guard'
|
||||
import { StageModule } from './stage/stage.module'
|
||||
import { DeployModule } from './deploy/deploy.module'
|
||||
import { EditorModule } from './editor/editor.module'
|
||||
import { HomeModule } from './home/home.module'
|
||||
import { LicensingModule } from './licensing/licensing.module'
|
||||
import { ReviewModule } from './review/review.module'
|
||||
import { ReviewRouteComponent } from './routes/review-route/review-route.component'
|
||||
import { StageModule } from './stage/stage.module'
|
||||
import { SystemModule } from './system/system.module'
|
||||
import { ViewerModule } from './viewer/viewer.module'
|
||||
import { SystemComponent } from './system/system.component'
|
||||
|
||||
/**
|
||||
* Defining routes
|
||||
*/
|
||||
export const ROUTES: Routes = [
|
||||
{ path: '', redirectTo: 'home', pathMatch: 'full' },
|
||||
{
|
||||
@@ -30,23 +28,31 @@ export const ROUTES: Routes = [
|
||||
loadChildren: () => ViewerModule
|
||||
},
|
||||
{
|
||||
path: 'approve',
|
||||
component: ApproveRouteComponent,
|
||||
/**
|
||||
* Load review module (approve, history, submitted)
|
||||
*/
|
||||
path: 'review',
|
||||
component: ReviewRouteComponent,
|
||||
children: [
|
||||
{ path: '', pathMatch: 'full', redirectTo: 'toapprove' },
|
||||
{ path: 'toapprove', component: ApproveComponent },
|
||||
{ path: 'approveDet/:tableId', component: ApproveDetailsComponent },
|
||||
{ path: 'submitted', component: SubmitterComponent }
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => ReviewModule
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'licensing/:action',
|
||||
component: LicensingComponent,
|
||||
canActivate: [LicensingGuard],
|
||||
canDeactivate: [LicensingGuard]
|
||||
path: 'licensing',
|
||||
loadChildren: () => LicensingModule
|
||||
},
|
||||
{ path: 'home', component: HomeComponent },
|
||||
{
|
||||
path: 'home',
|
||||
loadChildren: () => HomeModule
|
||||
},
|
||||
{
|
||||
/**
|
||||
* Load editor module with subroutes
|
||||
*/
|
||||
path: 'editor',
|
||||
loadChildren: () => EditorModule
|
||||
},
|
||||
@@ -54,16 +60,20 @@ export const ROUTES: Routes = [
|
||||
path: 'stage',
|
||||
loadChildren: () => StageModule
|
||||
},
|
||||
{ path: 'system', component: SystemComponent },
|
||||
{ path: 'actions/:libds/:dsid', component: ActionsComponent },
|
||||
{ path: 'history', component: HistoryComponent },
|
||||
{ path: 'submitted', component: SubmitterComponent },
|
||||
{ path: 'submitted/:tableId', component: SubmitterComponent },
|
||||
{ path: 'deploy', component: DeployComponent },
|
||||
{ path: 'deploy/manualdeploy', component: DeployComponent },
|
||||
{
|
||||
path: 'system',
|
||||
loadChildren: () => SystemModule
|
||||
},
|
||||
{
|
||||
path: 'deploy',
|
||||
loadChildren: () => DeployModule
|
||||
},
|
||||
{ path: '**', component: NotFoundComponent }
|
||||
]
|
||||
|
||||
/**
|
||||
* Exporting routes
|
||||
*/
|
||||
export const ROUTING: ModuleWithProviders<RouterModule> = RouterModule.forRoot(
|
||||
ROUTES,
|
||||
{ useHash: true }
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
.loader {
|
||||
display:flex;
|
||||
justify-content: center;
|
||||
height:75vh;
|
||||
align-items:center;
|
||||
flex-direction:column
|
||||
}
|
||||
.modalLarge {
|
||||
width: 50rem!important;
|
||||
}
|
||||
|
||||
.addedRow {
|
||||
background: rgb(146, 208, 154);
|
||||
border: 1px solid rgba(9, 77, 117, 0.2);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.deletedRow {
|
||||
background: rgb(230, 179, 179);
|
||||
border: 1px solid rgba(70, 71, 70, 0.2);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.updatedRow {
|
||||
background: #fafda8;
|
||||
border: 1px solid rgba(9, 117, 9, 0.2);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.table {
|
||||
border: 0px solid;
|
||||
}
|
||||
|
||||
.ch {
|
||||
background: rgba(0,0,0,.1);
|
||||
border: 1px solid rgba(104, 100, 0, 0.4);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.ch:hover {
|
||||
background: rgba(252, 135, 120, 0.4);
|
||||
}
|
||||
|
||||
.tooltip .tooltip-content.tooltip-top-right, .tooltip.tooltip-top-right>.tooltip-content, .tooltip>.tooltip-content {
|
||||
font-size: .54167rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: normal;
|
||||
background: #314351;
|
||||
border-radius: .125rem;
|
||||
color: #f0f1ec;;
|
||||
line-height: .75rem;
|
||||
margin: 0;
|
||||
padding: .375rem .5rem;
|
||||
width: 235px;
|
||||
position: absolute;
|
||||
top: auto;
|
||||
bottom: 100%;
|
||||
left: 12px;
|
||||
right: auto;
|
||||
border-bottom-left-radius: 0;
|
||||
margin-bottom: .66667rem;
|
||||
}
|
||||
|
||||
.tooltip .tooltip-content.tooltip-top-right:before, .tooltip.tooltip-top-right>.tooltip-content:before, .tooltip>.tooltip-content:before {
|
||||
position: absolute;
|
||||
bottom: -.375rem;
|
||||
left: 0;
|
||||
top: auto;
|
||||
right: auto;
|
||||
content: "";
|
||||
border-left: .25rem solid #314351;
|
||||
border-top: .20833rem solid #314351;
|
||||
border-right: .25rem solid transparent;
|
||||
border-bottom: .20833rem solid transparent;
|
||||
}
|
||||
|
||||
.table {
|
||||
border: 0px solid;
|
||||
}
|
||||
|
||||
.toggle-switch input[type=checkbox]:checked+label:before {
|
||||
border-color: #314351;
|
||||
background-color: #314351!important;
|
||||
transition: .15s ease-in;
|
||||
transition-property: border-color,background-color;
|
||||
}
|
||||
|
||||
.tableCont {
|
||||
overflow:auto;
|
||||
margin: 15px 10px 10px 10px;
|
||||
|
||||
td {
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.approvalInfo {
|
||||
display: flex;
|
||||
justify-content: flex-end
|
||||
}
|
||||
|
||||
.approvalBack {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
@media screen and (max-width:768px) {
|
||||
.approvalInfo {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 15px;
|
||||
|
||||
}
|
||||
|
||||
.approvalBack {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-top:0rem!important;
|
||||
min-height: calc(100vh - 0px)!important;
|
||||
}
|
||||
|
||||
.table td.left, .table th.left {
|
||||
text-align: left;
|
||||
width: 150px!important;
|
||||
flex: 0
|
||||
}
|
||||
}
|
||||
|
||||
.table td.left, .table th.left {
|
||||
text-align: left;
|
||||
flex: 1;
|
||||
width: 300px!important;
|
||||
}
|
||||
|
||||
|
||||
.tooll {
|
||||
position: absolute;
|
||||
background: #e6b3b3;
|
||||
color: #314351;
|
||||
top: 0px;
|
||||
height: 36px;
|
||||
width: 100%;
|
||||
left: 0px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#acceptBtn, #rejectBtn {
|
||||
width: 175px
|
||||
}
|
||||
|
||||
.formatted-values-toggle {
|
||||
min-width: 75px
|
||||
}
|
||||
|
||||
clr-modal {
|
||||
::ng-deep {
|
||||
.modal-body-wrapper {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rows-notice {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 10px;
|
||||
color: #6a6a6a;
|
||||
font-size: 15px;
|
||||
|
||||
clr-icon {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
.column-center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.datagrid .datagrid-column .datagrid-column-title{
|
||||
outline: none!important;
|
||||
}
|
||||
|
||||
.links {
|
||||
font-weight: 700;cursor: pointer;
|
||||
}
|
||||
|
||||
.tooltip.tooltip-bottom-left>.tooltip-content, .tooltip .tooltip-content.tooltip-bottom-left {
|
||||
background: #314351!important;
|
||||
}
|
||||
.tooltip.tooltip-bottom-left>.tooltip-content:before, .tooltip .tooltip-content.tooltip-bottom-left:before {
|
||||
border-right: .25rem solid #314351;
|
||||
border-bottom: .20833rem solid #314351;
|
||||
}
|
||||
|
||||
.noBorder {
|
||||
border-bottom: 1px solid transparent!important;
|
||||
}
|
||||
|
||||
.approvals-list-wrapper {
|
||||
height: 70vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.noapprovals-info-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: calc(100vh - 200px);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { DeployComponent } from './deploy.component'
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: DeployComponent },
|
||||
{ path: 'manualdeploy', component: DeployComponent }
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class DeployRoutingModule {}
|
||||
@@ -1,11 +1,11 @@
|
||||
<div class="content-area position-relative">
|
||||
<main class="content-area position-relative">
|
||||
<div class="clr-row">
|
||||
<!-- T&C section -->
|
||||
<div *ngIf="step === 0" id="TCS" class="card">
|
||||
<div class="card-header">Terms and Conditions</div>
|
||||
<div class="card-block">
|
||||
<div class="card-text">
|
||||
<p>
|
||||
<p class="mt-0">
|
||||
The Demo version of Data Controller is free for EVALUATION purposes
|
||||
only. Before proceeding with configuration, please confirm that you
|
||||
have read, understood, and agreed to the
|
||||
@@ -97,4 +97,4 @@
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
.card {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.log-wrapper {
|
||||
width: 100%;
|
||||
background: #f0f0f0;
|
||||
border: 1px solid #c9c9c9;
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
#contexts-btn {
|
||||
padding: 0;
|
||||
min-width: 30px;
|
||||
margin-left: 10px;
|
||||
height: 30px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.validation-bar {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
align-items: center;
|
||||
|
||||
clr-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.autodeploy-section {
|
||||
padding: 0px 15px;
|
||||
|
||||
.clr-checkbox-wrapper {
|
||||
margin: 20px 0 20px 0;
|
||||
}
|
||||
|
||||
.btn-autodeploy {
|
||||
display: block;
|
||||
margin: 15px 0 15px 0;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { Component, OnInit, ViewEncapsulation } from '@angular/core'
|
||||
import { SasService } from '../services/sas.service'
|
||||
import { SASjsConfig } from '@sasjs/adapter'
|
||||
import { Router } from '@angular/router'
|
||||
@@ -13,7 +13,9 @@ import { DcAdapterSettings } from '../models/DcAdapterSettings'
|
||||
styleUrls: ['./deploy.component.scss'],
|
||||
host: {
|
||||
class: 'content-container'
|
||||
}
|
||||
},
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: false
|
||||
})
|
||||
export class DeployComponent implements OnInit {
|
||||
public step: number = 0
|
||||
@@ -56,28 +58,12 @@ export class DeployComponent implements OnInit {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.sasJsConfig.serverType === ServerType.SasViya) {
|
||||
fetch('sasbuild/viya.json')
|
||||
.then((res) => res.text())
|
||||
.then((res) => {
|
||||
let initJsonFile: any = null
|
||||
|
||||
try {
|
||||
initJsonFile = JSON.parse(res)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
if (initJsonFile) {
|
||||
this.jsonFile = initJsonFile
|
||||
this.loggerService.log(this.jsonFile)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.setDeployDefaults()
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting default values used for deploy request
|
||||
*/
|
||||
public setDeployDefaults() {
|
||||
this.dcPath = this.dcAdapterSettings?.dcPath || ''
|
||||
this.selectedAdminGroup = this.dcAdapterSettings?.adminGroup || ''
|
||||
@@ -86,6 +72,9 @@ export class DeployComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepting terms of service shows next screen
|
||||
*/
|
||||
public termsAgreeChange() {
|
||||
if (!this.autodeploy) {
|
||||
this.getAdminGroups()
|
||||
@@ -94,6 +83,9 @@ export class DeployComponent implements OnInit {
|
||||
this.step++
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches admin groups from VIYA to be selected for a backend deploy
|
||||
*/
|
||||
public getAdminGroups() {
|
||||
fetch(
|
||||
this.sasJsConfig.serverUrl + '/identities/groups?sortBy=name&limit=5000',
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { DeployComponent } from './deploy.component'
|
||||
import { AutomaticComponent } from './sections/automatic/automatic.component'
|
||||
import { ManualComponent } from './sections/manual/manual.component'
|
||||
import { SasjsConfiguratorComponent } from './sections/sasjs-configurator/sasjs-configurator.component'
|
||||
import { ClarityModule } from '@clr/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { DeployRoutingModule } from './deploy-routing.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
DeployComponent,
|
||||
AutomaticComponent,
|
||||
ManualComponent,
|
||||
SasjsConfiguratorComponent
|
||||
],
|
||||
imports: [CommonModule, FormsModule, ClarityModule, DeployRoutingModule]
|
||||
})
|
||||
export class DeployModule {}
|
||||
@@ -9,14 +9,17 @@
|
||||
<p class="m-0 align-self-start">Done</p>
|
||||
<hr class="w-100" />
|
||||
|
||||
<div class="deploy-status-row">
|
||||
<div
|
||||
*ngIf="autoDeployStatus.deployServicePack !== null"
|
||||
class="deploy-status-row"
|
||||
>
|
||||
<clr-icon
|
||||
*ngIf="autoDeployStatus.deployServicePack"
|
||||
*ngIf="autoDeployStatus.deployServicePack === true"
|
||||
class="deploy-success"
|
||||
shape="success-standard"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
*ngIf="!autoDeployStatus.deployServicePack"
|
||||
*ngIf="!autoDeployStatus.deployServicePack === false"
|
||||
class="deploy-error"
|
||||
shape="times-circle"
|
||||
></clr-icon>
|
||||
@@ -52,7 +55,7 @@
|
||||
class="deploy-error"
|
||||
shape="times-circle"
|
||||
></clr-icon>
|
||||
LAUNCH / CONFIGURE
|
||||
LAUNCH
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -94,20 +97,72 @@
|
||||
</div>
|
||||
|
||||
<label for="dcloc" class="mt-20 clr-control-label">DC Loc</label>
|
||||
<div class="mb-10 clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
<p class="mt-0">{{ dcPath }}</p>
|
||||
<div class="mb-10 clr-control-container dc-loc-input-wrapper">
|
||||
<div class="clr-input-wrapper small-mt">
|
||||
<input clrInput name="dcloc" [(ngModel)]="dcPath" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="dcloc" class="mt-20 clr-control-label">SAS Admin group</label>
|
||||
<div class="mb-10 clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
<p class="mt-0">{{ selectedAdminGroup }}</p>
|
||||
<div class="clr-input-wrapper small-mt">
|
||||
<select
|
||||
*ngIf="!adminGroupsLoading"
|
||||
clrSelect
|
||||
name="options"
|
||||
[(ngModel)]="selectedAdminGroup"
|
||||
>
|
||||
<option *ngFor="let adminGroup of adminGroups" [value]="adminGroup.id">
|
||||
{{ adminGroup.name }}
|
||||
</option>
|
||||
</select>
|
||||
<clr-spinner
|
||||
clrInline
|
||||
class="spinner-sm"
|
||||
*ngIf="adminGroupsLoading"
|
||||
></clr-spinner>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<clr-checkbox-wrapper>
|
||||
<label for="computeContext" class="mt-20 clr-control-label"
|
||||
>Compute Context</label
|
||||
>
|
||||
<div class="mb-10 clr-control-container">
|
||||
<div class="clr-input-wrapper small-mt">
|
||||
<select
|
||||
*ngIf="!computeContextsLoading"
|
||||
clrSelect
|
||||
name="options"
|
||||
(ngModelChange)="onComputeContextChange($event)"
|
||||
[(ngModel)]="selectedComputeContext"
|
||||
>
|
||||
<option
|
||||
*ngFor="let computeContext of computeContexts"
|
||||
[value]="computeContext.id"
|
||||
>
|
||||
{{ computeContext.name }}
|
||||
</option>
|
||||
</select>
|
||||
<clr-spinner
|
||||
clrInline
|
||||
class="spinner-sm"
|
||||
*ngIf="computeContextsLoading"
|
||||
></clr-spinner>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="runningAsUser">
|
||||
<label for="dcloc" class="mt-20 clr-control-label">Running as user:</label>
|
||||
<div class="mb-10 clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
<p class="mt-0">{{ runningAsUser }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- Keeping this for a reference in case future VIYA changes and starts allowing separate backend and frontend) -->
|
||||
|
||||
<!-- <clr-checkbox-wrapper>
|
||||
<input
|
||||
clrCheckbox
|
||||
[(ngModel)]="recreateDatabase"
|
||||
@@ -116,19 +171,28 @@
|
||||
checked
|
||||
/>
|
||||
<label>Recreate database</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-wrapper> -->
|
||||
|
||||
<hr />
|
||||
|
||||
<button
|
||||
(click)="runAutoDeploy()"
|
||||
class="btn-autodeploy btn btn-primary d-inline-block mr-10"
|
||||
>
|
||||
Deploy
|
||||
</button>
|
||||
|
||||
<!-- Keeping this for a reference in case future VIYA changes and starts allowing separate backend and frontend) -->
|
||||
|
||||
<!-- <button
|
||||
(click)="executeJson()"
|
||||
class="btn-autodeploy btn btn-primary d-inline-block mr-10"
|
||||
[disabled]="!jsonFile"
|
||||
>
|
||||
Deploy {{ !jsonFile ? '(json file is not available)' : '' }}
|
||||
</button>
|
||||
</button> -->
|
||||
|
||||
<button
|
||||
<!-- <button
|
||||
(click)="uploadJsonAuto.click()"
|
||||
class="btn-autodeploy btn btn-primary d-inline-block mr-10"
|
||||
>
|
||||
@@ -140,7 +204,7 @@
|
||||
hidden
|
||||
(click)="clearUploadInput($event)"
|
||||
(change)="onJsonFileChange($event)"
|
||||
/>
|
||||
/> -->
|
||||
|
||||
<clr-modal [(clrModalOpen)]="recreateDatabaseModal" [clrModalClosable]="false">
|
||||
<h3 class="modal-title">Warning</h3>
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
.auto-deploy {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.spinner-box {
|
||||
width: 400px;
|
||||
padding: 20px;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
box-shadow: 1px 1px 8px 0px #00000082;
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.deploy-status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: flex-start;
|
||||
|
||||
p {
|
||||
margin: 0 0 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.deploy-success {
|
||||
color: #6ECF44;
|
||||
}
|
||||
|
||||
.deploy-error {
|
||||
color: #E74C3C;
|
||||
// width: 20px;
|
||||
// height: 20px;
|
||||
}
|
||||
|
||||
.deploy-undeterminated {
|
||||
color: #cacaca;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-bottom: 1px solid #00000045;
|
||||
}
|
||||
@@ -1,15 +1,36 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core'
|
||||
import SASjs, { SASjsConfig } from '@sasjs/adapter'
|
||||
import { DcAdapterSettings } from 'src/app/models/DcAdapterSettings'
|
||||
import { HelperService } from 'src/app/services'
|
||||
import { DeployService } from 'src/app/services/deploy.service'
|
||||
import { EventService } from 'src/app/services/event.service'
|
||||
import { LoggerService } from 'src/app/services/logger.service'
|
||||
import { SasViyaService } from 'src/app/services/sas-viya.service'
|
||||
import { SasService } from 'src/app/services/sas.service'
|
||||
import { ViyaApiCurrentUser } from 'src/app/viya-api-explorer/models/viya-api-current-user.model'
|
||||
import {
|
||||
Item,
|
||||
ViyaApiIdentities
|
||||
} from 'src/app/viya-api-explorer/models/viya-api-identities.model'
|
||||
import { ComputeContextDetails } from 'src/app/viya-api-explorer/models/viya-compute-context-details.model'
|
||||
import {
|
||||
ViyaComputeContexts,
|
||||
Item as ComputeContextItem
|
||||
} from 'src/app/viya-api-explorer/models/viya-compute-contexts.model'
|
||||
|
||||
@Component({
|
||||
selector: 'app-automatic-deploy',
|
||||
templateUrl: './automatic.component.html',
|
||||
styleUrls: ['./automatic.component.scss']
|
||||
styleUrls: ['./automatic.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: false
|
||||
})
|
||||
export class AutomaticComponent implements OnInit {
|
||||
@Input() sasJs!: SASjs
|
||||
@@ -21,6 +42,7 @@ export class AutomaticComponent implements OnInit {
|
||||
|
||||
@Output() onNavigateToHome: EventEmitter<any> = new EventEmitter<any>()
|
||||
|
||||
public selectedComputeContext: string = ''
|
||||
public makeDataResponse: string = ''
|
||||
public jsonFile: any = null
|
||||
public autodeploying: boolean = false
|
||||
@@ -28,8 +50,19 @@ export class AutomaticComponent implements OnInit {
|
||||
public recreateDatabaseModal: boolean = false
|
||||
public isSubmittingJson: boolean = false
|
||||
public isJsonSubmitted: boolean = false
|
||||
public recreateDatabase: boolean = false
|
||||
/**
|
||||
* Default was `false` when deploy was done with frontend and backend separately.
|
||||
* Now we are using only streaming app, so we always want to recreate database (makedata)
|
||||
*/
|
||||
public recreateDatabase: boolean = true
|
||||
public createDatabaseLoading: boolean = false
|
||||
public adminGroupsLoading: boolean = false
|
||||
public currentUserInfoLoading: boolean = false
|
||||
public computeContextsLoading: boolean = false
|
||||
public adminGroups: { id: string; name: string }[] = []
|
||||
public runningAsUser: string | undefined
|
||||
public currentUserInfo: ViyaApiCurrentUser | null = null
|
||||
public computeContexts: ComputeContextItem[] = []
|
||||
|
||||
/** autoDeployStatus
|
||||
* This object presents the status for two steps that we have for deploy.
|
||||
@@ -46,17 +79,147 @@ export class AutomaticComponent implements OnInit {
|
||||
runMakeData: null
|
||||
}
|
||||
|
||||
public sasjsConfig = this.sasService.getSasjsConfig()
|
||||
|
||||
/**
|
||||
* makedata service will be run in a new window
|
||||
* This is needed to ensure that the user can see the logs
|
||||
* and the progress of the service execution.
|
||||
* If this is set to `false`, the service will be run in the same window
|
||||
* using the adapter request method.
|
||||
*/
|
||||
public deployInNewWindow: boolean = true
|
||||
|
||||
constructor(
|
||||
private eventService: EventService,
|
||||
private deployService: DeployService,
|
||||
private sasService: SasService,
|
||||
private loggerService: LoggerService
|
||||
private sasViyaService: SasViyaService,
|
||||
private loggerService: LoggerService,
|
||||
private helperService: HelperService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
ngOnInit(): void {
|
||||
this.loadData()
|
||||
}
|
||||
|
||||
public async loadData() {
|
||||
await this.getAdminGroups()
|
||||
await this.getComputeContexts()
|
||||
await this.getCurrentUser()
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.selectedComputeContext) {
|
||||
this.onComputeContextChange(this.selectedComputeContext)
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
public async getComputeContexts() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.computeContextsLoading = true
|
||||
|
||||
this.sasViyaService.getComputeContexts().subscribe(
|
||||
(res: ViyaComputeContexts) => {
|
||||
this.computeContextsLoading = false
|
||||
|
||||
const defaultContext = res.items.find(
|
||||
(item: ComputeContextItem) =>
|
||||
item.name === 'SAS Job Execution compute context'
|
||||
)
|
||||
|
||||
if (defaultContext) {
|
||||
this.selectedComputeContext = defaultContext.id
|
||||
}
|
||||
|
||||
this.computeContexts = res.items
|
||||
|
||||
resolve()
|
||||
},
|
||||
(err) => {
|
||||
reject(err)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public async getCurrentUser() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.currentUserInfoLoading = true
|
||||
|
||||
this.sasViyaService.getCurrentUser().subscribe(
|
||||
(res: ViyaApiCurrentUser) => {
|
||||
this.currentUserInfoLoading = false
|
||||
|
||||
this.currentUserInfo = res
|
||||
|
||||
this.dcPath = `/export/viya/homes/${res.id}`
|
||||
|
||||
resolve()
|
||||
},
|
||||
(err) => {
|
||||
console.error('Error while getting current user', err)
|
||||
reject(err)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public async getAdminGroups() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.adminGroupsLoading = true
|
||||
;(this.sasViyaService
|
||||
.getAdminGroups()
|
||||
.subscribe((res: ViyaApiIdentities) => {
|
||||
this.adminGroupsLoading = false
|
||||
// Map admin groups with only needed fields
|
||||
this.adminGroups = res.items.map((item: Item) => {
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name
|
||||
}
|
||||
})
|
||||
|
||||
resolve()
|
||||
}),
|
||||
(err: any) => {
|
||||
this.adminGroupsLoading = false
|
||||
this.loggerService.error('Error while getting admin groups', err)
|
||||
this.eventService.showAbortModal('admin groups', err)
|
||||
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public async onComputeContextChange(computeContextId: string) {
|
||||
this.sasViyaService
|
||||
.getComputeContextById(computeContextId)
|
||||
.subscribe((res: ComputeContextDetails) => {
|
||||
if (res.attributes && res.attributes.runServerAs) {
|
||||
this.runningAsUser = res.attributes.runServerAs
|
||||
} else {
|
||||
this.runningAsUser = this.currentUserInfo?.id || 'unknown'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public getComputeContextName(id: string): string | undefined {
|
||||
return (
|
||||
this.computeContexts.find(
|
||||
(context: ComputeContextItem) => context.id === id
|
||||
)?.name || undefined
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes sas.json file to deploy the backend
|
||||
* Method will first try to run the `auto deploy`
|
||||
* If that fails the rest of the code is ignored.
|
||||
* If request is successfull, method will continue to try
|
||||
* to create database if checkbox is toggled on
|
||||
*/
|
||||
public async executeJson() {
|
||||
this.autodeploying = true
|
||||
this.isSubmittingJson = true
|
||||
|
||||
try {
|
||||
@@ -91,14 +254,25 @@ export class AutomaticComponent implements OnInit {
|
||||
}
|
||||
|
||||
this.isSubmittingJson = false
|
||||
}
|
||||
|
||||
public async runAutoDeploy(executeJson: boolean = false) {
|
||||
if (!this.deployInNewWindow) this.autodeploying = true
|
||||
|
||||
if (executeJson) {
|
||||
this.executeJson()
|
||||
}
|
||||
|
||||
if (this.recreateDatabase) {
|
||||
this.createDatabase()
|
||||
} else {
|
||||
this.autodeployDone = true
|
||||
if (!this.deployInNewWindow) this.autodeployDone = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the `makedata` request sending the ADMIN and DCPATH values
|
||||
*/
|
||||
public createDatabase() {
|
||||
let data = {
|
||||
fromjs: [
|
||||
@@ -109,45 +283,160 @@ export class AutomaticComponent implements OnInit {
|
||||
]
|
||||
}
|
||||
|
||||
// Get and run service using the selected context name
|
||||
let selectedComputeContextName = this.sasJsConfig.contextName
|
||||
|
||||
if (this.selectedComputeContext.length && this.computeContexts.length) {
|
||||
const computeContextName = this.getComputeContextName(
|
||||
this.selectedComputeContext
|
||||
)
|
||||
|
||||
if (computeContextName) {
|
||||
selectedComputeContextName = computeContextName
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We are overriding default `sasjsConfig` object fields with this object fields.
|
||||
* Here we want to run this request using original WEB method.
|
||||
* contextName: null is the MUST field for it.
|
||||
*/
|
||||
let overrideConfig = {
|
||||
useComputeApi: false,
|
||||
contextName: this.sasJsConfig.contextName,
|
||||
useComputeApi: null,
|
||||
contextName: selectedComputeContextName,
|
||||
debug: true
|
||||
}
|
||||
|
||||
this.sasJs
|
||||
.request(`services/admin/makedata`, data, overrideConfig, () => {
|
||||
this.sasService.shouldLogin.next(true)
|
||||
if (this.deployInNewWindow) {
|
||||
this.runMakedataInNewWindow({
|
||||
contextName: selectedComputeContextName,
|
||||
admin: this.selectedAdminGroup,
|
||||
dcPath: this.dcPath
|
||||
})
|
||||
.then((res: any) => {
|
||||
this.autodeployDone = true
|
||||
} else {
|
||||
this.sasJs
|
||||
.request(`services/admin/makedata`, data, overrideConfig, () => {
|
||||
this.sasService.shouldLogin.next(true)
|
||||
})
|
||||
.then((res: any) => {
|
||||
this.autodeployDone = true
|
||||
|
||||
try {
|
||||
this.makeDataResponse = JSON.stringify(res)
|
||||
} catch {
|
||||
this.makeDataResponse = res
|
||||
}
|
||||
try {
|
||||
this.makeDataResponse = JSON.stringify(res)
|
||||
} catch {
|
||||
this.makeDataResponse = res
|
||||
}
|
||||
|
||||
if (res.result && res.result.length > 0) {
|
||||
this.autoDeployStatus.runMakeData = true
|
||||
} else {
|
||||
if (res.result && res.result.length > 0) {
|
||||
this.autoDeployStatus.runMakeData = true
|
||||
} else {
|
||||
this.autoDeployStatus.runMakeData = false
|
||||
}
|
||||
|
||||
if (typeof res.sasjsAbort !== 'undefined') {
|
||||
const abortRes = res
|
||||
const abortMsg = abortRes.sasjsAbort[0].MSG
|
||||
const macMsg = abortRes.sasjsAbort[0].MAC
|
||||
|
||||
this.eventService.showAbortModal('makedata', abortMsg, {
|
||||
SYSWARNINGTEXT: abortRes.SYSWARNINGTEXT,
|
||||
SYSERRORTEXT: abortRes.SYSERRORTEXT,
|
||||
MAC: macMsg
|
||||
})
|
||||
}
|
||||
|
||||
if (this.helperService.isStreamingViya())
|
||||
this.updateIndexHtmlComputeContext()
|
||||
})
|
||||
.catch((err: any) => {
|
||||
this.eventService.showAbortModal('makedata', JSON.stringify(err))
|
||||
this.autoDeployStatus.runMakeData = false
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
this.autoDeployStatus.runMakeData = false
|
||||
this.autodeployDone = true
|
||||
this.autodeployDone = true
|
||||
|
||||
try {
|
||||
this.makeDataResponse = JSON.stringify(err)
|
||||
} catch {
|
||||
this.makeDataResponse = err
|
||||
}
|
||||
try {
|
||||
this.makeDataResponse = JSON.stringify(err)
|
||||
} catch {
|
||||
this.makeDataResponse = err
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public runMakedataInNewWindow(params: {
|
||||
contextName: string
|
||||
admin: string
|
||||
dcPath: string
|
||||
}) {
|
||||
let serverUrl = this.sasjsConfig.serverUrl
|
||||
let appLoc = this.sasjsConfig.appLoc
|
||||
const execPath = this.sasService.getExecutionPath()
|
||||
let contextname = `&_contextname=${params.contextName}`
|
||||
let admin = `&admin=${params.admin}`
|
||||
let dcPath = `&dcpath=${params.dcPath}`
|
||||
let debug = this.sasService.getDebugUrlParam()
|
||||
|
||||
let programUrl =
|
||||
serverUrl +
|
||||
execPath +
|
||||
'/?_program=' +
|
||||
appLoc +
|
||||
'/services/admin/makedata' +
|
||||
contextname +
|
||||
admin +
|
||||
dcPath +
|
||||
debug
|
||||
|
||||
window.open(programUrl)
|
||||
}
|
||||
|
||||
/**
|
||||
* Only when on Viya streamed app, this method will update the `contextname` in the `index.html` on the SAS drive
|
||||
* This is needed to ensure that the DC will use the same compute context `makedata` service used to run against.
|
||||
*/
|
||||
public async updateIndexHtmlComputeContext() {
|
||||
const filenamePath = location.search.split('/').pop()
|
||||
const filename = filenamePath?.includes('.') ? filenamePath : undefined
|
||||
|
||||
if (!filename) {
|
||||
this.eventService.showAbortModal(
|
||||
null,
|
||||
'We could not figure out the file name of `index.html` based on the url.'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const indexHtmlContent = await this.sasService.getFileContent(
|
||||
`${this.appLoc}/services`,
|
||||
filename
|
||||
)
|
||||
|
||||
if (!indexHtmlContent) {
|
||||
this.loggerService.error(
|
||||
`Failed to get ${filename} at ${this.appLoc}/services`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const computeContextName = this.getComputeContextName(
|
||||
this.selectedComputeContext
|
||||
)
|
||||
|
||||
if (!computeContextName) {
|
||||
this.loggerService.error(
|
||||
`Compute context name not found for ID: ${this.selectedComputeContext} | List: ${JSON.stringify(this.computeContexts)}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const updatedContent = indexHtmlContent.replace(
|
||||
/contextname="[^"]*"/g,
|
||||
`contextname="${computeContextName}"`
|
||||
)
|
||||
|
||||
await this.sasService
|
||||
.updateFileContent(`${this.appLoc}/services`, filename, updatedContent)
|
||||
.catch((err: any) => {
|
||||
this.loggerService.error(`Failed to update DataController.html: ${err}`)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.clear-memory-button {
|
||||
right: 10px;
|
||||
top: 2px;
|
||||
}
|
||||
@@ -1,6 +1,14 @@
|
||||
import { Component, Input, OnInit, Output, EventEmitter } from '@angular/core'
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
EventEmitter,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core'
|
||||
import SASjs, { SASjsConfig } from '@sasjs/adapter'
|
||||
import { DcAdapterSettings } from 'src/app/models/DcAdapterSettings'
|
||||
import { RequestWrapperResponse } from 'src/app/models/request-wrapper/RequestWrapperResponse'
|
||||
import { DeployService } from 'src/app/services/deploy.service'
|
||||
import { EventService } from 'src/app/services/event.service'
|
||||
import { LoggerService } from 'src/app/services/logger.service'
|
||||
@@ -9,7 +17,9 @@ import { SasService } from 'src/app/services/sas.service'
|
||||
@Component({
|
||||
selector: 'app-manual-deploy',
|
||||
templateUrl: './manual.component.html',
|
||||
styleUrls: ['./manual.component.scss']
|
||||
styleUrls: ['./manual.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: false
|
||||
})
|
||||
export class ManualComponent implements OnInit {
|
||||
@Input() sasJs!: SASjs
|
||||
@@ -52,6 +62,9 @@ export class ManualComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
/**
|
||||
* FIXME: Remove
|
||||
*/
|
||||
public async executableContext() {
|
||||
// getExecutableContexts now need AuthConfig parameter which we don't have on web
|
||||
// this.contextsLoading = true
|
||||
@@ -65,10 +78,16 @@ export class ManualComponent implements OnInit {
|
||||
// this.contextsLoading = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes sas.json file attached to the input
|
||||
*/
|
||||
public clearUploadInput(event: Event) {
|
||||
this.deployService.clearUploadInput(event)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads attached SAS file to be sent to sas for execution (backend deploy)
|
||||
*/
|
||||
public onSasFileChange(event: any) {
|
||||
this.preloadedFile = false
|
||||
|
||||
@@ -93,12 +112,18 @@ export class ManualComponent implements OnInit {
|
||||
fileReader.readAsText(file)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads attached JSON file to be sent to sas for execution (backend deploy)
|
||||
*/
|
||||
public async onJsonFileChange(event: any) {
|
||||
let file = event.target.files[0]
|
||||
|
||||
this.jsonFile = await this.deployService.readFile(file)
|
||||
}
|
||||
|
||||
/**
|
||||
* Appending precode lines to the attached sas or json file for backend deploy
|
||||
*/
|
||||
public addPrecodeLines() {
|
||||
let headerLines = [
|
||||
`%let context=${this.selectedContext};`,
|
||||
@@ -110,6 +135,9 @@ export class ManualComponent implements OnInit {
|
||||
this.linesOfCode.unshift(...headerLines)
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloadng file with precode included
|
||||
*/
|
||||
public downloadSasPrecodeFile() {
|
||||
let linesAsText = this.linesOfCode.join('\n')
|
||||
let filename = this.fileName.split('.')[0]
|
||||
@@ -117,6 +145,9 @@ export class ManualComponent implements OnInit {
|
||||
this.downloadFile(linesAsText, filename, 'sas')
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for downloading log and repsonse as a file
|
||||
*/
|
||||
public downloadFile(
|
||||
content: any,
|
||||
filename: string,
|
||||
@@ -125,10 +156,17 @@ export class ManualComponent implements OnInit {
|
||||
this.deployService.downloadFile(content, filename, extension)
|
||||
}
|
||||
|
||||
/**
|
||||
* Saving dcpath to localstorage
|
||||
* FIXME: maybe it'snot necessary
|
||||
*/
|
||||
public saveDcPath() {
|
||||
localStorage.setItem('deploy_dc_loc', this.dcPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send sas.json to be executed (deploying backend)
|
||||
*/
|
||||
public async executeJson() {
|
||||
this.isSubmittingJson = true
|
||||
|
||||
@@ -162,6 +200,9 @@ export class ManualComponent implements OnInit {
|
||||
this.isSubmittingJson = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Send sas file to be executed (deploying backend)
|
||||
*/
|
||||
public async executeSAS() {
|
||||
this.executingScript = true
|
||||
this.jobLog = ''
|
||||
@@ -194,6 +235,10 @@ export class ManualComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Running makedata service
|
||||
* @param newTab open and run in new tab
|
||||
*/
|
||||
public createDatabase(newTab: boolean = true) {
|
||||
if (newTab) {
|
||||
let url =
|
||||
@@ -206,7 +251,7 @@ export class ManualComponent implements OnInit {
|
||||
this.selectedAdminGroup +
|
||||
'&DCPATH=' +
|
||||
this.dcPath +
|
||||
'&_debug=131'
|
||||
this.sasService.getDebugUrlParam()
|
||||
|
||||
window.open(url, '_blank')
|
||||
|
||||
@@ -230,7 +275,7 @@ export class ManualComponent implements OnInit {
|
||||
* contextName: null is the MUST field for it.
|
||||
*/
|
||||
let overrideConfig = {
|
||||
useComputeApi: false,
|
||||
useComputeApi: null,
|
||||
contextName: this.sasJsConfig.contextName,
|
||||
debug: true
|
||||
}
|
||||
@@ -268,10 +313,10 @@ export class ManualComponent implements OnInit {
|
||||
|
||||
this.sasService
|
||||
.request('public/startupservice', null)
|
||||
.then((res: any) => {
|
||||
this.loggerService.log(res)
|
||||
.then((res: RequestWrapperResponse) => {
|
||||
this.loggerService.log(res.adapterResponse)
|
||||
|
||||
if (res.saslibs) {
|
||||
if (res.adapterResponse.saslibs) {
|
||||
this.validationState = 'success'
|
||||
} else {
|
||||
this.validationState = 'error'
|
||||
|
||||
@@ -10,11 +10,13 @@
|
||||
</p>
|
||||
|
||||
<p class="m-0 mt-10">
|
||||
Please specify a physical directory below, to which user
|
||||
<strong>{{ SYSUSERID }}</strong> can write, on behalf of Data Controller:
|
||||
Please specify a physical directory (on the
|
||||
<strong> {{ SYSHOSTNAME }}</strong>
|
||||
compute server) below, to which user
|
||||
<strong>{{ SYSUSERID }}</strong> can write, on behalf of Data Controller.
|
||||
</p>
|
||||
|
||||
<label class="mt-20 clr-control-label">DC Directory</label>
|
||||
<label class="mt-20 clr-control-label">DC Staging Directory</label>
|
||||
<div class="mb-10 clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
<input
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
.clr-control-container {
|
||||
width: 50vw;
|
||||
}
|
||||
|
||||
.clr-input-wrapper {
|
||||
width: 100%;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.thinProgress {
|
||||
left: 0px;
|
||||
right: 0;
|
||||
width: unset;
|
||||
height: 1px;
|
||||
margin-top: 0 !important;
|
||||
|
||||
&::after {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,27 @@
|
||||
import { Location } from '@angular/common'
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core'
|
||||
import SASjs, { SASjsConfig } from '@sasjs/adapter'
|
||||
import { ServerType } from '@sasjs/utils/types/serverType'
|
||||
import { DcAdapterSettings } from 'src/app/models/DcAdapterSettings'
|
||||
import { RequestWrapperResponse } from 'src/app/models/request-wrapper/RequestWrapperResponse'
|
||||
import { SASGroup } from 'src/app/models/sas/public-getgroups.model'
|
||||
import { SASjsApiServerInfo } from 'src/app/models/sasjs-api/SASjsApiServerInfo.model'
|
||||
import { HelperService } from 'src/app/services/helper.service'
|
||||
import { SasService } from 'src/app/services/sas.service'
|
||||
import { SasjsService } from 'src/app/services/sasjs.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-sasjs-configurator',
|
||||
templateUrl: './sasjs-configurator.component.html',
|
||||
styleUrls: ['./sasjs-configurator.component.scss']
|
||||
styleUrls: ['./sasjs-configurator.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: false
|
||||
})
|
||||
export class SasjsConfiguratorComponent implements OnInit {
|
||||
@Input() sasJs!: SASjs
|
||||
@@ -51,6 +60,9 @@ export class SasjsConfiguratorComponent implements OnInit {
|
||||
this.getServerInfo()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fethes the sasjs server instance info
|
||||
*/
|
||||
getServerInfo() {
|
||||
this.sasjsService
|
||||
.getServerInfo()
|
||||
@@ -59,15 +71,18 @@ export class SasjsConfiguratorComponent implements OnInit {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches user groups from the `usernav/usergroupsbymember` service
|
||||
*/
|
||||
getUserGroups() {
|
||||
this.loading = true
|
||||
|
||||
this.sasService.request('usernav/usergroupsbymember', null).then(
|
||||
(res: any) => {
|
||||
this.METAPERSON = res.MF_GETUSER
|
||||
this.SYSUSERID = res.SYSUSERID
|
||||
this.SYSHOSTNAME = res.SYSHOSTNAME
|
||||
this.SYSVLONG = res.SYSVLONG
|
||||
(res: RequestWrapperResponse) => {
|
||||
this.METAPERSON = res.adapterResponse.MF_GETUSER
|
||||
this.SYSUSERID = res.adapterResponse.SYSUSERID
|
||||
this.SYSHOSTNAME = res.adapterResponse.SYSHOSTNAME
|
||||
this.SYSVLONG = res.adapterResponse.SYSVLONG
|
||||
|
||||
/*
|
||||
We would like to present a default DCPATH (deployment path) to the
|
||||
@@ -83,12 +98,14 @@ export class SasjsConfiguratorComponent implements OnInit {
|
||||
*/
|
||||
this.dcDirectory =
|
||||
this.tmpDirectories[
|
||||
['L', 'H', 'A', 'S'].includes(res.SYSSCPL.substring(0, 1))
|
||||
['L', 'H', 'A', 'S'].includes(
|
||||
res.adapterResponse.SYSSCPL.substring(0, 1)
|
||||
)
|
||||
? 'linux'
|
||||
: 'windows'
|
||||
]
|
||||
|
||||
this.dcAdminGroupList = res.groups
|
||||
this.dcAdminGroupList = res.adapterResponse.groups
|
||||
this.dcAdminGroup = this.dcAdminGroupList[0].GROUPNAME
|
||||
|
||||
this.loading = false
|
||||
@@ -99,6 +116,9 @@ export class SasjsConfiguratorComponent implements OnInit {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creating database
|
||||
*/
|
||||
makeData() {
|
||||
// const _debug = "&_debug=131"; //debug on
|
||||
const _debug = ' ' //debug off
|
||||
|
||||
@@ -4,20 +4,23 @@ import { NgVarDirective } from './ng-var.directive'
|
||||
import { DragNdropDirective } from './drag-ndrop.directive'
|
||||
import { FileDropDirective } from './file-drop.directive'
|
||||
import { FileSelectDirective } from './file-select.directive'
|
||||
import { StealFocusDirective } from './steal-focus.directive'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
NgVarDirective,
|
||||
DragNdropDirective,
|
||||
FileDropDirective,
|
||||
FileSelectDirective
|
||||
FileSelectDirective,
|
||||
StealFocusDirective
|
||||
],
|
||||
imports: [CommonModule],
|
||||
exports: [
|
||||
NgVarDirective,
|
||||
DragNdropDirective,
|
||||
FileDropDirective,
|
||||
FileSelectDirective
|
||||
FileSelectDirective,
|
||||
StealFocusDirective
|
||||
]
|
||||
})
|
||||
export class DirectivesModule {}
|
||||
|
||||
@@ -7,14 +7,17 @@ import {
|
||||
} from '@angular/core'
|
||||
|
||||
@Directive({
|
||||
selector: '[appDragNdrop]'
|
||||
selector: '[appDragNdrop]',
|
||||
standalone: false
|
||||
})
|
||||
export class DragNdropDirective {
|
||||
@HostBinding('class.fileover') fileOver: boolean = false
|
||||
@Output() fileDropped = new EventEmitter<any>()
|
||||
@Output() fileDraggedOver = new EventEmitter<any>()
|
||||
|
||||
// Dragover listener
|
||||
/**
|
||||
* Dragover listener
|
||||
*/
|
||||
@HostListener('dragover', ['$event'])
|
||||
onDragOver(event: any) {
|
||||
event.preventDefault()
|
||||
@@ -26,7 +29,9 @@ export class DragNdropDirective {
|
||||
}
|
||||
}
|
||||
|
||||
// Dragleave listener
|
||||
/**
|
||||
* Dragleave listener
|
||||
*/
|
||||
@HostListener('dragleave', ['$event'])
|
||||
public onDragLeave(event: any) {
|
||||
event.preventDefault()
|
||||
@@ -34,7 +39,9 @@ export class DragNdropDirective {
|
||||
this.fileOver = false
|
||||
}
|
||||
|
||||
// Drop listener
|
||||
/**
|
||||
* Drop listener
|
||||
*/
|
||||
@HostListener('drop', ['$event'])
|
||||
public ondrop(event: any) {
|
||||
event.preventDefault()
|
||||
@@ -48,6 +55,9 @@ export class DragNdropDirective {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks wether dragging element contain files
|
||||
*/
|
||||
private containsFiles(event: any) {
|
||||
if (event && event.dataTransfer && event.dataTransfer.types) {
|
||||
for (let i = 0; i < event.dataTransfer.types.length; i++) {
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
import { FileUploader } from '../models/FileUploader.class'
|
||||
|
||||
@Directive({
|
||||
selector: '[appFileDrop]'
|
||||
selector: '[appFileDrop]',
|
||||
standalone: false
|
||||
})
|
||||
export class FileDropDirective {
|
||||
@Input() uploader?: FileUploader
|
||||
@@ -22,6 +23,9 @@ export class FileDropDirective {
|
||||
this.element = element
|
||||
}
|
||||
|
||||
/**
|
||||
* Dragging element drop event
|
||||
*/
|
||||
@HostListener('drop', ['$event'])
|
||||
onDrop(event: DragEvent): void {
|
||||
this._preventAndStop(event)
|
||||
@@ -39,6 +43,9 @@ export class FileDropDirective {
|
||||
this.fileDrop.emit(fileList)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dragging element drag over event
|
||||
*/
|
||||
@HostListener('dragover', ['$event'])
|
||||
onDragOver(event: DragEvent): void {
|
||||
this._preventAndStop(event)
|
||||
@@ -59,6 +66,10 @@ export class FileDropDirective {
|
||||
this.fileOver.emit(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent propagation trough elements and stop default behavior
|
||||
* For particular event
|
||||
*/
|
||||
protected _preventAndStop(event: MouseEvent): void {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
import { FileUploader } from '../models/FileUploader.class'
|
||||
|
||||
@Directive({
|
||||
selector: '[appFileSelect]'
|
||||
selector: '[appFileSelect]',
|
||||
standalone: false
|
||||
})
|
||||
export class FileSelectDirective {
|
||||
@Input() uploader?: FileUploader
|
||||
@@ -21,6 +22,9 @@ export class FileSelectDirective {
|
||||
this.element = element
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if files exist in the input after input change
|
||||
*/
|
||||
isEmptyAfterSelection(): boolean {
|
||||
return !!this.element.nativeElement.attributes.multiple
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'
|
||||
* Calling functions in html is bad for performance
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[ngVar]'
|
||||
selector: '[ngVar]',
|
||||
standalone: false
|
||||
})
|
||||
export class NgVarDirective {
|
||||
@Input()
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Directive, HostListener } from '@angular/core'
|
||||
|
||||
@Directive({
|
||||
selector: '[appStealFocus]',
|
||||
standalone: false
|
||||
})
|
||||
export class StealFocusDirective {
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* For some reason newest version of Clarity v17.0.1 is stealing focus when
|
||||
* clicking on the input inside of the clr-tree-view
|
||||
* This is workaround
|
||||
*/
|
||||
@HostListener('click', ['$event']) onClick(event: any) {
|
||||
event.target.focus()
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export interface RowValidation {
|
||||
valid: boolean
|
||||
invalidError: string
|
||||
rowNumber?: number
|
||||
colName?: string
|
||||
value?: string
|
||||
}
|
||||
@@ -24,8 +24,8 @@
|
||||
generatedRecordUrl
|
||||
? 'copy to clipboard'
|
||||
: generateEditRecordUrlLoading
|
||||
? 'Generating url...'
|
||||
: 'Link to this record'
|
||||
? 'Generating url...'
|
||||
: 'Link to this record'
|
||||
}}
|
||||
</button>
|
||||
</ng-container>
|
||||
@@ -112,7 +112,7 @@
|
||||
|
||||
<div
|
||||
*ngIf="
|
||||
['autocomplete'].includes(
|
||||
['autocomplete', 'autocomplete.custom'].includes(
|
||||
$any(currentRecordValidator?.getRule(col.key)?.editor)
|
||||
)
|
||||
"
|
||||
@@ -163,7 +163,7 @@
|
||||
|
||||
<div
|
||||
*ngIf="
|
||||
['autocomplete'].includes(
|
||||
['autocomplete', 'autocomplete.custom'].includes(
|
||||
$any(currentRecordValidator?.getRule(col.key)?.editor)
|
||||
)
|
||||
"
|
||||
@@ -177,45 +177,45 @@
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<clr-textarea-container class="date-field" *ngSwitchCase="'time'">
|
||||
<textarea
|
||||
clrTextarea
|
||||
(paste)="recordInputPaste($event)"
|
||||
(input)="recordInputChange($event, col.key)"
|
||||
<div class="date-field" *ngSwitchCase="'intl-time'">
|
||||
<input
|
||||
type="time"
|
||||
step="1"
|
||||
class="date-input"
|
||||
[attr.aria-label]="col.key"
|
||||
[class.invalid-data]="
|
||||
currentRecordInvalidCols.includes(col.key)
|
||||
"
|
||||
[rows]="col.value.length > 80 ? 6 : 1"
|
||||
[(ngModel)]="currentRecord[col.key]"
|
||||
[class.not-char]="
|
||||
currentRecordValidator?.getRule(col.key)?.type
|
||||
"
|
||||
></textarea>
|
||||
<clr-control-helper>HH:mm:ss</clr-control-helper>
|
||||
</clr-textarea-container>
|
||||
[value]="currentRecord[col.key]"
|
||||
(change)="recordTimeChange($event, col.key)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="date-field" *ngSwitchCase="'date'">
|
||||
<textarea
|
||||
clrTextarea
|
||||
(paste)="recordInputPaste($event)"
|
||||
(input)="recordInputChange($event, col.key)"
|
||||
<div class="date-field" *ngSwitchCase="'intl-datetime'">
|
||||
<input
|
||||
type="datetime-local"
|
||||
step="1"
|
||||
class="date-input"
|
||||
[attr.aria-label]="col.key"
|
||||
[class.invalid-data]="
|
||||
currentRecordInvalidCols.includes(col.key)
|
||||
"
|
||||
rows="1"
|
||||
cols="auto"
|
||||
class="not-char"
|
||||
[(ngModel)]="currentRecord[col.key]"
|
||||
></textarea>
|
||||
<clr-date-container class="date-picker">
|
||||
<input
|
||||
type="date"
|
||||
name="date"
|
||||
class="d-none"
|
||||
(clrDateChange)="recordDateChange($event, col.key)"
|
||||
clrDate
|
||||
/>
|
||||
</clr-date-container>
|
||||
[value]="toNativeDatetime(currentRecord[col.key])"
|
||||
(change)="recordDatetimeChange($event, col.key)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="date-field" *ngSwitchCase="'intl-date'">
|
||||
<input
|
||||
type="date"
|
||||
class="date-input"
|
||||
[attr.aria-label]="col.key"
|
||||
[class.invalid-data]="
|
||||
currentRecordInvalidCols.includes(col.key)
|
||||
"
|
||||
[value]="currentRecord[col.key]"
|
||||
(change)="recordDateChange($event, col.key)"
|
||||
/>
|
||||
</div>
|
||||
<div *ngSwitchCase="'autocomplete'">
|
||||
<ng-container
|
||||
@@ -255,6 +255,7 @@
|
||||
type="button"
|
||||
class="btn btn-outline focusable"
|
||||
tabindex="0"
|
||||
aria-label="Previous record"
|
||||
(click)="onPreviousRecordClick()"
|
||||
[disabled]="currentRecordInvalidCols.length > 0"
|
||||
>
|
||||
@@ -267,6 +268,7 @@
|
||||
type="button"
|
||||
class="btn btn-outline focusable"
|
||||
tabindex="0"
|
||||
aria-label="Next record"
|
||||
(click)="onNextRecordClick()"
|
||||
[disabled]="currentRecordInvalidCols.length > 0"
|
||||
>
|
||||
@@ -277,7 +279,7 @@
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline focusable"
|
||||
class="btn btn-outline focusable mr-5i"
|
||||
(click)="currentRecord!.noLinkOption = false; closeRecordEdit()"
|
||||
>
|
||||
Cancel
|
||||
|
||||
@@ -1,241 +0,0 @@
|
||||
.record-edit-modal {
|
||||
.column-entry {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.name-input-row {
|
||||
width: 100%;
|
||||
max-width: 260px;
|
||||
|
||||
.cell-desc {
|
||||
margin-right: 30px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.inputs-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
::ng-deep >*:not(.date-field):not(clr-select-container) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
::ng-deep {
|
||||
.clr-textarea-wrapper {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.clr-form-control {
|
||||
margin-top: 0px !important;
|
||||
}
|
||||
|
||||
app-soft-select {
|
||||
display: block;
|
||||
width: 224px;
|
||||
background: #fff;
|
||||
border: 1px solid #999;
|
||||
color: #000;
|
||||
padding: calc(.25rem + 2px) .5rem;
|
||||
border-radius: .125rem;
|
||||
font-size: .541667rem;
|
||||
margin-right: 6px;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
background-color: #fff;
|
||||
|
||||
&:focus {
|
||||
background: none;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.date-field {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
textarea {
|
||||
width: 230px;
|
||||
}
|
||||
|
||||
.date-picker {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 4px;
|
||||
|
||||
::ng-deep {
|
||||
// clr-datepicker-view-manager {
|
||||
// transform: unset !important;
|
||||
// left: unset !important;
|
||||
// right: 70px !important;
|
||||
// }
|
||||
.clr-input-group {
|
||||
border: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
::ng-deep {
|
||||
clr-select-container {
|
||||
border: 1px solid #999;
|
||||
color: #000;
|
||||
border-radius: .125rem;
|
||||
margin-right: 5px;
|
||||
|
||||
.clr-select-wrapper {
|
||||
max-height: unset;
|
||||
|
||||
&::after {
|
||||
top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
height: auto;
|
||||
padding: 10px;
|
||||
padding-right: 20px;
|
||||
border: 0 !important;
|
||||
|
||||
&:focus {
|
||||
background: 0 0 !important;
|
||||
}
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clr-input-container {
|
||||
width: 224px;
|
||||
background: #fff;
|
||||
border: 1px solid #999;
|
||||
color: #000;
|
||||
padding: calc(.25rem + 2px) .5rem;
|
||||
border-radius: .125rem;
|
||||
font-size: .541667rem;
|
||||
margin-right: 6px;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
|
||||
&:focus {
|
||||
background: none;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.invalid-data {
|
||||
border-color: red;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
width: 80vw;
|
||||
}
|
||||
|
||||
.clr-control-container {
|
||||
width: 100%;
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
resize: none;
|
||||
border-color: #999;
|
||||
|
||||
&.invalid-data {
|
||||
border-color: red;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&.not-char {
|
||||
font-family: "Lucida Console", Monaco, monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.generate-record-url {
|
||||
right: 40px;
|
||||
top: 40px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.generate-record-url-button {
|
||||
right: 25px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
// height: 65px;
|
||||
|
||||
.alert {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.prev-next {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.focusable {
|
||||
&:focus {
|
||||
box-shadow: 0 0 3px 0px #5aa220;
|
||||
}
|
||||
}
|
||||
|
||||
.entry-input-left-offset {
|
||||
left: -30px;
|
||||
}
|
||||
|
||||
.validation-info-alert {
|
||||
width: 310px
|
||||
}
|
||||
@@ -1,12 +1,20 @@
|
||||
import { KeyValue } from '@angular/common'
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
||||
import moment from 'moment'
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core'
|
||||
import { ValidateFilterSASResponse } from 'src/app/models/sas/validate-filter.model'
|
||||
import { QueryClause } from 'src/app/models/TableData'
|
||||
import { HelperService } from 'src/app/services/helper.service'
|
||||
import { SasStoreService } from 'src/app/services/sas-store.service'
|
||||
import { DcValidator } from 'src/app/shared/dc-validator/dc-validator'
|
||||
import { DcValidation } from 'src/app/shared/dc-validator/models/dc-validation.model'
|
||||
import { isEmpty } from 'src/app/shared/dc-validator/utils/isEmpty'
|
||||
import {
|
||||
EditRecordDropdownChangeEvent,
|
||||
EditRecordInputFocusedEvent
|
||||
@@ -16,7 +24,9 @@ import { EditRecordModal } from '../../models/EditRecordModal'
|
||||
@Component({
|
||||
selector: 'app-edit-record',
|
||||
templateUrl: './edit-record.component.html',
|
||||
styleUrls: ['./edit-record.component.scss']
|
||||
styleUrls: ['./edit-record.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: false
|
||||
})
|
||||
export class EditRecordComponent implements OnInit {
|
||||
@Input() currentRecord!: EditRecordModal
|
||||
@@ -59,6 +69,12 @@ export class EditRecordComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
/**
|
||||
* Runs native HOT validator against cell value
|
||||
* @param cellValidation column rules
|
||||
* @param cellValue value in the cell that is beign validated
|
||||
* @returns Promise boolean - wether valid or invalid
|
||||
*/
|
||||
async validateRecordCol(
|
||||
cellValidation: any,
|
||||
cellValue: any
|
||||
@@ -74,32 +90,114 @@ export class EditRecordComponent implements OnInit {
|
||||
})
|
||||
}
|
||||
|
||||
recordDateChange(date: Date, colKey: string) {
|
||||
let cellValidation = this.currentRecordValidator?.getRule(colKey)
|
||||
let format = cellValidation ? cellValidation.dateFormat : ''
|
||||
|
||||
if (this.currentRecord)
|
||||
this.currentRecord[colKey] = moment(date).format(format)
|
||||
/**
|
||||
* Fired when the native date picker (intl-date) changes. A native
|
||||
* `<input type="date">` value is always ISO `YYYY-MM-DD`, so it is stored
|
||||
* as-is. Mirrors recordTimeChange / recordDatetimeChange.
|
||||
* @param event native <input type="date"> change event
|
||||
* @param colKey column name (key)
|
||||
*/
|
||||
recordDateChange(event: Event, colKey: string) {
|
||||
const value = (event.target as HTMLInputElement).value // YYYY-MM-DD
|
||||
if (!value || !this.currentRecord) return
|
||||
this.currentRecord[colKey] = value
|
||||
this.revalidateRecordCol(colKey, value)
|
||||
}
|
||||
|
||||
isRecordModalInvalid(): boolean {
|
||||
return this.currentRecordInvalidCols.length > 0
|
||||
/**
|
||||
* Stored `YYYY-MM-DD HH:mm:ss` → native `<input type="datetime-local">` value
|
||||
* `YYYY-MM-DDTHH:mm:ss`, so the picker opens at the current value.
|
||||
*/
|
||||
toNativeDatetime(value: any): string {
|
||||
if (typeof value !== 'string') return ''
|
||||
return value.includes(' ') ? value.replace(' ', 'T') : value
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-runs validation for a column after a native picker change (the picker
|
||||
* handlers set the value programmatically, which does not fire the textarea's
|
||||
* input handler). Mirrors recordInputChange's validate + state update.
|
||||
*/
|
||||
private revalidateRecordCol(colName: string, value: any) {
|
||||
const colRules = this.currentRecordValidator?.getRule(colName)
|
||||
this.validateRecordCol(colRules, value).then((valid: boolean) =>
|
||||
this.updateValidationState(colName, valid)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired when the native time picker (intl-time) changes. Stores 24h
|
||||
* `HH:mm:ss`, padding the seconds the browser may omit. Mirrors the
|
||||
* IntlDatetimeEditor conversion used by the HOT grid.
|
||||
* @param event native <input type="time"> change event
|
||||
* @param colKey column name (key)
|
||||
*/
|
||||
recordTimeChange(event: Event, colKey: string) {
|
||||
const value = (event.target as HTMLInputElement).value // HH:mm[:ss]
|
||||
if (!value || !this.currentRecord) return
|
||||
const stored = value.length === 5 ? `${value}:00` : value
|
||||
this.currentRecord[colKey] = stored
|
||||
this.revalidateRecordCol(colKey, stored)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired when the native datetime picker (intl-datetime) changes. Converts the
|
||||
* native `YYYY-MM-DDTHH:mm[:ss]` into SAS' stored `YYYY-MM-DD HH:mm:ss`,
|
||||
* padding seconds. Mirrors the IntlDatetimeEditor conversion used by the grid.
|
||||
* @param event native <input type="datetime-local"> change event
|
||||
* @param colKey column name (key)
|
||||
*/
|
||||
recordDatetimeChange(event: Event, colKey: string) {
|
||||
const value = (event.target as HTMLInputElement).value // YYYY-MM-DDTHH:mm[:ss]
|
||||
if (!value || !this.currentRecord) return
|
||||
const [date, time] = value.split('T')
|
||||
const stored = `${date} ${time.length === 5 ? `${time}:00` : time}`
|
||||
this.currentRecord[colKey] = stored
|
||||
this.revalidateRecordCol(colKey, stored)
|
||||
}
|
||||
|
||||
/**
|
||||
* Close edit record modal and apply changes by emitting output event
|
||||
*/
|
||||
confirmRecordEdit() {
|
||||
if (this.currentRecordInvalidCols.length < 1) {
|
||||
this.onRecordChange.emit(this.currentRecord)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close edit record modal without applying the changes
|
||||
*/
|
||||
closeRecordEdit() {
|
||||
this.onRecordEditClose.emit()
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the modal on Escape (cancel, like the Close button). A native picker
|
||||
* swallows the first Escape to dismiss itself, so the modal only closes once
|
||||
* focus is back on the form.
|
||||
*/
|
||||
@HostListener('document:keydown.escape')
|
||||
onEscapeKey() {
|
||||
this.closeRecordEdit()
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitting output event when dropdown (autocomplete) input in any col change
|
||||
* @param colName column name (key)
|
||||
* @param col column index
|
||||
*/
|
||||
onRecordDropdownChange(colName: string, col: number) {
|
||||
this.onRecordDropdownChanged.emit({ colName, col })
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitting output event when input is focused (clicked on) so we can run a `dynamic cell validation`
|
||||
* Since that bit must be run from the parent component (editor.component)
|
||||
* Result is then applied in the `cellValidation` variable and automatically updated in this component.
|
||||
* @param event input event
|
||||
* @param colName column name (key)
|
||||
*/
|
||||
onRecordInputFocus(event: any, colName: number) {
|
||||
this.onRecordInputFocused.emit({ event, colName })
|
||||
}
|
||||
@@ -111,23 +209,63 @@ export class EditRecordComponent implements OnInit {
|
||||
}, 0)
|
||||
}
|
||||
|
||||
async recordInputChange(event: any, colName: string) {
|
||||
async recordInputChange(event: any, colName: string): Promise<void> {
|
||||
const colRules = this.currentRecordValidator?.getRule(colName)
|
||||
const value = event.target.value
|
||||
|
||||
this.helperService.debounceCall(300, () => {
|
||||
this.validateRecordCol(colRules, value).then((valid: boolean) => {
|
||||
const index = this.currentRecordInvalidCols.indexOf(colName)
|
||||
this.updateValidationState(colName, valid)
|
||||
|
||||
if (valid) {
|
||||
if (index > -1) this.currentRecordInvalidCols.splice(index, 1)
|
||||
} else {
|
||||
if (index < 0) this.currentRecordInvalidCols.push(colName)
|
||||
if (!valid) {
|
||||
this.tryAutoPopulateNotNull(event, colName, colRules, value)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the invalid columns list based on validation result
|
||||
*/
|
||||
private updateValidationState(colName: string, valid: boolean): void {
|
||||
const index = this.currentRecordInvalidCols.indexOf(colName)
|
||||
|
||||
if (valid && index > -1) {
|
||||
this.currentRecordInvalidCols.splice(index, 1)
|
||||
} else if (!valid && index < 0) {
|
||||
this.currentRecordInvalidCols.push(colName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-populates NOTNULL default value when the field is empty and has a default
|
||||
*/
|
||||
private tryAutoPopulateNotNull(
|
||||
event: any,
|
||||
colName: string,
|
||||
colRules: DcValidation | undefined,
|
||||
value: any
|
||||
): void {
|
||||
if (
|
||||
!isEmpty(value) ||
|
||||
!this.currentRecordValidator ||
|
||||
!this.currentRecord
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const defaultValue =
|
||||
this.currentRecordValidator.getNotNullDefaultValue(colName)
|
||||
if (defaultValue === undefined) return
|
||||
|
||||
this.currentRecord[colName] = defaultValue
|
||||
event.target.value = defaultValue
|
||||
|
||||
this.validateRecordCol(colRules, defaultValue).then((isValid: boolean) => {
|
||||
this.updateValidationState(colName, isValid)
|
||||
})
|
||||
}
|
||||
|
||||
onNextRecordClick() {
|
||||
this.onNextRecord.emit()
|
||||
}
|
||||
@@ -137,23 +275,8 @@ export class EditRecordComponent implements OnInit {
|
||||
}
|
||||
|
||||
public copyToClip(text: string) {
|
||||
const modalElement = document.querySelector('#recordModalRef .modal-title')
|
||||
|
||||
if (modalElement) {
|
||||
const selBox = document.createElement('textarea')
|
||||
selBox.style.position = 'fixed'
|
||||
selBox.style.left = '0'
|
||||
selBox.style.top = '0'
|
||||
selBox.style.opacity = '0'
|
||||
selBox.style.zIndex = '5000'
|
||||
selBox.value = text
|
||||
modalElement.appendChild(selBox)
|
||||
selBox.focus()
|
||||
selBox.select()
|
||||
document.execCommand('copy')
|
||||
modalElement.removeChild(selBox)
|
||||
this.generatedRecordUrl = text
|
||||
}
|
||||
navigator.clipboard.writeText(text)
|
||||
this.generatedRecordUrl = text
|
||||
}
|
||||
|
||||
async generateEditRecordUrl() {
|
||||
@@ -172,8 +295,9 @@ export class EditRecordComponent implements OnInit {
|
||||
if (obj.data === key) {
|
||||
if (
|
||||
obj.type === 'numeric' ||
|
||||
obj.type === 'date' ||
|
||||
obj.type === 'time'
|
||||
obj.type === 'intl-date' ||
|
||||
obj.type === 'intl-time' ||
|
||||
obj.type === 'intl-datetime'
|
||||
) {
|
||||
type = 'N'
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user