Compare commits
589 Commits
HEAD
...
171f0dfa89
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
171f0dfa89 | ||
|
|
6869505d9a | ||
|
|
a8ec97969f | ||
| c58887942b | |||
| 7fccf97eaf | |||
| e233e08857 | |||
| 85f1a7e569 | |||
| d9fd738a19 | |||
| 5cc2b6111b | |||
| 409db1e111 | |||
| 803d3b05a0 | |||
| 044934ea28 | |||
| 058d402643 | |||
| 179d6e1106 | |||
| 974f90eb2a | |||
| 0f6494acdc | |||
|
|
3c6e4a6faa | ||
|
|
e1b416060f | ||
|
|
a78f403d29 | ||
|
|
47a2fd588e | ||
| ca54f9d7a7 | |||
|
|
3279959bdb | ||
| dfa0842208 | |||
| b475b7fc99 | |||
| e4556a0d13 | |||
| 078929a254 | |||
| a8027a05bb | |||
| c0672f2487 | |||
| 14191e4f1e | |||
| a7788c87be | |||
| b95d81405d | |||
| f70a2cfde6 | |||
| f27c0728b7 | |||
| 2627c57b30 | |||
| af959469de | |||
|
|
b68d58fd0f | ||
| b502a3ba8f | |||
|
|
160367f91b | ||
| dec42a505e | |||
| 65b82949e6 | |||
| 95154907a4 | |||
| 59bddd4421 | |||
| 3f1accc32e | |||
|
|
dcbfb851ec | ||
|
|
4b6472f48e | ||
| 3eedbf7564 | |||
| 9c9b5aca95 | |||
| d31d3d0910 | |||
|
|
9d0cabcffa | ||
|
|
a4d59de2bc | ||
|
|
b1c55e3c31 | ||
|
|
d254cb7e54 | ||
|
|
a92873b971 | ||
| 5ebfd22652 | |||
| be203cb715 | |||
|
|
84ba118bb1 | ||
|
|
5d7ffa0138 | ||
|
|
972bbf9013 | ||
| 57af07ba73 | |||
| 8996a4c836 | |||
| fb0674ecff | |||
| 1da6a0c42b | |||
|
|
e9fa013d7d | ||
|
|
354abf252b | ||
| 8c481ecf4c | |||
| 76784b2683 | |||
| a5f5b3fccf | |||
|
|
ef9ed68468 | ||
| 1420773548 | |||
| 27ae3e186a | |||
| d3d3aa2d18 | |||
| c2a7ead1e3 | |||
|
|
d7280d66b2 | ||
|
|
e7f4078ca3 | ||
|
|
7cac64ced8 | ||
|
|
fef9e74ba1 | ||
|
|
736c722eb7 | ||
| 51e15287f5 | |||
| 0f639cdfa0 | |||
|
|
c0bda31353 | ||
|
|
78e7f1c840 | ||
|
|
35270e32f5 | ||
|
|
6b94af76c1 | ||
|
|
1135361b00 | ||
| 39775a5179 | |||
| 2823364148 | |||
|
|
40cadc82d0 | ||
| b928f2f3ef | |||
| 7b0b77dcb3 | |||
| 64aa2de133 | |||
| eea3761e52 | |||
|
|
74e8b78f6d | ||
| 3c4766cdad | |||
|
|
163e14022a | ||
|
|
8e0a9287e9 | ||
|
|
76643d280a | ||
|
|
f33fd0c64b | ||
| d6bd8fdee0 | |||
| c684105c2c | |||
| 9eec7e835d | |||
|
|
3ecb08b872 | ||
|
|
b95548e7f7 | ||
|
|
305868f091 | ||
|
|
7221f97139 | ||
| a3df6c4b37 | |||
| b874d0c776 | |||
| 21e0b9d6df | |||
| 9b1ae3bb5b | |||
| 8c4aa3119a | |||
| 62b6d7a878 | |||
| c5de6ace46 | |||
| 198e442292 | |||
|
|
cba365af9c | ||
| 91df7ed0fa | |||
| 3aec075927 | |||
| 29501f785f | |||
| 7e9654ab4c | |||
| 155cbd43e8 | |||
|
|
1786ea920a | ||
|
|
c8bb551ed1 | ||
|
|
e616f0c142 | ||
|
|
bfa85fab41 | ||
|
|
d3478ef851 | ||
|
|
b26877ef58 | ||
|
|
4336462ff1 | ||
| a6576827fd | |||
| b98c20d7ff | |||
|
|
93f44282d3 | ||
| adb0df3b17 | |||
| 4f17a6952d | |||
| c5c4f79e4f | |||
| 6504d8d29f | |||
| 9f61ab300c | |||
| 0c384bf57b | |||
|
|
a7860ed94a | ||
| 25ee2f8747 | |||
| 0d396024cb | |||
|
|
2a76f5a4c7 | ||
|
|
dcfe0d44fd | ||
|
|
660ffe36c2 | ||
|
|
8a3aaec8d5 | ||
| f13d1d51ca | |||
| bc52daa46a | |||
|
|
e3d842de15 | ||
| ff46485498 | |||
| d23e16d1eb | |||
| 8ee013527a | |||
| 39b02e0a8e | |||
| 647a5f4b8e | |||
| f3b93dc426 | |||
| c0feb74a13 | |||
| 77516b8814 | |||
|
|
bca2f46ea5 | ||
|
|
024645e16a | ||
|
|
df5b012bcd | ||
| 49d9475304 | |||
| 41aa11720d | |||
| 01b3e8b8b5 | |||
| 3726c8f342 | |||
| 2b4f4292a8 | |||
| 3b15dabedc | |||
| aa7daf4447 | |||
|
|
c5048a2ac3 | ||
| 0232e0d2c2 | |||
| 97d1232def | |||
| 3447c7832c | |||
| 65a7db7b3f | |||
| 1f176d40bc | |||
|
|
af77632da5 | ||
|
|
7a1ff8ac30 | ||
| cc726c7f68 | |||
| 0d825f6387 | |||
| 1db7d66648 | |||
| 440a0bd647 | |||
|
|
0fddeaa036 | ||
|
|
4cd3aff868 | ||
|
|
055ec1aeaf | ||
|
|
321f14b229 | ||
|
|
1626091e36 | ||
| fa1d3b01b0 | |||
|
|
bf4abe3cad | ||
|
|
3f98e17215 | ||
|
|
4563274789 | ||
|
|
3b9f08b43d | ||
|
|
4c8cc1def9 | ||
|
|
7284f982a3 | ||
|
|
ed27a18d25 | ||
|
|
40074fb162 | ||
|
|
96ec2a059e | ||
|
|
d833ebb086 | ||
|
|
f26ef1dd42 | ||
|
|
7ac722081e | ||
|
|
fa120d2ce9 | ||
|
|
1f0b05ee13 | ||
|
|
c2c6dee8c5 | ||
|
|
43ad0f5dd8 | ||
|
|
a3375c4526 | ||
|
|
1028327a37 | ||
|
|
f5b5607297 | ||
| dd814993f1 | |||
| 9dce750ee5 | |||
| 2ffa1d9438 | |||
|
|
9d853944cb | ||
|
|
5a670e7ef9 | ||
|
|
b8e76e0dc1 | ||
|
|
3a29aad32e | ||
|
|
b7dd258c0a | ||
|
|
2d7c9a5c3f | ||
|
|
9ddd3783f6 | ||
|
|
886969e941 | ||
|
|
ae67ec8751 | ||
|
|
138ede6191 | ||
|
|
f30ffaa137 | ||
|
|
30f44ca923 | ||
|
|
58bf916810 | ||
|
|
843db5f10c | ||
|
|
1e4cb27998 | ||
|
|
df408ff2a8 | ||
|
|
32ffee0c93 | ||
|
|
7ec2b6df28 | ||
|
|
662af6a226 | ||
|
|
4eb66684da | ||
|
|
20ca47c004 | ||
|
|
90bcfc30b9 | ||
|
|
db8d942eaf | ||
|
|
c2793698c5 | ||
|
|
1654387fe5 | ||
|
|
9dd2ffd549 | ||
|
|
04200193a8 | ||
|
|
91d52cb6e2 | ||
|
|
fc510c6aec | ||
|
|
d416b5e5cb | ||
|
|
cc8e5389d4 | ||
|
|
8859a6c57a | ||
|
|
9f7b6f0c83 | ||
|
|
b3c65fb4b5 | ||
|
|
c907fd8473 | ||
|
|
ad827c1dc8 | ||
|
|
c7eb8171ef | ||
|
|
a7783987a8 | ||
|
|
3925a0a721 | ||
|
|
814e0b1842 | ||
|
|
5fc03c6fe0 | ||
|
|
60359ed9bd | ||
|
|
e492659dbf | ||
|
|
c482b9ff5f | ||
|
|
fcbbecea75 | ||
|
|
329b28c459 | ||
|
|
55e69380aa | ||
|
|
af22f6e603 | ||
|
|
1e3b2d6392 | ||
|
|
d1634b6e81 | ||
|
|
ccb64a020b | ||
|
|
c3f6de6733 | ||
|
|
82ef3ebde0 | ||
|
|
1cbb519c92 | ||
|
|
b5fa5cdf57 | ||
|
|
acf8f0489d | ||
|
|
2f11323193 | ||
|
|
9eb87fe3f3 | ||
|
|
fe311a2be4 | ||
|
|
8b9f83ab25 | ||
|
|
22a8bf99ca | ||
|
|
f699e25090 | ||
|
|
cca7a1ba22 | ||
|
|
53e5a449d4 | ||
|
|
c723b500b3 | ||
|
|
4fcbf25233 | ||
|
|
b3bc646147 | ||
|
|
392b7caa7f | ||
|
|
d9b3f57191 | ||
|
|
5889874720 | ||
|
|
7e6143cac7 | ||
|
|
4a4a9d72e6 | ||
|
|
914b0d85df | ||
|
|
ad96c923b2 | ||
|
|
43994ca213 | ||
|
|
29cfed37f4 | ||
|
|
119091eaee | ||
|
|
d7e48483e9 | ||
|
|
a6d49c3f96 | ||
|
|
a2f57f06cf | ||
|
|
f626f5722b | ||
|
|
5cc4ece713 | ||
|
|
b6db5f274e | ||
|
|
936ef54072 | ||
|
|
dfaa77f2b9 | ||
|
|
26d0805dd9 | ||
|
|
850383d1d2 | ||
|
|
a4e9d73f3e | ||
|
|
35df63664e | ||
|
|
6cd4fb9327 | ||
|
|
f1b0b0a820 | ||
|
|
70aa9ef914 | ||
|
|
bf4ddba490 | ||
|
|
48f2c4a2d1 | ||
|
|
24fca834e0 | ||
|
|
a81f5e8c7f | ||
|
|
b6a68198f5 | ||
|
|
7bfba06317 | ||
|
|
1dd13487bd | ||
|
|
df2dbe7b8b | ||
|
|
48d2d9c473 | ||
|
|
748ccb8029 | ||
|
|
c50e84a01f | ||
|
|
1262f6040b | ||
|
|
838388fe08 | ||
|
|
770e31accd | ||
|
|
6023237db9 | ||
|
|
ce7973a635 | ||
|
|
d2b1e03a5f | ||
|
|
96722d9c2f | ||
|
|
9186cfb3d0 | ||
|
|
5079dffc25 | ||
|
|
f0b76057bb | ||
|
|
a96477be9a | ||
|
|
8b63bfc784 | ||
|
|
9cbd07db37 | ||
|
|
0a3182431f | ||
|
|
0bbd98ad78 | ||
|
|
95bc3575d2 | ||
|
|
f0f282afe0 | ||
|
|
e9eafdaf1a | ||
|
|
92864eb6ad | ||
|
|
cc4276b727 | ||
|
|
b9a7dd99da | ||
|
|
d4ade4c167 | ||
|
|
c6a577b5ec | ||
|
|
6fff3ec006 | ||
|
|
64e72a66d5 | ||
|
|
af5f27c8c5 | ||
|
|
5823c989c2 | ||
|
|
ee362a6a93 | ||
|
|
6ba1e0d958 | ||
|
|
fde66aa480 | ||
|
|
79a8598468 | ||
|
|
f70c27d814 | ||
|
|
432e52d322 | ||
|
|
f3893c2500 | ||
|
|
e726843189 | ||
|
|
183abeba41 | ||
|
|
70d2513cd7 | ||
|
|
0fda3d143c | ||
|
|
a85a3683f2 | ||
|
|
14287b6948 | ||
|
|
0443257f86 | ||
|
|
18b97c249a | ||
|
|
4ac4c9e9f4 | ||
|
|
24a70aed2e | ||
|
|
79e38ba6dd | ||
|
|
303a3ffc23 | ||
|
|
af86edd3e2 | ||
|
|
c87cff4181 | ||
|
|
05db5a0522 | ||
|
|
bac7020c15 | ||
|
|
c1d33e3ff0 | ||
| 832d46d360 | |||
|
|
c8f9cbda9a | ||
|
|
c1c265c73e | ||
| e4800d2937 | |||
| 550e59b4db | |||
|
|
26a685b872 | ||
|
|
8514adf15e | ||
|
|
5f2059fd6a | ||
| 504a76b4ac | |||
|
|
80694ee49c | ||
|
|
493b9ca0e9 | ||
|
|
55bbf50656 | ||
| 4b27504b99 | |||
|
|
1d248bde43 | ||
|
|
5cad10575f | ||
|
|
3b56c78bf1 | ||
|
|
6ab668f86a | ||
|
|
7b1daaab33 | ||
|
|
ef47912c37 | ||
|
|
140c48bcb7 | ||
| ba0b96146e | |||
| 53aa5dff88 | |||
| e52b02ec7f | |||
| 47dbaaf87d | |||
| 478c751ea7 | |||
| 4ee72a8a75 | |||
|
|
e2db112080 | ||
|
|
3c0d572a0e | ||
|
|
c225ddd39d | ||
| 0e9b0ea693 | |||
| 36f225a5b6 | |||
| 9496128e02 | |||
| 612962cc83 | |||
| 52514b7197 | |||
|
|
234e55bc01 | ||
|
|
f4a1bc3e58 | ||
|
|
192174b2ca | ||
|
|
544370688e | ||
|
|
6a6e772e32 | ||
|
|
f4500a09bc | ||
|
|
e7c3d3c8ad | ||
|
|
91bd72f7e0 | ||
|
|
2e69dc4dcf | ||
|
|
535049a188 | ||
|
|
5bde55e5fc | ||
|
|
c1cad3d7a9 | ||
|
|
c378bfe20c | ||
|
|
0e0d028c24 | ||
|
|
3b6eed32ec | ||
|
|
f62a7c3389 | ||
|
|
a161d8e2c8 | ||
|
|
b97888fca9 | ||
|
|
371ec3a133 | ||
|
|
711b9afaea | ||
|
|
00227ca713 | ||
|
|
842edd6f1f | ||
|
|
cd0c3197fa | ||
|
|
7f35a91765 | ||
| c44b42f498 | |||
| cff6ee5478 | |||
|
|
40b21604f9 | ||
|
|
98fcd2eb47 | ||
|
|
c3b560dbc9 | ||
| 53aadfcaaa | |||
|
|
dda9b7adad | ||
|
|
7fb3d08ccb | ||
|
|
cf4209333d | ||
|
|
61ac281134 | ||
|
|
22e9094d4c | ||
|
|
a13721f63e | ||
|
|
05b9a0ce1b | ||
|
|
b25c17ab53 | ||
|
|
64d2cadd82 | ||
|
|
371e2ee073 | ||
|
|
a7078b54c5 | ||
|
|
5663c313ea | ||
|
|
7da9b81319 | ||
| c426bbf793 | |||
| 329debaab8 | |||
|
|
994a0174f5 | ||
| 2846297112 | |||
| 5cf60ddfdc | |||
| 0de33f04bc | |||
| 472353632c | |||
|
|
1a48fb5b20 | ||
| 99d3a01991 | |||
|
|
da24972482 | ||
| ecb90e7120 | |||
|
|
784cb7a473 | ||
| 5349c46225 | |||
| 3be4402239 | |||
| 4c482ea289 | |||
| 44a73478a7 | |||
| bceac2f172 | |||
| 98842db343 | |||
| 03904a4e35 | |||
| 09b3c8df47 | |||
| 26761f6d39 | |||
| 72608a8ffd | |||
| d1ec123c8b | |||
| 4102ff5522 | |||
| 08e857884b | |||
| c0db2d230b | |||
| 2b9b772b39 | |||
| c46e8a7047 | |||
| 6e6350d6ce | |||
| 1e7d553bd6 | |||
| 35ae2539cb | |||
|
|
a614d754f4 | ||
| 46a6ba534e | |||
| 8fecde4c42 | |||
|
|
3e73799532 | ||
| 3159366560 | |||
| 5176cff2bd | |||
| 978aa723ae | |||
| 926c338f73 | |||
| 6de60c82ba | |||
| 6ed17a50e5 | |||
| 5cd6d02f6e | |||
| 276e5e9122 | |||
| 6e75a2a524 | |||
| ea6e603036 | |||
| edad30e264 | |||
| 58616100f4 | |||
| 359558bad3 | |||
| 6a6554ed1f | |||
| e20625abdb | |||
| f83dd693d5 | |||
| a464297511 | |||
| 991c8e8083 | |||
| 9ea183ff2d | |||
| 34d368b730 | |||
| 17760a6926 | |||
| 8b9cfa34c7 | |||
| 5fb8fe53bb | |||
| 5cb9375db3 | |||
| 55181f2c57 | |||
| 4577df371a | |||
| 0f154832a5 | |||
| 5951fcc108 | |||
| 7966217ac2 | |||
| 64851bd51d | |||
| 10e584d2ac | |||
| b97a728624 | |||
| 5a875e6853 | |||
| 57a4823f61 | |||
| 761eb4c13e | |||
| 354fc6868d | |||
| 2538e4526f | |||
| 26890f5b35 | |||
| 086b5e2621 | |||
| 617b060869 | |||
| a782e92bd6 | |||
| f60f97380f | |||
| cfc416fd14 | |||
| df593d2ffe | |||
| 9cd4c8a5ab | |||
|
|
ead6658455 | ||
| f6d677b51f | |||
| 25f05ab018 | |||
| 178cf33949 | |||
| b54c0f8022 | |||
| c5cc3c1a24 | |||
| 0b414fbfbe | |||
| 981d7a5062 | |||
| 33fc0b74ef | |||
| 286acc4584 | |||
| cdfb1d4310 | |||
| 6844652b5d | |||
| 9e72acf84b | |||
| 52f4af759e | |||
| 76457b6667 | |||
| e1f2afa942 | |||
| 8d5dff60f1 | |||
| 461cd26690 | |||
| 4c6b2ea844 | |||
| 418f45f997 | |||
| 51ebe99d1c | |||
| 2f1f8a60fc | |||
| 64795a80c7 | |||
| 94a17073dd | |||
| 7f7d625fdd | |||
| 6902079866 | |||
| 6aa057e590 | |||
| 2721cd60d1 | |||
| 1f8d079d4d | |||
|
|
f34dec1054 | ||
|
|
01b64f1aba | ||
|
|
3367ada343 | ||
|
|
f4ea3eaccb | ||
|
|
4adb8401d6 | ||
| df59bff6ae | |||
| 3ae685a0e2 | |||
|
|
fce56e43c3 | ||
|
|
42618602f4 | ||
| c1e50736e8 | |||
|
|
e02131846b | ||
|
|
2c3b27d9de | ||
|
|
6ccc833f7d | ||
|
|
a0ecc2eee3 | ||
| 59e6d33656 | |||
| b641c729c2 | |||
| 142cdcda38 | |||
| fc2669dabf | |||
| 8e095b51e3 | |||
| ff03490209 | |||
| 40cd9998d0 | |||
| 973ba159b4 | |||
| 063a5af822 | |||
|
|
6a06c71104 | ||
| cccff1d16d | |||
|
|
100d8de54f | ||
|
|
7c667660c0 | ||
|
|
4eb56372a5 | ||
|
|
16edd398be | ||
|
|
4e08159e6d | ||
| 7a9718a6fa | |||
| f597f7aa5a | |||
| 0ec2b22754 | |||
| a04f7b1bd5 | |||
| 2240603c2c | |||
| d9a7532805 | |||
| e59f9b802b | |||
| ad1c330231 | |||
| a1905a1274 | |||
| e515f99a44 | |||
| d42fac9a7d | |||
| da3df6f0a7 | |||
| 0bf3a1b2c8 | |||
| b63999c7f9 | |||
| 772ab3c6a5 | |||
| e3c66a5bc4 | |||
| 9e510b0183 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,3 +1,8 @@
|
|||||||
.vscode
|
.vscode
|
||||||
.mypy_cache
|
.mypy_cache
|
||||||
docker-stack.yml
|
docker-stack.yml
|
||||||
|
backend/pyvenv.cfg
|
||||||
|
backend/Include/
|
||||||
|
backend/Scripts/
|
||||||
|
|
||||||
|
log/api.log
|
||||||
|
|||||||
2
backend/.deployment
Normal file
2
backend/.deployment
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[config]
|
||||||
|
SCM_DO_BUILD_DURING_DEPLOYMENT=true
|
||||||
4
backend/.gitignore
vendored
4
backend/.gitignore
vendored
@@ -56,6 +56,7 @@ coverage.xml
|
|||||||
|
|
||||||
# Django stuff:
|
# Django stuff:
|
||||||
*.log
|
*.log
|
||||||
|
*.log.*
|
||||||
local_settings.py
|
local_settings.py
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
db.sqlite3-journal
|
db.sqlite3-journal
|
||||||
@@ -125,4 +126,5 @@ cython_debug/
|
|||||||
# VS Code settings
|
# VS Code settings
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
*.lock
|
*.lock
|
||||||
|
Temp/
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
FROM python:3.8
|
FROM python:3.11.4
|
||||||
|
|
||||||
RUN mkdir /app
|
RUN mkdir /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
103
backend/Temp/alc_runtime.js
Normal file
103
backend/Temp/alc_runtime.js
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1,46 +1,65 @@
|
|||||||
|
|
||||||
|
from fastapi import Request
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
from fastapi.security import OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from app.db.session import get_db
|
|
||||||
from app.core import security
|
|
||||||
from app.core.auth import authenticate_user, sign_up_new_user
|
from app.core.auth import authenticate_user, sign_up_new_user
|
||||||
|
from app.core import security,tenantCacheService
|
||||||
|
from app.core.dbmanager import get_db
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
auth_router = r = APIRouter()
|
auth_router = r = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@r.post("/token")
|
@r.post("/token")
|
||||||
async def login(
|
async def login(request: Request,db:Session= Depends(get_db) ,form_data: OAuth2PasswordRequestForm = Depends()):
|
||||||
db=Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()
|
if not db :
|
||||||
):
|
|
||||||
user = authenticate_user(db, form_data.username, form_data.password)
|
|
||||||
if not user:
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Incorrect username or password",
|
detail="Incorrect username or password",
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
user = authenticate_user(db, form_data.username, form_data.password)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="abcIncorrect username or password",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
access_token_expires = timedelta(
|
access_token_expires = timedelta(
|
||||||
minutes=security.ACCESS_TOKEN_EXPIRE_MINUTES
|
minutes=security.ACCESS_TOKEN_EXPIRE_MINUTES
|
||||||
)
|
)
|
||||||
if user.is_superuser:
|
if user.is_superuser:
|
||||||
permissions = "admin"
|
roles = "super"
|
||||||
|
permissions = "ALL"
|
||||||
else:
|
else:
|
||||||
permissions = "user"
|
roles = ";".join(role.name for role in user.roles)
|
||||||
|
perlst = [perm.privilege for role in user.roles for perm in role.permissions]
|
||||||
|
permissions =";".join(list(set(perlst)))
|
||||||
|
|
||||||
access_token = security.create_access_token(
|
access_token = security.create_access_token(
|
||||||
data={"sub": user.email, "permissions": permissions},
|
data={"sub": user.id,"roles":roles,"permissions": permissions,"tenant":user.tenantid,},
|
||||||
expires_delta=access_token_expires,
|
expires_delta=access_token_expires,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
request.state.user = user.id
|
||||||
|
|
||||||
return {"access_token": access_token, "token_type": "bearer"}
|
return JSONResponse(
|
||||||
|
status_code=200,
|
||||||
|
content={"access_token": access_token, "token_type": "bearer","user_name":user.first_name + " " + user.last_name}
|
||||||
|
)
|
||||||
|
|
||||||
@r.post("/signup")
|
@r.post("/signup")
|
||||||
async def signup(
|
async def signup(
|
||||||
|
firstname:str, lastname:str,
|
||||||
db=Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()
|
db=Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()
|
||||||
):
|
):
|
||||||
user = sign_up_new_user(db, form_data.username, form_data.password)
|
user = sign_up_new_user(db, form_data.username, form_data.password,firstname,lastname)
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_409_CONFLICT,
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
@@ -56,8 +75,8 @@ async def signup(
|
|||||||
else:
|
else:
|
||||||
permissions = "user"
|
permissions = "user"
|
||||||
access_token = security.create_access_token(
|
access_token = security.create_access_token(
|
||||||
data={"sub": user.email, "permissions": permissions},
|
data={"sub": user.id, "permissions": permissions},
|
||||||
expires_delta=access_token_expires,
|
expires_delta=access_token_expires,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"access_token": access_token, "token_type": "bearer"}
|
return {"access_token": access_token, "token_type": "bearer","user_name":user.first_name + " " + user.last_name}
|
||||||
|
|||||||
@@ -3,35 +3,154 @@ from io import BytesIO
|
|||||||
import typing as t
|
import typing as t
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import json
|
import json
|
||||||
|
import base64
|
||||||
import httpx
|
import httpx
|
||||||
import deepdiff
|
import deepdiff
|
||||||
import app.core.config as c
|
import app.core.config as config
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from app.core.dbmanager import get_db
|
||||||
|
from app.db.crud import get_flows_by_app,get_kintoneformat
|
||||||
|
from app.core.auth import get_current_active_user,get_current_user
|
||||||
|
from app.core.apiexception import APIException
|
||||||
|
from app.db.cruddb import domainService,appService
|
||||||
|
|
||||||
kinton_router = r = APIRouter()
|
kinton_router = r = APIRouter()
|
||||||
|
|
||||||
def getfieldsfromexcel(df):
|
def getkintoneenv(user = Depends(get_current_user),db = Depends(get_db)):
|
||||||
|
#db = SessionLocal()
|
||||||
|
domain = domainService.get_default_domain(db,user.id) #get_activedomain(db, user.id)
|
||||||
|
#db.close()
|
||||||
|
kintoneevn = config.KINTONE_ENV(domain)
|
||||||
|
return kintoneevn
|
||||||
|
|
||||||
|
|
||||||
|
def getkintoneformat(db,user = Depends(get_current_user)):
|
||||||
|
#db = SessionLocal()
|
||||||
|
formats = get_kintoneformat(db)
|
||||||
|
#db.close()
|
||||||
|
return formats
|
||||||
|
|
||||||
|
|
||||||
|
def createkintonefields(property,value,trueformat):
|
||||||
|
p = []
|
||||||
|
if(property=="options"):
|
||||||
|
o=[]
|
||||||
|
for v in value.split(','):
|
||||||
|
o.append(f"\"{v.split('|')[0]}\":{{\"label\":\"{v.split('|')[0]}\",\"index\":\"{v.split('|')[1]}\"}}")
|
||||||
|
p.append(f"\"options\":{{{','.join(o)}}}")
|
||||||
|
elif(property =="expression"):
|
||||||
|
p.append(f"\"hideExpression\":true")
|
||||||
|
p.append(f"\"expression\":\"{value.split(':')[1]}\"")
|
||||||
|
elif(property =="required" or property =="unique" or property =="defaultNowValue" or property =="hideExpression" or property =="digit"):
|
||||||
|
if str(value) == trueformat:
|
||||||
|
p.append(f"\"{property}\":true")
|
||||||
|
else:
|
||||||
|
p.append(f"\"{property}\":false")
|
||||||
|
elif(property =="protocol"):
|
||||||
|
if(value == "メールアドレス"):
|
||||||
|
p.append("\"protocol\":\"MAIL\"")
|
||||||
|
elif(value == "Webサイト"):
|
||||||
|
p.append("\"protocol\":\"WEB\"")
|
||||||
|
elif(value == "電話番号"):
|
||||||
|
p.append("\"protocol\":\"CALL\"")
|
||||||
|
else:
|
||||||
|
p.append(f"\"{property}\":\"{value}\"")
|
||||||
|
return p
|
||||||
|
|
||||||
|
def getfieldsfromexcel(df,mapping):
|
||||||
|
startrow = mapping.startrow
|
||||||
|
startcolumn = mapping.startcolumn
|
||||||
|
typecolumn = mapping.typecolumn
|
||||||
|
codecolumn = mapping.codecolumn
|
||||||
|
property = mapping.field.split(",")
|
||||||
|
trueformat = mapping.trueformat
|
||||||
appname = df.iloc[0,2]
|
appname = df.iloc[0,2]
|
||||||
col=[]
|
col=[]
|
||||||
for row in range(5,len(df)):
|
for row in range(startrow,len(df)):
|
||||||
if pd.isna(df.iloc[row,1]):
|
if pd.isna(df.iloc[row,startcolumn]):
|
||||||
break
|
break
|
||||||
if not df.iloc[row,3] in c.KINTONE_FIELD_TYPE:
|
if not df.iloc[row,typecolumn] in config.KINTONE_FIELD_TYPE:
|
||||||
continue
|
continue
|
||||||
p=[]
|
p=[]
|
||||||
for column in range(1,7):
|
for column in range(startcolumn,startcolumn + len(property)):
|
||||||
if(not pd.isna(df.iloc[row,column])):
|
if(not pd.isna(df.iloc[row,column])):
|
||||||
if(property[column-1]=="options"):
|
propertyname =property[column-1]
|
||||||
o=[]
|
if(propertyname.find("[") == 0):
|
||||||
for v in df.iloc[row,column].split(','):
|
continue
|
||||||
o.append(f"\"{v.split('|')[0]}\":{{\"label\":\"{v.split('|')[0]}\",\"index\":\"{v.split('|')[1]}\"}}")
|
elif (propertyname =="remark"):
|
||||||
p.append(f"\"{property[column-1]}\":{{{','.join(o)}}}")
|
if (df.iloc[row,column].find("|") !=-1):
|
||||||
elif(property[column-1]=="required"):
|
propertyname = "options"
|
||||||
p.append(f"\"{property[column-1]}\":{df.iloc[row,column]}")
|
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
|
||||||
|
if (df.iloc[row,column] == "メールアドレス" or df.iloc[row,column] == "Webサイト" or df.iloc[row,column] == "電話番号"):
|
||||||
|
propertyname = "protocol"
|
||||||
|
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
|
||||||
|
if (df.iloc[row,column].find("桁区切り") !=-1):
|
||||||
|
propertyname = "digit"
|
||||||
|
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
|
||||||
|
if (df.iloc[row,column].find("前単位") !=-1):
|
||||||
|
propertyname = "unitPosition"
|
||||||
|
p = p + createkintonefields(propertyname, "BEFORE",trueformat)
|
||||||
|
if (df.iloc[row,column].find("後単位") !=-1):
|
||||||
|
propertyname = "unitPosition"
|
||||||
|
p = p + createkintonefields(propertyname, "AFTER",trueformat)
|
||||||
|
if (df.iloc[row,column].find("単位「") !=-1):
|
||||||
|
propertyname = "unit"
|
||||||
|
ids = df.iloc[row,column].index("単位「")
|
||||||
|
ide = df.iloc[row,column].index("」")
|
||||||
|
unit = df.iloc[row,column][ids+3:ide]
|
||||||
|
p = p + createkintonefields(propertyname, unit,trueformat)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
elif(propertyname =="mixValue"):
|
||||||
|
if(df.iloc[row,column].find("レコード登録時の日") != -1):
|
||||||
|
propertyname = "defaultNowValue"
|
||||||
|
df.iloc[row,column] = trueformat
|
||||||
|
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
|
||||||
|
elif(df.iloc[row,column].find("計:") != -1):
|
||||||
|
propertyname = "expression"
|
||||||
|
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
|
||||||
|
elif(df.iloc[row,column] !=""):
|
||||||
|
propertyname = "defaultValue"
|
||||||
|
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
elif(propertyname=="max" or propertyname == "min"):
|
||||||
|
if(df.iloc[row,typecolumn] == "NUMBER"):
|
||||||
|
propertyname = property[column-1] + "Value"
|
||||||
|
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
|
||||||
|
else:
|
||||||
|
propertyname = property[column-1] + "Length"
|
||||||
|
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
|
||||||
else:
|
else:
|
||||||
p.append(f"\"{property[column-1]}\":\"{df.iloc[row,column]}\"")
|
p = p + createkintonefields(propertyname, df.iloc[row,column],trueformat)
|
||||||
col.append(f"\"{df.iloc[row,2]}\":{{{','.join(p)}}}")
|
|
||||||
fields = ",".join(col).replace("False","false").replace("True","true")
|
# if(propertyname=="options"):
|
||||||
|
# o=[]
|
||||||
|
# for v in df.iloc[row,column].split(','):
|
||||||
|
# o.append(f"\"{v.split('|')[0]}\":{{\"label\":\"{v.split('|')[0]}\",\"index\":\"{v.split('|')[1]}\"}}")
|
||||||
|
# p.append(f"\"options\":{{{','.join(o)}}}")
|
||||||
|
# elif(propertyname=="expression"):
|
||||||
|
# p.append(f"\"hideExpression\":true")
|
||||||
|
# p.append(f"\"expression\":{df.iloc[row,column].split(':')[1]}")
|
||||||
|
# elif(propertyname=="required" or propertyname =="unique" or propertyname=="defaultNowValue" or propertyname=="hideExpression" or propertyname=="digit"):
|
||||||
|
# if (df.iloc[row,column] == trueformat):
|
||||||
|
# p.append(f"\"{propertyname}\":true")
|
||||||
|
# else:
|
||||||
|
# p.append(f"\"{propertyname}\":false")
|
||||||
|
# elif(propertyname =="protocol"):
|
||||||
|
# if(df.iloc[row,column] == "メールアドレス"):
|
||||||
|
# p.append("\"protocol\":\"MAIL\"")
|
||||||
|
# elif(df.iloc[row,column] == "Webサイト"):
|
||||||
|
# p.append("\"protocol\":\"WEB\"")
|
||||||
|
# elif(df.iloc[row,column] == "電話番号"):
|
||||||
|
# p.append("\"protocol\":\"CALL\"")
|
||||||
|
# else:
|
||||||
|
# p.append(f"\"{propertyname}\":\"{df.iloc[row,column]}\"")
|
||||||
|
|
||||||
|
|
||||||
|
col.append(f"\"{df.iloc[row,codecolumn]}\":{{{','.join(p)}}}")
|
||||||
|
fields = ",".join(col).replace("\\", "\\\\")
|
||||||
return json.loads(f"{{{fields}}}")
|
return json.loads(f"{{{fields}}}")
|
||||||
|
|
||||||
def getsettingfromexcel(df):
|
def getsettingfromexcel(df):
|
||||||
@@ -39,10 +158,10 @@ def getsettingfromexcel(df):
|
|||||||
des = df.iloc[2,2]
|
des = df.iloc[2,2]
|
||||||
return {"name":appname,"description":des}
|
return {"name":appname,"description":des}
|
||||||
|
|
||||||
def getsettingfromkintone(app:str):
|
def getsettingfromkintone(app:str,env:config.KINTONE_ENV):
|
||||||
headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
|
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
|
||||||
params = {"app":app}
|
params = {"app":app}
|
||||||
url = f"{c.BASE_URL}{c.API_V1_STR}/app/settings.json"
|
url = f"{env.BASE_URL}{config.API_V1_STR}/app/settings.json"
|
||||||
r = httpx.get(url,headers=headers,params=params)
|
r = httpx.get(url,headers=headers,params=params)
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
@@ -54,60 +173,101 @@ def analysesettings(excel,kintone):
|
|||||||
updatesettings[key] = excel[key]
|
updatesettings[key] = excel[key]
|
||||||
return updatesettings
|
return updatesettings
|
||||||
|
|
||||||
def createkintoneapp(name:str):
|
def createkintoneapp(name:str,env:config.KINTONE_ENV):
|
||||||
headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
|
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
|
||||||
data = {"name":name}
|
data = {"name":name}
|
||||||
url = f"{c.BASE_URL}{c.API_V1_STR}/preview/app.json"
|
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app.json"
|
||||||
r = httpx.post(url,headers=headers,data=json.dumps(data))
|
r = httpx.post(url,headers=headers,data=json.dumps(data))
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
def updateappsettingstokintone(app:str,updates:dict):
|
def updateappsettingstokintone(app:str,updates:dict,env:config.KINTONE_ENV):
|
||||||
headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
|
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
|
||||||
url = f"{c.BASE_URL}{c.API_V1_STR}/preview/app/settings.json"
|
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/settings.json"
|
||||||
data = {"app":app}
|
data = {"app":app}
|
||||||
data.update(updates)
|
data.update(updates)
|
||||||
r = httpx.put(url,headers=headers,data=json.dumps(data))
|
r = httpx.put(url,headers=headers,data=json.dumps(data))
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
def addfieldstokintone(app:str,fields:dict,revision:str = None):
|
def addfieldstokintone(app:str,fields:dict,env:config.KINTONE_ENV,revision:str = None):
|
||||||
headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
|
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
|
||||||
url = f"{c.BASE_URL}{c.API_V1_STR}/preview/app/form/fields.json"
|
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/form/fields.json"
|
||||||
if revision != None:
|
if revision != None:
|
||||||
data = {"app":app,"revision":revision,"properties":fields}
|
data = {"app":app,"revision":revision,"properties":fields}
|
||||||
else:
|
else:
|
||||||
data = {"app":app,"properties":fields}
|
data = {"app":app,"properties":fields}
|
||||||
r = httpx.post(url,headers=headers,data=json.dumps(data))
|
r = httpx.post(url,headers=headers,data=json.dumps(data))
|
||||||
|
r.raise_for_status()
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
def updatefieldstokintone(app:str,revision:str,fields:dict):
|
def updatefieldstokintone(app:str,revision:str,fields:dict,env:config.KINTONE_ENV):
|
||||||
headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
|
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
|
||||||
url = f"{c.BASE_URL}{c.API_V1_STR}/preview/app/form/fields.json"
|
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/form/fields.json"
|
||||||
data = {"app":app,"properties":fields}
|
data = {"app":app,"properties":fields}
|
||||||
r = httpx.put(url,headers=headers,data=json.dumps(data))
|
r = httpx.put(url,headers=headers,data=json.dumps(data))
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
def deletefieldsfromkintone(app:str,revision:str,fields:dict):
|
def deletefieldsfromkintone(app:str,revision:str,fields:dict,env:config.KINTONE_ENV):
|
||||||
headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
|
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
|
||||||
url = f"{c.BASE_URL}{c.API_V1_STR}/preview/app/form/fields.json"
|
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/form/fields.json"
|
||||||
params = {"app":app,"revision":revision,"fields":fields}
|
params = {"app":app,"revision":revision,"fields":fields}
|
||||||
#r = httpx.delete(url,headers=headers,content=json.dumps(params))
|
#r = httpx.delete(url,headers=headers,content=json.dumps(params))
|
||||||
r = httpx.request(method="DELETE",url=url,headers=headers,content=json.dumps(params))
|
r = httpx.request(method="DELETE",url=url,headers=headers,content=json.dumps(params))
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
def deoployappfromkintone(app:str,revision:str):
|
def deoployappfromkintone(app:str,revision:str,env:config.KINTONE_ENV):
|
||||||
headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
|
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
|
||||||
url = f"{c.BASE_URL}{c.API_V1_STR}/preview/app/deploy.json"
|
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/deploy.json"
|
||||||
data = {"apps":[{"app":app,"revision":revision}],"revert": False}
|
data = {"apps":[{"app":app,"revision":revision}],"revert": False}
|
||||||
r = httpx.post(url,headers=headers,data=json.dumps(data))
|
r = httpx.post(url,headers=headers,data=json.dumps(data))
|
||||||
return r.json
|
return r.json
|
||||||
|
|
||||||
def getfieldsfromkintone(app):
|
# 既定項目に含めるアプリのフィールドのみ取得する
|
||||||
headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
|
# スペース、枠線、ラベルを含まない
|
||||||
|
def getfieldsfromkintone(app:str,env:config.KINTONE_ENV):
|
||||||
|
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
|
||||||
params = {"app":app}
|
params = {"app":app}
|
||||||
url = f"{c.BASE_URL}{c.API_V1_STR}/app/form/fields.json"
|
url = f"{env.BASE_URL}{config.API_V1_STR}/app/form/fields.json"
|
||||||
r = httpx.get(url,headers=headers,params=params)
|
r = httpx.get(url,headers=headers,params=params)
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
# フォームに配置するフィールドのみ取得する
|
||||||
|
# スペース、枠線、ラベルも含める
|
||||||
|
def getformfromkintone(app:str,env:config.KINTONE_ENV):
|
||||||
|
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
|
||||||
|
params = {"app":app}
|
||||||
|
url = f"{env.BASE_URL}{config.API_V1_STR}/form.json"
|
||||||
|
r = httpx.get(url,headers=headers,params=params)
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
|
def merge_kintone_fields(fields_response: dict, form_response: dict) -> dict:
|
||||||
|
fields_properties = fields_response.get('properties', {})
|
||||||
|
form_properties = form_response.get('properties', [])
|
||||||
|
|
||||||
|
merged_properties = {k: v for k, v in fields_properties.items()}
|
||||||
|
|
||||||
|
for index, form_field in enumerate(form_properties):
|
||||||
|
code = form_field.get('code')
|
||||||
|
if code:
|
||||||
|
if code and code not in merged_properties:
|
||||||
|
merged_properties[code] = form_field
|
||||||
|
else:
|
||||||
|
element_id = form_field.get('elementId')
|
||||||
|
if element_id:
|
||||||
|
key = element_id
|
||||||
|
form_field['code']=element_id
|
||||||
|
form_field['label']=form_field.get('type')
|
||||||
|
# else:
|
||||||
|
# key = f"{form_field.get('type')}_{index}"
|
||||||
|
merged_properties[key] = form_field
|
||||||
|
|
||||||
|
merged_response = {
|
||||||
|
'revision': fields_response.get('revision', ''),
|
||||||
|
'properties': merged_properties
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged_response
|
||||||
|
|
||||||
def analysefields(excel,kintone):
|
def analysefields(excel,kintone):
|
||||||
updatefields={}
|
updatefields={}
|
||||||
addfields={}
|
addfields={}
|
||||||
@@ -116,22 +276,22 @@ def analysefields(excel,kintone):
|
|||||||
adds = excel.keys() - kintone.keys()
|
adds = excel.keys() - kintone.keys()
|
||||||
dels = kintone.keys() - excel.keys()
|
dels = kintone.keys() - excel.keys()
|
||||||
for key in updates:
|
for key in updates:
|
||||||
for p in property:
|
for p in config.KINTONE_FIELD_PROPERTY:
|
||||||
if excel[key].get(p) != None and kintone[key][p] != excel[key][p]:
|
if excel[key].get(p) != None and kintone[key].get(p) != None and kintone[key][p] != excel[key][p]:
|
||||||
updatefields[key] = excel[key]
|
updatefields[key] = excel[key]
|
||||||
break
|
break
|
||||||
for key in adds:
|
for key in adds:
|
||||||
addfields[key] = excel[key]
|
addfields[key] = excel[key]
|
||||||
for key in dels:
|
for key in dels:
|
||||||
if kintone[key]["type"] in c.KINTONE_FIELD_TYPE:
|
if kintone[key]["type"] in config.KINTONE_FIELD_TYPE:
|
||||||
delfields.append(key)
|
delfields.append(key)
|
||||||
|
|
||||||
return {"update":updatefields,"add":addfields,"del":delfields}
|
return {"update":updatefields,"add":addfields,"del":delfields}
|
||||||
|
|
||||||
def getprocessfromkintone(app:str):
|
def getprocessfromkintone(app:str,env:config.KINTONE_ENV):
|
||||||
headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
|
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
|
||||||
params = {"app":app}
|
params = {"app":app}
|
||||||
url = f"{c.BASE_URL}{c.API_V1_STR}/app/status.json"
|
url = f"{env.BASE_URL}{config.API_V1_STR}/app/status.json"
|
||||||
r = httpx.get(url,headers=headers,params=params)
|
r = httpx.get(url,headers=headers,params=params)
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
@@ -195,27 +355,121 @@ def analysprocess(excel,kintone):
|
|||||||
# return True
|
# return True
|
||||||
return diff
|
return diff
|
||||||
|
|
||||||
def updateprocesstokintone(app:str,process:dict):
|
def updateprocesstokintone(app:str,process:dict,c:config.KINTONE_ENV):
|
||||||
headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
|
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
|
||||||
url = f"{c.BASE_URL}{c.API_V1_STR}/preview/app/status.json"
|
url = f"{c.BASE_URL}{config.API_V1_STR}/preview/app/status.json"
|
||||||
data = {"app":app,"enable":True}
|
data = {"app":app,"enable":True}
|
||||||
data.update(process)
|
data.update(process)
|
||||||
r = httpx.put(url,headers=headers,data=json.dumps(data))
|
r = httpx.put(url,headers=headers,data=json.dumps(data))
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
def getkintoneusers():
|
def getkintoneusers(c:config.KINTONE_ENV):
|
||||||
headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
|
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
|
||||||
url = f"{c.BASE_URL}/v1/users.json"
|
url = f"{c.BASE_URL}/v1/users.json"
|
||||||
r = httpx.get(url,headers=headers)
|
r = httpx.get(url,headers=headers)
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
def getkintoneorgs():
|
def getkintoneorgs(c:config.KINTONE_ENV):
|
||||||
headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
|
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
|
||||||
params = {"code":c.KINTONE_USER}
|
params = {"code":c.KINTONE_USER}
|
||||||
url = f"{c.BASE_URL}/v1/user/organizations.json"
|
url = f"{c.BASE_URL}/v1/user/organizations.json"
|
||||||
r = httpx.get(url,headers=headers,params=params)
|
r = httpx.get(url,headers=headers,params=params)
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
def uploadkintonefiles(file,env:config.KINTONE_ENV):
|
||||||
|
if (file.endswith('alc_runtime.js') and config.DEPLOY_MODE == "DEV"):
|
||||||
|
return {'fileKey':file}
|
||||||
|
upload_files = {'file': open(file,'rb')}
|
||||||
|
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
|
||||||
|
data ={'name':'file','filename':os.path.basename(file)}
|
||||||
|
url = f"{env.BASE_URL}/k/v1/file.json"
|
||||||
|
r = httpx.post(url,headers=headers,data=data,files=upload_files)
|
||||||
|
#{"name":data['filename'],'fileKey':r['fileKey']}
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
def updateappjscss(app,uploads,env:config.KINTONE_ENV):
|
||||||
|
dsjs = []
|
||||||
|
dscss = []
|
||||||
|
#mobile側
|
||||||
|
mbjs = []
|
||||||
|
mbcss = []
|
||||||
|
customize = getappcustomize(app, env)
|
||||||
|
current_js = customize['desktop'].get('js', [])
|
||||||
|
current_css = customize['desktop'].get('css', [])
|
||||||
|
current_mobile_js = customize['mobile'].get('js', [])
|
||||||
|
current_mobile_css = customize['mobile'].get('css', [])
|
||||||
|
current_js = [item for item in current_js if not (item.get('type') == 'URL' and item.get('url', '').endswith('alc_runtime.js'))]
|
||||||
|
for upload in uploads:
|
||||||
|
for key in upload:
|
||||||
|
filename = os.path.basename(key)
|
||||||
|
if key.endswith('.js'):
|
||||||
|
existing_js = next((item for item in current_js
|
||||||
|
if item.get('type') == 'FILE' and item['file']['name'] == filename
|
||||||
|
), None)
|
||||||
|
if existing_js:
|
||||||
|
current_js = [item for item in current_js if item.get('type') == 'URL' or item['file'].get('name') != filename]
|
||||||
|
dsjs.append({'type':'FILE','file':{'fileKey':upload[key]}})
|
||||||
|
else:
|
||||||
|
if (key.endswith('alc_runtime.js') and config.DEPLOY_MODE == "DEV"):
|
||||||
|
dsjs.append({'type':'URL','url':config.DEPLOY_JS_URL})
|
||||||
|
else:
|
||||||
|
dsjs.append({'type':'FILE','file':{'fileKey':upload[key]}})
|
||||||
|
elif key.endswith('.css'):
|
||||||
|
existing_css = next((item for item in current_css
|
||||||
|
if item.get('type') == 'FILE' and item['file']['name'] == filename
|
||||||
|
), None)
|
||||||
|
if existing_css:
|
||||||
|
current_css = [item for item in current_css if item.get('type') == 'URL' or item['file'].get('name') != filename]
|
||||||
|
dscss.append({'type': 'FILE', 'file': {'fileKey': upload[key]}})
|
||||||
|
else:
|
||||||
|
dscss.append({'type': 'FILE', 'file': {'fileKey': upload[key]}})
|
||||||
|
#現在のJSとCSSがdsjsに追加する
|
||||||
|
dsjs.extend(current_js)
|
||||||
|
dscss.extend(current_css)
|
||||||
|
mbjs.extend(current_mobile_js)
|
||||||
|
mbcss.extend(current_mobile_css)
|
||||||
|
|
||||||
|
ds ={'js':dsjs,'css':dscss}
|
||||||
|
mb ={'js':mbjs,'css':mbcss}
|
||||||
|
data = {'app':app,'scope':'ALL','desktop':ds,'mobile':mb,'revision':customize["revision"]}
|
||||||
|
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
|
||||||
|
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/customize.json"
|
||||||
|
print(json.dumps(data))
|
||||||
|
r = httpx.put(url,headers=headers,data=json.dumps(data))
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
#kintone カスタマイズ情報
|
||||||
|
def getappcustomize(app,env:config.KINTONE_ENV):
|
||||||
|
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
|
||||||
|
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/customize.json"
|
||||||
|
params = {"app":app}
|
||||||
|
r = httpx.get(url,headers=headers,params=params)
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
|
def getTempPath(filename):
|
||||||
|
scriptdir = Path(__file__).resolve().parent
|
||||||
|
rootdir = scriptdir.parent.parent.parent.parent
|
||||||
|
fpath = os.path.join(rootdir,"Temp",filename)
|
||||||
|
return fpath
|
||||||
|
|
||||||
|
def createappjs(domain_url,app,db):
|
||||||
|
#db = SessionLocal()
|
||||||
|
flows = appService.get_flow(db,domain_url,app) #get_flows_by_app(db,domain_url,app)
|
||||||
|
#db.close()
|
||||||
|
content={}
|
||||||
|
for flow in flows:
|
||||||
|
content[flow.eventid] = {'flowid':flow.flowid,'name':flow.name,'content':flow.content}
|
||||||
|
js = 'const alcflow=' + json.dumps(content)
|
||||||
|
# scriptdir = Path(__file__).resolve().parent
|
||||||
|
# rootdir = scriptdir.parent.parent.parent.parent
|
||||||
|
# fpath = os.path.join(rootdir,"Temp",f"alc_setting_{app}.js")
|
||||||
|
fpath = getTempPath(f"alc_setting_{app}.js")
|
||||||
|
print(fpath)
|
||||||
|
with open(fpath,'w') as file:
|
||||||
|
file.write(js)
|
||||||
|
return fpath
|
||||||
|
|
||||||
@r.post("/test",)
|
@r.post("/test",)
|
||||||
async def test(file:UploadFile= File(...),app:str=None):
|
async def test(file:UploadFile= File(...),app:str=None):
|
||||||
if file.filename.endswith('.xlsx'):
|
if file.filename.endswith('.xlsx'):
|
||||||
@@ -233,15 +487,37 @@ async def test(file:UploadFile= File(...),app:str=None):
|
|||||||
# kintone = getfieldsfromkintone(app)
|
# kintone = getfieldsfromkintone(app)
|
||||||
# fields = analysefields(excel,kintone["properties"])
|
# fields = analysefields(excel,kintone["properties"])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=400, detail=f"Error occurred while parsing file {file.filename}: {str(e)}")
|
raise HTTPException(status_code=400, detail=f"Error occurred while parsing file {file.filename}")
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=400, detail=f"File {file.filename} is not an Excel file")
|
raise HTTPException(status_code=400, detail=f"File {file.filename} is not an Excel file")
|
||||||
|
|
||||||
return test
|
return test
|
||||||
|
|
||||||
|
|
||||||
@r.post("/upload",)
|
@r.get("/group")
|
||||||
async def upload(files:t.List[UploadFile] = File(...)):
|
async def group(request:Request,kintoneurl:str,kintoneuser:str,kintonepwd:str):
|
||||||
|
try:
|
||||||
|
auth_value = base64.b64encode(bytes(f"{kintoneuser}:{kintonepwd}","utf-8"))
|
||||||
|
headers={config.API_V1_AUTH_KEY:auth_value}
|
||||||
|
url = f"{kintoneurl}/v1/user/groups.json?code={kintoneuser}"
|
||||||
|
r = httpx.get(url,headers=headers)
|
||||||
|
return r.json()
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('kintone:group',request.url._url, f"Error occurred while get group(url:{kintoneurl} user:{kintoneuser}):",e)
|
||||||
|
|
||||||
|
@r.post("/download",)
|
||||||
|
async def download(request:Request,key,c:config.KINTONE_ENV=Depends(getkintoneenv)):
|
||||||
|
try:
|
||||||
|
headers={config.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
|
||||||
|
params = {"fileKey":key}
|
||||||
|
url = f"{c.BASE_URL}/k/v1/file.json"
|
||||||
|
r = httpx.get(url,headers=headers,params=params)
|
||||||
|
return r.json()
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('kintone:download',request.url._url,f"Error occurred while download file.json:",e)
|
||||||
|
|
||||||
|
@r.post("/upload")
|
||||||
|
async def upload(request:Request,files:t.List[UploadFile] = File(...)):
|
||||||
dataframes = []
|
dataframes = []
|
||||||
for file in files:
|
for file in files:
|
||||||
if file.filename.endswith('.xlsx'):
|
if file.filename.endswith('.xlsx'):
|
||||||
@@ -251,61 +527,134 @@ async def upload(files:t.List[UploadFile] = File(...)):
|
|||||||
print(df)
|
print(df)
|
||||||
dataframes.append(df)
|
dataframes.append(df)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=400, detail=f"Error occurred while parsing file {file.filename}: {str(e)}")
|
raise APIException('kintone:upload',request.url._url,f"Error occurred while uploading file {file.filename}:",e)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=400, detail=f"File {file.filename} is not an Excel file")
|
raise APIException('kintone:upload',request.url._url, f"File {file.filename} is not an Excel file",e)
|
||||||
|
|
||||||
return {"files": [file.filename for file in files]}
|
return {"files": [file.filename for file in files]}
|
||||||
|
|
||||||
@r.get("/allapps",)
|
@r.post("/updatejscss")
|
||||||
async def allapps():
|
async def jscss(request:Request,app:str,files:t.List[UploadFile] = File(...),env:config.KINTONE_ENV = Depends(getkintoneenv)):
|
||||||
headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
|
try:
|
||||||
url = f"{c.BASE_URL}{c.API_V1_STR}/apps.json"
|
jscs=[]
|
||||||
r = httpx.get(url,headers=headers)
|
for file in files:
|
||||||
return r.json()
|
fbytes = file.file.read()
|
||||||
|
fname = file.filename
|
||||||
|
fpath = '{}\\{}'.format('Temp',fname)
|
||||||
|
fout = open(fpath,'wb')
|
||||||
|
fout.write(fbytes)
|
||||||
|
fout.close()
|
||||||
|
upload = uploadkintonefiles(fpath,env)
|
||||||
|
if upload.get('fileKey') != None:
|
||||||
|
jscs.append({ file.filename:upload['fileKey']})
|
||||||
|
appjscs = updateappjscss(app,jscs,env)
|
||||||
|
if appjscs.get("revision") != None:
|
||||||
|
deoployappfromkintone(app,appjscs["revision"],env)
|
||||||
|
return appjscs
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('kintone:updatejscss',request.url._url, f"Error occurred while update js/css {file.filename} is not an Excel file",e)
|
||||||
|
|
||||||
@r.get("/app")
|
@r.get("/app")
|
||||||
async def app(app:str):
|
async def app(request:Request,app:str,env:config.KINTONE_ENV=Depends(getkintoneenv)):
|
||||||
headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
|
try:
|
||||||
url = f"{c.BASE_URL}{c.API_V1_STR}/app.json"
|
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
|
||||||
params ={"id":app}
|
url = f"{env.BASE_URL}{config.API_V1_STR}/app.json"
|
||||||
r = httpx.get(url,headers=headers,params=params)
|
params ={"id":app}
|
||||||
return r.json()
|
r = httpx.get(url,headers=headers,params=params)
|
||||||
|
return r.json()
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('kintone:app',request.url._url, f"Error occurred while get app({env.DOMAIN_NAME}->{app}):",e)
|
||||||
|
|
||||||
|
@r.get("/allapps")
|
||||||
|
async def allapps(request:Request,env:config.KINTONE_ENV=Depends(getkintoneenv)):
|
||||||
|
try:
|
||||||
|
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
|
||||||
|
url = f"{env.BASE_URL}{config.API_V1_STR}/apps.json"
|
||||||
|
offset = 0
|
||||||
|
limit = 100
|
||||||
|
all_apps = []
|
||||||
|
|
||||||
|
while True:
|
||||||
|
r = httpx.get(f"{url}?limit={limit}&offset={offset}", headers=headers)
|
||||||
|
json_data = r.json()
|
||||||
|
apps = json_data.get("apps",[])
|
||||||
|
all_apps.extend(apps)
|
||||||
|
if len(apps)<limit:
|
||||||
|
break
|
||||||
|
offset += limit
|
||||||
|
return {"apps": all_apps}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('kintone:allapps', request.url._url, f"Error occurred while get allapps({env.DOMAIN_NAME}):", e)
|
||||||
|
|
||||||
@r.get("/appfields")
|
@r.get("/appfields")
|
||||||
async def appfields(app:str):
|
async def appfields(request:Request,app:str,env:config.KINTONE_ENV = Depends(getkintoneenv)):
|
||||||
return getfieldsfromkintone(app)
|
try:
|
||||||
|
return getfieldsfromkintone(app,env)
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('kintone:appfields',request.url._url, f"Error occurred while get app fileds({env.DOMAIN_NAME}->{app}):",e)
|
||||||
|
|
||||||
|
@r.get("/allfields")
|
||||||
|
async def allfields(request:Request,app:str,env:config.KINTONE_ENV = Depends(getkintoneenv)):
|
||||||
|
try:
|
||||||
|
field_resp = getfieldsfromkintone(app,env)
|
||||||
|
form_resp = getformfromkintone(app,env)
|
||||||
|
return merge_kintone_fields(field_resp,form_resp)
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('kintone:allfields',request.url._url, f"Error occurred while get form fileds({env.DOMAIN_NAME}->{app}):",e)
|
||||||
|
|
||||||
@r.get("/appprocess")
|
@r.get("/appprocess")
|
||||||
async def appprocess(app:str):
|
async def appprocess(request:Request,app:str,env:config.KINTONE_ENV = Depends(getkintoneenv)):
|
||||||
return getprocessfromkintone(app)
|
try:
|
||||||
|
return getprocessfromkintone(app,env)
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('kintone:appprocess',request.url._url, f"Error occurred while get app process({env.DOMAIN_NAME}->{app}):",e)
|
||||||
|
|
||||||
@r.get("/alljscs")
|
@r.get("/alljscss")
|
||||||
async def alljscs(app:str):
|
async def alljscs(request:Request,app:str,env:config.KINTONE_ENV=Depends(getkintoneenv)):
|
||||||
headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE}
|
try:
|
||||||
url = f"{c.BASE_URL}{c.API_V1_STR}/app/customize.json"
|
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE}
|
||||||
params = {"app":app}
|
url = f"{env.BASE_URL}{config.API_V1_STR}/app/customize.json"
|
||||||
r = httpx.get(url,headers=headers,params=params)
|
params = {"app":app}
|
||||||
return r.json()
|
r = httpx.get(url,headers=headers,params=params)
|
||||||
|
return r.json()
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('kintone:alljscss',request.url._url, f"Error occurred while get app js/css({env.DOMAIN_NAME}->{app}):",e)
|
||||||
|
|
||||||
@r.post("/createapp",)
|
@r.post("/createapp",)
|
||||||
async def createapp(name:str):
|
async def createapp(request:Request,name:str,env:config.KINTONE_ENV=Depends(getkintoneenv)):
|
||||||
headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
|
try:
|
||||||
data = {"name":name}
|
headers={config.API_V1_AUTH_KEY:env.API_V1_AUTH_VALUE,"Content-Type": "application/json"}
|
||||||
url = f"{c.BASE_URL}{c.API_V1_STR}/preview/app.json"
|
data = {"name":name}
|
||||||
r = httpx.post(url,headers=headers,data=json.dumps(data))
|
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app.json"
|
||||||
result = r.json()
|
|
||||||
if result.get("app") != None:
|
|
||||||
url = f"{c.BASE_URL}{c.API_V1_STR}/preview/app/deploy.json"
|
|
||||||
data = {"apps":[result],"revert": False}
|
|
||||||
r = httpx.post(url,headers=headers,data=json.dumps(data))
|
r = httpx.post(url,headers=headers,data=json.dumps(data))
|
||||||
return r.json
|
result = r.json()
|
||||||
|
if result.get("app") != None:
|
||||||
property=["label","code","type","required","defaultValue","options"]
|
url = f"{env.BASE_URL}{config.API_V1_STR}/preview/app/deploy.json"
|
||||||
|
data = {"apps":[result],"revert": False}
|
||||||
|
r = httpx.post(url,headers=headers,data=json.dumps(data))
|
||||||
|
return r.json
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('kintone:createapp',request.url._url, f"Error occurred while create app({env.DOMAIN_NAME}->{name}):",e)
|
||||||
|
|
||||||
|
@r.get("/defaultgroup")
|
||||||
|
async def currentgroup(request:Request,env:config.KINTONE_ENV=Depends(getkintoneenv)):
|
||||||
|
try:
|
||||||
|
auth_value = env.API_V1_AUTH_VALUE
|
||||||
|
headers={config.API_V1_AUTH_KEY:auth_value}
|
||||||
|
url = f"{env.BASE_URL}/v1/user/groups.json?code={env.KINTONE_USER}"
|
||||||
|
r = httpx.get(url,headers=headers)
|
||||||
|
return r.json()
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('kintone:currentgroup',request.url._url, f"Error occurred while get default domain group(domain:{env.DOMAIN_NAME} url:{env.BASE_URL} user:{env.KINTONE_USER}):",e)
|
||||||
|
|
||||||
@r.post("/createappfromexcel",)
|
@r.post("/createappfromexcel",)
|
||||||
async def createappfromexcel(files:t.List[UploadFile] = File(...)):
|
async def createappfromexcel(request:Request,files:t.List[UploadFile] = File(...),format:int = 0,env = Depends(getkintoneenv),db = Depends(get_db)):
|
||||||
|
try:
|
||||||
|
mapping = getkintoneformat(db)[format]
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('kintone:createappfromexcel',request.url._url, f"Error occurred while get kintone format:",e)
|
||||||
|
|
||||||
for file in files:
|
for file in files:
|
||||||
if file.filename.endswith('.xlsx'):
|
if file.filename.endswith('.xlsx'):
|
||||||
try:
|
try:
|
||||||
@@ -315,87 +664,90 @@ async def createappfromexcel(files:t.List[UploadFile] = File(...)):
|
|||||||
appname = df.iloc[0,2]
|
appname = df.iloc[0,2]
|
||||||
desc = df.iloc[2,2]
|
desc = df.iloc[2,2]
|
||||||
result = {"app":0,"revision":0,"msg":""}
|
result = {"app":0,"revision":0,"msg":""}
|
||||||
fields = getfieldsfromexcel(df)
|
fields = getfieldsfromexcel(df,mapping)
|
||||||
users = getkintoneusers()
|
users = getkintoneusers(env)
|
||||||
orgs = getkintoneorgs()
|
orgs = getkintoneorgs(env)
|
||||||
processes = getprocessfromexcel(df,users["users"], orgs["organizationTitles"])
|
processes = getprocessfromexcel(df,users["users"], orgs["organizationTitles"])
|
||||||
app = createkintoneapp(appname)
|
app = createkintoneapp(appname,env)
|
||||||
if app.get("app") != None:
|
if app.get("app") != None:
|
||||||
result["app"] = app["app"]
|
result["app"] = app["app"]
|
||||||
app = updateappsettingstokintone(result["app"],{"description":desc})
|
app = updateappsettingstokintone(result["app"],{"description":desc},env)
|
||||||
if app.get("revision") != None:
|
if app.get("revision") != None:
|
||||||
result["revision"] = app["revision"]
|
result["revision"] = app["revision"]
|
||||||
app = addfieldstokintone(result["app"],fields)
|
app = addfieldstokintone(result["app"],fields,env)
|
||||||
if len(processes)> 0:
|
if len(processes)> 0:
|
||||||
app = updateprocesstokintone(result["app"],processes)
|
app = updateprocesstokintone(result["app"],processes,env)
|
||||||
if app.get("revision") != None:
|
if app.get("revision") != None:
|
||||||
result["revision"] = app["revision"]
|
result["revision"] = app["revision"]
|
||||||
deoployappfromkintone(result["app"],result["revision"])
|
deoployappfromkintone(result["app"],result["revision"],env)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=400, detail=f"Error occurred while parsing file {file.filename}: {str(e)}")
|
raise APIException('kintone:createappfromexcel',request.url._url, f"Error occurred while parsing file ({env.DOMAIN_NAME}->{file.filename}):",e)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=400, detail=f"File {file.filename} is not an Excel file")
|
raise APIException('kintone:createappfromexcel',request.url._url, f"File {file.filename} is not an Excel file",e)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@r.post("/updateappfromexcel",)
|
@r.post("/updateappfromexcel")
|
||||||
async def updateappfromexcel(app:str,files:t.List[UploadFile] = File(...)):
|
async def updateappfromexcel(request:Request,app:str,files:t.List[UploadFile] = File(...),format:int = 0,env = Depends(getkintoneenv),db = Depends(get_db)):
|
||||||
|
try:
|
||||||
|
mapping = getkintoneformat(db)[format]
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('kintone:updateappfromexcel',request.url._url, f"Error occurred while get kintone format:",e)
|
||||||
|
|
||||||
for file in files:
|
for file in files:
|
||||||
if file.filename.endswith('.xlsx'):
|
if file.filename.endswith('.xlsx'):
|
||||||
try:
|
try:
|
||||||
content = await file.read()
|
content = await file.read()
|
||||||
df = pd.read_excel(BytesIO(content))
|
df = pd.read_excel(BytesIO(content))
|
||||||
excel = getsettingfromexcel(df)
|
excel = getsettingfromexcel(df)
|
||||||
kintone= getsettingfromkintone(app)
|
kintone= getsettingfromkintone(app,env)
|
||||||
settings = analysesettings(excel,kintone)
|
settings = analysesettings(excel,kintone)
|
||||||
excel = getfieldsfromexcel(df)
|
excel = getfieldsfromexcel(df,mapping)
|
||||||
kintone = getfieldsfromkintone(app)
|
kintone = getfieldsfromkintone(app,env)
|
||||||
users = getkintoneusers()
|
users = getkintoneusers(env)
|
||||||
orgs = getkintoneorgs()
|
orgs = getkintoneorgs(env)
|
||||||
exp = getprocessfromexcel(df,users["users"], orgs["organizationTitles"])
|
exp = getprocessfromexcel(df,users["users"], orgs["organizationTitles"])
|
||||||
#exp = getprocessfromexcel(df)
|
#exp = getprocessfromexcel(df)
|
||||||
kinp = getprocessfromkintone(app)
|
kinp = getprocessfromkintone(app,env)
|
||||||
process = analysprocess(exp,kinp)
|
process = analysprocess(exp,kinp)
|
||||||
revision = kintone["revision"]
|
revision = kintone["revision"]
|
||||||
fields = analysefields(excel,kintone["properties"])
|
fields = analysefields(excel,kintone["properties"])
|
||||||
result = {"app":app,"revision":revision,"msg":"No Update"}
|
result = {"app":app,"revision":revision,"msg":"No Update"}
|
||||||
deploy = False
|
deploy = False
|
||||||
if len(fields["update"]) > 0:
|
if len(fields["update"]) > 0:
|
||||||
result = updatefieldstokintone(app,revision,fields["update"])
|
result = updatefieldstokintone(app,revision,fields["update"],env)
|
||||||
revision = result["revision"]
|
revision = result["revision"]
|
||||||
deploy = True
|
deploy = True
|
||||||
if len(fields["add"]) > 0:
|
if len(fields["add"]) > 0:
|
||||||
result = addfieldstokintone(app,fields["add"],revision)
|
result = addfieldstokintone(app,fields["add"],env,revision)
|
||||||
revision = result["revision"]
|
revision = result["revision"]
|
||||||
deploy = True
|
deploy = True
|
||||||
if len(fields["del"]) > 0:
|
if len(fields["del"]) > 0:
|
||||||
result = deletefieldsfromkintone(app,revision,fields["del"])
|
result = deletefieldsfromkintone(app,revision,fields["del"],env)
|
||||||
revision = result["revision"]
|
revision = result["revision"]
|
||||||
deploy = True
|
deploy = True
|
||||||
if len(settings) > 0:
|
if len(settings) > 0:
|
||||||
result = updateappsettingstokintone(app,settings)
|
result = updateappsettingstokintone(app,settings,env)
|
||||||
revision = result["revision"]
|
revision = result["revision"]
|
||||||
deploy = True
|
deploy = True
|
||||||
if len(process)>0:
|
if len(process)>0:
|
||||||
result = updateprocesstokintone(app,exp)
|
result = updateprocesstokintone(app,exp,env)
|
||||||
revision = result["revision"]
|
revision = result["revision"]
|
||||||
deploy = True
|
deploy = True
|
||||||
if deploy:
|
if deploy:
|
||||||
result = deoployappfromkintone(app,revision)
|
result = deoployappfromkintone(app,revision,env)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=400, detail=f"Error occurred while parsing file {file.filename}: {str(e)}")
|
raise APIException('kintone:updateappfromexcel',request.url._url, f"Error occurred while parsing file ({env.DOMAIN_NAME}->{file.filename}):",e)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=400, detail=f"File {file.filename} is not an Excel file")
|
raise APIException('kintone:updateappfromexcel',request.url._url, f"File {file.filename} is not an Excel file",e)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@r.post("/updateprocessfromexcel",)
|
@r.post("/updateprocessfromexcel",)
|
||||||
async def updateprocessfromexcel(app:str):
|
async def updateprocessfromexcel(request:Request,app:str,env = Depends(getkintoneenv)):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
excel = getprocessfromexcel()
|
excel = getprocessfromexcel()
|
||||||
kintone = getprocessfromkintone(app)
|
kintone = getprocessfromkintone(app,env)
|
||||||
revision = kintone["revision"]
|
revision = kintone["revision"]
|
||||||
#fields = analysefields(excel,kintone["properties"])
|
#fields = analysefields(excel,kintone["properties"])
|
||||||
result = {"app":app,"revision":revision,"msg":"No Update"}
|
result = {"app":app,"revision":revision,"msg":"No Update"}
|
||||||
@@ -416,14 +768,33 @@ async def updateprocessfromexcel(app:str):
|
|||||||
# result = updateappsettingstokintone(app,settings)
|
# result = updateappsettingstokintone(app,settings)
|
||||||
# revision = result["revision"]
|
# revision = result["revision"]
|
||||||
# deploy = True
|
# deploy = True
|
||||||
result = updateprocesstokintone(app,excel)
|
result = updateprocesstokintone(app,excel,env)
|
||||||
revision = result["revision"]
|
revision = result["revision"]
|
||||||
deploy = True
|
deploy = True
|
||||||
if deploy:
|
if deploy:
|
||||||
result = deoployappfromkintone(app,revision)
|
result = deoployappfromkintone(app,revision,env)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=400, detail=f"Error occurred : {str(e)}")
|
raise APIException('kintone:updateprocessfromexcel',request.url._url, f"Error occurred while update process ({env.DOMAIN_NAME}->{app}):",e)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@r.post("/createjstokintone",)
|
||||||
|
async def createjstokintone(request:Request,app:str,env:config.KINTONE_ENV = Depends(getkintoneenv),db = Depends(get_db)):
|
||||||
|
try:
|
||||||
|
jscs=[]
|
||||||
|
files=[]
|
||||||
|
files.append(createappjs(env.BASE_URL, app, db))
|
||||||
|
files.append(getTempPath('alc_runtime.js'))
|
||||||
|
files.append(getTempPath('alc_runtime.css'))
|
||||||
|
for file in files:
|
||||||
|
upload = uploadkintonefiles(file,env)
|
||||||
|
if upload.get('fileKey') != None:
|
||||||
|
print(upload)
|
||||||
|
jscs.append({ file :upload['fileKey']})
|
||||||
|
appjscs = updateappjscss(app,jscs,env)
|
||||||
|
if appjscs.get("revision") != None:
|
||||||
|
deoployappfromkintone(app,appjscs["revision"],env)
|
||||||
|
return appjscs
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('kintone:createjstokintone',request.url._url, f"Error occurred while create js ({env.DOMAIN_NAME}->{app}):",e)
|
||||||
|
|||||||
@@ -1,11 +1,152 @@
|
|||||||
from fastapi import Request,Depends, APIRouter, UploadFile,HTTPException,File
|
from http import HTTPStatus
|
||||||
from app.db import Base,engine
|
from fastapi import Query, Request,Depends, APIRouter, UploadFile,HTTPException,File
|
||||||
from app.db.session import get_db
|
from fastapi.responses import JSONResponse
|
||||||
|
# from app.core.operation import log_operation
|
||||||
|
# from app.db import Base,engine
|
||||||
|
from app.core.dbmanager import get_db
|
||||||
from app.db.crud import *
|
from app.db.crud import *
|
||||||
from app.db.schemas import AppBase, AppEdit, App,Kintone
|
from app.db.schemas import *
|
||||||
|
from typing import List, Optional
|
||||||
|
from app.core.auth import get_current_active_user,get_current_user
|
||||||
|
from app.core.apiexception import APIException
|
||||||
|
from app.core.common import ApiReturnModel,ApiReturnPage
|
||||||
|
#from fastapi_pagination import Page
|
||||||
|
from app.db.cruddb import domainService,appService
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import app.core.config as config
|
||||||
|
from app.core import domainCacheService,tenantCacheService
|
||||||
|
|
||||||
platform_router = r = APIRouter()
|
platform_router = r = APIRouter()
|
||||||
|
|
||||||
|
@r.get(
|
||||||
|
"/test",
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
|
async def test(
|
||||||
|
request: Request,
|
||||||
|
user = Depends(get_current_active_user),
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
domainService.select(db,{"tenantid":1,"name":["b","c"]})
|
||||||
|
|
||||||
|
|
||||||
|
@r.get(
|
||||||
|
"/apps",tags=["App"],
|
||||||
|
response_model=ApiReturnModel[List[AppList]|None],
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
|
async def apps_list(
|
||||||
|
request: Request,
|
||||||
|
user = Depends(get_current_active_user),
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
|
||||||
|
domain = domainService.get_default_domain(db,user.id) #get_activedomain(db, user.id)
|
||||||
|
if not domain:
|
||||||
|
return ApiReturnModel(data = None)
|
||||||
|
filtered_apps = []
|
||||||
|
platformapps = appService.get_apps(db,domain.url)
|
||||||
|
kintoneevn = config.KINTONE_ENV(domain)
|
||||||
|
headers={config.API_V1_AUTH_KEY:kintoneevn.API_V1_AUTH_VALUE}
|
||||||
|
url = f"{kintoneevn.BASE_URL}{config.API_V1_STR}/apps.json"
|
||||||
|
offset = 0
|
||||||
|
limit = 100
|
||||||
|
all_apps = []
|
||||||
|
|
||||||
|
while True:
|
||||||
|
r = httpx.get(f"{url}?limit={limit}&offset={offset}", headers=headers)
|
||||||
|
json_data = r.json()
|
||||||
|
apps = json_data.get("apps",[])
|
||||||
|
all_apps.extend(apps)
|
||||||
|
if len(apps)<limit:
|
||||||
|
break
|
||||||
|
offset += limit
|
||||||
|
|
||||||
|
kintone_apps_dict = {app['appId']: app for app in all_apps}
|
||||||
|
for papp in platformapps:
|
||||||
|
if papp.appid in kintone_apps_dict:
|
||||||
|
papp.appname = kintone_apps_dict[papp.appid]["name"]
|
||||||
|
filtered_apps.append(papp)
|
||||||
|
return ApiReturnModel(data = filtered_apps)
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:apps',request.url._url,f"Error occurred while get apps:",e)
|
||||||
|
|
||||||
|
@r.post("/apps", tags=["App"],
|
||||||
|
response_model=ApiReturnModel[AppList|None],
|
||||||
|
response_model_exclude_none=True)
|
||||||
|
async def apps_update(
|
||||||
|
request: Request,
|
||||||
|
app: VersionUpdate,
|
||||||
|
user=Depends(get_current_active_user),
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
domainurl = domainCacheService.get_default_domainurl(db,user.id)
|
||||||
|
if not domainurl:
|
||||||
|
return ApiReturnModel(data = None)
|
||||||
|
return ApiReturnModel(data =appService.update_appversion(db, domainurl,app,user.id))
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:apps',request.url._url,f"Error occurred while get create app :",e)
|
||||||
|
|
||||||
|
@r.delete("/apps/{appid}",tags=["App"],
|
||||||
|
response_model=ApiReturnModel[AppList|None],
|
||||||
|
response_model_exclude_none=True
|
||||||
|
)
|
||||||
|
async def apps_delete(
|
||||||
|
request: Request,
|
||||||
|
appid: str,
|
||||||
|
user=Depends(get_current_active_user),
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
domainurl = domainCacheService.get_default_domainurl(db,user.id) #get_activedomain(db, user.id)
|
||||||
|
if not domainurl:
|
||||||
|
return ApiReturnModel(data = None)
|
||||||
|
return ApiReturnModel(data =appService.delete_app(db, domainurl,appid))
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:apps',request.url._url,f"Error occurred while delete app({appid}):",e)
|
||||||
|
|
||||||
|
@r.get(
|
||||||
|
"/appversions/{appid}",tags=["App"],
|
||||||
|
response_model=ApiReturnPage[AppVersion|None],
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
|
async def appversions_list(
|
||||||
|
request: Request,
|
||||||
|
appid: str,
|
||||||
|
user = Depends(get_current_active_user),
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
domainurl = domainCacheService.get_default_domainurl(db,user.id) #get_activedomain(db, user.id)
|
||||||
|
if not domainurl:
|
||||||
|
return ApiReturnPage(data = None)
|
||||||
|
return appService.get_appversions(db,domainurl,appid)
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:appversions',request.url._url,f"Error occurred while get app({appid}) version :",e)
|
||||||
|
|
||||||
|
@r.put(
|
||||||
|
"/appversions/{appid}/{version}",tags=["App"],
|
||||||
|
response_model=ApiReturnModel[AppList|None],
|
||||||
|
response_model_exclude_none=True
|
||||||
|
)
|
||||||
|
async def appversions_change(
|
||||||
|
request: Request,
|
||||||
|
appid: str,
|
||||||
|
version: int,
|
||||||
|
user = Depends(get_current_active_user),
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
domainurl = domainCacheService.get_default_domainurl(db,user.id) #get_activedomain(db, user.id)
|
||||||
|
if not domainurl:
|
||||||
|
ApiReturnModel(data = None)
|
||||||
|
return ApiReturnModel(data = appService.change_appversion(db, domainurl,appid,version,user.id))
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:appversions',request.url._url,f"Error occurred while change app version:",e)
|
||||||
|
|
||||||
@r.get(
|
@r.get(
|
||||||
"/appsettings/{id}",
|
"/appsettings/{id}",
|
||||||
response_model=App,
|
response_model=App,
|
||||||
@@ -16,9 +157,11 @@ async def appsetting_details(
|
|||||||
id: int,
|
id: int,
|
||||||
db=Depends(get_db),
|
db=Depends(get_db),
|
||||||
):
|
):
|
||||||
app = get_appsetting(db, id)
|
try:
|
||||||
return app
|
app = get_appsetting(db, id)
|
||||||
|
return app
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:appsettings',request.url._url,f"Error occurred while get app setting:",e)
|
||||||
|
|
||||||
@r.post("/appsettings", response_model=App, response_model_exclude_none=True)
|
@r.post("/appsettings", response_model=App, response_model_exclude_none=True)
|
||||||
async def appsetting_create(
|
async def appsetting_create(
|
||||||
@@ -26,7 +169,10 @@ async def appsetting_create(
|
|||||||
app: AppBase,
|
app: AppBase,
|
||||||
db=Depends(get_db),
|
db=Depends(get_db),
|
||||||
):
|
):
|
||||||
return create_appsetting(db, app)
|
try:
|
||||||
|
return create_appsetting(db, app)
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:appsettings',request.url._url,f"Error occurred while get create app setting:",e)
|
||||||
|
|
||||||
|
|
||||||
@r.put(
|
@r.put(
|
||||||
@@ -38,7 +184,10 @@ async def appsetting_edit(
|
|||||||
app: AppEdit,
|
app: AppEdit,
|
||||||
db=Depends(get_db),
|
db=Depends(get_db),
|
||||||
):
|
):
|
||||||
return edit_appsetting(db, id, app)
|
try:
|
||||||
|
return edit_appsetting(db, id, app)
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:appsettings',request.url._url,f"Error occurred while edit app setting:",e)
|
||||||
|
|
||||||
|
|
||||||
@r.delete(
|
@r.delete(
|
||||||
@@ -49,8 +198,10 @@ async def appsettings_delete(
|
|||||||
id: int,
|
id: int,
|
||||||
db=Depends(get_db),
|
db=Depends(get_db),
|
||||||
):
|
):
|
||||||
|
try:
|
||||||
return delete_appsetting(db, id)
|
return delete_appsetting(db, id)
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:appsettings',request.url._url,f"Error occurred while delete app setting:",e)
|
||||||
|
|
||||||
|
|
||||||
@r.get(
|
@r.get(
|
||||||
@@ -63,5 +214,407 @@ async def kintone_data(
|
|||||||
type: int,
|
type: int,
|
||||||
db=Depends(get_db),
|
db=Depends(get_db),
|
||||||
):
|
):
|
||||||
kintone = get_kintones(db, type)
|
try:
|
||||||
return kintone
|
kintone = get_kintones(db, type)
|
||||||
|
return kintone
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:kintone',request.url._url,f"Error occurred while get kintone env:",e)
|
||||||
|
|
||||||
|
@r.get(
|
||||||
|
"/actions",
|
||||||
|
response_model=t.List[Action],
|
||||||
|
response_model_exclude={"id"},
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
|
async def action_data(
|
||||||
|
request: Request,
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
actions = get_actions(db)
|
||||||
|
return actions
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:actions',request.url._url,f"Error occurred while get actions:",e)
|
||||||
|
|
||||||
|
@r.get(
|
||||||
|
"/flow/{appid}",tags=["App"],
|
||||||
|
response_model=ApiReturnModel[List[Flow]|None],
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
|
async def flow_details(
|
||||||
|
request: Request,
|
||||||
|
appid: str,
|
||||||
|
user=Depends(get_current_active_user),
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
domainurl = domainCacheService.get_default_domainurl(db,user.id)
|
||||||
|
if not domainurl:
|
||||||
|
return ApiReturnModel(data = None)
|
||||||
|
return ApiReturnModel(data = appService.get_flow(db, domainurl, appid))
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by flowid:",e)
|
||||||
|
|
||||||
|
@r.get(
|
||||||
|
"/flows/{appid}", tags=["App"],
|
||||||
|
response_model=List[Flow|None],
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
|
async def flow_list(
|
||||||
|
request: Request,
|
||||||
|
appid: str,
|
||||||
|
user=Depends(get_current_active_user),
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
domainurl = domainCacheService.get_default_domainurl(db,user.id) #get_activedomain(db, user.id)
|
||||||
|
if not domainurl:
|
||||||
|
return []
|
||||||
|
#flows = get_flows_by_app(db, domainurl, appid)
|
||||||
|
flows = appService.get_flow(db,domainurl,appid)
|
||||||
|
return flows
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by appid:",e)
|
||||||
|
|
||||||
|
|
||||||
|
@r.post("/flow", tags=["App"],
|
||||||
|
response_model=ApiReturnModel[Flow|None],
|
||||||
|
response_model_exclude_none=True)
|
||||||
|
async def flow_create(
|
||||||
|
request: Request,
|
||||||
|
flow: FlowIn,
|
||||||
|
user=Depends(get_current_active_user),
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
domainurl = domainCacheService.get_default_domainurl(db,user.id) #get_activedomain(db, user.id)
|
||||||
|
if not domainurl:
|
||||||
|
return ApiReturnModel(data = None)
|
||||||
|
return ApiReturnModel(data = appService.create_flow(db, domainurl, flow,user.id))
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:flow',request.url._url,f"Error occurred while create flow:",e)
|
||||||
|
|
||||||
|
|
||||||
|
@r.put(
|
||||||
|
"/flow", tags=["App"],
|
||||||
|
response_model=ApiReturnModel[Flow|None],
|
||||||
|
response_model_exclude_none=True
|
||||||
|
)
|
||||||
|
async def flow_edit(
|
||||||
|
request: Request,
|
||||||
|
flow: FlowIn,
|
||||||
|
user=Depends(get_current_active_user),
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
domainurl = domainCacheService.get_default_domainurl(db,user.id)
|
||||||
|
if not domainurl:
|
||||||
|
return ApiReturnModel(data = None)
|
||||||
|
return ApiReturnModel(data = appService.edit_flow(db,domainurl, flow,user.id))
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:flow',request.url._url,f"Error occurred while edit flow:",e)
|
||||||
|
|
||||||
|
|
||||||
|
@r.delete(
|
||||||
|
"/flow/{flowid}", tags=["App"],
|
||||||
|
response_model=ApiReturnModel[Flow|None],
|
||||||
|
response_model_exclude_none=True
|
||||||
|
)
|
||||||
|
async def flow_delete(
|
||||||
|
request: Request,
|
||||||
|
flowid: str,
|
||||||
|
user=Depends(get_current_active_user),
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
domainurl = domainCacheService.get_default_domainurl(db,user.id)
|
||||||
|
if not domainurl:
|
||||||
|
return ApiReturnModel(data = None)
|
||||||
|
return ApiReturnModel(data = appService.delete_flow(db, flowid))
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:flow',request.url._url,f"Error occurred while delete flow:",e)
|
||||||
|
|
||||||
|
@r.get(
|
||||||
|
"/domains",tags=["Domain"],
|
||||||
|
response_model=ApiReturnPage[Domain],
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
|
async def domain_list(
|
||||||
|
request: Request,
|
||||||
|
user=Depends(get_current_active_user),
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
if user.is_superuser:
|
||||||
|
domains = domainService.get_domains(db)
|
||||||
|
else:
|
||||||
|
domains = domainService.get_domains_by_manage(db,user.id)
|
||||||
|
return domains
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:domains',request.url._url,f"Error occurred while get domains:",e)
|
||||||
|
|
||||||
|
@r.get(
|
||||||
|
"/domain/{domain_id}",tags=["Domain"],
|
||||||
|
response_model=ApiReturnModel[Domain|None],
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
|
async def domain_detail(
|
||||||
|
request: Request,
|
||||||
|
domain_id:int,
|
||||||
|
user=Depends(get_current_active_user),
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
return ApiReturnModel(data = domainService.get(db,domain_id))
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:domain',request.url._url,f"Error occurred while get domain detail:",e)
|
||||||
|
|
||||||
|
@r.post("/domain", tags=["Domain"],
|
||||||
|
response_model=ApiReturnModel[Domain],
|
||||||
|
response_model_exclude_none=True)
|
||||||
|
async def domain_create(
|
||||||
|
request: Request,
|
||||||
|
domain: DomainIn,
|
||||||
|
user=Depends(get_current_active_user),
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
return ApiReturnModel(data = domainService.create_domain(db, domain,user.id))
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:domain',request.url._url,f"Error occurred while create domain:",e)
|
||||||
|
|
||||||
|
|
||||||
|
@r.put(
|
||||||
|
"/domain", tags=["Domain"],
|
||||||
|
response_model=ApiReturnModel[Domain|None],
|
||||||
|
response_model_exclude_none=True
|
||||||
|
)
|
||||||
|
async def domain_edit(
|
||||||
|
request: Request,
|
||||||
|
domain: DomainIn,
|
||||||
|
user=Depends(get_current_active_user),
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
domain = domainService.edit_domain(db, domain,user.id)
|
||||||
|
if domain :
|
||||||
|
domainCacheService.clear_default_domainurl()
|
||||||
|
return ApiReturnModel(data = domain)
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:domain',request.url._url,f"Error occurred while edit domain:",e)
|
||||||
|
|
||||||
|
|
||||||
|
@r.delete(
|
||||||
|
"/domain/{id}",tags=["Domain"],
|
||||||
|
response_model=ApiReturnModel[DomainOut|None],
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
|
async def domain_delete(
|
||||||
|
request: Request,
|
||||||
|
id: int,
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
return ApiReturnModel(data = domainService.delete_domain(db,id))
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:domain',request.url._url,f"Error occurred while delete domain({id}):",e)
|
||||||
|
|
||||||
|
@r.get(
|
||||||
|
"/domain",
|
||||||
|
response_model=List[Domain],
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
|
async def userdomain_details(
|
||||||
|
request: Request,
|
||||||
|
userId: Optional[int] = Query(None, alias="userId"),
|
||||||
|
user=Depends(get_current_active_user),
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
domains = get_domain(db, userId if userId is not None else user.id)
|
||||||
|
return domains
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:userdomain',request.url._url,f"Error occurred while get user({user.id}) domain:",e)
|
||||||
|
|
||||||
|
@r.post(
|
||||||
|
"/userdomain",tags=["Domain"],
|
||||||
|
response_model=ApiReturnModel[DomainOut|None],
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
|
async def create_userdomain(
|
||||||
|
request: Request,
|
||||||
|
userdomain:UserDomainParam,
|
||||||
|
user=Depends(get_current_active_user),
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
userid = userdomain.userid
|
||||||
|
domainid = userdomain.domainid
|
||||||
|
if user.is_superuser:
|
||||||
|
domain = domainService.add_userdomain(db,user.id,userid,domainid)
|
||||||
|
else:
|
||||||
|
domain = domainService.add_userdomain_by_owner(db,user.id,userid,domainid)
|
||||||
|
return ApiReturnModel(data = domain)
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:userdomain',request.url._url,f"Error occurred while add user({userid}) domain({domainid}):",e)
|
||||||
|
|
||||||
|
@r.delete(
|
||||||
|
"/domain/{domainid}/{userid}",tags=["Domain"],
|
||||||
|
response_model=ApiReturnModel[DomainOut|None],
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
|
async def delete_userdomain(
|
||||||
|
request: Request,
|
||||||
|
domainid:int,
|
||||||
|
userid: int,
|
||||||
|
user=Depends(get_current_active_user),
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
return ApiReturnModel(data = domainService.delete_userdomain(db,userid,domainid))
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:userdomain',request.url._url,f"Error occurred while delete user({userid}) domain({domainid}):",e)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@r.get(
|
||||||
|
"/managedomainuser/{domainid}",tags=["Domain"],
|
||||||
|
response_model=ApiReturnPage[UserOut|None],
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
|
async def get_managedomainuser(
|
||||||
|
request: Request,
|
||||||
|
domainid:int,
|
||||||
|
user=Depends(get_current_active_user),
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
return domainService.get_managedomain_users(db,domainid)
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:managedomain',request.url._url,f"Error occurred while get managedomain({user.id}) user:",e)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@r.post(
|
||||||
|
"/managedomain",tags=["Domain"],
|
||||||
|
response_model=ApiReturnModel[DomainOut|None],
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
|
async def create_managedomain(
|
||||||
|
request: Request,
|
||||||
|
userdomain:UserDomainParam,
|
||||||
|
user=Depends(get_current_active_user),
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
userid = userdomain.userid
|
||||||
|
domainid = userdomain.domainid
|
||||||
|
if user.is_superuser:
|
||||||
|
domain = domainService.add_managedomain(db,user.id,userid,domainid)
|
||||||
|
else:
|
||||||
|
domain = domainService.add_managedomain_by_owner(db,user.id,userid,domainid)
|
||||||
|
return ApiReturnModel(data = domain)
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:managedomain',request.url._url,f"Error occurred while add manage({userid}) domain({domainid}):",e)
|
||||||
|
|
||||||
|
@r.delete(
|
||||||
|
"/managedomain/{domainid}/{userid}",tags=["Domain"],
|
||||||
|
response_model=ApiReturnModel[DomainOut|None],
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
|
async def delete_managedomain(
|
||||||
|
request: Request,
|
||||||
|
domainid:int,
|
||||||
|
userid: int,
|
||||||
|
user=Depends(get_current_active_user),
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
return ApiReturnModel(data = domainService.delete_managedomain(db,userid,domainid))
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:managedomain',request.url._url,f"Error occurred while delete managedomain({userid}) domain({domainid}):",e)
|
||||||
|
|
||||||
|
@r.get(
|
||||||
|
"/defaultdomain",tags=["Domain"],
|
||||||
|
response_model=ApiReturnModel[DomainOut|None],
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
|
async def get_defaultuserdomain(
|
||||||
|
request: Request,
|
||||||
|
user=Depends(get_current_active_user),
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
return ApiReturnModel(data =domainService.get_default_domain(db,user.id))
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:defaultdomain',request.url._url,f"Error occurred while get user({user.id}) defaultdomain:",e)
|
||||||
|
|
||||||
|
@r.put(
|
||||||
|
"/defaultdomain/{domainid}",tags=["Domain"],
|
||||||
|
response_model=ApiReturnModel[DomainOut|None],
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
|
async def set_defualtuserdomain(
|
||||||
|
request: Request,
|
||||||
|
domainid:int,
|
||||||
|
user=Depends(get_current_active_user),
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
domain = domainCacheService.set_default_domain(db,user.id,domainid)
|
||||||
|
return ApiReturnModel(data= domain)
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:defaultdomain',request.url._url,f"Error occurred while update user({user.id}) defaultdomain:",e)
|
||||||
|
|
||||||
|
|
||||||
|
@r.get(
|
||||||
|
"/domainshareduser/{domainid}",tags=["Domain"],
|
||||||
|
response_model=ApiReturnPage[UserOut|None],
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
|
async def get_domainshareduser(
|
||||||
|
request: Request,
|
||||||
|
domainid:int,
|
||||||
|
user=Depends(get_current_active_user),
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
return domainService.get_shareddomain_users(db,domainid)
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:sharedomain',request.url._url,f"Error occurred while get user({user.id}) sharedomain:",e)
|
||||||
|
|
||||||
|
|
||||||
|
@r.get(
|
||||||
|
"/events",
|
||||||
|
response_model=t.List[Event],
|
||||||
|
response_model_exclude={"id"},
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
|
async def event_data(
|
||||||
|
request: Request,
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
events = get_events(db)
|
||||||
|
return events
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:events',request.url._url,f"Error occurred while get events:",e)
|
||||||
|
|
||||||
|
|
||||||
|
@r.get(
|
||||||
|
"/eventactions/{eventid}",
|
||||||
|
response_model=t.List[Action],
|
||||||
|
response_model_exclude={"id"},
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
|
async def eventactions_data(
|
||||||
|
request: Request,
|
||||||
|
eventid: str,
|
||||||
|
db=Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
actions = get_eventactions(db,eventid)
|
||||||
|
return actions
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('platform:eventactions',request.url._url,f"Error occurred while get eventactions:",e)
|
||||||
@@ -1,107 +1,187 @@
|
|||||||
from fastapi import APIRouter, Request, Depends, Response, encoders
|
from fastapi import APIRouter, Request, Depends, Response, Security, encoders
|
||||||
import typing as t
|
import typing as t
|
||||||
|
from app.core.common import ApiReturnModel,ApiReturnPage
|
||||||
from app.db.session import get_db
|
from app.core.apiexception import APIException
|
||||||
|
from app.core.dbmanager import get_db
|
||||||
from app.db.crud import (
|
from app.db.crud import (
|
||||||
|
get_allusers,
|
||||||
get_users,
|
get_users,
|
||||||
get_user,
|
get_user,
|
||||||
create_user,
|
create_user,
|
||||||
delete_user,
|
delete_user,
|
||||||
edit_user,
|
edit_user,
|
||||||
|
assign_userrole,
|
||||||
|
get_roles,
|
||||||
)
|
)
|
||||||
from app.db.schemas import UserCreate, UserEdit, User, UserOut
|
from app.db.schemas import UserCreate, UserEdit, User, UserOut,RoleBase,AssignUserRoles,Permission
|
||||||
from app.core.auth import get_current_active_user, get_current_active_superuser
|
from app.core.auth import get_current_user,get_current_active_user, get_current_active_superuser
|
||||||
|
from app.db.cruddb import userService
|
||||||
|
from app.core import tenantCacheService
|
||||||
|
|
||||||
users_router = r = APIRouter()
|
users_router = r = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@r.get(
|
@r.get(
|
||||||
"/users",
|
"/users",tags=["User"],
|
||||||
response_model=t.List[User],
|
response_model=ApiReturnPage[User],
|
||||||
response_model_exclude_none=True,
|
response_model_exclude_none=True,
|
||||||
)
|
)
|
||||||
async def users_list(
|
async def users_list(
|
||||||
response: Response,
|
request: Request,
|
||||||
db=Depends(get_db),
|
db=Depends(get_db),
|
||||||
current_user=Depends(get_current_active_superuser),
|
current_user=Depends(get_current_active_user),
|
||||||
):
|
):
|
||||||
"""
|
try:
|
||||||
Get all users
|
if current_user.is_superuser:
|
||||||
"""
|
users = userService.get_users(db)
|
||||||
users = get_users(db)
|
else:
|
||||||
# This is necessary for react-admin to work
|
users = userService.get_users_not_admin(db)
|
||||||
response.headers["Content-Range"] = f"0-9/{len(users)}"
|
return users
|
||||||
return users
|
except Exception as e:
|
||||||
|
raise APIException('user:users',request.url._url,f"Error occurred while get user list",e)
|
||||||
|
|
||||||
|
@r.get("/users/me", tags=["User"],
|
||||||
@r.get("/users/me", response_model=User, response_model_exclude_none=True)
|
response_model=ApiReturnModel[User],
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
async def user_me(current_user=Depends(get_current_active_user)):
|
async def user_me(current_user=Depends(get_current_active_user)):
|
||||||
"""
|
return ApiReturnModel(data = current_user)
|
||||||
Get own user
|
|
||||||
"""
|
|
||||||
return current_user
|
|
||||||
|
|
||||||
|
|
||||||
@r.get(
|
@r.get(
|
||||||
"/users/{user_id}",
|
"/users/{user_id}",tags=["User"],
|
||||||
response_model=User,
|
response_model=ApiReturnModel[User|None],
|
||||||
response_model_exclude_none=True,
|
response_model_exclude_none=True,
|
||||||
)
|
)
|
||||||
async def user_details(
|
async def user_details(
|
||||||
request: Request,
|
request: Request,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
db=Depends(get_db),
|
db=Depends(get_db),
|
||||||
current_user=Depends(get_current_active_superuser),
|
current_user=Depends(get_current_active_user),
|
||||||
):
|
):
|
||||||
"""
|
try:
|
||||||
Get any user details
|
user = userService.get(db, user_id)
|
||||||
"""
|
if user:
|
||||||
user = get_user(db, user_id)
|
if user.is_superuser and not current_user.is_superuser:
|
||||||
return user
|
user = None
|
||||||
# return encoders.jsonable_encoder(
|
return ApiReturnModel(data = user)
|
||||||
# user, skip_defaults=True, exclude_none=True,
|
except Exception as e:
|
||||||
# )
|
raise APIException('user:users',request.url._url,f"Error occurred while get user({user_id}) detail:",e)
|
||||||
|
|
||||||
|
|
||||||
@r.post("/users", response_model=User, response_model_exclude_none=True)
|
@r.post("/users", tags=["User"],
|
||||||
|
response_model=ApiReturnModel[User|None],
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
async def user_create(
|
async def user_create(
|
||||||
request: Request,
|
request: Request,
|
||||||
user: UserCreate,
|
user: UserCreate,
|
||||||
db=Depends(get_db),
|
db=Depends(get_db),
|
||||||
current_user=Depends(get_current_active_superuser),
|
current_user=Depends(get_current_active_user),
|
||||||
):
|
):
|
||||||
"""
|
try:
|
||||||
Create a new user
|
if user.is_superuser and not current_user.is_superuser:
|
||||||
"""
|
return ApiReturnModel(data = None)
|
||||||
return create_user(db, user)
|
return ApiReturnModel(data =userService.create_user(db, user,current_user.id))
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('user:users',request.url._url,f"Error occurred while create user({user.email}):",e)
|
||||||
|
|
||||||
|
|
||||||
@r.put(
|
@r.put(
|
||||||
"/users/{user_id}", response_model=User, response_model_exclude_none=True
|
"/users/{user_id}", tags=["User"],
|
||||||
|
response_model=ApiReturnModel[User|None],
|
||||||
|
response_model_exclude_none=True,
|
||||||
)
|
)
|
||||||
async def user_edit(
|
async def user_edit(
|
||||||
request: Request,
|
request: Request,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
user: UserEdit,
|
user: UserEdit,
|
||||||
db=Depends(get_db),
|
db=Depends(get_db),
|
||||||
current_user=Depends(get_current_active_superuser),
|
current_user=Depends(get_current_active_user),
|
||||||
):
|
):
|
||||||
"""
|
try:
|
||||||
Update existing user
|
if user.is_superuser and not current_user.is_superuser:
|
||||||
"""
|
return ApiReturnModel(data = None)
|
||||||
return edit_user(db, user_id, user)
|
return ApiReturnModel(data = userService.edit_user(db,user_id,user,current_user.id))
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('user:users',request.url._url,f"Error occurred while edit user({user_id}):",e)
|
||||||
|
|
||||||
@r.delete(
|
@r.delete(
|
||||||
"/users/{user_id}", response_model=User, response_model_exclude_none=True
|
"/users/{user_id}", tags=["User"],
|
||||||
|
response_model=ApiReturnModel[UserOut|None],
|
||||||
|
response_model_exclude_none=True
|
||||||
)
|
)
|
||||||
async def user_delete(
|
async def user_delete(
|
||||||
request: Request,
|
request: Request,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
db=Depends(get_db),
|
db=Depends(get_db),
|
||||||
current_user=Depends(get_current_active_superuser),
|
current_user=Depends(get_current_active_user),
|
||||||
):
|
):
|
||||||
"""
|
try:
|
||||||
Delete existing user
|
user = userService.get(db,user_id)
|
||||||
"""
|
if user.is_superuser and not current_user.is_superuser:
|
||||||
return delete_user(db, user_id)
|
return ApiReturnModel(data = None)
|
||||||
|
return ApiReturnModel(data = userService.delete_user(db, user_id))
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('user:users',request.url._url,f"Error occurred while delete user({user_id}):",e)
|
||||||
|
|
||||||
|
|
||||||
|
@r.post("/userrole",tags=["User"],
|
||||||
|
response_model=ApiReturnModel[User],
|
||||||
|
response_model_exclude_none=True,)
|
||||||
|
async def assign_role(
|
||||||
|
request: Request,
|
||||||
|
userroles:AssignUserRoles,
|
||||||
|
db=Depends(get_db)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
return ApiReturnModel(data = userService.assign_userrole(db,userroles.userid,userroles.roleids))
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('user:userrole',request.url._url,f"Error occurred while assign user({userroles.userid}) roles({userroles.roleids}):",e)
|
||||||
|
|
||||||
|
@r.get(
|
||||||
|
"/roles",tags=["User"],
|
||||||
|
response_model=ApiReturnModel[t.List[RoleBase]|None],
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
|
async def roles_list(
|
||||||
|
request: Request,
|
||||||
|
db=Depends(get_db),
|
||||||
|
current_user=Depends(get_current_active_user),
|
||||||
|
#current_user=Security(get_current_active_user, scopes=["role_list"]),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
|
||||||
|
if current_user.is_superuser:
|
||||||
|
roles = userService.get_roles(db)
|
||||||
|
else:
|
||||||
|
if len(current_user.roles)>0:
|
||||||
|
roles = userService.get_roles_by_level(db,current_user.roles)
|
||||||
|
else:
|
||||||
|
roles = []
|
||||||
|
return ApiReturnModel(data = roles)
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('user:roles',request.url._url,f"Error occurred while get roles:",e)
|
||||||
|
|
||||||
|
@r.get(
|
||||||
|
"/userpermssions",tags=["User"],
|
||||||
|
response_model=ApiReturnModel[t.List[Permission]|None],
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
|
async def permssions_list(
|
||||||
|
request: Request,
|
||||||
|
db=Depends(get_db),
|
||||||
|
current_user=Depends(get_current_active_user),
|
||||||
|
#current_user=Security(get_current_active_user, scopes=["role_list"]),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
if current_user.is_superuser:
|
||||||
|
permissions = userService.get_permissions(db)
|
||||||
|
else:
|
||||||
|
if len(current_user.roles)>0:
|
||||||
|
permissions = userService.get_user_permissions(db,current_user.id)
|
||||||
|
else:
|
||||||
|
permissions = []
|
||||||
|
return ApiReturnModel(data = permissions)
|
||||||
|
except Exception as e:
|
||||||
|
raise APIException('user:userpermssions',request.url._url,f"Error occurred while get user({current_user.id}) permissions:",e)
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
from app.core.cache import domainCacheService
|
||||||
|
from app.core.cache import tenantCacheService
|
||||||
40
backend/app/core/apiexception.py
Normal file
40
backend/app/core/apiexception.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from fastapi import HTTPException, status,Depends
|
||||||
|
import httpx
|
||||||
|
from app.db.schemas import ErrorCreate
|
||||||
|
from app.core.dbmanager import get_log_db
|
||||||
|
from app.db.crud import create_log
|
||||||
|
|
||||||
|
class APIException(Exception):
|
||||||
|
def __init__(self, location: str, title: str, content: str, e: Exception):
|
||||||
|
self.detail = str(e)
|
||||||
|
self.status_code = 500
|
||||||
|
if isinstance(e,httpx.HTTPStatusError):
|
||||||
|
try:
|
||||||
|
error_response = e.response.json()
|
||||||
|
self.detail = error_response.get('message', self.detail)
|
||||||
|
self.status_code = e.response.status_code
|
||||||
|
content += self.detail
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
elif hasattr(e, 'detail'):
|
||||||
|
self.detail = e.detail
|
||||||
|
self.status_code = e.status_code if hasattr(e, 'status_code') else 500
|
||||||
|
content += str(e.detail)
|
||||||
|
else:
|
||||||
|
self.detail = str(e)
|
||||||
|
self.status_code = 500
|
||||||
|
content += str(e)
|
||||||
|
|
||||||
|
if len(content) > 5000:
|
||||||
|
content = content[:5000]
|
||||||
|
|
||||||
|
self.error = ErrorCreate(location=location, title=title, content=content)
|
||||||
|
super().__init__(self.error)
|
||||||
|
|
||||||
|
def writedblog(exc: APIException,):
|
||||||
|
#db = SessionLocal()
|
||||||
|
db = get_log_db()
|
||||||
|
try:
|
||||||
|
create_log(db,exc.error)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
|
from fastapi.security import SecurityScopes
|
||||||
import jwt
|
import jwt
|
||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import Depends, HTTPException, Request, Security, status
|
||||||
from jwt import PyJWTError
|
from jwt import PyJWTError
|
||||||
|
|
||||||
from app.db import models, schemas, session
|
from app.db import models, schemas
|
||||||
from app.db.crud import get_user_by_email, create_user
|
from app.db.crud import get_user_by_email, create_user,get_user
|
||||||
from app.core import security
|
from app.core import security
|
||||||
|
from app.db.cruddb import userService
|
||||||
|
from app.core.dbmanager import get_db
|
||||||
|
|
||||||
|
async def get_current_user(request: Request,security_scopes: SecurityScopes,
|
||||||
async def get_current_user(
|
db=Depends(get_db), token: str = Depends(security.oauth2_scheme)
|
||||||
db=Depends(session.get_db), token: str = Depends(security.oauth2_scheme)
|
|
||||||
):
|
):
|
||||||
credentials_exception = HTTPException(
|
credentials_exception = HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
@@ -19,16 +21,28 @@ async def get_current_user(
|
|||||||
payload = jwt.decode(
|
payload = jwt.decode(
|
||||||
token, security.SECRET_KEY, algorithms=[security.ALGORITHM]
|
token, security.SECRET_KEY, algorithms=[security.ALGORITHM]
|
||||||
)
|
)
|
||||||
email: str = payload.get("sub")
|
id: int = payload.get("sub")
|
||||||
if email is None:
|
if id is None:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
|
|
||||||
|
tenant:str = payload.get("tenant")
|
||||||
|
if tenant is None:
|
||||||
|
raise credentials_exception
|
||||||
|
|
||||||
permissions: str = payload.get("permissions")
|
permissions: str = payload.get("permissions")
|
||||||
token_data = schemas.TokenData(email=email, permissions=permissions)
|
if not permissions =="ALL":
|
||||||
|
for scope in security_scopes.scopes:
|
||||||
|
if scope not in permissions.split(";"):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403, detail="The user doesn't have enough privileges"
|
||||||
|
)
|
||||||
|
token_data = schemas.TokenData(id = id, permissions=permissions)
|
||||||
except PyJWTError:
|
except PyJWTError:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
user = get_user_by_email(db, token_data.email)
|
user = userService.get_user(db, token_data.id)
|
||||||
if user is None:
|
if user is None:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
|
request.state.user = user.id
|
||||||
return user
|
return user
|
||||||
|
|
||||||
async def get_current_active_user(
|
async def get_current_active_user(
|
||||||
@@ -50,15 +64,15 @@ async def get_current_active_superuser(
|
|||||||
|
|
||||||
|
|
||||||
def authenticate_user(db, email: str, password: str):
|
def authenticate_user(db, email: str, password: str):
|
||||||
user = get_user_by_email(db, email)
|
user = userService.get_user_by_email(db,email) #get_user_by_email(db, email)
|
||||||
if not user:
|
if not user:
|
||||||
return False
|
return None
|
||||||
if not security.verify_password(password, user.hashed_password):
|
if not security.verify_password(password, user.hashed_password):
|
||||||
return False
|
return None
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
def sign_up_new_user(db, email: str, password: str):
|
def sign_up_new_user(db, email: str, password: str, firstname: str,lastname: str):
|
||||||
user = get_user_by_email(db, email)
|
user = get_user_by_email(db, email)
|
||||||
if user:
|
if user:
|
||||||
return False # User already exists
|
return False # User already exists
|
||||||
@@ -67,6 +81,8 @@ def sign_up_new_user(db, email: str, password: str):
|
|||||||
schemas.UserCreate(
|
schemas.UserCreate(
|
||||||
email=email,
|
email=email,
|
||||||
password=password,
|
password=password,
|
||||||
|
first_name = firstname,
|
||||||
|
last_name = lastname,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
is_superuser=False,
|
is_superuser=False,
|
||||||
),
|
),
|
||||||
|
|||||||
72
backend/app/core/cache.py
Normal file
72
backend/app/core/cache.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.db.cruddb import domainService,tenantService
|
||||||
|
from app.db.session import Database
|
||||||
|
|
||||||
|
class MemoryCache:
|
||||||
|
def __init__(self, max_cache_size: int = 100, ttl: int = 60):
|
||||||
|
self.cache = {}
|
||||||
|
self.max_cache_size = max_cache_size
|
||||||
|
self.ttl = ttl
|
||||||
|
|
||||||
|
def get(self, key: str) -> Any:
|
||||||
|
item = self.cache.get(key)
|
||||||
|
if item:
|
||||||
|
if time.time() - item['timestamp'] > self.ttl:
|
||||||
|
self.cache.pop(key)
|
||||||
|
return None
|
||||||
|
return item['value']
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set(self, key: str, value: Any) -> None:
|
||||||
|
if len(self.cache) >= self.max_cache_size:
|
||||||
|
self.cache.pop(next(iter(self.cache)))
|
||||||
|
self.cache[key] = {'value': value, 'timestamp': time.time()}
|
||||||
|
|
||||||
|
# def clear(self,key) -> None:
|
||||||
|
# self.cache.pop(key,None)
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
self.cache.clear()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class domainCache:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.memoryCache = MemoryCache(max_cache_size=50, ttl=120)
|
||||||
|
|
||||||
|
def set_default_domain(self, db: Session,userid: int,domainid:str):
|
||||||
|
domain = domainService.set_default_domain(db,userid,domainid)
|
||||||
|
if domain:
|
||||||
|
self.memoryCache.set(f"DOMAIN_{userid}",domain.url)
|
||||||
|
return domain
|
||||||
|
|
||||||
|
def get_default_domainurl(self,db: Session, userid: int):
|
||||||
|
if not self.memoryCache.get(f"DOMAIN_{userid}"):
|
||||||
|
domain = domainService.get_default_domain(db,userid)
|
||||||
|
if domain:
|
||||||
|
self.memoryCache.set(f"DOMAIN_{userid}",domain.url)
|
||||||
|
return self.memoryCache.get(f"DOMAIN_{userid}")
|
||||||
|
|
||||||
|
def clear_default_domainurl(self):
|
||||||
|
self.memoryCache.clear()
|
||||||
|
|
||||||
|
domainCacheService =domainCache()
|
||||||
|
|
||||||
|
|
||||||
|
class tenantCache:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.memoryCache = MemoryCache(max_cache_size=50, ttl=120)
|
||||||
|
|
||||||
|
def get_tenant_db(self,db: Session, tenantid: str):
|
||||||
|
if not self.memoryCache.get(f"TENANT_{tenantid}"):
|
||||||
|
tenant = tenantService.get_tenant(db,tenantid)
|
||||||
|
if tenant:
|
||||||
|
database = Database(tenant.db)
|
||||||
|
self.memoryCache.set(f"TENANT_{tenantid}",database)
|
||||||
|
return self.memoryCache.get(f"TENANT_{tenantid}")
|
||||||
|
|
||||||
|
tenantCacheService =tenantCache()
|
||||||
49
backend/app/core/common.py
Normal file
49
backend/app/core/common.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
|
||||||
|
import math
|
||||||
|
from fastapi import Query
|
||||||
|
from fastapi_pagination.bases import AbstractPage,AbstractParams,RawParams
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Any, Generic, List, Type,TypeVar,Generic,Sequence
|
||||||
|
from fastapi_pagination import Page,utils
|
||||||
|
|
||||||
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
class ApiReturnModel(BaseModel,Generic[T]):
|
||||||
|
code:int = 0
|
||||||
|
msg:str ="OK"
|
||||||
|
data:T
|
||||||
|
|
||||||
|
class ApiReturnError(BaseModel):
|
||||||
|
code:int = -1
|
||||||
|
msg:str =""
|
||||||
|
|
||||||
|
|
||||||
|
class Params(BaseModel, AbstractParams):
|
||||||
|
page:int = Query(1,get=1, description="Page number")
|
||||||
|
size:int = Query(20,get=0, le=100,description="Page size")
|
||||||
|
|
||||||
|
def to_raw_params(self) -> RawParams:
|
||||||
|
return RawParams(
|
||||||
|
limit=self.size,
|
||||||
|
offset=self.size*(self.page-1)
|
||||||
|
)
|
||||||
|
|
||||||
|
class ApiReturnPage(AbstractPage[T],Generic[T]):
|
||||||
|
code:int =0
|
||||||
|
msg:str ="OK"
|
||||||
|
data:Sequence[T]
|
||||||
|
total:int
|
||||||
|
page:int
|
||||||
|
size:int
|
||||||
|
# next:str
|
||||||
|
# previous:str
|
||||||
|
total_pages:int
|
||||||
|
|
||||||
|
__params_type__ =Params
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls,items:Sequence[T],params:Params,**kwargs: Any) -> Type[Page[T]]:
|
||||||
|
total = kwargs.get('total', 0)
|
||||||
|
total_pages = math.ceil(total/params.size)
|
||||||
|
|
||||||
|
return utils.create_pydantic_model(cls,data=items,total=total,page=params.page,size=params.size,total_pages=total_pages)
|
||||||
@@ -1,18 +1,42 @@
|
|||||||
import os
|
import os
|
||||||
|
import base64
|
||||||
|
|
||||||
|
|
||||||
PROJECT_NAME = "KintoneAppBuilder"
|
PROJECT_NAME = "KintoneAppBuilder"
|
||||||
|
|
||||||
SQLALCHEMY_DATABASE_URI = "mssql+pymssql://maxz64@maxzdb:m@xz1205@maxzdb.database.windows.net/alloc"
|
#SQLALCHEMY_DATABASE_URI = "postgres://kabAdmin:P@ssw0rd!@kintonetooldb.postgres.database.azure.com/dev"
|
||||||
|
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://kabAdmin:P%40ssw0rd!@kintonetooldb.postgres.database.azure.com/dev_v2"
|
||||||
BASE_URL = "https://mfu07rkgnb7c.cybozu.com"
|
#SQLALCHEMY_DATABASE_URI = "postgres://kabAdmin:P@ssw0rd!@kintonetooldb.postgres.database.azure.com/test"
|
||||||
|
#SQLALCHEMY_DATABASE_URI = "postgres://kabAdmin:P@ssw0rd!@ktune-prod-db.postgres.database.azure.com/postgres"
|
||||||
API_V1_STR = "/k/v1"
|
API_V1_STR = "/k/v1"
|
||||||
|
|
||||||
API_V1_AUTH_KEY = "X-Cybozu-Authorization"
|
API_V1_AUTH_KEY = "X-Cybozu-Authorization"
|
||||||
|
|
||||||
API_V1_AUTH_VALUE = "TVhaOm1heHoxMjA1"
|
DEPLOY_MODE = "PROD" #DEV,PROD
|
||||||
|
|
||||||
KINTONE_USER = "MXZ"
|
DEPLOY_JS_URL = "https://ka-addin.azurewebsites.net/alc_runtime.js"
|
||||||
|
|
||||||
KINTONE_FIELD_TYPE=["GROUP","GROUP_SELECT","CHECK_BOX","SUBTABLE","DROP_DOWN","USER_SELECT","RADIO_BUTTON","RICH_TEXT","LINK","REFERENCE_TABLE","CALC","TIME","NUMBER","ORGANIZATION_SELECT","FILE","DATETIME","DATE","MULTI_SELECT","SINGLE_LINE_TEXT","MULTI_LINE_TEXT"]
|
KINTONE_FIELD_TYPE=["GROUP","GROUP_SELECT","CHECK_BOX","SUBTABLE","DROP_DOWN","USER_SELECT","RADIO_BUTTON","RICH_TEXT","LINK","REFERENCE_TABLE","CALC","TIME","NUMBER","ORGANIZATION_SELECT","FILE","DATETIME","DATE","MULTI_SELECT","SINGLE_LINE_TEXT","MULTI_LINE_TEXT"]
|
||||||
|
|
||||||
|
KINTONE_FIELD_PROPERTY=['label','code','type','required','unique','maxValue','minValue','maxLength','minLength','defaultValue','defaultNowValue','options','expression','hideExpression','digit','protocol','displayScale','unit','unitPosition']
|
||||||
|
|
||||||
|
KINTONE_PSW_CRYPTO_KEY=bytes.fromhex("53 6c 93 bd 48 ad b5 c0 93 df a1 27 25 a1 a3 32 a2 03 3b a0 27 1f 51 dc 20 0e 6c d7 be fc fb ea")
|
||||||
|
|
||||||
|
class KINTONE_ENV:
|
||||||
|
|
||||||
|
BASE_URL = ""
|
||||||
|
|
||||||
|
API_V1_AUTH_VALUE = ""
|
||||||
|
|
||||||
|
KINTONE_USER = ""
|
||||||
|
|
||||||
|
DOMAIN_ID = ""
|
||||||
|
|
||||||
|
DOMAIN_NAME =""
|
||||||
|
|
||||||
|
def __init__(self,domain) -> None:
|
||||||
|
self.DOMAIN_NAME=domain.name
|
||||||
|
self.DOMAIN_ID=domain.id
|
||||||
|
self.BASE_URL = domain.url
|
||||||
|
self.KINTONE_USER = domain.kintoneuser
|
||||||
|
self.API_V1_AUTH_VALUE = base64.b64encode(bytes(f"{domain.kintoneuser}:{domain.decrypt_kintonepwd()}","utf-8"))
|
||||||
20
backend/app/core/dbmanager.py
Normal file
20
backend/app/core/dbmanager.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
from fastapi import Depends,Request
|
||||||
|
from app.db.session import get_tenant_db
|
||||||
|
from app.core import tenantCacheService
|
||||||
|
from app.db.session import tenantdb
|
||||||
|
|
||||||
|
def get_db(request: Request,tenant:str = "1",tenantdb = Depends(get_tenant_db)):
|
||||||
|
database = tenantCacheService.get_tenant_db(tenantdb,tenant)
|
||||||
|
try:
|
||||||
|
db = database.get_db()
|
||||||
|
request.state.tenant = tenant
|
||||||
|
request.state.db = db
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_log_db():
|
||||||
|
db = tenantdb.get_db()
|
||||||
|
return db
|
||||||
131
backend/app/core/operation.py
Normal file
131
backend/app/core/operation.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
|
||||||
|
from urllib.parse import parse_qs, urlencode
|
||||||
|
from fastapi import Request
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.db.models import OperationLog,User
|
||||||
|
from app.core.apiexception import APIException
|
||||||
|
from app.core.dbmanager import get_log_db
|
||||||
|
from app.db.crud import create_log
|
||||||
|
import json
|
||||||
|
|
||||||
|
class LoggingMiddleware(BaseHTTPMiddleware):
|
||||||
|
async def dispatch(self, request: Request, call_next):
|
||||||
|
if request.method in ("POST", "PUT", "PATCH","DELETE"):
|
||||||
|
content_type = request.headers.get('content-type', '')
|
||||||
|
if content_type.startswith('multipart/form-data'):
|
||||||
|
request.state.body = None
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
request.state.body = await request.json()
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
request.state.body = await request.body()
|
||||||
|
else:
|
||||||
|
request.state.body = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await call_next(request)
|
||||||
|
state = request.state
|
||||||
|
except Exception as e:
|
||||||
|
await self.log_error(request, e)
|
||||||
|
response = JSONResponse(
|
||||||
|
content={"detail": "Internal Server Error"},
|
||||||
|
status_code=500
|
||||||
|
)
|
||||||
|
|
||||||
|
if hasattr(request.state, "user") and hasattr(request.state, "tenant"):
|
||||||
|
await self.log_request(request, response,state)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def sanitize_password(self,data):
|
||||||
|
"""
|
||||||
|
データ内の password パラメータをフィルタリングする機能。
|
||||||
|
dict、JSON 文字列、URL エンコード文字列、QueryDict をサポート。
|
||||||
|
"""
|
||||||
|
if data is None:
|
||||||
|
return data
|
||||||
|
elif isinstance(data, dict):
|
||||||
|
data.pop('password', None)
|
||||||
|
return data
|
||||||
|
elif isinstance(data, list):
|
||||||
|
return [self.sanitize_password(item) for item in data]
|
||||||
|
elif isinstance(data, (str, bytes)):
|
||||||
|
if isinstance(data, bytes):
|
||||||
|
data = data.decode('utf-8') # bytes to str
|
||||||
|
# JSON解析
|
||||||
|
try:
|
||||||
|
parsed_json = json.loads(data)
|
||||||
|
sanitized_json = self.sanitize_password(parsed_json)
|
||||||
|
return json.dumps(sanitized_json, separators=(',', ':'))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# URL 解析
|
||||||
|
try:
|
||||||
|
parsed_dict = parse_qs(data)
|
||||||
|
parsed_dict.pop('password', None)
|
||||||
|
return urlencode(parsed_dict, doseq=True)
|
||||||
|
except:
|
||||||
|
parts = data.split('&')
|
||||||
|
filtered_parts = []
|
||||||
|
for part in parts:
|
||||||
|
if '=' in part:
|
||||||
|
key, _ = part.split('=', 1)
|
||||||
|
if key == 'password':
|
||||||
|
continue
|
||||||
|
filtered_parts.append(part)
|
||||||
|
return '&'.join(filtered_parts)
|
||||||
|
# QueryDict 例えば FastAPI の request.query_params)
|
||||||
|
elif hasattr(data, 'items'):
|
||||||
|
return {k: v for k, v in data.items() if k != 'password'}
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def log_request(self, request: Request, response,state):
|
||||||
|
try:
|
||||||
|
headers = dict(request.headers)
|
||||||
|
route = request.scope.get("route")
|
||||||
|
if route:
|
||||||
|
path_template = route.path
|
||||||
|
else:
|
||||||
|
path_template = request.url.path
|
||||||
|
|
||||||
|
# passwordのパラメータを除外する
|
||||||
|
safe_query = self.sanitize_password(request.query_params.items())
|
||||||
|
|
||||||
|
# passwordのパラメータを除外する
|
||||||
|
safe_body = self.sanitize_password(request.state.body)
|
||||||
|
|
||||||
|
db_operation = OperationLog(
|
||||||
|
tenantid =request.state.tenant,
|
||||||
|
clientip = request.client.host if request.client else None,
|
||||||
|
useragent =headers.get("user-agent", ""),
|
||||||
|
userid = request.state.user,
|
||||||
|
operation = request.method,
|
||||||
|
function = path_template,
|
||||||
|
parameters = str({
|
||||||
|
"path": request.path_params,
|
||||||
|
"query": safe_query,
|
||||||
|
"body": safe_body
|
||||||
|
}),
|
||||||
|
response = f"status_code:{response.status_code }" )
|
||||||
|
|
||||||
|
db = request.state.db
|
||||||
|
if db:
|
||||||
|
await self.write_log_to_db(db_operation,db)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Logging failed: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
async def log_error(self, request: Request, e: Exception):
|
||||||
|
exc = APIException('operation:dispatch',request.url._url,f"Error occurred while writting operation log:",e)
|
||||||
|
db = get_log_db()
|
||||||
|
try:
|
||||||
|
create_log(db,exc.error)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
async def write_log_to_db(self, db_operation,db):
|
||||||
|
db.add(db_operation)
|
||||||
|
db.commit()
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import jwt
|
import jwt
|
||||||
from fastapi.security import OAuth2PasswordBearer
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta,timezone
|
||||||
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
|
||||||
|
import os
|
||||||
|
import base64
|
||||||
|
from app.core import config
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/token")
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/token")
|
||||||
|
|
||||||
@@ -9,7 +13,7 @@ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|||||||
|
|
||||||
SECRET_KEY = "alicorns"
|
SECRET_KEY = "alicorns"
|
||||||
ALGORITHM = "HS256"
|
ALGORITHM = "HS256"
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
ACCESS_TOKEN_EXPIRE_MINUTES = 2880
|
||||||
|
|
||||||
|
|
||||||
def get_password_hash(password: str) -> str:
|
def get_password_hash(password: str) -> str:
|
||||||
@@ -23,9 +27,38 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
|
|||||||
def create_access_token(*, data: dict, expires_delta: timedelta = None):
|
def create_access_token(*, data: dict, expires_delta: timedelta = None):
|
||||||
to_encode = data.copy()
|
to_encode = data.copy()
|
||||||
if expires_delta:
|
if expires_delta:
|
||||||
expire = datetime.utcnow() + expires_delta
|
expire = datetime.now(timezone.utc) + expires_delta
|
||||||
else:
|
else:
|
||||||
expire = datetime.utcnow() + timedelta(minutes=15)
|
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
to_encode.update({"exp": expire})
|
to_encode.update({"exp": expire})
|
||||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||||
return encoded_jwt
|
return encoded_jwt
|
||||||
|
|
||||||
|
def chacha20Encrypt(plaintext:str, key=config.KINTONE_PSW_CRYPTO_KEY):
|
||||||
|
if plaintext is None or plaintext == '':
|
||||||
|
return None
|
||||||
|
nonce = os.urandom(16)
|
||||||
|
algorithm = algorithms.ChaCha20(key, nonce)
|
||||||
|
cipher = Cipher(algorithm, mode=None)
|
||||||
|
encryptor = cipher.encryptor()
|
||||||
|
ciphertext = encryptor.update(plaintext.encode('utf-8')) + encryptor.finalize()
|
||||||
|
return base64.b64encode(nonce +'𒀸'.encode('utf-8')+ ciphertext).decode('utf-8')
|
||||||
|
|
||||||
|
def chacha20Decrypt(encoded_str:str, key=config.KINTONE_PSW_CRYPTO_KEY):
|
||||||
|
try:
|
||||||
|
decoded_data = base64.b64decode(encoded_str)
|
||||||
|
if len(decoded_data) < 18:
|
||||||
|
return encoded_str
|
||||||
|
special_char = decoded_data[16:20]
|
||||||
|
if special_char != '𒀸'.encode('utf-8'):
|
||||||
|
return encoded_str
|
||||||
|
nonce = decoded_data[:16]
|
||||||
|
ciphertext = decoded_data[20:]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An error occurred: {e}")
|
||||||
|
return encoded_str
|
||||||
|
algorithm = algorithms.ChaCha20(key, nonce)
|
||||||
|
cipher = Cipher(algorithm, mode=None)
|
||||||
|
decryptor = cipher.decryptor()
|
||||||
|
plaintext_bytes = decryptor.update(ciphertext) + decryptor.finalize()
|
||||||
|
return plaintext_bytes.decode('utf-8')
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
from datetime import datetime
|
||||||
from fastapi import HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import and_
|
||||||
import typing as t
|
import typing as t
|
||||||
|
|
||||||
from . import models, schemas
|
from . import models, schemas
|
||||||
from app.core.security import get_password_hash
|
from app.core.security import chacha20Decrypt, get_password_hash
|
||||||
|
|
||||||
|
|
||||||
def get_user(db: Session, user_id: int):
|
def get_user(db: Session, user_id: int):
|
||||||
@@ -17,10 +19,15 @@ def get_user_by_email(db: Session, email: str) -> schemas.UserBase:
|
|||||||
return db.query(models.User).filter(models.User.email == email).first()
|
return db.query(models.User).filter(models.User.email == email).first()
|
||||||
|
|
||||||
|
|
||||||
def get_users(
|
def get_allusers(
|
||||||
db: Session, skip: int = 0, limit: int = 100
|
db: Session
|
||||||
) -> t.List[schemas.UserOut]:
|
) -> t.List[schemas.UserOut]:
|
||||||
return db.query(models.User).offset(skip).limit(limit).all()
|
return db.query(models.User).all()
|
||||||
|
|
||||||
|
def get_users(
|
||||||
|
db: Session
|
||||||
|
) -> t.List[schemas.UserOut]:
|
||||||
|
return db.query(models.User).filter(models.User.is_superuser == False)
|
||||||
|
|
||||||
|
|
||||||
def create_user(db: Session, user: schemas.UserCreate):
|
def create_user(db: Session, user: schemas.UserCreate):
|
||||||
@@ -69,6 +76,80 @@ def edit_user(
|
|||||||
return db_user
|
return db_user
|
||||||
|
|
||||||
|
|
||||||
|
def get_roles(
|
||||||
|
db: Session
|
||||||
|
) -> t.List[schemas.RoleBase]:
|
||||||
|
return db.query(models.Role).all()
|
||||||
|
|
||||||
|
def assign_userrole( db: Session, user_id: int, roles: t.List[int]):
|
||||||
|
db_user = db.query(models.User).get(user_id)
|
||||||
|
if db_user:
|
||||||
|
for role in db_user.roles:
|
||||||
|
db_user.roles.remove(role)
|
||||||
|
for roleid in roles:
|
||||||
|
role = db.query(models.Role).get(roleid)
|
||||||
|
if role:
|
||||||
|
db_user.roles.append(role)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_user)
|
||||||
|
return db_user
|
||||||
|
|
||||||
|
def get_apps(
|
||||||
|
db: Session,
|
||||||
|
domainurl:str
|
||||||
|
) -> t.List[schemas.AppList]:
|
||||||
|
return db.query(models.App).filter(models.App.domainurl == domainurl).all()
|
||||||
|
|
||||||
|
def update_appversion(db: Session, appedit: schemas.AppVersion,userid:int):
|
||||||
|
db_app = db.query(models.App).filter(and_(models.App.domainurl == appedit.domainurl,models.App.appid == appedit.appid)).first()
|
||||||
|
if not db_app:
|
||||||
|
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||||
|
|
||||||
|
db_app.version = db_app.version + 1
|
||||||
|
appversion = models.AppVersion(
|
||||||
|
domainurl = appedit.domainurl,
|
||||||
|
appid=appedit.appid,
|
||||||
|
appname=db_app.appname,
|
||||||
|
version = db_app.version,
|
||||||
|
versionname = appedit.versionname,
|
||||||
|
comment = appedit.comment,
|
||||||
|
updateuserid = userid,
|
||||||
|
createuserid = userid
|
||||||
|
)
|
||||||
|
db.add(appversion)
|
||||||
|
db.add(db_app)
|
||||||
|
|
||||||
|
flows = db.query(models.Flow).filter(and_(models.Flow.domainurl == appedit.domainurl,models.App.appid == appedit.appid))
|
||||||
|
for flow in flows:
|
||||||
|
db_flowhistory = models.FlowHistory(
|
||||||
|
flowid = flow.flowid,
|
||||||
|
appid = flow.appid,
|
||||||
|
eventid = flow.eventid,
|
||||||
|
domainurl = flow.domainurl,
|
||||||
|
name = flow.name,
|
||||||
|
content = flow.content,
|
||||||
|
createuser = userid,
|
||||||
|
version = db_app.version,
|
||||||
|
updateuserid = userid,
|
||||||
|
createuserid = userid
|
||||||
|
)
|
||||||
|
db.add(db_flowhistory)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_app)
|
||||||
|
return db_app
|
||||||
|
|
||||||
|
def delete_apps(db: Session, domainurl: str,appid: str ):
|
||||||
|
db_app = db.query(models.App).filter(and_(models.App.domainurl == domainurl,models.App.appid ==appid)).first()
|
||||||
|
if not db_app:
|
||||||
|
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="App not found")
|
||||||
|
db.delete(db_app)
|
||||||
|
db_flows = db.query(models.Flow).filter(and_(models.Flow.domainurl == domainurl,models.Flow.appid ==appid))
|
||||||
|
for flow in db_flows:
|
||||||
|
db.delete(flow)
|
||||||
|
db.commit()
|
||||||
|
return db_app
|
||||||
|
|
||||||
def get_appsetting(db: Session, id: int):
|
def get_appsetting(db: Session, id: int):
|
||||||
app = db.query(models.AppSetting).get(id)
|
app = db.query(models.AppSetting).get(id)
|
||||||
if not app:
|
if not app:
|
||||||
@@ -115,4 +196,271 @@ def get_kintones(db: Session, type: int):
|
|||||||
kintones = db.query(models.Kintone).filter(models.Kintone.type == type).all()
|
kintones = db.query(models.Kintone).filter(models.Kintone.type == type).all()
|
||||||
if not kintones:
|
if not kintones:
|
||||||
raise HTTPException(status_code=404, detail="Data not found")
|
raise HTTPException(status_code=404, detail="Data not found")
|
||||||
return kintones
|
return kintones
|
||||||
|
|
||||||
|
def get_actions(db: Session):
|
||||||
|
actions = db.query(models.Action).all()
|
||||||
|
if not actions:
|
||||||
|
raise HTTPException(status_code=404, detail="Data not found")
|
||||||
|
return actions
|
||||||
|
|
||||||
|
|
||||||
|
def create_flow(db: Session, domainurl: str, flow: schemas.FlowIn,userid:int):
|
||||||
|
db_flow = models.Flow(
|
||||||
|
flowid=flow.flowid,
|
||||||
|
appid=flow.appid,
|
||||||
|
eventid=flow.eventid,
|
||||||
|
domainurl=domainurl,
|
||||||
|
name=flow.name,
|
||||||
|
content=flow.content,
|
||||||
|
createuserid = userid,
|
||||||
|
updateuserid = userid
|
||||||
|
)
|
||||||
|
db.add(db_flow)
|
||||||
|
db_app = db.query(models.App).filter(and_(models.App.domainurl == domainurl,models.App.appid == flow.appid)).first()
|
||||||
|
if not db_app:
|
||||||
|
db_app = models.App(
|
||||||
|
domainurl = domainurl,
|
||||||
|
appid=flow.appid,
|
||||||
|
appname=flow.appname,
|
||||||
|
version = 0,
|
||||||
|
createuserid= userid,
|
||||||
|
updateuserid = userid
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_flow)
|
||||||
|
return db_flow
|
||||||
|
|
||||||
|
def delete_flow(db: Session, flowid: str):
|
||||||
|
flow = get_flow(db, flowid)
|
||||||
|
if not flow:
|
||||||
|
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Flow not found")
|
||||||
|
db.delete(flow)
|
||||||
|
db.commit()
|
||||||
|
return flow
|
||||||
|
|
||||||
|
|
||||||
|
def edit_flow(
|
||||||
|
db: Session, domainurl: str, flow: schemas.FlowIn,userid:int
|
||||||
|
) -> schemas.Flow:
|
||||||
|
db_flow = get_flow(db, flow.flowid)
|
||||||
|
if not db_flow:
|
||||||
|
#見つからない時新規作成
|
||||||
|
return create_flow(db,domainurl,flow,userid)
|
||||||
|
|
||||||
|
db_flow.appid =flow.appid
|
||||||
|
db_flow.eventid=flow.eventid
|
||||||
|
db_flow.domainurl=domainurl
|
||||||
|
db_flow.name=flow.name
|
||||||
|
db_flow.content=flow.content
|
||||||
|
db_flow.updateuserid = userid
|
||||||
|
|
||||||
|
db.add(db_flow)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_flow)
|
||||||
|
return db_flow
|
||||||
|
|
||||||
|
|
||||||
|
def get_flows(db: Session, flowid: str):
|
||||||
|
flows = db.query(models.Flow).all()
|
||||||
|
if not flows:
|
||||||
|
raise HTTPException(status_code=404, detail="Data not found")
|
||||||
|
return flows
|
||||||
|
|
||||||
|
def get_flow(db: Session, flowid: str):
|
||||||
|
flow = db.query(models.Flow).filter(models.Flow.flowid == flowid).first()
|
||||||
|
# if not flow:
|
||||||
|
# raise HTTPException(status_code=404, detail="Data not found")
|
||||||
|
return flow
|
||||||
|
|
||||||
|
def get_flows_by_app(db: Session,domainurl: str, appid: str):
|
||||||
|
flows = db.query(models.Flow).filter(and_(models.Flow.domainurl == domainurl,models.Flow.appid == appid)).all()
|
||||||
|
if not flows:
|
||||||
|
raise Exception("Data not found")
|
||||||
|
return flows
|
||||||
|
|
||||||
|
def create_domain(db: Session, domain: schemas.DomainIn,userid:int):
|
||||||
|
domain.encrypt_kintonepwd()
|
||||||
|
db_domain = models.Domain(
|
||||||
|
tenantid = domain.tenantid,
|
||||||
|
name=domain.name,
|
||||||
|
url=domain.url,
|
||||||
|
is_active=domain.is_active,
|
||||||
|
kintoneuser=domain.kintoneuser,
|
||||||
|
kintonepwd=domain.kintonepwd,
|
||||||
|
createuserid = userid,
|
||||||
|
updateuserid = userid,
|
||||||
|
ownerid = domain.ownerid
|
||||||
|
)
|
||||||
|
db.add(db_domain)
|
||||||
|
#add_userdomain(db,userid,db_domain.id)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_domain)
|
||||||
|
return db_domain
|
||||||
|
|
||||||
|
def delete_domain(db: Session,id: int):
|
||||||
|
db_domain = db.query(models.Domain).get(id)
|
||||||
|
#if not db_domain:
|
||||||
|
# raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
|
||||||
|
if db_domain:
|
||||||
|
db.delete(db_domain)
|
||||||
|
db.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def edit_domain(
|
||||||
|
db: Session, domain: schemas.DomainIn,userid:int
|
||||||
|
) -> schemas.Domain:
|
||||||
|
if domain.kintonepwd != "":
|
||||||
|
domain.encrypt_kintonepwd()
|
||||||
|
db_domain = db.query(models.Domain).get(domain.id)
|
||||||
|
if not db_domain:
|
||||||
|
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
|
||||||
|
db_domain.tenantid = domain.tenantid
|
||||||
|
db_domain.name=domain.name
|
||||||
|
db_domain.url=domain.url
|
||||||
|
if db_domain.is_active == True and domain.is_active == False:
|
||||||
|
db_userdomains = db.query(models.UserDomain).filter(and_(models.UserDomain.domainid == db_domain.id,models.UserDomain.active == True)).all()
|
||||||
|
for userdomain in db_userdomains:
|
||||||
|
userdomain.active = False
|
||||||
|
db.add(userdomain)
|
||||||
|
db_domain.is_active=domain.is_active
|
||||||
|
db_domain.kintoneuser=domain.kintoneuser
|
||||||
|
if domain.kintonepwd != "":
|
||||||
|
db_domain.kintonepwd = domain.kintonepwd
|
||||||
|
db_domain.updateuserid = userid
|
||||||
|
db_domain.ownerid = domain.ownerid
|
||||||
|
db.add(db_domain)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_domain)
|
||||||
|
return db_domain
|
||||||
|
|
||||||
|
|
||||||
|
def add_admindomain(db: Session,userid:int,domainid:int):
|
||||||
|
db_domain = db.query(models.Domain).filter(and_(models.Domain.id == domainid,models.Domain.is_active)).first()
|
||||||
|
if db_domain:
|
||||||
|
user_domain = models.UserDomain(userid = userid, domainid = domainid )
|
||||||
|
db.add(user_domain)
|
||||||
|
db.commit()
|
||||||
|
return db_domain
|
||||||
|
|
||||||
|
def add_userdomain(db: Session,ownerid:int, userid:int,domainid:int):
|
||||||
|
db_domain = db.query(models.Domain).filter(and_(models.Domain.id == domainid,models.Domain.ownerid == ownerid,models.Domain.is_active)).first()
|
||||||
|
if db_domain:
|
||||||
|
user_domain = models.UserDomain(userid = userid, domainid = domainid )
|
||||||
|
db.add(user_domain)
|
||||||
|
db.commit()
|
||||||
|
return db_domain
|
||||||
|
|
||||||
|
def add_userdomains(db: Session, userid:int,domainids:list[str]):
|
||||||
|
dbCommits = list(map(lambda domainid: models.UserDomain(userid = userid, domainid = domainid ), domainids))
|
||||||
|
db.bulk_save_objects(dbCommits)
|
||||||
|
db.commit()
|
||||||
|
return dbCommits
|
||||||
|
|
||||||
|
def delete_userdomain(db: Session, userid: int,domainid: int):
|
||||||
|
db_domain = db.query(models.UserDomain).filter(and_(models.UserDomain.userid == userid,models.UserDomain.domainid == domainid)).first()
|
||||||
|
#if not db_domain:
|
||||||
|
# raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
|
||||||
|
if db_domain:
|
||||||
|
db.delete(db_domain)
|
||||||
|
db.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def active_userdomain(db: Session, userid: int,domainid: int):
|
||||||
|
db_domain = db.query(models.Domain).filter(and_(models.Domain.id == domainid,models.Domain.is_active)).first()
|
||||||
|
if db_domain:
|
||||||
|
db_userdomains = db.query(models.UserDomain).filter(models.UserDomain.userid == userid).all()
|
||||||
|
# if not db_userdomains:
|
||||||
|
# raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
|
||||||
|
|
||||||
|
for domain in db_userdomains:
|
||||||
|
if domain.domainid == domainid:
|
||||||
|
domain.active = True
|
||||||
|
else:
|
||||||
|
domain.active = False
|
||||||
|
db.add(domain)
|
||||||
|
db.commit()
|
||||||
|
return db_domain
|
||||||
|
|
||||||
|
def get_activedomain(db: Session, userid: int):
|
||||||
|
# user_domains = (db.query(models.Domain,models.UserDomain.active)
|
||||||
|
# .join(models.UserDomain,models.UserDomain.domainid == models.Domain.id )
|
||||||
|
# .filter(models.UserDomain.userid == userid)
|
||||||
|
# .all())
|
||||||
|
db_domain=(db.query(models.Domain).filter(models.Domain.is_active)
|
||||||
|
.join(models.UserDomain,models.UserDomain.domainid == models.Domain.id).filter(and_(models.UserDomain.active,models.UserDomain.userid == userid)).first())
|
||||||
|
# if len(user_domains)==1:
|
||||||
|
# db_domain = user_domains[0][0];
|
||||||
|
# else:
|
||||||
|
# db_domain = next((domain for domain,active in user_domains if active),None)
|
||||||
|
# raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
|
||||||
|
return db_domain
|
||||||
|
|
||||||
|
def get_domain(db: Session, userid: str):
|
||||||
|
domains = db.query(models.Domain).join(models.UserDomain,models.UserDomain.domainid == models.Domain.id ).filter(models.UserDomain.userid == userid).all()
|
||||||
|
# if not domains:
|
||||||
|
# raise HTTPException(status_code=404, detail="Data not found")
|
||||||
|
# for domain in domains:
|
||||||
|
# decrypted_pwd = chacha20Decrypt(domain.kintonepwd)
|
||||||
|
# domain.kintonepwd = decrypted_pwd
|
||||||
|
return domains
|
||||||
|
|
||||||
|
def get_alldomains(db: Session):
|
||||||
|
domains = db.query(models.Domain).all()
|
||||||
|
return domains
|
||||||
|
|
||||||
|
def get_domains(db: Session,userid:int):
|
||||||
|
domains = db.query(models.Domain).filter(models.Domain.ownerid == userid ).all()
|
||||||
|
return domains
|
||||||
|
|
||||||
|
def get_events(db: Session):
|
||||||
|
events = db.query(models.Event).all()
|
||||||
|
if not events:
|
||||||
|
raise HTTPException(status_code=404, detail="Data not found")
|
||||||
|
return events
|
||||||
|
|
||||||
|
def get_category(db:Session):
|
||||||
|
categorys=db.query(models.Category).all()
|
||||||
|
return categorys
|
||||||
|
|
||||||
|
def get_eventactions(db: Session,eventid: str):
|
||||||
|
#eveactions = db.query(models.Action).join(models.EventAction,models.EventAction.actionid == models.Action.id ).join(models.Event,models.Event.id == models.EventAction.eventid).filter(models.Event.eventid == eventid).all()
|
||||||
|
#category = get_category(db)
|
||||||
|
blackactions = (
|
||||||
|
db.query(models.EventAction.actionid)
|
||||||
|
.filter(models.EventAction.eventid == eventid)
|
||||||
|
.subquery()
|
||||||
|
)
|
||||||
|
eveactions = (
|
||||||
|
db.query(
|
||||||
|
models.Action.id,
|
||||||
|
models.Action.name,
|
||||||
|
models.Action.title,
|
||||||
|
models.Action.subtitle,
|
||||||
|
models.Action.outputpoints,
|
||||||
|
models.Action.property,
|
||||||
|
models.Action.categoryid,
|
||||||
|
models.Action.nosort,
|
||||||
|
models.Category.categoryname)
|
||||||
|
.join(models.Category,models.Category.id == models.Action.categoryid)
|
||||||
|
.filter(models.Action.id.notin_(blackactions))
|
||||||
|
.order_by(models.Category.nosort,models.Action.nosort)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not eveactions:
|
||||||
|
raise HTTPException(status_code=404, detail="Data not found")
|
||||||
|
return eveactions
|
||||||
|
|
||||||
|
|
||||||
|
def create_log(db: Session, error:schemas.ErrorCreate):
|
||||||
|
db_log = models.ErrorLog(title=error.title,location=error.location,content=error.content)
|
||||||
|
db.add(db_log)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_log)
|
||||||
|
return db_log
|
||||||
|
|
||||||
|
def get_kintoneformat(db: Session):
|
||||||
|
formats = db.query(models.KintoneFormat).order_by(models.KintoneFormat.id).all()
|
||||||
|
return formats
|
||||||
4
backend/app/db/cruddb/__init__.py
Normal file
4
backend/app/db/cruddb/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from app.db.cruddb.dbuser import userService
|
||||||
|
from app.db.cruddb.dbdomain import domainService
|
||||||
|
from app.db.cruddb.dbapp import appService
|
||||||
|
from app.db.cruddb.dbtenant import tenantService
|
||||||
104
backend/app/db/cruddb/crudbase.py
Normal file
104
backend/app/db/cruddb/crudbase.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
from sqlalchemy import asc, desc, select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy.orm.query import Query
|
||||||
|
from typing import Type, List, Optional
|
||||||
|
from app.core.common import ApiReturnPage
|
||||||
|
from sqlalchemy import and_ ,or_
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from app.db import models
|
||||||
|
|
||||||
|
class crudbase:
|
||||||
|
def __init__(self, model: Type[models.Base]):
|
||||||
|
self.model = model
|
||||||
|
|
||||||
|
def _apply_filters(self, query: Query, filters: dict) -> Query:
|
||||||
|
and_conditions = []
|
||||||
|
or_conditions = []
|
||||||
|
for column_name, value in filters.items():
|
||||||
|
column = getattr(self.model, column_name, None)
|
||||||
|
if column:
|
||||||
|
if isinstance(value, dict):
|
||||||
|
if 'operator' in value:
|
||||||
|
operator = value['operator']
|
||||||
|
filter_value = value['value']
|
||||||
|
if operator == '!=':
|
||||||
|
and_conditions.append(column != filter_value)
|
||||||
|
elif operator == 'like':
|
||||||
|
and_conditions.append(column.like(f"%{filter_value}%"))
|
||||||
|
elif operator == '=':
|
||||||
|
and_conditions.append(column == filter_value)
|
||||||
|
elif operator == '>':
|
||||||
|
and_conditions.append(column > filter_value)
|
||||||
|
elif operator == '>=':
|
||||||
|
and_conditions.append(column >= filter_value)
|
||||||
|
elif operator == '<':
|
||||||
|
and_conditions.append(column < filter_value)
|
||||||
|
elif operator == '<=':
|
||||||
|
and_conditions.append(column <= filter_value)
|
||||||
|
elif operator == 'in':
|
||||||
|
if isinstance(filter_value, list):
|
||||||
|
or_conditions.append(column.in_(filter_value))
|
||||||
|
else:
|
||||||
|
and_conditions.append(column == filter_value)
|
||||||
|
else:
|
||||||
|
and_conditions.append(column == value)
|
||||||
|
else:
|
||||||
|
and_conditions.append(column == value)
|
||||||
|
|
||||||
|
if and_conditions:
|
||||||
|
query = query.where(and_(*and_conditions))
|
||||||
|
if or_conditions:
|
||||||
|
query = query.where(or_(*or_conditions))
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
def _apply_sorting(self, query: Query, sort_by: Optional[str], sort_order: Optional[str]) -> Query:
|
||||||
|
if sort_by:
|
||||||
|
column = getattr(self.model, sort_by, None)
|
||||||
|
if column:
|
||||||
|
if sort_order == "desc":
|
||||||
|
query = query.order_by(desc(column))
|
||||||
|
else:
|
||||||
|
query = query.order_by(asc(column))
|
||||||
|
return query
|
||||||
|
|
||||||
|
def get_all(self) -> Query:
|
||||||
|
return select(self.model)
|
||||||
|
|
||||||
|
def get(self, db: Session, item_id: int) -> Optional[models.Base]:
|
||||||
|
return db.execute(select(self.model).filter(self.model.id == item_id)).scalar_one_or_none()
|
||||||
|
|
||||||
|
def create(self, db: Session, obj_in: BaseModel) -> models.Base:
|
||||||
|
db_obj = self.model(**obj_in.model_dump())
|
||||||
|
db.add(db_obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_obj)
|
||||||
|
return db_obj
|
||||||
|
|
||||||
|
def update(self, db: Session, item_id: int, obj_in: BaseModel) -> Optional[models.Base]:
|
||||||
|
db_obj = self.get(db,item_id)
|
||||||
|
if db_obj:
|
||||||
|
for key, value in obj_in.model_dump(exclude_unset=True).items():
|
||||||
|
setattr(db_obj, key, value)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_obj)
|
||||||
|
return db_obj
|
||||||
|
return None
|
||||||
|
|
||||||
|
def delete(self, db: Session, item_id: int) -> Optional[models.Base]:
|
||||||
|
db_obj = self.get(db,item_id)
|
||||||
|
if db_obj:
|
||||||
|
db.delete(db_obj)
|
||||||
|
db.commit()
|
||||||
|
return db_obj
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_by_conditions(self, filters: Optional[dict] = None, sort_by: Optional[str] = None,
|
||||||
|
sort_order: Optional[str] = "asc") -> Query:
|
||||||
|
query = select(self.model)
|
||||||
|
if filters:
|
||||||
|
query = self._apply_filters(query, filters)
|
||||||
|
if sort_by:
|
||||||
|
query = self._apply_sorting(query, sort_by, sort_order)
|
||||||
|
print(str(query))
|
||||||
|
return query
|
||||||
219
backend/app/db/cruddb/dbapp.py
Normal file
219
backend/app/db/cruddb/dbapp.py
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
|
from sqlalchemy import select,and_
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
from app.db.cruddb.crudbase import crudbase
|
||||||
|
from fastapi_pagination.ext.sqlalchemy import paginate
|
||||||
|
from app.core.common import ApiReturnPage
|
||||||
|
|
||||||
|
from app.db import models, schemas
|
||||||
|
from app.core.security import chacha20Decrypt, get_password_hash
|
||||||
|
|
||||||
|
|
||||||
|
class dbflowhistory(crudbase):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(model=models.FlowHistory)
|
||||||
|
|
||||||
|
def get_flows_by_appid_version(self,db: Session,domainurl:str,appid:str,version:int):
|
||||||
|
return db.execute(super().get_by_conditions({"domainurl":domainurl,"appid":appid, "version":version})).scalars().all()
|
||||||
|
|
||||||
|
dbflowhistory = dbflowhistory()
|
||||||
|
|
||||||
|
class dbflow(crudbase):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(model=models.Flow)
|
||||||
|
|
||||||
|
def get_domain_apps(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_flow_by_flowid(self,db: Session,flowid:str):
|
||||||
|
return db.execute(super().get_by_conditions({"flowid":flowid})).scalars().first()
|
||||||
|
|
||||||
|
def get_flows_by_appid(self,db: Session,domainurl:str,appid:str):
|
||||||
|
return db.execute(select(models.Flow).filter(and_(models.Flow.domainurl == domainurl,models.Flow.appid == appid))).scalars().all()
|
||||||
|
|
||||||
|
|
||||||
|
def create_flow(self,db: Session, domainurl: str, flow: schemas.FlowIn,userid:int):
|
||||||
|
db_flow = models.Flow(
|
||||||
|
flowid=flow.flowid,
|
||||||
|
appid=flow.appid,
|
||||||
|
eventid=flow.eventid,
|
||||||
|
domainurl=domainurl,
|
||||||
|
name=flow.name,
|
||||||
|
content=flow.content,
|
||||||
|
createuserid = userid,
|
||||||
|
updateuserid = userid
|
||||||
|
)
|
||||||
|
db.add(db_flow)
|
||||||
|
db_app = db.execute(select(models.App).where(and_(models.App.domainurl == domainurl,models.App.appid == flow.appid))).scalars().first()
|
||||||
|
if not db_app:
|
||||||
|
db_app = models.App(
|
||||||
|
domainurl = domainurl,
|
||||||
|
appid=flow.appid,
|
||||||
|
appname=flow.appname,
|
||||||
|
version = 0,
|
||||||
|
createuserid= userid,
|
||||||
|
updateuserid = userid
|
||||||
|
)
|
||||||
|
db.add(db_app)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_flow)
|
||||||
|
return db_flow
|
||||||
|
|
||||||
|
dbflow = dbflow()
|
||||||
|
|
||||||
|
|
||||||
|
class dbappversion(crudbase):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(model=models.AppVersion)
|
||||||
|
|
||||||
|
def get_appversions(self,domainurl:str,appid:str):
|
||||||
|
return super().get_by_conditions({"domainurl":domainurl,"appid":appid})
|
||||||
|
|
||||||
|
def get_app_by_version(self,db: Session,domainurl:str,appid:str,version:int):
|
||||||
|
return db.execute(super().get_by_conditions({"domainurl":domainurl,"appid":appid,"version":version})).scalars().first()
|
||||||
|
|
||||||
|
def get_app_latestversion(self,db: Session,domainurl:str,appid:str):
|
||||||
|
appversion = db.execute(super().get_by_conditions({"domainurl":domainurl,"appid":appid},"version","desc")).scalars().first()
|
||||||
|
if appversion:
|
||||||
|
return appversion.version
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
dbappversion = dbappversion()
|
||||||
|
|
||||||
|
class dbapp(crudbase):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(model=models.App)
|
||||||
|
|
||||||
|
def get_app(self,db: Session,domainurl:str,appid:str):
|
||||||
|
return db.execute(super().get_by_conditions({"domainurl":domainurl,"appid":appid})).scalars().first()
|
||||||
|
|
||||||
|
def get_apps(self,db: Session,domainurl:str):
|
||||||
|
return db.execute(super().get_by_conditions({"domainurl":domainurl})).scalars().all()
|
||||||
|
|
||||||
|
def update_appversion(self,db: Session,domainurl, newversion: schemas.VersionUpdate,userid:int):
|
||||||
|
db_app = self.get_app(db,domainurl,newversion.appid)
|
||||||
|
if db_app:
|
||||||
|
db_app.version = dbappversion.get_app_latestversion(db,domainurl,newversion.appid)+1
|
||||||
|
db_app.updateuserid = userid,
|
||||||
|
db_app.versionname = newversion.versionname
|
||||||
|
db_app.is_saved = False
|
||||||
|
appversion = models.AppVersion(
|
||||||
|
domainurl = db_app.domainurl,
|
||||||
|
appid=db_app.appid,
|
||||||
|
appname=db_app.appname,
|
||||||
|
version = db_app.version,
|
||||||
|
versionname = newversion.versionname,
|
||||||
|
comment = newversion.comment,
|
||||||
|
updateuserid = userid,
|
||||||
|
createuserid = userid
|
||||||
|
)
|
||||||
|
db.add(appversion)
|
||||||
|
db.add(db_app)
|
||||||
|
|
||||||
|
flows = dbflow.get_flows_by_appid(db,domainurl,newversion.appid)
|
||||||
|
for flow in flows:
|
||||||
|
db_flowhistory = models.FlowHistory(
|
||||||
|
flowid = flow.flowid,
|
||||||
|
appid = flow.appid,
|
||||||
|
eventid = flow.eventid,
|
||||||
|
domainurl = flow.domainurl,
|
||||||
|
name = flow.name,
|
||||||
|
content = flow.content,
|
||||||
|
version = db_app.version,
|
||||||
|
updateuserid = userid,
|
||||||
|
createuserid = userid
|
||||||
|
)
|
||||||
|
db.add(db_flowhistory)
|
||||||
|
db.add(db_flowhistory)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_app)
|
||||||
|
return db_app
|
||||||
|
return None
|
||||||
|
|
||||||
|
def change_appversion(self,db: Session, domainurl:str,appid:str,version:int,userid:int):
|
||||||
|
db_app = self.get_app(db, domainurl, appid)
|
||||||
|
if not db_app:
|
||||||
|
return None
|
||||||
|
db_appversion = dbappversion.get_app_by_version(db, domainurl, appid, version)
|
||||||
|
if not db_appversion:
|
||||||
|
return None
|
||||||
|
db_app.version = version
|
||||||
|
db_app.versionname = db_appversion.versionname
|
||||||
|
db_app.updateuserid = userid
|
||||||
|
db_app.is_saved = False
|
||||||
|
db.add(db_app)
|
||||||
|
|
||||||
|
flows = dbflow.get_flows_by_appid(db, domainurl, appid)
|
||||||
|
for flow in flows:
|
||||||
|
db.delete(flow)
|
||||||
|
db.flush()
|
||||||
|
flowhistorys = dbflowhistory.get_flows_by_appid_version(db, domainurl, appid, version)
|
||||||
|
for flow in flowhistorys:
|
||||||
|
db_flow = models.Flow(
|
||||||
|
flowid = flow.flowid,
|
||||||
|
appid = flow.appid,
|
||||||
|
eventid = flow.eventid,
|
||||||
|
domainurl = flow.domainurl,
|
||||||
|
name = flow.name,
|
||||||
|
content = flow.content,
|
||||||
|
updateuserid = userid,
|
||||||
|
createuserid = userid
|
||||||
|
)
|
||||||
|
db.add(db_flow)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_app)
|
||||||
|
return db_app
|
||||||
|
|
||||||
|
def delete_app(self,db: Session, domainurl: str,appid: str ):
|
||||||
|
db_app =self.get_app(db,domainurl,appid)
|
||||||
|
if db_app:
|
||||||
|
flows = dbflow.get_flows_by_appid(db,domainurl,appid)
|
||||||
|
for flow in flows:
|
||||||
|
db.delete(flow)
|
||||||
|
db.delete(db_app)
|
||||||
|
db.commit()
|
||||||
|
return db_app
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_appversions(self,db: Session, domainurl:str,appid:str):
|
||||||
|
return paginate(db,dbappversion.get_appversions(domainurl,appid))
|
||||||
|
|
||||||
|
def get_flow(self,db: Session, domainurl: str, appid:str):
|
||||||
|
return dbflow.get_flows_by_appid(db,domainurl,appid)
|
||||||
|
|
||||||
|
def create_flow(self,db: Session, domainurl: str, flow: schemas.FlowIn,userid:int):
|
||||||
|
return dbflow.create_flow(db,domainurl,flow,userid)
|
||||||
|
|
||||||
|
def edit_flow(self,db: Session, domainurl: str, flow: schemas.FlowIn,userid:int):
|
||||||
|
db_flow = dbflow.get_flow_by_flowid(db, flow.flowid)
|
||||||
|
if not db_flow:
|
||||||
|
return dbflow.create_flow(db,domainurl,flow,userid)
|
||||||
|
db_flow.appid =flow.appid
|
||||||
|
db_flow.eventid=flow.eventid
|
||||||
|
db_flow.domainurl=domainurl
|
||||||
|
db_flow.name=flow.name
|
||||||
|
db_flow.content=flow.content
|
||||||
|
db_flow.updateuserid = userid
|
||||||
|
db.add(db_flow)
|
||||||
|
db_app = self.get_app(db, domainurl, flow.appid)
|
||||||
|
if db_app and db_app.version > 0:
|
||||||
|
db_app.is_saved = True
|
||||||
|
flag_modified(db_app, 'is_saved')
|
||||||
|
db_app.updateuserid = userid
|
||||||
|
db.add(db_app)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_flow)
|
||||||
|
return db_flow
|
||||||
|
|
||||||
|
def delete_flow(self,db: Session, flowid: str):
|
||||||
|
db_flow = dbflow.get_flow_by_flowid(db,flowid)
|
||||||
|
if db_flow:
|
||||||
|
return dbflow.delete(db,db_flow.id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
appService = dbapp()
|
||||||
218
backend/app/db/cruddb/dbdomain.py
Normal file
218
backend/app/db/cruddb/dbdomain.py
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import select,and_
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
from app.db.cruddb.crudbase import crudbase
|
||||||
|
from fastapi_pagination.ext.sqlalchemy import paginate
|
||||||
|
from app.core.common import ApiReturnPage
|
||||||
|
|
||||||
|
from app.db import models, schemas
|
||||||
|
from app.core.security import chacha20Decrypt, get_password_hash
|
||||||
|
|
||||||
|
class dbuserdomain(crudbase):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(model=models.UserDomain)
|
||||||
|
|
||||||
|
def get_userdomain(self,db: Session,userid:int,domainid:int):
|
||||||
|
return db.execute(super().get_by_conditions({"userid":userid,"domainid":domainid})).scalars().first()
|
||||||
|
|
||||||
|
def get_userdomain_by_domainid(self,db: Session,ownerid:int,domainid:int):
|
||||||
|
return super().get_by_conditions({"domainid":domainid})
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_domains(self,db: Session,domainid:int):
|
||||||
|
return db.execute(super().get_by_conditions({"domainid":domainid,"is_default":True})).scalars().all()
|
||||||
|
|
||||||
|
def get_user_default_domain(self,db: Session,userid:int):
|
||||||
|
return db.execute(super().get_by_conditions({"userid":userid,"is_default":True})).scalars().first()
|
||||||
|
|
||||||
|
|
||||||
|
dbuserdomain = dbuserdomain()
|
||||||
|
|
||||||
|
class dbmanagedomain(crudbase):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(model=models.ManageDomain)
|
||||||
|
|
||||||
|
def get_managedomain(self,db: Session,userid:int,domainid:int):
|
||||||
|
return db.execute(super().get_by_conditions({"userid":userid,"domainid":domainid})).scalars().first()
|
||||||
|
|
||||||
|
def get_managedomain_by_domain(self,db: Session,domainid:int):
|
||||||
|
return db.execute(super().get_by_conditions({"domainid":domainid})).scalars().all()
|
||||||
|
|
||||||
|
dbmanagedomain = dbmanagedomain()
|
||||||
|
|
||||||
|
class dbdomain(crudbase):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(model=models.Domain)
|
||||||
|
|
||||||
|
def get_domains(self,db: Session)-> ApiReturnPage[models.Base]:
|
||||||
|
return paginate(db,super().get_all())
|
||||||
|
|
||||||
|
def get_domains_by_manage(self,db: Session,userid:int)-> ApiReturnPage[models.Base]:
|
||||||
|
query = select(models.Domain).join(models.ManageDomain,models.ManageDomain.domainid == models.Domain.id).where(models.ManageDomain.userid == userid)
|
||||||
|
return paginate(db,query)
|
||||||
|
|
||||||
|
def get_domains_by_owner(self,db: Session,ownerid:int)-> ApiReturnPage[models.Base]:
|
||||||
|
return paginate(db,super().get_by_conditions({"ownerid":ownerid}))
|
||||||
|
|
||||||
|
def create_domain(self,db: Session, domain: schemas.DomainIn,userid:int):
|
||||||
|
#db_domain = super().get_by_conditions(db,{"url":domain.url,"kintoneuser":domain.kintoneuser,"onwerid":userid}).first()
|
||||||
|
#if not db_domain:
|
||||||
|
domain.encrypt_kintonepwd()
|
||||||
|
db_domain = models.Domain(
|
||||||
|
tenantid = domain.tenantid,
|
||||||
|
name = domain.name,
|
||||||
|
url = domain.url,
|
||||||
|
kintoneuser = domain.kintoneuser,
|
||||||
|
kintonepwd = domain.kintonepwd,
|
||||||
|
is_active = domain.is_active,
|
||||||
|
createuserid = userid,
|
||||||
|
updateuserid = userid,
|
||||||
|
ownerid = userid
|
||||||
|
)
|
||||||
|
db.add(db_domain)
|
||||||
|
db.flush()
|
||||||
|
user_domain = models.UserDomain(userid = userid, domainid = db_domain.id ,createuserid = userid,updateuserid = userid)
|
||||||
|
db.add(user_domain)
|
||||||
|
manage_domain = models.ManageDomain(userid = userid, domainid = db_domain.id ,createuserid = userid,updateuserid = userid)
|
||||||
|
db.add(manage_domain)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_domain)
|
||||||
|
return db_domain
|
||||||
|
|
||||||
|
def delete_domain(self,db: Session,id: int):
|
||||||
|
db_managedomains = dbmanagedomain.get_managedomain_by_domain(db,id)
|
||||||
|
for manage in db_managedomains:
|
||||||
|
db.delete(manage)
|
||||||
|
return super().delete(db,id)
|
||||||
|
|
||||||
|
def edit_domain(self,db: Session, domain: schemas.DomainIn,userid:int) -> schemas.DomainOut:
|
||||||
|
db_domain = super().get(db,domain.id)
|
||||||
|
if db_domain:
|
||||||
|
db_domain.tenantid = domain.tenantid
|
||||||
|
db_domain.name=domain.name
|
||||||
|
db_domain.url=domain.url
|
||||||
|
if db_domain.is_active == True and domain.is_active == False:
|
||||||
|
db_userdomains = dbuserdomain.get_default_domains(db,domain.id)
|
||||||
|
for userdomain in db_userdomains:
|
||||||
|
userdomain.is_default = False
|
||||||
|
db.add(userdomain)
|
||||||
|
db_domain.is_active=domain.is_active
|
||||||
|
db_domain.kintoneuser=domain.kintoneuser
|
||||||
|
if domain.kintonepwd != "" and domain.kintonepwd != None:
|
||||||
|
domain.encrypt_kintonepwd()
|
||||||
|
db_domain.kintonepwd = domain.kintonepwd
|
||||||
|
db_domain.updateuserid = userid
|
||||||
|
db.add(db_domain)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_domain)
|
||||||
|
return db_domain
|
||||||
|
return None
|
||||||
|
|
||||||
|
def add_userdomain(self,db: Session,ownerid:int,userid:int,domainid:int) -> schemas.DomainOut:
|
||||||
|
db_domain = super().get(db,domainid)
|
||||||
|
if db_domain and db_domain.is_active:
|
||||||
|
db_userdomain = dbuserdomain.get_userdomain(db,userid,domainid)
|
||||||
|
if not db_userdomain:
|
||||||
|
user_domain = models.UserDomain(userid = userid, domainid = domainid ,createuserid = ownerid,updateuserid = ownerid)
|
||||||
|
db.add(user_domain)
|
||||||
|
db.commit()
|
||||||
|
return db_domain
|
||||||
|
return None
|
||||||
|
|
||||||
|
def add_userdomain_by_owner(self,db: Session,ownerid:int, userid:int,domainid:int) -> schemas.DomainOut:
|
||||||
|
db_domain = db.execute(super().get_by_conditions({"id":domainid,"is_active":True})).scalars().first()
|
||||||
|
if db_domain:
|
||||||
|
db_userdomain = dbuserdomain.get_userdomain(db,userid,domainid)
|
||||||
|
if not db_userdomain:
|
||||||
|
user_domain = models.UserDomain(userid = userid, domainid = domainid ,createuserid =ownerid,updateuserid = ownerid)
|
||||||
|
db.add(user_domain)
|
||||||
|
db.commit()
|
||||||
|
return db_domain
|
||||||
|
return None
|
||||||
|
|
||||||
|
def delete_userdomain(self,db: Session, userid: int,domainid: int) -> schemas.DomainOut:
|
||||||
|
db_userdomain = dbuserdomain.get_userdomain(db,userid,domainid)
|
||||||
|
if db_userdomain:
|
||||||
|
domain = db_userdomain.domain
|
||||||
|
db.delete(db_userdomain)
|
||||||
|
db.commit()
|
||||||
|
return domain
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_default_domain(self,db: Session, userid: int) -> schemas.DomainOut:
|
||||||
|
userdomain = dbuserdomain.get_user_default_domain(db,userid)
|
||||||
|
if userdomain:
|
||||||
|
return userdomain.domain
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_default_domain(self,db: Session, userid: int,domainid: int):
|
||||||
|
db_domain =db.execute(super().get_by_conditions({"id":domainid,"is_active":True})).scalars().first()
|
||||||
|
if db_domain:
|
||||||
|
db_default_domain = dbuserdomain.get_user_default_domain(db,userid)
|
||||||
|
db_userdomain =dbuserdomain.get_userdomain(db,userid,domainid)
|
||||||
|
if db_default_domain:
|
||||||
|
if db_default_domain.domainid != domainid:
|
||||||
|
db_default_domain.is_default = False
|
||||||
|
db_default_domain.updateuserid = userid
|
||||||
|
db.add(db_default_domain)
|
||||||
|
else:
|
||||||
|
return db_domain
|
||||||
|
if db_userdomain:
|
||||||
|
db_userdomain.is_default = True
|
||||||
|
db_userdomain.updateuserid = userid
|
||||||
|
db.add(db_userdomain)
|
||||||
|
else:
|
||||||
|
db_userdomain = dbuserdomain.create(db,schemas.UserDomainIn(domainid=domainid,userid=userid,is_default = True))
|
||||||
|
db.add(db_userdomain)
|
||||||
|
db.commit()
|
||||||
|
return db_domain
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_shareddomain_users(self,db: Session,domainid: int) -> ApiReturnPage[models.Base]:
|
||||||
|
users = select(models.User).join(models.UserDomain,models.UserDomain.userid == models.User.id).filter(models.UserDomain.domainid ==domainid)
|
||||||
|
return paginate(db,users)
|
||||||
|
|
||||||
|
|
||||||
|
def add_managedomain(self,db: Session,ownerid:int,userid:int,domainid:int) -> schemas.DomainOut:
|
||||||
|
db_domain = self.get(db,domainid)
|
||||||
|
if db_domain:
|
||||||
|
db_managedomain = dbmanagedomain.get_managedomain(db,userid,domainid)
|
||||||
|
if not db_managedomain:
|
||||||
|
manage_domain = models.ManageDomain(userid = userid, domainid = domainid ,createuserid = ownerid,updateuserid = ownerid)
|
||||||
|
db.add(manage_domain)
|
||||||
|
db.commit()
|
||||||
|
return db_domain
|
||||||
|
return None
|
||||||
|
|
||||||
|
def add_managedomain_by_owner(self,db: Session,ownerid:int, userid:int,domainid:int) -> schemas.DomainOut:
|
||||||
|
db_domain = db.execute(super().get_by_conditions({"id":domainid,"ownerid":ownerid,})).scalars().first()
|
||||||
|
if db_domain:
|
||||||
|
db_managedomain = dbmanagedomain.get_managedomain(db,userid,domainid)
|
||||||
|
if not db_managedomain:
|
||||||
|
manage_domain = models.ManageDomain(userid = userid, domainid = domainid ,createuserid =ownerid,updateuserid = ownerid)
|
||||||
|
db.add(manage_domain)
|
||||||
|
db.commit()
|
||||||
|
return db_domain
|
||||||
|
return None
|
||||||
|
|
||||||
|
def delete_managedomain(self,db: Session, userid: int,domainid: int) -> schemas.DomainOut:
|
||||||
|
db_managedomain = dbmanagedomain.get_managedomain(db,userid,domainid)
|
||||||
|
if db_managedomain:
|
||||||
|
domain = db_managedomain.domain
|
||||||
|
if domain.ownerid != userid:
|
||||||
|
db.delete(db_managedomain)
|
||||||
|
db.commit()
|
||||||
|
return domain
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_managedomain_users(self,db: Session,domainid: int) -> ApiReturnPage[models.Base]:
|
||||||
|
users = select(models.User).join(models.ManageDomain,models.ManageDomain.userid == models.User.id).where(models.ManageDomain.domainid ==domainid)
|
||||||
|
return paginate(db,users)
|
||||||
|
|
||||||
|
domainService = dbdomain()
|
||||||
13
backend/app/db/cruddb/dbtenant.py
Normal file
13
backend/app/db/cruddb/dbtenant.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from app.db.cruddb.crudbase import crudbase
|
||||||
|
from app.db import models, schemas
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
class dbtenant(crudbase):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(model=models.Tenant)
|
||||||
|
|
||||||
|
def get_tenant(sefl,db:Session,tenantid: str):
|
||||||
|
tenant = db.execute(super().get_by_conditions({"tenantid":tenantid})).scalars().first()
|
||||||
|
return tenant
|
||||||
|
|
||||||
|
tenantService = dbtenant()
|
||||||
98
backend/app/db/cruddb/dbuser.py
Normal file
98
backend/app/db/cruddb/dbuser.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import and_
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
from app.db.cruddb.crudbase import crudbase
|
||||||
|
from fastapi_pagination.ext.sqlalchemy import paginate
|
||||||
|
from app.core.common import ApiReturnPage
|
||||||
|
|
||||||
|
from app.db import models, schemas
|
||||||
|
from app.core.security import chacha20Decrypt, get_password_hash
|
||||||
|
|
||||||
|
|
||||||
|
class dbpermission(crudbase):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(model=models.Permission)
|
||||||
|
|
||||||
|
dbpermission = dbpermission()
|
||||||
|
|
||||||
|
class dbrole(crudbase):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(model=models.Role)
|
||||||
|
|
||||||
|
dbrole = dbrole()
|
||||||
|
|
||||||
|
class dbuser(crudbase):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(model=models.User)
|
||||||
|
|
||||||
|
def get_user(self,db: Session, user_id: int) -> schemas.User:
|
||||||
|
return super().get(db,user_id)
|
||||||
|
|
||||||
|
def get_user_by_email(self,db: Session, email: str) -> schemas.User:
|
||||||
|
return db.execute(super().get_by_conditions({"email":email})).scalars().first()
|
||||||
|
|
||||||
|
def get_users(self,db: Session) -> ApiReturnPage[models.Base]:
|
||||||
|
return paginate(db,super().get_all())
|
||||||
|
|
||||||
|
def get_users_not_admin(self,db: Session) -> ApiReturnPage[models.Base]:
|
||||||
|
return paginate(db,super().get_by_conditions({"is_superuser":False}))
|
||||||
|
|
||||||
|
def create_user(self,db: Session, user: schemas.UserCreate,userid:int):
|
||||||
|
hashed_password = get_password_hash(user.password)
|
||||||
|
user.hashed_password = hashed_password
|
||||||
|
user.createuserid = userid
|
||||||
|
user.updateuserid = userid
|
||||||
|
del user.password
|
||||||
|
return super().create(db,user)
|
||||||
|
|
||||||
|
def delete_user(self,db: Session, user_id: int):
|
||||||
|
return super().delete(db,user_id)
|
||||||
|
|
||||||
|
|
||||||
|
def edit_user(self,db: Session, user_id:int,user: schemas.UserEdit,userid: int) -> schemas.User:
|
||||||
|
if not user.password is None and user.password != "":
|
||||||
|
user.hashed_password = get_password_hash(user.password)
|
||||||
|
del user.password
|
||||||
|
user.updateuserid = userid
|
||||||
|
return super().update(db,user_id,user)
|
||||||
|
|
||||||
|
def get_roles(self,db: Session) -> t.List[schemas.RoleBase]:
|
||||||
|
return db.execute(dbrole.get_all()).scalars().all()
|
||||||
|
#return dbrole.get_all().all()
|
||||||
|
|
||||||
|
def get_roles_by_level(self,db: Session,roles:t.List[models.Role]) -> t.List[schemas.RoleBase]:
|
||||||
|
level = 99999
|
||||||
|
for role in roles:
|
||||||
|
if role.level < level:
|
||||||
|
level = role.level
|
||||||
|
return db.execute(dbrole.get_by_conditions({"level":{"operator":">","value":level}})).scalars().all()
|
||||||
|
|
||||||
|
def assign_userrole(self,db: Session, user_id: int, roles: t.List[int]):
|
||||||
|
db_user = super().get(db,user_id)
|
||||||
|
if db_user:
|
||||||
|
for role in db_user.roles:
|
||||||
|
if role.id not in roles:
|
||||||
|
db_user.roles.remove(role)
|
||||||
|
for roleid in roles:
|
||||||
|
role = dbrole.get(db,roleid)
|
||||||
|
if role not in db_user.roles:
|
||||||
|
db_user.roles.append(role)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_user)
|
||||||
|
return db_user
|
||||||
|
|
||||||
|
def get_permissions(self,db: Session) -> t.List[schemas.Permission]:
|
||||||
|
return db.execute(dbpermission.get_all()).scalars().all()
|
||||||
|
|
||||||
|
def get_user_permissions(self,db: Session,user_id: int) -> t.List[schemas.Permission]:
|
||||||
|
permissions =[]
|
||||||
|
db_user = super().get(db,user_id)
|
||||||
|
if db_user:
|
||||||
|
for role in db_user.roles:
|
||||||
|
permissions += role.permissions
|
||||||
|
return list(set(permissions))
|
||||||
|
|
||||||
|
userService = dbuser()
|
||||||
@@ -1,31 +1,256 @@
|
|||||||
from sqlalchemy import Boolean, Column, Integer, String
|
from sqlalchemy import Boolean, Column, Integer, String, DateTime,ForeignKey,Table
|
||||||
|
from sqlalchemy.orm import Mapped,relationship,as_declarative,mapped_column
|
||||||
|
from datetime import datetime,timezone
|
||||||
|
from app.db import Base
|
||||||
|
from app.core.security import chacha20Decrypt
|
||||||
|
|
||||||
from .session import Base
|
@as_declarative()
|
||||||
|
class Base:
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
create_time = Column(DateTime, default=datetime.now(timezone.utc))
|
||||||
|
update_time = Column(DateTime, default=datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
|
||||||
|
|
||||||
|
|
||||||
|
userrole = Table(
|
||||||
|
"userrole",
|
||||||
|
Base.metadata,
|
||||||
|
Column("userid",Integer,ForeignKey("user.id")),
|
||||||
|
Column("roleid",Integer,ForeignKey("role.id")),
|
||||||
|
)
|
||||||
|
|
||||||
|
rolepermission = Table(
|
||||||
|
"rolepermission",
|
||||||
|
Base.metadata,
|
||||||
|
Column("roleid",Integer,ForeignKey("role.id")),
|
||||||
|
Column("permissionid",Integer,ForeignKey("permission.id")),
|
||||||
|
)
|
||||||
|
|
||||||
class User(Base):
|
class User(Base):
|
||||||
__tablename__ = "user"
|
__tablename__ = "user"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
email = mapped_column(String(50), unique=True, index=True, nullable=False)
|
||||||
email = Column(String(50), unique=True, index=True, nullable=False)
|
first_name = mapped_column(String(100))
|
||||||
first_name = Column(String(100))
|
last_name = mapped_column(String(100))
|
||||||
last_name = Column(String(100))
|
hashed_password = mapped_column(String(200), nullable=False)
|
||||||
hashed_password = Column(String(200), nullable=False)
|
is_active = mapped_column(Boolean, default=True)
|
||||||
is_active = Column(Boolean, default=True)
|
is_superuser = mapped_column(Boolean, default=False)
|
||||||
is_superuser = Column(Boolean, default=False)
|
tenantid = mapped_column(String(100))
|
||||||
|
createuserid = mapped_column(Integer,ForeignKey("user.id"))
|
||||||
|
updateuserid = mapped_column(Integer,ForeignKey("user.id"))
|
||||||
|
createuser = relationship('User',foreign_keys=[createuserid])
|
||||||
|
updateuser = relationship('User',foreign_keys=[updateuserid])
|
||||||
|
roles = relationship("Role",secondary=userrole,back_populates="users")
|
||||||
|
|
||||||
|
|
||||||
|
class Role(Base):
|
||||||
|
__tablename__ = "role"
|
||||||
|
|
||||||
|
name = mapped_column(String(100))
|
||||||
|
description = mapped_column(String(255))
|
||||||
|
level = mapped_column(Integer)
|
||||||
|
users = relationship("User",secondary=userrole,back_populates="roles")
|
||||||
|
permissions = relationship("Permission",secondary=rolepermission,back_populates="roles")
|
||||||
|
|
||||||
|
class Permission(Base):
|
||||||
|
__tablename__ = "permission"
|
||||||
|
|
||||||
|
menu = mapped_column(String(100))
|
||||||
|
function = mapped_column(String(255))
|
||||||
|
link = mapped_column(String(100))
|
||||||
|
privilege = mapped_column(String(100))
|
||||||
|
roles = relationship("Role",secondary=rolepermission,back_populates="permissions")
|
||||||
|
|
||||||
|
|
||||||
|
class App(Base):
|
||||||
|
__tablename__ = "app"
|
||||||
|
|
||||||
|
domainurl = mapped_column(String(200), nullable=False)
|
||||||
|
appname = mapped_column(String(200), nullable=False)
|
||||||
|
appid = mapped_column(String(100), index=True, nullable=False)
|
||||||
|
version = mapped_column(Integer)
|
||||||
|
versionname = mapped_column(String(200), nullable=False)
|
||||||
|
is_saved = mapped_column(Boolean, default=False)
|
||||||
|
createuserid = mapped_column(Integer,ForeignKey("user.id"))
|
||||||
|
updateuserid = mapped_column(Integer,ForeignKey("user.id"))
|
||||||
|
createuser = relationship('User',foreign_keys=[createuserid])
|
||||||
|
updateuser = relationship('User',foreign_keys=[updateuserid])
|
||||||
|
|
||||||
|
class AppVersion(Base):
|
||||||
|
__tablename__ = "appversion"
|
||||||
|
|
||||||
|
domainurl = mapped_column(String(200), nullable=False)
|
||||||
|
appname = mapped_column(String(200), nullable=False)
|
||||||
|
appid = mapped_column(String(100), index=True, nullable=False)
|
||||||
|
version = mapped_column(Integer)
|
||||||
|
versionname = mapped_column(String(200), nullable=False)
|
||||||
|
comment = mapped_column(String(200), nullable=False)
|
||||||
|
createuserid = mapped_column(Integer,ForeignKey("user.id"))
|
||||||
|
updateuserid = mapped_column(Integer,ForeignKey("user.id"))
|
||||||
|
createuser = relationship('User',foreign_keys=[createuserid])
|
||||||
|
updateuser = relationship('User',foreign_keys=[updateuserid])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AppSetting(Base):
|
class AppSetting(Base):
|
||||||
__tablename__ = "appsetting"
|
__tablename__ = "appsetting"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
appid = mapped_column(String(100), index=True, nullable=False)
|
||||||
appid = Column(String(100), index=True, nullable=False)
|
setting = mapped_column(String(1000))
|
||||||
setting = Column(String(1000))
|
|
||||||
|
|
||||||
class Kintone(Base):
|
class Kintone(Base):
|
||||||
__tablename__ = "kintone"
|
__tablename__ = "kintone"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
type = mapped_column(Integer, index=True, nullable=False)
|
||||||
type = Column(Integer, index=True, nullable=False)
|
name = mapped_column(String(100), nullable=False)
|
||||||
name = Column(String(100), nullable=False)
|
desc = mapped_column(String)
|
||||||
desc = Column(String(500))
|
content = mapped_column(String)
|
||||||
content = Column(String(2000))
|
|
||||||
|
class Action(Base):
|
||||||
|
__tablename__ = "action"
|
||||||
|
|
||||||
|
name = mapped_column(String(100), index=True, nullable=False)
|
||||||
|
title = mapped_column(String(200))
|
||||||
|
subtitle = mapped_column(String(500))
|
||||||
|
outputpoints = mapped_column(String)
|
||||||
|
property = mapped_column(String)
|
||||||
|
categoryid = mapped_column(Integer,ForeignKey("category.id"))
|
||||||
|
nosort = mapped_column(Integer)
|
||||||
|
|
||||||
|
class Flow(Base):
|
||||||
|
__tablename__ = "flow"
|
||||||
|
|
||||||
|
flowid = mapped_column(String(100), index=True, nullable=False)
|
||||||
|
appid = mapped_column(String(100), index=True, nullable=False)
|
||||||
|
eventid = mapped_column(String(100), index=True, nullable=False)
|
||||||
|
domainurl = mapped_column(String(200))
|
||||||
|
name = mapped_column(String(200))
|
||||||
|
content = mapped_column(String)
|
||||||
|
createuserid = mapped_column(Integer,ForeignKey("user.id"))
|
||||||
|
updateuserid = mapped_column(Integer,ForeignKey("user.id"))
|
||||||
|
createuser = relationship('User',foreign_keys=[createuserid])
|
||||||
|
updateuser = relationship('User',foreign_keys=[updateuserid])
|
||||||
|
|
||||||
|
class FlowHistory(Base):
|
||||||
|
__tablename__ = "flowhistory"
|
||||||
|
|
||||||
|
flowid = mapped_column(String(100), index=True, nullable=False)
|
||||||
|
appid = mapped_column(String(100), index=True, nullable=False)
|
||||||
|
eventid = mapped_column(String(100), index=True, nullable=False)
|
||||||
|
domainurl = mapped_column(String(200))
|
||||||
|
name = mapped_column(String(200))
|
||||||
|
content = mapped_column(String)
|
||||||
|
version = mapped_column(Integer)
|
||||||
|
createuserid = mapped_column(Integer,ForeignKey("user.id"))
|
||||||
|
updateuserid = mapped_column(Integer,ForeignKey("user.id"))
|
||||||
|
createuser = relationship('User',foreign_keys=[createuserid])
|
||||||
|
updateuser = relationship('User',foreign_keys=[updateuserid])
|
||||||
|
|
||||||
|
class Tenant(Base):
|
||||||
|
__tablename__ = "tenant"
|
||||||
|
|
||||||
|
tenantid = mapped_column(String(100), index=True, nullable=False)
|
||||||
|
name = mapped_column(String(200))
|
||||||
|
licence = mapped_column(String(200))
|
||||||
|
startdate = mapped_column(DateTime)
|
||||||
|
enddate = mapped_column(DateTime)
|
||||||
|
db = mapped_column(String(200))
|
||||||
|
|
||||||
|
|
||||||
|
class Domain(Base):
|
||||||
|
__tablename__ = "domain"
|
||||||
|
|
||||||
|
tenantid = mapped_column(String(100), index=True, nullable=False)
|
||||||
|
name = mapped_column(String(100), nullable=False)
|
||||||
|
url = mapped_column(String(200), nullable=False)
|
||||||
|
kintoneuser = mapped_column(String(100), nullable=False)
|
||||||
|
kintonepwd = mapped_column(String(100), nullable=False)
|
||||||
|
is_active = mapped_column(Boolean, default=True)
|
||||||
|
def decrypt_kintonepwd(self):
|
||||||
|
decrypted_pwd = chacha20Decrypt(self.kintonepwd)
|
||||||
|
return decrypted_pwd
|
||||||
|
createuserid = mapped_column(Integer,ForeignKey("user.id"))
|
||||||
|
updateuserid = mapped_column(Integer,ForeignKey("user.id"))
|
||||||
|
ownerid = mapped_column(Integer,ForeignKey("user.id"))
|
||||||
|
createuser = relationship('User',foreign_keys=[createuserid])
|
||||||
|
updateuser = relationship('User',foreign_keys=[updateuserid])
|
||||||
|
owner = relationship('User',foreign_keys=[ownerid])
|
||||||
|
|
||||||
|
|
||||||
|
class UserDomain(Base):
|
||||||
|
__tablename__ = "userdomain"
|
||||||
|
|
||||||
|
userid = mapped_column(Integer,ForeignKey("user.id"))
|
||||||
|
domainid = mapped_column(Integer,ForeignKey("domain.id"))
|
||||||
|
is_default = mapped_column(Boolean, default=False)
|
||||||
|
createuserid = mapped_column(Integer,ForeignKey("user.id"))
|
||||||
|
updateuserid = mapped_column(Integer,ForeignKey("user.id"))
|
||||||
|
domain = relationship("Domain")
|
||||||
|
user = relationship("User",foreign_keys=[userid])
|
||||||
|
createuser = relationship('User',foreign_keys=[createuserid])
|
||||||
|
updateuser = relationship('User',foreign_keys=[updateuserid])
|
||||||
|
|
||||||
|
class ManageDomain(Base):
|
||||||
|
__tablename__ = "managedomain"
|
||||||
|
|
||||||
|
userid = mapped_column(Integer,ForeignKey("user.id"))
|
||||||
|
domainid = mapped_column(Integer,ForeignKey("domain.id"))
|
||||||
|
createuserid = mapped_column(Integer,ForeignKey("user.id"))
|
||||||
|
updateuserid = mapped_column(Integer,ForeignKey("user.id"))
|
||||||
|
domain = relationship("Domain")
|
||||||
|
user = relationship("User",foreign_keys=[userid])
|
||||||
|
createuser = relationship('User',foreign_keys=[createuserid])
|
||||||
|
updateuser = relationship('User',foreign_keys=[updateuserid])
|
||||||
|
|
||||||
|
class Event(Base):
|
||||||
|
__tablename__ = "event"
|
||||||
|
|
||||||
|
category = mapped_column(String(100), nullable=False)
|
||||||
|
type = mapped_column(String(100), nullable=False)
|
||||||
|
eventid= mapped_column(String(100), nullable=False)
|
||||||
|
function = mapped_column(String(500), nullable=False)
|
||||||
|
mobile = mapped_column(Boolean, default=False)
|
||||||
|
eventgroup = mapped_column(Boolean, default=False)
|
||||||
|
|
||||||
|
class EventAction(Base):
|
||||||
|
__tablename__ = "eventaction"
|
||||||
|
|
||||||
|
eventid = mapped_column(String(100),ForeignKey("event.eventid"))
|
||||||
|
actionid = mapped_column(Integer,ForeignKey("action.id"))
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorLog(Base):
|
||||||
|
__tablename__ = "errorlog"
|
||||||
|
|
||||||
|
title = mapped_column(String(50))
|
||||||
|
location = mapped_column(String(500))
|
||||||
|
content = mapped_column(String(5000))
|
||||||
|
|
||||||
|
class OperationLog(Base):
|
||||||
|
__tablename__ = "operationlog"
|
||||||
|
|
||||||
|
tenantid = mapped_column(String(100))
|
||||||
|
clientip = mapped_column(String(200))
|
||||||
|
useragent = mapped_column(String(200))
|
||||||
|
userid = mapped_column(Integer,ForeignKey("user.id"))
|
||||||
|
operation = mapped_column(String(200))
|
||||||
|
function = mapped_column(String(200))
|
||||||
|
parameters = mapped_column(String)
|
||||||
|
response = mapped_column(String(200))
|
||||||
|
user = relationship('User')
|
||||||
|
|
||||||
|
class KintoneFormat(Base):
|
||||||
|
__tablename__ = "kintoneformat"
|
||||||
|
|
||||||
|
name = mapped_column(String(50))
|
||||||
|
startrow =mapped_column(Integer)
|
||||||
|
startcolumn =mapped_column(Integer)
|
||||||
|
typecolumn =mapped_column(Integer)
|
||||||
|
codecolumn =mapped_column(Integer)
|
||||||
|
field = mapped_column(String(5000))
|
||||||
|
trueformat = mapped_column(String(10))
|
||||||
|
|
||||||
|
class Category(Base):
|
||||||
|
__tablename__ = "category"
|
||||||
|
|
||||||
|
categoryname = mapped_column(String(20))
|
||||||
|
nosort = mapped_column(Integer)
|
||||||
@@ -1,37 +1,82 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from datetime import datetime
|
||||||
import typing as t
|
import typing as t
|
||||||
|
|
||||||
|
from app.core.security import chacha20Decrypt, chacha20Encrypt
|
||||||
|
|
||||||
|
class Base(BaseModel):
|
||||||
|
create_time: datetime
|
||||||
|
update_time: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class Permission(BaseModel):
|
||||||
|
id: int
|
||||||
|
menu:str
|
||||||
|
function:str
|
||||||
|
link:str
|
||||||
|
privilege:str
|
||||||
|
|
||||||
|
class RoleBase(BaseModel):
|
||||||
|
id: int
|
||||||
|
name:str
|
||||||
|
description:str
|
||||||
|
level:int
|
||||||
|
|
||||||
|
|
||||||
|
class RoleWithPermission(RoleBase):
|
||||||
|
permissions:t.List[Permission] = []
|
||||||
|
|
||||||
|
class AssignUserRoles(BaseModel):
|
||||||
|
userid:int
|
||||||
|
roleids:t.List[int]
|
||||||
|
|
||||||
class UserBase(BaseModel):
|
class UserBase(BaseModel):
|
||||||
email: str
|
email: str
|
||||||
is_active: bool = True
|
is_active: bool = True
|
||||||
is_superuser: bool = False
|
is_superuser: bool = False
|
||||||
first_name: str = None
|
first_name: str = None
|
||||||
last_name: str = None
|
last_name: str = None
|
||||||
|
roles:t.List[RoleBase] = []
|
||||||
|
|
||||||
|
|
||||||
|
class UserOut(BaseModel):
|
||||||
|
id: int
|
||||||
|
email: str
|
||||||
|
is_active: bool = True
|
||||||
|
is_superuser: bool = False
|
||||||
|
first_name: str = None
|
||||||
|
last_name: str = None
|
||||||
|
|
||||||
|
|
||||||
class UserOut(UserBase):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class UserCreate(UserBase):
|
class UserCreate(UserBase):
|
||||||
|
email:str
|
||||||
password: str
|
password: str
|
||||||
|
hashed_password :str = None
|
||||||
|
first_name: str
|
||||||
|
last_name: str
|
||||||
|
is_active:bool
|
||||||
|
is_superuser:bool
|
||||||
|
tenantid:t.Optional[str] = "1"
|
||||||
|
createuserid:t.Optional[int] = None
|
||||||
|
updateuserid:t.Optional[int] = None
|
||||||
|
|
||||||
class Config:
|
class ConfigDict:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
class UserEdit(UserBase):
|
class UserEdit(UserBase):
|
||||||
password: t.Optional[str] = None
|
password: t.Optional[str] = None
|
||||||
|
hashed_password :str = None
|
||||||
|
updateuserid:t.Optional[int] = None
|
||||||
|
|
||||||
class Config:
|
class ConfigDict:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
class User(UserBase):
|
class User(UserBase):
|
||||||
id: int
|
id: int
|
||||||
|
|
||||||
class Config:
|
class ConfigDict:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
@@ -39,8 +84,35 @@ class Token(BaseModel):
|
|||||||
access_token: str
|
access_token: str
|
||||||
token_type: str
|
token_type: str
|
||||||
|
|
||||||
|
class AppList(Base):
|
||||||
|
domainurl: str
|
||||||
|
appname: str
|
||||||
|
appid:str
|
||||||
|
version:int
|
||||||
|
is_saved:bool
|
||||||
|
versionname: t.Optional[str] = None
|
||||||
|
updateuser: UserOut
|
||||||
|
createuser: UserOut
|
||||||
|
|
||||||
|
|
||||||
|
class AppVersion(Base):
|
||||||
|
domainurl: str
|
||||||
|
appname: str
|
||||||
|
versionname: str
|
||||||
|
comment:str
|
||||||
|
appid:str
|
||||||
|
version:t.Optional[int] = None
|
||||||
|
updateuser: UserOut
|
||||||
|
createuser: UserOut
|
||||||
|
|
||||||
|
class VersionUpdate(BaseModel):
|
||||||
|
appid:str
|
||||||
|
versionname: str
|
||||||
|
comment:str
|
||||||
|
|
||||||
|
|
||||||
class TokenData(BaseModel):
|
class TokenData(BaseModel):
|
||||||
|
id:int = 0
|
||||||
email: str = None
|
email: str = None
|
||||||
permissions: str = "user"
|
permissions: str = "user"
|
||||||
|
|
||||||
@@ -56,7 +128,7 @@ class AppBase(BaseModel):
|
|||||||
class App(AppBase):
|
class App(AppBase):
|
||||||
id: int
|
id: int
|
||||||
|
|
||||||
class Config:
|
class ConfigDict:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
@@ -67,5 +139,113 @@ class Kintone(BaseModel):
|
|||||||
desc: str = None
|
desc: str = None
|
||||||
content: str = None
|
content: str = None
|
||||||
|
|
||||||
class Config:
|
class ConfigDict:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
||||||
|
class Action(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str = None
|
||||||
|
title: str = None
|
||||||
|
subtitle: str = None
|
||||||
|
outputpoints: str = None
|
||||||
|
property: str = None
|
||||||
|
categoryid: int = None
|
||||||
|
nosort: int
|
||||||
|
categoryname : str =None
|
||||||
|
class ConfigDict:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
class FlowIn(BaseModel):
|
||||||
|
flowid: str
|
||||||
|
# domainurl:str
|
||||||
|
appid: str
|
||||||
|
appname:str
|
||||||
|
eventid: str
|
||||||
|
name: str = None
|
||||||
|
content: str = None
|
||||||
|
|
||||||
|
class Flow(Base):
|
||||||
|
id: int
|
||||||
|
flowid: str
|
||||||
|
appid: str
|
||||||
|
eventid: str
|
||||||
|
domainurl: str
|
||||||
|
name: str = None
|
||||||
|
content: str = None
|
||||||
|
|
||||||
|
class ConfigDict:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
class DomainIn(BaseModel):
|
||||||
|
id: int
|
||||||
|
tenantid: str
|
||||||
|
name: str
|
||||||
|
url: str
|
||||||
|
kintoneuser: str
|
||||||
|
kintonepwd: t.Optional[str] = None
|
||||||
|
is_active: bool
|
||||||
|
createuserid:t.Optional[int] = None
|
||||||
|
updateuserid:t.Optional[int] = None
|
||||||
|
ownerid:t.Optional[int] = None
|
||||||
|
|
||||||
|
def encrypt_kintonepwd(self):
|
||||||
|
encrypted_pwd = chacha20Encrypt(self.kintonepwd)
|
||||||
|
self.kintonepwd = encrypted_pwd
|
||||||
|
|
||||||
|
class DomainOut(BaseModel):
|
||||||
|
id: int
|
||||||
|
tenantid: str
|
||||||
|
name: str
|
||||||
|
url: str
|
||||||
|
kintoneuser: str
|
||||||
|
is_active: bool
|
||||||
|
ownerid:int
|
||||||
|
|
||||||
|
class ConfigDict:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
class UserDomainParam(BaseModel):
|
||||||
|
userid:int
|
||||||
|
domainid:int
|
||||||
|
|
||||||
|
class UserDomain(BaseModel):
|
||||||
|
id: int
|
||||||
|
is_default: bool
|
||||||
|
domain:DomainOut
|
||||||
|
user:UserOut
|
||||||
|
|
||||||
|
class UserDomainIn(BaseModel):
|
||||||
|
is_default: bool
|
||||||
|
domainid:int
|
||||||
|
userid:int
|
||||||
|
|
||||||
|
class Domain(Base):
|
||||||
|
id: int
|
||||||
|
tenantid: str
|
||||||
|
name: str
|
||||||
|
url: str
|
||||||
|
kintoneuser: str
|
||||||
|
is_active: bool
|
||||||
|
updateuser:UserOut
|
||||||
|
owner:UserOut
|
||||||
|
|
||||||
|
class ConfigDict:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class Event(Base):
|
||||||
|
id: int
|
||||||
|
category: str
|
||||||
|
type: str
|
||||||
|
eventid: str
|
||||||
|
function: str
|
||||||
|
mobile: bool
|
||||||
|
eventgroup: bool
|
||||||
|
|
||||||
|
class ConfigDict:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
class ErrorCreate(BaseModel):
|
||||||
|
title:str
|
||||||
|
location:str
|
||||||
|
content:str
|
||||||
@@ -1,21 +1,37 @@
|
|||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.orm import sessionmaker, declarative_base, Session
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
|
|
||||||
from app.core import config
|
from app.core import config
|
||||||
|
|
||||||
engine = create_engine(
|
|
||||||
config.SQLALCHEMY_DATABASE_URI,
|
|
||||||
)
|
|
||||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
||||||
|
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
||||||
|
# engine = create_engine(
|
||||||
|
# config.SQLALCHEMY_DATABASE_URI,
|
||||||
|
# )
|
||||||
|
# SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
# Dependency
|
class Database:
|
||||||
def get_db():
|
def __init__(self, database_url: str):
|
||||||
db = SessionLocal()
|
self.database_url = database_url
|
||||||
|
self.engine = create_engine(self.database_url)
|
||||||
|
self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine)
|
||||||
|
self.Base = declarative_base()
|
||||||
|
|
||||||
|
def get_db(self):
|
||||||
|
db =self.SessionLocal()
|
||||||
|
return db
|
||||||
|
|
||||||
|
tenantdb = Database(config.SQLALCHEMY_DATABASE_URI)
|
||||||
|
|
||||||
|
|
||||||
|
def get_tenant_db():
|
||||||
|
db = tenantdb.get_db()
|
||||||
try:
|
try:
|
||||||
yield db
|
yield db
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
def get_user_db(database_url: str):
|
||||||
|
database = Database(database_url)
|
||||||
|
db = database.get_db()
|
||||||
|
return db
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import os
|
||||||
from fastapi import FastAPI, Depends
|
from fastapi import FastAPI, Depends
|
||||||
|
from fastapi_pagination import add_pagination
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from app.api.api_v1.routers.kintone import kinton_router
|
from app.api.api_v1.routers.kintone import kinton_router
|
||||||
@@ -6,18 +8,46 @@ from app.api.api_v1.routers.users import users_router
|
|||||||
from app.api.api_v1.routers.auth import auth_router
|
from app.api.api_v1.routers.auth import auth_router
|
||||||
from app.api.api_v1.routers.platform import platform_router
|
from app.api.api_v1.routers.platform import platform_router
|
||||||
from app.core import config
|
from app.core import config
|
||||||
from app.db import Base,engine
|
#from app.db import Base,engine
|
||||||
from app.core.auth import get_current_active_user
|
from app.core.auth import get_current_active_user
|
||||||
from app.core.celery_app import celery_app
|
from app.core.celery_app import celery_app
|
||||||
from app import tasks
|
from app import tasks
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
import logging
|
||||||
|
from app.core.apiexception import APIException, writedblog
|
||||||
|
from app.core.common import ApiReturnError
|
||||||
|
from app.db.crud import create_log
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
import asyncio
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from app.core.operation import LoggingMiddleware
|
||||||
|
|
||||||
|
#Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
Base.metadata.create_all(bind=engine)
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
startup_event()
|
||||||
|
yield
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title=config.PROJECT_NAME, docs_url="/api/docs", openapi_url="/api"
|
title=config.PROJECT_NAME, docs_url="/api/docs", openapi_url="/api",lifespan=lifespan
|
||||||
)
|
)
|
||||||
|
|
||||||
|
origins = [
|
||||||
|
"*"
|
||||||
|
]
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=origins,
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
app.add_middleware(LoggingMiddleware)
|
||||||
|
|
||||||
|
add_pagination(app)
|
||||||
|
|
||||||
# @app.middleware("http")
|
# @app.middleware("http")
|
||||||
# async def db_session_middleware(request: Request, call_next):
|
# async def db_session_middleware(request: Request, call_next):
|
||||||
@@ -26,6 +56,24 @@ app = FastAPI(
|
|||||||
# request.state.db.close()
|
# request.state.db.close()
|
||||||
# return response
|
# return response
|
||||||
|
|
||||||
|
def startup_event():
|
||||||
|
log_dir="log"
|
||||||
|
if not os.path.exists(log_dir):
|
||||||
|
os.makedirs(log_dir)
|
||||||
|
|
||||||
|
logger = logging.getLogger("uvicorn.access")
|
||||||
|
handler = logging.handlers.RotatingFileHandler(f"{log_dir}/api.log",mode="a",maxBytes = 100*1024, backupCount = 3)
|
||||||
|
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
|
||||||
|
logger.addHandler(handler)
|
||||||
|
|
||||||
|
@app.exception_handler(APIException)
|
||||||
|
async def api_exception_handler(request: Request, exc: APIException):
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.run_in_executor(None,writedblog,exc)
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
content= ApiReturnError(msg = f"{exc.detail}").model_dump(),
|
||||||
|
)
|
||||||
|
|
||||||
@app.get("/api/v1")
|
@app.get("/api/v1")
|
||||||
async def root():
|
async def root():
|
||||||
|
|||||||
252
backend/app/tests/conftest.py
Normal file
252
backend/app/tests/conftest.py
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
import pytest
|
||||||
|
from sqlalchemy import create_engine, event
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
from app.core import config, security
|
||||||
|
from app.core.dbmanager import get_db
|
||||||
|
from app.db import models,schemas
|
||||||
|
from app.main import app
|
||||||
|
|
||||||
|
|
||||||
|
from app.core import security
|
||||||
|
import jwt
|
||||||
|
|
||||||
|
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://kabAdmin:P%40ssw0rd!@kintonetooldb.postgres.database.azure.com/test"
|
||||||
|
|
||||||
|
engine = create_engine(SQLALCHEMY_DATABASE_URI,echo=True)
|
||||||
|
|
||||||
|
test_session_maker = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def test_db():
|
||||||
|
connection = engine.connect()
|
||||||
|
transaction = connection.begin()
|
||||||
|
test_session = test_session_maker(bind=connection)
|
||||||
|
yield test_session
|
||||||
|
test_session.close()
|
||||||
|
transaction.rollback()
|
||||||
|
#transaction.commit()
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def test_client(test_db):
|
||||||
|
def get_test_db():
|
||||||
|
try:
|
||||||
|
yield test_db
|
||||||
|
finally:
|
||||||
|
test_db.close()
|
||||||
|
|
||||||
|
app.dependency_overrides[get_db] = get_test_db
|
||||||
|
with TestClient(app) as test_client:
|
||||||
|
yield test_client
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def test_tenant_id():
|
||||||
|
return "1"
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def test_user(test_db,test_tenant_id):
|
||||||
|
password ="test"
|
||||||
|
user = models.User(
|
||||||
|
email = "test@test.com",
|
||||||
|
first_name = "test",
|
||||||
|
last_name = "abc",
|
||||||
|
hashed_password = security.get_password_hash(password),
|
||||||
|
is_active = True,
|
||||||
|
is_superuser = False,
|
||||||
|
tenantid = test_tenant_id
|
||||||
|
)
|
||||||
|
test_db.add(user)
|
||||||
|
test_db.commit()
|
||||||
|
test_db.refresh(user)
|
||||||
|
dicUser = user.__dict__
|
||||||
|
dicUser["password"] = password
|
||||||
|
return dicUser
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def password():
|
||||||
|
return "password"
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def user(test_db,password,test_tenant_id):
|
||||||
|
user = models.User(
|
||||||
|
email = "user@test.com",
|
||||||
|
first_name = "user",
|
||||||
|
last_name = "abc",
|
||||||
|
hashed_password = security.get_password_hash(password),
|
||||||
|
is_active = True,
|
||||||
|
is_superuser = False,
|
||||||
|
tenantid = test_tenant_id
|
||||||
|
)
|
||||||
|
test_db.add(user)
|
||||||
|
test_db.commit()
|
||||||
|
test_db.refresh(user)
|
||||||
|
return user.__dict__
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def admin(test_db,password,test_tenant_id):
|
||||||
|
user = models.User(
|
||||||
|
email = "admin@test.com",
|
||||||
|
first_name = "admin",
|
||||||
|
last_name = "abc",
|
||||||
|
hashed_password = security.get_password_hash(password),
|
||||||
|
is_active = True,
|
||||||
|
is_superuser = True,
|
||||||
|
tenantid =test_tenant_id
|
||||||
|
)
|
||||||
|
test_db.add(user)
|
||||||
|
test_db.commit()
|
||||||
|
test_db.refresh(user)
|
||||||
|
return user.__dict__
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def login_user(test_db,test_client,user,password):
|
||||||
|
# test_db.add(user)
|
||||||
|
# test_db.commit()
|
||||||
|
#test_db.refresh(user)
|
||||||
|
response = test_client.post("/api/token", data={"username": user["email"], "password":password })
|
||||||
|
return response.json()["access_token"]
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def login_admin(test_db,test_client,admin,password):
|
||||||
|
# test_db.add(admin)
|
||||||
|
# test_db.commit()
|
||||||
|
#test_db.refresh(admin)
|
||||||
|
response = test_client.post("/api/token", data={"username": admin["email"], "password":password })
|
||||||
|
return response.json()["access_token"]
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def login_user_id(login_user):
|
||||||
|
payload = jwt.decode(login_user, security.SECRET_KEY, algorithms=[security.ALGORITHM])
|
||||||
|
id = payload.get("sub")
|
||||||
|
return id
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def login_admin_id(login_admin):
|
||||||
|
payload = jwt.decode(login_admin, security.SECRET_KEY, algorithms=[security.ALGORITHM])
|
||||||
|
id = payload.get("sub")
|
||||||
|
return id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def test_role(test_db):
|
||||||
|
role = models.Role(
|
||||||
|
name = "test",
|
||||||
|
description = "test",
|
||||||
|
level = 1
|
||||||
|
)
|
||||||
|
test_db.add(role)
|
||||||
|
test_db.commit()
|
||||||
|
test_db.refresh(role)
|
||||||
|
return role.__dict__
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def test_domain(test_db,login_user_id):
|
||||||
|
domain = models.Domain(
|
||||||
|
tenantid = "1",
|
||||||
|
name = "テスト環境",
|
||||||
|
url = "https://mfu07rkgnb7c.cybozu.com",
|
||||||
|
kintoneuser = "MXZ",
|
||||||
|
kintonepwd = security.chacha20Encrypt("maxz1205"),
|
||||||
|
is_active = True,
|
||||||
|
createuserid =login_user_id,
|
||||||
|
updateuserid =login_user_id,
|
||||||
|
ownerid = login_user_id
|
||||||
|
)
|
||||||
|
test_db.add(domain)
|
||||||
|
test_db.flush()
|
||||||
|
user_domain = models.UserDomain(userid = login_user_id, domainid = domain.id ,createuserid = login_user_id,updateuserid = login_user_id)
|
||||||
|
test_db.add(user_domain)
|
||||||
|
manage_domain = models.ManageDomain(userid = login_user_id, domainid = domain.id ,createuserid = login_user_id,updateuserid = login_user_id)
|
||||||
|
test_db.add(manage_domain)
|
||||||
|
test_db.commit()
|
||||||
|
test_db.refresh(domain)
|
||||||
|
return domain
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def test_app_id():
|
||||||
|
return "132"
|
||||||
|
|
||||||
|
|
||||||
|
# @pytest.fixture
|
||||||
|
# def test_password() -> str:
|
||||||
|
# return "securepassword"
|
||||||
|
|
||||||
|
|
||||||
|
# def get_password_hash() -> str:
|
||||||
|
# """
|
||||||
|
# Password hashing can be expensive so a mock will be much faster
|
||||||
|
# """
|
||||||
|
# return "supersecrethash"
|
||||||
|
|
||||||
|
|
||||||
|
# @pytest.fixture
|
||||||
|
# def test_user(test_db) -> models.User:
|
||||||
|
# """
|
||||||
|
# Make a test user in the database
|
||||||
|
# """
|
||||||
|
|
||||||
|
# user = models.User(
|
||||||
|
# email="fake@email.com",
|
||||||
|
# hashed_password=get_password_hash(),
|
||||||
|
# is_active=True,
|
||||||
|
# )
|
||||||
|
# test_db.add(user)
|
||||||
|
# test_db.commit()
|
||||||
|
# return user
|
||||||
|
|
||||||
|
|
||||||
|
# @pytest.fixture
|
||||||
|
# def test_superuser(test_db) -> models.User:
|
||||||
|
# """
|
||||||
|
# Superuser for testing
|
||||||
|
# """
|
||||||
|
|
||||||
|
# user = models.User(
|
||||||
|
# email="fakeadmin@email.com",
|
||||||
|
# hashed_password=get_password_hash(),
|
||||||
|
# is_superuser=True,
|
||||||
|
# )
|
||||||
|
# test_db.add(user)
|
||||||
|
# test_db.commit()
|
||||||
|
# return user
|
||||||
|
|
||||||
|
|
||||||
|
# def verify_password_mock(first: str, second: str) -> bool:
|
||||||
|
# return True
|
||||||
|
|
||||||
|
|
||||||
|
# @pytest.fixture
|
||||||
|
# def user_token_headers(
|
||||||
|
# client: TestClient, test_user, test_password, monkeypatch
|
||||||
|
# ) -> t.Dict[str, str]:
|
||||||
|
# monkeypatch.setattr(security, "verify_password", verify_password_mock)
|
||||||
|
|
||||||
|
# login_data = {
|
||||||
|
# "username": test_user.email,
|
||||||
|
# "password": test_password,
|
||||||
|
# }
|
||||||
|
# r = client.post("/api/token", data=login_data)
|
||||||
|
# tokens = r.json()
|
||||||
|
# a_token = tokens["access_token"]
|
||||||
|
# headers = {"Authorization": f"Bearer {a_token}"}
|
||||||
|
# return headers
|
||||||
|
|
||||||
|
|
||||||
|
# @pytest.fixture
|
||||||
|
# def superuser_token_headers(
|
||||||
|
# client: TestClient, test_superuser, test_password, monkeypatch
|
||||||
|
# ) -> t.Dict[str, str]:
|
||||||
|
# monkeypatch.setattr(security, "verify_password", verify_password_mock)
|
||||||
|
|
||||||
|
# login_data = {
|
||||||
|
# "username": test_superuser.email,
|
||||||
|
# "password": test_password,
|
||||||
|
# }
|
||||||
|
# r = client.post("/api/token", data=login_data)
|
||||||
|
# tokens = r.json()
|
||||||
|
# a_token = tokens["access_token"]
|
||||||
|
# headers = {"Authorization": f"Bearer {a_token}"}
|
||||||
|
# return headers
|
||||||
5
backend/app/tests/pytest.ini
Normal file
5
backend/app/tests/pytest.ini
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[pytest]
|
||||||
|
log_cli = 1
|
||||||
|
log_cli_level = CRITICAL
|
||||||
|
log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
|
||||||
|
log_cli_date_format=%Y-%m-%d %H:%M:%S
|
||||||
11
backend/app/tests/test_auth.py
Normal file
11
backend/app/tests/test_auth.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
def test_usr_login(test_client,test_user):
|
||||||
|
response = test_client.post("/api/token", data={"username": test_user["email"], "password": test_user["password"]})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "access_token" in response.json()
|
||||||
|
assert "token_type" in response.json()
|
||||||
|
assert response.json()["user_name"] == test_user["first_name"]+ " " + test_user["last_name"]
|
||||||
|
|
||||||
175
backend/app/tests/test_domain.py
Normal file
175
backend/app/tests/test_domain.py
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import logging
|
||||||
|
def test_get_domains(test_client,test_domain,login_user):
|
||||||
|
response = test_client.get("/api/domains",headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "data" in data
|
||||||
|
assert data["data"] is not None
|
||||||
|
assert len(data["data"]) == 1
|
||||||
|
assert data["data"][0]["name"] == test_domain.name
|
||||||
|
|
||||||
|
def test_create_domain(test_client, login_user,login_user_id):
|
||||||
|
create_domain ={
|
||||||
|
"id": 0,
|
||||||
|
"tenantid": "1",
|
||||||
|
"name": "abc",
|
||||||
|
"url": "efg",
|
||||||
|
"kintoneuser": "eee",
|
||||||
|
"kintonepwd": "fff",
|
||||||
|
"is_active": True,
|
||||||
|
}
|
||||||
|
response = test_client.post("/api/domain", json=create_domain,headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "data" in data
|
||||||
|
assert data["data"] is not None
|
||||||
|
assert data["data"]["name"] == create_domain["name"]
|
||||||
|
assert data["data"]["url"] == create_domain["url"]
|
||||||
|
assert data["data"]["kintoneuser"] == create_domain["kintoneuser"]
|
||||||
|
assert data["data"]["is_active"] == create_domain["is_active"]
|
||||||
|
assert data["data"]["owner"]["id"] == login_user_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_managedomainuser(test_client,test_domain,login_user,login_user_id):
|
||||||
|
response = test_client.get("/api/managedomainuser/" + str(test_domain.id),headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "data" in data
|
||||||
|
assert len(data["data"]) == 1
|
||||||
|
assert data["data"][0]["id"] == login_user_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_delete_userdomain(test_client,test_domain,test_user,login_user,login_user_id):
|
||||||
|
userdomain ={
|
||||||
|
"userid":test_user["id"],
|
||||||
|
"domainid":test_domain.id
|
||||||
|
}
|
||||||
|
response = test_client.post("/api/userdomain" , json=userdomain,headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "data" in data
|
||||||
|
assert data["data"] is not None
|
||||||
|
assert data["data"]["name"] == test_domain.name
|
||||||
|
assert data["data"]["url"] == test_domain.url
|
||||||
|
response = test_client.delete(f"/api/domain/{test_domain.id}/{test_user["id"]}" , headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert "data" in data
|
||||||
|
assert data["data"] is not None
|
||||||
|
assert data["data"]["name"] == test_domain.name
|
||||||
|
assert data["data"]["url"] == test_domain.url
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_delete_managedomain(test_client,test_domain,test_user,login_user,login_user_id):
|
||||||
|
userdomain ={
|
||||||
|
"userid":test_user["id"],
|
||||||
|
"domainid":test_domain.id
|
||||||
|
}
|
||||||
|
response = test_client.post("/api/managedomain" , json=userdomain,headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "data" in data
|
||||||
|
assert data["data"] is not None
|
||||||
|
assert data["data"]["name"] == test_domain.name
|
||||||
|
assert data["data"]["url"] == test_domain.url
|
||||||
|
response = test_client.delete(f"/api/managedomain/{test_domain.id}/{test_user["id"]}" , headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert "data" in data
|
||||||
|
assert data["data"] is not None
|
||||||
|
assert data["data"]["name"] == test_domain.name
|
||||||
|
assert data["data"]["url"] == test_domain.url
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_domain(test_client, login_user,login_user_id):
|
||||||
|
delete_domain ={
|
||||||
|
"id": 0,
|
||||||
|
"tenantid": "1",
|
||||||
|
"name": "delete",
|
||||||
|
"url": "delete",
|
||||||
|
"kintoneuser": "delete",
|
||||||
|
"kintonepwd": "delete",
|
||||||
|
"is_active": True,
|
||||||
|
}
|
||||||
|
response = test_client.post("/api/domain", json=delete_domain,headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "data" in data
|
||||||
|
assert data["data"] is not None
|
||||||
|
id = data["data"]["id"]
|
||||||
|
|
||||||
|
response = test_client.delete(f"/api/domain/{id}/{login_user_id}",headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
|
||||||
|
response = test_client.delete(f"/api/domain/{id}",headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["data"]["name"] == delete_domain["name"]
|
||||||
|
response = test_client.get(f"/api/domain/{id}", headers={"Authorization": "Bearer " + login_user})
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "data" not in response.json()
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_defaultuserdomain(test_client, test_domain,login_user):
|
||||||
|
|
||||||
|
response = test_client.put("/api/defaultdomain/"+str(test_domain.id), headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "data" in data
|
||||||
|
assert data["data"] is not None
|
||||||
|
assert data["data"]["name"] == test_domain.name
|
||||||
|
assert data["data"]["url"] == test_domain.url
|
||||||
|
assert data["data"]["kintoneuser"] == test_domain.kintoneuser
|
||||||
|
assert data["data"]["is_active"] == test_domain.is_active
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_defaultuserdomain(test_client, test_domain,login_user):
|
||||||
|
|
||||||
|
response = test_client.get("/api/defaultdomain", headers={"Authorization": "Bearer " + login_user})
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "data" in data
|
||||||
|
assert data["data"] is not None
|
||||||
|
assert data["data"]["name"] == test_domain.name
|
||||||
|
assert data["data"]["url"] == test_domain.url
|
||||||
|
assert data["data"]["kintoneuser"] == test_domain.kintoneuser
|
||||||
|
assert data["data"]["is_active"] == test_domain.is_active
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_domainshareduser(test_client, test_domain,login_user,login_user_id):
|
||||||
|
response = test_client.get("/api/domainshareduser/"+str(test_domain.id), headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "data" in data
|
||||||
|
assert data["data"] is not None
|
||||||
|
assert len(data["data"]) == 1
|
||||||
|
assert data["data"][0]["id"] == login_user_id
|
||||||
|
|
||||||
|
def test_edit_domain(test_client, test_domain, login_user):
|
||||||
|
update_domain ={
|
||||||
|
"id": test_domain.id,
|
||||||
|
"tenantid": "1",
|
||||||
|
"name": "テスト環境abc",
|
||||||
|
"url": test_domain.url,
|
||||||
|
"kintoneuser": test_domain.kintoneuser,
|
||||||
|
"is_active": True
|
||||||
|
}
|
||||||
|
response = test_client.put("/api/domain", json=update_domain,headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "data" in data
|
||||||
|
assert data["data"] is not None
|
||||||
|
assert data["data"]["name"] == update_domain["name"]
|
||||||
|
assert data["data"]["url"] == update_domain["url"]
|
||||||
|
assert data["data"]["kintoneuser"] == update_domain["kintoneuser"]
|
||||||
|
assert data["data"]["is_active"] == update_domain["is_active"]
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
def test_read_main(client):
|
|
||||||
response = client.get("/api/v1")
|
|
||||||
|
def test_read_main(test_client):
|
||||||
|
response = test_client.get("/api/v1")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json() == {"message": "Hello World"}
|
assert response.json() == {"message": "success"}
|
||||||
|
|||||||
156
backend/app/tests/test_user.py
Normal file
156
backend/app/tests/test_user.py
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
|
||||||
|
import logging
|
||||||
|
def test_users_list(test_client,login_user):
|
||||||
|
|
||||||
|
response = test_client.get("/api/v1/users", headers={"Authorization": "Bearer " + login_user})
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert len(data["data"]) == 2
|
||||||
|
|
||||||
|
def test_users_list_for_admin(test_client,login_admin):
|
||||||
|
|
||||||
|
response = test_client.get("/api/v1/users", headers={"Authorization": "Bearer " + login_admin})
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "data" in data
|
||||||
|
assert len(data["data"]) == 3
|
||||||
|
|
||||||
|
def test_user_create(test_client,login_user):
|
||||||
|
user_data = {
|
||||||
|
"email": "newuser1@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
"first_name": "New",
|
||||||
|
"last_name": "User",
|
||||||
|
"is_active": True,
|
||||||
|
"is_superuser": False
|
||||||
|
}
|
||||||
|
response = test_client.post("/api/v1/users", json=user_data, headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "data" in data
|
||||||
|
assert data["data"] is not None
|
||||||
|
assert data["data"]["id"] > 0
|
||||||
|
assert data["data"]["email"] == user_data["email"]
|
||||||
|
assert data["data"]["first_name"] == user_data["first_name"]
|
||||||
|
assert data["data"]["last_name"] == user_data["last_name"]
|
||||||
|
assert data["data"]["is_active"] == user_data["is_active"]
|
||||||
|
assert data["data"]["is_superuser"] == user_data["is_superuser"]
|
||||||
|
|
||||||
|
def test_admin_create(test_client,login_user):
|
||||||
|
user_data = {
|
||||||
|
"email": "newuser2@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
"first_name": "New",
|
||||||
|
"last_name": "User",
|
||||||
|
"is_active": True,
|
||||||
|
"is_superuser": True
|
||||||
|
}
|
||||||
|
response = test_client.post("/api/v1/users", json=user_data, headers={"Authorization": "Bearer " + login_user})
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "data" not in data
|
||||||
|
|
||||||
|
def test_admin_create_for_admin(test_client,login_admin):
|
||||||
|
user_data = {
|
||||||
|
"email": "admin@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
"first_name": "New",
|
||||||
|
"last_name": "User",
|
||||||
|
"is_active": True,
|
||||||
|
"is_superuser": True
|
||||||
|
}
|
||||||
|
response = test_client.post("/api/v1/users", json=user_data, headers={"Authorization": "Bearer " + login_admin})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "data" in data
|
||||||
|
assert data["data"] is not None
|
||||||
|
assert data["data"]["id"] > 0
|
||||||
|
assert data["data"]["email"] == user_data["email"]
|
||||||
|
assert data["data"]["first_name"] == user_data["first_name"]
|
||||||
|
assert data["data"]["last_name"] == user_data["last_name"]
|
||||||
|
assert data["data"]["is_active"] == user_data["is_active"]
|
||||||
|
assert data["data"]["is_superuser"] == user_data["is_superuser"]
|
||||||
|
|
||||||
|
def test_user_details(test_client,login_user_id, login_user,user):
|
||||||
|
id = login_user_id
|
||||||
|
response = test_client.get("/api/v1/users/"+ str(id), headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert data["data"]["email"] == user["email"]
|
||||||
|
assert data["data"]["first_name"] == user["first_name"]
|
||||||
|
assert data["data"]["last_name"] == user["last_name"]
|
||||||
|
assert data["data"]["is_active"] == user["is_active"]
|
||||||
|
assert data["data"]["is_superuser"] == user["is_superuser"]
|
||||||
|
assert data["data"]["id"] == id
|
||||||
|
|
||||||
|
def test_user_edit(test_client, login_user_id,login_user,user):
|
||||||
|
id = login_user_id
|
||||||
|
user_data = {
|
||||||
|
"email": user["email"],
|
||||||
|
"first_name": "Updated",
|
||||||
|
"last_name": "test",
|
||||||
|
"is_active": True,
|
||||||
|
"is_superuser": False
|
||||||
|
}
|
||||||
|
response = test_client.put("/api/v1/users/" + str(id), json=user_data, headers={"Authorization": "Bearer " + login_user})
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["data"]["email"] == user["email"]
|
||||||
|
assert data["data"]["first_name"] == user_data["first_name"]
|
||||||
|
assert data["data"]["last_name"] == user_data["last_name"]
|
||||||
|
assert data["data"]["is_active"] == user["is_active"]
|
||||||
|
assert data["data"]["id"] == id
|
||||||
|
|
||||||
|
def test_user_delete(test_client, login_user):
|
||||||
|
user_data = {
|
||||||
|
"email": "delete@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
"first_name": "delete",
|
||||||
|
"last_name": "User",
|
||||||
|
"is_active": True,
|
||||||
|
"is_superuser": False
|
||||||
|
}
|
||||||
|
response = test_client.post("/api/v1/users", json=user_data, headers={"Authorization": "Bearer " + login_user})
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
id = data["data"]["id"]
|
||||||
|
response = test_client.delete("/api/v1/users/"+ str(id),headers={"Authorization": "Bearer " + login_user})
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["data"]["email"] == "delete@example.com"
|
||||||
|
response = test_client.get("/api/v1/users/"+ str(id), headers={"Authorization": "Bearer " + login_user})
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "data" not in response.json()
|
||||||
|
|
||||||
|
def test_role_assign(test_client, login_user_id,login_user,test_role):
|
||||||
|
userroles ={
|
||||||
|
"userid":login_user_id,
|
||||||
|
"roleids":[test_role["id"]]
|
||||||
|
}
|
||||||
|
response = test_client.post("/api/v1/userrole", json=userroles, headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
response = test_client.get("/api/v1/users/"+ str(login_user_id), headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "data" in data
|
||||||
|
assert len(data["data"]["roles"]) == 1
|
||||||
|
|
||||||
|
def test_roles_get(test_client,login_user):
|
||||||
|
response = test_client.get("/api/v1/roles", headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(data["data"]) == 0
|
||||||
|
|
||||||
|
def test_roles_admin_get(test_client,login_admin):
|
||||||
|
response = test_client.get("/api/v1/roles", headers={"Authorization": "Bearer " + login_admin})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(data["data"]) == 1
|
||||||
121
backend/app/tests/test_user_app.py
Normal file
121
backend/app/tests/test_user_app.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import logging
|
||||||
|
def test_create_flow(test_client,test_domain,test_app_id,login_user):
|
||||||
|
test_flow={
|
||||||
|
"flowid": "73e82bee-76a2-4347-a069-e21bf5e21111",
|
||||||
|
"appid": test_app_id,
|
||||||
|
"appname": "test_app",
|
||||||
|
"eventid": "a",
|
||||||
|
"name": "保存をクリックしたとき",
|
||||||
|
"content": ""
|
||||||
|
}
|
||||||
|
response = test_client.post("/api/flow", json=test_flow,headers={"Authorization": "Bearer " + login_user})
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "data" in data
|
||||||
|
assert data["data"] is not None
|
||||||
|
assert data["data"]["domainurl"] == test_domain.url
|
||||||
|
assert data["data"]["flowid"] == test_flow["flowid"]
|
||||||
|
assert data["data"]["appid"] == test_flow["appid"]
|
||||||
|
assert data["data"]["eventid"] == test_flow["eventid"]
|
||||||
|
assert data["data"]["content"] == test_flow["content"]
|
||||||
|
|
||||||
|
def test_delete_flow(test_client,test_domain,test_app_id,login_user):
|
||||||
|
response = test_client.delete("/api/flow/73e82bee-76a2-4347-a069-e21bf5e21111",headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "data" in data
|
||||||
|
assert data["data"] is not None
|
||||||
|
|
||||||
|
def test_edit_flow(test_client,test_domain,test_app_id,login_user):
|
||||||
|
test_flow={
|
||||||
|
"flowid": "73e82bee-76a2-4347-a069-e21bf5e21111",
|
||||||
|
"appid": test_app_id,
|
||||||
|
"appname": "test_app_new",
|
||||||
|
"eventid": "abc",
|
||||||
|
"name": "保存をクリックしたとき",
|
||||||
|
"content": ""
|
||||||
|
}
|
||||||
|
response = test_client.put("/api/flow", json=test_flow,headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "data" in data
|
||||||
|
assert data["data"] is not None
|
||||||
|
assert data["data"]["domainurl"] == test_domain.url
|
||||||
|
assert data["data"]["flowid"] == test_flow["flowid"]
|
||||||
|
assert data["data"]["appid"] == test_flow["appid"]
|
||||||
|
assert data["data"]["eventid"] == test_flow["eventid"]
|
||||||
|
assert data["data"]["content"] == test_flow["content"]
|
||||||
|
|
||||||
|
def test_appversions_update(test_client,test_domain,test_app_id,login_user):
|
||||||
|
app_version ={
|
||||||
|
"versionname": "version1",
|
||||||
|
"comment": "save version1",
|
||||||
|
"appid": test_app_id
|
||||||
|
}
|
||||||
|
response = test_client.post("/api/apps", json=app_version,headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "data" in data
|
||||||
|
assert data["data"] is not None
|
||||||
|
assert data["data"]["domainurl"] == test_domain.url
|
||||||
|
assert data["data"]["version"] == 1
|
||||||
|
assert data["data"]["appid"] == app_version["appid"]
|
||||||
|
assert data["data"]["versionname"] == app_version["versionname"]
|
||||||
|
assert data["data"]["is_saved"] == False
|
||||||
|
|
||||||
|
def test_apps_list(test_client,login_user):
|
||||||
|
response = test_client.get("/api/apps", headers={"Authorization": "Bearer " + login_user})
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "data" in data
|
||||||
|
assert data["data"] is not None
|
||||||
|
assert len(data["data"]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_appversions_list(test_client,test_domain,test_app_id,login_user):
|
||||||
|
response = test_client.get("/api/appversions/" + test_app_id , headers={"Authorization": "Bearer " + login_user})
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "data" in data
|
||||||
|
assert data["data"] is not None
|
||||||
|
assert len(data["data"]) == 1
|
||||||
|
assert "versionname" in data["data"][0]
|
||||||
|
|
||||||
|
def test_appversions_change(test_client,test_domain,test_app_id,login_user):
|
||||||
|
app_version ={
|
||||||
|
"versionname": "version2",
|
||||||
|
"comment": "test",
|
||||||
|
"appid": test_app_id
|
||||||
|
}
|
||||||
|
response = test_client.post("/api/apps", json=app_version,headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "data" in data
|
||||||
|
assert data["data"] is not None
|
||||||
|
assert data["data"]["version"] == 2
|
||||||
|
assert data["data"]["versionname"] == app_version["versionname"]
|
||||||
|
assert data["data"]["is_saved"] == False
|
||||||
|
|
||||||
|
|
||||||
|
response = test_client.put("/api/appversions/" + test_app_id +"/1", headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "data" in data
|
||||||
|
assert data["data"] is not None
|
||||||
|
assert data["data"]["domainurl"] == test_domain.url
|
||||||
|
assert data["data"]["version"] == 1
|
||||||
|
assert data["data"]["appid"] == test_app_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_app(test_client,test_app_id,login_user):
|
||||||
|
response = test_client.delete("/api/apps/"+ test_app_id, headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "data" in data
|
||||||
|
assert data["data"] is not None
|
||||||
9
backend/app/tests/test_user_kintone.py
Normal file
9
backend/app/tests/test_user_kintone.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import logging
|
||||||
|
def test_get_allapps(test_client,test_domain,login_user):
|
||||||
|
response = test_client.get("/api/v1/allapps", headers={"Authorization": "Bearer " + login_user})
|
||||||
|
data = response.json()
|
||||||
|
logging.error(data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "apps" in data
|
||||||
|
assert data["apps"] is not None
|
||||||
|
assert len(data["apps"]) > 0
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
import pytest
|
|
||||||
from sqlalchemy import create_engine, event
|
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
from sqlalchemy_utils import database_exists, create_database, drop_database
|
|
||||||
from fastapi.testclient import TestClient
|
|
||||||
import typing as t
|
|
||||||
|
|
||||||
from app.core import config, security
|
|
||||||
from app.db.session import Base, get_db
|
|
||||||
from app.db import models
|
|
||||||
from app.main import app
|
|
||||||
|
|
||||||
|
|
||||||
def get_test_db_url() -> str:
|
|
||||||
return f"{config.SQLALCHEMY_DATABASE_URI}_test"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def test_db():
|
|
||||||
"""
|
|
||||||
Modify the db session to automatically roll back after each test.
|
|
||||||
This is to avoid tests affecting the database state of other tests.
|
|
||||||
"""
|
|
||||||
# Connect to the test database
|
|
||||||
engine = create_engine(
|
|
||||||
get_test_db_url(),
|
|
||||||
)
|
|
||||||
|
|
||||||
connection = engine.connect()
|
|
||||||
trans = connection.begin()
|
|
||||||
|
|
||||||
# Run a parent transaction that can roll back all changes
|
|
||||||
test_session_maker = sessionmaker(
|
|
||||||
autocommit=False, autoflush=False, bind=engine
|
|
||||||
)
|
|
||||||
test_session = test_session_maker()
|
|
||||||
test_session.begin_nested()
|
|
||||||
|
|
||||||
@event.listens_for(test_session, "after_transaction_end")
|
|
||||||
def restart_savepoint(s, transaction):
|
|
||||||
if transaction.nested and not transaction._parent.nested:
|
|
||||||
s.expire_all()
|
|
||||||
s.begin_nested()
|
|
||||||
|
|
||||||
yield test_session
|
|
||||||
|
|
||||||
# Roll back the parent transaction after the test is complete
|
|
||||||
test_session.close()
|
|
||||||
trans.rollback()
|
|
||||||
connection.close()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session", autouse=True)
|
|
||||||
def create_test_db():
|
|
||||||
"""
|
|
||||||
Create a test database and use it for the whole test session.
|
|
||||||
"""
|
|
||||||
|
|
||||||
test_db_url = get_test_db_url()
|
|
||||||
|
|
||||||
# Create the test database
|
|
||||||
assert not database_exists(
|
|
||||||
test_db_url
|
|
||||||
), "Test database already exists. Aborting tests."
|
|
||||||
create_database(test_db_url)
|
|
||||||
test_engine = create_engine(test_db_url)
|
|
||||||
Base.metadata.create_all(test_engine)
|
|
||||||
|
|
||||||
# Run the tests
|
|
||||||
yield
|
|
||||||
|
|
||||||
# Drop the test database
|
|
||||||
drop_database(test_db_url)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def client(test_db):
|
|
||||||
"""
|
|
||||||
Get a TestClient instance that reads/write to the test database.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_test_db():
|
|
||||||
yield test_db
|
|
||||||
|
|
||||||
app.dependency_overrides[get_db] = get_test_db
|
|
||||||
|
|
||||||
yield TestClient(app)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def test_password() -> str:
|
|
||||||
return "securepassword"
|
|
||||||
|
|
||||||
|
|
||||||
def get_password_hash() -> str:
|
|
||||||
"""
|
|
||||||
Password hashing can be expensive so a mock will be much faster
|
|
||||||
"""
|
|
||||||
return "supersecrethash"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def test_user(test_db) -> models.User:
|
|
||||||
"""
|
|
||||||
Make a test user in the database
|
|
||||||
"""
|
|
||||||
|
|
||||||
user = models.User(
|
|
||||||
email="fake@email.com",
|
|
||||||
hashed_password=get_password_hash(),
|
|
||||||
is_active=True,
|
|
||||||
)
|
|
||||||
test_db.add(user)
|
|
||||||
test_db.commit()
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def test_superuser(test_db) -> models.User:
|
|
||||||
"""
|
|
||||||
Superuser for testing
|
|
||||||
"""
|
|
||||||
|
|
||||||
user = models.User(
|
|
||||||
email="fakeadmin@email.com",
|
|
||||||
hashed_password=get_password_hash(),
|
|
||||||
is_superuser=True,
|
|
||||||
)
|
|
||||||
test_db.add(user)
|
|
||||||
test_db.commit()
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
def verify_password_mock(first: str, second: str) -> bool:
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def user_token_headers(
|
|
||||||
client: TestClient, test_user, test_password, monkeypatch
|
|
||||||
) -> t.Dict[str, str]:
|
|
||||||
monkeypatch.setattr(security, "verify_password", verify_password_mock)
|
|
||||||
|
|
||||||
login_data = {
|
|
||||||
"username": test_user.email,
|
|
||||||
"password": test_password,
|
|
||||||
}
|
|
||||||
r = client.post("/api/token", data=login_data)
|
|
||||||
tokens = r.json()
|
|
||||||
a_token = tokens["access_token"]
|
|
||||||
headers = {"Authorization": f"Bearer {a_token}"}
|
|
||||||
return headers
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def superuser_token_headers(
|
|
||||||
client: TestClient, test_superuser, test_password, monkeypatch
|
|
||||||
) -> t.Dict[str, str]:
|
|
||||||
monkeypatch.setattr(security, "verify_password", verify_password_mock)
|
|
||||||
|
|
||||||
login_data = {
|
|
||||||
"username": test_superuser.email,
|
|
||||||
"password": test_password,
|
|
||||||
}
|
|
||||||
r = client.post("/api/token", data=login_data)
|
|
||||||
tokens = r.json()
|
|
||||||
a_token = tokens["access_token"]
|
|
||||||
headers = {"Authorization": f"Bearer {a_token}"}
|
|
||||||
return headers
|
|
||||||
@@ -24,4 +24,16 @@ python -m venv env
|
|||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
```
|
```
|
||||||
|
4. backend 起動
|
||||||
|
```bash
|
||||||
|
uvicorn app.main:app --reload
|
||||||
|
```
|
||||||
|
# ZCC対応
|
||||||
|
1. ENV環境中Pythone現在使用している証明書(cacert.pem)のパス確認
|
||||||
|
```
|
||||||
|
python -m certifi
|
||||||
|
# C:\Projects\AI-IOT\AppBuilderforkintone\backend\env\Scripts\python.exe: No module named certifi
|
||||||
|
```
|
||||||
|
2. 上記のコマンドを実行すると、証明書までのパスが出てくるので、どこかにメモしてください。次のコマンドで使います。
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
7285
db/kintone-dev2-db.sql
Normal file
7285
db/kintone-dev2-db.sql
Normal file
File diff suppressed because one or more lines are too long
1958
document/ALCKintone_20231012.drawio
Normal file
1958
document/ALCKintone_20231012.drawio
Normal file
File diff suppressed because one or more lines are too long
1958
document/ALCKintone_20231208.drawio
Normal file
1958
document/ALCKintone_20231208.drawio
Normal file
File diff suppressed because one or more lines are too long
BIN
document/Kinton APIイベント一覧.xlsx
Normal file
BIN
document/Kinton APIイベント一覧.xlsx
Normal file
Binary file not shown.
0
document/Kintone機能整理.drawio
Normal file
0
document/Kintone機能整理.drawio
Normal file
BIN
document/Kintone自動作成ツールのプラグインについて.xlsx
Normal file
BIN
document/Kintone自動作成ツールのプラグインについて.xlsx
Normal file
Binary file not shown.
1047
document/Kintone自動化ツール設計図.drawio
Normal file
1047
document/Kintone自動化ツール設計図.drawio
Normal file
File diff suppressed because one or more lines are too long
BIN
document/Ver2機能一覧 1.xlsx
Normal file
BIN
document/Ver2機能一覧 1.xlsx
Normal file
Binary file not shown.
BIN
document/action-property.png
Normal file
BIN
document/action-property.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
BIN
document/kintoneジェネレータ ログ仕様.xlsx
Normal file
BIN
document/kintoneジェネレータ ログ仕様.xlsx
Normal file
Binary file not shown.
BIN
document/kintone自動化ツールの設計書取込の仕様説明.xlsx
Normal file
BIN
document/kintone自動化ツールの設計書取込の仕様説明.xlsx
Normal file
Binary file not shown.
BIN
document/kintone開発自動化 MVP開発内容 SEサービスデザイングループ 2023.pptx
Normal file
BIN
document/kintone開発自動化 MVP開発内容 SEサービスデザイングループ 2023.pptx
Normal file
Binary file not shown.
BIN
document/kintone開発自動化ツール UIデザイン案.pptx
Normal file
BIN
document/kintone開発自動化ツール UIデザイン案.pptx
Normal file
Binary file not shown.
BIN
document/kintone項目種別一覧.xlsx
Normal file
BIN
document/kintone項目種別一覧.xlsx
Normal file
Binary file not shown.
147
document/サイトマップ.drawio
Normal file
147
document/サイトマップ.drawio
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
<mxfile host="app.diagrams.net" modified="2024-02-21T05:42:02.026Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36" etag="T2S5cjvthSOlO5DmGw-C" version="23.1.5" type="device">
|
||||||
|
<diagram id="Z6uZM46JtkVaKDzPjE9h" name="サイトマップ">
|
||||||
|
<mxGraphModel dx="1434" dy="820" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0" />
|
||||||
|
<mxCell id="1" parent="0" />
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-14" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="Gi77RX5G2m4J9-6cMje4-1" target="Gi77RX5G2m4J9-6cMje4-13" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-1" value="テナント登録" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.login;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="60" y="50" width="120" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-2" value="Admin Login" style="html=1;whiteSpace=wrap;strokeColor=#2D7600;fillColor=#60a917;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#ffffff;sketch=0;shape=mxgraph.sitemap.login;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="60" y="270" width="120" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-8" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="Gi77RX5G2m4J9-6cMje4-5" target="Gi77RX5G2m4J9-6cMje4-7" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-9" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="Gi77RX5G2m4J9-6cMje4-5" target="Gi77RX5G2m4J9-6cMje4-7" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-12" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="Gi77RX5G2m4J9-6cMje4-5" target="Gi77RX5G2m4J9-6cMje4-11" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-5" value="Home" style="html=1;whiteSpace=wrap;strokeColor=#2D7600;fillColor=#60a917;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#ffffff;sketch=0;shape=mxgraph.sitemap.home;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="240" y="270" width="120" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="Gi77RX5G2m4J9-6cMje4-2" target="Gi77RX5G2m4J9-6cMje4-5" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-42" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="Gi77RX5G2m4J9-6cMje4-7" target="Gi77RX5G2m4J9-6cMje4-41" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-7" value="ユーザー登録" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.home;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="440" y="220" width="120" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-11" value="ドメイン登録" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.home;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="440" y="340" width="120" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-13" value="テナント管理者作成" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.login;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="240" y="50" width="120" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-15" value="ライセンス情報" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="480" y="10" width="90" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-16" value="Adminユーザー" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="480" y="90" width="90" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-17" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="Gi77RX5G2m4J9-6cMje4-13" target="Gi77RX5G2m4J9-6cMje4-15" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-18" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="Gi77RX5G2m4J9-6cMje4-13" target="Gi77RX5G2m4J9-6cMje4-16" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-19" value="テナントDB<br>作成" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="550" y="50" width="90" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-22" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="Gi77RX5G2m4J9-6cMje4-20" target="Gi77RX5G2m4J9-6cMje4-21" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-20" value="Login" style="html=1;whiteSpace=wrap;strokeColor=#005700;fillColor=#008a00;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;sketch=0;shape=mxgraph.sitemap.login;fontColor=#ffffff;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="50" y="610" width="120" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-24" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="Gi77RX5G2m4J9-6cMje4-21" target="Gi77RX5G2m4J9-6cMje4-25" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<mxPoint x="430" y="645" as="targetPoint" />
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-21" value="Home" style="html=1;whiteSpace=wrap;strokeColor=#005700;fillColor=#008a00;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#ffffff;sketch=0;shape=mxgraph.sitemap.home;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="230" y="610" width="120" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-27" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="Gi77RX5G2m4J9-6cMje4-25" target="Gi77RX5G2m4J9-6cMje4-26" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-25" value="アプリ一覧" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.news;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="440" y="610" width="120" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-29" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="Gi77RX5G2m4J9-6cMje4-26" target="Gi77RX5G2m4J9-6cMje4-28" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-26" value="フロー一覧" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.news;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="620" y="610" width="120" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-40" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="Gi77RX5G2m4J9-6cMje4-28" target="Gi77RX5G2m4J9-6cMje4-39" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-28" value="フローエディタ" style="html=1;whiteSpace=wrap;strokeColor=#005700;fillColor=#008a00;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#ffffff;sketch=0;shape=mxgraph.sitemap.news;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="800" y="610" width="120" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-33" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="Gi77RX5G2m4J9-6cMje4-30" target="Gi77RX5G2m4J9-6cMje4-32" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-30" value="設計書取込" style="html=1;whiteSpace=wrap;strokeColor=#005700;fillColor=#008a00;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#ffffff;sketch=0;shape=mxgraph.sitemap.news;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="440" y="715" width="120" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-31" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="Gi77RX5G2m4J9-6cMje4-21" target="Gi77RX5G2m4J9-6cMje4-30" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-32" value="取込結果表示" style="html=1;whiteSpace=wrap;strokeColor=#005700;fillColor=#008a00;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#ffffff;sketch=0;shape=mxgraph.sitemap.news;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="620" y="715" width="120" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-37" value="設計書ダウロード" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.news;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="440" y="825" width="120" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-38" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="Gi77RX5G2m4J9-6cMje4-21" target="Gi77RX5G2m4J9-6cMje4-37" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-39" value="フロー履歴管理" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.news;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="980" y="610" width="120" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-41" value="ALC設定" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.home;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="620" y="220" width="120" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-43" value="管理ドメイン設定" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.home;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="800" y="220" width="120" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-44" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="Gi77RX5G2m4J9-6cMje4-41" target="Gi77RX5G2m4J9-6cMje4-43" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-45" value="プロファイル" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.home;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="440" y="935" width="120" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-46" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="Gi77RX5G2m4J9-6cMje4-21" target="Gi77RX5G2m4J9-6cMje4-45" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-50" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="Gi77RX5G2m4J9-6cMje4-47" target="Gi77RX5G2m4J9-6cMje4-49" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-47" value="プロファイル" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.home;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="440" y="450" width="120" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-48" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="Gi77RX5G2m4J9-6cMje4-5" target="Gi77RX5G2m4J9-6cMje4-47" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-49" value="ライセンス情報" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.home;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="620" y="450" width="120" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-51" value="ライセンス情報" style="html=1;whiteSpace=wrap;strokeColor=none;fillColor=#0079D6;labelPosition=center;verticalLabelPosition=middle;verticalAlign=top;align=center;fontSize=12;outlineConnect=0;spacingTop=-6;fontColor=#FFFFFF;sketch=0;shape=mxgraph.sitemap.home;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="620" y="935" width="120" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Gi77RX5G2m4J9-6cMje4-52" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="Gi77RX5G2m4J9-6cMje4-45" target="Gi77RX5G2m4J9-6cMje4-51" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
194
document/ルックアップ同期仕様.drawio
Normal file
194
document/ルックアップ同期仕様.drawio
Normal file
File diff suppressed because one or more lines are too long
BIN
document/収支明細管理設計書.xlsx
Normal file
BIN
document/収支明細管理設計書.xlsx
Normal file
Binary file not shown.
BIN
document/日報設計書new.xlsx
Normal file
BIN
document/日報設計書new.xlsx
Normal file
Binary file not shown.
@@ -1,2 +1,6 @@
|
|||||||
KAB_BACKEND_URL="http://127.0.0.1:8000/api/v1/"
|
#開発環境
|
||||||
|
#KAB_BACKEND_URL="https://ktune-backend-dev-eba8fkeyffegc3cz.japanwest-01.azurewebsites.net/"
|
||||||
|
#単体テスト環境
|
||||||
|
#KAB_BACKEND_URL="https://kab-backend-unittest.azurewebsites.net/"
|
||||||
|
#ローカル開発環境
|
||||||
|
KAB_BACKEND_URL="http://127.0.0.1:8000/"
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
VUE_BACKEND_URL="http://localhost:8000/api/"
|
#KAB_BACKEND_URL="https://kab-backend.azurewebsites.net/"
|
||||||
|
KAB_BACKEND_URL="http://127.0.0.1:8000/"
|
||||||
|
|||||||
3
frontend/.gitignore
vendored
3
frontend/.gitignore
vendored
@@ -35,3 +35,6 @@ yarn-error.log*
|
|||||||
|
|
||||||
# local .env files
|
# local .env files
|
||||||
.env.local*
|
.env.local*
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|||||||
@@ -47,3 +47,7 @@ quasar build
|
|||||||
|
|
||||||
### Customize the configuration
|
### Customize the configuration
|
||||||
See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-vite/quasar-config-js).
|
See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-vite/quasar-config-js).
|
||||||
|
|
||||||
|
## VUE3.0编程规范
|
||||||
|
1. [VUE3.0编程概要](./VUE3.0概要.md)
|
||||||
|
2. [VUE3.0编程规范](./VUE3.0-coding-rule.md)
|
||||||
|
|||||||
241
frontend/VUE3.0-coding-rule.md
Normal file
241
frontend/VUE3.0-coding-rule.md
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
以下是一份 Vue 3 和 TypeScript 的编程规范:
|
||||||
|
|
||||||
|
**1. 组件定义**
|
||||||
|
|
||||||
|
- 推荐使用 `defineComponent` 来定义组件,以获取 TypeScript 的类型支持。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
// 组件选项
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. 数据定义**
|
||||||
|
|
||||||
|
- 使用 `reactive` 定义响应式对象。
|
||||||
|
- 使用 `ref` 定义响应式单值。
|
||||||
|
- 使用 `computed` 定义计算属性。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { reactive, ref, computed } from 'vue'
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
count: 0,
|
||||||
|
message: 'Hello Vue 3'
|
||||||
|
})
|
||||||
|
|
||||||
|
const count = ref(0)
|
||||||
|
|
||||||
|
const doubledCount = computed(() => state.count * 2)
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. 生命周期钩子**
|
||||||
|
|
||||||
|
- 使用 `onMounted`、`onUpdated` 等生命周期钩子,而不是 `beforeCreate`、`created`、`beforeMount`、`mounted`、`beforeUpdate`、`updated`、`beforeUnmount`、`unmounted`。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.log('Component is mounted.')
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**4. 组件通信**
|
||||||
|
|
||||||
|
- 使用 `props` 和 `emit` 实现父子组件通信。
|
||||||
|
- 使用 `provide` 和 `inject` 实现祖先和后代组件通信。
|
||||||
|
- 不再推荐使用 `event bus` 进行任意组件间的通信,可以使用 Vuex 或者全局 `provide`/`inject` 替代。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 父组件
|
||||||
|
<ChildComponent @my-event="handleEvent" />
|
||||||
|
|
||||||
|
// 子组件
|
||||||
|
this.$emit('my-event', eventData)
|
||||||
|
```
|
||||||
|
|
||||||
|
**5. 异步处理**
|
||||||
|
|
||||||
|
- 使用 `async/await` 进行异步处理。
|
||||||
|
- 使用 `Suspense` 和 `async setup()` 处理异步依赖。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const response = await axios.get('https://api.example.com/data')
|
||||||
|
data.value = response.data
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**6. Vue Router 和 Vuex**
|
||||||
|
|
||||||
|
- 使用 Vue Router 4 和 Vuex 4,它们是为 Vue 3 重新设计的。
|
||||||
|
- 使用 `useRouter` 和 `useRoute` 钩子函数在组件中使用 router。
|
||||||
|
- 使用 `useStore` 钩子函数在组件中使用 Vuex store。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
const router = useRouter()
|
||||||
|
```
|
||||||
|
|
||||||
|
**7. 组件模板**
|
||||||
|
|
||||||
|
- 使用 `v-model` 代替 `.sync` 修饰符进行双向绑定。
|
||||||
|
- 使用 `v-for` 和 `:key` 渲染列表。
|
||||||
|
- 使用 `v-if` 和 `v-else`、`v-else-if` 进行条件渲染。
|
||||||
|
- 使用 `v-on` 或者 `@` 监听事件。
|
||||||
|
- 使用 `v-bind` 或者 `:` 绑定属性。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template>
|
||||||
|
<div v-if="condition">If block</div>
|
||||||
|
<div v-else>Else block</div>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li v-for="(item, index) in list" :key="index">{{ item }}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<button @click="handleClick">Click me</button>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
8. **使用类型注解和接口**
|
||||||
|
|
||||||
|
在 TypeScript 中,尽可能使用类型注解和接口来提供更完善的类型信息和类型检查。这将有助于发现和预防错误。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface User {
|
||||||
|
name: string;
|
||||||
|
age: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user: User = {
|
||||||
|
name: 'John',
|
||||||
|
age: 30
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
9. **模块化和组件化**
|
||||||
|
|
||||||
|
尽可能将功能和逻辑模块化和组件化,使得代码更易于理解和维护。特别是使用 Composition API 时,可以将公共的逻辑封装成 composable 函数。
|
||||||
|
|
||||||
|
下面是一个使用 `Suspense` 和 `axios` 的示例,这个示例将会从一个 JSON Placeholder API 获取数据:
|
||||||
|
|
||||||
|
首先,我们创建一个 composable 函数,用于获取数据:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export function useAsyncData(url: string) {
|
||||||
|
const data = ref(null)
|
||||||
|
const error = ref(null)
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(url)
|
||||||
|
data.value = response.data
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data, error, fetchData }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
然后,我们创建一个 Vue 组件来使用这个函数:
|
||||||
|
|
||||||
|
```js
|
||||||
|
<template>
|
||||||
|
<Suspense>
|
||||||
|
<template #default>
|
||||||
|
<div v-if="error">{{ error.message }}</div>
|
||||||
|
<div v-else>{{ data }}</div>
|
||||||
|
</template>
|
||||||
|
<template #fallback>
|
||||||
|
<div>Loading...</div>
|
||||||
|
</template>
|
||||||
|
</Suspense>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, onMounted } from 'vue'
|
||||||
|
import { useAsyncData } from './composables/useAsyncData'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const { data, error, fetchData } = useAsyncData('https://jsonplaceholder.typicode.com/posts/1')
|
||||||
|
|
||||||
|
onMounted(fetchData)
|
||||||
|
|
||||||
|
return { data, error }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 在这里添加 CSS 样式 */
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
在这个示例中,我们在 `useAsyncData` 函数中获取数据。当 `onMounted` 钩子函数被调用时(也就是当组件挂载完成后),我们开始获取数据。这个过程是异步的,因此我们在 `Suspense` 组件的 `#fallback` 插槽中显示一个 "Loading..." 的提示。一旦数据加载完成,`Suspense` 组件的 `#default` 插槽就会被渲染,并显示获取到的数据。
|
||||||
|
|
||||||
|
10. **良好的代码风格**
|
||||||
|
|
||||||
|
遵循一致的代码风格和代码质量规则,例如使用 ESLint 和 Prettier 来检查和格式化代码。
|
||||||
|
|
||||||
|
11. **单元测试和端到端测试**
|
||||||
|
|
||||||
|
对关键的组件和函数编写单元测试,对用户的主要操作路径编写端到端测试,确保功能的正确性。
|
||||||
|
|
||||||
|
12. **注释和文档**
|
||||||
|
|
||||||
|
对复杂的逻辑和函数编写注释,提供必要的项目文档,帮助其他开发者理解和使用你的代码。
|
||||||
|
|
||||||
|
13. **以下是 Vue 2 中被废弃或改变的部分API,以及在 Vue 3 中的替代方案:**
|
||||||
|
|
||||||
|
| Vue 2.x | Vue 3.0 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Vue.set` / `this.$set` | 响应式数据现在是默认的,无需使用这些方法 | Vue 3 的响应式系统是从头开始构建的,所有的对象和数组都是响应式的 |
|
||||||
|
| `Vue.delete` / `this.$delete` | 无需使用这些方法 | 在 Vue 3 中,你只需要使用 `delete` 操作符即可 |
|
||||||
|
| `filters` | 无对应项 | Vue 3 不再支持过滤器,建议使用计算属性或方法替代 |
|
||||||
|
| `Vue.observable` | `reactive` | 用 `reactive` 替代 `Vue.observable`,实现数据的响应式 |
|
||||||
|
| `Vue.prototype` | `app.config.globalProperties` | 在 Vue 3 中,全局 API 已经改变,`Vue.prototype` 被 `app.config.globalProperties` 替代 |
|
||||||
|
| `Vue.component`, `Vue.directive`, `Vue.mixin`, `Vue.use` | `app.component`, `app.directive`, `app.mixin`, `app.use` | 全局注册的 API 改变,如 `Vue.component` 变为 `app.component` |
|
||||||
|
| `beforeDestroy` 和 `destroyed` 生命周期钩子 | `beforeUnmount` 和 `unmounted` | 生命周期钩子名字变化,`beforeDestroy` 和 `destroyed` 分别改为 `beforeUnmount` 和 `unmounted` |
|
||||||
|
| `$on`, `$off`, `$once` | 无对应项 | Event Bus方法被移除,需要用户自行实现或者使用第三方库 |
|
||||||
|
| `v-model` 在自定义组件上使用 | 需要明确的 `modelValue` 和 `update:modelValue` | Vue 3 对 `v-model` 的改动使其在自定义组件上更具灵活性 |
|
||||||
|
| `functional` 选项 | 无对应项 | Vue 3 不再支持函数式组件的写法,而是推荐使用 `render` 函数或 `setup` 函数 |
|
||||||
|
| 异步组件的 `functional` 写法 | `defineAsyncComponent` | 异步组件的创建方式更改,通过 `defineAsyncComponent` 方法创建 |
|
||||||
|
| `destroyed` 和 `beforeDestroy` 钩子函数 | `unmounted` 和 `beforeUnmount` | 生命周期钩子的名称已改为更直观的名称,以更好地表示其在组件实例生命周期中的角色 |
|
||||||
|
| `Vue.extend` | `defineComponent` | Vue 3 使用 `defineComponent` 方法定义组件,有更好的类型推断 |
|
||||||
|
|
||||||
|
注意:Vue 3 对于 Options API 和 Composition API 提供了完全的支持,你可以在一个组件中混合使用这两种 API。不过,为了代码的一致性和可读性,建议在一个项目中选择一种 API 并坚持使用。
|
||||||
|
|
||||||
|
|
||||||
|
- **以下是 Vue 3.0 中的 Composition API 函数的基本说明,以及与 Options API 的对比**
|
||||||
|
|
||||||
|
| Composition API | 说明 | 对应的 Options API |
|
||||||
|
| --------------- | ---- | ------------------ |
|
||||||
|
| `setup` | `setup` 是一个新引入的组件选项,用于使用 Composition API。它是组件内部使用 Composition API 的入口。| 无 |
|
||||||
|
| `ref` | `ref` 函数用于创建一个响应式的数据。它接收一个参数,返回一个响应式的 Ref 对象。| `data` |
|
||||||
|
| `reactive` | `reactive` 函数用于创建一个响应式的对象。它接收一个普通对象,返回一个响应式的对象。| `data` |
|
||||||
|
| `computed` | `computed` 函数用于创建一个计算属性。它接收一个 getter 函数或者一个具有 getter 和 setter 的对象,返回一个响应式的 Ref 对象。| `computed` |
|
||||||
|
| `watch` | `watch` 函数用于响应式地跟踪和触发副作用。它接收一个响应式的源和一个执行副作用的回调函数。| `watch` |
|
||||||
|
| `watchEffect` | `watchEffect` 函数用于立即执行传入的一个函数,并响应式地追踪其依赖,并在其依赖变更时重新运行该函数。 | 无 |
|
||||||
|
| `onMounted` | `onMounted` 函数在组件被挂载时调用。它接收一个在组件挂载后执行的回调函数。 | `mounted` |
|
||||||
|
| `onUnmounted` | `onUnmounted` 函数在组件被卸载时调用。它接收一个在组件卸载后执行的回调函数。 | `beforeDestroy`/`unmounted` |
|
||||||
|
| `onUpdated` | `onUpdated` 在组件更新后调用。它接收一个在组件更新后执行的回调函数。| `updated` |
|
||||||
|
| `provide` | `provide` 函数用于在组件上定义一个可以被后代组件注入的值。它接收一个提供的键和值。 | 有,与 `provide/inject` 相似但是属性而不是函数 |
|
||||||
|
| `inject` | `inject` 函数用于在组件中注入一个由祖先组件提供的值。它接收一个注入的键。 | 有,但与 `provide/inject` 相似但是属性而不是函数 |
|
||||||
|
|
||||||
|
值得注意的是,虽然一些 Composition API 函数与 Options API 的某些选项有相似之处,但它们的工作方式和使用方式可能有所不同。在实际使用中,你需要根据具体的使用场景和需求选择合适的 API。
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="ja-jp">
|
||||||
<head>
|
<head>
|
||||||
<title><%= productName %></title>
|
<title><%= productName %></title>
|
||||||
|
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="description" content="<%= productDescription %>">
|
<meta name="description" content="<%= productDescription %>">
|
||||||
<meta name="format-detection" content="telephone=no">
|
<meta name="format-detection" content="telephone=no">
|
||||||
|
|||||||
72
frontend/package-lock.json
generated
72
frontend/package-lock.json
generated
@@ -10,13 +10,16 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@quasar/extras": "^1.16.4",
|
"@quasar/extras": "^1.16.4",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
|
"pinia": "^2.1.6",
|
||||||
"quasar": "^2.6.0",
|
"quasar": "^2.6.0",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
"vue": "^3.0.0",
|
"vue": "^3.0.0",
|
||||||
"vue-router": "^4.0.0"
|
"vue-router": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@quasar/app-vite": "^1.3.0",
|
"@quasar/app-vite": "^1.3.0",
|
||||||
"@types/node": "^12.20.21",
|
"@types/node": "^12.20.21",
|
||||||
|
"@types/uuid": "^9.0.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
||||||
"@typescript-eslint/parser": "^5.10.0",
|
"@typescript-eslint/parser": "^5.10.0",
|
||||||
"autoprefixer": "^10.4.2",
|
"autoprefixer": "^10.4.2",
|
||||||
@@ -28,8 +31,9 @@
|
|||||||
"typescript": "^4.5.4"
|
"typescript": "^4.5.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18 || ^16 || ^14.19",
|
"node": "^20 ||^18 || ^16 || ^14.19",
|
||||||
"npm": ">= 6.13.4",
|
"npm": ">= 6.13.4",
|
||||||
|
"pnpm": ">=8.6.0",
|
||||||
"yarn": ">= 1.21.1"
|
"yarn": ">= 1.21.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -544,6 +548,12 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/uuid": {
|
||||||
|
"version": "9.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.3.tgz",
|
||||||
|
"integrity": "sha512-taHQQH/3ZyI3zP8M/puluDEIEvtQHVYcC6y3N8ijFtAd28+Ey/G4sg1u2gB01S8MwybLOKAp9/yCMu/uR5l3Ug==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "5.61.0",
|
"version": "5.61.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.61.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.61.0.tgz",
|
||||||
@@ -4070,6 +4080,56 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pinia": {
|
||||||
|
"version": "2.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.6.tgz",
|
||||||
|
"integrity": "sha512-bIU6QuE5qZviMmct5XwCesXelb5VavdOWKWaB17ggk++NUwQWWbP5YnsONTk3b752QkW9sACiR81rorpeOMSvQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-api": "^6.5.0",
|
||||||
|
"vue-demi": ">=0.14.5"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/posva"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/composition-api": "^1.4.0",
|
||||||
|
"typescript": ">=4.4.4",
|
||||||
|
"vue": "^2.6.14 || ^3.3.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pinia/node_modules/vue-demi": {
|
||||||
|
"version": "0.14.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz",
|
||||||
|
"integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"bin": {
|
||||||
|
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||||
|
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/composition-api": "^1.0.0-rc.1",
|
||||||
|
"vue": "^3.0.0-0 || ^2.6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.4.25",
|
"version": "8.4.25",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.25.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.25.tgz",
|
||||||
@@ -4946,7 +5006,7 @@
|
|||||||
"version": "4.9.5",
|
"version": "4.9.5",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -5045,6 +5105,14 @@
|
|||||||
"node": ">= 0.4.0"
|
"node": ">= 0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "9.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
|
||||||
|
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vary": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "kintone-app-builder",
|
"name": "k-tune",
|
||||||
"version": "0.0.1",
|
"version": "2.0.0 Beta",
|
||||||
"description": "Kintoneアプリの自動生成とデプロイを支援ツールです",
|
"description": "Kintoneアプリの自動生成とデプロイを支援ツールです",
|
||||||
"productName": "Kintone App Builder",
|
"productName": "k-tune | kintoneジェネレーター",
|
||||||
"author": "maxiaozhe@alicorns.co.jp <maxiaozhe@alicorns.co.jp>",
|
"author": "maxiaozhe@alicorns.co.jp <maxiaozhe@alicorns.co.jp>",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -10,18 +10,26 @@
|
|||||||
"format": "prettier --write \"**/*.{js,ts,vue,scss,html,md,json}\" --ignore-path .gitignore",
|
"format": "prettier --write \"**/*.{js,ts,vue,scss,html,md,json}\" --ignore-path .gitignore",
|
||||||
"test": "echo \"No test specified\" && exit 0",
|
"test": "echo \"No test specified\" && exit 0",
|
||||||
"dev": "quasar dev",
|
"dev": "quasar dev",
|
||||||
"build": "quasar build"
|
"dev:local": "set \"LOCAL=true\" && quasar dev",
|
||||||
|
"build": "set \"SOURCE_MAP=false\" && quasar build",
|
||||||
|
"build:dev": "set \"SOURCE_MAP=true\" && quasar build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@quasar/extras": "^1.16.4",
|
"@quasar/extras": "^1.16.4",
|
||||||
|
"@vueuse/core": "^10.9.0",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
|
"pinia-plugin-persistedstate": "^3.2.1",
|
||||||
"quasar": "^2.6.0",
|
"quasar": "^2.6.0",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
"vue": "^3.0.0",
|
"vue": "^3.0.0",
|
||||||
"vue-router": "^4.0.0"
|
"vue-router": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@quasar/app-vite": "^1.3.0",
|
"@quasar/app-vite": "^1.3.0",
|
||||||
"@types/node": "^12.20.21",
|
"@types/node": "^12.20.21",
|
||||||
|
"@types/uuid": "^9.0.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
||||||
"@typescript-eslint/parser": "^5.10.0",
|
"@typescript-eslint/parser": "^5.10.0",
|
||||||
"autoprefixer": "^10.4.2",
|
"autoprefixer": "^10.4.2",
|
||||||
@@ -33,8 +41,9 @@
|
|||||||
"typescript": "^4.5.4"
|
"typescript": "^4.5.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18 || ^16 || ^14.19",
|
"node": "^20 ||^18 || ^16 || ^14.19",
|
||||||
"npm": ">= 6.13.4",
|
"npm": ">= 6.13.4",
|
||||||
"yarn": ">= 1.21.1"
|
"yarn": ">= 1.21.1",
|
||||||
|
"pnpm": ">=8.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
frontend/public/web.config
Normal file
8
frontend/public/web.config
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<configuration>
|
||||||
|
<system.webServer>
|
||||||
|
<staticContent>
|
||||||
|
<mimeMap fileExtension=".woff" mimeType="application/font-woff" />
|
||||||
|
<mimeMap fileExtension=".woff2" mimeType="application/font-woff2" />
|
||||||
|
</staticContent>
|
||||||
|
</system.webServer>
|
||||||
|
</configuration>
|
||||||
@@ -10,10 +10,14 @@
|
|||||||
|
|
||||||
|
|
||||||
const { configure } = require('quasar/wrappers');
|
const { configure } = require('quasar/wrappers');
|
||||||
const dotenv = require('dotenv').config().parsed;
|
const envPath = process.env.LOCAL==='true'?'.env.development':'.env';
|
||||||
const package = require('./package.json');
|
const dotenv = require('dotenv').config({path:envPath}).parsed;
|
||||||
|
console.log('dotenv=>',dotenv);
|
||||||
|
// const package = require('./package.json');
|
||||||
const { Notify } = require('quasar');
|
const { Notify } = require('quasar');
|
||||||
const version = package.version;
|
const version = process.env.npm_package_version;
|
||||||
|
const productName=process.env.npm_package_productName;
|
||||||
|
// console.log(process.env);
|
||||||
module.exports = configure(function (/* ctx */) {
|
module.exports = configure(function (/* ctx */) {
|
||||||
return {
|
return {
|
||||||
eslint: {
|
eslint: {
|
||||||
@@ -32,7 +36,9 @@ module.exports = configure(function (/* ctx */) {
|
|||||||
// --> boot files are part of "main.js"
|
// --> boot files are part of "main.js"
|
||||||
// https://v2.quasar.dev/quasar-cli-vite/boot-files
|
// https://v2.quasar.dev/quasar-cli-vite/boot-files
|
||||||
boot: [
|
boot: [
|
||||||
'axios'
|
'axios',
|
||||||
|
'error-handler',
|
||||||
|
'permissions'
|
||||||
],
|
],
|
||||||
|
|
||||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
|
||||||
@@ -49,7 +55,6 @@ module.exports = configure(function (/* ctx */) {
|
|||||||
// 'themify',
|
// 'themify',
|
||||||
// 'line-awesome',
|
// 'line-awesome',
|
||||||
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
|
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
|
||||||
|
|
||||||
'roboto-font', // optional, you are not bound to it
|
'roboto-font', // optional, you are not bound to it
|
||||||
'material-icons', // optional, you are not bound to it
|
'material-icons', // optional, you are not bound to it
|
||||||
],
|
],
|
||||||
@@ -60,6 +65,7 @@ module.exports = configure(function (/* ctx */) {
|
|||||||
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
|
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
|
||||||
node: 'node16'
|
node: 'node16'
|
||||||
},
|
},
|
||||||
|
sourcemap:process.env.SOURCE_MAP === 'true',
|
||||||
|
|
||||||
vueRouterMode: 'hash', // available values: 'hash', 'history'
|
vueRouterMode: 'hash', // available values: 'hash', 'history'
|
||||||
// vueRouterBase,
|
// vueRouterBase,
|
||||||
@@ -70,7 +76,7 @@ module.exports = configure(function (/* ctx */) {
|
|||||||
|
|
||||||
// publicPath: '/',
|
// publicPath: '/',
|
||||||
// analyze: true,
|
// analyze: true,
|
||||||
env: { ...dotenv, version },
|
env: { ...dotenv, version ,productName},
|
||||||
// rawDefine: {}
|
// rawDefine: {}
|
||||||
// ignorePublicFolder: true,
|
// ignorePublicFolder: true,
|
||||||
// minify: false,
|
// minify: false,
|
||||||
@@ -89,6 +95,7 @@ module.exports = configure(function (/* ctx */) {
|
|||||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
|
||||||
devServer: {
|
devServer: {
|
||||||
// https: true
|
// https: true
|
||||||
|
port:9001,
|
||||||
open: true, // opens browser window automatically
|
open: true, // opens browser window automatically
|
||||||
env: { ...dotenv },
|
env: { ...dotenv },
|
||||||
},
|
},
|
||||||
@@ -98,7 +105,7 @@ module.exports = configure(function (/* ctx */) {
|
|||||||
config: {},
|
config: {},
|
||||||
|
|
||||||
// iconSet: 'material-icons', // Quasar icon set
|
// iconSet: 'material-icons', // Quasar icon set
|
||||||
// lang: 'en-US', // Quasar language pack
|
lang: 'ja', // Quasar language pack
|
||||||
|
|
||||||
// For special cases outside of where the auto-import strategy can have an impact
|
// For special cases outside of where the auto-import strategy can have an impact
|
||||||
// (like functional components as one of the examples),
|
// (like functional components as one of the examples),
|
||||||
@@ -109,7 +116,8 @@ module.exports = configure(function (/* ctx */) {
|
|||||||
|
|
||||||
// Quasar plugins
|
// Quasar plugins
|
||||||
plugins: [
|
plugins: [
|
||||||
'Notify'
|
'Notify',
|
||||||
|
'Dialog'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { boot } from 'quasar/wrappers';
|
import { boot } from 'quasar/wrappers';
|
||||||
import axios, { AxiosInstance } from 'axios';
|
import axios, { AxiosInstance, AxiosResponse } from 'axios';
|
||||||
|
import {router} from 'src/router';
|
||||||
|
import { IResponse } from 'src/types/BaseTypes';
|
||||||
|
|
||||||
|
|
||||||
declare module '@vue/runtime-core' {
|
declare module '@vue/runtime-core' {
|
||||||
interface ComponentCustomProperties {
|
interface ComponentCustomProperties {
|
||||||
@@ -14,11 +17,10 @@ declare module '@vue/runtime-core' {
|
|||||||
// good idea to move this instance creation inside of the
|
// good idea to move this instance creation inside of the
|
||||||
// "export default () => {}" function below (which runs individually
|
// "export default () => {}" function below (which runs individually
|
||||||
// for each client)
|
// for each client)
|
||||||
const api:AxiosInstance = axios.create({ baseURL: process.env.KAB_BACKEND_URL });
|
|
||||||
|
|
||||||
|
const api:AxiosInstance = axios.create({ baseURL: process.env.KAB_BACKEND_URL });
|
||||||
export default boot(({ app }) => {
|
export default boot(({ app }) => {
|
||||||
// for use inside Vue files (Options API) through this.$axios and this.$api
|
// for use inside Vue files (Options API) through this.$axios and this.$api
|
||||||
|
|
||||||
app.config.globalProperties.$axios = axios;
|
app.config.globalProperties.$axios = axios;
|
||||||
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
|
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
|
||||||
// so you won't necessarily have to import axios in each vue file
|
// so you won't necessarily have to import axios in each vue file
|
||||||
|
|||||||
21
frontend/src/boot/error-handler.ts
Normal file
21
frontend/src/boot/error-handler.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// src/boot/error-handler.ts
|
||||||
|
import { boot } from 'quasar/wrappers';
|
||||||
|
import { Router } from 'vue-router';
|
||||||
|
import { App } from 'vue';
|
||||||
|
|
||||||
|
export default boot(({ app, router }: { app: App<Element>; router: Router }) => {
|
||||||
|
document.documentElement.lang='ja-JP';
|
||||||
|
app.config.errorHandler = (err: any, instance: any, info: string) => {
|
||||||
|
if (err.response && err.response.status === 401) {
|
||||||
|
// 認証エラーの場合再ログインする
|
||||||
|
console.error('(; ゚Д゚)/認証エラー(401):', err, info);
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
router.replace({
|
||||||
|
path:'/login',
|
||||||
|
query:{redirect:router.currentRoute.value.fullPath}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('(; ゚Д゚)例外:', err, info);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
75
frontend/src/boot/permissions.ts
Normal file
75
frontend/src/boot/permissions.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// src/boot/permissions.ts
|
||||||
|
import { boot } from 'quasar/wrappers';
|
||||||
|
import { useAuthStore } from 'src/stores/useAuthStore';
|
||||||
|
import { DirectiveBinding } from 'vue';
|
||||||
|
|
||||||
|
export const MenuMapping = {
|
||||||
|
home: null,
|
||||||
|
app: null,
|
||||||
|
version: null,
|
||||||
|
user: 'user',
|
||||||
|
role: 'role',
|
||||||
|
domain: null,
|
||||||
|
userDomain: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Actions = {
|
||||||
|
user: {
|
||||||
|
show: 'user_list',
|
||||||
|
add: 'user_add',
|
||||||
|
edit: 'user_edit',
|
||||||
|
delete: 'user_delete',
|
||||||
|
},
|
||||||
|
role: {
|
||||||
|
show: 'role_list',
|
||||||
|
},
|
||||||
|
domain: {
|
||||||
|
show: 'domain_list',
|
||||||
|
add: 'domain_add',
|
||||||
|
edit: 'domain_edit',
|
||||||
|
delete: 'domain_delete',
|
||||||
|
grantUse: {
|
||||||
|
list: 'domain_grant_use_list',
|
||||||
|
edit: 'domain_grant_use_edit',
|
||||||
|
},
|
||||||
|
grantManage: {
|
||||||
|
list: 'domain_grant_manage_list',
|
||||||
|
edit: 'domain_grant_manage_edit',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = useAuthStore();
|
||||||
|
|
||||||
|
export default boot(({ app }) => {
|
||||||
|
app.directive('permissions', {
|
||||||
|
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||||
|
if (!hasPermission(binding.value)) {
|
||||||
|
hideElement(el);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
app.config.globalProperties.$hasPermission = hasPermission;
|
||||||
|
});
|
||||||
|
|
||||||
|
function hasPermission(value: any) {
|
||||||
|
if (!value || store.isSuperAdmin) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return store.permissions[value];
|
||||||
|
} else if (typeof value === 'object') {
|
||||||
|
return Object.values(value).some((permission: any) => store.permissions[permission]);
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
return value.some((permission) => store.permissions[permission]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideElement(el: HTMLElement) {
|
||||||
|
if (el.parentNode) {
|
||||||
|
el.parentNode.removeChild(el);
|
||||||
|
} else {
|
||||||
|
el.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
134
frontend/src/components/ActionSelect.vue
Normal file
134
frontend/src/components/ActionSelect.vue
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<template>
|
||||||
|
<div class="q-pa-md">
|
||||||
|
<div v-if="!isLoaded" class="spinner flex flex-center">
|
||||||
|
<q-spinner color="primary" size="3em" />
|
||||||
|
</div>
|
||||||
|
<q-splitter
|
||||||
|
v-model="splitterModel"
|
||||||
|
style="height: 100%"
|
||||||
|
before-class="tab"
|
||||||
|
unit="px"
|
||||||
|
v-else
|
||||||
|
>
|
||||||
|
<template v-slot:before>
|
||||||
|
<q-tabs
|
||||||
|
v-model="tab"
|
||||||
|
vertical
|
||||||
|
active-color="white"
|
||||||
|
indicator-color="primary"
|
||||||
|
active-bg-color="primary"
|
||||||
|
class="bg-grey-2 text-grey-8"
|
||||||
|
@update:model-value="() => selected = []"
|
||||||
|
dense
|
||||||
|
>
|
||||||
|
<q-tab :name="cate"
|
||||||
|
:label="cate"
|
||||||
|
v-for="(cate,) in categorys"
|
||||||
|
:key="cate"
|
||||||
|
></q-tab>
|
||||||
|
</q-tabs>
|
||||||
|
</template>
|
||||||
|
<template v-slot:after>
|
||||||
|
<q-table row-key="index" :selection="type" v-model:selected="selected" :columns="columns" :rows="actionForTab"
|
||||||
|
class="action-table"
|
||||||
|
flat bordered
|
||||||
|
virtual-scroll
|
||||||
|
:pagination="pagination"
|
||||||
|
:rows-per-page-options="[0]"
|
||||||
|
:filter="filter"></q-table>
|
||||||
|
</template>
|
||||||
|
</q-splitter>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { ref,onMounted,reactive,watchEffect,computed,watch } from 'vue'
|
||||||
|
import { api } from 'boot/axios';
|
||||||
|
import { useFlowEditorStore } from 'stores/flowEditor';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'actionSelect',
|
||||||
|
props: {
|
||||||
|
name: String,
|
||||||
|
type: String,
|
||||||
|
filter:String
|
||||||
|
},
|
||||||
|
emits:[
|
||||||
|
'clearFilter'
|
||||||
|
],
|
||||||
|
setup(props,{emit}) {
|
||||||
|
const isLoaded=ref(false);
|
||||||
|
const columns = [
|
||||||
|
{ name: 'name', required: true,label: 'アクション名',align: 'left',field: 'name',sortable: true},
|
||||||
|
{ name: 'desc', align: 'left', label: '説明', field: 'desc', sortable: true },
|
||||||
|
// { name: 'content', label: '内容', field: 'content', sortable: true }
|
||||||
|
];
|
||||||
|
const store = useFlowEditorStore();
|
||||||
|
let actionData =reactive([]);
|
||||||
|
const categorys = ref('');
|
||||||
|
const tab=ref('');
|
||||||
|
const actionForTab=computed(()=>{
|
||||||
|
const rows=[];
|
||||||
|
const actions= props.filter? actionData:actionData.filter(x=>x.categoryname===tab.value);
|
||||||
|
actions.forEach((item,index) =>{
|
||||||
|
rows.push({index,
|
||||||
|
name:item.name,
|
||||||
|
desc:item.title,
|
||||||
|
outputPoints:item.outputpoints,
|
||||||
|
property:item.property});
|
||||||
|
});
|
||||||
|
return rows;
|
||||||
|
});
|
||||||
|
onMounted(async () => {
|
||||||
|
let eventId='';
|
||||||
|
if(store.selectedEvent ){
|
||||||
|
eventId=store.selectedEvent.header!=='DELETABLE'? store.selectedEvent.eventId : store.selectedEvent.parentId;
|
||||||
|
}
|
||||||
|
const res =await api.get(`api/eventactions/${eventId}`);
|
||||||
|
actionData= res.data;
|
||||||
|
const categoryNames = Array.from(new Set(actionData.map(x=>x.categoryname)));
|
||||||
|
categorys.value=categoryNames;
|
||||||
|
tab.value = categoryNames.length>0? categoryNames[0]:'';
|
||||||
|
isLoaded.value=true;
|
||||||
|
});
|
||||||
|
// watch(props.filter,()=>{
|
||||||
|
// if(props.filter && props.filter!==''){
|
||||||
|
// tab.value='';
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
watch(tab,()=>{
|
||||||
|
if(tab.value!==''){
|
||||||
|
emit('clearFilter','');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// watchEffect(()=>{
|
||||||
|
// if(props.filter && props.filter!==''){
|
||||||
|
// tab.value='';
|
||||||
|
// }
|
||||||
|
// if(tab.value!==''){
|
||||||
|
// emit('update:filter','');
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
return {
|
||||||
|
columns,
|
||||||
|
selected: ref([]),
|
||||||
|
pagination:ref({
|
||||||
|
rowsPerPage:0
|
||||||
|
}),
|
||||||
|
isLoaded,
|
||||||
|
tab,
|
||||||
|
actionData,
|
||||||
|
categorys,
|
||||||
|
splitterModel: ref(150),
|
||||||
|
actionForTab
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
.action-table{
|
||||||
|
min-height: 10vh;
|
||||||
|
max-height: 68vh;
|
||||||
|
min-width: 550px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
83
frontend/src/components/ActionSetting.vue
Normal file
83
frontend/src/components/ActionSetting.vue
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<div id="action1">
|
||||||
|
<q-card class="my-card">
|
||||||
|
<q-card-section class="bg-primary text-white">
|
||||||
|
<div class="text-h6">Our Changing Planet</div>
|
||||||
|
<div class="text-subtitle2">by John Doe</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-separator />
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn flat>設定</q-btn>
|
||||||
|
<q-btn class="del" flat @click="clickdel">削除</q-btn>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
<div class="next" style="display: table;width: 100%;height:40px;" @mouseenter="showAdd = true" @mouseleave="()=>{if(!showMenu) showAdd = false;}">
|
||||||
|
<div aria-hidden="false" style="display: table-row;">
|
||||||
|
<div
|
||||||
|
style="display: table-cell;background: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iNDAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDEzIDMwIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAyMCA0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCiA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPi5zdDB7ZmlsbDpub25lO3N0cm9rZTojNUI1QjVDO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1taXRlcmxpbWl0OjEwO30NCgkuc3Qxe2ZpbGw6IzVCNUI1Qzt9PC9zdHlsZT4NCiA8cmVjdCB4PSI5IiB3aWR0aD0iMiIgaGVpZ2h0PSIzNy45NzQiIGZpbGw9IiM1MTUxNTEiIHN0cm9rZS13aWR0aD0iNC41MDYyIi8+DQogPHBvbHlnb24gdHJhbnNmb3JtPSJtYXRyaXgoMS4xNzg1IDAgMCAxLjE3ODUgLS42MDY5MiAyMy41NDQpIiBwb2ludHM9IjEuOTI4IDQuMDY1IDguOTk5IDExLjEzNiAxNi4wNzIgNC4wNjUgMTcuNDg2IDUuNDc5IDguOTk5IDEzLjk2NCAwLjUxNSA1LjQ3OSIgZmlsbD0iIzUxNTE1MSIvPg0KPC9zdmc+DQo=") center center no-repeat;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="showAdd" style="display:table-row;height:inherit;position:absolute;left:50%;">
|
||||||
|
<div style="display:table-cell;">
|
||||||
|
<q-btn round size="xs" color="primary" label="+">
|
||||||
|
<q-menu v-model="showMenu">
|
||||||
|
<q-list style="min-width: 100px">
|
||||||
|
<q-item clickable v-close-popup>
|
||||||
|
<q-item-section @click="clickadd">New tab</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { ref,watch } from 'vue'
|
||||||
|
export default {
|
||||||
|
emits: [
|
||||||
|
'addaction'
|
||||||
|
],
|
||||||
|
setup(props,context) {
|
||||||
|
const showAdd = ref(false)
|
||||||
|
const showMenu = ref(false)
|
||||||
|
|
||||||
|
watch(showMenu,(newVal) =>{
|
||||||
|
console.log('3');
|
||||||
|
if(!newVal)
|
||||||
|
{
|
||||||
|
showAdd.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const clickadd = () => {
|
||||||
|
console.log('3');
|
||||||
|
context.emit('addaction');
|
||||||
|
//let oDiv1 = pdiv;
|
||||||
|
// let oDiv1 = document.getElementById('action1');
|
||||||
|
// let oDiv2 = document.createElement('div');
|
||||||
|
// if (oDiv1 !== null) {
|
||||||
|
// oDiv2.innerHTML = oDiv1?.innerHTML;
|
||||||
|
// oDiv1?.after(oDiv2);
|
||||||
|
// let oAdd = oDiv2.getElementsByClassName('next')[0];
|
||||||
|
// oAdd.addEventListener('mouseenter', mouseenter);
|
||||||
|
// oAdd.addEventListener('mouseleave', mouseleave);
|
||||||
|
// let oDel = oDiv2.getElementsByClassName('del')[0];
|
||||||
|
// oDel.addEventListener('click', clickdel);
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
const clickdel = (event: Event) => {
|
||||||
|
let oBtn = event.target as Element;
|
||||||
|
oBtn.parentElement?.parentElement?.parentElement?.parentElement?.remove();
|
||||||
|
};
|
||||||
|
// window.clickadd = clickadd;
|
||||||
|
// window.clickdel = clickdel;
|
||||||
|
// window.mouseenter = mouseenter;
|
||||||
|
// window.mouseleave = mouseleave;
|
||||||
|
|
||||||
|
return {clickadd, clickdel, showAdd, showMenu }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
131
frontend/src/components/AppFieldSelectBox.vue
Normal file
131
frontend/src/components/AppFieldSelectBox.vue
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="q-mx-md q-mb-lg">
|
||||||
|
<div class="q-mb-xs q-ml-md text-primary">アプリ選択</div>
|
||||||
|
|
||||||
|
<div class="q-pa-md row" style="border: 1px solid rgba(0, 0, 0, 0.12); border-radius: 4px;">
|
||||||
|
<div v-if="selField?.app && !showSelectApp">{{ selField?.app?.name }}</div>
|
||||||
|
<q-space />
|
||||||
|
<div>
|
||||||
|
<q-btn outline dense label="選 択" padding="none sm" color="primary" @click="() => {
|
||||||
|
showSelectApp = true;
|
||||||
|
}"></q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!showSelectApp && selField?.app?.name">
|
||||||
|
<div>
|
||||||
|
<div class="row q-mb-md">
|
||||||
|
<!-- <div class="col"> -->
|
||||||
|
<div class="q-mb-xs q-ml-md text-primary">フィールド選択</div>
|
||||||
|
<!-- </div> -->
|
||||||
|
<q-space />
|
||||||
|
<!-- <div class="col"> -->
|
||||||
|
<div class="q-mr-md">
|
||||||
|
<q-input dense debounce="300" v-model="fieldFilter" placeholder="フィールド検索" clearable>
|
||||||
|
<template v-slot:before>
|
||||||
|
<q-icon name="search" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<field-select ref="fieldDlg" name="フィールド" :type="selectType" :updateSelectFields="updateSelectFields"
|
||||||
|
:appId="selField?.app?.id" not_page :filter="fieldFilter"
|
||||||
|
:selectedFields="selField.fields" :fieldTypes="fieldTypes"></field-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="min-width: 45vw;" v-else>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<show-dialog v-model:visible="showSelectApp" name="アプリ選択">
|
||||||
|
<template v-slot:toolbar>
|
||||||
|
<q-input dense debounce="300" v-model="filter" placeholder="検索" clearable>
|
||||||
|
<template v-slot:before>
|
||||||
|
<q-icon name="search" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<AppSelectBox ref="appDlg" name="アプリ" type="single" :filter="filter"
|
||||||
|
:updateSelectApp="updateSelectApp"></AppSelectBox>
|
||||||
|
</show-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref, watchEffect, computed, reactive } from 'vue';
|
||||||
|
import ShowDialog from './ShowDialog.vue';
|
||||||
|
import FieldSelect from './FieldSelect.vue';
|
||||||
|
import AppSelectBox from './AppSelectBox.vue';
|
||||||
|
interface IApp {
|
||||||
|
id: string,
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
interface IField {
|
||||||
|
name: string,
|
||||||
|
code: string,
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAppFields {
|
||||||
|
app?: IApp,
|
||||||
|
fields: IField[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
inheritAttrs: false,
|
||||||
|
name: 'AppFieldSelectBox',
|
||||||
|
components: {
|
||||||
|
ShowDialog,
|
||||||
|
FieldSelect,
|
||||||
|
AppSelectBox,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
selectedField: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
selectType: {
|
||||||
|
type: String,
|
||||||
|
default: 'single'
|
||||||
|
},
|
||||||
|
fieldTypes:{
|
||||||
|
type:Array,
|
||||||
|
default:()=>[]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const showSelectApp = ref(false);
|
||||||
|
const selField = reactive(props.selectedField);
|
||||||
|
|
||||||
|
const isSelected = computed(() => {
|
||||||
|
return selField !== null && typeof selField === 'object' && ('app' in selField)
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateSelectApp = (newAppinfo: IApp) => {
|
||||||
|
selField.app = newAppinfo
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSelectFields = (newFields: IField[]) => {
|
||||||
|
selField.fields = newFields
|
||||||
|
}
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
emit('update:modelValue', selField);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
showSelectApp,
|
||||||
|
isSelected,
|
||||||
|
updateSelectApp,
|
||||||
|
filter: ref(),
|
||||||
|
updateSelectFields,
|
||||||
|
fieldFilter: ref(),
|
||||||
|
selField
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</q-field>
|
</q-field>
|
||||||
<q-field stack-label full-width label="アプリ説明">
|
<q-field stack-label full-width label="アプリの説明">
|
||||||
<template v-slot:control>
|
<template v-slot:control>
|
||||||
<div class="self-center full-width no-outline" tabindex="0">
|
<div class="self-center full-width no-outline" tabindex="0">
|
||||||
{{ appinfo?.description }}
|
{{ appinfo?.description }}
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
import { AppInfo, AppSeed } from './models';
|
import { AppInfo, AppSeed } from './models';
|
||||||
import { ref, defineComponent, watch, onMounted , toRefs } from 'vue';
|
import { ref, defineComponent, watch, onMounted , toRefs } from 'vue';
|
||||||
import { api } from 'boot/axios';
|
import { api } from 'boot/axios';
|
||||||
import { promises } from 'dns';
|
import { useAuthStore } from 'src/stores/useAuthStore';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
@@ -44,22 +44,23 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const { app } = toRefs(props);
|
const { app } = toRefs(props);
|
||||||
|
const authStore = useAuthStore();
|
||||||
const appinfo = ref<AppInfo>({
|
const appinfo = ref<AppInfo>({
|
||||||
appId: "",
|
appId: '',
|
||||||
name: "",
|
name: '',
|
||||||
description: ""
|
description: ''
|
||||||
});
|
});
|
||||||
const link= ref('https://mfu07rkgnb7c.cybozu.com/k/' + app.value);
|
const link= ref(`${authStore.currentDomain.kintoneUrl}/k/${app.value}`);
|
||||||
const getAppInfo = async (appId:string|undefined) => {
|
const getAppInfo = async (appId:string|undefined) => {
|
||||||
if(!appId){
|
if(!appId){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let result : any ={appId:"",name:""};
|
let result : any ={appId:'',name:''};
|
||||||
let retry =0;
|
let retry =0;
|
||||||
while(retry<=3 && result && result.appId!==appId){
|
while(retry<=3 && result && result.appId!==appId){
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
const response = await api.get('app', {
|
const response = await api.get('api/v1/app', {
|
||||||
params:{
|
params:{
|
||||||
app: appId
|
app: appId
|
||||||
}
|
}
|
||||||
@@ -73,7 +74,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
watch(app, async (newApp) => {
|
watch(app, async (newApp) => {
|
||||||
appinfo.value = await getAppInfo(newApp);
|
appinfo.value = await getAppInfo(newApp);
|
||||||
link.value = 'https://mfu07rkgnb7c.cybozu.com/k/' + newApp;
|
link.value = `${authStore.currentDomain.kintoneUrl}/k/${newApp}`;
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
const linkClick=(ev : MouseEvent)=>{
|
const linkClick=(ev : MouseEvent)=>{
|
||||||
@@ -82,7 +83,7 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
onMounted(async ()=>{
|
onMounted(async ()=>{
|
||||||
appinfo.value = await getAppInfo(app.value);
|
appinfo.value = await getAppInfo(app.value);
|
||||||
link.value = 'https://mfu07rkgnb7c.cybozu.com/k/' + app.value;
|
link.value = `${authStore.currentDomain.kintoneUrl}/k/${app.value}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
76
frontend/src/components/AppSelectBox.vue
Normal file
76
frontend/src/components/AppSelectBox.vue
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<template>
|
||||||
|
<detail-field-table
|
||||||
|
detailField="description"
|
||||||
|
:name="name"
|
||||||
|
:type="type"
|
||||||
|
:filter="filter"
|
||||||
|
:columns="columns"
|
||||||
|
:fetchData="fetchApps"
|
||||||
|
@update:selected="(item) => { selected = item }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { ref, PropType } from 'vue';
|
||||||
|
import { api } from 'boot/axios';
|
||||||
|
import DetailFieldTable from './dialog/DetailFieldTable.vue';
|
||||||
|
|
||||||
|
interface IAppDisplay {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
createdate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AppSelectBox',
|
||||||
|
components: {
|
||||||
|
DetailFieldTable
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
name: String,
|
||||||
|
type: String,
|
||||||
|
filter: String,
|
||||||
|
filterInitRowsFunc: {
|
||||||
|
type: Function as PropType<(app: IAppDisplay) => boolean>,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const selected = ref<IAppDisplay[]>([]);
|
||||||
|
const columns = [
|
||||||
|
{ name: 'id', required: true, label: 'ID', align: 'left', field: 'id', sortable: true, sort: (a: string, b: string) => parseInt(a, 10) - parseInt(b, 10) },
|
||||||
|
{ name: 'name', label: 'アプリ名', field: 'name', sortable: true, align: 'left' },
|
||||||
|
{ name: 'description', label: '概要', field: 'description', align: 'left', sortable: false },
|
||||||
|
{ name: 'createdate', label: '作成日時', field: 'createdate', align: 'left' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const fetchApps = async () => {
|
||||||
|
const res = await api.get('api/v1/allapps');
|
||||||
|
return res.data.apps.map((item: any) => ({
|
||||||
|
id: item.appId,
|
||||||
|
name: item.name,
|
||||||
|
description: item.description,
|
||||||
|
createdate: dateFormat(item.createdAt)
|
||||||
|
})).filter(app => !props.filterInitRowsFunc || props.filterInitRowsFunc(app));
|
||||||
|
};
|
||||||
|
|
||||||
|
const dateFormat = (dateStr: string) => {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
const pad = (num: number) => num.toString().padStart(2, '0');
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = pad(date.getMonth() + 1);
|
||||||
|
const day = pad(date.getDate());
|
||||||
|
const hours = pad(date.getHours());
|
||||||
|
const minutes = pad(date.getMinutes());
|
||||||
|
const seconds = pad(date.getSeconds());
|
||||||
|
return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
columns,
|
||||||
|
fetchApps,
|
||||||
|
selected
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
271
frontend/src/components/CascadingDropDownBox.vue
Normal file
271
frontend/src/components/CascadingDropDownBox.vue
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<q-stepper v-model="step" ref="stepper" color="primary" animated flat>
|
||||||
|
<q-step :name="1" title="データソースの設定" icon="app_registration" :done="step > 1">
|
||||||
|
<div class="row justify-between items-center">
|
||||||
|
<div>アプリの選択 :</div>
|
||||||
|
<div>
|
||||||
|
<a v-if="data.sourceApp?.name" class="q-mr-xs"
|
||||||
|
:href="data.sourceApp ? `${authStore.currentDomain.kintoneUrl}/k/${data.sourceApp.id}` : ''"
|
||||||
|
target="_blank" title="Kiontoneへ">
|
||||||
|
{{ data.sourceApp?.name }}
|
||||||
|
</a>
|
||||||
|
<div v-else class="text-red">APPを選択してください</div>
|
||||||
|
<q-btn v-if="data.sourceApp?.name" flat color="grey" icon="clear" size="sm" padding="none"
|
||||||
|
@click="clearSelectedApp" />
|
||||||
|
</div>
|
||||||
|
<q-btn outline dense label="変更" padding="xs sm" color="primary" @click="showAppDialog" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- フィールド設定部分 -->
|
||||||
|
<template v-if="data.sourceApp?.name">
|
||||||
|
<q-separator class="q-mt-md" />
|
||||||
|
<div class="q-my-md row justify-between items-center">
|
||||||
|
データ階層を設定する :
|
||||||
|
<q-btn icon="add" size="sm" padding="xs" outline color="primary" @click="addRow" />
|
||||||
|
</div>
|
||||||
|
<q-virtual-scroll style="max-height: 13.5rem;" :items="data.fieldList" separator v-slot="{ item, index }">
|
||||||
|
<div class="row justify-between items-center q-my-md">
|
||||||
|
<div>{{ index + 1 }}階層 :</div>
|
||||||
|
<div>{{ item.source?.name }}</div>
|
||||||
|
<q-btn-group outline>
|
||||||
|
<q-btn outline dense label="変更" padding="xs sm" color="primary"
|
||||||
|
@click="() => showFieldDialog(item, 'source')" />
|
||||||
|
<q-btn outline dense label="削除" padding="xs sm" color="primary" @click="() => delRow(index)" />
|
||||||
|
</q-btn-group>
|
||||||
|
</div>
|
||||||
|
</q-virtual-scroll>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- アプリ選択ダイアログ -->
|
||||||
|
<ShowDialog v-model:visible="data.sourceApp.showSelectApp" name="アプリ選択" @close="closeAppDialog" min-width="50vw"
|
||||||
|
min-height="50vh">
|
||||||
|
<template v-slot:toolbar>
|
||||||
|
<q-input dense debounce="300" v-model="data.sourceApp.appFilter" placeholder="検索" clearable>
|
||||||
|
<template v-slot:before>
|
||||||
|
<q-icon name="search" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</template>
|
||||||
|
<AppSelectBox ref="appDg" name="アプリ" type="single" :filter="data.sourceApp.appFilter" />
|
||||||
|
</ShowDialog>
|
||||||
|
</q-step>
|
||||||
|
|
||||||
|
<q-step :name="2" title="ドロップダウンフィールドの設定" icon="multiple_stop" :done="step > 2">
|
||||||
|
<div class="row q-pa-sm q-col-gutter-x-sm flex-center">
|
||||||
|
<div class="col-grow row q-col-gutter-x-sm">
|
||||||
|
<div class="col-6">データソース</div>
|
||||||
|
<div class="col-6">ドロップダウンフィールド</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<div style="width: 88px; height: 1px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-for="(item) in data.fieldList" :key="item.id" class="row q-pa-sm q-col-gutter-x-sm flex-center">
|
||||||
|
<div class="col-grow row q-col-gutter-x-sm">
|
||||||
|
|
||||||
|
<div class="col-6">{{ item.source.name }}</div>
|
||||||
|
<div class="col-6">{{ item.dropDown?.name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="row justify-end">
|
||||||
|
<q-btn-group outline>
|
||||||
|
<q-btn outline dense label="設定" padding="xs sm" color="primary"
|
||||||
|
@click="() => showFieldDialog(item, 'dropDown')" />
|
||||||
|
<q-btn outline dense label="クリア" padding="xs sm" color="primary"
|
||||||
|
@click="() => item.dropDown = undefined" />
|
||||||
|
</q-btn-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-step>
|
||||||
|
|
||||||
|
<!-- ステップナビゲーション -->
|
||||||
|
<template v-slot:navigation>
|
||||||
|
<q-stepper-navigation>
|
||||||
|
<div class="row justify-end q-mt-md">
|
||||||
|
<q-btn v-if="step > 1" flat color="primary" @click="$refs.stepper.previous()" label="戻る" class="q-ml-sm" />
|
||||||
|
<q-btn @click="stepperNext" color="primary" :label="step === 2 ? '確定' : '次へ'"
|
||||||
|
:disable="nextBtnCheck()" />
|
||||||
|
</div>
|
||||||
|
</q-stepper-navigation>
|
||||||
|
</template>
|
||||||
|
</q-stepper>
|
||||||
|
|
||||||
|
<!-- フィールド選択ダイアログ -->
|
||||||
|
<template v-for="(item, index) in data.fieldList" :key="`dg${item.id}`">
|
||||||
|
<show-dialog v-model:visible="item.sourceDg.show" name="フィールド一覧" min-width="400px">
|
||||||
|
<template v-slot:toolbar>
|
||||||
|
<q-input dense debounce="300" v-model="item.sourceDg.filter" placeholder="検索" clearable>
|
||||||
|
<template v-slot:before>
|
||||||
|
<q-icon name="search" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</template>
|
||||||
|
<FieldSelect name="フィールド" :appId="data.sourceApp.id" :selectedFields="item.source"
|
||||||
|
:filter="item.sourceDg.filter" :updateSelectFields="(f) => updateSelectField(f, item, index, 'source')"
|
||||||
|
:blackListLabel="blackListLabel" />
|
||||||
|
</show-dialog>
|
||||||
|
|
||||||
|
<show-dialog v-model:visible="item.dropDownDg.show" name="フィールド一覧" min-width="400px">
|
||||||
|
<template v-slot:toolbar>
|
||||||
|
<q-input dense debounce="300" v-model="item.dropDownDg.filter" placeholder="検索" clearable>
|
||||||
|
<template v-slot:before>
|
||||||
|
<q-icon name="search" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</template>
|
||||||
|
<FieldSelect name="フィールド" :appId="data.dropDownApp.id" :selectedFields="item.source"
|
||||||
|
:filter="item.dropDownDg.filter" :updateSelectFields="(f) => updateSelectField(f, item, index, 'dropDown')"
|
||||||
|
:blackListLabel="blackListLabel" />
|
||||||
|
</show-dialog>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, reactive, ref, watchEffect,watch } from 'vue';
|
||||||
|
import ShowDialog from './ShowDialog.vue';
|
||||||
|
import AppSelectBox from './AppSelectBox.vue';
|
||||||
|
import FieldSelect from './FieldSelect.vue';
|
||||||
|
import { useAuthStore } from 'src/stores/useAuthStore';
|
||||||
|
import { useFlowEditorStore } from 'src/stores/flowEditor';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'CascadingDropDownBox',
|
||||||
|
inheritAttrs: false,
|
||||||
|
components: { ShowDialog, AppSelectBox, FieldSelect },
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
finishDialogHandler: Function,
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const flowStore = useFlowEditorStore();
|
||||||
|
const $q = useQuasar();
|
||||||
|
const appDg = ref();
|
||||||
|
const stepper = ref();
|
||||||
|
const step = ref(1);
|
||||||
|
|
||||||
|
const data =ref(props.modelValue);
|
||||||
|
// const data = ref({
|
||||||
|
// sourceApp: props.modelValue.sourceApp ?? { appFilter: '', showSelectApp: false },
|
||||||
|
// dropDownApp: props.modelValue.dropDownApp,
|
||||||
|
// fieldList: props.modelValue.fieldList ?? [],
|
||||||
|
// });
|
||||||
|
|
||||||
|
// アプリ関連の関数
|
||||||
|
const showAppDialog = () => data.value.sourceApp.showSelectApp = true;
|
||||||
|
|
||||||
|
const clearSelectedApp = () => {
|
||||||
|
data.value.sourceApp = { appFilter: '', showSelectApp: false };
|
||||||
|
data.value.fieldList = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeAppDialog = (val: 'OK' | 'Cancel') => {
|
||||||
|
data.value.sourceApp.showSelectApp = false;
|
||||||
|
const selected = appDg.value?.selected[0];
|
||||||
|
if (val === 'OK' && selected) {
|
||||||
|
if (flowStore.appInfo?.appId === selected.id) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
caption: 'エラー',
|
||||||
|
message: 'データソースを現在のアプリにすることはできません。'
|
||||||
|
});
|
||||||
|
} else if (selected.id !== data.value.sourceApp.id) {
|
||||||
|
clearSelectedApp();
|
||||||
|
Object.assign(data.value.sourceApp, { id: selected.id, name: selected.name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// フィールド関連の関数
|
||||||
|
const defaultRow = () => ({
|
||||||
|
id: uuidv4(),
|
||||||
|
source: undefined,
|
||||||
|
dropDown: undefined,
|
||||||
|
sourceDg: { show: false, filter: '' },
|
||||||
|
dropDownDg: { show: false, filter: '' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const addRow = () => data.value.fieldList.push(defaultRow());
|
||||||
|
const delRow = (index: number) => data.value.fieldList.splice(index, 1);
|
||||||
|
|
||||||
|
const showFieldDialog = (item: any, keyName: string) => item[`${keyName}Dg`].show = true;
|
||||||
|
|
||||||
|
const updateSelectField = (f: any, item: any, index: number, keyName: 'source' | 'dropDown') => {
|
||||||
|
const [selected] = f.value;
|
||||||
|
const isDuplicate = data.value.fieldList.some((field, idx) =>
|
||||||
|
idx !== index && (field[keyName]?.code === selected.code || field[keyName]?.label === selected.label)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDuplicate) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
caption: 'エラー',
|
||||||
|
message: '重複したフィールドは選択できません'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
item[keyName] = selected;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ステッパー関連の関数
|
||||||
|
const nextBtnCheck = () => {
|
||||||
|
const stepNo = step.value
|
||||||
|
if (stepNo === 1) {
|
||||||
|
return !(data.value.sourceApp?.id && data.value.fieldList?.length > 0 && data.value.fieldList?.every(f => f.source?.name));
|
||||||
|
} else if (stepNo === 2) {
|
||||||
|
return !data.value.fieldList?.every(f => f.dropDown?.name);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stepperNext = () => {
|
||||||
|
if (step.value === 2) {
|
||||||
|
props.finishDialogHandler?.(data.value);
|
||||||
|
} else {
|
||||||
|
data.value.dropDownApp = { name: flowStore.appInfo?.name, id: flowStore.appInfo?.appId };
|
||||||
|
stepper.value?.next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// // データ変更の監視
|
||||||
|
// watchEffect(() =>{
|
||||||
|
// emit('update:modelValue', data.value);
|
||||||
|
// });
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状態と参照
|
||||||
|
authStore,
|
||||||
|
step,
|
||||||
|
stepper,
|
||||||
|
appDg,
|
||||||
|
data,
|
||||||
|
// アプリ関連の関数
|
||||||
|
showAppDialog,
|
||||||
|
closeAppDialog,
|
||||||
|
clearSelectedApp,
|
||||||
|
|
||||||
|
// フィールド関連の関数
|
||||||
|
addRow,
|
||||||
|
delRow,
|
||||||
|
showFieldDialog,
|
||||||
|
updateSelectField,
|
||||||
|
|
||||||
|
// ステッパー関連の関数
|
||||||
|
nextBtnCheck,
|
||||||
|
stepperNext,
|
||||||
|
|
||||||
|
// 定数
|
||||||
|
blackListLabel: ['レコード番号', '作業者', '更新者', '更新日時', '作成日時', '作成者', 'カテゴリー', 'ステータス'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
107
frontend/src/components/ConditionEditor/ConditionEditor.vue
Normal file
107
frontend/src/components/ConditionEditor/ConditionEditor.vue
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<show-dialog v-model:visible="showflg" name="条件エディタ" @close="closeDg" min-width="50vw" min-height="60vh">
|
||||||
|
<template v-slot:toolbar>
|
||||||
|
<q-btn flat round dense icon="more_vert" >
|
||||||
|
<q-menu auto-close anchor="bottom start">
|
||||||
|
<q-list>
|
||||||
|
<q-item clickable @click="copyCondition()">
|
||||||
|
<q-item-section avatar><q-icon name="content_copy" ></q-icon></q-item-section>
|
||||||
|
<q-item-section >コピー</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable @click="pasteCondition()">
|
||||||
|
<q-item-section avatar><q-icon name="content_paste" ></q-icon></q-item-section>
|
||||||
|
<q-item-section >貼り付け</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
|
<NodeCondition v-model:conditionTree="tree"></NodeCondition>
|
||||||
|
</show-dialog>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref ,watchEffect} from 'vue';
|
||||||
|
import ShowDialog from '../../components/ShowDialog.vue';
|
||||||
|
import NodeCondition from './NodeCondition.vue';
|
||||||
|
import { ConditionTree } from '../../types/Conditions';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ConditionObject',
|
||||||
|
components: {
|
||||||
|
ShowDialog,
|
||||||
|
NodeCondition,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
conditionTree: {
|
||||||
|
type: ConditionTree,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
show:{
|
||||||
|
type:Boolean,
|
||||||
|
default:false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits:[
|
||||||
|
'closed',
|
||||||
|
'update:conditionTree',
|
||||||
|
'update:show'
|
||||||
|
],
|
||||||
|
setup(props,context) {
|
||||||
|
const appDg = ref();
|
||||||
|
const $q=useQuasar();
|
||||||
|
const tree = ref(props.conditionTree);
|
||||||
|
const closeDg = (val:string) => {
|
||||||
|
if (val == 'OK') {
|
||||||
|
// if(tree.value.root.children.length===0){
|
||||||
|
// $q.notify({
|
||||||
|
// type: 'negative',
|
||||||
|
// message: `条件式を設定してください。`
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
context.emit('update:conditionTree',tree.value);
|
||||||
|
}
|
||||||
|
showflg.value=false;
|
||||||
|
context.emit('update:show',false);
|
||||||
|
context.emit('closed',val);
|
||||||
|
};
|
||||||
|
const showflg =ref(props.show);
|
||||||
|
//条件式をコピーする
|
||||||
|
const copyCondition=()=>{
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
const jsonData=tree.value.toJson();
|
||||||
|
navigator.clipboard.writeText(jsonData).then(() => {
|
||||||
|
console.log('Text successfully copied to clipboard');
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
console.error('Error in copying text: ', err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('Clipboard API not available');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
//条件式を貼り付ける
|
||||||
|
const pasteCondition=async ()=>{
|
||||||
|
try {
|
||||||
|
const text = await navigator.clipboard.readText();
|
||||||
|
console.log('Text from clipboard:', text);
|
||||||
|
tree.value.fromJson(text);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to read text from clipboard: ', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
watchEffect(() => {
|
||||||
|
showflg.value=props.show;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
tree,
|
||||||
|
appDg,
|
||||||
|
closeDg,
|
||||||
|
showflg,
|
||||||
|
copyCondition,
|
||||||
|
pasteCondition
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
144
frontend/src/components/ConditionEditor/ConditionObject.vue
Normal file
144
frontend/src/components/ConditionEditor/ConditionObject.vue
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
<template>
|
||||||
|
<q-field labelColor="primary" class="condition-object" dense outlined :label="label" :disable="disabled"
|
||||||
|
:clearable="isSelected">
|
||||||
|
<template v-slot:control>
|
||||||
|
<q-chip color="primary" text-color="white" v-if="isSelected && selectedObject.objectType==='field'" :dense="true" class="selected-obj">
|
||||||
|
{{ selectedObject.name }}
|
||||||
|
</q-chip>
|
||||||
|
<q-chip color="info" text-color="white" v-if="isSelected && selectedObject.objectType==='variable'" :dense="true" class="selected-obj">
|
||||||
|
{{ selectedObject.name.name }}
|
||||||
|
</q-chip>
|
||||||
|
<div v-if="isSelected && selectedObject.objectType==='text'">{{ selectedObject?.sharedText }}</div>
|
||||||
|
</template>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon name="search" class="cursor-pointer" @click="showDg" />
|
||||||
|
</template>
|
||||||
|
</q-field>
|
||||||
|
<show-dialog v-model:visible="show" name="設定項目" @close="closeDg" min-width="400px">
|
||||||
|
<!-- <template v-slot:toolbar>
|
||||||
|
<q-input dense debounce="200" v-model="filter" placeholder="検索" clearable>
|
||||||
|
<template v-slot:before>
|
||||||
|
<q-icon name="search" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</template>
|
||||||
|
<condition-objects ref="appDg" name="フィールド" type="single" :filter="filter" :appId="store.appInfo?.appId" :vars="vars"></condition-objects>
|
||||||
|
-->
|
||||||
|
<DynamicItemInput v-model:selectedObject="selectedObject" :canInput="config.canInput"
|
||||||
|
:buttonsConfig="config.buttonsConfig" :appId="store.appInfo?.appId" :options="options" ref="inputRef" />
|
||||||
|
|
||||||
|
</show-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, reactive, ref, watchEffect, computed ,PropType} from 'vue';
|
||||||
|
import ShowDialog from '../ShowDialog.vue';
|
||||||
|
// import ConditionObjects from '../ConditionObjects.vue';
|
||||||
|
import DynamicItemInput from '../DynamicItemInput/DynamicItemInput.vue';
|
||||||
|
import { useFlowEditorStore } from '../../stores/flowEditor';
|
||||||
|
import { IActionFlow, IActionNode, IActionVariable } from '../../types/ActionTypes';
|
||||||
|
import { IDynamicInputConfig } from 'src/types/ComponentTypes';
|
||||||
|
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ConditionObject',
|
||||||
|
components: {
|
||||||
|
ShowDialog,
|
||||||
|
DynamicItemInput,
|
||||||
|
// ConditionObjects
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
type: Object as PropType<IDynamicInputConfig>,
|
||||||
|
default: () => {
|
||||||
|
return {
|
||||||
|
canInput: false,
|
||||||
|
buttonsConfig: [
|
||||||
|
{ label: 'フィールド', color: 'primary', type: 'FieldAdd' },
|
||||||
|
{ label: '変数', color: 'green', type: 'VariableAdd' },
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
options:
|
||||||
|
{
|
||||||
|
type:Array as PropType< string[]>,
|
||||||
|
default:()=>[]
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, { emit }) {
|
||||||
|
// const appDg = ref();
|
||||||
|
const inputRef=ref();
|
||||||
|
const show = ref(false);
|
||||||
|
const selectedObject = ref(props.modelValue);
|
||||||
|
const store = useFlowEditorStore();
|
||||||
|
// const sharedText = ref(''); // 共享的文本状态
|
||||||
|
const isSelected = computed(() => {
|
||||||
|
return selectedObject.value?.sharedText !== '';
|
||||||
|
});
|
||||||
|
// const isSelected = computed(()=>{
|
||||||
|
// return selectedObject.value!==null && typeof selectedObject.value === 'object' && ('name' in selectedObject.value)
|
||||||
|
// });
|
||||||
|
let vars: IActionVariable[] = [];
|
||||||
|
if (store.currentFlow !== undefined && store.activeNode !== undefined) {
|
||||||
|
vars = store.currentFlow.getVarNames(store.activeNode);
|
||||||
|
}
|
||||||
|
// const filter=ref('');
|
||||||
|
const showDg = () => {
|
||||||
|
show.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDg = (val: string) => {
|
||||||
|
if (val == 'OK') {
|
||||||
|
// selectedObject.value = appDg.value.selected[0];
|
||||||
|
selectedObject.value = inputRef.value.selectedObjectRef
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
emit('update:modelValue', selectedObject.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
inputRef,
|
||||||
|
store,
|
||||||
|
// appDg,
|
||||||
|
show,
|
||||||
|
showDg,
|
||||||
|
closeDg,
|
||||||
|
selectedObject,
|
||||||
|
vars: reactive(vars),
|
||||||
|
isSelected,
|
||||||
|
buttonsConfig: [
|
||||||
|
{ label: 'フィールド', color: 'primary', type: 'FieldAdd' },
|
||||||
|
{ label: '変数', color: 'green', type: 'VariableAdd' },
|
||||||
|
]
|
||||||
|
// filter
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
.condition-object {
|
||||||
|
min-width: 200px;
|
||||||
|
max-height: 40px;
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-obj {
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
277
frontend/src/components/ConditionEditor/NodeCondition.vue
Normal file
277
frontend/src/components/ConditionEditor/NodeCondition.vue
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
<template>
|
||||||
|
<!-- <q-toolbar class="bg-grey-3" flat dense round icon="menu" aria-label="Menu" @click.stop>
|
||||||
|
<q-toolbar-title>条件エディタ</q-toolbar-title>
|
||||||
|
<q-space></q-space>
|
||||||
|
<q-btn flat round dense icon="info" color="blue" @click="showingCondition=!showingCondition"></q-btn>
|
||||||
|
</q-toolbar> -->
|
||||||
|
<div class="q-pa-md">
|
||||||
|
<q-tree :nodes="[tree.root]" node-key="index" children-key="children"
|
||||||
|
tick-strategy="strict" v-model:ticked="ticked" :expanded="expanded" default-expand-all dense color="primary" >
|
||||||
|
<template v-slot:header-root="prop">
|
||||||
|
<!-- root -->
|
||||||
|
<div class="row items-center" @click.stop>
|
||||||
|
<q-select v-model="prop.node.logicalOperator" :options="logicalOperators" filled outlined dense></q-select>
|
||||||
|
<q-btn flat round dense icon="more_horiz" size="sm" >
|
||||||
|
<q-menu auto-close anchor="top right">
|
||||||
|
<q-list>
|
||||||
|
<q-item clickable @click="addGroup(prop.node, LogicalOperator.AND)">
|
||||||
|
<q-item-section avatar><q-icon name="playlist_add" ></q-icon></q-item-section>
|
||||||
|
<q-item-section>グループの追加</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable @click="addCondition(prop.node)">
|
||||||
|
<q-item-section avatar><q-icon name="add_circle_outline" ></q-icon></q-item-section>
|
||||||
|
<q-item-section >条件式の追加</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-slot:header-generic="prop">
|
||||||
|
<!-- logic group -->
|
||||||
|
<div v-if="prop.node.type !== NodeType.Condition" class="row items-center" @click.stop>
|
||||||
|
<q-select v-model="prop.node.logicalOperator" :options="logicalOperators" :outlined="true" :filled="true" :dense="true"></q-select>
|
||||||
|
<q-btn flat round dense icon="more_horiz" size="sm" >
|
||||||
|
<q-menu auto-close anchor="top right">
|
||||||
|
<q-list>
|
||||||
|
<q-item clickable @click="moveUp(prop.node)">
|
||||||
|
<q-item-section avatar><q-icon name="arrow_upward" ></q-icon></q-item-section>
|
||||||
|
<q-item-section >一つ上に移動</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable @click="moveDown(prop.node)">
|
||||||
|
<q-item-section avatar><q-icon name="arrow_downward" ></q-icon></q-item-section>
|
||||||
|
<q-item-section >一つ下に移動</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-separator inset/>
|
||||||
|
<q-item clickable @click="addGroup(prop.node, LogicalOperator.AND)">
|
||||||
|
<q-item-section avatar><q-icon name="playlist_add" ></q-icon></q-item-section>
|
||||||
|
<q-item-section >グループ追加</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable @click="addCondition(prop.node)">
|
||||||
|
<q-item-section avatar><q-icon name="add_circle_outline" ></q-icon></q-item-section>
|
||||||
|
<q-item-section >条件式追加</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-separator inset/>
|
||||||
|
<q-item clickable @click="splitGroup(prop.node)">
|
||||||
|
<q-item-section avatar><q-icon name="playlist_remove" color="negative"></q-icon></q-item-section>
|
||||||
|
<q-item-section >グループ化解除</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable @click="removeNode(prop.node)">
|
||||||
|
<q-item-section avatar><q-icon name="delete" color="negative"></q-icon></q-item-section>
|
||||||
|
<q-item-section >削除</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
<!-- condition -->
|
||||||
|
<div @click.stop @keypress.stop v-else >
|
||||||
|
<div class="row no-wrap items-center q-my-xs">
|
||||||
|
<ConditionObject v-bind="prop.node" v-model="prop.node.object" :config="leftDynamicItemConfig" class="col-4"/>
|
||||||
|
<q-select v-model="prop.node.operator" :options="operators" class="operator" :outlined="true" :dense="true"></q-select>
|
||||||
|
<ConditionObject v-bind="prop.node" v-model="prop.node.value" :config="rightDynamicItemConfig" class="col-4"
|
||||||
|
:options="objectValueOptions(prop.node?.object?.options)"
|
||||||
|
/>
|
||||||
|
<!-- <ConditionObject v-bind="prop.node" v-model="prop.node.object" class="col-4"/> -->
|
||||||
|
<!-- <q-input v-if="!prop.node.object || !('options' in prop.node.object)"
|
||||||
|
v-model="prop.node.value"
|
||||||
|
class="condition-value" :outlined="true" :dense="true" ></q-input> -->
|
||||||
|
<!-- <q-select v-if="prop.node.object && ('options' in prop.node.object)"
|
||||||
|
v-model="prop.node.value"
|
||||||
|
:options="objectValueOptions(prop.node.object.options)"
|
||||||
|
clearable
|
||||||
|
value-key="index"
|
||||||
|
class="condition-value" :outlined="true" :dense="true" ></q-select> -->
|
||||||
|
<q-btn flat round dense icon="more_horiz" size="sm" >
|
||||||
|
<q-menu auto-close anchor="top right">
|
||||||
|
<q-list>
|
||||||
|
<q-item clickable @click="moveUp(prop.node)">
|
||||||
|
<q-item-section avatar><q-icon name="arrow_upward" ></q-icon></q-item-section>
|
||||||
|
<q-item-section >一つ上に移動</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable @click="moveDown(prop.node)">
|
||||||
|
<q-item-section avatar><q-icon name="arrow_downward" ></q-icon></q-item-section>
|
||||||
|
<q-item-section >一つ下に移動</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-separator inset/>
|
||||||
|
<q-item clickable @click="groupMerge(prop.node)" v-if="canMerge(prop.node)">
|
||||||
|
<q-item-section avatar><q-icon name="playlist_add"></q-icon></q-item-section>
|
||||||
|
<q-item-section >グループ化</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-separator inset/>
|
||||||
|
<q-item clickable @click="removeNode(prop.node)">
|
||||||
|
<q-item-section avatar><q-icon name="delete" color="negative"></q-icon></q-item-section>
|
||||||
|
<q-item-section>削除</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</q-tree>
|
||||||
|
<q-tooltip anchor="center middle" v-model="showingCondition" no-parent-event>
|
||||||
|
import { finished } from 'stream';
|
||||||
|
{{ conditionString }}
|
||||||
|
</q-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent,ref,reactive, computed, inject } from 'vue';
|
||||||
|
import { INode,ConditionTree,GroupNode,ConditionNode, LogicalOperator,Operator,NodeType } from '../../types/Conditions';
|
||||||
|
import ConditionObject from './ConditionObject.vue';
|
||||||
|
import { IDynamicInputConfig } from 'src/types/ComponentTypes';
|
||||||
|
export default defineComponent( {
|
||||||
|
name: 'NodeCondition',
|
||||||
|
components: {
|
||||||
|
ConditionObject
|
||||||
|
},
|
||||||
|
props:{
|
||||||
|
conditionTree: {
|
||||||
|
type: ConditionTree,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
show:{
|
||||||
|
type:Boolean,
|
||||||
|
default:false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const ticked= ref([]);
|
||||||
|
const showingCondition=ref(false);
|
||||||
|
|
||||||
|
const logicalOperators = computed(()=>{
|
||||||
|
const opts=[];
|
||||||
|
for(const op in LogicalOperator){
|
||||||
|
opts.push(LogicalOperator[op as keyof typeof LogicalOperator]);
|
||||||
|
}
|
||||||
|
return opts;
|
||||||
|
});
|
||||||
|
|
||||||
|
const operatorSet = inject<Array<any>>('Operator')
|
||||||
|
const operators = ref(operatorSet ? operatorSet : Object.values(Operator));
|
||||||
|
const tree = reactive(props.conditionTree);
|
||||||
|
|
||||||
|
const conditionString = computed(()=>{
|
||||||
|
return tree.buildConditionString(tree.root);
|
||||||
|
});
|
||||||
|
|
||||||
|
const objectValueOptions=(options:any):any[]|null=>{
|
||||||
|
if(!options){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const opts:any[] =[];
|
||||||
|
Object.keys(options).forEach((key) =>
|
||||||
|
{
|
||||||
|
const opt=options[key];
|
||||||
|
opts.push(opt);
|
||||||
|
});
|
||||||
|
return opts;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addGroup = (parent:GroupNode, logicOp:LogicalOperator) => {
|
||||||
|
if(!parent){
|
||||||
|
parent=tree.root;
|
||||||
|
}
|
||||||
|
tree.addNode(parent,new GroupNode(logicOp,parent));
|
||||||
|
};
|
||||||
|
|
||||||
|
const addCondition = (parent:GroupNode) => {
|
||||||
|
const newNode = new ConditionNode({},Operator.Equal,'',parent);
|
||||||
|
tree.addNode(parent,newNode);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeNode = (node:INode) => {
|
||||||
|
tree.removeNode(node);
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveUp =(node:INode)=>{
|
||||||
|
tree.moveNode(node,'up');
|
||||||
|
}
|
||||||
|
|
||||||
|
const moveDown =(node:INode)=>{
|
||||||
|
tree.moveNode(node,'down');
|
||||||
|
}
|
||||||
|
|
||||||
|
const getConditionJson=()=>{
|
||||||
|
return tree.toJson();
|
||||||
|
}
|
||||||
|
//JsonからConditionTreeのインスタンスを作成
|
||||||
|
const LoadCondition=()=>{
|
||||||
|
tree.fromJson(conditionString.value);
|
||||||
|
}
|
||||||
|
//グループ化
|
||||||
|
const groupMerge=(node:INode)=>{
|
||||||
|
const checkedNodes:INode[]=[];
|
||||||
|
const checkedIndexs:number[] = ticked.value;
|
||||||
|
checkedIndexs.forEach(index => {
|
||||||
|
const node = tree.findByIndex(index);
|
||||||
|
if(node){
|
||||||
|
checkedNodes.push(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tree.createGroupNode(node,checkedNodes,LogicalOperator.AND);
|
||||||
|
ticked.value=[];
|
||||||
|
}
|
||||||
|
//グループ化可能かをチェックする
|
||||||
|
const canMerge =(node:INode)=>{
|
||||||
|
const checkedIndexs:number[] = ticked.value;
|
||||||
|
const findNode = checkedIndexs.find(index=>node.index===index);
|
||||||
|
console.log('findNode=>',findNode!==undefined,findNode);
|
||||||
|
return findNode!==undefined;
|
||||||
|
}
|
||||||
|
//グループ化解散
|
||||||
|
const splitGroup=(node:INode)=>{
|
||||||
|
tree.dissolveGroupNode(node as GroupNode);
|
||||||
|
ticked.value=[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const expanded=computed(()=>tree.getGroups(tree.root));
|
||||||
|
// addCondition(tree.root);
|
||||||
|
const leftDynamicItemConfig = inject<IDynamicInputConfig>('leftDynamicItemConfig');
|
||||||
|
const rightDynamicItemConfig = inject<IDynamicInputConfig>('rightDynamicItemConfig');
|
||||||
|
|
||||||
|
return {
|
||||||
|
leftDynamicItemConfig,
|
||||||
|
rightDynamicItemConfig,
|
||||||
|
showingCondition,
|
||||||
|
conditionString,
|
||||||
|
tree,
|
||||||
|
ticked,
|
||||||
|
logicalOperators,
|
||||||
|
operators,
|
||||||
|
addGroup,
|
||||||
|
addCondition,
|
||||||
|
removeNode,
|
||||||
|
moveUp,
|
||||||
|
moveDown,
|
||||||
|
LogicalOperator,
|
||||||
|
Operator,
|
||||||
|
NodeType,
|
||||||
|
getConditionJson,
|
||||||
|
LoadCondition,
|
||||||
|
objectValueOptions,
|
||||||
|
expanded,
|
||||||
|
canMerge,
|
||||||
|
groupMerge,
|
||||||
|
splitGroup
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
.condition-value{
|
||||||
|
min-width: 200px;
|
||||||
|
max-height: 40px;
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operator{
|
||||||
|
min-width: 150px;
|
||||||
|
max-height: 40px;
|
||||||
|
margin: 0 2px;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12pt;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
63
frontend/src/components/ConditionObjects.vue
Normal file
63
frontend/src/components/ConditionObjects.vue
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<div class="q-gutter-y-md" style="max-width: 600px;">
|
||||||
|
<q-card >
|
||||||
|
<q-tabs
|
||||||
|
v-model="tab"
|
||||||
|
dense
|
||||||
|
class="text-grey"
|
||||||
|
active-color="white"
|
||||||
|
active-bg-color="primary"
|
||||||
|
indicator-color="primary"
|
||||||
|
align="justify"
|
||||||
|
narrow-indicator
|
||||||
|
>
|
||||||
|
<q-tab name="fields" label="フィールド"></q-tab>
|
||||||
|
<q-tab name="vars" label="変数"></q-tab>
|
||||||
|
</q-tabs>
|
||||||
|
|
||||||
|
<q-separator></q-separator>
|
||||||
|
|
||||||
|
<q-tab-panels v-model="tab" animated>
|
||||||
|
<q-tab-panel name="fields">
|
||||||
|
<field-list v-model="selected" type="single" :filter="filter" :appId="sourceApp ? sourceApp :appId " :fields="sourceFields"></field-list>
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<q-tab-panel name="vars" >
|
||||||
|
<variable-list v-model="selected" type="single" :vars="vars"></variable-list>
|
||||||
|
</q-tab-panel>
|
||||||
|
</q-tab-panels>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { ref, onMounted, reactive, inject } from 'vue'
|
||||||
|
import FieldList from './FieldList.vue';
|
||||||
|
import VariableList from './VariableList.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ConditionObjects',
|
||||||
|
components:{
|
||||||
|
FieldList,
|
||||||
|
VariableList
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
name: String,
|
||||||
|
type: String,
|
||||||
|
appId: Number,
|
||||||
|
vars: Array,
|
||||||
|
filter:String
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const selected = ref([]);
|
||||||
|
console.log(selected);
|
||||||
|
|
||||||
|
return {
|
||||||
|
sourceFields : inject('sourceFields'),
|
||||||
|
sourceApp : inject('sourceApp'),
|
||||||
|
tab: ref('fields'),
|
||||||
|
selected
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,77 +1,105 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="q-pa-md">
|
<div class="q-pa-md">
|
||||||
<q-uploader
|
<q-uploader
|
||||||
style="max-width: 400px"
|
style="max-width: 400px"
|
||||||
:url="uploadUrl"
|
:url="uploadUrl"
|
||||||
:label="title"
|
:label="title"
|
||||||
accept=".csv,.xlsx"
|
:headers="headers"
|
||||||
v-on:rejected="onRejected"
|
accept=".xlsx"
|
||||||
v-on:uploaded="onUploadFinished"
|
v-on:rejected="onRejected"
|
||||||
v-on:failed="onFailed"
|
v-on:uploaded="onUploadFinished"
|
||||||
field-name="files"
|
v-on:failed="onFailed"
|
||||||
|
field-name="files"
|
||||||
></q-uploader>
|
></q-uploader>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { createUploaderComponent, useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
const $q=useQuasar();
|
import { useAuthStore } from 'src/stores/useAuthStore';
|
||||||
const emit =defineEmits(['uploaded']);
|
import { ref } from 'vue';
|
||||||
/**
|
|
||||||
* ファイルアップロードを拒否する時の処理
|
|
||||||
* @param rejectedEntries
|
|
||||||
*/
|
|
||||||
function onRejected (rejectedEntries:any) {
|
|
||||||
// Notify plugin needs to be installed
|
|
||||||
// https://quasar.dev/quasar-plugins/notify#Installation
|
|
||||||
$q.notify({
|
|
||||||
type: 'negative',
|
|
||||||
message: `CSVおよびExcelファイルを選択してください。`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
const $q = useQuasar();
|
||||||
* ファイルアップロード成功時の処理
|
const authStore = useAuthStore();
|
||||||
*/
|
const emit = defineEmits(['uploaded']);
|
||||||
function onUploadFinished({xhr}:{xhr:XMLHttpRequest}){
|
|
||||||
let msg="ファイルのアップロードが完了しました。";
|
|
||||||
if(xhr && xhr.response){
|
|
||||||
msg=`${msg} (${xhr.responseText})`;
|
|
||||||
}
|
|
||||||
$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
caption:"通知",
|
|
||||||
message: msg
|
|
||||||
});
|
|
||||||
setTimeout(() => {
|
|
||||||
emit('uploaded',xhr.responseText);
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
interface Props {
|
||||||
*
|
title?: string;
|
||||||
* @param info ファイルアップロード失敗時の処理
|
uploadUrl?: string;
|
||||||
*/
|
}
|
||||||
function onFailed({files,xhr}:{files: readonly any[],xhr:any}){
|
|
||||||
let msg ="ファイルアップロードが失敗しました。";
|
|
||||||
if(xhr && xhr.status){
|
|
||||||
msg=`${msg} (${xhr.status }:${xhr.statusText})`
|
|
||||||
}
|
|
||||||
$q.notify({
|
|
||||||
type:"negative",
|
|
||||||
message:msg
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
const headers = ref([
|
||||||
title: string;
|
{ name: 'Authorization', value: 'Bearer ' + authStore.token },
|
||||||
uploadUrl:string;
|
]);
|
||||||
}
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
title:"設計書から導入する(csv or excel)",
|
title: '設計書から導入する(Excel)',
|
||||||
uploadUrl: `${process.env.KAB_BACKEND_URL}createappfromexcel`
|
uploadUrl: `${process.env.KAB_BACKEND_URL}api/v1/createappfromexcel?format=1`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ファイルアップロードを拒否する時の処理
|
||||||
|
* @param rejectedEntries
|
||||||
|
*/
|
||||||
|
function onRejected(rejectedEntries: any) {
|
||||||
|
// Notify plugin needs to be installed
|
||||||
|
// https://quasar.dev/quasar-plugins/notify#Installation
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: 'Excelファイルを選択してください。',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ファイルアップロード成功時の処理
|
||||||
|
*/
|
||||||
|
function onUploadFinished({ xhr }: { xhr: XMLHttpRequest }) {
|
||||||
|
let msg = 'ファイルのアップロードが完了しました。';
|
||||||
|
if (xhr && xhr.response) {
|
||||||
|
msg = `${msg} (${xhr.responseText})`;
|
||||||
|
}
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
caption: '通知',
|
||||||
|
message: msg,
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
emit('uploaded', xhr.responseText);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 例外発生時、responseからエラー情報を取得する
|
||||||
|
* @param xhr
|
||||||
|
*/
|
||||||
|
function getResponseError(xhr: XMLHttpRequest) {
|
||||||
|
try {
|
||||||
|
const resp = JSON.parse(xhr.responseText);
|
||||||
|
return 'detail' in resp ? resp.detail : '';
|
||||||
|
} catch (err) {
|
||||||
|
return xhr.responseText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param info ファイルアップロード失敗時の処理
|
||||||
|
*/
|
||||||
|
function onFailed({
|
||||||
|
files,
|
||||||
|
xhr,
|
||||||
|
}: {
|
||||||
|
files: readonly any[];
|
||||||
|
xhr: XMLHttpRequest;
|
||||||
|
}) {
|
||||||
|
let msg = 'ファイルアップロードが失敗しました。';
|
||||||
|
if (xhr && xhr.status) {
|
||||||
|
const detail = getResponseError(xhr);
|
||||||
|
msg = `${msg} (${xhr.status}:${detail})`;
|
||||||
|
}
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: msg,
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss"></style>
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
91
frontend/src/components/DomainSelect.vue
Normal file
91
frontend/src/components/DomainSelect.vue
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<div class="q-pa-md">
|
||||||
|
<q-table :loading="loading" :title="name+'一覧'" :selection="type" v-model:selected="selected" :columns="columns" :rows="rows">
|
||||||
|
<template v-slot:body-cell-name="p">
|
||||||
|
<q-td class="content-box flex justify-between items-center" :props="p">
|
||||||
|
{{ p.row.name }}
|
||||||
|
<q-badge v-if="!p.row.domainActive" color="grey">未启用</q-badge>
|
||||||
|
<q-badge v-if="p.row.id == currentDomainId" color="primary">現在</q-badge>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { ref,onMounted,reactive, computed } from 'vue'
|
||||||
|
import { api } from 'boot/axios';
|
||||||
|
import { useAuthStore } from 'src/stores/useAuthStore';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DomainSelect',
|
||||||
|
props: {
|
||||||
|
name: String,
|
||||||
|
type: String,
|
||||||
|
filterInitRowsFunc: {
|
||||||
|
type: Function,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const currentDomainId = computed(() => authStore.currentDomain.id);
|
||||||
|
const loading = ref(true);
|
||||||
|
const inactiveRowClass = (row) => row.domainActive ? '' : 'inactive-row';
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true, classes: inactiveRowClass },
|
||||||
|
{ name: 'name', align: 'left', label: 'ドメイン', field: 'name', sortable: true, classes: inactiveRowClass },
|
||||||
|
{ name: 'url', label: 'URL', field: 'url',align: 'left', sortable: true, classes: inactiveRowClass },
|
||||||
|
{ name: 'user', label: 'アカウント', field: 'user',align: 'left', classes: inactiveRowClass },
|
||||||
|
{ name: 'owner', label: '所有者', field: row => row.owner.fullName, align: 'left', classes: inactiveRowClass },
|
||||||
|
]
|
||||||
|
const rows = reactive([]);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loading.value = true;
|
||||||
|
api.get('api/domains').then(res =>{
|
||||||
|
res.data.data.forEach((data) => {
|
||||||
|
const item = {
|
||||||
|
id: data.id,
|
||||||
|
tenantid: data.tenantid,
|
||||||
|
domainActive: data.is_active,
|
||||||
|
name: data.name,
|
||||||
|
url: data.url,
|
||||||
|
user: data.kintoneuser,
|
||||||
|
owner: {
|
||||||
|
id: data.owner.id,
|
||||||
|
firstName: data.owner.first_name,
|
||||||
|
lastName: data.owner.last_name,
|
||||||
|
fullNameSearch: (data.owner.last_name + data.owner.first_name).toLowerCase(),
|
||||||
|
fullName: data.owner.last_name + ' ' + data.owner.first_name,
|
||||||
|
email: data.owner.email,
|
||||||
|
isActive: data.owner.is_active,
|
||||||
|
isSuperuser: data.owner.is_superuser,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (props.filterInitRowsFunc && !props.filterInitRowsFunc(item)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rows.push(item);
|
||||||
|
})
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
currentDomainId,
|
||||||
|
columns,
|
||||||
|
rows,
|
||||||
|
selected: ref([]),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
.q-table td.inactive-row {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
.q-table .content-box {
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
32
frontend/src/components/DomainSelector.vue
Normal file
32
frontend/src/components/DomainSelector.vue
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<q-btn-dropdown
|
||||||
|
class="customized-disabled-btn"
|
||||||
|
push
|
||||||
|
flat
|
||||||
|
no-caps
|
||||||
|
icon="share"
|
||||||
|
size="md"
|
||||||
|
:label="userStore.currentDomain.domainName"
|
||||||
|
:disable-dropdown="true"
|
||||||
|
dropdown-icon="none"
|
||||||
|
:disable="true"
|
||||||
|
>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAuthStore } from 'stores/useAuthStore';
|
||||||
|
|
||||||
|
const userStore = useAuthStore();
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
.q-btn.disabled.customized-disabled-btn {
|
||||||
|
opacity: 1 !important;
|
||||||
|
cursor: default !important;
|
||||||
|
.q-icon.q-btn-dropdown__arrow {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
cursor: default !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
161
frontend/src/components/DynamicItemInput/DynamicItemInput.vue
Normal file
161
frontend/src/components/DynamicItemInput/DynamicItemInput.vue
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<template>
|
||||||
|
<div class="q-mx-md" style="max-width: 600px;">
|
||||||
|
<!-- <q-card> -->
|
||||||
|
<div class="q-mb-md">
|
||||||
|
<q-input ref="inputRef" v-if="!optionsRef|| optionsRef.length===0"
|
||||||
|
outlined dense debounce="200" @update:model-value="updateSharedText"
|
||||||
|
v-model="sharedText" :readonly="!canInputFlag" autogrow>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-btn flat round padding="none" icon="cancel" @click="clearSharedText" color="grey-6" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
<q-select v-if="optionsRef && optionsRef.length>0"
|
||||||
|
:model-value="sharedText"
|
||||||
|
:options="optionsRef"
|
||||||
|
clearable
|
||||||
|
value-key="index"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
use-input
|
||||||
|
hide-selected
|
||||||
|
input-debounce="10"
|
||||||
|
fill-input
|
||||||
|
@input-value="setValue"
|
||||||
|
@clear="sharedText=null"
|
||||||
|
hide-dropdown-icon
|
||||||
|
:readonly="!canInputFlag"
|
||||||
|
>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-gutter-sm">
|
||||||
|
<q-btn v-for="button in buttonsConfig" :key="button.type" :color="button.color" @mousedown.prevent
|
||||||
|
@click="openDialog(button)" size="sm">
|
||||||
|
{{ button.label }}
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<show-dialog v-model:visible="dialogVisible" :name="currentDialogName" @close="closeDialog" min-width="400px">
|
||||||
|
<template v-slot:toolbar>
|
||||||
|
<q-input dense debounce="200" v-model="filter" clearable>
|
||||||
|
<template v-slot:before>
|
||||||
|
<q-icon name="search" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</template>
|
||||||
|
<!-- asdf -->
|
||||||
|
<component :is="currentComponent" @select="handleSelect" :filter="filter" :appId="appId" />
|
||||||
|
</show-dialog>
|
||||||
|
<!-- </q-card> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { ref, inject, watchEffect, defineComponent,PropType } from 'vue';
|
||||||
|
import FieldAdd from './FieldAdd.vue';
|
||||||
|
import VariableAdd from './VariableAdd.vue';
|
||||||
|
// import FunctionAdd from './FunctionAdd.vue';
|
||||||
|
import ShowDialog from '../ShowDialog.vue';
|
||||||
|
import { IButtonConfig } from 'src/types/ComponentTypes';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'DynamicItemInput',
|
||||||
|
components: {
|
||||||
|
FieldAdd,
|
||||||
|
VariableAdd,
|
||||||
|
// FunctionAdd,
|
||||||
|
ShowDialog
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
canInput: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
appId: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
selectedObject: {
|
||||||
|
default: {}
|
||||||
|
},
|
||||||
|
options:{
|
||||||
|
type:Array as PropType< string[]>
|
||||||
|
},
|
||||||
|
buttonsConfig: {
|
||||||
|
type: Array as PropType<IButtonConfig[]>,
|
||||||
|
default: () => [
|
||||||
|
{ label: 'フィールド', color: 'primary', type: 'FieldAdd' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const filter = ref('');
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const currentDialogName = ref('');
|
||||||
|
const selectedObjectRef = ref(props.selectedObject);
|
||||||
|
const currentComponent = ref('FieldAdd');
|
||||||
|
const sharedText = ref(props.selectedObject?.sharedText ?? '');
|
||||||
|
const inputRef = ref();
|
||||||
|
const canInputFlag = ref(props.canInput);
|
||||||
|
const editable = ref(false);
|
||||||
|
|
||||||
|
const openDialog = (button: IButtonConfig) => {
|
||||||
|
currentDialogName.value = button.label;
|
||||||
|
currentComponent.value = button.type;
|
||||||
|
dialogVisible.value = true;
|
||||||
|
editable.value = canInputFlag.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
dialogVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = (value:any) => {
|
||||||
|
|
||||||
|
if (value && value._t && (value._t as string).length > 0) {
|
||||||
|
canInputFlag.value = editable.value;
|
||||||
|
}
|
||||||
|
selectedObjectRef.value={ sharedText: value._t, ...value };
|
||||||
|
sharedText.value = `${value._t}`;
|
||||||
|
// emit('update:selectedObject', { sharedText: sharedText.value, ...value });
|
||||||
|
dialogVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearSharedText = () => {
|
||||||
|
sharedText.value = '';
|
||||||
|
selectedObjectRef.value={};
|
||||||
|
canInputFlag.value = true;
|
||||||
|
// emit('update:selectedObject', {});
|
||||||
|
}
|
||||||
|
const updateSharedText = (value:string) => {
|
||||||
|
sharedText.value = value;
|
||||||
|
selectedObjectRef.value= { sharedText: value,objectType:'text' }
|
||||||
|
// emit('update:selectedObject', { ...props.selectedObject, sharedText: value,objectType:'text' });
|
||||||
|
}
|
||||||
|
const setValue=(value:string)=>{
|
||||||
|
sharedText.value = value;
|
||||||
|
if(selectedObjectRef.value.sharedText!==value){
|
||||||
|
selectedObjectRef.value= { sharedText: value,objectType:'text' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const optionsRef=ref(props.options);
|
||||||
|
|
||||||
|
return {
|
||||||
|
filter,
|
||||||
|
dialogVisible,
|
||||||
|
currentDialogName,
|
||||||
|
currentComponent,
|
||||||
|
canInputFlag,
|
||||||
|
openDialog,
|
||||||
|
closeDialog,
|
||||||
|
handleSelect,
|
||||||
|
clearSharedText,
|
||||||
|
updateSharedText,
|
||||||
|
setValue,
|
||||||
|
sharedText,
|
||||||
|
inputRef,
|
||||||
|
optionsRef,
|
||||||
|
selectedObjectRef
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
41
frontend/src/components/DynamicItemInput/FieldAdd.vue
Normal file
41
frontend/src/components/DynamicItemInput/FieldAdd.vue
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<template>
|
||||||
|
<field-list v-model="selected" type="single" :filter="filter" :appId="sourceApp ? sourceApp : appId"
|
||||||
|
:fields="sourceFields" @update:modelValue="handleSelect" />
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, inject, ref } from 'vue';
|
||||||
|
import FieldList from '../FieldList.vue';
|
||||||
|
export default {
|
||||||
|
name: 'FieldAdd',
|
||||||
|
components: {
|
||||||
|
FieldList,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
appId: Number,
|
||||||
|
filter: String
|
||||||
|
},
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const sourceFields = inject<Array<unknown>>('sourceFields')
|
||||||
|
const sourceApp = inject<number>('sourceApp')
|
||||||
|
const appId = computed(() => {
|
||||||
|
if (sourceFields || sourceApp) {
|
||||||
|
return sourceApp.value
|
||||||
|
} else {
|
||||||
|
return props.appId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
sourceFields,
|
||||||
|
sourceApp,
|
||||||
|
selected: ref([]),
|
||||||
|
handleSelect: (newSelection: any[]) => {
|
||||||
|
|
||||||
|
if (newSelection.length > 0) {
|
||||||
|
const v = newSelection[0]
|
||||||
|
emit('select', { _t: `field(${appId.value},${v.name})`, ...v }); // 假设您只需要选择的第一个字段的名称
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
42
frontend/src/components/DynamicItemInput/VariableAdd.vue
Normal file
42
frontend/src/components/DynamicItemInput/VariableAdd.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<variable-list v-model="selected" type="single" :vars="vars" :filter="filter" @update:modelValue="handleSelect" />
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import VariableList from '../VariableList.vue';
|
||||||
|
import { useFlowEditorStore } from 'src/stores/flowEditor';
|
||||||
|
import { IActionVariable } from 'src/types/ActionTypes';
|
||||||
|
export default {
|
||||||
|
name: 'VariableAdd',
|
||||||
|
components: {
|
||||||
|
VariableList,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
appId: Number,
|
||||||
|
filter: String
|
||||||
|
},
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const store = useFlowEditorStore();
|
||||||
|
let vars: IActionVariable[] = [];
|
||||||
|
console.log(store.currentFlow !== undefined && store.activeNode !== undefined);
|
||||||
|
|
||||||
|
if (store.currentFlow !== undefined && store.activeNode !== undefined) {
|
||||||
|
vars = store.currentFlow.getVarNames(store.activeNode);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
vars,
|
||||||
|
selected: ref([]),
|
||||||
|
handleSelect: (newSelection: any[]) => {
|
||||||
|
if (newSelection.length > 0) {
|
||||||
|
const v = newSelection[0];
|
||||||
|
let name = v.name
|
||||||
|
if (typeof name === 'object') {
|
||||||
|
name = name.name
|
||||||
|
}
|
||||||
|
emit('select', { _t: `var(${name})`, ...v }); // 假设您只需要选择的第一个字段的名称
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-item
|
<q-item
|
||||||
|
v-permissions="permission"
|
||||||
clickable
|
clickable
|
||||||
tag="a"
|
tag="a"
|
||||||
:target="target?target:'_blank'"
|
:target="target?target:'_blank'"
|
||||||
:href="link"
|
:href="link"
|
||||||
|
:disable="disable"
|
||||||
v-if="!isSeparator"
|
v-if="!isSeparator"
|
||||||
>
|
>
|
||||||
<q-item-section
|
<q-item-section
|
||||||
@@ -19,6 +21,7 @@
|
|||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-separator
|
<q-separator
|
||||||
|
class="q-my-sm"
|
||||||
v-if="isSeparator"
|
v-if="isSeparator"
|
||||||
inset
|
inset
|
||||||
/>
|
/>
|
||||||
@@ -32,6 +35,8 @@ export interface EssentialLinkProps {
|
|||||||
icon?: string;
|
icon?: string;
|
||||||
isSeparator?: boolean;
|
isSeparator?: boolean;
|
||||||
target?:string;
|
target?:string;
|
||||||
|
disable?:boolean;
|
||||||
|
permission?: string|null;
|
||||||
}
|
}
|
||||||
withDefaults(defineProps<EssentialLinkProps>(), {
|
withDefaults(defineProps<EssentialLinkProps>(), {
|
||||||
caption: '',
|
caption: '',
|
||||||
|
|||||||
69
frontend/src/components/FieldList.vue
Normal file
69
frontend/src/components/FieldList.vue
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<div class="q-pa-md">
|
||||||
|
<q-table flat bordered :loading="!isLoaded" row-key="name" :selection="type" :selected="modelValue"
|
||||||
|
@update:selected="$emit('update:modelValue', $event)"
|
||||||
|
:filter="filter"
|
||||||
|
:columns="columns"
|
||||||
|
:rows="rows"
|
||||||
|
:pagination="pagination"
|
||||||
|
style="max-height: 55vh;"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { useAsyncState } from '@vueuse/core';
|
||||||
|
import { api } from 'boot/axios';
|
||||||
|
import { computed ,Prop,PropType,ref} from 'vue';
|
||||||
|
import {IField} from 'src/types/ComponentTypes';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FieldList',
|
||||||
|
props: {
|
||||||
|
fields: Array as PropType<IField[]>,
|
||||||
|
name: String,
|
||||||
|
type: String,
|
||||||
|
appId: Number,
|
||||||
|
modelValue: Array,
|
||||||
|
filter: String
|
||||||
|
},
|
||||||
|
emits: [
|
||||||
|
'update:modelValue'
|
||||||
|
],
|
||||||
|
setup(props) {
|
||||||
|
// const rows = ref([]);
|
||||||
|
// const isLoaded = ref(false);
|
||||||
|
const columns = [
|
||||||
|
{ name: 'name', required: true, label: 'フィールド名', align: 'left', field: 'name', sortable: true },
|
||||||
|
{ name: 'code', label: 'フィールドコード', align: 'left', field: 'code', sortable: true },
|
||||||
|
{ name: 'type', label: 'フィールドタイプ', align: 'left', field: 'type', sortable: true }
|
||||||
|
]
|
||||||
|
|
||||||
|
const { state : rows, isReady: isLoaded, isLoading } = useAsyncState((args) => {
|
||||||
|
if (props.fields && Object.keys(props.fields).length > 0) {
|
||||||
|
return props.fields.map(f => ({ name: f.label, ...f ,objectType: 'field'}));
|
||||||
|
} else {
|
||||||
|
return api.get('api/v1/appfields', {
|
||||||
|
params: {
|
||||||
|
app: props.appId
|
||||||
|
}
|
||||||
|
}).then(res => {
|
||||||
|
const fields = res.data.properties;
|
||||||
|
return Object.values(fields).map((f:any) => ({ name: f.label, objectType: 'field', ...f }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [{ name: '', objectType: '', type: '', code: '', label: '' }])
|
||||||
|
return {
|
||||||
|
columns,
|
||||||
|
rows,
|
||||||
|
// selected: ref([]),
|
||||||
|
isLoaded,
|
||||||
|
pagination: ref({
|
||||||
|
rowsPerPage: 25,
|
||||||
|
sortBy: 'name',
|
||||||
|
descending: false,
|
||||||
|
page: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
107
frontend/src/components/FieldSelect.vue
Normal file
107
frontend/src/components/FieldSelect.vue
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<div class="q-px-md" style=" min-width: 50vw; max-width: 85vw;">
|
||||||
|
<div v-if="!isLoaded" class="spinner flex flex-center">
|
||||||
|
<q-spinner color="primary" size="3em" />
|
||||||
|
</div>
|
||||||
|
<q-table flat bordered v-else row-key="id" :selection="type" v-model:selected="selected" :columns="columns"
|
||||||
|
:rows="rows" :pagination="pageSetting" :filter="filter" style="max-height: 55vh;"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { ref, onMounted, reactive, watchEffect } from 'vue'
|
||||||
|
import { api } from 'boot/axios';
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'fieldSelect',
|
||||||
|
props: {
|
||||||
|
name: String,
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'single'
|
||||||
|
},
|
||||||
|
appId: Number,
|
||||||
|
not_page: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
selectedFields:{
|
||||||
|
type:Array ,
|
||||||
|
default:()=>[]
|
||||||
|
},
|
||||||
|
fieldTypes:{
|
||||||
|
type:Array,
|
||||||
|
default:()=>[]
|
||||||
|
},
|
||||||
|
filter: String,
|
||||||
|
updateSelectFields: {
|
||||||
|
type: Function
|
||||||
|
},
|
||||||
|
blackListLabel: {
|
||||||
|
type:Array,
|
||||||
|
default:()=>[]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const isLoaded = ref(false);
|
||||||
|
const columns = [
|
||||||
|
{ name: 'name', required: true, label: 'フィールド名', align: 'left', field: row => row.name, sortable: true },
|
||||||
|
{ name: 'code', label: 'フィールドコード', align: 'left', field: 'code', sortable: true },
|
||||||
|
{ name: 'type', label: 'フィールドタイプ', align: 'left', field: 'type', sortable: true }
|
||||||
|
];
|
||||||
|
const pageSetting = ref({
|
||||||
|
sortBy: 'name',
|
||||||
|
descending: false,
|
||||||
|
page: 1,
|
||||||
|
rowsPerPage: props.not_page ? 0 : 25
|
||||||
|
// rowsNumber: xx if getting data from a server
|
||||||
|
});
|
||||||
|
const rows = reactive([]);
|
||||||
|
const selected = ref((props.selectedFields && props.selectedFields.length>0)?props.selectedFields:[]);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const url = props.fieldTypes.includes('SPACER')?'api/v1/allfields':'api/v1/appfields';
|
||||||
|
const res = await api.get(url, {
|
||||||
|
params: {
|
||||||
|
app: props.appId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let fields = Object.values(res.data.properties);
|
||||||
|
for (const index in fields) {
|
||||||
|
const fld = fields[index]
|
||||||
|
if(props.blackListLabel.length > 0){
|
||||||
|
if(!props.blackListLabel.find(blackListItem => blackListItem === fld.label)){
|
||||||
|
if(props.fieldTypes.length===0 || props.fieldTypes.includes(fld.type)){
|
||||||
|
rows.push({id:index, name: fld.label || fld.code, ...fld });
|
||||||
|
}else if(props.fieldTypes.includes('lookup') && ('lookup' in fld)){
|
||||||
|
rows.push({id:index, name: fld.label || fld.code, ...fld });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(props.fieldTypes.length===0 || props.fieldTypes.includes(fld.type)){
|
||||||
|
rows.push({id:index, name: fld.label || fld.code, ...fld });
|
||||||
|
}else if(props.fieldTypes.includes('lookup') && ('lookup' in fld)){
|
||||||
|
rows.push({id:index, name: fld.label || fld.code, ...fld });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isLoaded.value = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
watchEffect(()=>{
|
||||||
|
if (selected.value && selected.value[0] && props.updateSelectFields) {
|
||||||
|
props.updateSelectFields(selected)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
columns,
|
||||||
|
rows,
|
||||||
|
selected,
|
||||||
|
isLoaded,
|
||||||
|
pageSetting
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export interface Rule{
|
|
||||||
id:number;
|
|
||||||
name:string;
|
|
||||||
condtion:CondtionTree
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CondtionTree{
|
|
||||||
|
|
||||||
}
|
|
||||||
24
frontend/src/components/ShareDomain/RoleLabel.vue
Normal file
24
frontend/src/components/ShareDomain/RoleLabel.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<q-badge class="q-mr-xs" v-if="isOwner" color="secondary">所有者</q-badge>
|
||||||
|
<!-- <q-badge v-else-if="isManager" color="primary">管理者</q-badge> -->
|
||||||
|
<q-badge v-if="isSelf" color="purple">自分</q-badge>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { IDomainOwnerDisplay } from '../../types/DomainTypes';
|
||||||
|
import { useAuthStore } from 'stores/useAuthStore';
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
user: { id: number };
|
||||||
|
domain: IDomainOwnerDisplay;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const isSelf = computed(() => props.user.id === (Number)(authStore.userId));
|
||||||
|
const isOwner = computed(() => props.user.id === props.domain.owner.id);
|
||||||
|
const isManager = computed(() => props.user.id === props.domain.owner.id); // TODO
|
||||||
|
</script>
|
||||||
276
frontend/src/components/ShareDomain/ShareDomainDialog.vue
Normal file
276
frontend/src/components/ShareDomain/ShareDomainDialog.vue
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
<template>
|
||||||
|
<q-dialog :auto-close="false" :model-value="visible" persistent bordered>
|
||||||
|
<q-card class="dialog-content" >
|
||||||
|
<q-toolbar class="bg-grey-4">
|
||||||
|
<q-toolbar-title>{{ dialogTitle }}</q-toolbar-title>
|
||||||
|
<q-btn flat round dense icon="close" @click="close" />
|
||||||
|
</q-toolbar>
|
||||||
|
|
||||||
|
<q-card-section class="q-mx-md " >
|
||||||
|
<q-select
|
||||||
|
v-permissions="props.actionPermissions.edit"
|
||||||
|
class="q-mt-md"
|
||||||
|
:disable="loading||!domain.domainActive"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="canSharedUserFilter"
|
||||||
|
use-input
|
||||||
|
input-debounce="0"
|
||||||
|
:options="canSharedUserFilteredOptions"
|
||||||
|
clearable
|
||||||
|
:placeholder="canSharedUserFilter ? '' : domain.domainActive ? '権限を付与するユーザーを選択' : '接続先が無効なため、権限を付与できません'"
|
||||||
|
@filter="filterFn">
|
||||||
|
|
||||||
|
<template v-slot:selected-item="scope">
|
||||||
|
<span v-if="canSharedUserFilter">
|
||||||
|
{{ canSharedUserFilter.fullName }} ({{ canSharedUserFilter.email }})
|
||||||
|
<role-label :domain="domain" :user="scope.opt"></role-label>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:after>
|
||||||
|
<q-btn :disable="!canSharedUserFilter" :loading="addLoading" label="付与" color="primary" @click="shareTo(canSharedUserFilter as IUserDisplayWithShareRole)" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:option="scope">
|
||||||
|
<q-item v-bind="scope.itemProps">
|
||||||
|
<q-item-section avatar>{{scope.opt.id}}</q-item-section>
|
||||||
|
<q-item-section>{{scope.opt.fullName}}</q-item-section>
|
||||||
|
<q-item-section>{{scope.opt.email}}</q-item-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<div style="width: 6.5em;">
|
||||||
|
<role-label :domain="domain" :user="scope.opt"></role-label>
|
||||||
|
</div>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
|
||||||
|
<sharing-user-list class="q-mt-md" style="height: 330px" :users="sharedUsers" :loading="loading" :title="userListTitle">
|
||||||
|
<template v-slot:body-cell-role="{ row }">
|
||||||
|
<q-td auto-width>
|
||||||
|
<role-label :domain="domain" :user="row"></role-label>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:actions="{ row }">
|
||||||
|
<q-btn v-permissions="props.actionPermissions.edit" round title="解除" flat color="primary" :disable="isActionDisable && isActionDisable(row)" padding="xs" size="1em" :loading="row.isRemoving" icon="person_off" @click="removeShareTo(row)" />
|
||||||
|
</template>
|
||||||
|
</sharing-user-list>
|
||||||
|
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-actions align="right" class="text-primary">
|
||||||
|
<q-btn flat label="確定" @click="checkClose" />
|
||||||
|
<q-btn flat label="キャンセル" @click="close" />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { IDomainOwnerDisplay } from '../../types/DomainTypes';
|
||||||
|
import { IUser, IUserDisplay } from '../../types/UserTypes';
|
||||||
|
import { api } from 'boot/axios';
|
||||||
|
import { useAuthStore } from 'stores/useAuthStore';
|
||||||
|
import SharingUserList from 'components/ShareDomain/SharingUserList.vue';
|
||||||
|
import RoleLabel from 'components/ShareDomain/RoleLabel.vue';
|
||||||
|
import { Dialog } from 'quasar'
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: boolean;
|
||||||
|
domain: IDomainOwnerDisplay;
|
||||||
|
dialogTitle: string;
|
||||||
|
userListTitle: string;
|
||||||
|
isActionDisable?: (user: IUserDisplay) => boolean;
|
||||||
|
shareApi: (user: IUserDisplay, domain: IDomainOwnerDisplay) => Promise<any>;
|
||||||
|
removeSharedApi: (user: IUserDisplay, domain: IDomainOwnerDisplay) => Promise<any>;
|
||||||
|
getSharedApi: (domain: IDomainOwnerDisplay) => Promise<any>;
|
||||||
|
actionPermissions: { 'list': string, 'edit': string };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IUserDisplayWithShareRole extends IUserDisplay {
|
||||||
|
isRemoving: boolean;
|
||||||
|
role: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: boolean): void;
|
||||||
|
(e: 'close'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const addLoading = ref(false);
|
||||||
|
const loading = ref(true);
|
||||||
|
const visible = ref(props.modelValue);
|
||||||
|
|
||||||
|
const allUsers = ref<IUserDisplayWithShareRole[]>([]);
|
||||||
|
const sharedUsers = ref<IUserDisplayWithShareRole[]>([]);
|
||||||
|
const sharedUsersIdSet = new Set<number>();
|
||||||
|
|
||||||
|
const canSharedUsers = ref<IUserDisplayWithShareRole[]>([]);
|
||||||
|
const canSharedUserFilter = ref<IUserDisplayWithShareRole>();
|
||||||
|
const canSharedUserFilteredOptions = ref<IUserDisplayWithShareRole[]>([]);
|
||||||
|
|
||||||
|
const filterFn = (val:string, update: (cb: () => void) => void) => {
|
||||||
|
update(() => {
|
||||||
|
if (val === '') {
|
||||||
|
canSharedUserFilteredOptions.value = canSharedUsers.value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const needle = val.toLowerCase();
|
||||||
|
canSharedUserFilteredOptions.value = canSharedUsers.value.filter(v =>
|
||||||
|
v.email.toLowerCase().indexOf(needle) > -1 || v.fullNameSearch.toLowerCase().indexOf(needle) > -1);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
async (newValue) => {
|
||||||
|
visible.value = newValue;
|
||||||
|
sharedUsers.value = [];
|
||||||
|
canSharedUserFilter.value = undefined
|
||||||
|
loading.value = false;
|
||||||
|
addLoading.value = false;
|
||||||
|
if (newValue) {
|
||||||
|
if (Object.keys(allUsers.value).length == 0) {
|
||||||
|
await getUsers();
|
||||||
|
}
|
||||||
|
await loadShared();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => visible.value,
|
||||||
|
(newValue) => {
|
||||||
|
emit('update:modelValue', newValue);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkClose = () => {
|
||||||
|
if (!canSharedUserFilter.value) {
|
||||||
|
close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Dialog.create({
|
||||||
|
title: '注意',
|
||||||
|
message: '選択済だがまだ付与未完了のユーザーがあります。<br>必要な操作を選んでください。',
|
||||||
|
html: true,
|
||||||
|
persistent: true,
|
||||||
|
ok: {
|
||||||
|
color: 'primary',
|
||||||
|
label: '付与'
|
||||||
|
},
|
||||||
|
cancel: '直接閉じる',
|
||||||
|
}).onCancel(() => {
|
||||||
|
close();
|
||||||
|
}).onOk(() => {
|
||||||
|
shareTo(canSharedUserFilter.value as IUserDisplayWithShareRole);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const shareTo = async (user: IUserDisplayWithShareRole) => {
|
||||||
|
addLoading.value = true;
|
||||||
|
loading.value = true;
|
||||||
|
await props.shareApi(user, props.domain);
|
||||||
|
await loadShared();
|
||||||
|
canSharedUserFilter.value = undefined;
|
||||||
|
loading.value = false;
|
||||||
|
addLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeShareTo = async (user: IUserDisplayWithShareRole) => {
|
||||||
|
loading.value = true;
|
||||||
|
user.isRemoving = true;
|
||||||
|
await props.removeSharedApi(user, props.domain);
|
||||||
|
if (isCurrentDomain()) {
|
||||||
|
await authStore.loadCurrentDomain();
|
||||||
|
}
|
||||||
|
await loadShared();
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCurrentDomain = () => {
|
||||||
|
return props.domain.id === authStore.currentDomain.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadShared = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
sharedUsersIdSet.clear();
|
||||||
|
|
||||||
|
const { data } = await props.getSharedApi(props.domain);
|
||||||
|
|
||||||
|
sharedUsers.value = data.data.reduce((arr: IUserDisplayWithShareRole[], item: IUser) => {
|
||||||
|
const val = itemToDisplay(item);
|
||||||
|
if(!sharedUsersIdSet.has(val.id)) {
|
||||||
|
sharedUsersIdSet.add(val.id);
|
||||||
|
// for sort
|
||||||
|
if (isOwner(val.id)) {
|
||||||
|
val.role = 2;
|
||||||
|
} else if (isManager(val.id)) {
|
||||||
|
val.role = 1;
|
||||||
|
} else {
|
||||||
|
val.role = 0;
|
||||||
|
}
|
||||||
|
arr.push(val);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}, []).sort((a: IUserDisplayWithShareRole, b: IUserDisplayWithShareRole) => b.role - a.role);
|
||||||
|
|
||||||
|
canSharedUsers.value = allUsers.value.filter((item) => !sharedUsersIdSet.has(item.id));
|
||||||
|
canSharedUserFilteredOptions.value = canSharedUsers.value;
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOwner(userId: number) {
|
||||||
|
return userId === props.domain?.owner?.id
|
||||||
|
}
|
||||||
|
|
||||||
|
function isManager(userId: number) {
|
||||||
|
return false // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUsers = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
const result = await api.get('api/v1/users');
|
||||||
|
allUsers.value = result.data.data.map(itemToDisplay);
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemToDisplay = (item: IUser) => {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
firstName: item.first_name,
|
||||||
|
lastName: item.last_name,
|
||||||
|
fullNameSearch: (item.last_name + item.first_name).toLowerCase(),
|
||||||
|
fullName: item.last_name + ' ' + item.first_name,
|
||||||
|
email: item.email,
|
||||||
|
isSuperuser: item.is_superuser,
|
||||||
|
isActive: item.is_active,
|
||||||
|
role: 0,
|
||||||
|
} as IUserDisplayWithShareRole
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.dialog-content {
|
||||||
|
width: 700px !important;
|
||||||
|
max-width: 80vw !important;
|
||||||
|
max-height: 80vh;
|
||||||
|
.q-select {
|
||||||
|
min-width: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
63
frontend/src/components/ShareDomain/ShareManageDialog.vue
Normal file
63
frontend/src/components/ShareDomain/ShareManageDialog.vue
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<share-domain-dialog
|
||||||
|
:dialogTitle="`「${domain.name}」の接続先管理権限設定`"
|
||||||
|
userListTitle="接続先管理権限を持つユーザー"
|
||||||
|
:domain="domain"
|
||||||
|
:share-api="shareApi"
|
||||||
|
:remove-shared-api="removeSharedApi"
|
||||||
|
:get-shared-api="getSharedApi"
|
||||||
|
:is-action-disable="(row) => row.id === authStore.userId"
|
||||||
|
:model-value="modelValue"
|
||||||
|
:action-permissions="Actions.domain.grantManage"
|
||||||
|
@update:modelValue="updateModelValue"
|
||||||
|
@close="close"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps, defineEmits } from 'vue';
|
||||||
|
import ShareDomainDialog from 'components/ShareDomain/ShareDomainDialog.vue';
|
||||||
|
import { IUserDisplay } from '../../types/UserTypes';
|
||||||
|
import { IDomainOwnerDisplay } from '../../types/DomainTypes';
|
||||||
|
import { api } from 'boot/axios';
|
||||||
|
import { Actions } from 'boot/permissions';
|
||||||
|
import { useAuthStore } from 'src/stores/useAuthStore';
|
||||||
|
import { IResponse } from 'src/types/BaseTypes';
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: boolean;
|
||||||
|
domain: IDomainOwnerDisplay;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
async function shareApi(user: IUserDisplay, domain: IDomainOwnerDisplay) {
|
||||||
|
return api.post('api/managedomain', {
|
||||||
|
userid: user.id,
|
||||||
|
domainid: domain.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeSharedApi(user: IUserDisplay, domain: IDomainOwnerDisplay) {
|
||||||
|
return api.delete<IResponse>(`api/managedomain/${domain.id}/${user.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSharedApi(domain: IDomainOwnerDisplay) {
|
||||||
|
return api.get<IResponse>(`/api/managedomainuser/${domain.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: boolean): void;
|
||||||
|
(e: 'close'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const updateModelValue = (value: boolean) => {
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
58
frontend/src/components/ShareDomain/ShareUsageDialog.vue
Normal file
58
frontend/src/components/ShareDomain/ShareUsageDialog.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<share-domain-dialog
|
||||||
|
:dialogTitle="`「${domain.name}」の接続先利用権限設定`"
|
||||||
|
userListTitle="接続先利用権限を持つユーザー"
|
||||||
|
:domain="domain"
|
||||||
|
:share-api="shareApi"
|
||||||
|
:remove-shared-api="removeSharedApi"
|
||||||
|
:get-shared-api="getSharedApi"
|
||||||
|
:model-value="modelValue"
|
||||||
|
:action-permissions="Actions.domain.grantUse"
|
||||||
|
@update:modelValue="updateModelValue"
|
||||||
|
@close="close"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps, defineEmits } from 'vue';
|
||||||
|
import ShareDomainDialog from 'components/ShareDomain/ShareDomainDialog.vue';
|
||||||
|
import { IUserDisplay } from '../../types/UserTypes';
|
||||||
|
import { IDomainOwnerDisplay } from '../../types/DomainTypes';
|
||||||
|
import { api } from 'boot/axios';
|
||||||
|
import { Actions } from 'boot/permissions';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: boolean;
|
||||||
|
domain: IDomainOwnerDisplay;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
async function shareApi(user: IUserDisplay, domain: IDomainOwnerDisplay) {
|
||||||
|
return api.post('api/userdomain', {
|
||||||
|
userid: user.id,
|
||||||
|
domainid: domain.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeSharedApi(user: IUserDisplay, domain: IDomainOwnerDisplay) {
|
||||||
|
return api.delete(`api/domain/${domain.id}/${user.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSharedApi(domain: IDomainOwnerDisplay) {
|
||||||
|
return api.get(`/api/domainshareduser/${domain.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: boolean): void;
|
||||||
|
(e: 'close'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const updateModelValue = (value: boolean) => {
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
53
frontend/src/components/ShareDomain/SharingUserList.vue
Normal file
53
frontend/src/components/ShareDomain/SharingUserList.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<q-table :rows="users" :filter="filter" dense :columns="columns" row-key="id" :loading="loading" :pagination="pagination">
|
||||||
|
<template v-slot:top>
|
||||||
|
<div class="h6 text-weight-bold">{{props.title}}</div>
|
||||||
|
<q-space />
|
||||||
|
<q-input borderless dense filled debounce="300" v-model="filter" placeholder="検索">
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon name="search" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-for="col in columns" :key="col.name" v-slot:[`body-cell-${col.name}`]="props">
|
||||||
|
<slot :name="`body-cell-${col.name}`" :row="props.row" :column="props.col">
|
||||||
|
<!-- 默认内容 -->
|
||||||
|
<q-td v-if="col.name !== 'actions'" :props="props" >
|
||||||
|
<span>{{ props.row[col.name] }}</span>
|
||||||
|
</q-td>
|
||||||
|
<!-- actions -->
|
||||||
|
<q-td v-else auto-width :props="props">
|
||||||
|
<slot name="actions" :row="props.row"></slot>
|
||||||
|
</q-td>
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, PropType } from 'vue';
|
||||||
|
import { IUserDisplay } from '../../types/UserTypes';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
users: {
|
||||||
|
type: Array as PropType<IUserDisplay[]>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
title: String
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
|
||||||
|
{ name: 'fullName', label: '名前', field: 'fullName', align: 'left', sortable: true },
|
||||||
|
{ name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true },
|
||||||
|
{ name: 'role', label: '', field: 'role', align: 'left', sortable: false },
|
||||||
|
{ name: 'actions', label: '', field: 'actions', sortable: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
const filter = ref('');
|
||||||
|
const pagination = ref({ rowsPerPage: 10 });
|
||||||
|
</script>
|
||||||
71
frontend/src/components/ShowDialog.vue
Normal file
71
frontend/src/components/ShowDialog.vue
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<!-- <div class="q-pa-md q-gutter-sm" > -->
|
||||||
|
<q-dialog :model-value="visible" persistent bordered >
|
||||||
|
<q-card class="" style="min-width: 40vw; max-width: 80vw; max-height: 95vh;" :style="cardStyle">
|
||||||
|
<q-toolbar class="bg-grey-4">
|
||||||
|
<q-toolbar-title>{{ name }}</q-toolbar-title>
|
||||||
|
<q-space v-if="$slots.toolbar"></q-space>
|
||||||
|
<slot name="toolbar"></slot>
|
||||||
|
<q-btn flat round dense icon="close" @click="CloseDialogue('Cancel')" />
|
||||||
|
</q-toolbar>
|
||||||
|
<q-card-section class="q-mt-md" :style="sectionStyle">
|
||||||
|
<slot></slot>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions v-if="!disableBtn" align="right" class="text-primary">
|
||||||
|
<q-btn flat :label="okBtnLabel || '確定'" :loading="okBtnLoading" :v-close-popup="okBtnAutoClose" @click="CloseDialogue('OK')" />
|
||||||
|
<q-btn flat label="キャンセル" :disable="okBtnLoading" v-close-popup @click="CloseDialogue('Cancel')" />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
<!-- </div> -->
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import {computed} from 'vue'
|
||||||
|
export default {
|
||||||
|
name: 'ShowDialog',
|
||||||
|
props: {
|
||||||
|
name:String,
|
||||||
|
visible: Boolean,
|
||||||
|
width:String,
|
||||||
|
height:String,
|
||||||
|
minWidth:String,
|
||||||
|
minHeight:String,
|
||||||
|
okBtnLabel:String,
|
||||||
|
okBtnLoading:Boolean,
|
||||||
|
okBtnAutoClose:{
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
disableBtn:{
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: [
|
||||||
|
'close',
|
||||||
|
'update:visible'
|
||||||
|
],
|
||||||
|
setup(props, context) {
|
||||||
|
const CloseDialogue = (val) => {
|
||||||
|
context.emit('update:visible', false);
|
||||||
|
context.emit('close', val);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardStyle = computed(() => ({
|
||||||
|
minWidth: props.minWidth,
|
||||||
|
width: props.width
|
||||||
|
}));
|
||||||
|
|
||||||
|
const sectionStyle = computed(() => ({
|
||||||
|
height: props.height,
|
||||||
|
minHeight: props.minHeight
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
CloseDialogue,
|
||||||
|
cardStyle,
|
||||||
|
sectionStyle
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
92
frontend/src/components/TableActionMenu.vue
Normal file
92
frontend/src/components/TableActionMenu.vue
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<template>
|
||||||
|
<q-btn v-if="hasPermission()" flat padding="xs" round size="1em" icon="more_vert" class="action-menu">
|
||||||
|
<q-menu :max-width="maxWidth">
|
||||||
|
<q-list dense :style="{ 'min-width': minWidth }">
|
||||||
|
<template v-for="(item, index) in actions" :key="index" >
|
||||||
|
<q-item v-if="isAction(item)" v-permissions="item.permission" :disable="isFunction(item.disable) ? item.disable(row) : item.disable"
|
||||||
|
:class="item.class" clickable v-close-popup @click="item.action(row)">
|
||||||
|
<q-item-section side style="color: inherit;">
|
||||||
|
<q-icon size="1.2em" :name="item.icon" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>{{ item.label }}</q-item-section>
|
||||||
|
<q-tooltip v-if="item.tooltip && !isFunction(item.tooltip) || (isFunction(item.tooltip) && item.tooltip(row))" :delay="500" self="center middle">
|
||||||
|
{{ isFunction(item.tooltip) ? item.tooltip(row) : item.tooltip }}
|
||||||
|
</q-tooltip>
|
||||||
|
</q-item>
|
||||||
|
<q-separator v-else />
|
||||||
|
</template>
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { PropType, getCurrentInstance } from 'vue';
|
||||||
|
import { IDomainOwnerDisplay } from '../types/DomainTypes';
|
||||||
|
|
||||||
|
interface Action {
|
||||||
|
label: string;
|
||||||
|
icon?: string;
|
||||||
|
tooltip?: string|((row: IDomainOwnerDisplay) => string);
|
||||||
|
disable?: boolean|((row: IDomainOwnerDisplay) => boolean);
|
||||||
|
permission?: string|object;
|
||||||
|
action: (row: any) => void|Promise<void>;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
interface Separator {
|
||||||
|
separator: boolean;
|
||||||
|
}
|
||||||
|
type MenuItem = Action | Separator;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TableActionMenu',
|
||||||
|
props: {
|
||||||
|
row: {
|
||||||
|
type: Object as PropType<IDomainOwnerDisplay>,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
maxWidth: {
|
||||||
|
type: String,
|
||||||
|
default: '150px'
|
||||||
|
},
|
||||||
|
minWidth: {
|
||||||
|
type: String,
|
||||||
|
default: '100px'
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
type: Array as PropType<MenuItem[]>,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isAction(item: MenuItem): item is Action {
|
||||||
|
return !('separator' in item);
|
||||||
|
},
|
||||||
|
|
||||||
|
isFunction(item: any): item is ((row: IDomainOwnerDisplay) => boolean|string) {
|
||||||
|
return typeof item === 'function';
|
||||||
|
},
|
||||||
|
|
||||||
|
hasPermission() {
|
||||||
|
const proxy = getCurrentInstance()?.proxy;
|
||||||
|
if (!proxy) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const item of this.actions) {
|
||||||
|
if (this.isAction(item) && proxy.$hasPermission(item.permission)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.q-table tr > td:last-child .action-menu {
|
||||||
|
opacity: 0.25 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.q-table tr:hover > td:last-child .action-menu:not([disabled]) {
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
74
frontend/src/components/UserDomain/DomainCard.vue
Normal file
74
frontend/src/components/UserDomain/DomainCard.vue
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<template>
|
||||||
|
<q-card :class="['domain-card', item.id == activeId ? 'default': '']">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row no-wrap">
|
||||||
|
<div class="col">
|
||||||
|
<div class="text-h6 ellipsis">{{ item.name }}</div>
|
||||||
|
<div class="text-subtitle2">{{ item.url }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<!-- <q-badge color="secondary" text-color="white" align="middle" class="q-mb-xs" label="他人の所有" /> -->
|
||||||
|
<q-chip v-if="!isOwnerFunc(item.owner.id)" square color="secondary" text-color="white" icon="people" label="他人の所有" size="sm" />
|
||||||
|
<q-chip v-else square color="purple" text-color="white" icon="people" label="自分" size="sm" />
|
||||||
|
<div class="text-right">
|
||||||
|
<!-- icon="add_moderator" -->
|
||||||
|
<!-- <q-chip square color="primary" text-color="white" label="管理者" size="sm" /> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row items-center no-wrap">
|
||||||
|
<div class="col">
|
||||||
|
<div class="text-grey-7 text-caption text-weight-medium">
|
||||||
|
アカウント
|
||||||
|
</div>
|
||||||
|
<div class="smaller-font-size">{{ item.user }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="text-grey-7 text-caption text-weight-medium">
|
||||||
|
所有者
|
||||||
|
</div>
|
||||||
|
<div class="smaller-font-size">{{ !isOwnerFunc(item.owner.id) ? item.owner.fullName : '自分' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-separator v-if="$slots.actions" />
|
||||||
|
<slot name="actions" :item="item"></slot>
|
||||||
|
</q-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps, computed } from 'vue';
|
||||||
|
import { IDomainOwnerDisplay } from 'src/types/DomainTypes';
|
||||||
|
import { useAuthStore } from 'stores/useAuthStore';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
item: IDomainOwnerDisplay;
|
||||||
|
activeId: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const isOwnerFunc = computed(() => (ownerId: string) => {
|
||||||
|
return ownerId == authStore.userId;
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.domain-card.default {
|
||||||
|
box-shadow: 0 6px 6px -3px rgba(0, 0, 0, 0.2),
|
||||||
|
0 10px 14px 1px rgba(0, 0, 0, 0.14),
|
||||||
|
0 4px 18px 3px rgba(0, 0, 0, 0.12),
|
||||||
|
inset 0 0 0px 2px #1976D2;
|
||||||
|
}
|
||||||
|
.domain-card {
|
||||||
|
width: 22rem;
|
||||||
|
word-break: break-word;
|
||||||
|
|
||||||
|
.smaller-font-size {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user