Compare commits
19 Commits
fix/create
...
fix/update
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b60c48838d | ||
|
|
c0962e9f70 | ||
|
|
3e6aec2d6b | ||
|
|
4658b2961c | ||
|
|
bc8b5463b3 | ||
|
|
9eee59a877 | ||
|
|
e32e400976 | ||
|
|
b4b07f3120 | ||
|
|
e39faa1172 | ||
|
|
b46979f539 | ||
|
|
f4237fffe6 | ||
|
|
1d9bb96367 | ||
|
|
bcb449e394 | ||
|
|
ae4bc77407 | ||
|
|
6056167063 | ||
|
|
adf91c7dfd | ||
|
|
bba9a94e45 | ||
|
|
e36eb64c59 | ||
|
|
201fe05493 |
@@ -238,9 +238,9 @@ checksums:
|
|||||||
ZXSPJt/message: 1b5749b0cca6a62d75a577bee6804074
|
ZXSPJt/message: 1b5749b0cca6a62d75a577bee6804074
|
||||||
ZXSPJt/origin/0/0: 0627e0040ca4939c9a57349e98c51797
|
ZXSPJt/origin/0/0: 0627e0040ca4939c9a57349e98c51797
|
||||||
ZXSPJt/translation: 1b5749b0cca6a62d75a577bee6804074
|
ZXSPJt/translation: 1b5749b0cca6a62d75a577bee6804074
|
||||||
YkfDoz/translation: 5d4f1c56f4e9e579ec9967ca68cc64ac
|
|
||||||
YkfDoz/message: 5d4f1c56f4e9e579ec9967ca68cc64ac
|
YkfDoz/message: 5d4f1c56f4e9e579ec9967ca68cc64ac
|
||||||
YkfDoz/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
YkfDoz/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
||||||
|
YkfDoz/translation: 5d4f1c56f4e9e579ec9967ca68cc64ac
|
||||||
3bqt9U/message: 2366ed295eb2c03c425559c24cb31606
|
3bqt9U/message: 2366ed295eb2c03c425559c24cb31606
|
||||||
3bqt9U/origin/0/0: 8628ddb6f4ded25c0f17e2cbda8207d3
|
3bqt9U/origin/0/0: 8628ddb6f4ded25c0f17e2cbda8207d3
|
||||||
3bqt9U/translation: 2366ed295eb2c03c425559c24cb31606
|
3bqt9U/translation: 2366ed295eb2c03c425559c24cb31606
|
||||||
@@ -322,9 +322,9 @@ checksums:
|
|||||||
1rWxEw/message: 122c108794f321c10e05a08f09b11457
|
1rWxEw/message: 122c108794f321c10e05a08f09b11457
|
||||||
1rWxEw/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
1rWxEw/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
||||||
1rWxEw/translation: 122c108794f321c10e05a08f09b11457
|
1rWxEw/translation: 122c108794f321c10e05a08f09b11457
|
||||||
iH8pgl/translation: f541015a827e37cb3b1234e56bc2aa3c
|
|
||||||
iH8pgl/message: f541015a827e37cb3b1234e56bc2aa3c
|
iH8pgl/message: f541015a827e37cb3b1234e56bc2aa3c
|
||||||
iH8pgl/origin/0/0: 69d8634b177fdf937764b406fd477547
|
iH8pgl/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||||
|
iH8pgl/translation: f541015a827e37cb3b1234e56bc2aa3c
|
||||||
KNKCTb/message: 35919fd5b10ad212839a75e72829fe4e
|
KNKCTb/message: 35919fd5b10ad212839a75e72829fe4e
|
||||||
KNKCTb/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
KNKCTb/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
||||||
KNKCTb/translation: 35919fd5b10ad212839a75e72829fe4e
|
KNKCTb/translation: 35919fd5b10ad212839a75e72829fe4e
|
||||||
@@ -343,9 +343,9 @@ checksums:
|
|||||||
UoSI%2Bi/message: 47f1051a134e111930be7f94179df4c8
|
UoSI%2Bi/message: 47f1051a134e111930be7f94179df4c8
|
||||||
UoSI%2Bi/origin/0/0: fadd5a5b6963ec5f261072f7e8d2140b
|
UoSI%2Bi/origin/0/0: fadd5a5b6963ec5f261072f7e8d2140b
|
||||||
UoSI%2Bi/translation: 47f1051a134e111930be7f94179df4c8
|
UoSI%2Bi/translation: 47f1051a134e111930be7f94179df4c8
|
||||||
zt%2F6cS/translation: aadb69a7f7421be6bb022fbde08a6d77
|
|
||||||
zt%2F6cS/message: aadb69a7f7421be6bb022fbde08a6d77
|
zt%2F6cS/message: aadb69a7f7421be6bb022fbde08a6d77
|
||||||
zt%2F6cS/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
zt%2F6cS/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
||||||
|
zt%2F6cS/translation: aadb69a7f7421be6bb022fbde08a6d77
|
||||||
qaS%2B1%2F/message: 4e25347caf65e1b63bec90e101b86ddd
|
qaS%2B1%2F/message: 4e25347caf65e1b63bec90e101b86ddd
|
||||||
qaS%2B1%2F/origin/0/0: fadd5a5b6963ec5f261072f7e8d2140b
|
qaS%2B1%2F/origin/0/0: fadd5a5b6963ec5f261072f7e8d2140b
|
||||||
qaS%2B1%2F/translation: 4e25347caf65e1b63bec90e101b86ddd
|
qaS%2B1%2F/translation: 4e25347caf65e1b63bec90e101b86ddd
|
||||||
@@ -372,8 +372,7 @@ checksums:
|
|||||||
QD8opX/origin/2/0: faa8a2576972b4eb31565d8ea6098f43
|
QD8opX/origin/2/0: faa8a2576972b4eb31565d8ea6098f43
|
||||||
QD8opX/translation: 101ff39aab674c033c15ab978d0d8bac
|
QD8opX/translation: 101ff39aab674c033c15ab978d0d8bac
|
||||||
YhvWXb/message: 336a8c70a91adc49c15266167ee84cc0
|
YhvWXb/message: 336a8c70a91adc49c15266167ee84cc0
|
||||||
YhvWXb/origin/0/0: ff4a160eca5f8da4f075fd1dd6061fa6
|
YhvWXb/origin/0/0: dbad0c8d7863cb41f5495264f82e7081
|
||||||
YhvWXb/origin/1/0: dbad0c8d7863cb41f5495264f82e7081
|
|
||||||
YhvWXb/translation: 336a8c70a91adc49c15266167ee84cc0
|
YhvWXb/translation: 336a8c70a91adc49c15266167ee84cc0
|
||||||
i0ZMQl/message: f739058266a2cb6a3d7cca14de09c69a
|
i0ZMQl/message: f739058266a2cb6a3d7cca14de09c69a
|
||||||
i0ZMQl/origin/0/0: 0ad2af981579a62b084b3d41462c5546
|
i0ZMQl/origin/0/0: 0ad2af981579a62b084b3d41462c5546
|
||||||
@@ -593,9 +592,9 @@ checksums:
|
|||||||
EH8IL3/origin/0/0: c0b59968b2b234167c488a05cc38710b
|
EH8IL3/origin/0/0: c0b59968b2b234167c488a05cc38710b
|
||||||
EH8IL3/origin/1/0: fadd5a5b6963ec5f261072f7e8d2140b
|
EH8IL3/origin/1/0: fadd5a5b6963ec5f261072f7e8d2140b
|
||||||
EH8IL3/translation: 6f79129c8f08ee54d858a2af57d16dd9
|
EH8IL3/translation: 6f79129c8f08ee54d858a2af57d16dd9
|
||||||
zMquA7/translation: 38ff8d574865bb71d2f620bc4ba98c20
|
|
||||||
zMquA7/message: 38ff8d574865bb71d2f620bc4ba98c20
|
zMquA7/message: 38ff8d574865bb71d2f620bc4ba98c20
|
||||||
zMquA7/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
zMquA7/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
||||||
|
zMquA7/translation: 38ff8d574865bb71d2f620bc4ba98c20
|
||||||
VHS0aJ/message: 31d0962985c83a29558c98a765bb1b16
|
VHS0aJ/message: 31d0962985c83a29558c98a765bb1b16
|
||||||
VHS0aJ/origin/0/0: 15792234f408027822ee71ccddde6f9b
|
VHS0aJ/origin/0/0: 15792234f408027822ee71ccddde6f9b
|
||||||
VHS0aJ/translation: 31d0962985c83a29558c98a765bb1b16
|
VHS0aJ/translation: 31d0962985c83a29558c98a765bb1b16
|
||||||
@@ -629,9 +628,8 @@ checksums:
|
|||||||
dtQIWT/origin/0/0: c0b59968b2b234167c488a05cc38710b
|
dtQIWT/origin/0/0: c0b59968b2b234167c488a05cc38710b
|
||||||
dtQIWT/translation: 9d2308d578ec21bd22374e880fe3bd3c
|
dtQIWT/translation: 9d2308d578ec21bd22374e880fe3bd3c
|
||||||
AUYALh/message: ee2b0671e00972773210c5be5a9ccb89
|
AUYALh/message: ee2b0671e00972773210c5be5a9ccb89
|
||||||
AUYALh/origin/0/0: ff4a160eca5f8da4f075fd1dd6061fa6
|
AUYALh/origin/0/0: d77eb1f90f2fa86a55181629d88d237a
|
||||||
AUYALh/origin/1/0: d77eb1f90f2fa86a55181629d88d237a
|
AUYALh/origin/1/0: dbad0c8d7863cb41f5495264f82e7081
|
||||||
AUYALh/origin/2/0: dbad0c8d7863cb41f5495264f82e7081
|
|
||||||
AUYALh/translation: ee2b0671e00972773210c5be5a9ccb89
|
AUYALh/translation: ee2b0671e00972773210c5be5a9ccb89
|
||||||
VbyRUy/message: 502237cd1044491c48494aa60967a985
|
VbyRUy/message: 502237cd1044491c48494aa60967a985
|
||||||
VbyRUy/origin/0/0: d77eb1f90f2fa86a55181629d88d237a
|
VbyRUy/origin/0/0: d77eb1f90f2fa86a55181629d88d237a
|
||||||
@@ -701,10 +699,10 @@ checksums:
|
|||||||
AX4ktp/message: 4e6be22ade0188f408471325fe363575
|
AX4ktp/message: 4e6be22ade0188f408471325fe363575
|
||||||
AX4ktp/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
AX4ktp/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
||||||
AX4ktp/translation: 4e6be22ade0188f408471325fe363575
|
AX4ktp/translation: 4e6be22ade0188f408471325fe363575
|
||||||
xGVfLh/translation: 3cfba90b4600131e82fc4260c568d044
|
|
||||||
xGVfLh/message: 3cfba90b4600131e82fc4260c568d044
|
xGVfLh/message: 3cfba90b4600131e82fc4260c568d044
|
||||||
xGVfLh/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
xGVfLh/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
||||||
xGVfLh/origin/1/0: 69d8634b177fdf937764b406fd477547
|
xGVfLh/origin/1/0: 69d8634b177fdf937764b406fd477547
|
||||||
|
xGVfLh/translation: 3cfba90b4600131e82fc4260c568d044
|
||||||
J2Wbjt/message: 8ed03cf7c5e60a6edf3470a4558ff058
|
J2Wbjt/message: 8ed03cf7c5e60a6edf3470a4558ff058
|
||||||
J2Wbjt/origin/0/0: d96af8a06c2e38a72238bcd16e426859
|
J2Wbjt/origin/0/0: d96af8a06c2e38a72238bcd16e426859
|
||||||
J2Wbjt/translation: 8ed03cf7c5e60a6edf3470a4558ff058
|
J2Wbjt/translation: 8ed03cf7c5e60a6edf3470a4558ff058
|
||||||
@@ -736,6 +734,9 @@ checksums:
|
|||||||
uvH2xS/message: ecc2617544a6d385d18678577fa112fd
|
uvH2xS/message: ecc2617544a6d385d18678577fa112fd
|
||||||
uvH2xS/origin/0/0: 97bd206a05414e2a571020d64e166093
|
uvH2xS/origin/0/0: 97bd206a05414e2a571020d64e166093
|
||||||
uvH2xS/translation: ecc2617544a6d385d18678577fa112fd
|
uvH2xS/translation: ecc2617544a6d385d18678577fa112fd
|
||||||
|
7mrkyO/translation: d3853b58e9b736eefb0f9bf400daf4d7
|
||||||
|
7mrkyO/message: d3853b58e9b736eefb0f9bf400daf4d7
|
||||||
|
7mrkyO/origin/0/0: 1b05beba6bdd908015fa8e798717f024
|
||||||
9PaIxv/message: da95932e7a1465a5d21aa3b855a46dc2
|
9PaIxv/message: da95932e7a1465a5d21aa3b855a46dc2
|
||||||
9PaIxv/origin/0/0: c0b59968b2b234167c488a05cc38710b
|
9PaIxv/origin/0/0: c0b59968b2b234167c488a05cc38710b
|
||||||
9PaIxv/translation: da95932e7a1465a5d21aa3b855a46dc2
|
9PaIxv/translation: da95932e7a1465a5d21aa3b855a46dc2
|
||||||
@@ -832,12 +833,11 @@ checksums:
|
|||||||
7RpHg%2F/origin/0/0: c0b59968b2b234167c488a05cc38710b
|
7RpHg%2F/origin/0/0: c0b59968b2b234167c488a05cc38710b
|
||||||
7RpHg%2F/origin/1/0: c0b59968b2b234167c488a05cc38710b
|
7RpHg%2F/origin/1/0: c0b59968b2b234167c488a05cc38710b
|
||||||
7RpHg%2F/translation: 66f702512197d25cfdb117f2415ecd88
|
7RpHg%2F/translation: 66f702512197d25cfdb117f2415ecd88
|
||||||
ZDPJBc/translation: f9154d0c02a7db907f0a33fb5edd4be1
|
|
||||||
ZDPJBc/message: f9154d0c02a7db907f0a33fb5edd4be1
|
ZDPJBc/message: f9154d0c02a7db907f0a33fb5edd4be1
|
||||||
ZDPJBc/origin/0/0: 69d8634b177fdf937764b406fd477547
|
ZDPJBc/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||||
|
ZDPJBc/translation: f9154d0c02a7db907f0a33fb5edd4be1
|
||||||
j9IZZ0/message: 7ba841d0946eb04fa3d17d74365be37b
|
j9IZZ0/message: 7ba841d0946eb04fa3d17d74365be37b
|
||||||
j9IZZ0/origin/0/0: ff4a160eca5f8da4f075fd1dd6061fa6
|
j9IZZ0/origin/0/0: dbad0c8d7863cb41f5495264f82e7081
|
||||||
j9IZZ0/origin/1/0: dbad0c8d7863cb41f5495264f82e7081
|
|
||||||
j9IZZ0/translation: 7ba841d0946eb04fa3d17d74365be37b
|
j9IZZ0/translation: 7ba841d0946eb04fa3d17d74365be37b
|
||||||
OwneeH/message: e97476d280d4ebfd196d6c5336f3511f
|
OwneeH/message: e97476d280d4ebfd196d6c5336f3511f
|
||||||
OwneeH/origin/0/0: fadd5a5b6963ec5f261072f7e8d2140b
|
OwneeH/origin/0/0: fadd5a5b6963ec5f261072f7e8d2140b
|
||||||
@@ -1049,9 +1049,9 @@ checksums:
|
|||||||
bFREhu/message: b2cc9a311fc28d1594252f697c7f5342
|
bFREhu/message: b2cc9a311fc28d1594252f697c7f5342
|
||||||
bFREhu/origin/0/0: 97bd206a05414e2a571020d64e166093
|
bFREhu/origin/0/0: 97bd206a05414e2a571020d64e166093
|
||||||
bFREhu/translation: b2cc9a311fc28d1594252f697c7f5342
|
bFREhu/translation: b2cc9a311fc28d1594252f697c7f5342
|
||||||
SUXBxp/translation: 76e841ae8ec3340d7d0a5c2191abbea4
|
|
||||||
SUXBxp/message: 76e841ae8ec3340d7d0a5c2191abbea4
|
SUXBxp/message: 76e841ae8ec3340d7d0a5c2191abbea4
|
||||||
SUXBxp/origin/0/0: 69d8634b177fdf937764b406fd477547
|
SUXBxp/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||||
|
SUXBxp/translation: 76e841ae8ec3340d7d0a5c2191abbea4
|
||||||
0%2BewPp/message: 785fe23c0eef0a5b60b5b2a88151de31
|
0%2BewPp/message: 785fe23c0eef0a5b60b5b2a88151de31
|
||||||
0%2BewPp/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
0%2BewPp/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
||||||
0%2BewPp/origin/1/0: eb63312c63f6c8d2a5b6520c89012123
|
0%2BewPp/origin/1/0: eb63312c63f6c8d2a5b6520c89012123
|
||||||
@@ -1332,9 +1332,9 @@ checksums:
|
|||||||
csFbe%2B/message: e4e58e33d637282d141df466d729bc7c
|
csFbe%2B/message: e4e58e33d637282d141df466d729bc7c
|
||||||
csFbe%2B/origin/0/0: 94d3e20c29bb517dd394711d7524e478
|
csFbe%2B/origin/0/0: 94d3e20c29bb517dd394711d7524e478
|
||||||
csFbe%2B/translation: e4e58e33d637282d141df466d729bc7c
|
csFbe%2B/translation: e4e58e33d637282d141df466d729bc7c
|
||||||
SUvm1Y/translation: 6c0f26526f77e9abb92fb2b1106a66db
|
|
||||||
SUvm1Y/message: 6c0f26526f77e9abb92fb2b1106a66db
|
SUvm1Y/message: 6c0f26526f77e9abb92fb2b1106a66db
|
||||||
SUvm1Y/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
SUvm1Y/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
||||||
|
SUvm1Y/translation: 6c0f26526f77e9abb92fb2b1106a66db
|
||||||
cdyS7J/message: 0f10050aeed2555a34fe3c27ce616607
|
cdyS7J/message: 0f10050aeed2555a34fe3c27ce616607
|
||||||
cdyS7J/origin/0/0: c0b59968b2b234167c488a05cc38710b
|
cdyS7J/origin/0/0: c0b59968b2b234167c488a05cc38710b
|
||||||
cdyS7J/origin/1/0: c0b59968b2b234167c488a05cc38710b
|
cdyS7J/origin/1/0: c0b59968b2b234167c488a05cc38710b
|
||||||
@@ -1380,6 +1380,9 @@ checksums:
|
|||||||
2Py5S%2F/message: fe1183a7439299210d8ab7379b8f4d01
|
2Py5S%2F/message: fe1183a7439299210d8ab7379b8f4d01
|
||||||
2Py5S%2F/origin/0/0: 83911e3eacbad4583e2b1647a784d154
|
2Py5S%2F/origin/0/0: 83911e3eacbad4583e2b1647a784d154
|
||||||
2Py5S%2F/translation: fe1183a7439299210d8ab7379b8f4d01
|
2Py5S%2F/translation: fe1183a7439299210d8ab7379b8f4d01
|
||||||
|
VOwhhz/translation: 613f43526ea128b2749f70878f10008c
|
||||||
|
VOwhhz/message: 613f43526ea128b2749f70878f10008c
|
||||||
|
VOwhhz/origin/0/0: 1b05beba6bdd908015fa8e798717f024
|
||||||
iwr7AP/message: 27c24b56895defecd10b0171b39bc93f
|
iwr7AP/message: 27c24b56895defecd10b0171b39bc93f
|
||||||
iwr7AP/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
iwr7AP/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
||||||
iwr7AP/translation: 27c24b56895defecd10b0171b39bc93f
|
iwr7AP/translation: 27c24b56895defecd10b0171b39bc93f
|
||||||
@@ -1509,9 +1512,8 @@ checksums:
|
|||||||
k7rCa%2F/origin/0/0: 3de48fb04f7df9bdbe399911cdb3fe86
|
k7rCa%2F/origin/0/0: 3de48fb04f7df9bdbe399911cdb3fe86
|
||||||
k7rCa%2F/translation: 890b7851af05f97ac53644e4154c13b4
|
k7rCa%2F/translation: 890b7851af05f97ac53644e4154c13b4
|
||||||
8Is1ih/message: 624592ba2ecf647a3fe60d493c1ee71a
|
8Is1ih/message: 624592ba2ecf647a3fe60d493c1ee71a
|
||||||
8Is1ih/origin/0/0: ff4a160eca5f8da4f075fd1dd6061fa6
|
8Is1ih/origin/0/0: fadd5a5b6963ec5f261072f7e8d2140b
|
||||||
8Is1ih/origin/1/0: fadd5a5b6963ec5f261072f7e8d2140b
|
8Is1ih/origin/1/0: dbad0c8d7863cb41f5495264f82e7081
|
||||||
8Is1ih/origin/2/0: dbad0c8d7863cb41f5495264f82e7081
|
|
||||||
8Is1ih/translation: 624592ba2ecf647a3fe60d493c1ee71a
|
8Is1ih/translation: 624592ba2ecf647a3fe60d493c1ee71a
|
||||||
EEHmPC/message: 8c1be80f5b2fcc112922282afbe30287
|
EEHmPC/message: 8c1be80f5b2fcc112922282afbe30287
|
||||||
EEHmPC/origin/0/0: 7493592314bc3b96ad96127a949ba1ba
|
EEHmPC/origin/0/0: 7493592314bc3b96ad96127a949ba1ba
|
||||||
@@ -1595,9 +1597,9 @@ checksums:
|
|||||||
jl3aEJ/placeholders/0/0: 06f23520ebabfffed4999b5bcf61c9cd
|
jl3aEJ/placeholders/0/0: 06f23520ebabfffed4999b5bcf61c9cd
|
||||||
jl3aEJ/origin/0/0: 1d6b5f88cdad160a67842f6b65387448
|
jl3aEJ/origin/0/0: 1d6b5f88cdad160a67842f6b65387448
|
||||||
jl3aEJ/translation: 4c38799ff25321ea25017bf4cd8e2e4f
|
jl3aEJ/translation: 4c38799ff25321ea25017bf4cd8e2e4f
|
||||||
vo2a%2Ba/translation: fcf0f06f8b64b458c7ca6d95541a3cc8
|
|
||||||
vo2a%2Ba/message: fcf0f06f8b64b458c7ca6d95541a3cc8
|
vo2a%2Ba/message: fcf0f06f8b64b458c7ca6d95541a3cc8
|
||||||
vo2a%2Ba/origin/0/0: 69d8634b177fdf937764b406fd477547
|
vo2a%2Ba/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||||
|
vo2a%2Ba/translation: fcf0f06f8b64b458c7ca6d95541a3cc8
|
||||||
agPptk/message: 43983f2178fb28aacaef6c5f41282c4a
|
agPptk/message: 43983f2178fb28aacaef6c5f41282c4a
|
||||||
agPptk/origin/0/0: 3de48fb04f7df9bdbe399911cdb3fe86
|
agPptk/origin/0/0: 3de48fb04f7df9bdbe399911cdb3fe86
|
||||||
agPptk/translation: 43983f2178fb28aacaef6c5f41282c4a
|
agPptk/translation: 43983f2178fb28aacaef6c5f41282c4a
|
||||||
@@ -1868,9 +1870,9 @@ checksums:
|
|||||||
8RdLDU/message: d7820b1bf4efecc61ed89234567aaa9c
|
8RdLDU/message: d7820b1bf4efecc61ed89234567aaa9c
|
||||||
8RdLDU/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
8RdLDU/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
||||||
8RdLDU/translation: d7820b1bf4efecc61ed89234567aaa9c
|
8RdLDU/translation: d7820b1bf4efecc61ed89234567aaa9c
|
||||||
Oi%2BJ%2BN/translation: 313804fe615d51e0f0cbd599161d1ade
|
|
||||||
Oi%2BJ%2BN/message: 313804fe615d51e0f0cbd599161d1ade
|
Oi%2BJ%2BN/message: 313804fe615d51e0f0cbd599161d1ade
|
||||||
Oi%2BJ%2BN/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
Oi%2BJ%2BN/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
||||||
|
Oi%2BJ%2BN/translation: 313804fe615d51e0f0cbd599161d1ade
|
||||||
Iqh0Uv/message: 0cc9dcd6ac624383e9977f66d4b3b777
|
Iqh0Uv/message: 0cc9dcd6ac624383e9977f66d4b3b777
|
||||||
Iqh0Uv/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
Iqh0Uv/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
||||||
Iqh0Uv/origin/1/0: eb63312c63f6c8d2a5b6520c89012123
|
Iqh0Uv/origin/1/0: eb63312c63f6c8d2a5b6520c89012123
|
||||||
@@ -1964,8 +1966,9 @@ checksums:
|
|||||||
fuwKpE/origin/1/0: fd0e66cef8d46e96bcf3143a12f9b6db
|
fuwKpE/origin/1/0: fd0e66cef8d46e96bcf3143a12f9b6db
|
||||||
fuwKpE/origin/2/0: fc0c18a095a68277a0f90a115942683b
|
fuwKpE/origin/2/0: fc0c18a095a68277a0f90a115942683b
|
||||||
fuwKpE/origin/3/0: 1b05beba6bdd908015fa8e798717f024
|
fuwKpE/origin/3/0: 1b05beba6bdd908015fa8e798717f024
|
||||||
fuwKpE/origin/4/0: 9c8b544f5db130cd7c5eda9064ba3957
|
fuwKpE/origin/4/0: 1b05beba6bdd908015fa8e798717f024
|
||||||
fuwKpE/origin/5/0: faa8a2576972b4eb31565d8ea6098f43
|
fuwKpE/origin/5/0: 9c8b544f5db130cd7c5eda9064ba3957
|
||||||
|
fuwKpE/origin/6/0: faa8a2576972b4eb31565d8ea6098f43
|
||||||
fuwKpE/translation: 346a18a54564881c888b95873d03df44
|
fuwKpE/translation: 346a18a54564881c888b95873d03df44
|
||||||
nYNMZZ/message: d5a8a1b38eaf02d02cd6ac2211b42a92
|
nYNMZZ/message: d5a8a1b38eaf02d02cd6ac2211b42a92
|
||||||
nYNMZZ/origin/0/0: 97bd206a05414e2a571020d64e166093
|
nYNMZZ/origin/0/0: 97bd206a05414e2a571020d64e166093
|
||||||
@@ -2191,9 +2194,9 @@ checksums:
|
|||||||
Eit43O/message: b5d9a1edd988f455041aee9fa7fb3d63
|
Eit43O/message: b5d9a1edd988f455041aee9fa7fb3d63
|
||||||
Eit43O/origin/0/0: 1d6b5f88cdad160a67842f6b65387448
|
Eit43O/origin/0/0: 1d6b5f88cdad160a67842f6b65387448
|
||||||
Eit43O/translation: b5d9a1edd988f455041aee9fa7fb3d63
|
Eit43O/translation: b5d9a1edd988f455041aee9fa7fb3d63
|
||||||
JjEJSy/translation: 0154ff378b3f4329871378229e269fc7
|
|
||||||
JjEJSy/message: 0154ff378b3f4329871378229e269fc7
|
JjEJSy/message: 0154ff378b3f4329871378229e269fc7
|
||||||
JjEJSy/origin/0/0: 69d8634b177fdf937764b406fd477547
|
JjEJSy/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||||
|
JjEJSy/translation: 0154ff378b3f4329871378229e269fc7
|
||||||
Tz0i8g/message: 8df6777277469c1fd88cc18dde2f1cc3
|
Tz0i8g/message: 8df6777277469c1fd88cc18dde2f1cc3
|
||||||
Tz0i8g/origin/0/0: 77c9f6139024b2069e4fdc5120f69cb5
|
Tz0i8g/origin/0/0: 77c9f6139024b2069e4fdc5120f69cb5
|
||||||
Tz0i8g/origin/1/0: 94d3e20c29bb517dd394711d7524e478
|
Tz0i8g/origin/1/0: 94d3e20c29bb517dd394711d7524e478
|
||||||
@@ -2228,10 +2231,10 @@ checksums:
|
|||||||
n1ekoW/message: ec7b8f314fe9bc6591006707484ede61
|
n1ekoW/message: ec7b8f314fe9bc6591006707484ede61
|
||||||
n1ekoW/origin/0/0: 6cda8b872e4902cbc0191d9375adf4ef
|
n1ekoW/origin/0/0: 6cda8b872e4902cbc0191d9375adf4ef
|
||||||
n1ekoW/translation: ec7b8f314fe9bc6591006707484ede61
|
n1ekoW/translation: ec7b8f314fe9bc6591006707484ede61
|
||||||
fcWrnU/translation: 6d842546a84e919c519bf8cbffdf25a0
|
|
||||||
fcWrnU/message: 6d842546a84e919c519bf8cbffdf25a0
|
fcWrnU/message: 6d842546a84e919c519bf8cbffdf25a0
|
||||||
fcWrnU/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
fcWrnU/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
||||||
fcWrnU/origin/1/0: 69d8634b177fdf937764b406fd477547
|
fcWrnU/origin/1/0: 69d8634b177fdf937764b406fd477547
|
||||||
|
fcWrnU/translation: 6d842546a84e919c519bf8cbffdf25a0
|
||||||
mErq7F/message: 0dd2ae69be4618c1f9e615774a4509ca
|
mErq7F/message: 0dd2ae69be4618c1f9e615774a4509ca
|
||||||
mErq7F/origin/0/0: 6cda8b872e4902cbc0191d9375adf4ef
|
mErq7F/origin/0/0: 6cda8b872e4902cbc0191d9375adf4ef
|
||||||
mErq7F/translation: 0dd2ae69be4618c1f9e615774a4509ca
|
mErq7F/translation: 0dd2ae69be4618c1f9e615774a4509ca
|
||||||
@@ -2267,9 +2270,9 @@ checksums:
|
|||||||
wIb7Ng/message: 67659097ef6bcb194839858b2c4f3a3c
|
wIb7Ng/message: 67659097ef6bcb194839858b2c4f3a3c
|
||||||
wIb7Ng/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
wIb7Ng/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
||||||
wIb7Ng/translation: 67659097ef6bcb194839858b2c4f3a3c
|
wIb7Ng/translation: 67659097ef6bcb194839858b2c4f3a3c
|
||||||
6sKjjx/translation: 50861c0881fe87a77412417e5ffb3fbb
|
|
||||||
6sKjjx/message: 50861c0881fe87a77412417e5ffb3fbb
|
6sKjjx/message: 50861c0881fe87a77412417e5ffb3fbb
|
||||||
6sKjjx/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
6sKjjx/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
||||||
|
6sKjjx/translation: 50861c0881fe87a77412417e5ffb3fbb
|
||||||
vnS6Rf/message: 003f7f93d44b0f5c2e40bf24e7af3bd7
|
vnS6Rf/message: 003f7f93d44b0f5c2e40bf24e7af3bd7
|
||||||
vnS6Rf/origin/0/0: c0b59968b2b234167c488a05cc38710b
|
vnS6Rf/origin/0/0: c0b59968b2b234167c488a05cc38710b
|
||||||
vnS6Rf/translation: 003f7f93d44b0f5c2e40bf24e7af3bd7
|
vnS6Rf/translation: 003f7f93d44b0f5c2e40bf24e7af3bd7
|
||||||
@@ -2309,9 +2312,9 @@ checksums:
|
|||||||
D%2BNlUC/message: 803281c327a02a2feba182f4a2b38311
|
D%2BNlUC/message: 803281c327a02a2feba182f4a2b38311
|
||||||
D%2BNlUC/origin/0/0: 358ea1b5babb2e9ccbf1a4c9f89dd3a5
|
D%2BNlUC/origin/0/0: 358ea1b5babb2e9ccbf1a4c9f89dd3a5
|
||||||
D%2BNlUC/translation: 803281c327a02a2feba182f4a2b38311
|
D%2BNlUC/translation: 803281c327a02a2feba182f4a2b38311
|
||||||
KM6m8p/translation: c621ea9404a37af289def443b309bf1b
|
|
||||||
KM6m8p/message: c621ea9404a37af289def443b309bf1b
|
KM6m8p/message: c621ea9404a37af289def443b309bf1b
|
||||||
KM6m8p/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
KM6m8p/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
||||||
|
KM6m8p/translation: c621ea9404a37af289def443b309bf1b
|
||||||
bff61F/message: 3f0ae9c914f16ceb9a0d7db868f4aa20
|
bff61F/message: 3f0ae9c914f16ceb9a0d7db868f4aa20
|
||||||
bff61F/origin/0/0: 8628ddb6f4ded25c0f17e2cbda8207d3
|
bff61F/origin/0/0: 8628ddb6f4ded25c0f17e2cbda8207d3
|
||||||
bff61F/origin/1/0: 7493592314bc3b96ad96127a949ba1ba
|
bff61F/origin/1/0: 7493592314bc3b96ad96127a949ba1ba
|
||||||
@@ -2347,6 +2350,14 @@ checksums:
|
|||||||
NnH3pK/message: 82a46f197779af4dde92e7e5f9336ccf
|
NnH3pK/message: 82a46f197779af4dde92e7e5f9336ccf
|
||||||
NnH3pK/origin/0/0: dc1a240d2d0cccb102c67ca311d4e457
|
NnH3pK/origin/0/0: dc1a240d2d0cccb102c67ca311d4e457
|
||||||
NnH3pK/translation: 82a46f197779af4dde92e7e5f9336ccf
|
NnH3pK/translation: 82a46f197779af4dde92e7e5f9336ccf
|
||||||
|
InJcdo/message: 1b2364c95fd318d99cc502745beac94a
|
||||||
|
InJcdo/origin/0/0: f64cd62b620f454998523af2a4330278
|
||||||
|
InJcdo/origin/1/0: dc1a240d2d0cccb102c67ca311d4e457
|
||||||
|
InJcdo/translation: 1b2364c95fd318d99cc502745beac94a
|
||||||
|
zDNL2V/message: 6e053a87e63372e903c16a06de25c9a3
|
||||||
|
zDNL2V/origin/0/0: f64cd62b620f454998523af2a4330278
|
||||||
|
zDNL2V/origin/1/0: dc1a240d2d0cccb102c67ca311d4e457
|
||||||
|
zDNL2V/translation: 6e053a87e63372e903c16a06de25c9a3
|
||||||
itgYbC/message: efe14181bc517316e3ab3258d72d2ff9
|
itgYbC/message: efe14181bc517316e3ab3258d72d2ff9
|
||||||
itgYbC/origin/0/0: f64cd62b620f454998523af2a4330278
|
itgYbC/origin/0/0: f64cd62b620f454998523af2a4330278
|
||||||
itgYbC/origin/1/0: dc1a240d2d0cccb102c67ca311d4e457
|
itgYbC/origin/1/0: dc1a240d2d0cccb102c67ca311d4e457
|
||||||
@@ -2417,12 +2428,12 @@ checksums:
|
|||||||
vlxQFL/message: 730f33b8f1fc002c4f393ccd262b6664
|
vlxQFL/message: 730f33b8f1fc002c4f393ccd262b6664
|
||||||
vlxQFL/origin/0/0: afee530613110e248ffde6b56872ebed
|
vlxQFL/origin/0/0: afee530613110e248ffde6b56872ebed
|
||||||
vlxQFL/translation: 730f33b8f1fc002c4f393ccd262b6664
|
vlxQFL/translation: 730f33b8f1fc002c4f393ccd262b6664
|
||||||
HQVm6e/translation: ec66b543a593e9dabf47de5979892d8d
|
|
||||||
HQVm6e/message: ec66b543a593e9dabf47de5979892d8d
|
HQVm6e/message: ec66b543a593e9dabf47de5979892d8d
|
||||||
HQVm6e/origin/0/0: 69d8634b177fdf937764b406fd477547
|
HQVm6e/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||||
gKmoow/translation: 83997c77cb0dc8c8c90f7ab706aa817f
|
HQVm6e/translation: ec66b543a593e9dabf47de5979892d8d
|
||||||
gKmoow/message: 83997c77cb0dc8c8c90f7ab706aa817f
|
gKmoow/message: 83997c77cb0dc8c8c90f7ab706aa817f
|
||||||
gKmoow/origin/0/0: 69d8634b177fdf937764b406fd477547
|
gKmoow/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||||
|
gKmoow/translation: 83997c77cb0dc8c8c90f7ab706aa817f
|
||||||
OAYToL/message: 1c989752736fa41ecdd68ea890f16a15
|
OAYToL/message: 1c989752736fa41ecdd68ea890f16a15
|
||||||
OAYToL/origin/0/0: 15792234f408027822ee71ccddde6f9b
|
OAYToL/origin/0/0: 15792234f408027822ee71ccddde6f9b
|
||||||
OAYToL/translation: 1c989752736fa41ecdd68ea890f16a15
|
OAYToL/translation: 1c989752736fa41ecdd68ea890f16a15
|
||||||
@@ -2441,6 +2452,9 @@ checksums:
|
|||||||
kp6L3T/message: b7eadb89c615874f416d9658d0428c4c
|
kp6L3T/message: b7eadb89c615874f416d9658d0428c4c
|
||||||
kp6L3T/origin/0/0: 30791e26a172d47d3cb0693e82679246
|
kp6L3T/origin/0/0: 30791e26a172d47d3cb0693e82679246
|
||||||
kp6L3T/translation: b7eadb89c615874f416d9658d0428c4c
|
kp6L3T/translation: b7eadb89c615874f416d9658d0428c4c
|
||||||
|
pEb95p/translation: 1d4c2a0ec7e4c707a4470fa4262b495d
|
||||||
|
pEb95p/message: 1d4c2a0ec7e4c707a4470fa4262b495d
|
||||||
|
pEb95p/origin/0/0: 1b05beba6bdd908015fa8e798717f024
|
||||||
MHrjPM/message: 344e64395eaff6822a57d18623853e1a
|
MHrjPM/message: 344e64395eaff6822a57d18623853e1a
|
||||||
MHrjPM/origin/0/0: a50c4fbde8da6e0a77c4e0aef0b44dea
|
MHrjPM/origin/0/0: a50c4fbde8da6e0a77c4e0aef0b44dea
|
||||||
MHrjPM/translation: 344e64395eaff6822a57d18623853e1a
|
MHrjPM/translation: 344e64395eaff6822a57d18623853e1a
|
||||||
@@ -2488,6 +2502,9 @@ checksums:
|
|||||||
LeM5RY/message: dfeac280858332082ad34d2623a59863
|
LeM5RY/message: dfeac280858332082ad34d2623a59863
|
||||||
LeM5RY/origin/0/0: 0dccc39d0debbf3e089cb6d292de762b
|
LeM5RY/origin/0/0: 0dccc39d0debbf3e089cb6d292de762b
|
||||||
LeM5RY/translation: dfeac280858332082ad34d2623a59863
|
LeM5RY/translation: dfeac280858332082ad34d2623a59863
|
||||||
|
5MPY04/translation: 661f2e21a85fbe1281b141a8041c8778
|
||||||
|
5MPY04/message: 661f2e21a85fbe1281b141a8041c8778
|
||||||
|
5MPY04/origin/0/0: 1b05beba6bdd908015fa8e798717f024
|
||||||
W1ewR0/message: ddcac5d14d48358b043a9487b42eec16
|
W1ewR0/message: ddcac5d14d48358b043a9487b42eec16
|
||||||
W1ewR0/origin/0/0: fc0c18a095a68277a0f90a115942683b
|
W1ewR0/origin/0/0: fc0c18a095a68277a0f90a115942683b
|
||||||
W1ewR0/origin/1/0: 1b05beba6bdd908015fa8e798717f024
|
W1ewR0/origin/1/0: 1b05beba6bdd908015fa8e798717f024
|
||||||
@@ -2507,6 +2524,9 @@ checksums:
|
|||||||
YcyP2z/origin/0/0: 3161dd30737e7d849d4ccc30a28bada9
|
YcyP2z/origin/0/0: 3161dd30737e7d849d4ccc30a28bada9
|
||||||
YcyP2z/origin/1/0: 3161dd30737e7d849d4ccc30a28bada9
|
YcyP2z/origin/1/0: 3161dd30737e7d849d4ccc30a28bada9
|
||||||
YcyP2z/translation: 7495f1434c9188849bffd015811d5337
|
YcyP2z/translation: 7495f1434c9188849bffd015811d5337
|
||||||
|
c1vOQR/message: 640ab5162eed616a7235d68376a016ed
|
||||||
|
c1vOQR/origin/0/0: f64cd62b620f454998523af2a4330278
|
||||||
|
c1vOQR/translation: 640ab5162eed616a7235d68376a016ed
|
||||||
MxQXhL/message: c1c949477e121d73ed6683eddf9cd874
|
MxQXhL/message: c1c949477e121d73ed6683eddf9cd874
|
||||||
MxQXhL/origin/0/0: ff4a160eca5f8da4f075fd1dd6061fa6
|
MxQXhL/origin/0/0: ff4a160eca5f8da4f075fd1dd6061fa6
|
||||||
MxQXhL/origin/1/0: 69d8634b177fdf937764b406fd477547
|
MxQXhL/origin/1/0: 69d8634b177fdf937764b406fd477547
|
||||||
@@ -2526,6 +2546,9 @@ checksums:
|
|||||||
2QGEbi/message: 550198b2c87f06726a843c79c1026ed9
|
2QGEbi/message: 550198b2c87f06726a843c79c1026ed9
|
||||||
2QGEbi/origin/0/0: 4b4f55c033add8db69178563832227c6
|
2QGEbi/origin/0/0: 4b4f55c033add8db69178563832227c6
|
||||||
2QGEbi/translation: 550198b2c87f06726a843c79c1026ed9
|
2QGEbi/translation: 550198b2c87f06726a843c79c1026ed9
|
||||||
|
23nvb2/message: b9dedce59b5594ee2a88bf5d55d6dfac
|
||||||
|
23nvb2/origin/0/0: a1fde3fcce608870830f168f027727ff
|
||||||
|
23nvb2/translation: b9dedce59b5594ee2a88bf5d55d6dfac
|
||||||
Oo4E6p/message: af985cf72c47df76e2f2139a427d9948
|
Oo4E6p/message: af985cf72c47df76e2f2139a427d9948
|
||||||
Oo4E6p/origin/0/0: 97bd206a05414e2a571020d64e166093
|
Oo4E6p/origin/0/0: 97bd206a05414e2a571020d64e166093
|
||||||
Oo4E6p/translation: af985cf72c47df76e2f2139a427d9948
|
Oo4E6p/translation: af985cf72c47df76e2f2139a427d9948
|
||||||
@@ -2544,9 +2567,13 @@ checksums:
|
|||||||
A8jqN7/message: 656c93265d7e2bef1245b17d85f85ae5
|
A8jqN7/message: 656c93265d7e2bef1245b17d85f85ae5
|
||||||
A8jqN7/origin/0/0: 1fd8ccf586705354bcae6e1186108fc7
|
A8jqN7/origin/0/0: 1fd8ccf586705354bcae6e1186108fc7
|
||||||
A8jqN7/translation: 656c93265d7e2bef1245b17d85f85ae5
|
A8jqN7/translation: 656c93265d7e2bef1245b17d85f85ae5
|
||||||
Q60WUZ/translation: 4284c33aed2f2c5e004ed0a7d7d9bf87
|
|
||||||
Q60WUZ/message: 4284c33aed2f2c5e004ed0a7d7d9bf87
|
Q60WUZ/message: 4284c33aed2f2c5e004ed0a7d7d9bf87
|
||||||
Q60WUZ/origin/0/0: 69d8634b177fdf937764b406fd477547
|
Q60WUZ/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||||
|
Q60WUZ/translation: 4284c33aed2f2c5e004ed0a7d7d9bf87
|
||||||
|
KNK33q/message: a606d59661b59b5e2e9ab58e8cbc143b
|
||||||
|
KNK33q/origin/0/0: f64cd62b620f454998523af2a4330278
|
||||||
|
KNK33q/origin/1/0: dc1a240d2d0cccb102c67ca311d4e457
|
||||||
|
KNK33q/translation: a606d59661b59b5e2e9ab58e8cbc143b
|
||||||
T7Mywq/message: 62899897d7bffcb1df3af77d460f0881
|
T7Mywq/message: 62899897d7bffcb1df3af77d460f0881
|
||||||
T7Mywq/origin/0/0: 0ad2af981579a62b084b3d41462c5546
|
T7Mywq/origin/0/0: 0ad2af981579a62b084b3d41462c5546
|
||||||
T7Mywq/translation: 62899897d7bffcb1df3af77d460f0881
|
T7Mywq/translation: 62899897d7bffcb1df3af77d460f0881
|
||||||
@@ -2589,6 +2616,9 @@ checksums:
|
|||||||
TFv5tM/message: 4f2240aeff6f7275feb33ca3f608e48c
|
TFv5tM/message: 4f2240aeff6f7275feb33ca3f608e48c
|
||||||
TFv5tM/origin/0/0: 7493592314bc3b96ad96127a949ba1ba
|
TFv5tM/origin/0/0: 7493592314bc3b96ad96127a949ba1ba
|
||||||
TFv5tM/translation: 4f2240aeff6f7275feb33ca3f608e48c
|
TFv5tM/translation: 4f2240aeff6f7275feb33ca3f608e48c
|
||||||
|
V1o9Jo/message: 7b212f1c2fdc370974f859b08349b0c6
|
||||||
|
V1o9Jo/origin/0/0: f64cd62b620f454998523af2a4330278
|
||||||
|
V1o9Jo/translation: 7b212f1c2fdc370974f859b08349b0c6
|
||||||
It4%2FFS/message: ae1527c2765592f13980acb66997e570
|
It4%2FFS/message: ae1527c2765592f13980acb66997e570
|
||||||
It4%2FFS/origin/0/0: 0ad2af981579a62b084b3d41462c5546
|
It4%2FFS/origin/0/0: 0ad2af981579a62b084b3d41462c5546
|
||||||
It4%2FFS/translation: ae1527c2765592f13980acb66997e570
|
It4%2FFS/translation: ae1527c2765592f13980acb66997e570
|
||||||
@@ -2633,15 +2663,14 @@ checksums:
|
|||||||
7Ufoyg/origin/0/0: 9737a9cef0646ff4bbd942b1924b9a2c
|
7Ufoyg/origin/0/0: 9737a9cef0646ff4bbd942b1924b9a2c
|
||||||
7Ufoyg/translation: 5c418701b27b9acbc5dd199719007362
|
7Ufoyg/translation: 5c418701b27b9acbc5dd199719007362
|
||||||
mx8%2B1u/message: f2949ca2dc18b0063af92e60779abb65
|
mx8%2B1u/message: f2949ca2dc18b0063af92e60779abb65
|
||||||
mx8%2B1u/origin/0/0: ff4a160eca5f8da4f075fd1dd6061fa6
|
mx8%2B1u/origin/0/0: c0b59968b2b234167c488a05cc38710b
|
||||||
mx8%2B1u/origin/1/0: c0b59968b2b234167c488a05cc38710b
|
mx8%2B1u/origin/1/0: c0b59968b2b234167c488a05cc38710b
|
||||||
mx8%2B1u/origin/2/0: c0b59968b2b234167c488a05cc38710b
|
mx8%2B1u/origin/2/0: fadd5a5b6963ec5f261072f7e8d2140b
|
||||||
mx8%2B1u/origin/3/0: fadd5a5b6963ec5f261072f7e8d2140b
|
mx8%2B1u/origin/3/0: dbad0c8d7863cb41f5495264f82e7081
|
||||||
mx8%2B1u/origin/4/0: dbad0c8d7863cb41f5495264f82e7081
|
|
||||||
mx8%2B1u/translation: f2949ca2dc18b0063af92e60779abb65
|
mx8%2B1u/translation: f2949ca2dc18b0063af92e60779abb65
|
||||||
8AwlaR/translation: acb1fdc14ed875815ae9f8a5254bc634
|
|
||||||
8AwlaR/message: acb1fdc14ed875815ae9f8a5254bc634
|
8AwlaR/message: acb1fdc14ed875815ae9f8a5254bc634
|
||||||
8AwlaR/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
8AwlaR/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
||||||
|
8AwlaR/translation: acb1fdc14ed875815ae9f8a5254bc634
|
||||||
Ws%2B3X%2B/message: 905b10eeb09fdc5ac7fc757e935ee757
|
Ws%2B3X%2B/message: 905b10eeb09fdc5ac7fc757e935ee757
|
||||||
Ws%2B3X%2B/origin/0/0: fadd5a5b6963ec5f261072f7e8d2140b
|
Ws%2B3X%2B/origin/0/0: fadd5a5b6963ec5f261072f7e8d2140b
|
||||||
Ws%2B3X%2B/translation: 905b10eeb09fdc5ac7fc757e935ee757
|
Ws%2B3X%2B/translation: 905b10eeb09fdc5ac7fc757e935ee757
|
||||||
@@ -2681,8 +2710,7 @@ checksums:
|
|||||||
IelE1h/origin/2/0: 5f6c4a0c9be5098099167871ae1564af
|
IelE1h/origin/2/0: 5f6c4a0c9be5098099167871ae1564af
|
||||||
IelE1h/translation: 972773025e763ddf53273df6d37a729a
|
IelE1h/translation: 972773025e763ddf53273df6d37a729a
|
||||||
cWtfn1/message: 9a91ea70f5eb55aac96640bf0fd7feff
|
cWtfn1/message: 9a91ea70f5eb55aac96640bf0fd7feff
|
||||||
cWtfn1/origin/0/0: ff4a160eca5f8da4f075fd1dd6061fa6
|
cWtfn1/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||||
cWtfn1/origin/1/0: 69d8634b177fdf937764b406fd477547
|
|
||||||
cWtfn1/translation: 9a91ea70f5eb55aac96640bf0fd7feff
|
cWtfn1/translation: 9a91ea70f5eb55aac96640bf0fd7feff
|
||||||
b3Thhd/message: 5b7b755f05330164dbfb00096c500ee3
|
b3Thhd/message: 5b7b755f05330164dbfb00096c500ee3
|
||||||
b3Thhd/origin/0/0: 12c1fab543c8848bcb10107d47336736
|
b3Thhd/origin/0/0: 12c1fab543c8848bcb10107d47336736
|
||||||
@@ -2755,9 +2783,15 @@ checksums:
|
|||||||
9y1RcT/message: 2dfa121e0ff1914e08068308a81973c0
|
9y1RcT/message: 2dfa121e0ff1914e08068308a81973c0
|
||||||
9y1RcT/origin/0/0: d77eb1f90f2fa86a55181629d88d237a
|
9y1RcT/origin/0/0: d77eb1f90f2fa86a55181629d88d237a
|
||||||
9y1RcT/translation: 2dfa121e0ff1914e08068308a81973c0
|
9y1RcT/translation: 2dfa121e0ff1914e08068308a81973c0
|
||||||
|
an6ayw/message: 2d39f6440ad379e417193dbfaa265b5f
|
||||||
|
an6ayw/origin/0/0: f64cd62b620f454998523af2a4330278
|
||||||
|
an6ayw/translation: 2d39f6440ad379e417193dbfaa265b5f
|
||||||
GdWB%2BV/message: 6a15a9b926ecd2c91454e9da19c6049e
|
GdWB%2BV/message: 6a15a9b926ecd2c91454e9da19c6049e
|
||||||
GdWB%2BV/origin/0/0: f64cd62b620f454998523af2a4330278
|
GdWB%2BV/origin/0/0: f64cd62b620f454998523af2a4330278
|
||||||
GdWB%2BV/translation: 6a15a9b926ecd2c91454e9da19c6049e
|
GdWB%2BV/translation: 6a15a9b926ecd2c91454e9da19c6049e
|
||||||
|
LnaC5y/message: 229d2a9159e17f243cf317b9bd7d471c
|
||||||
|
LnaC5y/origin/0/0: a1fde3fcce608870830f168f027727ff
|
||||||
|
LnaC5y/translation: 229d2a9159e17f243cf317b9bd7d471c
|
||||||
2X4ecw/message: fcefd247ec76a372002d2cffac3c5b0f
|
2X4ecw/message: fcefd247ec76a372002d2cffac3c5b0f
|
||||||
2X4ecw/origin/0/0: a1fde3fcce608870830f168f027727ff
|
2X4ecw/origin/0/0: a1fde3fcce608870830f168f027727ff
|
||||||
2X4ecw/translation: fcefd247ec76a372002d2cffac3c5b0f
|
2X4ecw/translation: fcefd247ec76a372002d2cffac3c5b0f
|
||||||
@@ -2771,6 +2805,9 @@ checksums:
|
|||||||
8iP9oQ/origin/0/0: f64cd62b620f454998523af2a4330278
|
8iP9oQ/origin/0/0: f64cd62b620f454998523af2a4330278
|
||||||
8iP9oQ/origin/1/0: dc1a240d2d0cccb102c67ca311d4e457
|
8iP9oQ/origin/1/0: dc1a240d2d0cccb102c67ca311d4e457
|
||||||
8iP9oQ/translation: 5c07446c838d07572c6b40621507e951
|
8iP9oQ/translation: 5c07446c838d07572c6b40621507e951
|
||||||
|
kxZ7LS/message: 41ebcdff41ce85329915754faf7140c0
|
||||||
|
kxZ7LS/origin/0/0: f64cd62b620f454998523af2a4330278
|
||||||
|
kxZ7LS/translation: 41ebcdff41ce85329915754faf7140c0
|
||||||
3d54Wj/message: d67dc42810694fd1344a9a9545aca1e7
|
3d54Wj/message: d67dc42810694fd1344a9a9545aca1e7
|
||||||
3d54Wj/origin/0/0: f64cd62b620f454998523af2a4330278
|
3d54Wj/origin/0/0: f64cd62b620f454998523af2a4330278
|
||||||
3d54Wj/translation: d67dc42810694fd1344a9a9545aca1e7
|
3d54Wj/translation: d67dc42810694fd1344a9a9545aca1e7
|
||||||
@@ -2877,9 +2914,9 @@ checksums:
|
|||||||
RtlIYB/message: df4783d593505766d78c5bc08d3d48ac
|
RtlIYB/message: df4783d593505766d78c5bc08d3d48ac
|
||||||
RtlIYB/origin/0/0: 627b72cd19a7154ae1c28cbd7ca0c730
|
RtlIYB/origin/0/0: 627b72cd19a7154ae1c28cbd7ca0c730
|
||||||
RtlIYB/translation: df4783d593505766d78c5bc08d3d48ac
|
RtlIYB/translation: df4783d593505766d78c5bc08d3d48ac
|
||||||
3WXdoZ/translation: 4cbeab40f77975492a23eec428732440
|
|
||||||
3WXdoZ/message: 4cbeab40f77975492a23eec428732440
|
3WXdoZ/message: 4cbeab40f77975492a23eec428732440
|
||||||
3WXdoZ/origin/0/0: 69d8634b177fdf937764b406fd477547
|
3WXdoZ/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||||
|
3WXdoZ/translation: 4cbeab40f77975492a23eec428732440
|
||||||
aelko%2B/message: 41dd145ef56f539e12dc064a9807c660
|
aelko%2B/message: 41dd145ef56f539e12dc064a9807c660
|
||||||
aelko%2B/origin/0/0: 4980d8e27a628e7cd9a70b839d844083
|
aelko%2B/origin/0/0: 4980d8e27a628e7cd9a70b839d844083
|
||||||
aelko%2B/translation: 41dd145ef56f539e12dc064a9807c660
|
aelko%2B/translation: 41dd145ef56f539e12dc064a9807c660
|
||||||
@@ -2923,9 +2960,9 @@ checksums:
|
|||||||
GoDLB9/message: 8c8d944e07388c5877effdb2c2803dcf
|
GoDLB9/message: 8c8d944e07388c5877effdb2c2803dcf
|
||||||
GoDLB9/origin/0/0: 5c60161e173f5eae9276d8e4c70b4ebb
|
GoDLB9/origin/0/0: 5c60161e173f5eae9276d8e4c70b4ebb
|
||||||
GoDLB9/translation: 8c8d944e07388c5877effdb2c2803dcf
|
GoDLB9/translation: 8c8d944e07388c5877effdb2c2803dcf
|
||||||
c4nMcR/translation: bf4f697079eef51c3ebe46ce79063895
|
|
||||||
c4nMcR/message: bf4f697079eef51c3ebe46ce79063895
|
c4nMcR/message: bf4f697079eef51c3ebe46ce79063895
|
||||||
c4nMcR/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
c4nMcR/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
||||||
|
c4nMcR/translation: bf4f697079eef51c3ebe46ce79063895
|
||||||
evg7%2BA/message: 403972e7a25afc2415762c1c2b1ec868
|
evg7%2BA/message: 403972e7a25afc2415762c1c2b1ec868
|
||||||
evg7%2BA/origin/0/0: 3c57a918258af4b44c187f58d203345f
|
evg7%2BA/origin/0/0: 3c57a918258af4b44c187f58d203345f
|
||||||
evg7%2BA/translation: 403972e7a25afc2415762c1c2b1ec868
|
evg7%2BA/translation: 403972e7a25afc2415762c1c2b1ec868
|
||||||
@@ -2962,12 +2999,12 @@ checksums:
|
|||||||
25fAUC/message: 3e4cce96bc1197a1caa2b7fb4d3c7e53
|
25fAUC/message: 3e4cce96bc1197a1caa2b7fb4d3c7e53
|
||||||
25fAUC/origin/0/0: d1e1d7ccce60e05570bedfbb93fc9416
|
25fAUC/origin/0/0: d1e1d7ccce60e05570bedfbb93fc9416
|
||||||
25fAUC/translation: 3e4cce96bc1197a1caa2b7fb4d3c7e53
|
25fAUC/translation: 3e4cce96bc1197a1caa2b7fb4d3c7e53
|
||||||
GRAGsB/translation: 751a534fd6e62a92ff64c46894c7db86
|
|
||||||
GRAGsB/message: 751a534fd6e62a92ff64c46894c7db86
|
GRAGsB/message: 751a534fd6e62a92ff64c46894c7db86
|
||||||
GRAGsB/origin/0/0: 69d8634b177fdf937764b406fd477547
|
GRAGsB/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||||
j0o6ml/translation: 1e036ffca9d9cb9e2941b83fbbf375c0
|
GRAGsB/translation: 751a534fd6e62a92ff64c46894c7db86
|
||||||
j0o6ml/message: 1e036ffca9d9cb9e2941b83fbbf375c0
|
j0o6ml/message: 1e036ffca9d9cb9e2941b83fbbf375c0
|
||||||
j0o6ml/origin/0/0: 69d8634b177fdf937764b406fd477547
|
j0o6ml/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||||
|
j0o6ml/translation: 1e036ffca9d9cb9e2941b83fbbf375c0
|
||||||
GnSSGm/message: 98de6f65c567c6654cc0adce3c9d5656
|
GnSSGm/message: 98de6f65c567c6654cc0adce3c9d5656
|
||||||
GnSSGm/origin/0/0: 33bf266228f835e12c0bfb0e5efb3be9
|
GnSSGm/origin/0/0: 33bf266228f835e12c0bfb0e5efb3be9
|
||||||
GnSSGm/translation: 98de6f65c567c6654cc0adce3c9d5656
|
GnSSGm/translation: 98de6f65c567c6654cc0adce3c9d5656
|
||||||
@@ -2980,10 +3017,10 @@ checksums:
|
|||||||
Hg1qE9/message: c808949b9b2b4a9aba2472f5d1050167
|
Hg1qE9/message: c808949b9b2b4a9aba2472f5d1050167
|
||||||
Hg1qE9/origin/0/0: 30791e26a172d47d3cb0693e82679246
|
Hg1qE9/origin/0/0: 30791e26a172d47d3cb0693e82679246
|
||||||
Hg1qE9/translation: c808949b9b2b4a9aba2472f5d1050167
|
Hg1qE9/translation: c808949b9b2b4a9aba2472f5d1050167
|
||||||
OTxjpR/translation: 370a32c49489db61978ac91b55699c24
|
|
||||||
OTxjpR/message: 370a32c49489db61978ac91b55699c24
|
OTxjpR/message: 370a32c49489db61978ac91b55699c24
|
||||||
OTxjpR/origin/0/0: 69d8634b177fdf937764b406fd477547
|
OTxjpR/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||||
OTxjpR/origin/1/0: 69d8634b177fdf937764b406fd477547
|
OTxjpR/origin/1/0: 69d8634b177fdf937764b406fd477547
|
||||||
|
OTxjpR/translation: 370a32c49489db61978ac91b55699c24
|
||||||
4DOCMI/message: 0b48896061a1124501fdaba026804148
|
4DOCMI/message: 0b48896061a1124501fdaba026804148
|
||||||
4DOCMI/origin/0/0: a50c4fbde8da6e0a77c4e0aef0b44dea
|
4DOCMI/origin/0/0: a50c4fbde8da6e0a77c4e0aef0b44dea
|
||||||
4DOCMI/translation: 0b48896061a1124501fdaba026804148
|
4DOCMI/translation: 0b48896061a1124501fdaba026804148
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ type SearchResult =
|
|||||||
boardPublicId: string;
|
boardPublicId: string;
|
||||||
boardName: string;
|
boardName: string;
|
||||||
listName: string;
|
listName: string;
|
||||||
|
cardNumber: number | null;
|
||||||
updatedAt: Date | null;
|
updatedAt: Date | null;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
type: "card";
|
type: "card";
|
||||||
@@ -159,8 +160,17 @@ export default function CommandPallette({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1 text-left">
|
<div className="min-w-0 flex-1 text-left">
|
||||||
<div className="truncate text-sm font-bold text-light-900 dark:text-dark-900">
|
<div className="flex items-center gap-2">
|
||||||
{result.title}
|
<div className="truncate text-sm font-bold text-light-900 dark:text-dark-900">
|
||||||
|
{result.title}
|
||||||
|
</div>
|
||||||
|
{result.type === "card" &&
|
||||||
|
result.cardNumber != null &&
|
||||||
|
workspace.cardPrefix && (
|
||||||
|
<span className="flex-shrink-0 text-xs text-light-600 dark:text-dark-600">
|
||||||
|
{workspace.cardPrefix}-{result.cardNumber}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{result.type === "card" && (
|
{result.type === "card" && (
|
||||||
<div className="truncate text-xs text-light-700 dark:text-dark-700">
|
<div className="truncate text-xs text-light-700 dark:text-dark-700">
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export interface RenderSuggestionsProps {
|
|||||||
export interface WorkspaceMember {
|
export interface WorkspaceMember {
|
||||||
publicId: string;
|
publicId: string;
|
||||||
user: {
|
user: {
|
||||||
id: string;
|
id: string | null;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
image: string | null;
|
image: string | null;
|
||||||
} | null;
|
} | null;
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ export function LabelForm({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-12 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
<div className="mt-12 flex items-center justify-end space-x-4 border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
||||||
{!isEdit && (
|
{!isEdit && (
|
||||||
<Toggle
|
<Toggle
|
||||||
label={t`Create another`}
|
label={t`Create another`}
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { env } from "next-runtime-env";
|
import { env } from "next-runtime-env";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import {
|
import { HiCheck, HiXMark } from "react-icons/hi2";
|
||||||
HiBolt,
|
|
||||||
HiCheck,
|
|
||||||
HiCheckBadge,
|
|
||||||
HiInformationCircle,
|
|
||||||
HiXMark,
|
|
||||||
} from "react-icons/hi2";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
import Input from "~/components/Input";
|
import Input from "~/components/Input";
|
||||||
import Toggle from "~/components/Toggle";
|
|
||||||
import { useDebounce } from "~/hooks/useDebounce";
|
import { useDebounce } from "~/hooks/useDebounce";
|
||||||
import { useModal } from "~/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
import { usePopup } from "~/providers/popup";
|
import { usePopup } from "~/providers/popup";
|
||||||
@@ -54,7 +47,6 @@ export function NewWorkspaceForm() {
|
|||||||
watch,
|
watch,
|
||||||
trigger,
|
trigger,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
setValue,
|
|
||||||
} = useForm<FormValues>({
|
} = useForm<FormValues>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -67,16 +59,10 @@ export function NewWorkspaceForm() {
|
|||||||
|
|
||||||
const hasAvailableWorkspaces = availableWorkspaces.length > 0;
|
const hasAvailableWorkspaces = availableWorkspaces.length > 0;
|
||||||
|
|
||||||
const isCloudEnv = env("NEXT_PUBLIC_KAN_ENV") === "cloud";
|
|
||||||
|
|
||||||
const slug = watch("slug");
|
const slug = watch("slug");
|
||||||
const [debouncedSlug] = useDebounce(slug, 500);
|
const [debouncedSlug] = useDebounce(slug, 500);
|
||||||
const isTyping = slug !== debouncedSlug;
|
const isTyping = slug !== debouncedSlug;
|
||||||
|
|
||||||
// Pro toggle state management
|
|
||||||
const [isProToggleEnabled, setIsProToggleEnabled] = useState(false);
|
|
||||||
const [lastAvailableSlug, setLastAvailableSlug] = useState<string>("");
|
|
||||||
|
|
||||||
// Validate slug only after debounce
|
// Validate slug only after debounce
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isTyping) {
|
if (isTyping) {
|
||||||
@@ -101,7 +87,7 @@ export function NewWorkspaceForm() {
|
|||||||
const isWorkspaceSlugAvailable = checkWorkspaceSlugAvailability.data;
|
const isWorkspaceSlugAvailable = checkWorkspaceSlugAvailability.data;
|
||||||
|
|
||||||
const createWorkspace = api.workspace.create.useMutation({
|
const createWorkspace = api.workspace.create.useMutation({
|
||||||
onSuccess: async (values, variables) => {
|
onSuccess: (values) => {
|
||||||
if (values.publicId && values.name) {
|
if (values.publicId && values.name) {
|
||||||
void utils.workspace.all.invalidate();
|
void utils.workspace.all.invalidate();
|
||||||
switchWorkspace({
|
switchWorkspace({
|
||||||
@@ -114,42 +100,6 @@ export function NewWorkspaceForm() {
|
|||||||
weekStartDay: 1,
|
weekStartDay: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
// If in cloud and Pro toggle is enabled, create checkout session for pro
|
|
||||||
if (env("NEXT_PUBLIC_KAN_ENV") === "cloud" && isProToggleEnabled) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
"/api/stripe/create_checkout_session",
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
slug: slug || undefined,
|
|
||||||
workspacePublicId: values.publicId,
|
|
||||||
cancelUrl: window.location.pathname + window.location.search,
|
|
||||||
successUrl: "/boards",
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
const url = (data as { url: string }).url;
|
|
||||||
|
|
||||||
if (url) {
|
|
||||||
window.location.href = url;
|
|
||||||
return; // Don't close modal if redirecting to checkout
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating checkout session:", error);
|
|
||||||
showPopup({
|
|
||||||
header: t`Error upgrading to Pro`,
|
|
||||||
message: t`Workspace created successfully. You can upgrade later in settings.`,
|
|
||||||
icon: "warning",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
closeModal();
|
closeModal();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -168,57 +118,8 @@ export function NewWorkspaceForm() {
|
|||||||
if (nameElement) nameElement.focus();
|
if (nameElement) nameElement.focus();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [shouldShowBenefits, setShouldShowBenefits] = useState(false);
|
|
||||||
|
|
||||||
const isValidSlug = slug && slug.length >= 3 && !errors.slug;
|
const isValidSlug = slug && slug.length >= 3 && !errors.slug;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
isCloudEnv &&
|
|
||||||
!checkWorkspaceSlugAvailability.isPending &&
|
|
||||||
isValidSlug
|
|
||||||
) {
|
|
||||||
const isAvailable = isWorkspaceSlugAvailable?.isAvailable === true;
|
|
||||||
setShouldShowBenefits(isAvailable);
|
|
||||||
|
|
||||||
// Automatically enable Pro toggle when slug is available
|
|
||||||
if (isAvailable) {
|
|
||||||
setIsProToggleEnabled(true);
|
|
||||||
setLastAvailableSlug(slug);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
isValidSlug,
|
|
||||||
isWorkspaceSlugAvailable?.isAvailable,
|
|
||||||
checkWorkspaceSlugAvailability.isPending,
|
|
||||||
slug,
|
|
||||||
isCloudEnv,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Reset benefits when slug becomes invalid
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isValidSlug) {
|
|
||||||
setShouldShowBenefits(false);
|
|
||||||
}
|
|
||||||
}, [isValidSlug]);
|
|
||||||
|
|
||||||
// Handle Pro toggle changes
|
|
||||||
const handleProToggleChange = () => {
|
|
||||||
if (isProToggleEnabled) {
|
|
||||||
// Turning off: clear the slug and disable Pro
|
|
||||||
setValue("slug", "");
|
|
||||||
setIsProToggleEnabled(false);
|
|
||||||
} else {
|
|
||||||
// Turning on: restore the last available slug if we have one
|
|
||||||
if (lastAvailableSlug) {
|
|
||||||
setValue("slug", lastAvailableSlug);
|
|
||||||
}
|
|
||||||
setIsProToggleEnabled(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const showProBenefits = isProToggleEnabled;
|
|
||||||
|
|
||||||
const onSubmit = (values: FormValues) => {
|
const onSubmit = (values: FormValues) => {
|
||||||
// Don't submit if slug is provided but not available
|
// Don't submit if slug is provided but not available
|
||||||
if (values.slug && isWorkspaceSlugAvailable?.isAvailable === false) {
|
if (values.slug && isWorkspaceSlugAvailable?.isAvailable === false) {
|
||||||
@@ -227,7 +128,7 @@ export function NewWorkspaceForm() {
|
|||||||
|
|
||||||
createWorkspace.mutate({
|
createWorkspace.mutate({
|
||||||
name: values.name,
|
name: values.name,
|
||||||
slug: !isCloudEnv && values.slug ? values.slug : undefined,
|
slug: values.slug,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -290,11 +191,7 @@ export function NewWorkspaceForm() {
|
|||||||
? t`This workspace URL is reserved`
|
? t`This workspace URL is reserved`
|
||||||
: undefined)
|
: undefined)
|
||||||
}
|
}
|
||||||
prefix={
|
prefix={`${env("NEXT_PUBLIC_BASE_URL")}/`}
|
||||||
env("NEXT_PUBLIC_KAN_ENV") === "cloud"
|
|
||||||
? "kan.bn/"
|
|
||||||
: `${env("NEXT_PUBLIC_BASE_URL")}/`
|
|
||||||
}
|
|
||||||
iconRight={
|
iconRight={
|
||||||
slug && slug.length >= 3 && !errors.slug ? (
|
slug && slug.length >= 3 && !errors.slug ? (
|
||||||
isWorkspaceSlugAvailable?.isAvailable ? (
|
isWorkspaceSlugAvailable?.isAvailable ? (
|
||||||
@@ -311,68 +208,9 @@ export function NewWorkspaceForm() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{slug && slug.length >= 3 && shouldShowBenefits && (
|
|
||||||
<div className="mt-2 flex items-center gap-1">
|
|
||||||
<HiInformationCircle className="h-4 w-4 text-dark-900" />
|
|
||||||
<p className="text-xs text-gray-500 dark:text-dark-900">
|
|
||||||
{t`Custom URLs require upgrading to a Pro plan`}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showProBenefits && (
|
|
||||||
<div className="mt-6">
|
|
||||||
<div className="rounded-md bg-light-100 p-3 text-xs text-light-900 dark:bg-dark-200 dark:text-dark-900">
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<HiCheckBadge className="h-[18px] w-[18px] flex-shrink-0 text-light-1000 dark:text-dark-950" />
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<span className="text-xs text-neutral-900 dark:text-dark-1000">
|
|
||||||
{t`Unlimited members`}
|
|
||||||
</span>
|
|
||||||
<span className="inline-flex items-center rounded-full bg-emerald-500/10 px-2 py-0.5 text-[10px] font-medium text-emerald-600 ring-1 ring-inset ring-emerald-500/20 dark:text-emerald-400 sm:text-[10px]">
|
|
||||||
{t`Launch offer`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<HiCheckBadge className="h-[18px] w-[18px] flex-shrink-0 text-light-1000 dark:text-dark-950" />
|
|
||||||
<span className="text-xs text-neutral-900 dark:text-dark-1000">
|
|
||||||
{t`Custom workspace URL`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<HiCheckBadge className="h-[18px] w-[18px] flex-shrink-0 text-light-1000 dark:text-dark-950" />
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<span className="text-xs text-neutral-900 dark:text-dark-1000">
|
|
||||||
{t`Board analytics`}
|
|
||||||
</span>
|
|
||||||
<span className="inline-flex items-center rounded-full bg-gray-500/10 px-2 py-0.5 text-[10px] font-medium text-gray-600 ring-1 ring-inset ring-gray-500/20 dark:text-gray-400 sm:text-[10px]">
|
|
||||||
{t`Coming soon`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="mt-6 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
||||||
className={twMerge(
|
|
||||||
"mt-6 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600",
|
|
||||||
!showProBenefits && "mt-12",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{/* Pro Toggle - only show in cloud environment */}
|
|
||||||
{isCloudEnv && (
|
|
||||||
<Toggle
|
|
||||||
isChecked={isProToggleEnabled}
|
|
||||||
onChange={handleProToggleChange}
|
|
||||||
label={t`Upgrade to Pro ($29/month)`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Locale as DateFnsLocale } from "date-fns";
|
import type { Locale as DateFnsLocale } from "date-fns";
|
||||||
import { useLingui } from "@lingui/react";
|
import { useLingui } from "@lingui/react";
|
||||||
import { de, enGB, es, fr, it, nl, pl, ru, ptBR} from "date-fns/locale";
|
import { de, enGB, es, fr, it, nl, pl, ptBR, ru } from "date-fns/locale";
|
||||||
|
|
||||||
import type { Locale } from "~/locales";
|
import type { Locale } from "~/locales";
|
||||||
import { useLinguiContext } from "~/providers/lingui";
|
import { useLinguiContext } from "~/providers/lingui";
|
||||||
@@ -19,7 +19,7 @@ export function useLocalisation() {
|
|||||||
nl,
|
nl,
|
||||||
ru,
|
ru,
|
||||||
pl,
|
pl,
|
||||||
"pt-BR": ptBR,
|
ptbr: ptBR,
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentDateLocale = dateLocaleMap[locale] ?? enGB;
|
const currentDateLocale = dateLocaleMap[locale] ?? enGB;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -20,13 +20,13 @@ const workspaceSlugSchema = z
|
|||||||
interface CheckoutSessionRequest {
|
interface CheckoutSessionRequest {
|
||||||
successUrl: string;
|
successUrl: string;
|
||||||
cancelUrl: string;
|
cancelUrl: string;
|
||||||
|
plan: "team" | "pro";
|
||||||
billing?: string;
|
billing?: string;
|
||||||
workspacePublicId?: string;
|
workspacePublicId?: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
workspaceName?: string;
|
workspaceName?: string;
|
||||||
workspaceDescription?: string;
|
workspaceDescription?: string;
|
||||||
workspaceSlug?: string;
|
workspaceSlug?: string;
|
||||||
plan?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRateLimit(
|
export default withRateLimit(
|
||||||
@@ -38,117 +38,114 @@ export default withRateLimit(
|
|||||||
return res.status(405).json({ error: "Method not allowed" });
|
return res.status(405).json({ error: "Method not allowed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const { user, db } = await createNextApiContext(req);
|
||||||
const { user, db } = await createNextApiContext(req);
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(404).json({ error: "User not found" });
|
return res.status(404).json({ error: "User not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = req.body as CheckoutSessionRequest;
|
||||||
|
const {
|
||||||
|
successUrl,
|
||||||
|
cancelUrl,
|
||||||
|
plan,
|
||||||
|
billing,
|
||||||
|
workspacePublicId,
|
||||||
|
slug,
|
||||||
|
workspaceName,
|
||||||
|
workspaceDescription,
|
||||||
|
workspaceSlug,
|
||||||
|
} = body;
|
||||||
|
|
||||||
|
if (!successUrl || !cancelUrl || !plan) {
|
||||||
|
return res.status(400).json({ error: "Missing required fields" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!workspacePublicId && !workspaceName) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: "Must provide workspacePublicId or workspaceName" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedSlug = slug ?? workspaceSlug;
|
||||||
|
|
||||||
|
if (resolvedSlug) {
|
||||||
|
const slugResult = workspaceSlugSchema.safeParse(resolvedSlug);
|
||||||
|
if (!slugResult.success) {
|
||||||
|
return res.status(400).json({ error: "Invalid workspace slug" });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const body = req.body as CheckoutSessionRequest;
|
let resolvedWorkspacePublicId = workspacePublicId;
|
||||||
const {
|
let subscriptionId: number | undefined;
|
||||||
successUrl,
|
|
||||||
cancelUrl,
|
if (workspacePublicId) {
|
||||||
billing,
|
// Existing workspace upgrade
|
||||||
|
const workspace = await workspaceRepo.getByPublicId(
|
||||||
|
db,
|
||||||
workspacePublicId,
|
workspacePublicId,
|
||||||
slug,
|
);
|
||||||
workspaceName,
|
|
||||||
workspaceDescription,
|
|
||||||
workspaceSlug,
|
|
||||||
} = body;
|
|
||||||
|
|
||||||
if (!successUrl || !cancelUrl) {
|
if (!workspace) {
|
||||||
return res.status(400).json({ error: "Missing required fields" });
|
return res.status(404).json({ error: "Workspace not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!workspacePublicId && !workspaceName) {
|
try {
|
||||||
return res
|
await assertPermission(db, user.id, workspace.id, "workspace:manage");
|
||||||
.status(400)
|
} catch {
|
||||||
.json({ error: "Must provide workspacePublicId or workspaceName" });
|
return res.status(403).json({ error: "Unauthorized" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedSlug = slug ?? workspaceSlug;
|
const subscription = await subscriptionRepo.create(db, {
|
||||||
|
plan: plan,
|
||||||
if (resolvedSlug) {
|
referenceId: workspacePublicId,
|
||||||
const slugResult = workspaceSlugSchema.safeParse(resolvedSlug);
|
userId: user.id,
|
||||||
if (!slugResult.success) {
|
stripeCustomerId: user.stripeCustomerId ?? "",
|
||||||
return res.status(400).json({ error: "Invalid workspace slug" });
|
status: "incomplete",
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let resolvedWorkspacePublicId = workspacePublicId;
|
|
||||||
let subscriptionId: number | undefined;
|
|
||||||
|
|
||||||
if (workspacePublicId) {
|
|
||||||
// Existing workspace upgrade
|
|
||||||
const workspace = await workspaceRepo.getByPublicId(
|
|
||||||
db,
|
|
||||||
workspacePublicId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!workspace) {
|
|
||||||
return res.status(404).json({ error: "Workspace not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await assertPermission(db, user.id, workspace.id, "workspace:manage");
|
|
||||||
} catch {
|
|
||||||
return res.status(403).json({ error: "Unauthorized" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const subscription = await subscriptionRepo.create(db, {
|
|
||||||
plan: "pro",
|
|
||||||
referenceId: workspacePublicId,
|
|
||||||
userId: user.id,
|
|
||||||
stripeCustomerId: user.stripeCustomerId ?? "",
|
|
||||||
status: "incomplete",
|
|
||||||
});
|
|
||||||
|
|
||||||
subscriptionId = subscription?.id;
|
|
||||||
|
|
||||||
if (!subscriptionId) {
|
|
||||||
return res.status(500).json({ error: "Error creating subscription" });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
resolvedWorkspacePublicId = generateUID();
|
|
||||||
}
|
|
||||||
|
|
||||||
const isTeam = body.plan === "team";
|
|
||||||
const annualPriceId = isTeam
|
|
||||||
? (process.env.STRIPE_TEAM_PLAN_ANNUAL_PRICE_ID ??
|
|
||||||
process.env.STRIPE_TEAM_PLAN_MONTHLY_PRICE_ID)
|
|
||||||
: (process.env.STRIPE_PRO_PLAN_ANNUAL_PRICE_ID ??
|
|
||||||
process.env.STRIPE_PRO_PLAN_MONTHLY_PRICE_ID);
|
|
||||||
const monthlyPriceId = isTeam
|
|
||||||
? process.env.STRIPE_TEAM_PLAN_MONTHLY_PRICE_ID
|
|
||||||
: process.env.STRIPE_PRO_PLAN_MONTHLY_PRICE_ID;
|
|
||||||
const priceId = billing === "annual" ? annualPriceId : monthlyPriceId;
|
|
||||||
|
|
||||||
const session = await stripe.checkout.sessions.create({
|
|
||||||
mode: "subscription",
|
|
||||||
payment_method_collection: "always",
|
|
||||||
line_items: [{ price: priceId, quantity: 1 }],
|
|
||||||
subscription_data: { trial_period_days: 14 },
|
|
||||||
success_url: `${env("NEXT_PUBLIC_BASE_URL")}${successUrl}?workspacePublicId=${resolvedWorkspacePublicId}`,
|
|
||||||
cancel_url: `${env("NEXT_PUBLIC_BASE_URL")}${cancelUrl}`,
|
|
||||||
client_reference_id: resolvedWorkspacePublicId,
|
|
||||||
customer: user.stripeCustomerId ?? undefined,
|
|
||||||
metadata: {
|
|
||||||
workspacePublicId: resolvedWorkspacePublicId!,
|
|
||||||
userId: user.id,
|
|
||||||
userEmail: user.email ?? "",
|
|
||||||
...(resolvedSlug && { workspaceSlug: resolvedSlug }),
|
|
||||||
...(workspaceName && { workspaceName }),
|
|
||||||
...(workspaceDescription && { workspaceDescription }),
|
|
||||||
...(subscriptionId && { subscriptionId: String(subscriptionId) }),
|
|
||||||
plan: body.plan ?? "pro",
|
|
||||||
isNewWorkspace: workspaceName ? "true" : "false",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).json({ url: session.url });
|
subscriptionId = subscription?.id;
|
||||||
} catch (error) {
|
|
||||||
return res.status(500).json({ error: "Error creating checkout session" });
|
if (!subscriptionId) {
|
||||||
|
return res.status(500).json({ error: "Error creating subscription" });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolvedWorkspacePublicId = generateUID();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isTeam = body.plan === "team";
|
||||||
|
const annualPriceId = isTeam
|
||||||
|
? (process.env.STRIPE_TEAM_PLAN_YEARLY_PRICE_ID ??
|
||||||
|
process.env.STRIPE_TEAM_PLAN_MONTHLY_PRICE_ID)
|
||||||
|
: (process.env.STRIPE_PRO_PLAN_YEARLY_PRICE_ID ??
|
||||||
|
process.env.STRIPE_PRO_PLAN_MONTHLY_PRICE_ID);
|
||||||
|
const monthlyPriceId = isTeam
|
||||||
|
? process.env.STRIPE_TEAM_PLAN_MONTHLY_PRICE_ID
|
||||||
|
: process.env.STRIPE_PRO_PLAN_MONTHLY_PRICE_ID;
|
||||||
|
const priceId = billing === "annual" ? annualPriceId : monthlyPriceId;
|
||||||
|
|
||||||
|
const session = await stripe.checkout.sessions.create({
|
||||||
|
mode: "subscription",
|
||||||
|
payment_method_collection: "always",
|
||||||
|
line_items: [{ price: priceId, quantity: 1 }],
|
||||||
|
subscription_data: { trial_period_days: 14 },
|
||||||
|
success_url: `${env("NEXT_PUBLIC_BASE_URL")}${successUrl}?workspacePublicId=${resolvedWorkspacePublicId}`,
|
||||||
|
cancel_url: `${env("NEXT_PUBLIC_BASE_URL")}${cancelUrl}`,
|
||||||
|
client_reference_id: resolvedWorkspacePublicId,
|
||||||
|
customer: user.stripeCustomerId ?? undefined,
|
||||||
|
metadata: {
|
||||||
|
workspacePublicId: resolvedWorkspacePublicId!,
|
||||||
|
userId: user.id,
|
||||||
|
userEmail: user.email ?? "",
|
||||||
|
...(resolvedSlug && { workspaceSlug: resolvedSlug }),
|
||||||
|
...(workspaceName && { workspaceName }),
|
||||||
|
...(workspaceDescription && { workspaceDescription }),
|
||||||
|
...(subscriptionId && { subscriptionId: String(subscriptionId) }),
|
||||||
|
plan: plan,
|
||||||
|
isNewWorkspace: workspaceName ? "true" : "false",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({ url: session.url });
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ interface Workspace {
|
|||||||
plan: "free" | "team" | "pro" | "enterprise" | undefined;
|
plan: "free" | "team" | "pro" | "enterprise" | undefined;
|
||||||
role: "admin" | "member" | "guest";
|
role: "admin" | "member" | "guest";
|
||||||
weekStartDay: 0 | 1 | 6;
|
weekStartDay: 0 | 1 | 6;
|
||||||
|
cardPrefix: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialWorkspace: Workspace = {
|
const initialWorkspace: Workspace = {
|
||||||
@@ -30,6 +31,7 @@ const initialWorkspace: Workspace = {
|
|||||||
plan: "free" as const,
|
plan: "free" as const,
|
||||||
role: "member",
|
role: "member",
|
||||||
weekStartDay: 1,
|
weekStartDay: 1,
|
||||||
|
cardPrefix: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialAvailableWorkspaces: Workspace[] = [];
|
const initialAvailableWorkspaces: Workspace[] = [];
|
||||||
@@ -90,6 +92,7 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
description: workspace.description,
|
description: workspace.description,
|
||||||
plan: workspace.plan,
|
plan: workspace.plan,
|
||||||
weekStartDay: workspace.weekStartDay,
|
weekStartDay: workspace.weekStartDay,
|
||||||
|
cardPrefix: workspace.cardPrefix,
|
||||||
hasLoaded: true,
|
hasLoaded: true,
|
||||||
})) as Workspace[];
|
})) as Workspace[];
|
||||||
|
|
||||||
@@ -121,6 +124,7 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
description: selectedWorkspace.workspace.description,
|
description: selectedWorkspace.workspace.description,
|
||||||
role: selectedWorkspace.role,
|
role: selectedWorkspace.role,
|
||||||
weekStartDay: selectedWorkspace.workspace.weekStartDay as 0 | 1 | 6,
|
weekStartDay: selectedWorkspace.workspace.weekStartDay as 0 | 1 | 6,
|
||||||
|
cardPrefix: selectedWorkspace.workspace.cardPrefix,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (workspacePublicId) {
|
if (workspacePublicId) {
|
||||||
@@ -141,6 +145,7 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
description: primaryWorkspace.description,
|
description: primaryWorkspace.description,
|
||||||
role: primaryWorkspaceRole,
|
role: primaryWorkspaceRole,
|
||||||
weekStartDay: primaryWorkspace.weekStartDay as 0 | 1 | 6,
|
weekStartDay: primaryWorkspace.weekStartDay as 0 | 1 | 6,
|
||||||
|
cardPrefix: primaryWorkspace.cardPrefix,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data, isLoading, workspacePublicId, router]);
|
}, [data, isLoading, workspacePublicId, router]);
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ const loadMessages = async (locale: Locale) => {
|
|||||||
return (await import("~/locales/ru/messages")).messages;
|
return (await import("~/locales/ru/messages")).messages;
|
||||||
case "pl":
|
case "pl":
|
||||||
return (await import("~/locales/pl/messages")).messages;
|
return (await import("~/locales/pl/messages")).messages;
|
||||||
case "pt-BR":
|
case "ptbr":
|
||||||
return (await import("~/locales/pt-BR/messages")).messages;
|
return (await import("~/locales/ptbr/messages")).messages;
|
||||||
default:
|
default:
|
||||||
return enMessages;
|
return enMessages;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import {
|
import {
|
||||||
HiEllipsisHorizontal,
|
HiEllipsisHorizontal,
|
||||||
@@ -29,6 +30,7 @@ export default function BoardDropdown({
|
|||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
boardName?: string;
|
boardName?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const router = useRouter();
|
||||||
const { openModal } = useModal();
|
const { openModal } = useModal();
|
||||||
const { showPopup } = usePopup();
|
const { showPopup } = usePopup();
|
||||||
const { canEditBoard, canDeleteBoard, canCreateBoard, canArchiveBoard } =
|
const { canEditBoard, canDeleteBoard, canCreateBoard, canArchiveBoard } =
|
||||||
@@ -47,6 +49,7 @@ export default function BoardDropdown({
|
|||||||
: t`The board has been unarchived.`,
|
: t`The board has been unarchived.`,
|
||||||
icon: "success",
|
icon: "success",
|
||||||
});
|
});
|
||||||
|
void router.push(`/boards`);
|
||||||
} else if (variables.favorite !== undefined) {
|
} else if (variables.favorite !== undefined) {
|
||||||
showPopup({
|
showPopup({
|
||||||
header: variables.favorite
|
header: variables.favorite
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { getAvatarUrl } from "~/utils/helpers";
|
|||||||
|
|
||||||
const Card = ({
|
const Card = ({
|
||||||
title,
|
title,
|
||||||
|
ticketNumber,
|
||||||
labels,
|
labels,
|
||||||
members,
|
members,
|
||||||
checklists,
|
checklists,
|
||||||
@@ -25,6 +26,7 @@ const Card = ({
|
|||||||
dueDate,
|
dueDate,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
|
ticketNumber?: string | null;
|
||||||
labels: { name: string; colourCode: string | null }[];
|
labels: { name: string; colourCode: string | null }[];
|
||||||
members: {
|
members: {
|
||||||
publicId: string;
|
publicId: string;
|
||||||
@@ -67,6 +69,11 @@ const Card = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col overflow-hidden rounded-md border border-light-200 bg-light-50 px-3 py-2 text-sm text-neutral-900 dark:border-dark-200 dark:bg-dark-200 dark:text-dark-1000 dark:hover:bg-dark-300">
|
<div className="flex flex-col overflow-hidden rounded-md border border-light-200 bg-light-50 px-3 py-2 text-sm text-neutral-900 dark:border-dark-200 dark:bg-dark-200 dark:text-dark-1000 dark:hover:bg-dark-300">
|
||||||
|
{ticketNumber && (
|
||||||
|
<span className="mb-1 text-xs text-light-700 dark:text-dark-800">
|
||||||
|
{ticketNumber}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span className="break-words">{title}</span>
|
<span className="break-words">{title}</span>
|
||||||
{labels.length ||
|
{labels.length ||
|
||||||
members.length ||
|
members.length ||
|
||||||
|
|||||||
@@ -146,6 +146,10 @@ export function NewCardForm({
|
|||||||
listId: 2,
|
listId: 2,
|
||||||
description: "",
|
description: "",
|
||||||
dueDate: args.dueDate ?? null,
|
dueDate: args.dueDate ?? null,
|
||||||
|
cardNumber: null,
|
||||||
|
comments: [],
|
||||||
|
checklists: [],
|
||||||
|
attachments: [],
|
||||||
labels: oldBoard.labels.filter((label) =>
|
labels: oldBoard.labels.filter((label) =>
|
||||||
args.labelPublicIds.includes(label.publicId),
|
args.labelPublicIds.includes(label.publicId),
|
||||||
),
|
),
|
||||||
@@ -158,6 +162,9 @@ export function NewCardForm({
|
|||||||
...member,
|
...member,
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
})) ?? [],
|
})) ?? [],
|
||||||
|
comments: [],
|
||||||
|
checklists: [],
|
||||||
|
attachments: [],
|
||||||
_filteredLabels: labelPublicIds.map((id) => ({ publicId: id })),
|
_filteredLabels: labelPublicIds.map((id) => ({ publicId: id })),
|
||||||
_filteredMembers: memberPublicIds.map((id) => ({ publicId: id })),
|
_filteredMembers: memberPublicIds.map((id) => ({ publicId: id })),
|
||||||
index: position === "start" ? 0 : list.cards.length,
|
index: position === "start" ? 0 : list.cards.length,
|
||||||
@@ -515,7 +522,7 @@ export function NewCardForm({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
<div className="mt-5 flex items-center justify-end space-x-4 border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
||||||
<Toggle
|
<Toggle
|
||||||
label={t`Create another`}
|
label={t`Create another`}
|
||||||
isChecked={isCreateAnotherEnabled}
|
isChecked={isCreateAnotherEnabled}
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export function NewListForm({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-12 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
<div className="mt-12 flex items-center justify-end space-x-4 border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
||||||
<Toggle
|
<Toggle
|
||||||
label={t`Create another`}
|
label={t`Create another`}
|
||||||
isChecked={isCreateAnotherEnabled}
|
isChecked={isCreateAnotherEnabled}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import type { DropResult } from "react-beautiful-dnd";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { env } from "next-runtime-env";
|
|
||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { keepPreviousData } from "@tanstack/react-query";
|
import { keepPreviousData } from "@tanstack/react-query";
|
||||||
|
import { env } from "next-runtime-env";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { DragDropContext, Draggable } from "react-beautiful-dnd";
|
import { DragDropContext, Draggable } from "react-beautiful-dnd";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -757,6 +757,11 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
|
|||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
title={card.title}
|
title={card.title}
|
||||||
|
ticketNumber={
|
||||||
|
card.cardNumber != null
|
||||||
|
? `${boardData.workspace.cardPrefix}-${card.cardNumber}`
|
||||||
|
: null
|
||||||
|
}
|
||||||
labels={card.labels}
|
labels={card.labels}
|
||||||
members={card.members}
|
members={card.members}
|
||||||
checklists={card.checklists ?? []}
|
checklists={card.checklists ?? []}
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ const SelectSource = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-12 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
<div className="mt-12 flex items-center justify-end space-x-4 border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -310,7 +310,7 @@ const ImportGithub: React.FC = () => {
|
|||||||
<form onSubmit={handleSubmitProjects(onSubmitProjects)}>
|
<form onSubmit={handleSubmitProjects(onSubmitProjects)}>
|
||||||
<div className="h-[105px] overflow-auto px-5">{renderContent()}</div>
|
<div className="h-[105px] overflow-auto px-5">{renderContent()}</div>
|
||||||
|
|
||||||
<div className="mt-12 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
<div className="mt-12 flex items-center justify-end space-x-4 border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
||||||
<Toggle
|
<Toggle
|
||||||
label={t`Select all`}
|
label={t`Select all`}
|
||||||
isChecked={!!isSelectAllEnabled}
|
isChecked={!!isSelectAllEnabled}
|
||||||
@@ -459,7 +459,7 @@ const ImportTrello: React.FC = () => {
|
|||||||
<form onSubmit={handleSubmitBoards(onSubmitBoards)}>
|
<form onSubmit={handleSubmitBoards(onSubmitBoards)}>
|
||||||
<div className="h-[105px] overflow-auto px-5">{renderContent()}</div>
|
<div className="h-[105px] overflow-auto px-5">{renderContent()}</div>
|
||||||
|
|
||||||
<div className="mt-12 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
<div className="mt-12 flex items-center justify-end space-x-4 border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
||||||
<Toggle
|
<Toggle
|
||||||
label={t`Select all`}
|
label={t`Select all`}
|
||||||
isChecked={!!isSelectAllEnabled}
|
isChecked={!!isSelectAllEnabled}
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export function NewBoardForm({ isTemplate }: { isTemplate?: boolean }) {
|
|||||||
showTemplates={showTemplates}
|
showTemplates={showTemplates}
|
||||||
customTemplates={formattedTemplates ?? []}
|
customTemplates={formattedTemplates ?? []}
|
||||||
/>
|
/>
|
||||||
<div className="mt-12 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
<div className="mt-12 flex items-center justify-end space-x-4 border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
||||||
{!isTemplate && (
|
{!isTemplate && (
|
||||||
<Toggle
|
<Toggle
|
||||||
label={t`Use template`}
|
label={t`Use template`}
|
||||||
|
|||||||
@@ -525,7 +525,6 @@ const ActivityList = ({
|
|||||||
comment={activity.comment?.comment}
|
comment={activity.comment?.comment}
|
||||||
isEdited={!!activity.comment?.updatedAt}
|
isEdited={!!activity.comment?.updatedAt}
|
||||||
isAuthor={activity.comment?.createdBy === sessionData?.user.id}
|
isAuthor={activity.comment?.createdBy === sessionData?.user.id}
|
||||||
isAdmin={isAdmin ?? false}
|
|
||||||
isViewOnly={!!isViewOnly}
|
isViewOnly={!!isViewOnly}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import {
|
import {
|
||||||
HiEllipsisHorizontal,
|
HiEllipsisHorizontal,
|
||||||
|
HiHashtag,
|
||||||
HiLink,
|
HiLink,
|
||||||
HiOutlineCheckCircle,
|
HiOutlineCheckCircle,
|
||||||
HiOutlineTrash,
|
HiOutlineTrash,
|
||||||
@@ -18,11 +19,13 @@ export default function CardDropdown({
|
|||||||
isTemplate,
|
isTemplate,
|
||||||
boardPublicId,
|
boardPublicId,
|
||||||
cardCreatedBy,
|
cardCreatedBy,
|
||||||
|
ticketNumber,
|
||||||
}: {
|
}: {
|
||||||
cardPublicId: string;
|
cardPublicId: string;
|
||||||
isTemplate?: boolean;
|
isTemplate?: boolean;
|
||||||
boardPublicId?: string;
|
boardPublicId?: string;
|
||||||
cardCreatedBy?: string | null;
|
cardCreatedBy?: string | null;
|
||||||
|
ticketNumber?: string | null;
|
||||||
}) {
|
}) {
|
||||||
const { openModal } = useModal();
|
const { openModal } = useModal();
|
||||||
const { showPopup } = usePopup();
|
const { showPopup } = usePopup();
|
||||||
@@ -53,12 +56,40 @@ export default function CardDropdown({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCopyTicketId = async () => {
|
||||||
|
if (!ticketNumber) return;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(ticketNumber);
|
||||||
|
showPopup({
|
||||||
|
header: t`ID copied`,
|
||||||
|
icon: "success",
|
||||||
|
message: t`Ticket ID copied to clipboard`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
showPopup({
|
||||||
|
header: t`Unable to copy ID`,
|
||||||
|
icon: "error",
|
||||||
|
message: t`Please try again.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
label: t`Copy card link`,
|
label: t`Copy card link`,
|
||||||
action: handleCopyCardLink,
|
action: handleCopyCardLink,
|
||||||
icon: <HiLink className="h-[16px] w-[16px] text-dark-900" />,
|
icon: <HiLink className="h-[16px] w-[16px] text-dark-900" />,
|
||||||
},
|
},
|
||||||
|
...(ticketNumber
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: t`Copy ticket ID`,
|
||||||
|
action: handleCopyTicketId,
|
||||||
|
icon: <HiHashtag className="h-[16px] w-[16px] text-dark-900" />,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
...(canEditCard
|
...(canEditCard
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -340,6 +340,14 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
|
|||||||
>
|
>
|
||||||
{board?.name}
|
{board?.name}
|
||||||
</Link>
|
</Link>
|
||||||
|
{card.cardNumber != null && card.list.board.workspace.cardPrefix && (
|
||||||
|
<>
|
||||||
|
<IoChevronForwardSharp className="h-[10px] w-[10px] text-light-900 dark:text-dark-900" />
|
||||||
|
<span className="whitespace-nowrap text-sm font-bold leading-[1.5rem] text-light-700 dark:text-dark-800">
|
||||||
|
{card.list.board.workspace.cardPrefix}-{card.cardNumber}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -347,6 +355,11 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
|
|||||||
isTemplate={isTemplate}
|
isTemplate={isTemplate}
|
||||||
boardPublicId={boardId}
|
boardPublicId={boardId}
|
||||||
cardCreatedBy={card?.createdBy}
|
cardCreatedBy={card?.createdBy}
|
||||||
|
ticketNumber={
|
||||||
|
card.cardNumber != null && card.list.board.workspace.cardPrefix
|
||||||
|
? `${card.list.board.workspace.cardPrefix}-${card.cardNumber}`
|
||||||
|
: null
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Link
|
<Link
|
||||||
href={`/${isTemplate ? "templates" : "boards"}/${boardId}`}
|
href={`/${isTemplate ? "templates" : "boards"}/${boardId}`}
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ export function InviteMemberForm({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-12 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
<div className="mt-12 flex items-center justify-end space-x-4 border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
||||||
<Toggle
|
<Toggle
|
||||||
label={
|
label={
|
||||||
isShareInviteLinkEnabled
|
isShareInviteLinkEnabled
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export default function WorkspaceNameView() {
|
|||||||
const returnUrl = searchParams.get("returnUrl") ?? "/boards";
|
const returnUrl = searchParams.get("returnUrl") ?? "/boards";
|
||||||
const { showPopup } = usePopup();
|
const { showPopup } = usePopup();
|
||||||
const [isProToggle, setIsProToggle] = useState(plan === "pro");
|
const [isProToggle, setIsProToggle] = useState(plan === "pro");
|
||||||
|
const effectivePlan = isProToggle ? "pro" : plan;
|
||||||
|
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [slug, setSlug] = useState("");
|
const [slug, setSlug] = useState("");
|
||||||
@@ -109,7 +110,7 @@ export default function WorkspaceNameView() {
|
|||||||
const handleContinue = async () => {
|
const handleContinue = async () => {
|
||||||
if (!name.trim()) return;
|
if (!name.trim()) return;
|
||||||
|
|
||||||
if (plan === "solo") {
|
if (effectivePlan === "solo") {
|
||||||
createWorkspace.mutate({
|
createWorkspace.mutate({
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
...(description.trim() && { description: description.trim() }),
|
...(description.trim() && { description: description.trim() }),
|
||||||
@@ -128,11 +129,11 @@ export default function WorkspaceNameView() {
|
|||||||
...(description.trim() && {
|
...(description.trim() && {
|
||||||
workspaceDescription: description.trim(),
|
workspaceDescription: description.trim(),
|
||||||
}),
|
}),
|
||||||
...(plan === "pro" && slug ? { workspaceSlug: slug } : {}),
|
...(effectivePlan === "pro" && slug ? { workspaceSlug: slug } : {}),
|
||||||
cancelUrl: window.location.pathname + window.location.search,
|
cancelUrl: `${window.location.pathname}?plan=${effectivePlan}&billing=${billing}&returnUrl=${encodeURIComponent(returnUrl)}`,
|
||||||
successUrl: "/boards",
|
successUrl: "/boards",
|
||||||
billing,
|
billing,
|
||||||
plan,
|
plan: effectivePlan,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -268,7 +269,7 @@ export default function WorkspaceNameView() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.replace(
|
router.replace(
|
||||||
`/onboarding/select-plan?plan=${plan}&billing=${billing}&returnUrl=${encodeURIComponent(returnUrl)}`,
|
`/onboarding/select-plan?plan=${effectivePlan}&billing=${billing}&returnUrl=${encodeURIComponent(returnUrl)}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -140,6 +140,11 @@ export function CardModal({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
{data?.cardNumber != null && data.list.board.workspace.cardPrefix && (
|
||||||
|
<span className="mb-1 block text-xs font-medium text-light-700 dark:text-dark-800">
|
||||||
|
{data.list.board.workspace.cardPrefix}-{data.cardNumber}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<h1 className="pr-8 font-bold leading-[2.3rem] tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
|
<h1 className="pr-8 font-bold leading-[2.3rem] tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
|
||||||
{data?.title}
|
{data?.title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ import { useModal } from "~/providers/modal";
|
|||||||
import { useWorkspace } from "~/providers/workspace";
|
import { useWorkspace } from "~/providers/workspace";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { DeleteWorkspaceConfirmation } from "./components/DeleteWorkspaceConfirmation";
|
import { DeleteWorkspaceConfirmation } from "./components/DeleteWorkspaceConfirmation";
|
||||||
|
import UpdateWeekStartDayForm from "./components/UpdateWeekStartDayForm";
|
||||||
import UpdateWorkspaceDescriptionForm from "./components/UpdateWorkspaceDescriptionForm";
|
import UpdateWorkspaceDescriptionForm from "./components/UpdateWorkspaceDescriptionForm";
|
||||||
import UpdateWorkspaceEmailVisibilityForm from "./components/UpdateWorkspaceEmailVisibilityForm";
|
import UpdateWorkspaceEmailVisibilityForm from "./components/UpdateWorkspaceEmailVisibilityForm";
|
||||||
import UpdateWorkspaceNameForm from "./components/UpdateWorkspaceNameForm";
|
import UpdateWorkspaceNameForm from "./components/UpdateWorkspaceNameForm";
|
||||||
import UpdateWorkspaceUrlForm from "./components/UpdateWorkspaceUrlForm";
|
import UpdateWorkspaceUrlForm from "./components/UpdateWorkspaceUrlForm";
|
||||||
import UpdateWeekStartDayForm from "./components/UpdateWeekStartDayForm";
|
|
||||||
import { UpgradeToProConfirmation } from "./components/UpgradeToProConfirmation";
|
import { UpgradeToProConfirmation } from "./components/UpgradeToProConfirmation";
|
||||||
|
|
||||||
export default function WorkspaceSettings() {
|
export default function WorkspaceSettings() {
|
||||||
|
|||||||
@@ -13,20 +13,29 @@ interface DeleteWebhookConfirmationProps {
|
|||||||
export function DeleteWebhookConfirmation({
|
export function DeleteWebhookConfirmation({
|
||||||
workspacePublicId,
|
workspacePublicId,
|
||||||
}: DeleteWebhookConfirmationProps) {
|
}: DeleteWebhookConfirmationProps) {
|
||||||
const { closeModal, entityId: webhookPublicId, entityLabel: webhookName } = useModal();
|
const {
|
||||||
|
closeModal,
|
||||||
|
entityId: webhookPublicId,
|
||||||
|
entityLabel: webhookName,
|
||||||
|
} = useModal();
|
||||||
const { showPopup } = usePopup();
|
const { showPopup } = usePopup();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const deleteWebhookMutation = api.webhook.delete.useMutation({
|
const deleteWebhookMutation = api.webhook.delete.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
void utils.webhook.list.invalidate({ workspacePublicId });
|
void utils.webhook.list.invalidate({ workspacePublicId });
|
||||||
showPopup({ message: t`Webhook deleted successfully`, type: "success" });
|
showPopup({
|
||||||
|
header: t`Webhook deleted`,
|
||||||
|
message: t`Webhook deleted successfully`,
|
||||||
|
icon: "success",
|
||||||
|
});
|
||||||
closeModal();
|
closeModal();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
showPopup({
|
showPopup({
|
||||||
|
header: t`Unable to delete webhook`,
|
||||||
message: error.message || t`Failed to delete webhook`,
|
message: error.message || t`Failed to delete webhook`,
|
||||||
type: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -35,7 +44,7 @@ export function DeleteWebhookConfirmation({
|
|||||||
if (!webhookPublicId) return;
|
if (!webhookPublicId) return;
|
||||||
deleteWebhookMutation.mutate({
|
deleteWebhookMutation.mutate({
|
||||||
workspacePublicId,
|
workspacePublicId,
|
||||||
webhookPublicId: webhookPublicId as string,
|
webhookPublicId: webhookPublicId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,12 @@ export function NewWebhookModal({
|
|||||||
workspacePublicId,
|
workspacePublicId,
|
||||||
isEdit = false,
|
isEdit = false,
|
||||||
}: NewWebhookModalProps) {
|
}: NewWebhookModalProps) {
|
||||||
const { closeModal, entityId: webhookPublicId, getModalState, clearModalState } = useModal();
|
const {
|
||||||
|
closeModal,
|
||||||
|
entityId: webhookPublicId,
|
||||||
|
getModalState,
|
||||||
|
clearModalState,
|
||||||
|
} = useModal();
|
||||||
const { showPopup } = usePopup();
|
const { showPopup } = usePopup();
|
||||||
const [isTestingWebhook, setIsTestingWebhook] = useState(false);
|
const [isTestingWebhook, setIsTestingWebhook] = useState(false);
|
||||||
|
|
||||||
@@ -101,13 +106,18 @@ export function NewWebhookModal({
|
|||||||
const createWebhookMutation = api.webhook.create.useMutation({
|
const createWebhookMutation = api.webhook.create.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
void utils.webhook.list.invalidate({ workspacePublicId });
|
void utils.webhook.list.invalidate({ workspacePublicId });
|
||||||
showPopup({ message: t`Webhook created successfully`, type: "success" });
|
showPopup({
|
||||||
|
header: t`Webhook created`,
|
||||||
|
message: t`Webhook created successfully`,
|
||||||
|
icon: "success",
|
||||||
|
});
|
||||||
closeModal();
|
closeModal();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
showPopup({
|
showPopup({
|
||||||
|
header: t`Unable to create webhook`,
|
||||||
message: error.message || t`Failed to create webhook`,
|
message: error.message || t`Failed to create webhook`,
|
||||||
type: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -115,13 +125,18 @@ export function NewWebhookModal({
|
|||||||
const updateWebhookMutation = api.webhook.update.useMutation({
|
const updateWebhookMutation = api.webhook.update.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
void utils.webhook.list.invalidate({ workspacePublicId });
|
void utils.webhook.list.invalidate({ workspacePublicId });
|
||||||
showPopup({ message: t`Webhook updated successfully`, type: "success" });
|
showPopup({
|
||||||
|
header: t`Webhook updated`,
|
||||||
|
message: t`Webhook updated successfully`,
|
||||||
|
icon: "success",
|
||||||
|
});
|
||||||
closeModal();
|
closeModal();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
showPopup({
|
showPopup({
|
||||||
|
header: t`Unable to update webhook`,
|
||||||
message: error.message || t`Failed to update webhook`,
|
message: error.message || t`Failed to update webhook`,
|
||||||
type: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -129,19 +144,25 @@ export function NewWebhookModal({
|
|||||||
const testWebhookMutation = api.webhook.test.useMutation({
|
const testWebhookMutation = api.webhook.test.useMutation({
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
showPopup({ message: t`Test webhook sent successfully!`, type: "success" });
|
showPopup({
|
||||||
|
header: t`Test sent`,
|
||||||
|
message: t`Test webhook sent successfully!`,
|
||||||
|
icon: "success",
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
showPopup({
|
showPopup({
|
||||||
|
header: t`Test failed`,
|
||||||
message: result.error || t`Webhook test failed`,
|
message: result.error || t`Webhook test failed`,
|
||||||
type: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setIsTestingWebhook(false);
|
setIsTestingWebhook(false);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
showPopup({
|
showPopup({
|
||||||
|
header: t`Unable to test webhook`,
|
||||||
message: error.message || t`Failed to test webhook`,
|
message: error.message || t`Failed to test webhook`,
|
||||||
type: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
setIsTestingWebhook(false);
|
setIsTestingWebhook(false);
|
||||||
},
|
},
|
||||||
@@ -151,7 +172,7 @@ export function NewWebhookModal({
|
|||||||
if (isEdit && webhookPublicId) {
|
if (isEdit && webhookPublicId) {
|
||||||
updateWebhookMutation.mutate({
|
updateWebhookMutation.mutate({
|
||||||
workspacePublicId,
|
workspacePublicId,
|
||||||
webhookPublicId: webhookPublicId as string,
|
webhookPublicId: webhookPublicId,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
url: data.url,
|
url: data.url,
|
||||||
secret: data.secret || undefined,
|
secret: data.secret || undefined,
|
||||||
@@ -174,11 +195,12 @@ export function NewWebhookModal({
|
|||||||
setIsTestingWebhook(true);
|
setIsTestingWebhook(true);
|
||||||
testWebhookMutation.mutate({
|
testWebhookMutation.mutate({
|
||||||
workspacePublicId,
|
workspacePublicId,
|
||||||
webhookPublicId: webhookPublicId as string,
|
webhookPublicId: webhookPublicId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const isPending = createWebhookMutation.isPending || updateWebhookMutation.isPending;
|
const isPending =
|
||||||
|
createWebhookMutation.isPending || updateWebhookMutation.isPending;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
@@ -231,7 +253,11 @@ export function NewWebhookModal({
|
|||||||
<Input
|
<Input
|
||||||
id="secret"
|
id="secret"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder={isEdit ? t`Enter new secret to update` : t`HMAC secret for signature verification`}
|
placeholder={
|
||||||
|
isEdit
|
||||||
|
? t`Enter new secret to update`
|
||||||
|
: t`HMAC secret for signature verification`
|
||||||
|
}
|
||||||
{...register("secret")}
|
{...register("secret")}
|
||||||
errorMessage={errors.secret?.message}
|
errorMessage={errors.secret?.message}
|
||||||
/>
|
/>
|
||||||
@@ -252,7 +278,7 @@ export function NewWebhookModal({
|
|||||||
{webhookEvents.map((event) => (
|
{webhookEvents.map((event) => (
|
||||||
<label
|
<label
|
||||||
key={event}
|
key={event}
|
||||||
className="flex items-center space-x-2 cursor-pointer"
|
className="flex cursor-pointer items-center space-x-2"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -262,11 +288,11 @@ export function NewWebhookModal({
|
|||||||
field.onChange([...field.value, event]);
|
field.onChange([...field.value, event]);
|
||||||
} else {
|
} else {
|
||||||
field.onChange(
|
field.onChange(
|
||||||
field.value.filter((v) => v !== event)
|
field.value.filter((v) => v !== event),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="h-4 w-4 rounded border-light-400 text-primary-600 focus:ring-primary-500 dark:border-dark-400"
|
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-light-400 dark:border-dark-400"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-light-900 dark:text-dark-900">
|
<span className="text-sm text-light-900 dark:text-dark-900">
|
||||||
{event}
|
{event}
|
||||||
@@ -277,17 +303,19 @@ export function NewWebhookModal({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{errors.events && (
|
{errors.events && (
|
||||||
<p className="mt-1 text-xs text-red-500">{errors.events.message}</p>
|
<p className="mt-1 text-xs text-red-500">
|
||||||
|
{errors.events.message}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isEdit && (
|
{isEdit && (
|
||||||
<div>
|
<div>
|
||||||
<label className="flex items-center space-x-2 cursor-pointer">
|
<label className="flex cursor-pointer items-center space-x-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
{...register("active")}
|
{...register("active")}
|
||||||
className="h-4 w-4 rounded border-light-400 text-primary-600 focus:ring-primary-500 dark:border-dark-400"
|
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-light-400 dark:border-dark-400"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-light-900 dark:text-dark-900">
|
<span className="text-sm text-light-900 dark:text-dark-900">
|
||||||
{t`Active`}
|
{t`Active`}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export default function UpdateWeekStartDayForm({
|
|||||||
setValue(newValue);
|
setValue(newValue);
|
||||||
updateWorkspace.mutate({
|
updateWorkspace.mutate({
|
||||||
workspacePublicId,
|
workspacePublicId,
|
||||||
weekStartDay: newValue,
|
weekStartDay: newValue as 0 | 1 | 6,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const UpdateWorkspaceUrlForm = ({
|
|||||||
}: {
|
}: {
|
||||||
workspacePublicId: string;
|
workspacePublicId: string;
|
||||||
workspaceUrl: string;
|
workspaceUrl: string;
|
||||||
workspacePlan: "free" | "pro" | "enterprise";
|
workspacePlan: "free" | "team" | "pro" | "enterprise";
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export function UpgradeToProConfirmation({
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
...(entityId && { slug: entityId }),
|
...(entityId && { slug: entityId }),
|
||||||
workspacePublicId: workspacePublicId,
|
workspacePublicId: workspacePublicId,
|
||||||
|
plan: "pro",
|
||||||
|
billing: "monthly",
|
||||||
cancelUrl: "/settings",
|
cancelUrl: "/settings",
|
||||||
successUrl: "/settings",
|
successUrl: "/settings",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -171,18 +171,20 @@ export default function WebhookList({ workspacePublicId }: WebhookListProps) {
|
|||||||
const testWebhookMutation = api.webhook.test.useMutation({
|
const testWebhookMutation = api.webhook.test.useMutation({
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
showPopup({ message: t`Test webhook sent successfully!`, type: "success" });
|
showPopup({ header: t`Test sent`, message: t`Test webhook sent successfully!`, icon: "success" });
|
||||||
} else {
|
} else {
|
||||||
showPopup({
|
showPopup({
|
||||||
|
header: t`Test failed`,
|
||||||
message: result.error || t`Webhook test failed`,
|
message: result.error || t`Webhook test failed`,
|
||||||
type: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
showPopup({
|
showPopup({
|
||||||
|
header: t`Unable to test webhook`,
|
||||||
message: error.message || t`Failed to test webhook`,
|
message: error.message || t`Failed to test webhook`,
|
||||||
type: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ services:
|
|||||||
- S3_ENDPOINT=${S3_ENDPOINT}
|
- S3_ENDPOINT=${S3_ENDPOINT}
|
||||||
- S3_FORCE_PATH_STYLE=${S3_FORCE_PATH_STYLE}
|
- S3_FORCE_PATH_STYLE=${S3_FORCE_PATH_STYLE}
|
||||||
- S3_AVATAR_UPLOAD_LIMIT=${S3_AVATAR_UPLOAD_LIMIT}
|
- S3_AVATAR_UPLOAD_LIMIT=${S3_AVATAR_UPLOAD_LIMIT}
|
||||||
- NEXT_PUBLIC_STORAGE_URL=${NEXT_PUBLIC_STORAGE_URL}
|
- NEXT_PUBLIC_STORAGE_URL
|
||||||
- NEXT_PUBLIC_AVATAR_BUCKET_NAME=${NEXT_PUBLIC_AVATAR_BUCKET_NAME}
|
- NEXT_PUBLIC_AVATAR_BUCKET_NAME=${NEXT_PUBLIC_AVATAR_BUCKET_NAME}
|
||||||
- NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME=${NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME}
|
- NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME=${NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME}
|
||||||
- NEXT_PUBLIC_STORAGE_DOMAIN=${NEXT_PUBLIC_STORAGE_DOMAIN}
|
- NEXT_PUBLIC_STORAGE_DOMAIN=${NEXT_PUBLIC_STORAGE_DOMAIN}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
|||||||
import { generateUID } from "@kan/shared/utils";
|
import { generateUID } from "@kan/shared/utils";
|
||||||
|
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
import { attachmentConfirmResponseSchema } from "../schemas";
|
||||||
import { assertPermission } from "../utils/permissions";
|
import { assertPermission } from "../utils/permissions";
|
||||||
import { deleteObject, generateUploadUrl } from "@kan/shared/utils";
|
import { deleteObject, generateUploadUrl } from "@kan/shared/utils";
|
||||||
|
|
||||||
@@ -110,7 +111,7 @@ export const attachmentRouter = createTRPCRouter({
|
|||||||
size: z.number().positive(),
|
size: z.number().positive(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(z.custom<Awaited<ReturnType<typeof cardAttachmentRepo.create>>>())
|
.output(attachmentConfirmResponseSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ import {
|
|||||||
} from "@kan/shared/utils";
|
} from "@kan/shared/utils";
|
||||||
|
|
||||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||||
|
import {
|
||||||
|
boardListItemSchema,
|
||||||
|
boardDetailSchema,
|
||||||
|
boardBySlugSchema,
|
||||||
|
boardCreateResponseSchema,
|
||||||
|
boardUpdateResponseSchema,
|
||||||
|
} from "../schemas";
|
||||||
import { assertCanDelete, assertCanEdit, assertPermission } from "../utils/permissions";
|
import { assertCanDelete, assertCanEdit, assertPermission } from "../utils/permissions";
|
||||||
|
|
||||||
export const boardRouter = createTRPCRouter({
|
export const boardRouter = createTRPCRouter({
|
||||||
@@ -37,9 +44,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
archived: z.boolean().optional(),
|
archived: z.boolean().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(
|
.output(z.array(boardListItemSchema))
|
||||||
z.custom<Awaited<ReturnType<typeof boardRepo.getAllByWorkspaceId>>>(),
|
|
||||||
)
|
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -106,7 +111,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
type: z.enum(["regular", "template"]).optional(),
|
type: z.enum(["regular", "template"]).optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(z.custom<Awaited<ReturnType<typeof boardRepo.getByPublicId>>>())
|
.output(boardDetailSchema)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -246,7 +251,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
.optional(),
|
.optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(z.custom<Awaited<ReturnType<typeof boardRepo.getBySlug>>>())
|
.output(boardBySlugSchema.nullable())
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const workspace = await workspaceRepo.getBySlugWithBoards(
|
const workspace = await workspaceRepo.getBySlugWithBoards(
|
||||||
ctx.db,
|
ctx.db,
|
||||||
@@ -299,7 +304,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
sourceBoardPublicId: z.string().min(12).optional(),
|
sourceBoardPublicId: z.string().min(12).optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(z.custom<Awaited<ReturnType<typeof boardRepo.create>>>())
|
.output(boardCreateResponseSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -469,7 +474,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
isArchived: z.boolean().optional(),
|
isArchived: z.boolean().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(z.object({ success: z.boolean() }).or(z.custom<Awaited<ReturnType<typeof boardRepo.update>>>()))
|
.output(boardUpdateResponseSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ import * as listRepo from "@kan/db/repository/list.repo";
|
|||||||
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
||||||
|
|
||||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||||
|
import {
|
||||||
|
cardCreateResponseSchema,
|
||||||
|
cardUpdateResponseSchema,
|
||||||
|
cardDetailSchema,
|
||||||
|
commentResponseSchema,
|
||||||
|
commentDeleteResponseSchema,
|
||||||
|
activityItemSchema,
|
||||||
|
} from "../schemas";
|
||||||
import { mergeActivities } from "../utils/activities";
|
import { mergeActivities } from "../utils/activities";
|
||||||
import { sendMentionEmails } from "../utils/notifications";
|
import { sendMentionEmails } from "../utils/notifications";
|
||||||
import { assertCanDelete, assertCanEdit, assertPermission } from "../utils/permissions";
|
import { assertCanDelete, assertCanEdit, assertPermission } from "../utils/permissions";
|
||||||
@@ -42,7 +50,7 @@ export const cardRouter = createTRPCRouter({
|
|||||||
dueDate: z.date().nullable().optional(),
|
dueDate: z.date().nullable().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(z.custom<Awaited<ReturnType<typeof cardRepo.create>>>())
|
.output(cardCreateResponseSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -70,6 +78,7 @@ export const cardRouter = createTRPCRouter({
|
|||||||
description: input.description,
|
description: input.description,
|
||||||
createdBy: userId,
|
createdBy: userId,
|
||||||
listId: list.id,
|
listId: list.id,
|
||||||
|
workspaceId: list.workspaceId,
|
||||||
position: input.position,
|
position: input.position,
|
||||||
dueDate: input.dueDate ?? null,
|
dueDate: input.dueDate ?? null,
|
||||||
});
|
});
|
||||||
@@ -181,7 +190,7 @@ export const cardRouter = createTRPCRouter({
|
|||||||
title: input.title,
|
title: input.title,
|
||||||
description: input.description,
|
description: input.description,
|
||||||
dueDate: input.dueDate ?? null,
|
dueDate: input.dueDate ?? null,
|
||||||
listId: String(newCard.listId),
|
listId: list.publicId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
boardId: list.boardPublicId,
|
boardId: list.boardPublicId,
|
||||||
@@ -215,7 +224,7 @@ export const cardRouter = createTRPCRouter({
|
|||||||
comment: z.string().min(1),
|
comment: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(z.custom<Awaited<ReturnType<typeof cardCommentRepo.create>>>())
|
.output(commentResponseSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -288,7 +297,7 @@ export const cardRouter = createTRPCRouter({
|
|||||||
comment: z.string().min(1),
|
comment: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(z.custom<Awaited<ReturnType<typeof cardCommentRepo.update>>>())
|
.output(commentResponseSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -376,7 +385,7 @@ export const cardRouter = createTRPCRouter({
|
|||||||
commentPublicId: z.string().min(12),
|
commentPublicId: z.string().min(12),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(z.custom<Awaited<ReturnType<typeof cardCommentRepo.softDelete>>>())
|
.output(commentDeleteResponseSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -435,7 +444,7 @@ export const cardRouter = createTRPCRouter({
|
|||||||
createdBy: userId,
|
createdBy: userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return deletedComment;
|
return { publicId: input.commentPublicId };
|
||||||
}),
|
}),
|
||||||
addOrRemoveLabel: protectedProcedure
|
addOrRemoveLabel: protectedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@@ -639,25 +648,7 @@ export const cardRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.input(z.object({ cardPublicId: z.string().min(12) }))
|
.input(z.object({ cardPublicId: z.string().min(12) }))
|
||||||
.output(
|
.output(cardDetailSchema)
|
||||||
z.custom<
|
|
||||||
Omit<
|
|
||||||
NonNullable<
|
|
||||||
Awaited<ReturnType<typeof cardRepo.getWithListAndMembersByPublicId>>
|
|
||||||
>,
|
|
||||||
"attachments"
|
|
||||||
> & {
|
|
||||||
attachments: {
|
|
||||||
publicId: string;
|
|
||||||
contentType: string;
|
|
||||||
s3Key: string;
|
|
||||||
originalFilename: string | null;
|
|
||||||
size?: number | null;
|
|
||||||
url: string | null;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
>(),
|
|
||||||
)
|
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const card = await cardRepo.getWorkspaceAndCardIdByCardPublicId(
|
const card = await cardRepo.getWorkspaceAndCardIdByCardPublicId(
|
||||||
ctx.db,
|
ctx.db,
|
||||||
@@ -763,15 +754,7 @@ export const cardRouter = createTRPCRouter({
|
|||||||
)
|
)
|
||||||
.output(
|
.output(
|
||||||
z.object({
|
z.object({
|
||||||
activities: z.array(
|
activities: z.array(activityItemSchema),
|
||||||
z.custom<
|
|
||||||
NonNullable<
|
|
||||||
Awaited<
|
|
||||||
ReturnType<typeof cardActivityRepo.getPaginatedActivities>
|
|
||||||
>
|
|
||||||
>["activities"][number]
|
|
||||||
>(),
|
|
||||||
),
|
|
||||||
hasMore: z.boolean(),
|
hasMore: z.boolean(),
|
||||||
nextCursor: z.string().datetime().nullable(),
|
nextCursor: z.string().datetime().nullable(),
|
||||||
}),
|
}),
|
||||||
@@ -871,7 +854,7 @@ export const cardRouter = createTRPCRouter({
|
|||||||
dueDate: z.date().nullable().optional(),
|
dueDate: z.date().nullable().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(z.custom<Awaited<ReturnType<typeof cardRepo.update>>>())
|
.output(cardUpdateResponseSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -906,9 +889,18 @@ export const cardRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
|
|
||||||
let newListId: number | undefined;
|
let newListId: number | undefined;
|
||||||
|
let newList:
|
||||||
|
| {
|
||||||
|
id: number;
|
||||||
|
publicId: string;
|
||||||
|
name: string;
|
||||||
|
boardId: number;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
|
||||||
if (input.listPublicId) {
|
if (input.listPublicId) {
|
||||||
const newList = await listRepo.getByPublicId(
|
newList = await listRepo.getByPublicId(
|
||||||
ctx.db,
|
ctx.db,
|
||||||
input.listPublicId,
|
input.listPublicId,
|
||||||
);
|
);
|
||||||
@@ -953,7 +945,7 @@ export const cardRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.index !== undefined) {
|
if (input.index !== undefined || newListId !== undefined) {
|
||||||
result = await cardRepo.reorder(ctx.db, {
|
result = await cardRepo.reorder(ctx.db, {
|
||||||
cardId: existingCard.id,
|
cardId: existingCard.id,
|
||||||
newIndex: input.index,
|
newIndex: input.index,
|
||||||
@@ -1055,8 +1047,19 @@ export const cardRouter = createTRPCRouter({
|
|||||||
) {
|
) {
|
||||||
webhookChanges.dueDate = { from: previousDueDate, to: input.dueDate };
|
webhookChanges.dueDate = { from: previousDueDate, to: input.dueDate };
|
||||||
}
|
}
|
||||||
if (newListId && existingCard.listId !== newListId) {
|
const movedToNewList = Boolean(newListId && existingCard.listId !== newListId);
|
||||||
webhookChanges.listId = { from: existingCard.listId, to: newListId };
|
const currentWebhookListPublicId = movedToNewList
|
||||||
|
? input.listPublicId!
|
||||||
|
: existingCard.list.publicId;
|
||||||
|
const currentWebhookListName = movedToNewList
|
||||||
|
? newList?.name ?? card.listName
|
||||||
|
: existingCard.list.name;
|
||||||
|
|
||||||
|
if (movedToNewList) {
|
||||||
|
webhookChanges.listId = {
|
||||||
|
from: existingCard.list.publicId,
|
||||||
|
to: input.listPublicId!,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fire webhooks (non-blocking)
|
// Fire webhooks (non-blocking)
|
||||||
@@ -1064,20 +1067,18 @@ export const cardRouter = createTRPCRouter({
|
|||||||
ctx.db,
|
ctx.db,
|
||||||
card.workspaceId,
|
card.workspaceId,
|
||||||
createCardWebhookPayload(
|
createCardWebhookPayload(
|
||||||
newListId && existingCard.listId !== newListId
|
movedToNewList ? "card.moved" : "card.updated",
|
||||||
? "card.moved"
|
|
||||||
: "card.updated",
|
|
||||||
{
|
{
|
||||||
id: String(result.id),
|
id: String(result.id),
|
||||||
title: result.title,
|
title: result.title,
|
||||||
description: result.description,
|
description: result.description,
|
||||||
dueDate: result.dueDate,
|
dueDate: result.dueDate,
|
||||||
listId: String(newListId ?? existingCard.listId),
|
listId: currentWebhookListPublicId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
boardId: card.boardPublicId,
|
boardId: card.boardPublicId,
|
||||||
boardName: card.boardName,
|
boardName: card.boardName,
|
||||||
listName: card.listName,
|
listName: currentWebhookListName,
|
||||||
user: ctx.user
|
user: ctx.user
|
||||||
? { id: ctx.user.id, name: ctx.user.name }
|
? { id: ctx.user.id, name: ctx.user.name }
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -1167,12 +1168,12 @@ export const cardRouter = createTRPCRouter({
|
|||||||
title: fullCard.title,
|
title: fullCard.title,
|
||||||
description: fullCard.description,
|
description: fullCard.description,
|
||||||
dueDate: fullCard.dueDate,
|
dueDate: fullCard.dueDate,
|
||||||
listId: String(fullCard.listId),
|
listId: fullCard.list.publicId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
boardId: card.boardPublicId,
|
boardId: card.boardPublicId,
|
||||||
boardName: card.boardName,
|
boardName: card.boardName,
|
||||||
listName: card.listName,
|
listName: fullCard.list.name,
|
||||||
user: ctx.user
|
user: ctx.user
|
||||||
? { id: ctx.user.id, name: ctx.user.name }
|
? { id: ctx.user.id, name: ctx.user.name }
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -1272,6 +1273,7 @@ export const cardRouter = createTRPCRouter({
|
|||||||
description: sourceCard.description ?? "",
|
description: sourceCard.description ?? "",
|
||||||
createdBy: userId,
|
createdBy: userId,
|
||||||
listId: targetList.id,
|
listId: targetList.id,
|
||||||
|
workspaceId: targetList.workspaceId,
|
||||||
position: "end",
|
position: "end",
|
||||||
dueDate: sourceCard.dueDate ?? null,
|
dueDate: sourceCard.dueDate ?? null,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -340,6 +340,7 @@ export const importRouter = createTRPCRouter({
|
|||||||
description: card.description,
|
description: card.description,
|
||||||
createdBy: userId,
|
createdBy: userId,
|
||||||
listId: newListId,
|
listId: newListId,
|
||||||
|
workspaceId: workspace.id,
|
||||||
index,
|
index,
|
||||||
importId: newImportId,
|
importId: newImportId,
|
||||||
}));
|
}));
|
||||||
@@ -888,6 +889,7 @@ export const importRouter = createTRPCRouter({
|
|||||||
description: data.description,
|
description: data.description,
|
||||||
createdBy: userId,
|
createdBy: userId,
|
||||||
listId: data.listId,
|
listId: data.listId,
|
||||||
|
workspaceId: workspace.id,
|
||||||
index: index,
|
index: index,
|
||||||
importId: newImportId,
|
importId: newImportId,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import * as activityRepo from "@kan/db/repository/cardActivity.repo";
|
|||||||
import * as listRepo from "@kan/db/repository/list.repo";
|
import * as listRepo from "@kan/db/repository/list.repo";
|
||||||
|
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
import { listCreateResponseSchema, listUpdateResponseSchema } from "../schemas";
|
||||||
import { assertCanDelete, assertCanEdit, assertPermission } from "../utils/permissions";
|
import { assertCanDelete, assertCanEdit, assertPermission } from "../utils/permissions";
|
||||||
|
|
||||||
export const listRouter = createTRPCRouter({
|
export const listRouter = createTRPCRouter({
|
||||||
@@ -27,7 +28,7 @@ export const listRouter = createTRPCRouter({
|
|||||||
boardPublicId: z.string().min(12),
|
boardPublicId: z.string().min(12),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(z.custom<Awaited<ReturnType<typeof listRepo.create>>>())
|
.output(listCreateResponseSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -163,12 +164,7 @@ export const listRouter = createTRPCRouter({
|
|||||||
index: z.number().optional(),
|
index: z.number().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(
|
.output(listUpdateResponseSchema)
|
||||||
z.custom<
|
|
||||||
| Awaited<ReturnType<typeof listRepo.update>>
|
|
||||||
| Awaited<ReturnType<typeof listRepo.reorder>>
|
|
||||||
>(),
|
|
||||||
)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
import { updateSubscriptionSeats } from "@kan/stripe";
|
import { updateSubscriptionSeats } from "@kan/stripe";
|
||||||
|
|
||||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||||
|
import { memberInviteResponseSchema } from "../schemas";
|
||||||
import {
|
import {
|
||||||
assertCanManageMember,
|
assertCanManageMember,
|
||||||
assertCanManageRole,
|
assertCanManageRole,
|
||||||
@@ -40,7 +41,7 @@ export const memberRouter = createTRPCRouter({
|
|||||||
workspacePublicId: z.string().min(12),
|
workspacePublicId: z.string().min(12),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(z.custom<Awaited<ReturnType<typeof memberRepo.create>>>())
|
.output(memberInviteResponseSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ import * as workspaceSlugRepo from "@kan/db/repository/workspaceSlug.repo";
|
|||||||
import { generateAvatarUrl, generateUID } from "@kan/shared/utils";
|
import { generateAvatarUrl, generateUID } from "@kan/shared/utils";
|
||||||
|
|
||||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||||
|
import {
|
||||||
|
workspaceListItemSchema,
|
||||||
|
workspaceDetailSchema,
|
||||||
|
workspaceWithBoardsSchema,
|
||||||
|
workspaceCreateResponseSchema,
|
||||||
|
workspaceUpdateResponseSchema,
|
||||||
|
workspaceDeleteResponseSchema,
|
||||||
|
} from "../schemas";
|
||||||
import { assertPermission } from "../utils/permissions";
|
import { assertPermission } from "../utils/permissions";
|
||||||
|
|
||||||
export const workspaceRouter = createTRPCRouter({
|
export const workspaceRouter = createTRPCRouter({
|
||||||
@@ -22,9 +30,7 @@ export const workspaceRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.input(z.void())
|
.input(z.void())
|
||||||
.output(
|
.output(z.array(workspaceListItemSchema))
|
||||||
z.custom<Awaited<ReturnType<typeof workspaceRepo.getAllByUserId>>>(),
|
|
||||||
)
|
|
||||||
.query(async ({ ctx }) => {
|
.query(async ({ ctx }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -50,11 +56,7 @@ export const workspaceRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.input(z.object({ workspacePublicId: z.string().min(12) }))
|
.input(z.object({ workspacePublicId: z.string().min(12) }))
|
||||||
.output(
|
.output(workspaceDetailSchema)
|
||||||
z.custom<
|
|
||||||
Awaited<ReturnType<typeof workspaceRepo.getByPublicIdWithMembers>>
|
|
||||||
>(),
|
|
||||||
)
|
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -134,7 +136,7 @@ export const workspaceRouter = createTRPCRouter({
|
|||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
members: sanitizedMembers,
|
members: sanitizedMembers,
|
||||||
} as Awaited<ReturnType<typeof workspaceRepo.getByPublicIdWithMembers>>;
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -162,9 +164,7 @@ export const workspaceRouter = createTRPCRouter({
|
|||||||
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/),
|
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(
|
.output(workspaceWithBoardsSchema)
|
||||||
z.custom<Awaited<ReturnType<typeof workspaceRepo.getBySlugWithBoards>>>(),
|
|
||||||
)
|
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -211,7 +211,7 @@ export const workspaceRouter = createTRPCRouter({
|
|||||||
.optional(),
|
.optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(z.custom<Awaited<ReturnType<typeof workspaceRepo.create>>>())
|
.output(workspaceCreateResponseSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
const userEmail = ctx.user?.email;
|
const userEmail = ctx.user?.email;
|
||||||
@@ -270,7 +270,13 @@ export const workspaceRouter = createTRPCRouter({
|
|||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return {
|
||||||
|
publicId: result.publicId,
|
||||||
|
name: result.name!,
|
||||||
|
slug: result.slug!,
|
||||||
|
description: result.description ?? null,
|
||||||
|
plan: result.plan!,
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@@ -300,7 +306,7 @@ export const workspaceRouter = createTRPCRouter({
|
|||||||
.optional(),
|
.optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(z.custom<Awaited<ReturnType<typeof workspaceRepo.update>>>())
|
.output(workspaceUpdateResponseSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -383,7 +389,7 @@ export const workspaceRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.input(z.object({ workspacePublicId: z.string().min(12) }))
|
.input(z.object({ workspacePublicId: z.string().min(12) }))
|
||||||
.output(z.custom<Awaited<ReturnType<typeof workspaceRepo.hardDelete>>>())
|
.output(workspaceDeleteResponseSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -405,12 +411,12 @@ export const workspaceRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
await assertPermission(ctx.db, userId, workspace.id, "workspace:delete");
|
await assertPermission(ctx.db, userId, workspace.id, "workspace:delete");
|
||||||
|
|
||||||
const result = await workspaceRepo.hardDelete(
|
await workspaceRepo.hardDelete(
|
||||||
ctx.db,
|
ctx.db,
|
||||||
input.workspacePublicId,
|
input.workspacePublicId,
|
||||||
);
|
);
|
||||||
|
|
||||||
return result;
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
checkSlugAvailability: publicProcedure
|
checkSlugAvailability: publicProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@@ -515,6 +521,7 @@ export const workspaceRouter = createTRPCRouter({
|
|||||||
boardPublicId: z.string(),
|
boardPublicId: z.string(),
|
||||||
boardName: z.string(),
|
boardName: z.string(),
|
||||||
listName: z.string(),
|
listName: z.string(),
|
||||||
|
cardNumber: z.number().nullable(),
|
||||||
updatedAt: z.date().nullable(),
|
updatedAt: z.date().nullable(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
type: z.literal("card"),
|
type: z.literal("card"),
|
||||||
|
|||||||
12
packages/api/src/schemas/attachment.ts
Normal file
12
packages/api/src/schemas/attachment.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// ─── attachment.confirm ──────────────────────────────────────
|
||||||
|
export const attachmentConfirmResponseSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
filename: z.string(),
|
||||||
|
originalFilename: z.string(),
|
||||||
|
contentType: z.string(),
|
||||||
|
size: z.number(),
|
||||||
|
s3Key: z.string(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
});
|
||||||
130
packages/api/src/schemas/board.ts
Normal file
130
packages/api/src/schemas/board.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
checklistResponseSchema,
|
||||||
|
labelSchema,
|
||||||
|
workspaceMemberSchema,
|
||||||
|
} from "./common";
|
||||||
|
|
||||||
|
// ─── board.all ───────────────────────────────────────────────
|
||||||
|
export const boardListItemSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
favorite: z.boolean(),
|
||||||
|
lists: z.array(
|
||||||
|
z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
index: z.number(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
labels: z.array(labelSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Card sub-object inside board detail (byId) ─────────────
|
||||||
|
const boardCardMemberSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
email: z.string(),
|
||||||
|
user: z
|
||||||
|
.object({
|
||||||
|
name: z.string().nullable(),
|
||||||
|
email: z.string(),
|
||||||
|
image: z.string().nullable(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const boardDetailCardSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
index: z.number(),
|
||||||
|
dueDate: z.date().nullable(),
|
||||||
|
labels: z.array(labelSchema),
|
||||||
|
members: z.array(boardCardMemberSchema),
|
||||||
|
attachments: z.array(z.object({ publicId: z.string() })),
|
||||||
|
checklists: z.array(checklistResponseSchema),
|
||||||
|
comments: z.array(z.object({ publicId: z.string() })),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── board.byId ──────────────────────────────────────────────
|
||||||
|
export const boardDetailSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
visibility: z.string(),
|
||||||
|
isArchived: z.boolean(),
|
||||||
|
favorite: z.boolean(),
|
||||||
|
workspace: z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
members: z.array(workspaceMemberSchema),
|
||||||
|
}),
|
||||||
|
labels: z.array(labelSchema),
|
||||||
|
lists: z.array(
|
||||||
|
z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
index: z.number(),
|
||||||
|
cards: z.array(boardDetailCardSchema),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
allLists: z.array(
|
||||||
|
z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Card sub-object inside board detail (bySlug — no members) ─
|
||||||
|
const boardSlugCardSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
index: z.number(),
|
||||||
|
dueDate: z.date().nullable(),
|
||||||
|
labels: z.array(labelSchema),
|
||||||
|
attachments: z.array(z.object({ publicId: z.string() })),
|
||||||
|
checklists: z.array(checklistResponseSchema),
|
||||||
|
comments: z.array(z.object({ publicId: z.string() })),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── board.bySlug ────────────────────────────────────────────
|
||||||
|
export const boardBySlugSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
visibility: z.string(),
|
||||||
|
workspace: z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
}),
|
||||||
|
labels: z.array(labelSchema),
|
||||||
|
lists: z.array(
|
||||||
|
z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
index: z.number(),
|
||||||
|
cards: z.array(boardSlugCardSchema),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
allLists: z.array(
|
||||||
|
z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── board.create / board.createFromSnapshot ─────────────────
|
||||||
|
export const boardCreateResponseSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── board.update ────────────────────────────────────────────
|
||||||
|
export const boardUpdateResponseSchema = z.union([
|
||||||
|
z.object({ success: z.boolean() }),
|
||||||
|
z.object({ publicId: z.string(), name: z.string() }),
|
||||||
|
]);
|
||||||
216
packages/api/src/schemas/card.ts
Normal file
216
packages/api/src/schemas/card.ts
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
checklistResponseSchema,
|
||||||
|
labelSchema,
|
||||||
|
workspaceMemberSchema,
|
||||||
|
} from "./common";
|
||||||
|
|
||||||
|
// ─── card.create ─────────────────────────────────────────────
|
||||||
|
export const cardCreateResponseSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── card.update ─────────────────────────────────────────────
|
||||||
|
export const cardUpdateResponseSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
dueDate: z.date().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Comment responses ───────────────────────────────────────
|
||||||
|
export const commentResponseSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
comment: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const commentDeleteResponseSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── card.byId ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
const cardMemberSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
email: z.string(),
|
||||||
|
user: z
|
||||||
|
.object({
|
||||||
|
id: z.string().nullable(),
|
||||||
|
name: z.string().nullable(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const cardDetailSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
dueDate: z.date().nullable(),
|
||||||
|
createdBy: z.string().nullable(),
|
||||||
|
labels: z.array(labelSchema),
|
||||||
|
attachments: z.array(
|
||||||
|
z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
contentType: z.string(),
|
||||||
|
s3Key: z.string(),
|
||||||
|
originalFilename: z.string().nullable(),
|
||||||
|
size: z.number().nullable(),
|
||||||
|
url: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
checklists: z.array(checklistResponseSchema),
|
||||||
|
list: z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
board: z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
labels: z.array(labelSchema),
|
||||||
|
lists: z.array(
|
||||||
|
z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
workspace: z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
members: z.array(workspaceMemberSchema),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
members: z.array(cardMemberSchema),
|
||||||
|
activities: z.array(
|
||||||
|
z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
type: z.string(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
fromIndex: z.number().nullable(),
|
||||||
|
toIndex: z.number().nullable(),
|
||||||
|
fromTitle: z.string().nullable(),
|
||||||
|
toTitle: z.string().nullable(),
|
||||||
|
fromDescription: z.string().nullable(),
|
||||||
|
toDescription: z.string().nullable(),
|
||||||
|
fromDueDate: z.date().nullable(),
|
||||||
|
toDueDate: z.date().nullable(),
|
||||||
|
fromList: z
|
||||||
|
.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
index: z.number(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
toList: z
|
||||||
|
.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
index: z.number(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
label: z
|
||||||
|
.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
member: z
|
||||||
|
.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
user: z
|
||||||
|
.object({
|
||||||
|
name: z.string().nullable(),
|
||||||
|
email: z.string(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
user: z
|
||||||
|
.object({
|
||||||
|
name: z.string().nullable(),
|
||||||
|
email: z.string(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
comment: z
|
||||||
|
.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
comment: z.string(),
|
||||||
|
createdBy: z.string().nullable(),
|
||||||
|
updatedAt: z.date().nullable(),
|
||||||
|
deletedAt: z.date().nullable(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── card.getActivities ──────────────────────────────────────
|
||||||
|
export const activityItemSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
type: z.string(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
fromIndex: z.number().nullable(),
|
||||||
|
toIndex: z.number().nullable(),
|
||||||
|
fromTitle: z.string().nullable(),
|
||||||
|
toTitle: z.string().nullable(),
|
||||||
|
fromDescription: z.string().nullable(),
|
||||||
|
toDescription: z.string().nullable(),
|
||||||
|
fromDueDate: z.date().nullable(),
|
||||||
|
toDueDate: z.date().nullable(),
|
||||||
|
fromList: z
|
||||||
|
.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
index: z.number(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
toList: z
|
||||||
|
.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
index: z.number(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
label: z
|
||||||
|
.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
member: z
|
||||||
|
.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
user: z
|
||||||
|
.object({
|
||||||
|
id: z.string().nullable(),
|
||||||
|
name: z.string().nullable(),
|
||||||
|
email: z.string(),
|
||||||
|
image: z.string().nullable(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
user: z
|
||||||
|
.object({
|
||||||
|
id: z.string().nullable(),
|
||||||
|
name: z.string().nullable(),
|
||||||
|
email: z.string(),
|
||||||
|
image: z.string().nullable(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
comment: z
|
||||||
|
.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
comment: z.string(),
|
||||||
|
createdBy: z.string().nullable(),
|
||||||
|
updatedAt: z.date().nullable(),
|
||||||
|
deletedAt: z.date().nullable(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
attachment: z
|
||||||
|
.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
filename: z.string(),
|
||||||
|
originalFilename: z.string().nullable(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
});
|
||||||
40
packages/api/src/schemas/common.ts
Normal file
40
packages/api/src/schemas/common.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// Shared label schema used across board and card responses
|
||||||
|
export const labelSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
colourCode: z.string().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Shared checklist item schema
|
||||||
|
export const checklistItemResponseSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
title: z.string(),
|
||||||
|
completed: z.boolean(),
|
||||||
|
index: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Shared checklist schema with items
|
||||||
|
export const checklistResponseSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
index: z.number(),
|
||||||
|
items: z.array(checklistItemResponseSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Shared user sub-object (used inside member/workspace responses)
|
||||||
|
export const userSchema = z.object({
|
||||||
|
id: z.string().nullable(),
|
||||||
|
name: z.string().nullable(),
|
||||||
|
email: z.string(),
|
||||||
|
image: z.string().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Workspace member sub-object
|
||||||
|
export const workspaceMemberSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
email: z.string(),
|
||||||
|
status: z.string(),
|
||||||
|
user: userSchema.nullable(),
|
||||||
|
});
|
||||||
39
packages/api/src/schemas/index.ts
Normal file
39
packages/api/src/schemas/index.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
export {
|
||||||
|
boardListItemSchema,
|
||||||
|
boardDetailSchema,
|
||||||
|
boardBySlugSchema,
|
||||||
|
boardCreateResponseSchema,
|
||||||
|
boardUpdateResponseSchema,
|
||||||
|
} from "./board";
|
||||||
|
|
||||||
|
export {
|
||||||
|
cardCreateResponseSchema,
|
||||||
|
cardUpdateResponseSchema,
|
||||||
|
cardDetailSchema,
|
||||||
|
commentResponseSchema,
|
||||||
|
commentDeleteResponseSchema,
|
||||||
|
activityItemSchema,
|
||||||
|
} from "./card";
|
||||||
|
|
||||||
|
export {
|
||||||
|
labelSchema,
|
||||||
|
checklistItemResponseSchema,
|
||||||
|
checklistResponseSchema,
|
||||||
|
userSchema,
|
||||||
|
workspaceMemberSchema,
|
||||||
|
} from "./common";
|
||||||
|
|
||||||
|
export {
|
||||||
|
workspaceListItemSchema,
|
||||||
|
workspaceDetailSchema,
|
||||||
|
workspaceWithBoardsSchema,
|
||||||
|
workspaceCreateResponseSchema,
|
||||||
|
workspaceUpdateResponseSchema,
|
||||||
|
workspaceDeleteResponseSchema,
|
||||||
|
} from "./workspace";
|
||||||
|
|
||||||
|
export { listCreateResponseSchema, listUpdateResponseSchema } from "./list";
|
||||||
|
|
||||||
|
export { memberInviteResponseSchema } from "./member";
|
||||||
|
|
||||||
|
export { attachmentConfirmResponseSchema } from "./attachment";
|
||||||
13
packages/api/src/schemas/list.ts
Normal file
13
packages/api/src/schemas/list.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// ─── list.create ─────────────────────────────────────────────
|
||||||
|
export const listCreateResponseSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── list.update / list.reorder ──────────────────────────────
|
||||||
|
export const listUpdateResponseSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
});
|
||||||
6
packages/api/src/schemas/member.ts
Normal file
6
packages/api/src/schemas/member.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// ─── member.invite ───────────────────────────────────────────
|
||||||
|
export const memberInviteResponseSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
});
|
||||||
93
packages/api/src/schemas/workspace.ts
Normal file
93
packages/api/src/schemas/workspace.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { userSchema } from "./common";
|
||||||
|
|
||||||
|
// ─── workspace.all ───────────────────────────────────────────
|
||||||
|
export const workspaceListItemSchema = z.object({
|
||||||
|
role: z.string(),
|
||||||
|
workspace: z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
slug: z.string(),
|
||||||
|
plan: z.enum(["free", "team", "pro", "enterprise"]),
|
||||||
|
weekStartDay: z.number().nullable(),
|
||||||
|
deletedAt: z.date().nullable(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── workspace.byId ──────────────────────────────────────────
|
||||||
|
const workspaceMemberDetailSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
email: z.string().optional(),
|
||||||
|
role: z.string(),
|
||||||
|
status: z.string(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
user: z
|
||||||
|
.object({
|
||||||
|
id: z.string().nullable(),
|
||||||
|
name: z.string().nullable(),
|
||||||
|
email: z.string().optional(),
|
||||||
|
image: z.string().nullable(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const workspaceSubscriptionSchema = z.object({
|
||||||
|
plan: z.string(),
|
||||||
|
status: z.string(),
|
||||||
|
seats: z.number().nullable(),
|
||||||
|
unlimitedSeats: z.boolean().nullable(),
|
||||||
|
periodStart: z.date().nullable(),
|
||||||
|
periodEnd: z.date().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const workspaceDetailSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
showEmailsToMembers: z.boolean().nullable(),
|
||||||
|
weekStartDay: z.number().nullable(),
|
||||||
|
members: z.array(workspaceMemberDetailSchema),
|
||||||
|
subscriptions: z.array(workspaceSubscriptionSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── workspace.bySlug ────────────────────────────────────────
|
||||||
|
export const workspaceWithBoardsSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
slug: z.string(),
|
||||||
|
boards: z.array(
|
||||||
|
z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── workspace.create ────────────────────────────────────────
|
||||||
|
export const workspaceCreateResponseSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
plan: z.enum(["free", "team", "pro", "enterprise"]),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── workspace.update ────────────────────────────────────────
|
||||||
|
export const workspaceUpdateResponseSchema = z.object({
|
||||||
|
publicId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
plan: z.enum(["free", "team", "pro", "enterprise"]),
|
||||||
|
showEmailsToMembers: z.boolean().nullable(),
|
||||||
|
weekStartDay: z.number().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── workspace.delete ────────────────────────────────────────
|
||||||
|
export const workspaceDeleteResponseSchema = z.object({
|
||||||
|
success: z.boolean(),
|
||||||
|
});
|
||||||
@@ -20,8 +20,11 @@ export function withApiLogging(
|
|||||||
const requestId = randomUUID();
|
const requestId = randomUUID();
|
||||||
const route = req.url?.split("?")[0] ?? "unknown";
|
const route = req.url?.split("?")[0] ?? "unknown";
|
||||||
const input = {
|
const input = {
|
||||||
...(req.query && Object.keys(req.query).length > 0 && { query: req.query }),
|
...(req.query &&
|
||||||
...(req.body && typeof req.body === "object" && Object.keys(req.body).length > 0 && { body: req.body }),
|
Object.keys(req.query).length > 0 && { query: req.query }),
|
||||||
|
...(req.body &&
|
||||||
|
typeof req.body === "object" &&
|
||||||
|
Object.keys(req.body).length > 0 && { body: req.body }),
|
||||||
};
|
};
|
||||||
|
|
||||||
let statusCode = 200;
|
let statusCode = 200;
|
||||||
@@ -41,7 +44,16 @@ export function withApiLogging(
|
|||||||
// unauthenticated or auth unavailable
|
// unauthenticated or auth unavailable
|
||||||
}
|
}
|
||||||
|
|
||||||
await handler(req, res);
|
let handlerError: unknown;
|
||||||
|
try {
|
||||||
|
await handler(req, res);
|
||||||
|
} catch (err) {
|
||||||
|
handlerError = err;
|
||||||
|
statusCode = 500;
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.status(500).json({ error: "Internal server error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const duration = Date.now() - start;
|
const duration = Date.now() - start;
|
||||||
const meta = {
|
const meta = {
|
||||||
@@ -53,6 +65,10 @@ export function withApiLogging(
|
|||||||
...(isCloud && email && { email }),
|
...(isCloud && email && { email }),
|
||||||
...(Object.keys(input).length > 0 && { input }),
|
...(Object.keys(input).length > 0 && { input }),
|
||||||
status: statusCode,
|
status: statusCode,
|
||||||
|
...(handlerError instanceof Error && {
|
||||||
|
error: handlerError.message,
|
||||||
|
stack: handlerError.stack,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (statusCode < 400) {
|
if (statusCode < 400) {
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
|
|
||||||
|
const { mockLogger } = vi.hoisted(() => ({
|
||||||
|
mockLogger: {
|
||||||
|
error: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("@kan/db/repository/webhook.repo", () => ({
|
vi.mock("@kan/db/repository/webhook.repo", () => ({
|
||||||
getActiveByWorkspaceId: vi.fn(),
|
getActiveByWorkspaceId: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("@kan/logger", () => ({
|
||||||
|
createLogger: vi.fn(() => mockLogger),
|
||||||
|
}));
|
||||||
|
|
||||||
import * as webhookRepo from "@kan/db/repository/webhook.repo";
|
import * as webhookRepo from "@kan/db/repository/webhook.repo";
|
||||||
import {
|
import {
|
||||||
sendWebhookToUrl,
|
sendWebhookToUrl,
|
||||||
@@ -155,6 +166,33 @@ describe("webhook utilities", () => {
|
|||||||
title: { from: "Old Title", to: "Updated Title" },
|
title: { from: "Old Title", to: "Updated Title" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preserves public list IDs in moved payloads", () => {
|
||||||
|
const payload = createCardWebhookPayload(
|
||||||
|
"card.moved",
|
||||||
|
{
|
||||||
|
id: "card-123",
|
||||||
|
title: "Moved Card",
|
||||||
|
listId: "list-public-done",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
boardId: "board-789",
|
||||||
|
listName: "Done",
|
||||||
|
changes: {
|
||||||
|
listId: { from: "list-public-backlog", to: "list-public-done" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(payload.data.card.listId).toBe("list-public-done");
|
||||||
|
expect(payload.data.list).toEqual({
|
||||||
|
id: "list-public-done",
|
||||||
|
name: "Done",
|
||||||
|
});
|
||||||
|
expect(payload.data.changes).toEqual({
|
||||||
|
listId: { from: "list-public-backlog", to: "list-public-done" },
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("sendWebhookToUrl", () => {
|
describe("sendWebhookToUrl", () => {
|
||||||
@@ -434,8 +472,6 @@ describe("webhook utilities", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("continues sending to other webhooks when one fails", async () => {
|
it("continues sending to other webhooks when one fails", async () => {
|
||||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
||||||
|
|
||||||
mockGetActiveByWorkspaceId.mockResolvedValueOnce([
|
mockGetActiveByWorkspaceId.mockResolvedValueOnce([
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -462,11 +498,15 @@ describe("webhook utilities", () => {
|
|||||||
await sendWebhooksForWorkspace(mockDb, 1, mockPayload);
|
await sendWebhooksForWorkspace(mockDb, 1, mockPayload);
|
||||||
|
|
||||||
expect(global.fetch).toHaveBeenCalledTimes(2);
|
expect(global.fetch).toHaveBeenCalledTimes(2);
|
||||||
expect(consoleSpy).toHaveBeenCalledWith(
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||||
expect.stringContaining("Webhook delivery failed"),
|
expect.objectContaining({
|
||||||
|
url: "https://example.com/webhook1",
|
||||||
|
event: "card.created",
|
||||||
|
error: "500 Error",
|
||||||
|
statusCode: 500,
|
||||||
|
}),
|
||||||
|
"Webhook delivery failed",
|
||||||
);
|
);
|
||||||
|
|
||||||
consoleSpy.mockRestore();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles empty webhook list", async () => {
|
it("handles empty webhook list", async () => {
|
||||||
@@ -478,9 +518,6 @@ describe("webhook utilities", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("catches and logs DB errors without throwing", async () => {
|
it("catches and logs DB errors without throwing", async () => {
|
||||||
const consoleSpy = vi
|
|
||||||
.spyOn(console, "error")
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
mockGetActiveByWorkspaceId.mockRejectedValueOnce(
|
mockGetActiveByWorkspaceId.mockRejectedValueOnce(
|
||||||
new Error("DB connection failed"),
|
new Error("DB connection failed"),
|
||||||
);
|
);
|
||||||
@@ -490,12 +527,13 @@ describe("webhook utilities", () => {
|
|||||||
sendWebhooksForWorkspace(mockDb, 1, mockPayload),
|
sendWebhooksForWorkspace(mockDb, 1, mockPayload),
|
||||||
).resolves.toBeUndefined();
|
).resolves.toBeUndefined();
|
||||||
|
|
||||||
expect(consoleSpy).toHaveBeenCalledWith(
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||||
"Failed to send webhooks for workspace:",
|
expect.objectContaining({
|
||||||
expect.any(Error),
|
err: expect.any(Error),
|
||||||
|
workspaceId: 1,
|
||||||
|
}),
|
||||||
|
"Failed to send webhooks for workspace",
|
||||||
);
|
);
|
||||||
|
|
||||||
consoleSpy.mockRestore();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ export function createPlugins(db: dbClient) {
|
|||||||
if (authorization?.startsWith("Bearer ")) {
|
if (authorization?.startsWith("Bearer ")) {
|
||||||
return authorization.slice(7);
|
return authorization.slice(7);
|
||||||
}
|
}
|
||||||
return ctx.headers?.get("x-api-key") ?? undefined;
|
return ctx.headers?.get("x-api-key") ?? null;
|
||||||
},
|
},
|
||||||
rateLimit: {
|
rateLimit: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -92,14 +92,6 @@ export const socialProvidersPlugin = () => ({
|
|||||||
const providers = ctx.context.socialProviders.map((p) =>
|
const providers = ctx.context.socialProviders.map((p) =>
|
||||||
p.id.toLowerCase(),
|
p.id.toLowerCase(),
|
||||||
);
|
);
|
||||||
// Add OIDC provider if configured
|
|
||||||
if (
|
|
||||||
process.env.OIDC_CLIENT_ID &&
|
|
||||||
process.env.OIDC_CLIENT_SECRET &&
|
|
||||||
process.env.OIDC_DISCOVERY_URL
|
|
||||||
) {
|
|
||||||
providers.push("oidc");
|
|
||||||
}
|
|
||||||
return ctx.json(providers);
|
return ctx.json(providers);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
47
packages/db/migrations/20260421220939_AddCardNumber.sql
Normal file
47
packages/db/migrations/20260421220939_AddCardNumber.sql
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
ALTER TABLE "card" ADD COLUMN "cardNumber" integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE "workspace" ADD COLUMN "cardPrefix" varchar(10) DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "workspace" ADD COLUMN "cardCounter" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "card_list_number_idx" ON "card" USING btree ("listId","cardNumber");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "workspace_card_prefix_idx" ON "workspace" USING btree ("cardPrefix");--> statement-breakpoint
|
||||||
|
|
||||||
|
-- Populate cardPrefix for existing workspaces from their name
|
||||||
|
UPDATE "workspace"
|
||||||
|
SET "cardPrefix" = (
|
||||||
|
SELECT CASE
|
||||||
|
WHEN array_length(words, 1) = 1 THEN UPPER(LEFT(words[1], 3))
|
||||||
|
ELSE UPPER(LEFT(array_to_string(ARRAY(
|
||||||
|
SELECT LEFT(w, 1) FROM unnest(words) AS w WHERE w != ''
|
||||||
|
), ''), 4))
|
||||||
|
END
|
||||||
|
FROM (
|
||||||
|
SELECT regexp_split_to_array(trim("name"), '\s+') AS words
|
||||||
|
) sub
|
||||||
|
)
|
||||||
|
WHERE "cardPrefix" = '';--> statement-breakpoint
|
||||||
|
|
||||||
|
-- Assign sequential cardNumber to existing cards (including soft-deleted),
|
||||||
|
-- ordered by createdAt, scoped to workspace. Deleted cards still receive a
|
||||||
|
-- number so future un-archive/restore flows can keep their ticket ID.
|
||||||
|
WITH numbered AS (
|
||||||
|
SELECT
|
||||||
|
c.id,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY b."workspaceId" ORDER BY c."createdAt", c.id) AS rn
|
||||||
|
FROM "card" c
|
||||||
|
JOIN "list" l ON c."listId" = l.id
|
||||||
|
JOIN "board" b ON l."boardId" = b.id
|
||||||
|
WHERE c."cardNumber" IS NULL
|
||||||
|
)
|
||||||
|
UPDATE "card" c
|
||||||
|
SET "cardNumber" = n.rn
|
||||||
|
FROM numbered n
|
||||||
|
WHERE c.id = n.id;--> statement-breakpoint
|
||||||
|
|
||||||
|
-- Update cardCounter on each workspace to the max cardNumber assigned
|
||||||
|
UPDATE "workspace" w
|
||||||
|
SET "cardCounter" = COALESCE((
|
||||||
|
SELECT MAX(c."cardNumber")
|
||||||
|
FROM "card" c
|
||||||
|
JOIN "list" l ON c."listId" = l.id
|
||||||
|
JOIN "board" b ON l."boardId" = b.id
|
||||||
|
WHERE b."workspaceId" = w.id AND c."cardNumber" IS NOT NULL
|
||||||
|
), 0);
|
||||||
3927
packages/db/migrations/meta/20260421220939_snapshot.json
Normal file
3927
packages/db/migrations/meta/20260421220939_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -225,6 +225,13 @@
|
|||||||
"when": 1775170588247,
|
"when": 1775170588247,
|
||||||
"tag": "20260402225628_AddTeamWorkspacePlan",
|
"tag": "20260402225628_AddTeamWorkspacePlan",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 32,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1776809379931,
|
||||||
|
"tag": "20260421220939_AddCardNumber",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -209,6 +209,7 @@ export const getByPublicId = async (
|
|||||||
workspace: {
|
workspace: {
|
||||||
columns: {
|
columns: {
|
||||||
publicId: true,
|
publicId: true,
|
||||||
|
cardPrefix: true,
|
||||||
},
|
},
|
||||||
with: {
|
with: {
|
||||||
members: {
|
members: {
|
||||||
@@ -220,6 +221,7 @@ export const getByPublicId = async (
|
|||||||
with: {
|
with: {
|
||||||
user: {
|
user: {
|
||||||
columns: {
|
columns: {
|
||||||
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
email: true,
|
email: true,
|
||||||
image: true,
|
image: true,
|
||||||
@@ -254,6 +256,7 @@ export const getByPublicId = async (
|
|||||||
listId: true,
|
listId: true,
|
||||||
index: true,
|
index: true,
|
||||||
dueDate: true,
|
dueDate: true,
|
||||||
|
cardNumber: true,
|
||||||
},
|
},
|
||||||
with: {
|
with: {
|
||||||
labels: {
|
labels: {
|
||||||
@@ -278,6 +281,7 @@ export const getByPublicId = async (
|
|||||||
with: {
|
with: {
|
||||||
user: {
|
user: {
|
||||||
columns: {
|
columns: {
|
||||||
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
email: true,
|
email: true,
|
||||||
image: true,
|
image: true,
|
||||||
@@ -422,6 +426,7 @@ export const getBySlug = async (
|
|||||||
publicId: true,
|
publicId: true,
|
||||||
name: true,
|
name: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
|
cardPrefix: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
labels: {
|
labels: {
|
||||||
@@ -448,6 +453,7 @@ export const getBySlug = async (
|
|||||||
listId: true,
|
listId: true,
|
||||||
index: true,
|
index: true,
|
||||||
dueDate: true,
|
dueDate: true,
|
||||||
|
cardNumber: true,
|
||||||
},
|
},
|
||||||
with: {
|
with: {
|
||||||
labels: {
|
labels: {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
labels,
|
labels,
|
||||||
lists,
|
lists,
|
||||||
workspaceMembers,
|
workspaceMembers,
|
||||||
|
workspaces,
|
||||||
} from "@kan/db/schema";
|
} from "@kan/db/schema";
|
||||||
import { generateUID } from "@kan/shared/utils";
|
import { generateUID } from "@kan/shared/utils";
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ export const create = async (
|
|||||||
description: string;
|
description: string;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
listId: number;
|
listId: number;
|
||||||
|
workspaceId: number;
|
||||||
position: "start" | "end";
|
position: "start" | "end";
|
||||||
dueDate?: Date | null;
|
dueDate?: Date | null;
|
||||||
},
|
},
|
||||||
@@ -82,6 +84,17 @@ export const create = async (
|
|||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [counterResult] = await tx
|
||||||
|
.update(workspaces)
|
||||||
|
.set({ cardCounter: sql`${workspaces.cardCounter} + 1` })
|
||||||
|
.where(eq(workspaces.id, cardInput.workspaceId))
|
||||||
|
.returning({ cardCounter: workspaces.cardCounter });
|
||||||
|
|
||||||
|
if (!counterResult)
|
||||||
|
throw new Error(`Workspace ${cardInput.workspaceId} not found`);
|
||||||
|
|
||||||
|
const cardNumber = counterResult.cardCounter;
|
||||||
|
|
||||||
const result = await tx
|
const result = await tx
|
||||||
.insert(cards)
|
.insert(cards)
|
||||||
.values({
|
.values({
|
||||||
@@ -91,9 +104,10 @@ export const create = async (
|
|||||||
createdBy: cardInput.createdBy,
|
createdBy: cardInput.createdBy,
|
||||||
listId: cardInput.listId,
|
listId: cardInput.listId,
|
||||||
index: index,
|
index: index,
|
||||||
|
cardNumber,
|
||||||
dueDate: cardInput.dueDate ?? null,
|
dueDate: cardInput.dueDate ?? null,
|
||||||
})
|
})
|
||||||
.returning({ id: cards.id, listId: cards.listId, publicId: cards.publicId });
|
.returning({ id: cards.id, listId: cards.listId, publicId: cards.publicId, cardNumber: cards.cardNumber });
|
||||||
|
|
||||||
if (!result[0]) throw new Error("Unable to create card");
|
if (!result[0]) throw new Error("Unable to create card");
|
||||||
|
|
||||||
@@ -241,6 +255,14 @@ export const getByPublicId = (db: dbClient, cardPublicId: string) => {
|
|||||||
listId: true,
|
listId: true,
|
||||||
dueDate: true,
|
dueDate: true,
|
||||||
},
|
},
|
||||||
|
with: {
|
||||||
|
list: {
|
||||||
|
columns: {
|
||||||
|
publicId: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
where: eq(cards.publicId, cardPublicId),
|
where: eq(cards.publicId, cardPublicId),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -265,6 +287,7 @@ export const bulkCreate = async (
|
|||||||
description: string;
|
description: string;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
listId: number;
|
listId: number;
|
||||||
|
workspaceId: number;
|
||||||
index: number;
|
index: number;
|
||||||
importId?: number;
|
importId?: number;
|
||||||
}[],
|
}[],
|
||||||
@@ -280,6 +303,34 @@ export const bulkCreate = async (
|
|||||||
byList.set(item.listId, arr);
|
byList.set(item.listId, arr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Atomically reserve a contiguous range of cardNumbers per workspace by
|
||||||
|
// bumping cardCounter once per workspace.
|
||||||
|
const countsByWorkspace = new Map<number, number>();
|
||||||
|
for (const item of cardInput) {
|
||||||
|
countsByWorkspace.set(
|
||||||
|
item.workspaceId,
|
||||||
|
(countsByWorkspace.get(item.workspaceId) ?? 0) + 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardNumberByWorkspaceQueue = new Map<number, number[]>();
|
||||||
|
for (const [workspaceId, count] of countsByWorkspace.entries()) {
|
||||||
|
const [counterResult] = await tx
|
||||||
|
.update(workspaces)
|
||||||
|
.set({ cardCounter: sql`${workspaces.cardCounter} + ${count}` })
|
||||||
|
.where(eq(workspaces.id, workspaceId))
|
||||||
|
.returning({ cardCounter: workspaces.cardCounter });
|
||||||
|
|
||||||
|
if (!counterResult)
|
||||||
|
throw new Error(`Workspace ${workspaceId} not found`);
|
||||||
|
|
||||||
|
const last = counterResult.cardCounter;
|
||||||
|
const start = last - count + 1;
|
||||||
|
const queue: number[] = [];
|
||||||
|
for (let n = start; n <= last; n++) queue.push(n);
|
||||||
|
cardNumberByWorkspaceQueue.set(workspaceId, queue);
|
||||||
|
}
|
||||||
|
|
||||||
const allValuesToInsert: {
|
const allValuesToInsert: {
|
||||||
publicId: string;
|
publicId: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -287,6 +338,7 @@ export const bulkCreate = async (
|
|||||||
createdBy: string;
|
createdBy: string;
|
||||||
listId: number;
|
listId: number;
|
||||||
index: number;
|
index: number;
|
||||||
|
cardNumber: number;
|
||||||
importId?: number;
|
importId?: number;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
@@ -301,6 +353,12 @@ export const bulkCreate = async (
|
|||||||
let nextIndex = last ? last.index + 1 : 0;
|
let nextIndex = last ? last.index + 1 : 0;
|
||||||
const sorted = [...items].sort((a, b) => a.index - b.index);
|
const sorted = [...items].sort((a, b) => a.index - b.index);
|
||||||
for (const it of sorted) {
|
for (const it of sorted) {
|
||||||
|
const queue = cardNumberByWorkspaceQueue.get(it.workspaceId);
|
||||||
|
const cardNumber = queue?.shift();
|
||||||
|
if (cardNumber === undefined)
|
||||||
|
throw new Error(
|
||||||
|
`Failed to allocate cardNumber for workspace ${it.workspaceId}`,
|
||||||
|
);
|
||||||
allValuesToInsert.push({
|
allValuesToInsert.push({
|
||||||
publicId: it.publicId,
|
publicId: it.publicId,
|
||||||
title: it.title,
|
title: it.title,
|
||||||
@@ -308,6 +366,7 @@ export const bulkCreate = async (
|
|||||||
createdBy: it.createdBy,
|
createdBy: it.createdBy,
|
||||||
listId: it.listId,
|
listId: it.listId,
|
||||||
index: nextIndex++,
|
index: nextIndex++,
|
||||||
|
cardNumber,
|
||||||
importId: it.importId,
|
importId: it.importId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -426,6 +485,7 @@ export const getWithListAndMembersByPublicId = async (
|
|||||||
description: true,
|
description: true,
|
||||||
dueDate: true,
|
dueDate: true,
|
||||||
createdBy: true,
|
createdBy: true,
|
||||||
|
cardNumber: true,
|
||||||
},
|
},
|
||||||
with: {
|
with: {
|
||||||
labels: {
|
labels: {
|
||||||
@@ -502,6 +562,7 @@ export const getWithListAndMembersByPublicId = async (
|
|||||||
workspace: {
|
workspace: {
|
||||||
columns: {
|
columns: {
|
||||||
publicId: true,
|
publicId: true,
|
||||||
|
cardPrefix: true,
|
||||||
},
|
},
|
||||||
with: {
|
with: {
|
||||||
members: {
|
members: {
|
||||||
@@ -945,7 +1006,7 @@ export const getWorkspaceAndCardIdByCardPublicId = async (
|
|||||||
where: and(eq(cards.publicId, cardPublicId), isNull(cards.deletedAt)),
|
where: and(eq(cards.publicId, cardPublicId), isNull(cards.deletedAt)),
|
||||||
with: {
|
with: {
|
||||||
list: {
|
list: {
|
||||||
columns: { name: true },
|
columns: { name: true, publicId: true },
|
||||||
with: {
|
with: {
|
||||||
board: {
|
board: {
|
||||||
columns: {
|
columns: {
|
||||||
@@ -966,6 +1027,7 @@ export const getWorkspaceAndCardIdByCardPublicId = async (
|
|||||||
createdBy: result.createdBy,
|
createdBy: result.createdBy,
|
||||||
workspaceId: result.list.board.workspaceId,
|
workspaceId: result.list.board.workspaceId,
|
||||||
workspaceVisibility: result.list.board.visibility,
|
workspaceVisibility: result.list.board.visibility,
|
||||||
|
listPublicId: result.list.publicId,
|
||||||
listName: result.list.name,
|
listName: result.list.name,
|
||||||
boardPublicId: result.list.board.publicId,
|
boardPublicId: result.list.board.publicId,
|
||||||
boardName: result.list.board.name,
|
boardName: result.list.board.name,
|
||||||
|
|||||||
@@ -209,10 +209,12 @@ export const getByPublicId = async (db: dbClient, listPublicId: string) => {
|
|||||||
return db.query.lists.findFirst({
|
return db.query.lists.findFirst({
|
||||||
columns: {
|
columns: {
|
||||||
id: true,
|
id: true,
|
||||||
|
publicId: true,
|
||||||
|
name: true,
|
||||||
boardId: true,
|
boardId: true,
|
||||||
index: true,
|
index: true,
|
||||||
},
|
},
|
||||||
where: eq(lists.publicId, listPublicId),
|
where: and(eq(lists.publicId, listPublicId), isNull(lists.deletedAt)),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -433,6 +435,7 @@ export const getWorkspaceAndListIdByListPublicId = async (
|
|||||||
return result
|
return result
|
||||||
? {
|
? {
|
||||||
id: result.id,
|
id: result.id,
|
||||||
|
publicId: listPublicId,
|
||||||
name: result.name,
|
name: result.name,
|
||||||
createdBy: result.createdBy,
|
createdBy: result.createdBy,
|
||||||
workspaceId: result.board.workspaceId,
|
workspaceId: result.board.workspaceId,
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import {
|
import {
|
||||||
and,
|
and,
|
||||||
|
asc,
|
||||||
count,
|
count,
|
||||||
desc,
|
desc,
|
||||||
eq,
|
eq,
|
||||||
ilike,
|
ilike,
|
||||||
inArray,
|
inArray,
|
||||||
isNull,
|
isNull,
|
||||||
asc,
|
|
||||||
or,
|
or,
|
||||||
sql,
|
sql,
|
||||||
} from "drizzle-orm";
|
} from "drizzle-orm";
|
||||||
|
|
||||||
import type { dbClient } from "@kan/db/client";
|
import type { dbClient } from "@kan/db/client";
|
||||||
|
import type { Permission, Role } from "@kan/shared";
|
||||||
import {
|
import {
|
||||||
boards,
|
boards,
|
||||||
cards,
|
cards,
|
||||||
@@ -19,8 +20,11 @@ import {
|
|||||||
workspaceMembers,
|
workspaceMembers,
|
||||||
workspaces,
|
workspaces,
|
||||||
} from "@kan/db/schema";
|
} from "@kan/db/schema";
|
||||||
import type { Permission, Role } from "@kan/shared";
|
import {
|
||||||
import { generateUID, getDefaultPermissions } from "@kan/shared";
|
generateUID,
|
||||||
|
generateWorkspacePrefix,
|
||||||
|
getDefaultPermissions,
|
||||||
|
} from "@kan/shared";
|
||||||
|
|
||||||
import * as permissionRepo from "./permission.repo";
|
import * as permissionRepo from "./permission.repo";
|
||||||
|
|
||||||
@@ -30,22 +34,22 @@ const SYSTEM_ROLES: {
|
|||||||
description: string;
|
description: string;
|
||||||
hierarchyLevel: number;
|
hierarchyLevel: number;
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
name: "admin",
|
name: "admin",
|
||||||
description: "Full access to all workspace features",
|
description: "Full access to all workspace features",
|
||||||
hierarchyLevel: 100,
|
hierarchyLevel: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "member",
|
name: "member",
|
||||||
description: "Standard member with create and edit permissions",
|
description: "Standard member with create and edit permissions",
|
||||||
hierarchyLevel: 50,
|
hierarchyLevel: 50,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "guest",
|
name: "guest",
|
||||||
description: "View-only access",
|
description: "View-only access",
|
||||||
hierarchyLevel: 10,
|
hierarchyLevel: 10,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const getCount = async (db: dbClient) => {
|
export const getCount = async (db: dbClient) => {
|
||||||
const result = await db
|
const result = await db
|
||||||
@@ -75,8 +79,12 @@ export const create = async (
|
|||||||
name: workspaceInput.name,
|
name: workspaceInput.name,
|
||||||
slug: workspaceInput.slug,
|
slug: workspaceInput.slug,
|
||||||
createdBy: workspaceInput.createdBy,
|
createdBy: workspaceInput.createdBy,
|
||||||
...(workspaceInput.description && { description: workspaceInput.description }),
|
...(workspaceInput.description && {
|
||||||
|
description: workspaceInput.description,
|
||||||
|
}),
|
||||||
...(workspaceInput.plan && { plan: workspaceInput.plan }),
|
...(workspaceInput.plan && { plan: workspaceInput.plan }),
|
||||||
|
cardPrefix: generateWorkspacePrefix(workspaceInput.name),
|
||||||
|
cardCounter: 0,
|
||||||
})
|
})
|
||||||
.returning({
|
.returning({
|
||||||
id: workspaces.id,
|
id: workspaces.id,
|
||||||
@@ -258,8 +266,12 @@ export const getBySlugWithBoards = (db: dbClient, workspaceSlug: string) => {
|
|||||||
slug: true,
|
slug: true,
|
||||||
name: true,
|
name: true,
|
||||||
},
|
},
|
||||||
where: and(isNull(boards.deletedAt), eq(boards.visibility, "public"), eq(boards.isArchived, false)),
|
where: and(
|
||||||
orderBy: [asc(boards.name)]
|
isNull(boards.deletedAt),
|
||||||
|
eq(boards.visibility, "public"),
|
||||||
|
eq(boards.isArchived, false),
|
||||||
|
),
|
||||||
|
orderBy: [asc(boards.name)],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
where: and(
|
where: and(
|
||||||
@@ -283,6 +295,7 @@ export const getAllByUserId = async (db: dbClient, userId: string) => {
|
|||||||
slug: true,
|
slug: true,
|
||||||
plan: true,
|
plan: true,
|
||||||
weekStartDay: true,
|
weekStartDay: true,
|
||||||
|
cardPrefix: true,
|
||||||
deletedAt: true,
|
deletedAt: true,
|
||||||
},
|
},
|
||||||
// https://github.com/drizzle-team/drizzle-orm/issues/2903
|
// https://github.com/drizzle-team/drizzle-orm/issues/2903
|
||||||
@@ -365,6 +378,14 @@ export const isUserInWorkspace = async (
|
|||||||
return result?.id !== undefined;
|
return result?.id !== undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const parseTicketId = (
|
||||||
|
query: string,
|
||||||
|
): { prefix: string; number: number } | null => {
|
||||||
|
const match = /^([A-Za-z0-9]{1,10})-(\d+)$/.exec(query);
|
||||||
|
if (!match) return null;
|
||||||
|
return { prefix: match[1]!.toUpperCase(), number: parseInt(match[2]!, 10) };
|
||||||
|
};
|
||||||
|
|
||||||
export const searchBoardsAndCards = async (
|
export const searchBoardsAndCards = async (
|
||||||
db: dbClient,
|
db: dbClient,
|
||||||
workspaceId: number,
|
workspaceId: number,
|
||||||
@@ -373,6 +394,8 @@ export const searchBoardsAndCards = async (
|
|||||||
) => {
|
) => {
|
||||||
const searchQuery = `%${query}%`;
|
const searchQuery = `%${query}%`;
|
||||||
|
|
||||||
|
const ticketId = parseTicketId(query.trim());
|
||||||
|
|
||||||
// Search for boards
|
// Search for boards
|
||||||
const boardResults = await db
|
const boardResults = await db
|
||||||
.select({
|
.select({
|
||||||
@@ -400,25 +423,19 @@ export const searchBoardsAndCards = async (
|
|||||||
sql`similarity(${boards.name}, ${query}) DESC`,
|
sql`similarity(${boards.name}, ${query}) DESC`,
|
||||||
desc(boards.updatedAt),
|
desc(boards.updatedAt),
|
||||||
)
|
)
|
||||||
.limit(Math.ceil(limit * 0.4));
|
.limit(ticketId ? 0 : Math.ceil(limit * 0.4));
|
||||||
|
|
||||||
// Search for cards
|
// Search for cards by ticket ID or by title
|
||||||
const cardResults = await db
|
const cardWhereConditions = ticketId
|
||||||
.select({
|
? and(
|
||||||
publicId: cards.publicId,
|
eq(boards.workspaceId, workspaceId),
|
||||||
title: cards.title,
|
eq(cards.cardNumber, ticketId.number),
|
||||||
description: cards.description,
|
ilike(workspaces.cardPrefix, ticketId.prefix),
|
||||||
boardPublicId: boards.publicId,
|
isNull(cards.deletedAt),
|
||||||
boardName: boards.name,
|
isNull(lists.deletedAt),
|
||||||
listName: lists.name,
|
isNull(boards.deletedAt),
|
||||||
updatedAt: cards.updatedAt,
|
)
|
||||||
createdAt: cards.createdAt,
|
: and(
|
||||||
})
|
|
||||||
.from(cards)
|
|
||||||
.innerJoin(lists, eq(cards.listId, lists.id))
|
|
||||||
.innerJoin(boards, eq(lists.boardId, boards.id))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(boards.workspaceId, workspaceId),
|
eq(boards.workspaceId, workspaceId),
|
||||||
or(
|
or(
|
||||||
ilike(cards.title, searchQuery),
|
ilike(cards.title, searchQuery),
|
||||||
@@ -427,19 +444,46 @@ export const searchBoardsAndCards = async (
|
|||||||
isNull(cards.deletedAt),
|
isNull(cards.deletedAt),
|
||||||
isNull(lists.deletedAt),
|
isNull(lists.deletedAt),
|
||||||
isNull(boards.deletedAt),
|
isNull(boards.deletedAt),
|
||||||
),
|
);
|
||||||
)
|
|
||||||
|
const cardResults = await db
|
||||||
|
.select({
|
||||||
|
publicId: cards.publicId,
|
||||||
|
title: cards.title,
|
||||||
|
description: cards.description,
|
||||||
|
boardPublicId: boards.publicId,
|
||||||
|
boardName: boards.name,
|
||||||
|
listName: lists.name,
|
||||||
|
cardNumber: cards.cardNumber,
|
||||||
|
updatedAt: cards.updatedAt,
|
||||||
|
createdAt: cards.createdAt,
|
||||||
|
})
|
||||||
|
.from(cards)
|
||||||
|
.innerJoin(lists, eq(cards.listId, lists.id))
|
||||||
|
.innerJoin(boards, eq(lists.boardId, boards.id))
|
||||||
|
.innerJoin(workspaces, eq(boards.workspaceId, workspaces.id))
|
||||||
|
.where(cardWhereConditions)
|
||||||
.orderBy(
|
.orderBy(
|
||||||
sql`CASE WHEN ${cards.title} ILIKE ${searchQuery} THEN 1 ELSE 0 END DESC`,
|
...(ticketId
|
||||||
sql`similarity(${cards.title}, ${query}) DESC`,
|
? [desc(cards.createdAt)]
|
||||||
desc(cards.updatedAt),
|
: [
|
||||||
|
sql`CASE WHEN ${cards.title} ILIKE ${searchQuery} THEN 1 ELSE 0 END DESC`,
|
||||||
|
sql`similarity(${cards.title}, ${query}) DESC`,
|
||||||
|
desc(cards.updatedAt),
|
||||||
|
]),
|
||||||
)
|
)
|
||||||
.limit(Math.floor(limit * 0.6));
|
.limit(ticketId ? limit : Math.floor(limit * 0.6));
|
||||||
|
|
||||||
// Combine results
|
// Combine results
|
||||||
const allResults = [
|
const allResults = [
|
||||||
...boardResults.map((board) => ({ ...board, type: "board" as const })),
|
...boardResults.map((board) => ({
|
||||||
...cardResults.map((card) => ({ ...card, type: "card" as const })),
|
...board,
|
||||||
|
type: "board" as const,
|
||||||
|
})),
|
||||||
|
...cardResults.map((card) => ({
|
||||||
|
...card,
|
||||||
|
type: "card" as const,
|
||||||
|
})),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Ensure we don't exceed the total limit
|
// Ensure we don't exceed the total limit
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { relations } from "drizzle-orm";
|
|||||||
import {
|
import {
|
||||||
bigint,
|
bigint,
|
||||||
bigserial,
|
bigserial,
|
||||||
|
index,
|
||||||
integer,
|
integer,
|
||||||
pgEnum,
|
pgEnum,
|
||||||
pgTable,
|
pgTable,
|
||||||
@@ -54,27 +55,36 @@ export type ActivityType = (typeof activityTypes)[number];
|
|||||||
|
|
||||||
export const activityTypeEnum = pgEnum("card_activity_type", activityTypes);
|
export const activityTypeEnum = pgEnum("card_activity_type", activityTypes);
|
||||||
|
|
||||||
export const cards = pgTable("card", {
|
export const cards = pgTable(
|
||||||
id: bigserial("id", { mode: "number" }).primaryKey(),
|
"card",
|
||||||
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
|
{
|
||||||
title: text("title").notNull(),
|
id: bigserial("id", { mode: "number" }).primaryKey(),
|
||||||
description: text("description"),
|
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
|
||||||
index: integer("index").notNull(),
|
title: text("title").notNull(),
|
||||||
createdBy: uuid("createdBy").references(() => users.id, {
|
description: text("description"),
|
||||||
onDelete: "set null",
|
index: integer("index").notNull(),
|
||||||
}),
|
cardNumber: integer("cardNumber"),
|
||||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
createdBy: uuid("createdBy").references(() => users.id, {
|
||||||
updatedAt: timestamp("updatedAt"),
|
onDelete: "set null",
|
||||||
deletedAt: timestamp("deletedAt"),
|
}),
|
||||||
deletedBy: uuid("deletedBy").references(() => users.id, {
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||||
onDelete: "set null",
|
updatedAt: timestamp("updatedAt"),
|
||||||
}),
|
deletedAt: timestamp("deletedAt"),
|
||||||
listId: bigint("listId", { mode: "number" })
|
deletedBy: uuid("deletedBy").references(() => users.id, {
|
||||||
.notNull()
|
onDelete: "set null",
|
||||||
.references(() => lists.id, { onDelete: "cascade" }),
|
}),
|
||||||
importId: bigint("importId", { mode: "number" }).references(() => imports.id),
|
listId: bigint("listId", { mode: "number" })
|
||||||
dueDate: timestamp("dueDate"),
|
.notNull()
|
||||||
}).enableRLS();
|
.references(() => lists.id, { onDelete: "cascade" }),
|
||||||
|
importId: bigint("importId", { mode: "number" }).references(
|
||||||
|
() => imports.id,
|
||||||
|
),
|
||||||
|
dueDate: timestamp("dueDate"),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index("card_list_number_idx").on(table.listId, table.cardNumber),
|
||||||
|
],
|
||||||
|
).enableRLS();
|
||||||
|
|
||||||
export const cardsRelations = relations(cards, ({ one, many }) => ({
|
export const cardsRelations = relations(cards, ({ one, many }) => ({
|
||||||
createdBy: one(users, {
|
createdBy: one(users, {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
bigint,
|
bigint,
|
||||||
bigserial,
|
bigserial,
|
||||||
boolean,
|
boolean,
|
||||||
|
index,
|
||||||
integer,
|
integer,
|
||||||
pgEnum,
|
pgEnum,
|
||||||
pgTable,
|
pgTable,
|
||||||
@@ -38,25 +39,31 @@ export const workspacePlans = ["free", "team", "pro", "enterprise"] as const;
|
|||||||
export type WorkspacePlan = (typeof workspacePlans)[number];
|
export type WorkspacePlan = (typeof workspacePlans)[number];
|
||||||
export const workspacePlanEnum = pgEnum("workspace_plan", workspacePlans);
|
export const workspacePlanEnum = pgEnum("workspace_plan", workspacePlans);
|
||||||
|
|
||||||
export const workspaces = pgTable("workspace", {
|
export const workspaces = pgTable(
|
||||||
id: bigserial("id", { mode: "number" }).primaryKey(),
|
"workspace",
|
||||||
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
|
{
|
||||||
name: varchar("name", { length: 255 }).notNull(),
|
id: bigserial("id", { mode: "number" }).primaryKey(),
|
||||||
description: text("description"),
|
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
|
||||||
slug: varchar("slug", { length: 255 }).notNull().unique(),
|
name: varchar("name", { length: 255 }).notNull(),
|
||||||
plan: workspacePlanEnum("plan").notNull().default("free"),
|
description: text("description"),
|
||||||
showEmailsToMembers: boolean("showEmailsToMembers").notNull().default(true),
|
slug: varchar("slug", { length: 255 }).notNull().unique(),
|
||||||
weekStartDay: integer("weekStartDay").notNull().default(1),
|
plan: workspacePlanEnum("plan").notNull().default("free"),
|
||||||
createdBy: uuid("createdBy").references(() => users.id, {
|
showEmailsToMembers: boolean("showEmailsToMembers").notNull().default(true),
|
||||||
onDelete: "set null",
|
weekStartDay: integer("weekStartDay").notNull().default(1),
|
||||||
}),
|
cardPrefix: varchar("cardPrefix", { length: 10 }).notNull().default(""),
|
||||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
cardCounter: integer("cardCounter").notNull().default(0),
|
||||||
updatedAt: timestamp("updatedAt"),
|
createdBy: uuid("createdBy").references(() => users.id, {
|
||||||
deletedAt: timestamp("deletedAt"),
|
onDelete: "set null",
|
||||||
deletedBy: uuid("deletedBy").references(() => users.id, {
|
}),
|
||||||
onDelete: "set null",
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||||
}),
|
updatedAt: timestamp("updatedAt"),
|
||||||
}).enableRLS();
|
deletedAt: timestamp("deletedAt"),
|
||||||
|
deletedBy: uuid("deletedBy").references(() => users.id, {
|
||||||
|
onDelete: "set null",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
(table) => [index("workspace_card_prefix_idx").on(table.cardPrefix)],
|
||||||
|
).enableRLS();
|
||||||
|
|
||||||
export const workspaceRelations = relations(workspaces, ({ one, many }) => ({
|
export const workspaceRelations = relations(workspaces, ({ one, many }) => ({
|
||||||
user: one(users, {
|
user: one(users, {
|
||||||
|
|||||||
26
packages/shared/src/utils/generateWorkspacePrefix.ts
Normal file
26
packages/shared/src/utils/generateWorkspacePrefix.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
export const generateWorkspacePrefix = (name: string): string => {
|
||||||
|
const words = name
|
||||||
|
.trim()
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter((w) => w.length > 0);
|
||||||
|
|
||||||
|
if (words.length === 0) return "WS";
|
||||||
|
|
||||||
|
const firstWord = words[0];
|
||||||
|
if (words.length === 1 && firstWord) {
|
||||||
|
const word = firstWord.replace(/[^a-zA-Z0-9]/g, "").toUpperCase();
|
||||||
|
return word.slice(0, 3) || "WS";
|
||||||
|
}
|
||||||
|
|
||||||
|
const initials = words
|
||||||
|
.map((w) => {
|
||||||
|
const cleaned = w.replace(/[^a-zA-Z0-9]/g, "");
|
||||||
|
return cleaned.length > 0 ? cleaned[0]! : "";
|
||||||
|
})
|
||||||
|
.filter((c) => c.length > 0)
|
||||||
|
.join("")
|
||||||
|
.toUpperCase()
|
||||||
|
.slice(0, 4);
|
||||||
|
|
||||||
|
return initials || "WS";
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from "./generateUID";
|
export * from "./generateUID";
|
||||||
export * from "./generateSlug";
|
export * from "./generateSlug";
|
||||||
|
export * from "./generateWorkspacePrefix";
|
||||||
export * from "./subscriptions";
|
export * from "./subscriptions";
|
||||||
export * from "./email";
|
export * from "./email";
|
||||||
export * from "./dueDateFilters";
|
export * from "./dueDateFilters";
|
||||||
|
|||||||
Reference in New Issue
Block a user