Compare commits
16 Commits
refactor/w
...
fix/update
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b60c48838d | ||
|
|
c0962e9f70 | ||
|
|
3e6aec2d6b | ||
|
|
4658b2961c | ||
|
|
bc8b5463b3 | ||
|
|
9eee59a877 | ||
|
|
e32e400976 | ||
|
|
b4b07f3120 | ||
|
|
e39faa1172 | ||
|
|
b46979f539 | ||
|
|
f4237fffe6 | ||
|
|
1d9bb96367 | ||
|
|
bcb449e394 | ||
|
|
ae4bc77407 | ||
|
|
6056167063 | ||
|
|
adf91c7dfd |
@@ -238,9 +238,9 @@ checksums:
|
||||
ZXSPJt/message: 1b5749b0cca6a62d75a577bee6804074
|
||||
ZXSPJt/origin/0/0: 0627e0040ca4939c9a57349e98c51797
|
||||
ZXSPJt/translation: 1b5749b0cca6a62d75a577bee6804074
|
||||
YkfDoz/translation: 5d4f1c56f4e9e579ec9967ca68cc64ac
|
||||
YkfDoz/message: 5d4f1c56f4e9e579ec9967ca68cc64ac
|
||||
YkfDoz/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
||||
YkfDoz/translation: 5d4f1c56f4e9e579ec9967ca68cc64ac
|
||||
3bqt9U/message: 2366ed295eb2c03c425559c24cb31606
|
||||
3bqt9U/origin/0/0: 8628ddb6f4ded25c0f17e2cbda8207d3
|
||||
3bqt9U/translation: 2366ed295eb2c03c425559c24cb31606
|
||||
@@ -322,9 +322,9 @@ checksums:
|
||||
1rWxEw/message: 122c108794f321c10e05a08f09b11457
|
||||
1rWxEw/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
||||
1rWxEw/translation: 122c108794f321c10e05a08f09b11457
|
||||
iH8pgl/translation: f541015a827e37cb3b1234e56bc2aa3c
|
||||
iH8pgl/message: f541015a827e37cb3b1234e56bc2aa3c
|
||||
iH8pgl/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||
iH8pgl/translation: f541015a827e37cb3b1234e56bc2aa3c
|
||||
KNKCTb/message: 35919fd5b10ad212839a75e72829fe4e
|
||||
KNKCTb/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
||||
KNKCTb/translation: 35919fd5b10ad212839a75e72829fe4e
|
||||
@@ -343,9 +343,9 @@ checksums:
|
||||
UoSI%2Bi/message: 47f1051a134e111930be7f94179df4c8
|
||||
UoSI%2Bi/origin/0/0: fadd5a5b6963ec5f261072f7e8d2140b
|
||||
UoSI%2Bi/translation: 47f1051a134e111930be7f94179df4c8
|
||||
zt%2F6cS/translation: aadb69a7f7421be6bb022fbde08a6d77
|
||||
zt%2F6cS/message: aadb69a7f7421be6bb022fbde08a6d77
|
||||
zt%2F6cS/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
||||
zt%2F6cS/translation: aadb69a7f7421be6bb022fbde08a6d77
|
||||
qaS%2B1%2F/message: 4e25347caf65e1b63bec90e101b86ddd
|
||||
qaS%2B1%2F/origin/0/0: fadd5a5b6963ec5f261072f7e8d2140b
|
||||
qaS%2B1%2F/translation: 4e25347caf65e1b63bec90e101b86ddd
|
||||
@@ -372,8 +372,7 @@ checksums:
|
||||
QD8opX/origin/2/0: faa8a2576972b4eb31565d8ea6098f43
|
||||
QD8opX/translation: 101ff39aab674c033c15ab978d0d8bac
|
||||
YhvWXb/message: 336a8c70a91adc49c15266167ee84cc0
|
||||
YhvWXb/origin/0/0: ff4a160eca5f8da4f075fd1dd6061fa6
|
||||
YhvWXb/origin/1/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
YhvWXb/origin/0/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
YhvWXb/translation: 336a8c70a91adc49c15266167ee84cc0
|
||||
i0ZMQl/message: f739058266a2cb6a3d7cca14de09c69a
|
||||
i0ZMQl/origin/0/0: 0ad2af981579a62b084b3d41462c5546
|
||||
@@ -593,9 +592,9 @@ checksums:
|
||||
EH8IL3/origin/0/0: c0b59968b2b234167c488a05cc38710b
|
||||
EH8IL3/origin/1/0: fadd5a5b6963ec5f261072f7e8d2140b
|
||||
EH8IL3/translation: 6f79129c8f08ee54d858a2af57d16dd9
|
||||
zMquA7/translation: 38ff8d574865bb71d2f620bc4ba98c20
|
||||
zMquA7/message: 38ff8d574865bb71d2f620bc4ba98c20
|
||||
zMquA7/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
||||
zMquA7/translation: 38ff8d574865bb71d2f620bc4ba98c20
|
||||
VHS0aJ/message: 31d0962985c83a29558c98a765bb1b16
|
||||
VHS0aJ/origin/0/0: 15792234f408027822ee71ccddde6f9b
|
||||
VHS0aJ/translation: 31d0962985c83a29558c98a765bb1b16
|
||||
@@ -629,9 +628,8 @@ checksums:
|
||||
dtQIWT/origin/0/0: c0b59968b2b234167c488a05cc38710b
|
||||
dtQIWT/translation: 9d2308d578ec21bd22374e880fe3bd3c
|
||||
AUYALh/message: ee2b0671e00972773210c5be5a9ccb89
|
||||
AUYALh/origin/0/0: ff4a160eca5f8da4f075fd1dd6061fa6
|
||||
AUYALh/origin/1/0: d77eb1f90f2fa86a55181629d88d237a
|
||||
AUYALh/origin/2/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
AUYALh/origin/0/0: d77eb1f90f2fa86a55181629d88d237a
|
||||
AUYALh/origin/1/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
AUYALh/translation: ee2b0671e00972773210c5be5a9ccb89
|
||||
VbyRUy/message: 502237cd1044491c48494aa60967a985
|
||||
VbyRUy/origin/0/0: d77eb1f90f2fa86a55181629d88d237a
|
||||
@@ -701,10 +699,10 @@ checksums:
|
||||
AX4ktp/message: 4e6be22ade0188f408471325fe363575
|
||||
AX4ktp/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
||||
AX4ktp/translation: 4e6be22ade0188f408471325fe363575
|
||||
xGVfLh/translation: 3cfba90b4600131e82fc4260c568d044
|
||||
xGVfLh/message: 3cfba90b4600131e82fc4260c568d044
|
||||
xGVfLh/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
||||
xGVfLh/origin/1/0: 69d8634b177fdf937764b406fd477547
|
||||
xGVfLh/translation: 3cfba90b4600131e82fc4260c568d044
|
||||
J2Wbjt/message: 8ed03cf7c5e60a6edf3470a4558ff058
|
||||
J2Wbjt/origin/0/0: d96af8a06c2e38a72238bcd16e426859
|
||||
J2Wbjt/translation: 8ed03cf7c5e60a6edf3470a4558ff058
|
||||
@@ -736,6 +734,9 @@ checksums:
|
||||
uvH2xS/message: ecc2617544a6d385d18678577fa112fd
|
||||
uvH2xS/origin/0/0: 97bd206a05414e2a571020d64e166093
|
||||
uvH2xS/translation: ecc2617544a6d385d18678577fa112fd
|
||||
7mrkyO/translation: d3853b58e9b736eefb0f9bf400daf4d7
|
||||
7mrkyO/message: d3853b58e9b736eefb0f9bf400daf4d7
|
||||
7mrkyO/origin/0/0: 1b05beba6bdd908015fa8e798717f024
|
||||
9PaIxv/message: da95932e7a1465a5d21aa3b855a46dc2
|
||||
9PaIxv/origin/0/0: c0b59968b2b234167c488a05cc38710b
|
||||
9PaIxv/translation: da95932e7a1465a5d21aa3b855a46dc2
|
||||
@@ -832,12 +833,11 @@ checksums:
|
||||
7RpHg%2F/origin/0/0: c0b59968b2b234167c488a05cc38710b
|
||||
7RpHg%2F/origin/1/0: c0b59968b2b234167c488a05cc38710b
|
||||
7RpHg%2F/translation: 66f702512197d25cfdb117f2415ecd88
|
||||
ZDPJBc/translation: f9154d0c02a7db907f0a33fb5edd4be1
|
||||
ZDPJBc/message: f9154d0c02a7db907f0a33fb5edd4be1
|
||||
ZDPJBc/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||
ZDPJBc/translation: f9154d0c02a7db907f0a33fb5edd4be1
|
||||
j9IZZ0/message: 7ba841d0946eb04fa3d17d74365be37b
|
||||
j9IZZ0/origin/0/0: ff4a160eca5f8da4f075fd1dd6061fa6
|
||||
j9IZZ0/origin/1/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
j9IZZ0/origin/0/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
j9IZZ0/translation: 7ba841d0946eb04fa3d17d74365be37b
|
||||
OwneeH/message: e97476d280d4ebfd196d6c5336f3511f
|
||||
OwneeH/origin/0/0: fadd5a5b6963ec5f261072f7e8d2140b
|
||||
@@ -1049,9 +1049,9 @@ checksums:
|
||||
bFREhu/message: b2cc9a311fc28d1594252f697c7f5342
|
||||
bFREhu/origin/0/0: 97bd206a05414e2a571020d64e166093
|
||||
bFREhu/translation: b2cc9a311fc28d1594252f697c7f5342
|
||||
SUXBxp/translation: 76e841ae8ec3340d7d0a5c2191abbea4
|
||||
SUXBxp/message: 76e841ae8ec3340d7d0a5c2191abbea4
|
||||
SUXBxp/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||
SUXBxp/translation: 76e841ae8ec3340d7d0a5c2191abbea4
|
||||
0%2BewPp/message: 785fe23c0eef0a5b60b5b2a88151de31
|
||||
0%2BewPp/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
||||
0%2BewPp/origin/1/0: eb63312c63f6c8d2a5b6520c89012123
|
||||
@@ -1332,9 +1332,9 @@ checksums:
|
||||
csFbe%2B/message: e4e58e33d637282d141df466d729bc7c
|
||||
csFbe%2B/origin/0/0: 94d3e20c29bb517dd394711d7524e478
|
||||
csFbe%2B/translation: e4e58e33d637282d141df466d729bc7c
|
||||
SUvm1Y/translation: 6c0f26526f77e9abb92fb2b1106a66db
|
||||
SUvm1Y/message: 6c0f26526f77e9abb92fb2b1106a66db
|
||||
SUvm1Y/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
||||
SUvm1Y/translation: 6c0f26526f77e9abb92fb2b1106a66db
|
||||
cdyS7J/message: 0f10050aeed2555a34fe3c27ce616607
|
||||
cdyS7J/origin/0/0: c0b59968b2b234167c488a05cc38710b
|
||||
cdyS7J/origin/1/0: c0b59968b2b234167c488a05cc38710b
|
||||
@@ -1380,6 +1380,9 @@ checksums:
|
||||
2Py5S%2F/message: fe1183a7439299210d8ab7379b8f4d01
|
||||
2Py5S%2F/origin/0/0: 83911e3eacbad4583e2b1647a784d154
|
||||
2Py5S%2F/translation: fe1183a7439299210d8ab7379b8f4d01
|
||||
VOwhhz/translation: 613f43526ea128b2749f70878f10008c
|
||||
VOwhhz/message: 613f43526ea128b2749f70878f10008c
|
||||
VOwhhz/origin/0/0: 1b05beba6bdd908015fa8e798717f024
|
||||
iwr7AP/message: 27c24b56895defecd10b0171b39bc93f
|
||||
iwr7AP/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
||||
iwr7AP/translation: 27c24b56895defecd10b0171b39bc93f
|
||||
@@ -1509,9 +1512,8 @@ checksums:
|
||||
k7rCa%2F/origin/0/0: 3de48fb04f7df9bdbe399911cdb3fe86
|
||||
k7rCa%2F/translation: 890b7851af05f97ac53644e4154c13b4
|
||||
8Is1ih/message: 624592ba2ecf647a3fe60d493c1ee71a
|
||||
8Is1ih/origin/0/0: ff4a160eca5f8da4f075fd1dd6061fa6
|
||||
8Is1ih/origin/1/0: fadd5a5b6963ec5f261072f7e8d2140b
|
||||
8Is1ih/origin/2/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
8Is1ih/origin/0/0: fadd5a5b6963ec5f261072f7e8d2140b
|
||||
8Is1ih/origin/1/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
8Is1ih/translation: 624592ba2ecf647a3fe60d493c1ee71a
|
||||
EEHmPC/message: 8c1be80f5b2fcc112922282afbe30287
|
||||
EEHmPC/origin/0/0: 7493592314bc3b96ad96127a949ba1ba
|
||||
@@ -1595,9 +1597,9 @@ checksums:
|
||||
jl3aEJ/placeholders/0/0: 06f23520ebabfffed4999b5bcf61c9cd
|
||||
jl3aEJ/origin/0/0: 1d6b5f88cdad160a67842f6b65387448
|
||||
jl3aEJ/translation: 4c38799ff25321ea25017bf4cd8e2e4f
|
||||
vo2a%2Ba/translation: fcf0f06f8b64b458c7ca6d95541a3cc8
|
||||
vo2a%2Ba/message: fcf0f06f8b64b458c7ca6d95541a3cc8
|
||||
vo2a%2Ba/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||
vo2a%2Ba/translation: fcf0f06f8b64b458c7ca6d95541a3cc8
|
||||
agPptk/message: 43983f2178fb28aacaef6c5f41282c4a
|
||||
agPptk/origin/0/0: 3de48fb04f7df9bdbe399911cdb3fe86
|
||||
agPptk/translation: 43983f2178fb28aacaef6c5f41282c4a
|
||||
@@ -1868,9 +1870,9 @@ checksums:
|
||||
8RdLDU/message: d7820b1bf4efecc61ed89234567aaa9c
|
||||
8RdLDU/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
||||
8RdLDU/translation: d7820b1bf4efecc61ed89234567aaa9c
|
||||
Oi%2BJ%2BN/translation: 313804fe615d51e0f0cbd599161d1ade
|
||||
Oi%2BJ%2BN/message: 313804fe615d51e0f0cbd599161d1ade
|
||||
Oi%2BJ%2BN/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
||||
Oi%2BJ%2BN/translation: 313804fe615d51e0f0cbd599161d1ade
|
||||
Iqh0Uv/message: 0cc9dcd6ac624383e9977f66d4b3b777
|
||||
Iqh0Uv/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
||||
Iqh0Uv/origin/1/0: eb63312c63f6c8d2a5b6520c89012123
|
||||
@@ -1964,8 +1966,9 @@ checksums:
|
||||
fuwKpE/origin/1/0: fd0e66cef8d46e96bcf3143a12f9b6db
|
||||
fuwKpE/origin/2/0: fc0c18a095a68277a0f90a115942683b
|
||||
fuwKpE/origin/3/0: 1b05beba6bdd908015fa8e798717f024
|
||||
fuwKpE/origin/4/0: 9c8b544f5db130cd7c5eda9064ba3957
|
||||
fuwKpE/origin/5/0: faa8a2576972b4eb31565d8ea6098f43
|
||||
fuwKpE/origin/4/0: 1b05beba6bdd908015fa8e798717f024
|
||||
fuwKpE/origin/5/0: 9c8b544f5db130cd7c5eda9064ba3957
|
||||
fuwKpE/origin/6/0: faa8a2576972b4eb31565d8ea6098f43
|
||||
fuwKpE/translation: 346a18a54564881c888b95873d03df44
|
||||
nYNMZZ/message: d5a8a1b38eaf02d02cd6ac2211b42a92
|
||||
nYNMZZ/origin/0/0: 97bd206a05414e2a571020d64e166093
|
||||
@@ -2191,9 +2194,9 @@ checksums:
|
||||
Eit43O/message: b5d9a1edd988f455041aee9fa7fb3d63
|
||||
Eit43O/origin/0/0: 1d6b5f88cdad160a67842f6b65387448
|
||||
Eit43O/translation: b5d9a1edd988f455041aee9fa7fb3d63
|
||||
JjEJSy/translation: 0154ff378b3f4329871378229e269fc7
|
||||
JjEJSy/message: 0154ff378b3f4329871378229e269fc7
|
||||
JjEJSy/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||
JjEJSy/translation: 0154ff378b3f4329871378229e269fc7
|
||||
Tz0i8g/message: 8df6777277469c1fd88cc18dde2f1cc3
|
||||
Tz0i8g/origin/0/0: 77c9f6139024b2069e4fdc5120f69cb5
|
||||
Tz0i8g/origin/1/0: 94d3e20c29bb517dd394711d7524e478
|
||||
@@ -2228,10 +2231,10 @@ checksums:
|
||||
n1ekoW/message: ec7b8f314fe9bc6591006707484ede61
|
||||
n1ekoW/origin/0/0: 6cda8b872e4902cbc0191d9375adf4ef
|
||||
n1ekoW/translation: ec7b8f314fe9bc6591006707484ede61
|
||||
fcWrnU/translation: 6d842546a84e919c519bf8cbffdf25a0
|
||||
fcWrnU/message: 6d842546a84e919c519bf8cbffdf25a0
|
||||
fcWrnU/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
||||
fcWrnU/origin/1/0: 69d8634b177fdf937764b406fd477547
|
||||
fcWrnU/translation: 6d842546a84e919c519bf8cbffdf25a0
|
||||
mErq7F/message: 0dd2ae69be4618c1f9e615774a4509ca
|
||||
mErq7F/origin/0/0: 6cda8b872e4902cbc0191d9375adf4ef
|
||||
mErq7F/translation: 0dd2ae69be4618c1f9e615774a4509ca
|
||||
@@ -2267,9 +2270,9 @@ checksums:
|
||||
wIb7Ng/message: 67659097ef6bcb194839858b2c4f3a3c
|
||||
wIb7Ng/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
||||
wIb7Ng/translation: 67659097ef6bcb194839858b2c4f3a3c
|
||||
6sKjjx/translation: 50861c0881fe87a77412417e5ffb3fbb
|
||||
6sKjjx/message: 50861c0881fe87a77412417e5ffb3fbb
|
||||
6sKjjx/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
||||
6sKjjx/translation: 50861c0881fe87a77412417e5ffb3fbb
|
||||
vnS6Rf/message: 003f7f93d44b0f5c2e40bf24e7af3bd7
|
||||
vnS6Rf/origin/0/0: c0b59968b2b234167c488a05cc38710b
|
||||
vnS6Rf/translation: 003f7f93d44b0f5c2e40bf24e7af3bd7
|
||||
@@ -2309,9 +2312,9 @@ checksums:
|
||||
D%2BNlUC/message: 803281c327a02a2feba182f4a2b38311
|
||||
D%2BNlUC/origin/0/0: 358ea1b5babb2e9ccbf1a4c9f89dd3a5
|
||||
D%2BNlUC/translation: 803281c327a02a2feba182f4a2b38311
|
||||
KM6m8p/translation: c621ea9404a37af289def443b309bf1b
|
||||
KM6m8p/message: c621ea9404a37af289def443b309bf1b
|
||||
KM6m8p/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
||||
KM6m8p/translation: c621ea9404a37af289def443b309bf1b
|
||||
bff61F/message: 3f0ae9c914f16ceb9a0d7db868f4aa20
|
||||
bff61F/origin/0/0: 8628ddb6f4ded25c0f17e2cbda8207d3
|
||||
bff61F/origin/1/0: 7493592314bc3b96ad96127a949ba1ba
|
||||
@@ -2347,6 +2350,14 @@ checksums:
|
||||
NnH3pK/message: 82a46f197779af4dde92e7e5f9336ccf
|
||||
NnH3pK/origin/0/0: dc1a240d2d0cccb102c67ca311d4e457
|
||||
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/origin/0/0: f64cd62b620f454998523af2a4330278
|
||||
itgYbC/origin/1/0: dc1a240d2d0cccb102c67ca311d4e457
|
||||
@@ -2417,12 +2428,12 @@ checksums:
|
||||
vlxQFL/message: 730f33b8f1fc002c4f393ccd262b6664
|
||||
vlxQFL/origin/0/0: afee530613110e248ffde6b56872ebed
|
||||
vlxQFL/translation: 730f33b8f1fc002c4f393ccd262b6664
|
||||
HQVm6e/translation: ec66b543a593e9dabf47de5979892d8d
|
||||
HQVm6e/message: ec66b543a593e9dabf47de5979892d8d
|
||||
HQVm6e/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||
gKmoow/translation: 83997c77cb0dc8c8c90f7ab706aa817f
|
||||
HQVm6e/translation: ec66b543a593e9dabf47de5979892d8d
|
||||
gKmoow/message: 83997c77cb0dc8c8c90f7ab706aa817f
|
||||
gKmoow/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||
gKmoow/translation: 83997c77cb0dc8c8c90f7ab706aa817f
|
||||
OAYToL/message: 1c989752736fa41ecdd68ea890f16a15
|
||||
OAYToL/origin/0/0: 15792234f408027822ee71ccddde6f9b
|
||||
OAYToL/translation: 1c989752736fa41ecdd68ea890f16a15
|
||||
@@ -2441,6 +2452,9 @@ checksums:
|
||||
kp6L3T/message: b7eadb89c615874f416d9658d0428c4c
|
||||
kp6L3T/origin/0/0: 30791e26a172d47d3cb0693e82679246
|
||||
kp6L3T/translation: b7eadb89c615874f416d9658d0428c4c
|
||||
pEb95p/translation: 1d4c2a0ec7e4c707a4470fa4262b495d
|
||||
pEb95p/message: 1d4c2a0ec7e4c707a4470fa4262b495d
|
||||
pEb95p/origin/0/0: 1b05beba6bdd908015fa8e798717f024
|
||||
MHrjPM/message: 344e64395eaff6822a57d18623853e1a
|
||||
MHrjPM/origin/0/0: a50c4fbde8da6e0a77c4e0aef0b44dea
|
||||
MHrjPM/translation: 344e64395eaff6822a57d18623853e1a
|
||||
@@ -2488,6 +2502,9 @@ checksums:
|
||||
LeM5RY/message: dfeac280858332082ad34d2623a59863
|
||||
LeM5RY/origin/0/0: 0dccc39d0debbf3e089cb6d292de762b
|
||||
LeM5RY/translation: dfeac280858332082ad34d2623a59863
|
||||
5MPY04/translation: 661f2e21a85fbe1281b141a8041c8778
|
||||
5MPY04/message: 661f2e21a85fbe1281b141a8041c8778
|
||||
5MPY04/origin/0/0: 1b05beba6bdd908015fa8e798717f024
|
||||
W1ewR0/message: ddcac5d14d48358b043a9487b42eec16
|
||||
W1ewR0/origin/0/0: fc0c18a095a68277a0f90a115942683b
|
||||
W1ewR0/origin/1/0: 1b05beba6bdd908015fa8e798717f024
|
||||
@@ -2507,6 +2524,9 @@ checksums:
|
||||
YcyP2z/origin/0/0: 3161dd30737e7d849d4ccc30a28bada9
|
||||
YcyP2z/origin/1/0: 3161dd30737e7d849d4ccc30a28bada9
|
||||
YcyP2z/translation: 7495f1434c9188849bffd015811d5337
|
||||
c1vOQR/message: 640ab5162eed616a7235d68376a016ed
|
||||
c1vOQR/origin/0/0: f64cd62b620f454998523af2a4330278
|
||||
c1vOQR/translation: 640ab5162eed616a7235d68376a016ed
|
||||
MxQXhL/message: c1c949477e121d73ed6683eddf9cd874
|
||||
MxQXhL/origin/0/0: ff4a160eca5f8da4f075fd1dd6061fa6
|
||||
MxQXhL/origin/1/0: 69d8634b177fdf937764b406fd477547
|
||||
@@ -2526,6 +2546,9 @@ checksums:
|
||||
2QGEbi/message: 550198b2c87f06726a843c79c1026ed9
|
||||
2QGEbi/origin/0/0: 4b4f55c033add8db69178563832227c6
|
||||
2QGEbi/translation: 550198b2c87f06726a843c79c1026ed9
|
||||
23nvb2/message: b9dedce59b5594ee2a88bf5d55d6dfac
|
||||
23nvb2/origin/0/0: a1fde3fcce608870830f168f027727ff
|
||||
23nvb2/translation: b9dedce59b5594ee2a88bf5d55d6dfac
|
||||
Oo4E6p/message: af985cf72c47df76e2f2139a427d9948
|
||||
Oo4E6p/origin/0/0: 97bd206a05414e2a571020d64e166093
|
||||
Oo4E6p/translation: af985cf72c47df76e2f2139a427d9948
|
||||
@@ -2544,9 +2567,13 @@ checksums:
|
||||
A8jqN7/message: 656c93265d7e2bef1245b17d85f85ae5
|
||||
A8jqN7/origin/0/0: 1fd8ccf586705354bcae6e1186108fc7
|
||||
A8jqN7/translation: 656c93265d7e2bef1245b17d85f85ae5
|
||||
Q60WUZ/translation: 4284c33aed2f2c5e004ed0a7d7d9bf87
|
||||
Q60WUZ/message: 4284c33aed2f2c5e004ed0a7d7d9bf87
|
||||
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/origin/0/0: 0ad2af981579a62b084b3d41462c5546
|
||||
T7Mywq/translation: 62899897d7bffcb1df3af77d460f0881
|
||||
@@ -2589,6 +2616,9 @@ checksums:
|
||||
TFv5tM/message: 4f2240aeff6f7275feb33ca3f608e48c
|
||||
TFv5tM/origin/0/0: 7493592314bc3b96ad96127a949ba1ba
|
||||
TFv5tM/translation: 4f2240aeff6f7275feb33ca3f608e48c
|
||||
V1o9Jo/message: 7b212f1c2fdc370974f859b08349b0c6
|
||||
V1o9Jo/origin/0/0: f64cd62b620f454998523af2a4330278
|
||||
V1o9Jo/translation: 7b212f1c2fdc370974f859b08349b0c6
|
||||
It4%2FFS/message: ae1527c2765592f13980acb66997e570
|
||||
It4%2FFS/origin/0/0: 0ad2af981579a62b084b3d41462c5546
|
||||
It4%2FFS/translation: ae1527c2765592f13980acb66997e570
|
||||
@@ -2633,15 +2663,14 @@ checksums:
|
||||
7Ufoyg/origin/0/0: 9737a9cef0646ff4bbd942b1924b9a2c
|
||||
7Ufoyg/translation: 5c418701b27b9acbc5dd199719007362
|
||||
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/2/0: c0b59968b2b234167c488a05cc38710b
|
||||
mx8%2B1u/origin/3/0: fadd5a5b6963ec5f261072f7e8d2140b
|
||||
mx8%2B1u/origin/4/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
mx8%2B1u/origin/2/0: fadd5a5b6963ec5f261072f7e8d2140b
|
||||
mx8%2B1u/origin/3/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
mx8%2B1u/translation: f2949ca2dc18b0063af92e60779abb65
|
||||
8AwlaR/translation: acb1fdc14ed875815ae9f8a5254bc634
|
||||
8AwlaR/message: acb1fdc14ed875815ae9f8a5254bc634
|
||||
8AwlaR/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
||||
8AwlaR/translation: acb1fdc14ed875815ae9f8a5254bc634
|
||||
Ws%2B3X%2B/message: 905b10eeb09fdc5ac7fc757e935ee757
|
||||
Ws%2B3X%2B/origin/0/0: fadd5a5b6963ec5f261072f7e8d2140b
|
||||
Ws%2B3X%2B/translation: 905b10eeb09fdc5ac7fc757e935ee757
|
||||
@@ -2681,8 +2710,7 @@ checksums:
|
||||
IelE1h/origin/2/0: 5f6c4a0c9be5098099167871ae1564af
|
||||
IelE1h/translation: 972773025e763ddf53273df6d37a729a
|
||||
cWtfn1/message: 9a91ea70f5eb55aac96640bf0fd7feff
|
||||
cWtfn1/origin/0/0: ff4a160eca5f8da4f075fd1dd6061fa6
|
||||
cWtfn1/origin/1/0: 69d8634b177fdf937764b406fd477547
|
||||
cWtfn1/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||
cWtfn1/translation: 9a91ea70f5eb55aac96640bf0fd7feff
|
||||
b3Thhd/message: 5b7b755f05330164dbfb00096c500ee3
|
||||
b3Thhd/origin/0/0: 12c1fab543c8848bcb10107d47336736
|
||||
@@ -2755,9 +2783,15 @@ checksums:
|
||||
9y1RcT/message: 2dfa121e0ff1914e08068308a81973c0
|
||||
9y1RcT/origin/0/0: d77eb1f90f2fa86a55181629d88d237a
|
||||
9y1RcT/translation: 2dfa121e0ff1914e08068308a81973c0
|
||||
an6ayw/message: 2d39f6440ad379e417193dbfaa265b5f
|
||||
an6ayw/origin/0/0: f64cd62b620f454998523af2a4330278
|
||||
an6ayw/translation: 2d39f6440ad379e417193dbfaa265b5f
|
||||
GdWB%2BV/message: 6a15a9b926ecd2c91454e9da19c6049e
|
||||
GdWB%2BV/origin/0/0: f64cd62b620f454998523af2a4330278
|
||||
GdWB%2BV/translation: 6a15a9b926ecd2c91454e9da19c6049e
|
||||
LnaC5y/message: 229d2a9159e17f243cf317b9bd7d471c
|
||||
LnaC5y/origin/0/0: a1fde3fcce608870830f168f027727ff
|
||||
LnaC5y/translation: 229d2a9159e17f243cf317b9bd7d471c
|
||||
2X4ecw/message: fcefd247ec76a372002d2cffac3c5b0f
|
||||
2X4ecw/origin/0/0: a1fde3fcce608870830f168f027727ff
|
||||
2X4ecw/translation: fcefd247ec76a372002d2cffac3c5b0f
|
||||
@@ -2771,6 +2805,9 @@ checksums:
|
||||
8iP9oQ/origin/0/0: f64cd62b620f454998523af2a4330278
|
||||
8iP9oQ/origin/1/0: dc1a240d2d0cccb102c67ca311d4e457
|
||||
8iP9oQ/translation: 5c07446c838d07572c6b40621507e951
|
||||
kxZ7LS/message: 41ebcdff41ce85329915754faf7140c0
|
||||
kxZ7LS/origin/0/0: f64cd62b620f454998523af2a4330278
|
||||
kxZ7LS/translation: 41ebcdff41ce85329915754faf7140c0
|
||||
3d54Wj/message: d67dc42810694fd1344a9a9545aca1e7
|
||||
3d54Wj/origin/0/0: f64cd62b620f454998523af2a4330278
|
||||
3d54Wj/translation: d67dc42810694fd1344a9a9545aca1e7
|
||||
@@ -2877,9 +2914,9 @@ checksums:
|
||||
RtlIYB/message: df4783d593505766d78c5bc08d3d48ac
|
||||
RtlIYB/origin/0/0: 627b72cd19a7154ae1c28cbd7ca0c730
|
||||
RtlIYB/translation: df4783d593505766d78c5bc08d3d48ac
|
||||
3WXdoZ/translation: 4cbeab40f77975492a23eec428732440
|
||||
3WXdoZ/message: 4cbeab40f77975492a23eec428732440
|
||||
3WXdoZ/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||
3WXdoZ/translation: 4cbeab40f77975492a23eec428732440
|
||||
aelko%2B/message: 41dd145ef56f539e12dc064a9807c660
|
||||
aelko%2B/origin/0/0: 4980d8e27a628e7cd9a70b839d844083
|
||||
aelko%2B/translation: 41dd145ef56f539e12dc064a9807c660
|
||||
@@ -2923,9 +2960,9 @@ checksums:
|
||||
GoDLB9/message: 8c8d944e07388c5877effdb2c2803dcf
|
||||
GoDLB9/origin/0/0: 5c60161e173f5eae9276d8e4c70b4ebb
|
||||
GoDLB9/translation: 8c8d944e07388c5877effdb2c2803dcf
|
||||
c4nMcR/translation: bf4f697079eef51c3ebe46ce79063895
|
||||
c4nMcR/message: bf4f697079eef51c3ebe46ce79063895
|
||||
c4nMcR/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
||||
c4nMcR/translation: bf4f697079eef51c3ebe46ce79063895
|
||||
evg7%2BA/message: 403972e7a25afc2415762c1c2b1ec868
|
||||
evg7%2BA/origin/0/0: 3c57a918258af4b44c187f58d203345f
|
||||
evg7%2BA/translation: 403972e7a25afc2415762c1c2b1ec868
|
||||
@@ -2962,12 +2999,12 @@ checksums:
|
||||
25fAUC/message: 3e4cce96bc1197a1caa2b7fb4d3c7e53
|
||||
25fAUC/origin/0/0: d1e1d7ccce60e05570bedfbb93fc9416
|
||||
25fAUC/translation: 3e4cce96bc1197a1caa2b7fb4d3c7e53
|
||||
GRAGsB/translation: 751a534fd6e62a92ff64c46894c7db86
|
||||
GRAGsB/message: 751a534fd6e62a92ff64c46894c7db86
|
||||
GRAGsB/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||
j0o6ml/translation: 1e036ffca9d9cb9e2941b83fbbf375c0
|
||||
GRAGsB/translation: 751a534fd6e62a92ff64c46894c7db86
|
||||
j0o6ml/message: 1e036ffca9d9cb9e2941b83fbbf375c0
|
||||
j0o6ml/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||
j0o6ml/translation: 1e036ffca9d9cb9e2941b83fbbf375c0
|
||||
GnSSGm/message: 98de6f65c567c6654cc0adce3c9d5656
|
||||
GnSSGm/origin/0/0: 33bf266228f835e12c0bfb0e5efb3be9
|
||||
GnSSGm/translation: 98de6f65c567c6654cc0adce3c9d5656
|
||||
@@ -2980,10 +3017,10 @@ checksums:
|
||||
Hg1qE9/message: c808949b9b2b4a9aba2472f5d1050167
|
||||
Hg1qE9/origin/0/0: 30791e26a172d47d3cb0693e82679246
|
||||
Hg1qE9/translation: c808949b9b2b4a9aba2472f5d1050167
|
||||
OTxjpR/translation: 370a32c49489db61978ac91b55699c24
|
||||
OTxjpR/message: 370a32c49489db61978ac91b55699c24
|
||||
OTxjpR/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||
OTxjpR/origin/1/0: 69d8634b177fdf937764b406fd477547
|
||||
OTxjpR/translation: 370a32c49489db61978ac91b55699c24
|
||||
4DOCMI/message: 0b48896061a1124501fdaba026804148
|
||||
4DOCMI/origin/0/0: a50c4fbde8da6e0a77c4e0aef0b44dea
|
||||
4DOCMI/translation: 0b48896061a1124501fdaba026804148
|
||||
|
||||
@@ -33,6 +33,7 @@ type SearchResult =
|
||||
boardPublicId: string;
|
||||
boardName: string;
|
||||
listName: string;
|
||||
cardNumber: number | null;
|
||||
updatedAt: Date | null;
|
||||
createdAt: Date;
|
||||
type: "card";
|
||||
@@ -159,8 +160,17 @@ export default function CommandPallette({
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 text-left">
|
||||
<div className="truncate text-sm font-bold text-light-900 dark:text-dark-900">
|
||||
{result.title}
|
||||
<div className="flex items-center gap-2">
|
||||
<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>
|
||||
{result.type === "card" && (
|
||||
<div className="truncate text-xs text-light-700 dark:text-dark-700">
|
||||
|
||||
@@ -85,7 +85,7 @@ export interface RenderSuggestionsProps {
|
||||
export interface WorkspaceMember {
|
||||
publicId: string;
|
||||
user: {
|
||||
id: string;
|
||||
id: string | null;
|
||||
name: string | null;
|
||||
image: string | null;
|
||||
} | null;
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { env } from "next-runtime-env";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
HiBolt,
|
||||
HiCheck,
|
||||
HiCheckBadge,
|
||||
HiInformationCircle,
|
||||
HiXMark,
|
||||
} from "react-icons/hi2";
|
||||
import { HiCheck, HiXMark } from "react-icons/hi2";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { z } from "zod";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
import Input from "~/components/Input";
|
||||
import Toggle from "~/components/Toggle";
|
||||
import { useDebounce } from "~/hooks/useDebounce";
|
||||
import { useModal } from "~/providers/modal";
|
||||
import { usePopup } from "~/providers/popup";
|
||||
@@ -54,7 +47,6 @@ export function NewWorkspaceForm() {
|
||||
watch,
|
||||
trigger,
|
||||
clearErrors,
|
||||
setValue,
|
||||
} = useForm<FormValues>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
@@ -67,16 +59,10 @@ export function NewWorkspaceForm() {
|
||||
|
||||
const hasAvailableWorkspaces = availableWorkspaces.length > 0;
|
||||
|
||||
const isCloudEnv = env("NEXT_PUBLIC_KAN_ENV") === "cloud";
|
||||
|
||||
const slug = watch("slug");
|
||||
const [debouncedSlug] = useDebounce(slug, 500);
|
||||
const isTyping = slug !== debouncedSlug;
|
||||
|
||||
// Pro toggle state management
|
||||
const [isProToggleEnabled, setIsProToggleEnabled] = useState(false);
|
||||
const [lastAvailableSlug, setLastAvailableSlug] = useState<string>("");
|
||||
|
||||
// Validate slug only after debounce
|
||||
useEffect(() => {
|
||||
if (isTyping) {
|
||||
@@ -101,7 +87,7 @@ export function NewWorkspaceForm() {
|
||||
const isWorkspaceSlugAvailable = checkWorkspaceSlugAvailability.data;
|
||||
|
||||
const createWorkspace = api.workspace.create.useMutation({
|
||||
onSuccess: async (values, variables) => {
|
||||
onSuccess: (values) => {
|
||||
if (values.publicId && values.name) {
|
||||
void utils.workspace.all.invalidate();
|
||||
switchWorkspace({
|
||||
@@ -132,57 +118,8 @@ export function NewWorkspaceForm() {
|
||||
if (nameElement) nameElement.focus();
|
||||
}, []);
|
||||
|
||||
const [shouldShowBenefits, setShouldShowBenefits] = useState(false);
|
||||
|
||||
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) => {
|
||||
// Don't submit if slug is provided but not available
|
||||
if (values.slug && isWorkspaceSlugAvailable?.isAvailable === false) {
|
||||
@@ -191,7 +128,7 @@ export function NewWorkspaceForm() {
|
||||
|
||||
createWorkspace.mutate({
|
||||
name: values.name,
|
||||
slug: !isCloudEnv && values.slug ? values.slug : undefined,
|
||||
slug: values.slug,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -254,11 +191,7 @@ export function NewWorkspaceForm() {
|
||||
? t`This workspace URL is reserved`
|
||||
: undefined)
|
||||
}
|
||||
prefix={
|
||||
env("NEXT_PUBLIC_KAN_ENV") === "cloud"
|
||||
? "kan.bn/"
|
||||
: `${env("NEXT_PUBLIC_BASE_URL")}/`
|
||||
}
|
||||
prefix={`${env("NEXT_PUBLIC_BASE_URL")}/`}
|
||||
iconRight={
|
||||
slug && slug.length >= 3 && !errors.slug ? (
|
||||
isWorkspaceSlugAvailable?.isAvailable ? (
|
||||
@@ -275,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>
|
||||
|
||||
{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
|
||||
className={twMerge(
|
||||
"mt-6 flex items-center justify-end space-x-4 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 className="mt-6 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
||||
<div>
|
||||
<Button
|
||||
type="submit"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Locale as DateFnsLocale } from "date-fns";
|
||||
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 { useLinguiContext } from "~/providers/lingui";
|
||||
@@ -19,7 +19,7 @@ export function useLocalisation() {
|
||||
nl,
|
||||
ru,
|
||||
pl,
|
||||
"pt-BR": ptBR,
|
||||
ptbr: ptBR,
|
||||
};
|
||||
|
||||
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,6 +20,7 @@ interface Workspace {
|
||||
plan: "free" | "team" | "pro" | "enterprise" | undefined;
|
||||
role: "admin" | "member" | "guest";
|
||||
weekStartDay: 0 | 1 | 6;
|
||||
cardPrefix: string;
|
||||
}
|
||||
|
||||
const initialWorkspace: Workspace = {
|
||||
@@ -30,6 +31,7 @@ const initialWorkspace: Workspace = {
|
||||
plan: "free" as const,
|
||||
role: "member",
|
||||
weekStartDay: 1,
|
||||
cardPrefix: "",
|
||||
};
|
||||
|
||||
const initialAvailableWorkspaces: Workspace[] = [];
|
||||
@@ -90,6 +92,7 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
|
||||
description: workspace.description,
|
||||
plan: workspace.plan,
|
||||
weekStartDay: workspace.weekStartDay,
|
||||
cardPrefix: workspace.cardPrefix,
|
||||
hasLoaded: true,
|
||||
})) as Workspace[];
|
||||
|
||||
@@ -121,6 +124,7 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
|
||||
description: selectedWorkspace.workspace.description,
|
||||
role: selectedWorkspace.role,
|
||||
weekStartDay: selectedWorkspace.workspace.weekStartDay as 0 | 1 | 6,
|
||||
cardPrefix: selectedWorkspace.workspace.cardPrefix,
|
||||
});
|
||||
|
||||
if (workspacePublicId) {
|
||||
@@ -141,6 +145,7 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
|
||||
description: primaryWorkspace.description,
|
||||
role: primaryWorkspaceRole,
|
||||
weekStartDay: primaryWorkspace.weekStartDay as 0 | 1 | 6,
|
||||
cardPrefix: primaryWorkspace.cardPrefix,
|
||||
});
|
||||
}
|
||||
}, [data, isLoading, workspacePublicId, router]);
|
||||
|
||||
@@ -22,8 +22,8 @@ const loadMessages = async (locale: Locale) => {
|
||||
return (await import("~/locales/ru/messages")).messages;
|
||||
case "pl":
|
||||
return (await import("~/locales/pl/messages")).messages;
|
||||
case "pt-BR":
|
||||
return (await import("~/locales/pt-BR/messages")).messages;
|
||||
case "ptbr":
|
||||
return (await import("~/locales/ptbr/messages")).messages;
|
||||
default:
|
||||
return enMessages;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import {
|
||||
HiEllipsisHorizontal,
|
||||
@@ -29,6 +30,7 @@ export default function BoardDropdown({
|
||||
isFavorite?: boolean;
|
||||
boardName?: string;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const { openModal } = useModal();
|
||||
const { showPopup } = usePopup();
|
||||
const { canEditBoard, canDeleteBoard, canCreateBoard, canArchiveBoard } =
|
||||
@@ -47,6 +49,7 @@ export default function BoardDropdown({
|
||||
: t`The board has been unarchived.`,
|
||||
icon: "success",
|
||||
});
|
||||
void router.push(`/boards`);
|
||||
} else if (variables.favorite !== undefined) {
|
||||
showPopup({
|
||||
header: variables.favorite
|
||||
|
||||
@@ -16,6 +16,7 @@ import { getAvatarUrl } from "~/utils/helpers";
|
||||
|
||||
const Card = ({
|
||||
title,
|
||||
ticketNumber,
|
||||
labels,
|
||||
members,
|
||||
checklists,
|
||||
@@ -25,6 +26,7 @@ const Card = ({
|
||||
dueDate,
|
||||
}: {
|
||||
title: string;
|
||||
ticketNumber?: string | null;
|
||||
labels: { name: string; colourCode: string | null }[];
|
||||
members: {
|
||||
publicId: string;
|
||||
@@ -67,6 +69,11 @@ const Card = ({
|
||||
|
||||
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">
|
||||
{ticketNumber && (
|
||||
<span className="mb-1 text-xs text-light-700 dark:text-dark-800">
|
||||
{ticketNumber}
|
||||
</span>
|
||||
)}
|
||||
<span className="break-words">{title}</span>
|
||||
{labels.length ||
|
||||
members.length ||
|
||||
|
||||
@@ -146,6 +146,10 @@ export function NewCardForm({
|
||||
listId: 2,
|
||||
description: "",
|
||||
dueDate: args.dueDate ?? null,
|
||||
cardNumber: null,
|
||||
comments: [],
|
||||
checklists: [],
|
||||
attachments: [],
|
||||
labels: oldBoard.labels.filter((label) =>
|
||||
args.labelPublicIds.includes(label.publicId),
|
||||
),
|
||||
@@ -158,6 +162,9 @@ export function NewCardForm({
|
||||
...member,
|
||||
deletedAt: null,
|
||||
})) ?? [],
|
||||
comments: [],
|
||||
checklists: [],
|
||||
attachments: [],
|
||||
_filteredLabels: labelPublicIds.map((id) => ({ publicId: id })),
|
||||
_filteredMembers: memberPublicIds.map((id) => ({ publicId: id })),
|
||||
index: position === "start" ? 0 : list.cards.length,
|
||||
|
||||
@@ -2,9 +2,9 @@ import type { DropResult } from "react-beautiful-dnd";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useRouter } from "next/router";
|
||||
import { env } from "next-runtime-env";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { keepPreviousData } from "@tanstack/react-query";
|
||||
import { env } from "next-runtime-env";
|
||||
import { useEffect, useState } from "react";
|
||||
import { DragDropContext, Draggable } from "react-beautiful-dnd";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -757,6 +757,11 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
|
||||
>
|
||||
<Card
|
||||
title={card.title}
|
||||
ticketNumber={
|
||||
card.cardNumber != null
|
||||
? `${boardData.workspace.cardPrefix}-${card.cardNumber}`
|
||||
: null
|
||||
}
|
||||
labels={card.labels}
|
||||
members={card.members}
|
||||
checklists={card.checklists ?? []}
|
||||
|
||||
@@ -525,7 +525,6 @@ const ActivityList = ({
|
||||
comment={activity.comment?.comment}
|
||||
isEdited={!!activity.comment?.updatedAt}
|
||||
isAuthor={activity.comment?.createdBy === sessionData?.user.id}
|
||||
isAdmin={isAdmin ?? false}
|
||||
isViewOnly={!!isViewOnly}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { t } from "@lingui/core/macro";
|
||||
import {
|
||||
HiEllipsisHorizontal,
|
||||
HiHashtag,
|
||||
HiLink,
|
||||
HiOutlineCheckCircle,
|
||||
HiOutlineTrash,
|
||||
@@ -18,11 +19,13 @@ export default function CardDropdown({
|
||||
isTemplate,
|
||||
boardPublicId,
|
||||
cardCreatedBy,
|
||||
ticketNumber,
|
||||
}: {
|
||||
cardPublicId: string;
|
||||
isTemplate?: boolean;
|
||||
boardPublicId?: string;
|
||||
cardCreatedBy?: string | null;
|
||||
ticketNumber?: string | null;
|
||||
}) {
|
||||
const { openModal } = useModal();
|
||||
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 = [
|
||||
{
|
||||
label: t`Copy card link`,
|
||||
action: handleCopyCardLink,
|
||||
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
|
||||
? [
|
||||
{
|
||||
|
||||
@@ -340,6 +340,14 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
|
||||
>
|
||||
{board?.name}
|
||||
</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 className="flex items-center gap-2">
|
||||
<Dropdown
|
||||
@@ -347,6 +355,11 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
|
||||
isTemplate={isTemplate}
|
||||
boardPublicId={boardId}
|
||||
cardCreatedBy={card?.createdBy}
|
||||
ticketNumber={
|
||||
card.cardNumber != null && card.list.board.workspace.cardPrefix
|
||||
? `${card.list.board.workspace.cardPrefix}-${card.cardNumber}`
|
||||
: null
|
||||
}
|
||||
/>
|
||||
<Link
|
||||
href={`/${isTemplate ? "templates" : "boards"}/${boardId}`}
|
||||
|
||||
@@ -41,6 +41,7 @@ export default function WorkspaceNameView() {
|
||||
const returnUrl = searchParams.get("returnUrl") ?? "/boards";
|
||||
const { showPopup } = usePopup();
|
||||
const [isProToggle, setIsProToggle] = useState(plan === "pro");
|
||||
const effectivePlan = isProToggle ? "pro" : plan;
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [slug, setSlug] = useState("");
|
||||
@@ -109,7 +110,7 @@ export default function WorkspaceNameView() {
|
||||
const handleContinue = async () => {
|
||||
if (!name.trim()) return;
|
||||
|
||||
if (plan === "solo") {
|
||||
if (effectivePlan === "solo") {
|
||||
createWorkspace.mutate({
|
||||
name: name.trim(),
|
||||
...(description.trim() && { description: description.trim() }),
|
||||
@@ -128,11 +129,11 @@ export default function WorkspaceNameView() {
|
||||
...(description.trim() && {
|
||||
workspaceDescription: description.trim(),
|
||||
}),
|
||||
...(plan === "pro" && slug ? { workspaceSlug: slug } : {}),
|
||||
cancelUrl: window.location.pathname + window.location.search,
|
||||
...(effectivePlan === "pro" && slug ? { workspaceSlug: slug } : {}),
|
||||
cancelUrl: `${window.location.pathname}?plan=${effectivePlan}&billing=${billing}&returnUrl=${encodeURIComponent(returnUrl)}`,
|
||||
successUrl: "/boards",
|
||||
billing,
|
||||
plan,
|
||||
plan: effectivePlan,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
@@ -268,7 +269,7 @@ export default function WorkspaceNameView() {
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
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>
|
||||
) : (
|
||||
<>
|
||||
{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]">
|
||||
{data?.title}
|
||||
</h1>
|
||||
|
||||
@@ -17,11 +17,11 @@ import { useModal } from "~/providers/modal";
|
||||
import { useWorkspace } from "~/providers/workspace";
|
||||
import { api } from "~/utils/api";
|
||||
import { DeleteWorkspaceConfirmation } from "./components/DeleteWorkspaceConfirmation";
|
||||
import UpdateWeekStartDayForm from "./components/UpdateWeekStartDayForm";
|
||||
import UpdateWorkspaceDescriptionForm from "./components/UpdateWorkspaceDescriptionForm";
|
||||
import UpdateWorkspaceEmailVisibilityForm from "./components/UpdateWorkspaceEmailVisibilityForm";
|
||||
import UpdateWorkspaceNameForm from "./components/UpdateWorkspaceNameForm";
|
||||
import UpdateWorkspaceUrlForm from "./components/UpdateWorkspaceUrlForm";
|
||||
import UpdateWeekStartDayForm from "./components/UpdateWeekStartDayForm";
|
||||
import { UpgradeToProConfirmation } from "./components/UpgradeToProConfirmation";
|
||||
|
||||
export default function WorkspaceSettings() {
|
||||
|
||||
@@ -13,20 +13,29 @@ interface DeleteWebhookConfirmationProps {
|
||||
export function DeleteWebhookConfirmation({
|
||||
workspacePublicId,
|
||||
}: DeleteWebhookConfirmationProps) {
|
||||
const { closeModal, entityId: webhookPublicId, entityLabel: webhookName } = useModal();
|
||||
const {
|
||||
closeModal,
|
||||
entityId: webhookPublicId,
|
||||
entityLabel: webhookName,
|
||||
} = useModal();
|
||||
const { showPopup } = usePopup();
|
||||
const utils = api.useUtils();
|
||||
|
||||
const deleteWebhookMutation = api.webhook.delete.useMutation({
|
||||
onSuccess: () => {
|
||||
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();
|
||||
},
|
||||
onError: (error) => {
|
||||
showPopup({
|
||||
header: t`Unable 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;
|
||||
deleteWebhookMutation.mutate({
|
||||
workspacePublicId,
|
||||
webhookPublicId: webhookPublicId as string,
|
||||
webhookPublicId: webhookPublicId,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -44,7 +44,12 @@ export function NewWebhookModal({
|
||||
workspacePublicId,
|
||||
isEdit = false,
|
||||
}: NewWebhookModalProps) {
|
||||
const { closeModal, entityId: webhookPublicId, getModalState, clearModalState } = useModal();
|
||||
const {
|
||||
closeModal,
|
||||
entityId: webhookPublicId,
|
||||
getModalState,
|
||||
clearModalState,
|
||||
} = useModal();
|
||||
const { showPopup } = usePopup();
|
||||
const [isTestingWebhook, setIsTestingWebhook] = useState(false);
|
||||
|
||||
@@ -101,13 +106,18 @@ export function NewWebhookModal({
|
||||
const createWebhookMutation = api.webhook.create.useMutation({
|
||||
onSuccess: () => {
|
||||
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();
|
||||
},
|
||||
onError: (error) => {
|
||||
showPopup({
|
||||
header: t`Unable 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({
|
||||
onSuccess: () => {
|
||||
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();
|
||||
},
|
||||
onError: (error) => {
|
||||
showPopup({
|
||||
header: t`Unable 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({
|
||||
onSuccess: (result) => {
|
||||
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 {
|
||||
showPopup({
|
||||
header: t`Test failed`,
|
||||
message: result.error || t`Webhook test failed`,
|
||||
type: "error",
|
||||
icon: "error",
|
||||
});
|
||||
}
|
||||
setIsTestingWebhook(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
showPopup({
|
||||
header: t`Unable to test webhook`,
|
||||
message: error.message || t`Failed to test webhook`,
|
||||
type: "error",
|
||||
icon: "error",
|
||||
});
|
||||
setIsTestingWebhook(false);
|
||||
},
|
||||
@@ -151,7 +172,7 @@ export function NewWebhookModal({
|
||||
if (isEdit && webhookPublicId) {
|
||||
updateWebhookMutation.mutate({
|
||||
workspacePublicId,
|
||||
webhookPublicId: webhookPublicId as string,
|
||||
webhookPublicId: webhookPublicId,
|
||||
name: data.name,
|
||||
url: data.url,
|
||||
secret: data.secret || undefined,
|
||||
@@ -174,11 +195,12 @@ export function NewWebhookModal({
|
||||
setIsTestingWebhook(true);
|
||||
testWebhookMutation.mutate({
|
||||
workspacePublicId,
|
||||
webhookPublicId: webhookPublicId as string,
|
||||
webhookPublicId: webhookPublicId,
|
||||
});
|
||||
};
|
||||
|
||||
const isPending = createWebhookMutation.isPending || updateWebhookMutation.isPending;
|
||||
const isPending =
|
||||
createWebhookMutation.isPending || updateWebhookMutation.isPending;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
@@ -231,7 +253,11 @@ export function NewWebhookModal({
|
||||
<Input
|
||||
id="secret"
|
||||
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")}
|
||||
errorMessage={errors.secret?.message}
|
||||
/>
|
||||
@@ -252,7 +278,7 @@ export function NewWebhookModal({
|
||||
{webhookEvents.map((event) => (
|
||||
<label
|
||||
key={event}
|
||||
className="flex items-center space-x-2 cursor-pointer"
|
||||
className="flex cursor-pointer items-center space-x-2"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -262,11 +288,11 @@ export function NewWebhookModal({
|
||||
field.onChange([...field.value, event]);
|
||||
} else {
|
||||
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">
|
||||
{event}
|
||||
@@ -277,17 +303,19 @@ export function NewWebhookModal({
|
||||
)}
|
||||
/>
|
||||
{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>
|
||||
|
||||
{isEdit && (
|
||||
<div>
|
||||
<label className="flex items-center space-x-2 cursor-pointer">
|
||||
<label className="flex cursor-pointer items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
{...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">
|
||||
{t`Active`}
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function UpdateWeekStartDayForm({
|
||||
setValue(newValue);
|
||||
updateWorkspace.mutate({
|
||||
workspacePublicId,
|
||||
weekStartDay: newValue,
|
||||
weekStartDay: newValue as 0 | 1 | 6,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ const UpdateWorkspaceUrlForm = ({
|
||||
}: {
|
||||
workspacePublicId: string;
|
||||
workspaceUrl: string;
|
||||
workspacePlan: "free" | "pro" | "enterprise";
|
||||
workspacePlan: "free" | "team" | "pro" | "enterprise";
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const utils = api.useUtils();
|
||||
|
||||
@@ -171,18 +171,20 @@ export default function WebhookList({ workspacePublicId }: WebhookListProps) {
|
||||
const testWebhookMutation = api.webhook.test.useMutation({
|
||||
onSuccess: (result) => {
|
||||
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 {
|
||||
showPopup({
|
||||
header: t`Test failed`,
|
||||
message: result.error || t`Webhook test failed`,
|
||||
type: "error",
|
||||
icon: "error",
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
showPopup({
|
||||
header: t`Unable 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_FORCE_PATH_STYLE=${S3_FORCE_PATH_STYLE}
|
||||
- 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_ATTACHMENTS_BUCKET_NAME=${NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME}
|
||||
- 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 { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { attachmentConfirmResponseSchema } from "../schemas";
|
||||
import { assertPermission } from "../utils/permissions";
|
||||
import { deleteObject, generateUploadUrl } from "@kan/shared/utils";
|
||||
|
||||
@@ -110,7 +111,7 @@ export const attachmentRouter = createTRPCRouter({
|
||||
size: z.number().positive(),
|
||||
}),
|
||||
)
|
||||
.output(z.custom<Awaited<ReturnType<typeof cardAttachmentRepo.create>>>())
|
||||
.output(attachmentConfirmResponseSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
|
||||
@@ -16,6 +16,13 @@ import {
|
||||
} from "@kan/shared/utils";
|
||||
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||
import {
|
||||
boardListItemSchema,
|
||||
boardDetailSchema,
|
||||
boardBySlugSchema,
|
||||
boardCreateResponseSchema,
|
||||
boardUpdateResponseSchema,
|
||||
} from "../schemas";
|
||||
import { assertCanDelete, assertCanEdit, assertPermission } from "../utils/permissions";
|
||||
|
||||
export const boardRouter = createTRPCRouter({
|
||||
@@ -37,9 +44,7 @@ export const boardRouter = createTRPCRouter({
|
||||
archived: z.boolean().optional(),
|
||||
}),
|
||||
)
|
||||
.output(
|
||||
z.custom<Awaited<ReturnType<typeof boardRepo.getAllByWorkspaceId>>>(),
|
||||
)
|
||||
.output(z.array(boardListItemSchema))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -106,7 +111,7 @@ export const boardRouter = createTRPCRouter({
|
||||
type: z.enum(["regular", "template"]).optional(),
|
||||
}),
|
||||
)
|
||||
.output(z.custom<Awaited<ReturnType<typeof boardRepo.getByPublicId>>>())
|
||||
.output(boardDetailSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -246,7 +251,7 @@ export const boardRouter = createTRPCRouter({
|
||||
.optional(),
|
||||
}),
|
||||
)
|
||||
.output(z.custom<Awaited<ReturnType<typeof boardRepo.getBySlug>>>())
|
||||
.output(boardBySlugSchema.nullable())
|
||||
.query(async ({ ctx, input }) => {
|
||||
const workspace = await workspaceRepo.getBySlugWithBoards(
|
||||
ctx.db,
|
||||
@@ -299,7 +304,7 @@ export const boardRouter = createTRPCRouter({
|
||||
sourceBoardPublicId: z.string().min(12).optional(),
|
||||
}),
|
||||
)
|
||||
.output(z.custom<Awaited<ReturnType<typeof boardRepo.create>>>())
|
||||
.output(boardCreateResponseSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -469,7 +474,7 @@ export const boardRouter = createTRPCRouter({
|
||||
isArchived: z.boolean().optional(),
|
||||
}),
|
||||
)
|
||||
.output(z.object({ success: z.boolean() }).or(z.custom<Awaited<ReturnType<typeof boardRepo.update>>>()))
|
||||
.output(boardUpdateResponseSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
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 { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||
import {
|
||||
cardCreateResponseSchema,
|
||||
cardUpdateResponseSchema,
|
||||
cardDetailSchema,
|
||||
commentResponseSchema,
|
||||
commentDeleteResponseSchema,
|
||||
activityItemSchema,
|
||||
} from "../schemas";
|
||||
import { mergeActivities } from "../utils/activities";
|
||||
import { sendMentionEmails } from "../utils/notifications";
|
||||
import { assertCanDelete, assertCanEdit, assertPermission } from "../utils/permissions";
|
||||
@@ -42,7 +50,7 @@ export const cardRouter = createTRPCRouter({
|
||||
dueDate: z.date().nullable().optional(),
|
||||
}),
|
||||
)
|
||||
.output(z.custom<Awaited<ReturnType<typeof cardRepo.create>>>())
|
||||
.output(cardCreateResponseSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -70,6 +78,7 @@ export const cardRouter = createTRPCRouter({
|
||||
description: input.description,
|
||||
createdBy: userId,
|
||||
listId: list.id,
|
||||
workspaceId: list.workspaceId,
|
||||
position: input.position,
|
||||
dueDate: input.dueDate ?? null,
|
||||
});
|
||||
@@ -181,7 +190,7 @@ export const cardRouter = createTRPCRouter({
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
dueDate: input.dueDate ?? null,
|
||||
listId: String(newCard.listId),
|
||||
listId: list.publicId,
|
||||
},
|
||||
{
|
||||
boardId: list.boardPublicId,
|
||||
@@ -215,7 +224,7 @@ export const cardRouter = createTRPCRouter({
|
||||
comment: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.output(z.custom<Awaited<ReturnType<typeof cardCommentRepo.create>>>())
|
||||
.output(commentResponseSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -288,7 +297,7 @@ export const cardRouter = createTRPCRouter({
|
||||
comment: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.output(z.custom<Awaited<ReturnType<typeof cardCommentRepo.update>>>())
|
||||
.output(commentResponseSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -376,7 +385,7 @@ export const cardRouter = createTRPCRouter({
|
||||
commentPublicId: z.string().min(12),
|
||||
}),
|
||||
)
|
||||
.output(z.custom<Awaited<ReturnType<typeof cardCommentRepo.softDelete>>>())
|
||||
.output(commentDeleteResponseSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -435,7 +444,7 @@ export const cardRouter = createTRPCRouter({
|
||||
createdBy: userId,
|
||||
});
|
||||
|
||||
return deletedComment;
|
||||
return { publicId: input.commentPublicId };
|
||||
}),
|
||||
addOrRemoveLabel: protectedProcedure
|
||||
.meta({
|
||||
@@ -639,25 +648,7 @@ export const cardRouter = createTRPCRouter({
|
||||
},
|
||||
})
|
||||
.input(z.object({ cardPublicId: z.string().min(12) }))
|
||||
.output(
|
||||
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;
|
||||
}[];
|
||||
}
|
||||
>(),
|
||||
)
|
||||
.output(cardDetailSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const card = await cardRepo.getWorkspaceAndCardIdByCardPublicId(
|
||||
ctx.db,
|
||||
@@ -763,15 +754,7 @@ export const cardRouter = createTRPCRouter({
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
activities: z.array(
|
||||
z.custom<
|
||||
NonNullable<
|
||||
Awaited<
|
||||
ReturnType<typeof cardActivityRepo.getPaginatedActivities>
|
||||
>
|
||||
>["activities"][number]
|
||||
>(),
|
||||
),
|
||||
activities: z.array(activityItemSchema),
|
||||
hasMore: z.boolean(),
|
||||
nextCursor: z.string().datetime().nullable(),
|
||||
}),
|
||||
@@ -871,7 +854,7 @@ export const cardRouter = createTRPCRouter({
|
||||
dueDate: z.date().nullable().optional(),
|
||||
}),
|
||||
)
|
||||
.output(z.custom<Awaited<ReturnType<typeof cardRepo.update>>>())
|
||||
.output(cardUpdateResponseSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -906,9 +889,18 @@ export const cardRouter = createTRPCRouter({
|
||||
);
|
||||
|
||||
let newListId: number | undefined;
|
||||
let newList:
|
||||
| {
|
||||
id: number;
|
||||
publicId: string;
|
||||
name: string;
|
||||
boardId: number;
|
||||
index: number;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
if (input.listPublicId) {
|
||||
const newList = await listRepo.getByPublicId(
|
||||
newList = await listRepo.getByPublicId(
|
||||
ctx.db,
|
||||
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, {
|
||||
cardId: existingCard.id,
|
||||
newIndex: input.index,
|
||||
@@ -1055,8 +1047,19 @@ export const cardRouter = createTRPCRouter({
|
||||
) {
|
||||
webhookChanges.dueDate = { from: previousDueDate, to: input.dueDate };
|
||||
}
|
||||
if (newListId && existingCard.listId !== newListId) {
|
||||
webhookChanges.listId = { from: existingCard.listId, to: newListId };
|
||||
const movedToNewList = Boolean(newListId && existingCard.listId !== 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)
|
||||
@@ -1064,20 +1067,18 @@ export const cardRouter = createTRPCRouter({
|
||||
ctx.db,
|
||||
card.workspaceId,
|
||||
createCardWebhookPayload(
|
||||
newListId && existingCard.listId !== newListId
|
||||
? "card.moved"
|
||||
: "card.updated",
|
||||
movedToNewList ? "card.moved" : "card.updated",
|
||||
{
|
||||
id: String(result.id),
|
||||
title: result.title,
|
||||
description: result.description,
|
||||
dueDate: result.dueDate,
|
||||
listId: String(newListId ?? existingCard.listId),
|
||||
listId: currentWebhookListPublicId,
|
||||
},
|
||||
{
|
||||
boardId: card.boardPublicId,
|
||||
boardName: card.boardName,
|
||||
listName: card.listName,
|
||||
listName: currentWebhookListName,
|
||||
user: ctx.user
|
||||
? { id: ctx.user.id, name: ctx.user.name }
|
||||
: undefined,
|
||||
@@ -1167,12 +1168,12 @@ export const cardRouter = createTRPCRouter({
|
||||
title: fullCard.title,
|
||||
description: fullCard.description,
|
||||
dueDate: fullCard.dueDate,
|
||||
listId: String(fullCard.listId),
|
||||
listId: fullCard.list.publicId,
|
||||
},
|
||||
{
|
||||
boardId: card.boardPublicId,
|
||||
boardName: card.boardName,
|
||||
listName: card.listName,
|
||||
listName: fullCard.list.name,
|
||||
user: ctx.user
|
||||
? { id: ctx.user.id, name: ctx.user.name }
|
||||
: undefined,
|
||||
@@ -1272,6 +1273,7 @@ export const cardRouter = createTRPCRouter({
|
||||
description: sourceCard.description ?? "",
|
||||
createdBy: userId,
|
||||
listId: targetList.id,
|
||||
workspaceId: targetList.workspaceId,
|
||||
position: "end",
|
||||
dueDate: sourceCard.dueDate ?? null,
|
||||
});
|
||||
|
||||
@@ -340,6 +340,7 @@ export const importRouter = createTRPCRouter({
|
||||
description: card.description,
|
||||
createdBy: userId,
|
||||
listId: newListId,
|
||||
workspaceId: workspace.id,
|
||||
index,
|
||||
importId: newImportId,
|
||||
}));
|
||||
@@ -888,6 +889,7 @@ export const importRouter = createTRPCRouter({
|
||||
description: data.description,
|
||||
createdBy: userId,
|
||||
listId: data.listId,
|
||||
workspaceId: workspace.id,
|
||||
index: index,
|
||||
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 { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { listCreateResponseSchema, listUpdateResponseSchema } from "../schemas";
|
||||
import { assertCanDelete, assertCanEdit, assertPermission } from "../utils/permissions";
|
||||
|
||||
export const listRouter = createTRPCRouter({
|
||||
@@ -27,7 +28,7 @@ export const listRouter = createTRPCRouter({
|
||||
boardPublicId: z.string().min(12),
|
||||
}),
|
||||
)
|
||||
.output(z.custom<Awaited<ReturnType<typeof listRepo.create>>>())
|
||||
.output(listCreateResponseSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -163,12 +164,7 @@ export const listRouter = createTRPCRouter({
|
||||
index: z.number().optional(),
|
||||
}),
|
||||
)
|
||||
.output(
|
||||
z.custom<
|
||||
| Awaited<ReturnType<typeof listRepo.update>>
|
||||
| Awaited<ReturnType<typeof listRepo.reorder>>
|
||||
>(),
|
||||
)
|
||||
.output(listUpdateResponseSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { updateSubscriptionSeats } from "@kan/stripe";
|
||||
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||
import { memberInviteResponseSchema } from "../schemas";
|
||||
import {
|
||||
assertCanManageMember,
|
||||
assertCanManageRole,
|
||||
@@ -40,7 +41,7 @@ export const memberRouter = createTRPCRouter({
|
||||
workspacePublicId: z.string().min(12),
|
||||
}),
|
||||
)
|
||||
.output(z.custom<Awaited<ReturnType<typeof memberRepo.create>>>())
|
||||
.output(memberInviteResponseSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
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 { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||
import {
|
||||
workspaceListItemSchema,
|
||||
workspaceDetailSchema,
|
||||
workspaceWithBoardsSchema,
|
||||
workspaceCreateResponseSchema,
|
||||
workspaceUpdateResponseSchema,
|
||||
workspaceDeleteResponseSchema,
|
||||
} from "../schemas";
|
||||
import { assertPermission } from "../utils/permissions";
|
||||
|
||||
export const workspaceRouter = createTRPCRouter({
|
||||
@@ -22,9 +30,7 @@ export const workspaceRouter = createTRPCRouter({
|
||||
},
|
||||
})
|
||||
.input(z.void())
|
||||
.output(
|
||||
z.custom<Awaited<ReturnType<typeof workspaceRepo.getAllByUserId>>>(),
|
||||
)
|
||||
.output(z.array(workspaceListItemSchema))
|
||||
.query(async ({ ctx }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -50,11 +56,7 @@ export const workspaceRouter = createTRPCRouter({
|
||||
},
|
||||
})
|
||||
.input(z.object({ workspacePublicId: z.string().min(12) }))
|
||||
.output(
|
||||
z.custom<
|
||||
Awaited<ReturnType<typeof workspaceRepo.getByPublicIdWithMembers>>
|
||||
>(),
|
||||
)
|
||||
.output(workspaceDetailSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -134,7 +136,7 @@ export const workspaceRouter = createTRPCRouter({
|
||||
return {
|
||||
...result,
|
||||
members: sanitizedMembers,
|
||||
} as Awaited<ReturnType<typeof workspaceRepo.getByPublicIdWithMembers>>;
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -162,9 +164,7 @@ export const workspaceRouter = createTRPCRouter({
|
||||
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/),
|
||||
}),
|
||||
)
|
||||
.output(
|
||||
z.custom<Awaited<ReturnType<typeof workspaceRepo.getBySlugWithBoards>>>(),
|
||||
)
|
||||
.output(workspaceWithBoardsSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -211,7 +211,7 @@ export const workspaceRouter = createTRPCRouter({
|
||||
.optional(),
|
||||
}),
|
||||
)
|
||||
.output(z.custom<Awaited<ReturnType<typeof workspaceRepo.create>>>())
|
||||
.output(workspaceCreateResponseSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
const userEmail = ctx.user?.email;
|
||||
@@ -270,7 +270,13 @@ export const workspaceRouter = createTRPCRouter({
|
||||
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
|
||||
.meta({
|
||||
@@ -300,7 +306,7 @@ export const workspaceRouter = createTRPCRouter({
|
||||
.optional(),
|
||||
}),
|
||||
)
|
||||
.output(z.custom<Awaited<ReturnType<typeof workspaceRepo.update>>>())
|
||||
.output(workspaceUpdateResponseSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -383,7 +389,7 @@ export const workspaceRouter = createTRPCRouter({
|
||||
},
|
||||
})
|
||||
.input(z.object({ workspacePublicId: z.string().min(12) }))
|
||||
.output(z.custom<Awaited<ReturnType<typeof workspaceRepo.hardDelete>>>())
|
||||
.output(workspaceDeleteResponseSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -405,12 +411,12 @@ export const workspaceRouter = createTRPCRouter({
|
||||
});
|
||||
await assertPermission(ctx.db, userId, workspace.id, "workspace:delete");
|
||||
|
||||
const result = await workspaceRepo.hardDelete(
|
||||
await workspaceRepo.hardDelete(
|
||||
ctx.db,
|
||||
input.workspacePublicId,
|
||||
);
|
||||
|
||||
return result;
|
||||
return { success: true };
|
||||
}),
|
||||
checkSlugAvailability: publicProcedure
|
||||
.meta({
|
||||
@@ -515,6 +521,7 @@ export const workspaceRouter = createTRPCRouter({
|
||||
boardPublicId: z.string(),
|
||||
boardName: z.string(),
|
||||
listName: z.string(),
|
||||
cardNumber: z.number().nullable(),
|
||||
updatedAt: z.date().nullable(),
|
||||
createdAt: z.date(),
|
||||
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(),
|
||||
});
|
||||
@@ -1,9 +1,20 @@
|
||||
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", () => ({
|
||||
getActiveByWorkspaceId: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@kan/logger", () => ({
|
||||
createLogger: vi.fn(() => mockLogger),
|
||||
}));
|
||||
|
||||
import * as webhookRepo from "@kan/db/repository/webhook.repo";
|
||||
import {
|
||||
sendWebhookToUrl,
|
||||
@@ -155,6 +166,33 @@ describe("webhook utilities", () => {
|
||||
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", () => {
|
||||
@@ -434,8 +472,6 @@ describe("webhook utilities", () => {
|
||||
});
|
||||
|
||||
it("continues sending to other webhooks when one fails", async () => {
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
mockGetActiveByWorkspaceId.mockResolvedValueOnce([
|
||||
{
|
||||
id: 1,
|
||||
@@ -462,11 +498,15 @@ describe("webhook utilities", () => {
|
||||
await sendWebhooksForWorkspace(mockDb, 1, mockPayload);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledTimes(2);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Webhook delivery failed"),
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
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 () => {
|
||||
@@ -478,9 +518,6 @@ describe("webhook utilities", () => {
|
||||
});
|
||||
|
||||
it("catches and logs DB errors without throwing", async () => {
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, "error")
|
||||
.mockImplementation(() => {});
|
||||
mockGetActiveByWorkspaceId.mockRejectedValueOnce(
|
||||
new Error("DB connection failed"),
|
||||
);
|
||||
@@ -490,12 +527,13 @@ describe("webhook utilities", () => {
|
||||
sendWebhooksForWorkspace(mockDb, 1, mockPayload),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"Failed to send webhooks for workspace:",
|
||||
expect.any(Error),
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
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 ")) {
|
||||
return authorization.slice(7);
|
||||
}
|
||||
return ctx.headers?.get("x-api-key") ?? undefined;
|
||||
return ctx.headers?.get("x-api-key") ?? null;
|
||||
},
|
||||
rateLimit: {
|
||||
enabled: true,
|
||||
|
||||
@@ -92,14 +92,6 @@ export const socialProvidersPlugin = () => ({
|
||||
const providers = ctx.context.socialProviders.map((p) =>
|
||||
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);
|
||||
},
|
||||
),
|
||||
|
||||
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,
|
||||
"tag": "20260402225628_AddTeamWorkspacePlan",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 32,
|
||||
"version": "7",
|
||||
"when": 1776809379931,
|
||||
"tag": "20260421220939_AddCardNumber",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -209,6 +209,7 @@ export const getByPublicId = async (
|
||||
workspace: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
cardPrefix: true,
|
||||
},
|
||||
with: {
|
||||
members: {
|
||||
@@ -220,6 +221,7 @@ export const getByPublicId = async (
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
image: true,
|
||||
@@ -254,6 +256,7 @@ export const getByPublicId = async (
|
||||
listId: true,
|
||||
index: true,
|
||||
dueDate: true,
|
||||
cardNumber: true,
|
||||
},
|
||||
with: {
|
||||
labels: {
|
||||
@@ -278,6 +281,7 @@ export const getByPublicId = async (
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
image: true,
|
||||
@@ -422,6 +426,7 @@ export const getBySlug = async (
|
||||
publicId: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
cardPrefix: true,
|
||||
},
|
||||
},
|
||||
labels: {
|
||||
@@ -448,6 +453,7 @@ export const getBySlug = async (
|
||||
listId: true,
|
||||
index: true,
|
||||
dueDate: true,
|
||||
cardNumber: true,
|
||||
},
|
||||
with: {
|
||||
labels: {
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
labels,
|
||||
lists,
|
||||
workspaceMembers,
|
||||
workspaces,
|
||||
} from "@kan/db/schema";
|
||||
import { generateUID } from "@kan/shared/utils";
|
||||
|
||||
@@ -41,6 +42,7 @@ export const create = async (
|
||||
description: string;
|
||||
createdBy: string;
|
||||
listId: number;
|
||||
workspaceId: number;
|
||||
position: "start" | "end";
|
||||
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
|
||||
.insert(cards)
|
||||
.values({
|
||||
@@ -91,9 +104,10 @@ export const create = async (
|
||||
createdBy: cardInput.createdBy,
|
||||
listId: cardInput.listId,
|
||||
index: index,
|
||||
cardNumber,
|
||||
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");
|
||||
|
||||
@@ -241,6 +255,14 @@ export const getByPublicId = (db: dbClient, cardPublicId: string) => {
|
||||
listId: true,
|
||||
dueDate: true,
|
||||
},
|
||||
with: {
|
||||
list: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: eq(cards.publicId, cardPublicId),
|
||||
});
|
||||
};
|
||||
@@ -265,6 +287,7 @@ export const bulkCreate = async (
|
||||
description: string;
|
||||
createdBy: string;
|
||||
listId: number;
|
||||
workspaceId: number;
|
||||
index: number;
|
||||
importId?: number;
|
||||
}[],
|
||||
@@ -280,6 +303,34 @@ export const bulkCreate = async (
|
||||
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: {
|
||||
publicId: string;
|
||||
title: string;
|
||||
@@ -287,6 +338,7 @@ export const bulkCreate = async (
|
||||
createdBy: string;
|
||||
listId: number;
|
||||
index: number;
|
||||
cardNumber: number;
|
||||
importId?: number;
|
||||
}[] = [];
|
||||
|
||||
@@ -301,6 +353,12 @@ export const bulkCreate = async (
|
||||
let nextIndex = last ? last.index + 1 : 0;
|
||||
const sorted = [...items].sort((a, b) => a.index - b.index);
|
||||
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({
|
||||
publicId: it.publicId,
|
||||
title: it.title,
|
||||
@@ -308,6 +366,7 @@ export const bulkCreate = async (
|
||||
createdBy: it.createdBy,
|
||||
listId: it.listId,
|
||||
index: nextIndex++,
|
||||
cardNumber,
|
||||
importId: it.importId,
|
||||
});
|
||||
}
|
||||
@@ -426,6 +485,7 @@ export const getWithListAndMembersByPublicId = async (
|
||||
description: true,
|
||||
dueDate: true,
|
||||
createdBy: true,
|
||||
cardNumber: true,
|
||||
},
|
||||
with: {
|
||||
labels: {
|
||||
@@ -502,6 +562,7 @@ export const getWithListAndMembersByPublicId = async (
|
||||
workspace: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
cardPrefix: true,
|
||||
},
|
||||
with: {
|
||||
members: {
|
||||
@@ -945,7 +1006,7 @@ export const getWorkspaceAndCardIdByCardPublicId = async (
|
||||
where: and(eq(cards.publicId, cardPublicId), isNull(cards.deletedAt)),
|
||||
with: {
|
||||
list: {
|
||||
columns: { name: true },
|
||||
columns: { name: true, publicId: true },
|
||||
with: {
|
||||
board: {
|
||||
columns: {
|
||||
@@ -966,6 +1027,7 @@ export const getWorkspaceAndCardIdByCardPublicId = async (
|
||||
createdBy: result.createdBy,
|
||||
workspaceId: result.list.board.workspaceId,
|
||||
workspaceVisibility: result.list.board.visibility,
|
||||
listPublicId: result.list.publicId,
|
||||
listName: result.list.name,
|
||||
boardPublicId: result.list.board.publicId,
|
||||
boardName: result.list.board.name,
|
||||
|
||||
@@ -209,10 +209,12 @@ export const getByPublicId = async (db: dbClient, listPublicId: string) => {
|
||||
return db.query.lists.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
publicId: true,
|
||||
name: true,
|
||||
boardId: 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
|
||||
? {
|
||||
id: result.id,
|
||||
publicId: listPublicId,
|
||||
name: result.name,
|
||||
createdBy: result.createdBy,
|
||||
workspaceId: result.board.workspaceId,
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import {
|
||||
and,
|
||||
asc,
|
||||
count,
|
||||
desc,
|
||||
eq,
|
||||
ilike,
|
||||
inArray,
|
||||
isNull,
|
||||
asc,
|
||||
or,
|
||||
sql,
|
||||
} from "drizzle-orm";
|
||||
|
||||
import type { dbClient } from "@kan/db/client";
|
||||
import type { Permission, Role } from "@kan/shared";
|
||||
import {
|
||||
boards,
|
||||
cards,
|
||||
@@ -19,8 +20,11 @@ import {
|
||||
workspaceMembers,
|
||||
workspaces,
|
||||
} from "@kan/db/schema";
|
||||
import type { Permission, Role } from "@kan/shared";
|
||||
import { generateUID, getDefaultPermissions } from "@kan/shared";
|
||||
import {
|
||||
generateUID,
|
||||
generateWorkspacePrefix,
|
||||
getDefaultPermissions,
|
||||
} from "@kan/shared";
|
||||
|
||||
import * as permissionRepo from "./permission.repo";
|
||||
|
||||
@@ -30,22 +34,22 @@ const SYSTEM_ROLES: {
|
||||
description: string;
|
||||
hierarchyLevel: number;
|
||||
}[] = [
|
||||
{
|
||||
name: "admin",
|
||||
description: "Full access to all workspace features",
|
||||
hierarchyLevel: 100,
|
||||
},
|
||||
{
|
||||
name: "member",
|
||||
description: "Standard member with create and edit permissions",
|
||||
hierarchyLevel: 50,
|
||||
},
|
||||
{
|
||||
name: "guest",
|
||||
description: "View-only access",
|
||||
hierarchyLevel: 10,
|
||||
},
|
||||
];
|
||||
{
|
||||
name: "admin",
|
||||
description: "Full access to all workspace features",
|
||||
hierarchyLevel: 100,
|
||||
},
|
||||
{
|
||||
name: "member",
|
||||
description: "Standard member with create and edit permissions",
|
||||
hierarchyLevel: 50,
|
||||
},
|
||||
{
|
||||
name: "guest",
|
||||
description: "View-only access",
|
||||
hierarchyLevel: 10,
|
||||
},
|
||||
];
|
||||
|
||||
export const getCount = async (db: dbClient) => {
|
||||
const result = await db
|
||||
@@ -75,8 +79,12 @@ export const create = async (
|
||||
name: workspaceInput.name,
|
||||
slug: workspaceInput.slug,
|
||||
createdBy: workspaceInput.createdBy,
|
||||
...(workspaceInput.description && { description: workspaceInput.description }),
|
||||
...(workspaceInput.description && {
|
||||
description: workspaceInput.description,
|
||||
}),
|
||||
...(workspaceInput.plan && { plan: workspaceInput.plan }),
|
||||
cardPrefix: generateWorkspacePrefix(workspaceInput.name),
|
||||
cardCounter: 0,
|
||||
})
|
||||
.returning({
|
||||
id: workspaces.id,
|
||||
@@ -258,8 +266,12 @@ export const getBySlugWithBoards = (db: dbClient, workspaceSlug: string) => {
|
||||
slug: true,
|
||||
name: true,
|
||||
},
|
||||
where: and(isNull(boards.deletedAt), eq(boards.visibility, "public"), eq(boards.isArchived, false)),
|
||||
orderBy: [asc(boards.name)]
|
||||
where: and(
|
||||
isNull(boards.deletedAt),
|
||||
eq(boards.visibility, "public"),
|
||||
eq(boards.isArchived, false),
|
||||
),
|
||||
orderBy: [asc(boards.name)],
|
||||
},
|
||||
},
|
||||
where: and(
|
||||
@@ -283,6 +295,7 @@ export const getAllByUserId = async (db: dbClient, userId: string) => {
|
||||
slug: true,
|
||||
plan: true,
|
||||
weekStartDay: true,
|
||||
cardPrefix: true,
|
||||
deletedAt: true,
|
||||
},
|
||||
// https://github.com/drizzle-team/drizzle-orm/issues/2903
|
||||
@@ -365,6 +378,14 @@ export const isUserInWorkspace = async (
|
||||
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 (
|
||||
db: dbClient,
|
||||
workspaceId: number,
|
||||
@@ -373,6 +394,8 @@ export const searchBoardsAndCards = async (
|
||||
) => {
|
||||
const searchQuery = `%${query}%`;
|
||||
|
||||
const ticketId = parseTicketId(query.trim());
|
||||
|
||||
// Search for boards
|
||||
const boardResults = await db
|
||||
.select({
|
||||
@@ -400,25 +423,19 @@ export const searchBoardsAndCards = async (
|
||||
sql`similarity(${boards.name}, ${query}) DESC`,
|
||||
desc(boards.updatedAt),
|
||||
)
|
||||
.limit(Math.ceil(limit * 0.4));
|
||||
.limit(ticketId ? 0 : Math.ceil(limit * 0.4));
|
||||
|
||||
// Search for cards
|
||||
const cardResults = await db
|
||||
.select({
|
||||
publicId: cards.publicId,
|
||||
title: cards.title,
|
||||
description: cards.description,
|
||||
boardPublicId: boards.publicId,
|
||||
boardName: boards.name,
|
||||
listName: lists.name,
|
||||
updatedAt: cards.updatedAt,
|
||||
createdAt: cards.createdAt,
|
||||
})
|
||||
.from(cards)
|
||||
.innerJoin(lists, eq(cards.listId, lists.id))
|
||||
.innerJoin(boards, eq(lists.boardId, boards.id))
|
||||
.where(
|
||||
and(
|
||||
// Search for cards by ticket ID or by title
|
||||
const cardWhereConditions = ticketId
|
||||
? and(
|
||||
eq(boards.workspaceId, workspaceId),
|
||||
eq(cards.cardNumber, ticketId.number),
|
||||
ilike(workspaces.cardPrefix, ticketId.prefix),
|
||||
isNull(cards.deletedAt),
|
||||
isNull(lists.deletedAt),
|
||||
isNull(boards.deletedAt),
|
||||
)
|
||||
: and(
|
||||
eq(boards.workspaceId, workspaceId),
|
||||
or(
|
||||
ilike(cards.title, searchQuery),
|
||||
@@ -427,19 +444,46 @@ export const searchBoardsAndCards = async (
|
||||
isNull(cards.deletedAt),
|
||||
isNull(lists.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(
|
||||
sql`CASE WHEN ${cards.title} ILIKE ${searchQuery} THEN 1 ELSE 0 END DESC`,
|
||||
sql`similarity(${cards.title}, ${query}) DESC`,
|
||||
desc(cards.updatedAt),
|
||||
...(ticketId
|
||||
? [desc(cards.createdAt)]
|
||||
: [
|
||||
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
|
||||
const allResults = [
|
||||
...boardResults.map((board) => ({ ...board, type: "board" as const })),
|
||||
...cardResults.map((card) => ({ ...card, type: "card" as const })),
|
||||
...boardResults.map((board) => ({
|
||||
...board,
|
||||
type: "board" as const,
|
||||
})),
|
||||
...cardResults.map((card) => ({
|
||||
...card,
|
||||
type: "card" as const,
|
||||
})),
|
||||
];
|
||||
|
||||
// Ensure we don't exceed the total limit
|
||||
|
||||
@@ -2,6 +2,7 @@ import { relations } from "drizzle-orm";
|
||||
import {
|
||||
bigint,
|
||||
bigserial,
|
||||
index,
|
||||
integer,
|
||||
pgEnum,
|
||||
pgTable,
|
||||
@@ -54,27 +55,36 @@ export type ActivityType = (typeof activityTypes)[number];
|
||||
|
||||
export const activityTypeEnum = pgEnum("card_activity_type", activityTypes);
|
||||
|
||||
export const cards = pgTable("card", {
|
||||
id: bigserial("id", { mode: "number" }).primaryKey(),
|
||||
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
|
||||
title: text("title").notNull(),
|
||||
description: text("description"),
|
||||
index: integer("index").notNull(),
|
||||
createdBy: uuid("createdBy").references(() => users.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updatedAt"),
|
||||
deletedAt: timestamp("deletedAt"),
|
||||
deletedBy: uuid("deletedBy").references(() => users.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
listId: bigint("listId", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => lists.id, { onDelete: "cascade" }),
|
||||
importId: bigint("importId", { mode: "number" }).references(() => imports.id),
|
||||
dueDate: timestamp("dueDate"),
|
||||
}).enableRLS();
|
||||
export const cards = pgTable(
|
||||
"card",
|
||||
{
|
||||
id: bigserial("id", { mode: "number" }).primaryKey(),
|
||||
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
|
||||
title: text("title").notNull(),
|
||||
description: text("description"),
|
||||
index: integer("index").notNull(),
|
||||
cardNumber: integer("cardNumber"),
|
||||
createdBy: uuid("createdBy").references(() => users.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updatedAt"),
|
||||
deletedAt: timestamp("deletedAt"),
|
||||
deletedBy: uuid("deletedBy").references(() => users.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
listId: bigint("listId", { mode: "number" })
|
||||
.notNull()
|
||||
.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 }) => ({
|
||||
createdBy: one(users, {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
bigint,
|
||||
bigserial,
|
||||
boolean,
|
||||
index,
|
||||
integer,
|
||||
pgEnum,
|
||||
pgTable,
|
||||
@@ -38,25 +39,31 @@ export const workspacePlans = ["free", "team", "pro", "enterprise"] as const;
|
||||
export type WorkspacePlan = (typeof workspacePlans)[number];
|
||||
export const workspacePlanEnum = pgEnum("workspace_plan", workspacePlans);
|
||||
|
||||
export const workspaces = pgTable("workspace", {
|
||||
id: bigserial("id", { mode: "number" }).primaryKey(),
|
||||
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
description: text("description"),
|
||||
slug: varchar("slug", { length: 255 }).notNull().unique(),
|
||||
plan: workspacePlanEnum("plan").notNull().default("free"),
|
||||
showEmailsToMembers: boolean("showEmailsToMembers").notNull().default(true),
|
||||
weekStartDay: integer("weekStartDay").notNull().default(1),
|
||||
createdBy: uuid("createdBy").references(() => users.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updatedAt"),
|
||||
deletedAt: timestamp("deletedAt"),
|
||||
deletedBy: uuid("deletedBy").references(() => users.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
}).enableRLS();
|
||||
export const workspaces = pgTable(
|
||||
"workspace",
|
||||
{
|
||||
id: bigserial("id", { mode: "number" }).primaryKey(),
|
||||
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
description: text("description"),
|
||||
slug: varchar("slug", { length: 255 }).notNull().unique(),
|
||||
plan: workspacePlanEnum("plan").notNull().default("free"),
|
||||
showEmailsToMembers: boolean("showEmailsToMembers").notNull().default(true),
|
||||
weekStartDay: integer("weekStartDay").notNull().default(1),
|
||||
cardPrefix: varchar("cardPrefix", { length: 10 }).notNull().default(""),
|
||||
cardCounter: integer("cardCounter").notNull().default(0),
|
||||
createdBy: uuid("createdBy").references(() => users.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updatedAt"),
|
||||
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 }) => ({
|
||||
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 "./generateSlug";
|
||||
export * from "./generateWorkspacePrefix";
|
||||
export * from "./subscriptions";
|
||||
export * from "./email";
|
||||
export * from "./dueDateFilters";
|
||||
|
||||
Reference in New Issue
Block a user