Compare commits
2 Commits
fix/select
...
refactor/w
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7367379a48 | ||
|
|
b0ac3e11de |
@@ -23,8 +23,8 @@ SMTP_REJECT_UNAUTHORIZED= # set to "false" to accept invalid certs
|
||||
NEXT_PUBLIC_DISABLE_EMAIL=
|
||||
|
||||
# S3 storage (optional)
|
||||
S3_REGION= # required for AWS S3 (e.g. us-east-1); ignored by most S3-compatible providers — defaults to us-east-1 if unset
|
||||
S3_ENDPOINT= # set for S3-compatible providers (MinIO, R2, Spaces, etc.); leave blank for AWS S3
|
||||
S3_REGION=
|
||||
S3_ENDPOINT=
|
||||
S3_ACCESS_KEY_ID=
|
||||
S3_SECRET_ACCESS_KEY=
|
||||
S3_FORCE_PATH_STYLE=
|
||||
|
||||
@@ -227,12 +227,6 @@ We welcome contributions! Please read our [contribution guidelines](CONTRIBUTING
|
||||
<img src="https://contrib.rocks/image?repo=kanbn/kan" />
|
||||
</a>
|
||||
|
||||
## Sponsors ❤️
|
||||
|
||||
[<img height="100" alt="image" src="https://github.com/user-attachments/assets/e331c71f-ac86-46a6-bceb-ce276de094b0" />](https://www.testmuai.com)
|
||||
|
||||
Proudly sponsored by [TestMu AI (formerly LambdaTest)](https://www.testmuai.com) - an AI-native testing cloud platform built for modern engineering teams. Covering everything from autonomous test creation and fast execution to testing AI agents like chatbots and voice assistants. If you're serious about testing, go check them out.
|
||||
|
||||
## License 📝
|
||||
|
||||
Kan is licensed under the [AGPLv3 license](LICENSE).
|
||||
|
||||
@@ -11,7 +11,6 @@ RUN apk update && \
|
||||
apk add --no-cache libc6-compat && \
|
||||
rm -rf /var/cache/apk/* /tmp/* || true && \
|
||||
corepack enable && \
|
||||
corepack prepare pnpm@9.14.2 --activate && \
|
||||
npm install turbo@2.3.1 --global && \
|
||||
pnpm config set store-dir ~/.pnpm-store
|
||||
|
||||
|
||||
@@ -88,12 +88,6 @@ checksums:
|
||||
7L01XJ/message: c46571856723b03262fd33f511116298
|
||||
7L01XJ/origin/0/0: feacdaf2791aaddec128f463ffa2e807
|
||||
7L01XJ/translation: c46571856723b03262fd33f511116298
|
||||
JEt3q7/message: 233710e683076c7eb6400a5c5ddd1d2a
|
||||
JEt3q7/origin/0/0: 055278d1067f7350adbbc1eaf446e43c
|
||||
JEt3q7/translation: 233710e683076c7eb6400a5c5ddd1d2a
|
||||
mkehy7/message: 71c4ebc206483ae49a2bd3dd7b6038ba
|
||||
mkehy7/origin/0/0: 055278d1067f7350adbbc1eaf446e43c
|
||||
mkehy7/translation: 71c4ebc206483ae49a2bd3dd7b6038ba
|
||||
F6pfE9/message: 3e1ec025c4a50830bbb9ad57a176630a
|
||||
F6pfE9/origin/0/0: 4b563046ba98abb3777fb1c3560866aa
|
||||
F6pfE9/origin/1/0: f64cd62b620f454998523af2a4330278
|
||||
@@ -230,8 +224,7 @@ checksums:
|
||||
Z3krJD/origin/0/0: e9da4b3a0eedd68ed126437194a482fe
|
||||
Z3krJD/translation: 0077436d9f37bfd64f3ae076a5a05040
|
||||
WnEwDO/message: 2959fd276248208b65cb27ed46b20135
|
||||
WnEwDO/origin/0/0: 055278d1067f7350adbbc1eaf446e43c
|
||||
WnEwDO/origin/1/0: d6f5120f072a821219608624f361384e
|
||||
WnEwDO/origin/0/0: d6f5120f072a821219608624f361384e
|
||||
WnEwDO/translation: 2959fd276248208b65cb27ed46b20135
|
||||
j1%2BHPc/message: baf4c3cd580291f41458f9ed28102c49
|
||||
j1%2BHPc/origin/0/0: 9d4260644f1a12f41d6b72574eeb111c
|
||||
@@ -245,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
|
||||
@@ -329,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
|
||||
@@ -350,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
|
||||
@@ -379,7 +372,8 @@ checksums:
|
||||
QD8opX/origin/2/0: faa8a2576972b4eb31565d8ea6098f43
|
||||
QD8opX/translation: 101ff39aab674c033c15ab978d0d8bac
|
||||
YhvWXb/message: 336a8c70a91adc49c15266167ee84cc0
|
||||
YhvWXb/origin/0/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
YhvWXb/origin/0/0: ff4a160eca5f8da4f075fd1dd6061fa6
|
||||
YhvWXb/origin/1/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
YhvWXb/translation: 336a8c70a91adc49c15266167ee84cc0
|
||||
i0ZMQl/message: f739058266a2cb6a3d7cca14de09c69a
|
||||
i0ZMQl/origin/0/0: 0ad2af981579a62b084b3d41462c5546
|
||||
@@ -589,9 +583,8 @@ checksums:
|
||||
pFGfsR/origin/0/0: 82383b95848d24b9311ceaef0ec94bb6
|
||||
pFGfsR/translation: b0d0be4f63c16552b3aa795af29240d1
|
||||
5IXWmO/message: e9a430fcd298def74212238df0f680d6
|
||||
5IXWmO/origin/0/0: 055278d1067f7350adbbc1eaf446e43c
|
||||
5IXWmO/origin/1/0: 4a1be3707d14c696a16f453d132fb248
|
||||
5IXWmO/origin/2/0: d6f5120f072a821219608624f361384e
|
||||
5IXWmO/origin/0/0: 4a1be3707d14c696a16f453d132fb248
|
||||
5IXWmO/origin/1/0: d6f5120f072a821219608624f361384e
|
||||
5IXWmO/translation: e9a430fcd298def74212238df0f680d6
|
||||
GnRmu4/message: 5eb5de823f7ca5a4d97bb41e6a3f675a
|
||||
GnRmu4/origin/0/0: a2dd1fc876bd42b9db7ca7aaa57ac5e2
|
||||
@@ -600,13 +593,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
|
||||
5EMoSo/translation: d5825bc247cfd3e52e233f7dab8051c6
|
||||
5EMoSo/message: d5825bc247cfd3e52e233f7dab8051c6
|
||||
5EMoSo/origin/0/0: 8628ddb6f4ded25c0f17e2cbda8207d3
|
||||
5EMoSo/origin/1/0: 619b4be43b9080346366c4bf6ba2253d
|
||||
VHS0aJ/message: 31d0962985c83a29558c98a765bb1b16
|
||||
VHS0aJ/origin/0/0: 15792234f408027822ee71ccddde6f9b
|
||||
VHS0aJ/translation: 31d0962985c83a29558c98a765bb1b16
|
||||
@@ -626,10 +615,9 @@ checksums:
|
||||
RRn9jf/origin/1/0: d6f5120f072a821219608624f361384e
|
||||
RRn9jf/translation: 210b6ff8727f976182ec3f29ea3c7667
|
||||
yz7wBu/message: 2c2e22f8424a1031de89063bd0022e16
|
||||
yz7wBu/origin/0/0: 278a40eb45b1219f424703d0abc1d273
|
||||
yz7wBu/origin/1/0: 15fd9c05fc09ccbbb54aadc4aa5e5501
|
||||
yz7wBu/origin/2/0: afee530613110e248ffde6b56872ebed
|
||||
yz7wBu/origin/3/0: dcafca531d4dce1faef7788feabfb048
|
||||
yz7wBu/origin/0/0: 15fd9c05fc09ccbbb54aadc4aa5e5501
|
||||
yz7wBu/origin/1/0: afee530613110e248ffde6b56872ebed
|
||||
yz7wBu/origin/2/0: dcafca531d4dce1faef7788feabfb048
|
||||
yz7wBu/translation: 2c2e22f8424a1031de89063bd0022e16
|
||||
TcXcmk/message: a2da6c2339301e7c3ddf068c4ff9f7e8
|
||||
TcXcmk/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
||||
@@ -641,8 +629,9 @@ checksums:
|
||||
dtQIWT/origin/0/0: c0b59968b2b234167c488a05cc38710b
|
||||
dtQIWT/translation: 9d2308d578ec21bd22374e880fe3bd3c
|
||||
AUYALh/message: ee2b0671e00972773210c5be5a9ccb89
|
||||
AUYALh/origin/0/0: d77eb1f90f2fa86a55181629d88d237a
|
||||
AUYALh/origin/1/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
AUYALh/origin/0/0: ff4a160eca5f8da4f075fd1dd6061fa6
|
||||
AUYALh/origin/1/0: d77eb1f90f2fa86a55181629d88d237a
|
||||
AUYALh/origin/2/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
AUYALh/translation: ee2b0671e00972773210c5be5a9ccb89
|
||||
VbyRUy/message: 502237cd1044491c48494aa60967a985
|
||||
VbyRUy/origin/0/0: d77eb1f90f2fa86a55181629d88d237a
|
||||
@@ -712,10 +701,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
|
||||
@@ -747,9 +736,6 @@ checksums:
|
||||
uvH2xS/message: ecc2617544a6d385d18678577fa112fd
|
||||
uvH2xS/origin/0/0: 97bd206a05414e2a571020d64e166093
|
||||
uvH2xS/translation: ecc2617544a6d385d18678577fa112fd
|
||||
7mrkyO/message: d3853b58e9b736eefb0f9bf400daf4d7
|
||||
7mrkyO/origin/0/0: 1b05beba6bdd908015fa8e798717f024
|
||||
7mrkyO/translation: d3853b58e9b736eefb0f9bf400daf4d7
|
||||
9PaIxv/message: da95932e7a1465a5d21aa3b855a46dc2
|
||||
9PaIxv/origin/0/0: c0b59968b2b234167c488a05cc38710b
|
||||
9PaIxv/translation: da95932e7a1465a5d21aa3b855a46dc2
|
||||
@@ -846,11 +832,12 @@ 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: dbad0c8d7863cb41f5495264f82e7081
|
||||
j9IZZ0/origin/0/0: ff4a160eca5f8da4f075fd1dd6061fa6
|
||||
j9IZZ0/origin/1/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
j9IZZ0/translation: 7ba841d0946eb04fa3d17d74365be37b
|
||||
OwneeH/message: e97476d280d4ebfd196d6c5336f3511f
|
||||
OwneeH/origin/0/0: fadd5a5b6963ec5f261072f7e8d2140b
|
||||
@@ -1062,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
|
||||
@@ -1134,9 +1121,6 @@ checksums:
|
||||
rarRW%2F/origin/0/0: 8628ddb6f4ded25c0f17e2cbda8207d3
|
||||
rarRW%2F/origin/1/0: 8628ddb6f4ded25c0f17e2cbda8207d3
|
||||
rarRW%2F/translation: 91553b606a6ebed3534cd2e5635ce004
|
||||
sZixXc/message: db4e3e372abb1ac510c62b14d544f0f8
|
||||
sZixXc/origin/0/0: 0627e0040ca4939c9a57349e98c51797
|
||||
sZixXc/translation: db4e3e372abb1ac510c62b14d544f0f8
|
||||
sVBn4K/message: 0d7979a25d290123ed8b8906bf468f1a
|
||||
sVBn4K/origin/0/0: d465da67c0a870d198eb7f313569c137
|
||||
sVBn4K/translation: 0d7979a25d290123ed8b8906bf468f1a
|
||||
@@ -1150,7 +1134,8 @@ checksums:
|
||||
x0kMO%2F/origin/0/0: 30791e26a172d47d3cb0693e82679246
|
||||
x0kMO%2F/translation: 9da6c012e1b512d8c7fbd812626c4675
|
||||
YaZDYI/message: 57cc47581c7faaffb3b1721b6352b576
|
||||
YaZDYI/origin/0/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
YaZDYI/origin/0/0: 8628ddb6f4ded25c0f17e2cbda8207d3
|
||||
YaZDYI/origin/1/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
YaZDYI/translation: 57cc47581c7faaffb3b1721b6352b576
|
||||
lsgnzd/message: 934c6867d9bdbe6099b2c0f5a3107f78
|
||||
lsgnzd/origin/0/0: ff4a160eca5f8da4f075fd1dd6061fa6
|
||||
@@ -1259,9 +1244,6 @@ checksums:
|
||||
2POOFK/origin/4/0: 9737a9cef0646ff4bbd942b1924b9a2c
|
||||
2POOFK/origin/5/0: fadd5a5b6963ec5f261072f7e8d2140b
|
||||
2POOFK/translation: 0326365539c004f6088656f692602078
|
||||
8LLt89/translation: 9a78178dc1874543d35153ae325e2811
|
||||
8LLt89/message: 9a78178dc1874543d35153ae325e2811
|
||||
8LLt89/origin/0/0: 619b4be43b9080346366c4bf6ba2253d
|
||||
uP4V6I/message: 2d403501f0488ec7d4eac979faf051a2
|
||||
uP4V6I/origin/0/0: fadd5a5b6963ec5f261072f7e8d2140b
|
||||
uP4V6I/translation: 2d403501f0488ec7d4eac979faf051a2
|
||||
@@ -1350,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
|
||||
@@ -1398,9 +1380,6 @@ checksums:
|
||||
2Py5S%2F/message: fe1183a7439299210d8ab7379b8f4d01
|
||||
2Py5S%2F/origin/0/0: 83911e3eacbad4583e2b1647a784d154
|
||||
2Py5S%2F/translation: fe1183a7439299210d8ab7379b8f4d01
|
||||
VOwhhz/message: 613f43526ea128b2749f70878f10008c
|
||||
VOwhhz/origin/0/0: 1b05beba6bdd908015fa8e798717f024
|
||||
VOwhhz/translation: 613f43526ea128b2749f70878f10008c
|
||||
iwr7AP/message: 27c24b56895defecd10b0171b39bc93f
|
||||
iwr7AP/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
||||
iwr7AP/translation: 27c24b56895defecd10b0171b39bc93f
|
||||
@@ -1485,9 +1464,6 @@ checksums:
|
||||
"%2BdjOzj/origin/0/0": faa852a0c0e3c81fe948e394029c1ba5
|
||||
"%2BdjOzj/origin/1/0": 8628ddb6f4ded25c0f17e2cbda8207d3
|
||||
"%2BdjOzj/translation": ade922db1be6b26bc979565ce5de2bc7
|
||||
M2zwdn/translation: 4c9c0d43434ad63a70e8f956d1900602
|
||||
M2zwdn/message: 4c9c0d43434ad63a70e8f956d1900602
|
||||
M2zwdn/origin/0/0: 8628ddb6f4ded25c0f17e2cbda8207d3
|
||||
qPP4lD/message: 03eeed2d715e770259f722ba48a61ab3
|
||||
qPP4lD/origin/0/0: 8628ddb6f4ded25c0f17e2cbda8207d3
|
||||
qPP4lD/translation: 03eeed2d715e770259f722ba48a61ab3
|
||||
@@ -1533,8 +1509,9 @@ checksums:
|
||||
k7rCa%2F/origin/0/0: 3de48fb04f7df9bdbe399911cdb3fe86
|
||||
k7rCa%2F/translation: 890b7851af05f97ac53644e4154c13b4
|
||||
8Is1ih/message: 624592ba2ecf647a3fe60d493c1ee71a
|
||||
8Is1ih/origin/0/0: fadd5a5b6963ec5f261072f7e8d2140b
|
||||
8Is1ih/origin/1/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
8Is1ih/origin/0/0: ff4a160eca5f8da4f075fd1dd6061fa6
|
||||
8Is1ih/origin/1/0: fadd5a5b6963ec5f261072f7e8d2140b
|
||||
8Is1ih/origin/2/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
8Is1ih/translation: 624592ba2ecf647a3fe60d493c1ee71a
|
||||
EEHmPC/message: 8c1be80f5b2fcc112922282afbe30287
|
||||
EEHmPC/origin/0/0: 7493592314bc3b96ad96127a949ba1ba
|
||||
@@ -1554,12 +1531,6 @@ checksums:
|
||||
iQmbPb/message: ac2bcac75151e2fcd9eb768cea275a77
|
||||
iQmbPb/origin/0/0: 98e2302d0d8d12ce65c4140b87450673
|
||||
iQmbPb/translation: ac2bcac75151e2fcd9eb768cea275a77
|
||||
aDzbuM/message: b6cb6ee4c4dd2cbe0d5d6a0088dcd731
|
||||
aDzbuM/origin/0/0: 278a40eb45b1219f424703d0abc1d273
|
||||
aDzbuM/translation: b6cb6ee4c4dd2cbe0d5d6a0088dcd731
|
||||
K3LUJr/message: 6dd9358a22160ebed51ab3f36e2bb0b1
|
||||
K3LUJr/origin/0/0: 278a40eb45b1219f424703d0abc1d273
|
||||
K3LUJr/translation: 6dd9358a22160ebed51ab3f36e2bb0b1
|
||||
1njn7W/message: bb78ea2c6edd8662676c81a8a13ecb93
|
||||
1njn7W/origin/0/0: 358ea1b5babb2e9ccbf1a4c9f89dd3a5
|
||||
1njn7W/translation: bb78ea2c6edd8662676c81a8a13ecb93
|
||||
@@ -1624,26 +1595,20 @@ 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
|
||||
W%2FElkg/message: 1f527cd6d1ed602930bcaa303f503b51
|
||||
W%2FElkg/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
||||
W%2FElkg/translation: 1f527cd6d1ed602930bcaa303f503b51
|
||||
NwiNTb/translation: 9840dd63c40b860a9de1488a07b86828
|
||||
NwiNTb/message: 9840dd63c40b860a9de1488a07b86828
|
||||
NwiNTb/origin/0/0: 619b4be43b9080346366c4bf6ba2253d
|
||||
OvoEq7/message: 1606dc30b369856b9dba1fe9aec425d2
|
||||
OvoEq7/origin/0/0: 1d6b5f88cdad160a67842f6b65387448
|
||||
OvoEq7/origin/1/0: 1d6b5f88cdad160a67842f6b65387448
|
||||
OvoEq7/origin/2/0: 7493592314bc3b96ad96127a949ba1ba
|
||||
OvoEq7/translation: 1606dc30b369856b9dba1fe9aec425d2
|
||||
GnG6Oy/translation: c7910e9020804dbe5c7022044c669e68
|
||||
GnG6Oy/message: c7910e9020804dbe5c7022044c669e68
|
||||
GnG6Oy/origin/0/0: 619b4be43b9080346366c4bf6ba2253d
|
||||
wlQNTg/message: 0932e80cba1e3e0a7f52bb67ff31da32
|
||||
wlQNTg/origin/0/0: 94d3e20c29bb517dd394711d7524e478
|
||||
wlQNTg/origin/1/0: db1e755dc7d73e037f425111bf7c30a6
|
||||
@@ -1776,9 +1741,6 @@ checksums:
|
||||
TXO5TM/message: 7f1ed5d777cade7d62303e9e591bbf63
|
||||
TXO5TM/origin/0/0: feacdaf2791aaddec128f463ffa2e807
|
||||
TXO5TM/translation: 7f1ed5d777cade7d62303e9e591bbf63
|
||||
HOMO91/message: 8a7b468d8a18e30de7743e4d5841c87f
|
||||
HOMO91/origin/0/0: 278a40eb45b1219f424703d0abc1d273
|
||||
HOMO91/translation: 8a7b468d8a18e30de7743e4d5841c87f
|
||||
hXMFoN/message: cedf633d99c77ff4356e089f2d98c0a6
|
||||
hXMFoN/origin/0/0: fc0c18a095a68277a0f90a115942683b
|
||||
hXMFoN/translation: cedf633d99c77ff4356e089f2d98c0a6
|
||||
@@ -1853,9 +1815,6 @@ checksums:
|
||||
Uworke/message: 213d63da450f35dabb3ab0e35e29feed
|
||||
Uworke/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
||||
Uworke/translation: 213d63da450f35dabb3ab0e35e29feed
|
||||
nG7mus/message: 03326832c645f4ddb873a6e7eab8a639
|
||||
nG7mus/origin/0/0: 278a40eb45b1219f424703d0abc1d273
|
||||
nG7mus/translation: 03326832c645f4ddb873a6e7eab8a639
|
||||
IrZaAn/message: 1fcebe9ddb46f722a57f195efddc695d
|
||||
IrZaAn/origin/0/0: 0627e0040ca4939c9a57349e98c51797
|
||||
IrZaAn/translation: 1fcebe9ddb46f722a57f195efddc695d
|
||||
@@ -1868,9 +1827,6 @@ checksums:
|
||||
vwGkYB/message: 4c30501d085eaccea47af34212bb26a7
|
||||
vwGkYB/origin/0/0: 0627e0040ca4939c9a57349e98c51797
|
||||
vwGkYB/translation: 4c30501d085eaccea47af34212bb26a7
|
||||
Xlkxmi/message: 3251e7239a9b349a86d5785a113cd6b9
|
||||
Xlkxmi/origin/0/0: 0627e0040ca4939c9a57349e98c51797
|
||||
Xlkxmi/translation: 3251e7239a9b349a86d5785a113cd6b9
|
||||
lpIMne/message: 37ca1f4e0afc9a0b8e9617f767103c92
|
||||
lpIMne/origin/0/0: 0627e0040ca4939c9a57349e98c51797
|
||||
lpIMne/translation: 37ca1f4e0afc9a0b8e9617f767103c92
|
||||
@@ -1912,12 +1868,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
|
||||
GdgCoi/translation: c6a08c771f97d86b02cf10d8f5f2d788
|
||||
GdgCoi/message: c6a08c771f97d86b02cf10d8f5f2d788
|
||||
GdgCoi/origin/0/0: 619b4be43b9080346366c4bf6ba2253d
|
||||
Iqh0Uv/message: 0cc9dcd6ac624383e9977f66d4b3b777
|
||||
Iqh0Uv/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
||||
Iqh0Uv/origin/1/0: eb63312c63f6c8d2a5b6520c89012123
|
||||
@@ -1988,18 +1941,19 @@ checksums:
|
||||
tyHiOa/origin/34/0: afee530613110e248ffde6b56872ebed
|
||||
tyHiOa/origin/35/0: afee530613110e248ffde6b56872ebed
|
||||
tyHiOa/origin/36/0: 8628ddb6f4ded25c0f17e2cbda8207d3
|
||||
tyHiOa/origin/37/0: 7493592314bc3b96ad96127a949ba1ba
|
||||
tyHiOa/origin/38/0: 69d8634b177fdf937764b406fd477547
|
||||
tyHiOa/origin/37/0: 8628ddb6f4ded25c0f17e2cbda8207d3
|
||||
tyHiOa/origin/38/0: 7493592314bc3b96ad96127a949ba1ba
|
||||
tyHiOa/origin/39/0: 69d8634b177fdf937764b406fd477547
|
||||
tyHiOa/origin/40/0: 03c00909b41be01231cd3e843c8ee44e
|
||||
tyHiOa/origin/41/0: 5c60161e173f5eae9276d8e4c70b4ebb
|
||||
tyHiOa/origin/42/0: 83911e3eacbad4583e2b1647a784d154
|
||||
tyHiOa/origin/43/0: d465da67c0a870d198eb7f313569c137
|
||||
tyHiOa/origin/44/0: 33bf266228f835e12c0bfb0e5efb3be9
|
||||
tyHiOa/origin/45/0: ff48991e314b0b3e989a4dac59860fd5
|
||||
tyHiOa/origin/46/0: 30791e26a172d47d3cb0693e82679246
|
||||
tyHiOa/origin/47/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
tyHiOa/origin/48/0: 0dccc39d0debbf3e089cb6d292de762b
|
||||
tyHiOa/origin/40/0: 69d8634b177fdf937764b406fd477547
|
||||
tyHiOa/origin/41/0: 03c00909b41be01231cd3e843c8ee44e
|
||||
tyHiOa/origin/42/0: 5c60161e173f5eae9276d8e4c70b4ebb
|
||||
tyHiOa/origin/43/0: 83911e3eacbad4583e2b1647a784d154
|
||||
tyHiOa/origin/44/0: d465da67c0a870d198eb7f313569c137
|
||||
tyHiOa/origin/45/0: 33bf266228f835e12c0bfb0e5efb3be9
|
||||
tyHiOa/origin/46/0: ff48991e314b0b3e989a4dac59860fd5
|
||||
tyHiOa/origin/47/0: 30791e26a172d47d3cb0693e82679246
|
||||
tyHiOa/origin/48/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
tyHiOa/origin/49/0: 0dccc39d0debbf3e089cb6d292de762b
|
||||
tyHiOa/translation: 21ffcf0b00e7cd7b64f7454a95762e1d
|
||||
PZCqeW/message: 325dea6dd0348a27a6818db2c1340c98
|
||||
PZCqeW/origin/0/0: 8628ddb6f4ded25c0f17e2cbda8207d3
|
||||
@@ -2010,9 +1964,8 @@ checksums:
|
||||
fuwKpE/origin/1/0: fd0e66cef8d46e96bcf3143a12f9b6db
|
||||
fuwKpE/origin/2/0: fc0c18a095a68277a0f90a115942683b
|
||||
fuwKpE/origin/3/0: 1b05beba6bdd908015fa8e798717f024
|
||||
fuwKpE/origin/4/0: 1b05beba6bdd908015fa8e798717f024
|
||||
fuwKpE/origin/5/0: 9c8b544f5db130cd7c5eda9064ba3957
|
||||
fuwKpE/origin/6/0: faa8a2576972b4eb31565d8ea6098f43
|
||||
fuwKpE/origin/4/0: 9c8b544f5db130cd7c5eda9064ba3957
|
||||
fuwKpE/origin/5/0: faa8a2576972b4eb31565d8ea6098f43
|
||||
fuwKpE/translation: 346a18a54564881c888b95873d03df44
|
||||
nYNMZZ/message: d5a8a1b38eaf02d02cd6ac2211b42a92
|
||||
nYNMZZ/origin/0/0: 97bd206a05414e2a571020d64e166093
|
||||
@@ -2187,9 +2140,6 @@ checksums:
|
||||
SkCNhl/message: 137017b2b3e885c4894ca8c899af5bc2
|
||||
SkCNhl/origin/0/0: 89a76a14edc1c0b7876d79d7571bb8fb
|
||||
SkCNhl/translation: 137017b2b3e885c4894ca8c899af5bc2
|
||||
MpFIca/translation: 6e3f470ef5468c5195ae09f518b4dea4
|
||||
MpFIca/message: 6e3f470ef5468c5195ae09f518b4dea4
|
||||
MpFIca/origin/0/0: 7493592314bc3b96ad96127a949ba1ba
|
||||
e1v%2BJ3/message: 9aeca4f286e14966ef1c32d2abc74584
|
||||
e1v%2BJ3/origin/0/0: f64cd62b620f454998523af2a4330278
|
||||
e1v%2BJ3/translation: 9aeca4f286e14966ef1c32d2abc74584
|
||||
@@ -2233,26 +2183,17 @@ checksums:
|
||||
v3YoMA/message: 3fff865dc00435f82896fc302ea45630
|
||||
v3YoMA/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
|
||||
v3YoMA/translation: 3fff865dc00435f82896fc302ea45630
|
||||
GXGbXi/message: 8686cb61f9bd3cd7ed100cc3af819458
|
||||
GXGbXi/origin/0/0: 627b72cd19a7154ae1c28cbd7ca0c730
|
||||
GXGbXi/translation: 8686cb61f9bd3cd7ed100cc3af819458
|
||||
8AbRER/message: 3cb81c4829857556f5d117c9a23f7bcc
|
||||
8AbRER/origin/0/0: 81b173297e9c022d9ebde5f7018c95f9
|
||||
8AbRER/origin/1/0: 0eeeb7e68164ff118040918f1ef00a7a
|
||||
8AbRER/origin/2/0: 2201bf679c663a732d7f1b3c3ce3897f
|
||||
8AbRER/translation: 3cb81c4829857556f5d117c9a23f7bcc
|
||||
2gHjVM/message: 873c4eb14a7e32e7fd7793047b6028a1
|
||||
2gHjVM/origin/0/0: 627b72cd19a7154ae1c28cbd7ca0c730
|
||||
2gHjVM/origin/1/0: 627b72cd19a7154ae1c28cbd7ca0c730
|
||||
2gHjVM/origin/2/0: 0627e0040ca4939c9a57349e98c51797
|
||||
2gHjVM/origin/3/0: 0627e0040ca4939c9a57349e98c51797
|
||||
2gHjVM/translation: 873c4eb14a7e32e7fd7793047b6028a1
|
||||
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
|
||||
@@ -2287,17 +2228,10 @@ checksums:
|
||||
n1ekoW/message: ec7b8f314fe9bc6591006707484ede61
|
||||
n1ekoW/origin/0/0: 6cda8b872e4902cbc0191d9375adf4ef
|
||||
n1ekoW/translation: ec7b8f314fe9bc6591006707484ede61
|
||||
jbGGzf/translation: 7f18a6418cc7eb05158cff67247bc966
|
||||
jbGGzf/message: 7f18a6418cc7eb05158cff67247bc966
|
||||
jbGGzf/placeholders/partnerName/0: f182b91b0442585d06f8bf9545912628
|
||||
jbGGzf/origin/0/0: 055278d1067f7350adbbc1eaf446e43c
|
||||
4dGN6E/message: dd086c229fd7cc0247d4c1b5c6f096a8
|
||||
4dGN6E/origin/0/0: 055278d1067f7350adbbc1eaf446e43c
|
||||
4dGN6E/translation: dd086c229fd7cc0247d4c1b5c6f096a8
|
||||
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
|
||||
@@ -2333,15 +2267,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
|
||||
J9%2BzIR/message: d719cefeec51f2a6dd01064715a1fccc
|
||||
J9%2BzIR/origin/0/0: 055278d1067f7350adbbc1eaf446e43c
|
||||
J9%2BzIR/translation: d719cefeec51f2a6dd01064715a1fccc
|
||||
r%2BdxEH/message: c3d9e1523d1401870fbf56fa8bce8f8b
|
||||
r%2BdxEH/origin/0/0: 278a40eb45b1219f424703d0abc1d273
|
||||
r%2BdxEH/translation: c3d9e1523d1401870fbf56fa8bce8f8b
|
||||
vnS6Rf/message: 003f7f93d44b0f5c2e40bf24e7af3bd7
|
||||
vnS6Rf/origin/0/0: c0b59968b2b234167c488a05cc38710b
|
||||
vnS6Rf/translation: 003f7f93d44b0f5c2e40bf24e7af3bd7
|
||||
@@ -2349,13 +2277,9 @@ checksums:
|
||||
R6pB8R/origin/0/0: 09a6a32f9a6b415e81b4d2f8181a0e6b
|
||||
R6pB8R/translation: e5cb0428f1ac6485688f120d78742875
|
||||
veIDCY/message: 4be2811a8f3455618032ccb59e40658b
|
||||
veIDCY/origin/0/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
veIDCY/origin/0/0: 8628ddb6f4ded25c0f17e2cbda8207d3
|
||||
veIDCY/origin/1/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
veIDCY/translation: 4be2811a8f3455618032ccb59e40658b
|
||||
4%2BJaWo/translation: e346e4ed7d138dcc873db187922369da
|
||||
4%2BJaWo/message: e346e4ed7d138dcc873db187922369da
|
||||
4%2BJaWo/origin/0/0: 94d3e20c29bb517dd394711d7524e478
|
||||
4%2BJaWo/origin/1/0: 94d3e20c29bb517dd394711d7524e478
|
||||
4%2BJaWo/origin/2/0: 94d3e20c29bb517dd394711d7524e478
|
||||
Y8yzGg/message: 396a94648584067cc5795f7b32d3d663
|
||||
Y8yzGg/origin/0/0: 82a5cf8d69f098641eb357bf102cd504
|
||||
Y8yzGg/translation: 396a94648584067cc5795f7b32d3d663
|
||||
@@ -2385,9 +2309,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
|
||||
@@ -2423,14 +2347,6 @@ 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
|
||||
@@ -2441,9 +2357,6 @@ checksums:
|
||||
nhMhRr/message: 07edd8c50685a52c0969d711df26d768
|
||||
nhMhRr/origin/0/0: 1fd8ccf586705354bcae6e1186108fc7
|
||||
nhMhRr/translation: 07edd8c50685a52c0969d711df26d768
|
||||
ssK7k%2B/message: 4321c1f033c350419ad198aaafe3b6fd
|
||||
ssK7k%2B/origin/0/0: 278a40eb45b1219f424703d0abc1d273
|
||||
ssK7k%2B/translation: 4321c1f033c350419ad198aaafe3b6fd
|
||||
NcFrgC/message: bd15a7b3d8b4bce18cd42682ea1ec164
|
||||
NcFrgC/origin/0/0: 0ad2af981579a62b084b3d41462c5546
|
||||
NcFrgC/translation: bd15a7b3d8b4bce18cd42682ea1ec164
|
||||
@@ -2504,12 +2417,12 @@ checksums:
|
||||
vlxQFL/message: 730f33b8f1fc002c4f393ccd262b6664
|
||||
vlxQFL/origin/0/0: afee530613110e248ffde6b56872ebed
|
||||
vlxQFL/translation: 730f33b8f1fc002c4f393ccd262b6664
|
||||
HQVm6e/translation: ec66b543a593e9dabf47de5979892d8d
|
||||
HQVm6e/message: ec66b543a593e9dabf47de5979892d8d
|
||||
HQVm6e/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||
HQVm6e/translation: ec66b543a593e9dabf47de5979892d8d
|
||||
gKmoow/translation: 83997c77cb0dc8c8c90f7ab706aa817f
|
||||
gKmoow/message: 83997c77cb0dc8c8c90f7ab706aa817f
|
||||
gKmoow/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||
gKmoow/translation: 83997c77cb0dc8c8c90f7ab706aa817f
|
||||
OAYToL/message: 1c989752736fa41ecdd68ea890f16a15
|
||||
OAYToL/origin/0/0: 15792234f408027822ee71ccddde6f9b
|
||||
OAYToL/translation: 1c989752736fa41ecdd68ea890f16a15
|
||||
@@ -2528,9 +2441,6 @@ checksums:
|
||||
kp6L3T/message: b7eadb89c615874f416d9658d0428c4c
|
||||
kp6L3T/origin/0/0: 30791e26a172d47d3cb0693e82679246
|
||||
kp6L3T/translation: b7eadb89c615874f416d9658d0428c4c
|
||||
pEb95p/message: 1d4c2a0ec7e4c707a4470fa4262b495d
|
||||
pEb95p/origin/0/0: 1b05beba6bdd908015fa8e798717f024
|
||||
pEb95p/translation: 1d4c2a0ec7e4c707a4470fa4262b495d
|
||||
MHrjPM/message: 344e64395eaff6822a57d18623853e1a
|
||||
MHrjPM/origin/0/0: a50c4fbde8da6e0a77c4e0aef0b44dea
|
||||
MHrjPM/translation: 344e64395eaff6822a57d18623853e1a
|
||||
@@ -2578,9 +2488,6 @@ checksums:
|
||||
LeM5RY/message: dfeac280858332082ad34d2623a59863
|
||||
LeM5RY/origin/0/0: 0dccc39d0debbf3e089cb6d292de762b
|
||||
LeM5RY/translation: dfeac280858332082ad34d2623a59863
|
||||
5MPY04/message: 661f2e21a85fbe1281b141a8041c8778
|
||||
5MPY04/origin/0/0: 1b05beba6bdd908015fa8e798717f024
|
||||
5MPY04/translation: 661f2e21a85fbe1281b141a8041c8778
|
||||
W1ewR0/message: ddcac5d14d48358b043a9487b42eec16
|
||||
W1ewR0/origin/0/0: fc0c18a095a68277a0f90a115942683b
|
||||
W1ewR0/origin/1/0: 1b05beba6bdd908015fa8e798717f024
|
||||
@@ -2600,9 +2507,6 @@ 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
|
||||
@@ -2622,9 +2526,6 @@ 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
|
||||
@@ -2643,13 +2544,9 @@ 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
|
||||
@@ -2692,9 +2589,6 @@ 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
|
||||
@@ -2738,21 +2632,16 @@ checksums:
|
||||
7Ufoyg/message: 5c418701b27b9acbc5dd199719007362
|
||||
7Ufoyg/origin/0/0: 9737a9cef0646ff4bbd942b1924b9a2c
|
||||
7Ufoyg/translation: 5c418701b27b9acbc5dd199719007362
|
||||
uXKtPH/translation: 8a95d730b360db6041001450aa751ab4
|
||||
uXKtPH/message: 8a95d730b360db6041001450aa751ab4
|
||||
uXKtPH/origin/0/0: 619b4be43b9080346366c4bf6ba2253d
|
||||
mx8%2B1u/message: f2949ca2dc18b0063af92e60779abb65
|
||||
mx8%2B1u/origin/0/0: c0b59968b2b234167c488a05cc38710b
|
||||
mx8%2B1u/origin/0/0: ff4a160eca5f8da4f075fd1dd6061fa6
|
||||
mx8%2B1u/origin/1/0: c0b59968b2b234167c488a05cc38710b
|
||||
mx8%2B1u/origin/2/0: fadd5a5b6963ec5f261072f7e8d2140b
|
||||
mx8%2B1u/origin/3/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
mx8%2B1u/origin/2/0: c0b59968b2b234167c488a05cc38710b
|
||||
mx8%2B1u/origin/3/0: fadd5a5b6963ec5f261072f7e8d2140b
|
||||
mx8%2B1u/origin/4/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
mx8%2B1u/translation: f2949ca2dc18b0063af92e60779abb65
|
||||
8AwlaR/translation: acb1fdc14ed875815ae9f8a5254bc634
|
||||
8AwlaR/message: acb1fdc14ed875815ae9f8a5254bc634
|
||||
8AwlaR/origin/0/0: 15ea2dfd5eae4e05b226b1320103476e
|
||||
8AwlaR/translation: acb1fdc14ed875815ae9f8a5254bc634
|
||||
i5yNAO/translation: 478092018a27a4e588cb5c70968ef11a
|
||||
i5yNAO/message: 478092018a27a4e588cb5c70968ef11a
|
||||
i5yNAO/origin/0/0: 7493592314bc3b96ad96127a949ba1ba
|
||||
Ws%2B3X%2B/message: 905b10eeb09fdc5ac7fc757e935ee757
|
||||
Ws%2B3X%2B/origin/0/0: fadd5a5b6963ec5f261072f7e8d2140b
|
||||
Ws%2B3X%2B/translation: 905b10eeb09fdc5ac7fc757e935ee757
|
||||
@@ -2786,15 +2675,14 @@ checksums:
|
||||
zERArS/placeholders/0/0: 06f23520ebabfffed4999b5bcf61c9cd
|
||||
zERArS/origin/0/0: 1d6b5f88cdad160a67842f6b65387448
|
||||
zERArS/translation: aca196d729401d8adc5c8ca9d558840e
|
||||
kwkhPe/message: 63c3b52882e0d779859307d672c178c2
|
||||
kwkhPe/origin/0/0: 7493592314bc3b96ad96127a949ba1ba
|
||||
kwkhPe/origin/1/0: 15ea2dfd5eae4e05b226b1320103476e
|
||||
kwkhPe/translation: 63c3b52882e0d779859307d672c178c2
|
||||
IelE1h/message: 972773025e763ddf53273df6d37a729a
|
||||
IelE1h/origin/0/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
IelE1h/origin/0/0: 94d3e20c29bb517dd394711d7524e478
|
||||
IelE1h/origin/1/0: dbad0c8d7863cb41f5495264f82e7081
|
||||
IelE1h/origin/2/0: 5f6c4a0c9be5098099167871ae1564af
|
||||
IelE1h/translation: 972773025e763ddf53273df6d37a729a
|
||||
cWtfn1/message: 9a91ea70f5eb55aac96640bf0fd7feff
|
||||
cWtfn1/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||
cWtfn1/origin/0/0: ff4a160eca5f8da4f075fd1dd6061fa6
|
||||
cWtfn1/origin/1/0: 69d8634b177fdf937764b406fd477547
|
||||
cWtfn1/translation: 9a91ea70f5eb55aac96640bf0fd7feff
|
||||
b3Thhd/message: 5b7b755f05330164dbfb00096c500ee3
|
||||
b3Thhd/origin/0/0: 12c1fab543c8848bcb10107d47336736
|
||||
@@ -2864,22 +2752,12 @@ checksums:
|
||||
"%2BEkBs0/message": 69d1d2575f2e73b08ac622bde0a45b4d
|
||||
"%2BEkBs0/origin/0/0": d1e1d7ccce60e05570bedfbb93fc9416
|
||||
"%2BEkBs0/translation": 69d1d2575f2e73b08ac622bde0a45b4d
|
||||
7sdSkl/message: 6132de0281f288a30690bb2218089951
|
||||
7sdSkl/placeholders/magicLinkRecipient/0: 63ab7807b81db57469a3b7fa40e6826e
|
||||
7sdSkl/origin/0/0: 055278d1067f7350adbbc1eaf446e43c
|
||||
7sdSkl/translation: 6132de0281f288a30690bb2218089951
|
||||
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
|
||||
@@ -2893,9 +2771,6 @@ 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
|
||||
@@ -3002,9 +2877,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
|
||||
@@ -3039,24 +2914,18 @@ checksums:
|
||||
MdN5zD/message: e2e816f2b7a13b1056d79e7f25088664
|
||||
MdN5zD/origin/0/0: 0dccc39d0debbf3e089cb6d292de762b
|
||||
MdN5zD/translation: e2e816f2b7a13b1056d79e7f25088664
|
||||
ZhQoZn/message: b05a1191bf048d76e7d6c1c3e76ce1fd
|
||||
ZhQoZn/origin/0/0: 0627e0040ca4939c9a57349e98c51797
|
||||
ZhQoZn/translation: b05a1191bf048d76e7d6c1c3e76ce1fd
|
||||
2SePRT/message: 257b840726f972f384243a72767f880f
|
||||
2SePRT/origin/0/0: 6cda8b872e4902cbc0191d9375adf4ef
|
||||
2SePRT/translation: 257b840726f972f384243a72767f880f
|
||||
uOqaMC/message: 24fc6cdc8740f37a83df85f582f03293
|
||||
uOqaMC/origin/0/0: 6cda8b872e4902cbc0191d9375adf4ef
|
||||
uOqaMC/translation: 24fc6cdc8740f37a83df85f582f03293
|
||||
8Zjc7q/message: 65609a209e74f230563a7c8d7258a83c
|
||||
8Zjc7q/origin/0/0: 278a40eb45b1219f424703d0abc1d273
|
||||
8Zjc7q/translation: 65609a209e74f230563a7c8d7258a83c
|
||||
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
|
||||
@@ -3075,18 +2944,9 @@ checksums:
|
||||
PELbXB/message: 6984aed03bbcd94041c5dd20f62e56ed
|
||||
PELbXB/origin/0/0: 9d4260644f1a12f41d6b72574eeb111c
|
||||
PELbXB/translation: 6984aed03bbcd94041c5dd20f62e56ed
|
||||
ygpeZ8/message: 59444b15ae58f987931a4746f4c77a0b
|
||||
ygpeZ8/origin/0/0: 278a40eb45b1219f424703d0abc1d273
|
||||
ygpeZ8/translation: 59444b15ae58f987931a4746f4c77a0b
|
||||
"%2BA6Wuq/message": 36d33d3622921dbb1475da2acb98f754
|
||||
"%2BA6Wuq/origin/0/0": 278a40eb45b1219f424703d0abc1d273
|
||||
"%2BA6Wuq/translation": 36d33d3622921dbb1475da2acb98f754
|
||||
Ozt2vv/message: 8f730f68d52b7efa727aa0784282ebcb
|
||||
Ozt2vv/origin/0/0: 0627e0040ca4939c9a57349e98c51797
|
||||
Ozt2vv/translation: 8f730f68d52b7efa727aa0784282ebcb
|
||||
EuK5ez/message: d262a1ba9ebeea549d982f9cf85a6c18
|
||||
EuK5ez/origin/0/0: 0627e0040ca4939c9a57349e98c51797
|
||||
EuK5ez/translation: d262a1ba9ebeea549d982f9cf85a6c18
|
||||
"%2FtOdd2/message": 099e1780348cd7bf617721344371a430
|
||||
"%2FtOdd2/origin/0/0": 03c00909b41be01231cd3e843c8ee44e
|
||||
"%2FtOdd2/translation": 099e1780348cd7bf617721344371a430
|
||||
@@ -3102,12 +2962,12 @@ checksums:
|
||||
25fAUC/message: 3e4cce96bc1197a1caa2b7fb4d3c7e53
|
||||
25fAUC/origin/0/0: d1e1d7ccce60e05570bedfbb93fc9416
|
||||
25fAUC/translation: 3e4cce96bc1197a1caa2b7fb4d3c7e53
|
||||
GRAGsB/translation: 751a534fd6e62a92ff64c46894c7db86
|
||||
GRAGsB/message: 751a534fd6e62a92ff64c46894c7db86
|
||||
GRAGsB/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||
GRAGsB/translation: 751a534fd6e62a92ff64c46894c7db86
|
||||
j0o6ml/translation: 1e036ffca9d9cb9e2941b83fbbf375c0
|
||||
j0o6ml/message: 1e036ffca9d9cb9e2941b83fbbf375c0
|
||||
j0o6ml/origin/0/0: 69d8634b177fdf937764b406fd477547
|
||||
j0o6ml/translation: 1e036ffca9d9cb9e2941b83fbbf375c0
|
||||
GnSSGm/message: 98de6f65c567c6654cc0adce3c9d5656
|
||||
GnSSGm/origin/0/0: 33bf266228f835e12c0bfb0e5efb3be9
|
||||
GnSSGm/translation: 98de6f65c567c6654cc0adce3c9d5656
|
||||
@@ -3120,10 +2980,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
|
||||
|
||||
@@ -46,7 +46,6 @@ interface FormValues {
|
||||
interface AuthProps {
|
||||
setIsMagicLinkSent: (value: boolean, recipient: string) => void;
|
||||
isSignUp?: boolean;
|
||||
callbackURL?: string;
|
||||
}
|
||||
|
||||
const EmailSchema = z.object({
|
||||
@@ -153,11 +152,7 @@ const availableSocialProviders = {
|
||||
},
|
||||
};
|
||||
|
||||
export function Auth({
|
||||
setIsMagicLinkSent,
|
||||
isSignUp,
|
||||
callbackURL: callbackURLProp,
|
||||
}: AuthProps) {
|
||||
export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
|
||||
const [isCloudEnv, setIsCloudEnv] = useState(false);
|
||||
const [isLoginWithProviderPending, setIsLoginWithProviderPending] =
|
||||
useState<null | AuthProvider>(null);
|
||||
@@ -170,7 +165,7 @@ export function Auth({
|
||||
const passwordRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const redirect = useSearchParams().get("next");
|
||||
const callbackURL = callbackURLProp ?? redirect ?? "/boards";
|
||||
const callbackURL = redirect ?? "/boards";
|
||||
|
||||
// Safely get environment variables on client side to avoid hydration mismatch
|
||||
useEffect(() => {
|
||||
|
||||
@@ -33,7 +33,6 @@ type SearchResult =
|
||||
boardPublicId: string;
|
||||
boardName: string;
|
||||
listName: string;
|
||||
cardNumber: number | null;
|
||||
updatedAt: Date | null;
|
||||
createdAt: Date;
|
||||
type: "card";
|
||||
@@ -160,17 +159,8 @@ export default function CommandPallette({
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 text-left">
|
||||
<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 className="truncate text-sm font-bold text-light-900 dark:text-dark-900">
|
||||
{result.title}
|
||||
</div>
|
||||
{result.type === "card" && (
|
||||
<div className="truncate text-xs text-light-700 dark:text-dark-700">
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { env } from "next-runtime-env";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { env } from "next-runtime-env";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
TbLayoutSidebarLeftCollapse,
|
||||
@@ -14,12 +13,8 @@ import { authClient } from "@kan/auth/client";
|
||||
|
||||
import { useClickOutside } from "~/hooks/useClickOutside";
|
||||
import { useModal } from "~/providers/modal";
|
||||
import { usePopup } from "~/providers/popup";
|
||||
import { useWorkspace, WorkspaceProvider } from "~/providers/workspace";
|
||||
import { api } from "~/utils/api";
|
||||
import { ChangePasswordFormConfirmation } from "~/views/settings/components/ChangePasswordConfirmation";
|
||||
import Button from "./Button";
|
||||
import Modal from "./modal";
|
||||
import SideNavigation from "./SideNavigation";
|
||||
|
||||
interface DashboardProps {
|
||||
@@ -48,11 +43,9 @@ export default function Dashboard({
|
||||
hasRightPanel = false,
|
||||
}: DashboardProps) {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const { openModal, closeModal, modalContentType } = useModal();
|
||||
const { openModal } = useModal();
|
||||
const { availableWorkspaces, hasLoaded } = useWorkspace();
|
||||
const { showPopup } = usePopup();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const { data: session, isPending: sessionLoading } = authClient.useSession();
|
||||
const { data: user, isLoading: userLoading } = api.user.getUser.useQuery(
|
||||
@@ -106,67 +99,16 @@ export default function Dashboard({
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const partnerActivated = searchParams.get("partner_activated");
|
||||
const partnerError = searchParams.get("partner_error");
|
||||
|
||||
if (partnerActivated) {
|
||||
showPopup({
|
||||
header: t`License activated`,
|
||||
message: t`Your license has been activated successfully.`,
|
||||
icon: "success",
|
||||
});
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.delete("partner_activated");
|
||||
router.replace(`?${params.toString()}`);
|
||||
} else if (partnerError) {
|
||||
const messages: Record<string, string> = {
|
||||
invalid_license: t`That license key could not be found. Please contact support.`,
|
||||
license_inactive: t`Your license is not active. Please check your account.`,
|
||||
missing_license: t`No license key was provided. Please try activating again.`,
|
||||
};
|
||||
showPopup({
|
||||
header: t`License activation failed`,
|
||||
message:
|
||||
messages[partnerError] ??
|
||||
t`Something went wrong during license activation.`,
|
||||
icon: "error",
|
||||
});
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.delete("partner_error");
|
||||
router.replace(`?${params.toString()}`);
|
||||
}
|
||||
}, [searchParams, showPopup, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasLoaded && availableWorkspaces.length === 0) {
|
||||
if (env("NEXT_PUBLIC_KAN_ENV") === "cloud") {
|
||||
router.push(
|
||||
`/onboarding/select-plan?returnUrl=${encodeURIComponent(window.location.pathname)}`,
|
||||
);
|
||||
router.push(`/onboarding/select-plan?returnUrl=${encodeURIComponent(window.location.pathname)}`);
|
||||
} else {
|
||||
openModal("NEW_WORKSPACE", undefined, undefined, false);
|
||||
}
|
||||
}
|
||||
}, [hasLoaded, availableWorkspaces.length, openModal, router]);
|
||||
|
||||
useEffect(() => {
|
||||
const isCredentialsEnabled =
|
||||
env("NEXT_PUBLIC_ALLOW_CREDENTIALS")?.toLowerCase() === "true";
|
||||
|
||||
if (
|
||||
!userLoading &&
|
||||
user &&
|
||||
isCredentialsEnabled &&
|
||||
user.hasMagicLinkAccount &&
|
||||
!user.hasPassword &&
|
||||
!sessionStorage.getItem("set_password_prompted")
|
||||
) {
|
||||
sessionStorage.setItem("set_password_prompted", "1");
|
||||
openModal("SET_PASSWORD_PROMPT");
|
||||
}
|
||||
}, [user, userLoading, openModal]);
|
||||
|
||||
const isDarkMode = resolvedTheme === "dark";
|
||||
|
||||
return (
|
||||
@@ -261,30 +203,6 @@ export default function Dashboard({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
modalSize="sm"
|
||||
isVisible={modalContentType === "SET_PASSWORD_PROMPT"}
|
||||
>
|
||||
{user?.hasPassword ? (
|
||||
<div className="p-5">
|
||||
<h2 className="pb-4 text-base font-medium dark:text-white">{t`Password already set`}</h2>
|
||||
<p className="mb-6 text-sm text-light-900">
|
||||
{t`Your account already has a password. You can change it from your account settings.`}
|
||||
</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={closeModal}
|
||||
fullWidth
|
||||
size="lg"
|
||||
>
|
||||
{t`Close`}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<ChangePasswordFormConfirmation hasPassword={false} />
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ export interface RenderSuggestionsProps {
|
||||
export interface WorkspaceMember {
|
||||
publicId: string;
|
||||
user: {
|
||||
id: string | null;
|
||||
id: string;
|
||||
name: string | null;
|
||||
image: string | null;
|
||||
} | null;
|
||||
@@ -474,7 +474,7 @@ export default function Editor({
|
||||
autolink: true,
|
||||
linkOnPaste: true,
|
||||
}),
|
||||
Markdown.configure({ transformPastedText: true }),
|
||||
Markdown,
|
||||
Placeholder.configure({
|
||||
placeholder: readOnly
|
||||
? ""
|
||||
|
||||
@@ -96,7 +96,6 @@ export function NewWorkspaceForm() {
|
||||
description: values.description,
|
||||
slug: values.slug,
|
||||
plan: values.plan,
|
||||
cardPrefix: values.cardPrefix,
|
||||
role: "admin",
|
||||
weekStartDay: 1,
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ import ButtonComponent from "~/components/Button";
|
||||
import ReactiveButton from "~/components/ReactiveButton";
|
||||
import UserMenu from "~/components/UserMenu";
|
||||
import WorkspaceMenu from "~/components/WorkspaceMenu";
|
||||
import { useModal } from "~/providers/modal";
|
||||
import { useWorkspace } from "~/providers/workspace";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
@@ -52,6 +53,7 @@ export default function SideNavigation({
|
||||
const { workspace } = useWorkspace();
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const [isInitialised, setIsInitialised] = useState(false);
|
||||
const { openModal } = useModal();
|
||||
|
||||
const { data: workspaceData } = api.workspace.byId.useQuery(
|
||||
{ workspacePublicId: workspace.publicId },
|
||||
@@ -221,19 +223,21 @@ export default function SideNavigation({
|
||||
<ButtonComponent
|
||||
iconLeft={<HiBolt />}
|
||||
variant="secondary"
|
||||
href={`/upgrade/select-plan?plan=pro&workspacePublicId=${workspace.publicId}&returnUrl=${encodeURIComponent("/settings/billing")}`}
|
||||
aria-label={t`Start free trial`}
|
||||
title={t`Start free trial`}
|
||||
href="/settings/workspace?upgrade=pro"
|
||||
aria-label="Upgrade to Pro"
|
||||
title="Upgrade to Pro"
|
||||
iconOnly
|
||||
onClick={() => openModal("UPGRADE_TO_PRO")}
|
||||
/>
|
||||
) : (
|
||||
<ButtonComponent
|
||||
iconLeft={<HiBolt />}
|
||||
fullWidth
|
||||
variant="secondary"
|
||||
href={`/upgrade/select-plan?plan=pro&workspacePublicId=${workspace.publicId}&returnUrl=${encodeURIComponent("/settings/billing")}`}
|
||||
href="/settings/workspace?upgrade=pro"
|
||||
onClick={() => openModal("UPGRADE_TO_PRO")}
|
||||
>
|
||||
{t`Start free trial`}
|
||||
{t`Upgrade to Pro`}
|
||||
</ButtonComponent>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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, ptBR, ru } from "date-fns/locale";
|
||||
import { de, enGB, es, fr, it, nl, pl, ru, ptBR} from "date-fns/locale";
|
||||
|
||||
import type { Locale } from "~/locales";
|
||||
import { useLinguiContext } from "~/providers/lingui";
|
||||
@@ -19,7 +19,7 @@ export function useLocalisation() {
|
||||
nl,
|
||||
ru,
|
||||
pl,
|
||||
ptbr: ptBR,
|
||||
"pt-BR": 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
@@ -1,19 +0,0 @@
|
||||
export type WorkspacePlan = "free" | "team" | "pro" | "enterprise";
|
||||
|
||||
export interface TierConfig {
|
||||
plan: WorkspacePlan;
|
||||
seats: number | null;
|
||||
unlimitedSeats: boolean;
|
||||
}
|
||||
|
||||
const TIER_MAP: Record<number, TierConfig> = {
|
||||
1: { plan: "team", seats: 5, unlimitedSeats: false },
|
||||
2: { plan: "pro", seats: 15, unlimitedSeats: false },
|
||||
3: { plan: "pro", seats: null, unlimitedSeats: true },
|
||||
};
|
||||
|
||||
export function tierConfig(tier: number): TierConfig {
|
||||
const config = TIER_MAP[tier];
|
||||
if (!config) throw new Error(`Unknown partner tier: ${tier}`);
|
||||
return config;
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { createNextApiContext } from "@kan/api/trpc";
|
||||
import { withApiLogging } from "@kan/api/utils/apiLogging";
|
||||
import { withRateLimit } from "@kan/api/utils/rateLimit";
|
||||
import * as subscriptionRepo from "@kan/db/repository/subscription.repo";
|
||||
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
||||
import { createLogger } from "@kan/logger";
|
||||
|
||||
import { tierConfig } from "./_utils";
|
||||
|
||||
const log = createLogger("api");
|
||||
|
||||
interface TokenResponse {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
}
|
||||
|
||||
interface OAuthLicenseResponse {
|
||||
license_key: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface LicenseDetailResponse {
|
||||
license_key: string;
|
||||
status: string;
|
||||
tier: number;
|
||||
}
|
||||
|
||||
async function exchangeCodeForToken(code: string): Promise<TokenResponse> {
|
||||
const tokenUrl = process.env.PARTNER_TOKEN_URL;
|
||||
const clientId = process.env.PARTNER_CLIENT_ID;
|
||||
const clientSecret = process.env.PARTNER_CLIENT_SECRET;
|
||||
const redirectUrl = process.env.PARTNER_REDIRECT_URL;
|
||||
if (!tokenUrl) throw new Error("PARTNER_TOKEN_URL not configured");
|
||||
if (!clientId) throw new Error("PARTNER_CLIENT_ID not configured");
|
||||
if (!clientSecret) throw new Error("PARTNER_CLIENT_SECRET not configured");
|
||||
if (!redirectUrl) throw new Error("PARTNER_REDIRECT_URL not configured");
|
||||
|
||||
const res = await fetch(tokenUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
redirect_uri: redirectUrl,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error(`Token exchange failed: ${res.status}`);
|
||||
return res.json() as Promise<TokenResponse>;
|
||||
}
|
||||
|
||||
async function fetchOAuthLicense(
|
||||
accessToken: string,
|
||||
): Promise<OAuthLicenseResponse> {
|
||||
const oauthLicenseUrl = process.env.PARTNER_OAUTH_LICENSE_URL;
|
||||
if (!oauthLicenseUrl)
|
||||
throw new Error("PARTNER_OAUTH_LICENSE_URL not configured");
|
||||
|
||||
const res = await fetch(`${oauthLicenseUrl}?access_token=${accessToken}`);
|
||||
if (!res.ok) throw new Error(`OAuth license fetch failed: ${res.status}`);
|
||||
return res.json() as Promise<OAuthLicenseResponse>;
|
||||
}
|
||||
|
||||
async function fetchLicenseDetail(
|
||||
licenseKey: string,
|
||||
): Promise<LicenseDetailResponse> {
|
||||
const licenseApiUrl = process.env.PARTNER_LICENSE_API_URL;
|
||||
const apiKey = process.env.PARTNER_API_KEY;
|
||||
if (!licenseApiUrl) throw new Error("PARTNER_LICENSE_API_URL not configured");
|
||||
if (!apiKey) throw new Error("PARTNER_API_KEY not configured");
|
||||
|
||||
const apiKeyHeader = process.env.PARTNER_API_KEY_HEADER;
|
||||
if (!apiKeyHeader) throw new Error("PARTNER_API_KEY_HEADER not configured");
|
||||
|
||||
const res = await fetch(`${licenseApiUrl}/${licenseKey}`, {
|
||||
headers: { [apiKeyHeader]: apiKey },
|
||||
});
|
||||
if (!res.ok) throw new Error(`License detail fetch failed: ${res.status}`);
|
||||
return res.json() as Promise<LicenseDetailResponse>;
|
||||
}
|
||||
|
||||
export default withRateLimit(
|
||||
{ points: 20, duration: 60 },
|
||||
withApiLogging(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method !== "GET") {
|
||||
return res.status(405).json({ message: "Method not allowed" });
|
||||
}
|
||||
|
||||
const { code } = req.query;
|
||||
|
||||
if (!code || typeof code !== "string") {
|
||||
return res.status(200).json({ message: "OK" });
|
||||
}
|
||||
|
||||
let license: LicenseDetailResponse;
|
||||
|
||||
try {
|
||||
const tokenData = await exchangeCodeForToken(code);
|
||||
const oauthLicense = await fetchOAuthLicense(tokenData.access_token);
|
||||
license = await fetchLicenseDetail(oauthLicense.license_key);
|
||||
} catch (err) {
|
||||
log.error({ err }, "Partner OAuth flow failed");
|
||||
return res.redirect(`/partner/activate?error=oauth_failed`);
|
||||
}
|
||||
|
||||
const { db, user } = await createNextApiContext(req);
|
||||
|
||||
const cfg = tierConfig(license.tier);
|
||||
const isActive = license.status === "active";
|
||||
const status = isActive ? "active" : "inactive";
|
||||
|
||||
if (!user) {
|
||||
await subscriptionRepo.upsertByPartnerLicenseKey(
|
||||
db,
|
||||
license.license_key,
|
||||
{
|
||||
plan: cfg.plan,
|
||||
status,
|
||||
partnerTier: license.tier,
|
||||
seats: cfg.seats,
|
||||
unlimitedSeats: cfg.unlimitedSeats,
|
||||
},
|
||||
);
|
||||
return res.redirect(
|
||||
`/partner/activate?license_key=${encodeURIComponent(license.license_key)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const memberships = await workspaceRepo.getAllByUserId(db, user.id);
|
||||
const workspace = memberships?.[0]?.workspace;
|
||||
|
||||
if (!workspace) {
|
||||
await subscriptionRepo.upsertByPartnerLicenseKey(
|
||||
db,
|
||||
license.license_key,
|
||||
{
|
||||
plan: cfg.plan,
|
||||
status,
|
||||
partnerTier: license.tier,
|
||||
seats: cfg.seats,
|
||||
unlimitedSeats: cfg.unlimitedSeats,
|
||||
},
|
||||
);
|
||||
return res.redirect(
|
||||
`/onboarding/workspace?license_key=${encodeURIComponent(license.license_key)}`,
|
||||
);
|
||||
}
|
||||
|
||||
await subscriptionRepo.upsertByPartnerLicenseKey(db, license.license_key, {
|
||||
plan: cfg.plan,
|
||||
status,
|
||||
partnerTier: license.tier,
|
||||
seats: cfg.seats,
|
||||
unlimitedSeats: cfg.unlimitedSeats,
|
||||
referenceId: workspace.publicId,
|
||||
});
|
||||
|
||||
if (isActive) {
|
||||
await workspaceRepo.update(db, workspace.publicId, { plan: cfg.plan });
|
||||
}
|
||||
|
||||
return res.redirect(`/?partner_activated=1`);
|
||||
}),
|
||||
);
|
||||
@@ -1,64 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { createNextApiContext } from "@kan/api/trpc";
|
||||
import { withApiLogging } from "@kan/api/utils/apiLogging";
|
||||
import { withRateLimit } from "@kan/api/utils/rateLimit";
|
||||
import * as subscriptionRepo from "@kan/db/repository/subscription.repo";
|
||||
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
||||
|
||||
export default withRateLimit(
|
||||
{ points: 20, duration: 60 },
|
||||
withApiLogging(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method !== "GET") {
|
||||
return res.status(405).json({ message: "Method not allowed" });
|
||||
}
|
||||
|
||||
const { license_key } = req.query;
|
||||
|
||||
if (!license_key || typeof license_key !== "string") {
|
||||
return res.redirect("/boards?partner_error=missing_license");
|
||||
}
|
||||
|
||||
const { db, user } = await createNextApiContext(req);
|
||||
|
||||
if (!user) {
|
||||
return res.redirect(
|
||||
`/login?next=${encodeURIComponent(`/api/partner/link?license_key=${license_key}`)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const sub = await subscriptionRepo.getByPartnerLicenseKey(db, license_key);
|
||||
|
||||
if (!sub) {
|
||||
return res.redirect("/boards?partner_error=invalid_license");
|
||||
}
|
||||
|
||||
if (sub.status !== "active") {
|
||||
return res.redirect("/boards?partner_error=license_inactive");
|
||||
}
|
||||
|
||||
const memberships = await workspaceRepo.getAllByUserId(db, user.id);
|
||||
const workspace = memberships?.[0]?.workspace;
|
||||
|
||||
if (!workspace) {
|
||||
return res.redirect(
|
||||
`/onboarding/workspace?license_key=${encodeURIComponent(license_key)}`,
|
||||
);
|
||||
}
|
||||
|
||||
await subscriptionRepo.upsertByPartnerLicenseKey(db, license_key, {
|
||||
plan: sub.plan,
|
||||
status: sub.status,
|
||||
partnerTier: sub.partnerTier ?? 1,
|
||||
seats: sub.seats ?? null,
|
||||
unlimitedSeats: sub.unlimitedSeats,
|
||||
referenceId: workspace.publicId,
|
||||
});
|
||||
|
||||
await workspaceRepo.update(db, workspace.publicId, {
|
||||
plan: sub.plan as "free" | "team" | "pro" | "enterprise",
|
||||
});
|
||||
|
||||
return res.redirect("/boards?partner_activated=1");
|
||||
}),
|
||||
);
|
||||
@@ -1,212 +0,0 @@
|
||||
import { createHmac, timingSafeEqual } from "crypto";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import type { Readable } from "node:stream";
|
||||
|
||||
import { createNextApiContext } from "@kan/api/trpc";
|
||||
import { withApiLogging } from "@kan/api/utils/apiLogging";
|
||||
import * as subscriptionRepo from "@kan/db/repository/subscription.repo";
|
||||
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
||||
import { createLogger } from "@kan/logger";
|
||||
import { getActiveSubscriptions } from "@kan/shared/utils";
|
||||
|
||||
import { tierConfig } from "./_utils";
|
||||
|
||||
const log = createLogger("api");
|
||||
|
||||
async function buffer(readable: Readable) {
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of readable) {
|
||||
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
||||
}
|
||||
return Buffer.concat(chunks);
|
||||
}
|
||||
|
||||
function verifySignature(
|
||||
rawBody: string,
|
||||
signature: string,
|
||||
timestamp: string,
|
||||
): boolean {
|
||||
const secret = process.env.PARTNER_API_KEY;
|
||||
if (!secret) return false;
|
||||
|
||||
const ts = Number(timestamp);
|
||||
if (isNaN(ts) || Date.now() - ts > 300_000) return false;
|
||||
|
||||
const payload = `${timestamp}${rawBody}`;
|
||||
const expected = createHmac("sha256", secret).update(payload).digest("hex");
|
||||
|
||||
try {
|
||||
return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
interface WebhookPayload {
|
||||
event:
|
||||
| "purchase"
|
||||
| "activate"
|
||||
| "deactivate"
|
||||
| "upgrade"
|
||||
| "downgrade"
|
||||
| "migrate";
|
||||
license_key: string;
|
||||
license_status: string;
|
||||
tier: number;
|
||||
prev_license_key?: string;
|
||||
}
|
||||
|
||||
export default withApiLogging(
|
||||
async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ message: "Method not allowed" });
|
||||
}
|
||||
|
||||
const signatureHeader = process.env.PARTNER_SIGNATURE_HEADER;
|
||||
const timestampHeader = process.env.PARTNER_TIMESTAMP_HEADER;
|
||||
|
||||
if (!signatureHeader || !timestampHeader) {
|
||||
log.error("Webhook signature headers not configured");
|
||||
return res.status(500).json({ message: "Server misconfiguration" });
|
||||
}
|
||||
|
||||
const signature = req.headers[signatureHeader] as string | undefined;
|
||||
const timestamp = req.headers[timestampHeader] as string | undefined;
|
||||
|
||||
if (!signature || !timestamp) {
|
||||
return res.status(400).json({ message: "Missing signature headers" });
|
||||
}
|
||||
|
||||
const buf = await buffer(req as unknown as Readable);
|
||||
const rawBody = buf.toString("utf8");
|
||||
|
||||
if (!verifySignature(rawBody, signature, timestamp)) {
|
||||
return res.status(401).json({ message: "Invalid signature" });
|
||||
}
|
||||
|
||||
const payload = JSON.parse(rawBody) as WebhookPayload;
|
||||
const { event, license_key, license_status, tier, prev_license_key } =
|
||||
payload;
|
||||
|
||||
log.info({ headers: req.headers, payload }, "partner webhook received");
|
||||
|
||||
const { db } = await createNextApiContext(req);
|
||||
|
||||
switch (event) {
|
||||
case "purchase": {
|
||||
const cfg = tierConfig(tier);
|
||||
await subscriptionRepo.upsertByPartnerLicenseKey(db, license_key, {
|
||||
plan: cfg.plan,
|
||||
status: license_status,
|
||||
partnerTier: tier,
|
||||
seats: cfg.seats,
|
||||
unlimitedSeats: cfg.unlimitedSeats,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "activate": {
|
||||
const cfg = tierConfig(tier);
|
||||
await subscriptionRepo.upsertByPartnerLicenseKey(db, license_key, {
|
||||
plan: cfg.plan,
|
||||
status: "active",
|
||||
partnerTier: tier,
|
||||
seats: cfg.seats,
|
||||
unlimitedSeats: cfg.unlimitedSeats,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "deactivate": {
|
||||
const sub = await subscriptionRepo.getByPartnerLicenseKey(
|
||||
db,
|
||||
license_key,
|
||||
);
|
||||
if (sub) {
|
||||
const [, allSubs] = await Promise.all([
|
||||
subscriptionRepo.updateById(db, sub.id, {
|
||||
plan: "free",
|
||||
status: "inactive",
|
||||
}),
|
||||
sub.referenceId
|
||||
? subscriptionRepo.getByReferenceId(db, sub.referenceId)
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
if (sub.referenceId) {
|
||||
const hasActiveSub = getActiveSubscriptions(allSubs).some(
|
||||
(s) => s.id !== sub.id,
|
||||
);
|
||||
if (!hasActiveSub) {
|
||||
await workspaceRepo.update(db, sub.referenceId, { plan: "free" });
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "upgrade":
|
||||
case "downgrade": {
|
||||
const lookupKey = prev_license_key ?? license_key;
|
||||
const sub = await subscriptionRepo.getByPartnerLicenseKey(
|
||||
db,
|
||||
lookupKey,
|
||||
);
|
||||
if (sub) {
|
||||
const cfg = tierConfig(tier);
|
||||
await subscriptionRepo.upsertByPartnerLicenseKey(db, license_key, {
|
||||
plan: cfg.plan,
|
||||
status: "active",
|
||||
partnerTier: tier,
|
||||
seats: cfg.seats,
|
||||
unlimitedSeats: cfg.unlimitedSeats,
|
||||
referenceId: sub.referenceId ?? undefined,
|
||||
});
|
||||
if (prev_license_key) {
|
||||
await subscriptionRepo.updateById(db, sub.id, {
|
||||
status: "inactive",
|
||||
});
|
||||
}
|
||||
if (sub.referenceId) {
|
||||
await workspaceRepo.update(db, sub.referenceId, { plan: cfg.plan });
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "migrate": {
|
||||
if (prev_license_key) {
|
||||
const sub = await subscriptionRepo.getByPartnerLicenseKey(
|
||||
db,
|
||||
prev_license_key,
|
||||
);
|
||||
if (sub) {
|
||||
const cfg = tierConfig(tier);
|
||||
await subscriptionRepo.upsertByPartnerLicenseKey(db, license_key, {
|
||||
plan: cfg.plan,
|
||||
status: "active",
|
||||
partnerTier: tier,
|
||||
seats: cfg.seats,
|
||||
unlimitedSeats: cfg.unlimitedSeats,
|
||||
referenceId: sub.referenceId ?? undefined,
|
||||
});
|
||||
await subscriptionRepo.updateById(db, sub.id, {
|
||||
status: "inactive",
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
log.warn({ event }, "Unhandled partner webhook event");
|
||||
}
|
||||
|
||||
return res.status(200).json({ success: true, event });
|
||||
},
|
||||
);
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
};
|
||||
@@ -64,10 +64,7 @@ export default async function handler(
|
||||
meta.userId &&
|
||||
meta.userEmail
|
||||
) {
|
||||
const existing = await workspaceRepo.getByPublicId(
|
||||
db,
|
||||
meta.workspacePublicId,
|
||||
);
|
||||
const existing = await workspaceRepo.getByPublicId(db, meta.workspacePublicId);
|
||||
|
||||
if (!existing) {
|
||||
const slug = meta.workspaceSlug ?? meta.workspacePublicId;
|
||||
@@ -93,10 +90,10 @@ export default async function handler(
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Existing workspace upgrade — update plan (and slug for pro)
|
||||
await workspaceRepo.update(db, meta.workspacePublicId, {
|
||||
plan,
|
||||
...(plan === "pro" &&
|
||||
meta.workspaceSlug && { slug: meta.workspaceSlug }),
|
||||
...(plan === "pro" && meta.workspaceSlug && { slug: meta.workspaceSlug }),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { env } from "next-runtime-env";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { authClient } from "@kan/auth/client";
|
||||
|
||||
import { Auth } from "~/components/AuthForm";
|
||||
import { PageHead } from "~/components/PageHead";
|
||||
import PatternedBackground from "~/components/PatternedBackground";
|
||||
|
||||
export default function PartnerActivatePage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const licenseKey = searchParams.get("license_key");
|
||||
const error = searchParams.get("error");
|
||||
|
||||
const partnerName = env("NEXT_PUBLIC_PARTNER_NAME");
|
||||
|
||||
const { data: session, isPending } = authClient.useSession();
|
||||
const [isMagicLinkSent, setIsMagicLinkSent] = useState(false);
|
||||
const [magicLinkRecipient, setMagicLinkRecipient] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (licenseKey) {
|
||||
localStorage.setItem("partnerLicenseKey", licenseKey);
|
||||
}
|
||||
}, [licenseKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPending && session?.user && licenseKey) {
|
||||
router.push(
|
||||
`/api/partner/link?license_key=${encodeURIComponent(licenseKey)}`,
|
||||
);
|
||||
} else if (!isPending && session?.user && !licenseKey) {
|
||||
router.push("/boards");
|
||||
}
|
||||
}, [session, isPending, licenseKey, router]);
|
||||
|
||||
if (isPending || (session?.user && licenseKey)) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={t`Activate | kan.bn`} />
|
||||
<main className="h-screen bg-light-100 pt-20 dark:bg-dark-50 sm:pt-0">
|
||||
<div className="justify-top flex h-full flex-col items-center px-4 sm:justify-center">
|
||||
<div className="z-10 flex w-full flex-col items-center">
|
||||
<Link href="/">
|
||||
<h1 className="mb-6 text-lg font-bold tracking-tight text-light-1000 dark:text-dark-1000">
|
||||
kan.bn
|
||||
</h1>
|
||||
</Link>
|
||||
<p className="mb-2 text-3xl font-bold tracking-tight text-light-1000 dark:text-dark-1000">
|
||||
{isMagicLinkSent ? t`Check your inbox` : t`Activate your account`}
|
||||
</p>
|
||||
<p className="mb-10 text-sm text-light-800 dark:text-dark-800">
|
||||
{isMagicLinkSent ? (
|
||||
<Trans>We sent a link to {magicLinkRecipient}</Trans>
|
||||
) : partnerName ? (
|
||||
<Trans>
|
||||
Sign in or create an account to activate your {partnerName}{" "}
|
||||
license
|
||||
</Trans>
|
||||
) : (
|
||||
t`Sign in or create an account to activate your license`
|
||||
)}
|
||||
</p>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 w-full rounded-md bg-red-50 px-4 py-3 text-sm text-red-700 dark:bg-red-900/20 dark:text-red-400 sm:max-w-md">
|
||||
{t`Something went wrong during activation. Please try again.`}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isMagicLinkSent && (
|
||||
<div className="w-full rounded-lg border border-light-500 bg-light-300 px-4 py-10 dark:border-dark-400 dark:bg-dark-200 sm:max-w-md lg:px-10">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
<Auth
|
||||
setIsMagicLinkSent={(val, recipient) => {
|
||||
setIsMagicLinkSent(val);
|
||||
setMagicLinkRecipient(recipient);
|
||||
}}
|
||||
callbackURL={
|
||||
licenseKey
|
||||
? `/api/partner/link?license_key=${encodeURIComponent(licenseKey)}`
|
||||
: "/boards"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="mt-4 text-sm text-light-1000 dark:text-dark-1000">
|
||||
<Trans>
|
||||
Already have an account?{" "}
|
||||
<span className="underline">
|
||||
<Link
|
||||
href={
|
||||
licenseKey
|
||||
? `/login?next=${encodeURIComponent(`/api/partner/link?license_key=${licenseKey}`)}`
|
||||
: "/login"
|
||||
}
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
</span>
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<PatternedBackground />
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import { env } from "next-runtime-env";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { authClient } from "@kan/auth/client";
|
||||
|
||||
import { PageHead } from "~/components/PageHead";
|
||||
import SelectPlanView from "~/views/onboarding/select-plan";
|
||||
|
||||
export default function UpgradeSelectPlanPage() {
|
||||
const router = useRouter();
|
||||
const { data: session, isPending } = authClient.useSession();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPending && !session?.user) {
|
||||
router.push("/login");
|
||||
}
|
||||
if (!isPending && env("NEXT_PUBLIC_KAN_ENV") !== "cloud") {
|
||||
router.push("/boards");
|
||||
}
|
||||
}, [session, isPending, router]);
|
||||
|
||||
if (isPending || !session?.user) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title="Upgrade | kan.bn" />
|
||||
<SelectPlanView />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -20,7 +20,6 @@ interface Workspace {
|
||||
plan: "free" | "team" | "pro" | "enterprise" | undefined;
|
||||
role: "admin" | "member" | "guest";
|
||||
weekStartDay: 0 | 1 | 6;
|
||||
cardPrefix: string;
|
||||
}
|
||||
|
||||
const initialWorkspace: Workspace = {
|
||||
@@ -31,7 +30,6 @@ const initialWorkspace: Workspace = {
|
||||
plan: "free" as const,
|
||||
role: "member",
|
||||
weekStartDay: 1,
|
||||
cardPrefix: "",
|
||||
};
|
||||
|
||||
const initialAvailableWorkspaces: Workspace[] = [];
|
||||
@@ -58,12 +56,9 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
|
||||
const pollAttemptsRef = React.useRef(0);
|
||||
const MAX_POLL_ATTEMPTS = 5;
|
||||
|
||||
const { data, isLoading, isFetching } = api.workspace.all.useQuery(
|
||||
undefined,
|
||||
{
|
||||
refetchInterval: pendingWorkspaceId ? 2000 : false,
|
||||
},
|
||||
);
|
||||
const { data, isLoading } = api.workspace.all.useQuery(undefined, {
|
||||
refetchInterval: pendingWorkspaceId ? 2000 : false,
|
||||
});
|
||||
const utils = api.useUtils();
|
||||
|
||||
const switchWorkspace = (_workspace: Workspace) => {
|
||||
@@ -79,7 +74,7 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (!data?.length) {
|
||||
if (!isLoading && !isFetching) setHasLoaded(true);
|
||||
if (!isLoading) setHasLoaded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -95,7 +90,6 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
|
||||
description: workspace.description,
|
||||
plan: workspace.plan,
|
||||
weekStartDay: workspace.weekStartDay,
|
||||
cardPrefix: workspace.cardPrefix,
|
||||
hasLoaded: true,
|
||||
})) as Workspace[];
|
||||
|
||||
@@ -103,72 +97,53 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
|
||||
}
|
||||
|
||||
if (storedWorkspaceId !== null) {
|
||||
const selectedWorkspace = data.find(
|
||||
const newData = data;
|
||||
const selectedWorkspace = newData.find(
|
||||
({ workspace }) => workspace.publicId === storedWorkspaceId,
|
||||
);
|
||||
|
||||
if (!selectedWorkspace?.workspace) {
|
||||
if (pendingWorkspaceId) {
|
||||
pollAttemptsRef.current += 1;
|
||||
if (pollAttemptsRef.current >= MAX_POLL_ATTEMPTS) {
|
||||
setPendingWorkspaceId(null);
|
||||
localStorage.removeItem("workspacePublicId");
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Clear stale workspacePublicId from localStorage
|
||||
localStorage.removeItem("workspacePublicId");
|
||||
pollAttemptsRef.current += 1;
|
||||
if (pollAttemptsRef.current >= MAX_POLL_ATTEMPTS) {
|
||||
setPendingWorkspaceId(null);
|
||||
}
|
||||
} else {
|
||||
pollAttemptsRef.current = 0;
|
||||
setPendingWorkspaceId(null);
|
||||
|
||||
setWorkspace({
|
||||
publicId: selectedWorkspace.workspace.publicId,
|
||||
name: selectedWorkspace.workspace.name,
|
||||
slug: selectedWorkspace.workspace.slug,
|
||||
plan: selectedWorkspace.workspace.plan,
|
||||
description: selectedWorkspace.workspace.description,
|
||||
role: selectedWorkspace.role as "admin" | "member" | "guest",
|
||||
weekStartDay: selectedWorkspace.workspace.weekStartDay as 0 | 1 | 6,
|
||||
cardPrefix: selectedWorkspace.workspace.cardPrefix,
|
||||
});
|
||||
|
||||
if (workspacePublicId) {
|
||||
router.push(`/boards`);
|
||||
localStorage.setItem("workspacePublicId", workspacePublicId);
|
||||
}
|
||||
|
||||
setHasLoaded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
pollAttemptsRef.current = 0;
|
||||
setPendingWorkspaceId(null);
|
||||
|
||||
setWorkspace({
|
||||
publicId: selectedWorkspace.workspace.publicId,
|
||||
name: selectedWorkspace.workspace.name,
|
||||
slug: selectedWorkspace.workspace.slug,
|
||||
plan: selectedWorkspace.workspace.plan,
|
||||
description: selectedWorkspace.workspace.description,
|
||||
role: selectedWorkspace.role,
|
||||
weekStartDay: selectedWorkspace.workspace.weekStartDay as 0 | 1 | 6,
|
||||
});
|
||||
|
||||
if (workspacePublicId) {
|
||||
router.push(`/boards`);
|
||||
localStorage.setItem("workspacePublicId", workspacePublicId);
|
||||
}
|
||||
} else {
|
||||
const primaryWorkspace = data[0]?.workspace;
|
||||
const primaryWorkspaceRole = data[0]?.role;
|
||||
|
||||
if (!primaryWorkspace || !primaryWorkspaceRole) return;
|
||||
localStorage.setItem("workspacePublicId", primaryWorkspace.publicId);
|
||||
setWorkspace({
|
||||
publicId: primaryWorkspace.publicId,
|
||||
name: primaryWorkspace.name,
|
||||
slug: primaryWorkspace.slug,
|
||||
plan: primaryWorkspace.plan,
|
||||
description: primaryWorkspace.description,
|
||||
role: primaryWorkspaceRole,
|
||||
weekStartDay: primaryWorkspace.weekStartDay as 0 | 1 | 6,
|
||||
});
|
||||
}
|
||||
|
||||
const primaryWorkspace = data[0]?.workspace;
|
||||
const primaryWorkspaceRole = data[0]?.role;
|
||||
|
||||
if (!primaryWorkspace || !primaryWorkspaceRole) return;
|
||||
localStorage.setItem("workspacePublicId", primaryWorkspace.publicId);
|
||||
setWorkspace({
|
||||
publicId: primaryWorkspace.publicId,
|
||||
name: primaryWorkspace.name,
|
||||
slug: primaryWorkspace.slug,
|
||||
plan: primaryWorkspace.plan,
|
||||
description: primaryWorkspace.description,
|
||||
role: primaryWorkspaceRole as "admin" | "member" | "guest",
|
||||
weekStartDay: primaryWorkspace.weekStartDay as 0 | 1 | 6,
|
||||
cardPrefix: primaryWorkspace.cardPrefix,
|
||||
});
|
||||
setHasLoaded(true);
|
||||
}, [
|
||||
data,
|
||||
isLoading,
|
||||
isFetching,
|
||||
workspacePublicId,
|
||||
pendingWorkspaceId,
|
||||
router,
|
||||
]);
|
||||
}, [data, isLoading, workspacePublicId, router]);
|
||||
|
||||
return (
|
||||
<WorkspaceContext.Provider
|
||||
|
||||
@@ -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 "ptbr":
|
||||
return (await import("~/locales/ptbr/messages")).messages;
|
||||
case "pt-BR":
|
||||
return (await import("~/locales/pt-BR/messages")).messages;
|
||||
default:
|
||||
return enMessages;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import {
|
||||
HiEllipsisHorizontal,
|
||||
@@ -30,7 +29,6 @@ export default function BoardDropdown({
|
||||
isFavorite?: boolean;
|
||||
boardName?: string;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const { openModal } = useModal();
|
||||
const { showPopup } = usePopup();
|
||||
const { canEditBoard, canDeleteBoard, canCreateBoard, canArchiveBoard } =
|
||||
@@ -49,7 +47,6 @@ 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,7 +16,6 @@ import { getAvatarUrl } from "~/utils/helpers";
|
||||
|
||||
const Card = ({
|
||||
title,
|
||||
ticketNumber,
|
||||
labels,
|
||||
members,
|
||||
checklists,
|
||||
@@ -26,7 +25,6 @@ const Card = ({
|
||||
dueDate,
|
||||
}: {
|
||||
title: string;
|
||||
ticketNumber?: string | null;
|
||||
labels: { name: string; colourCode: string | null }[];
|
||||
members: {
|
||||
publicId: string;
|
||||
@@ -69,11 +67,6 @@ 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,10 +146,6 @@ 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),
|
||||
),
|
||||
@@ -162,9 +158,6 @@ 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,11 +757,6 @@ 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,6 +525,7 @@ const ActivityList = ({
|
||||
comment={activity.comment?.comment}
|
||||
isEdited={!!activity.comment?.updatedAt}
|
||||
isAuthor={activity.comment?.createdBy === sessionData?.user.id}
|
||||
isAdmin={isAdmin ?? false}
|
||||
isViewOnly={!!isViewOnly}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { t } from "@lingui/core/macro";
|
||||
import {
|
||||
HiEllipsisHorizontal,
|
||||
HiHashtag,
|
||||
HiLink,
|
||||
HiOutlineCheckCircle,
|
||||
HiOutlineTrash,
|
||||
@@ -19,13 +18,11 @@ 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();
|
||||
@@ -56,40 +53,12 @@ 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,14 +340,6 @@ 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
|
||||
@@ -355,11 +347,6 @@ 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}`}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { z } from "zod";
|
||||
|
||||
import type { InviteMemberInput } from "@kan/api/types";
|
||||
import type { Subscription } from "@kan/shared/utils";
|
||||
import { authClient } from "@kan/auth/client";
|
||||
import { getSubscriptionByPlan } from "@kan/shared/utils";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
@@ -24,11 +25,15 @@ import { useWorkspace } from "~/providers/workspace";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
export function InviteMemberForm({
|
||||
numberOfMembers,
|
||||
subscriptions,
|
||||
unlimitedSeats,
|
||||
userId,
|
||||
}: {
|
||||
numberOfMembers: number;
|
||||
subscriptions: Subscription[] | undefined;
|
||||
unlimitedSeats: boolean;
|
||||
userId: string | undefined;
|
||||
}) {
|
||||
const utils = api.useUtils();
|
||||
const [isShareInviteLinkEnabled, setIsShareInviteLinkEnabled] =
|
||||
@@ -148,9 +153,6 @@ export function InviteMemberForm({
|
||||
|
||||
const hasTeamSubscription = !!teamSubscription;
|
||||
const hasProSubscription = !!proSubscription;
|
||||
const isPartnerTier = !!(
|
||||
teamSubscription?.partnerTier ?? proSubscription?.partnerTier
|
||||
);
|
||||
|
||||
let isYearly = false;
|
||||
let price = t`$10/month`;
|
||||
@@ -173,13 +175,13 @@ export function InviteMemberForm({
|
||||
inviteMember.mutate(member);
|
||||
};
|
||||
|
||||
const isFreePlan =
|
||||
env("NEXT_PUBLIC_KAN_ENV") === "cloud" &&
|
||||
!hasTeamSubscription &&
|
||||
!hasProSubscription;
|
||||
|
||||
const handleInviteLinkToggle = async () => {
|
||||
if (isFreePlan) return;
|
||||
if (
|
||||
env("NEXT_PUBLIC_KAN_ENV") === "cloud" &&
|
||||
!hasTeamSubscription &&
|
||||
!hasProSubscription
|
||||
)
|
||||
return handleUpgrade();
|
||||
|
||||
setIsLoadingInviteLink(true);
|
||||
|
||||
@@ -217,6 +219,31 @@ export function InviteMemberForm({
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpgrade = async () => {
|
||||
const { data, error } = await authClient.subscription.upgrade({
|
||||
plan: "team",
|
||||
referenceId: workspace.publicId,
|
||||
metadata: { userId },
|
||||
seats: numberOfMembers,
|
||||
successUrl: "/members",
|
||||
cancelUrl: "/members",
|
||||
returnUrl: "/members",
|
||||
disableRedirect: true,
|
||||
});
|
||||
|
||||
if (data?.url) {
|
||||
window.location.href = data.url;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
showPopup({
|
||||
header: t`Error upgrading subscription`,
|
||||
message: t`Please try again later, or contact customer support.`,
|
||||
icon: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const emailElement: HTMLElement | null =
|
||||
document.querySelector<HTMLElement>("#email");
|
||||
@@ -243,7 +270,11 @@ export function InviteMemberForm({
|
||||
<Input
|
||||
id="email"
|
||||
placeholder={t`Email`}
|
||||
disabled={isFreePlan}
|
||||
disabled={
|
||||
env("NEXT_PUBLIC_KAN_ENV") === "cloud" &&
|
||||
!hasTeamSubscription &&
|
||||
!hasProSubscription
|
||||
}
|
||||
{...register("email", { required: true })}
|
||||
onKeyDown={async (e) => {
|
||||
if (e.key === "Enter") {
|
||||
@@ -254,86 +285,78 @@ export function InviteMemberForm({
|
||||
errorMessage={errors.email?.message}
|
||||
/>
|
||||
)}
|
||||
{!isFreePlan &&
|
||||
(!isEmailEnabled || (isShareInviteLinkEnabled && inviteLink)) && (
|
||||
<div className="my-4">
|
||||
<div className="relative">
|
||||
<Input
|
||||
value={inviteLink}
|
||||
className="pr-10 text-sm text-light-900 dark:text-dark-900"
|
||||
readOnly
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 flex items-center pr-3 text-light-900 hover:text-light-950 dark:text-dark-900 dark:hover:text-dark-950"
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{copied ? (
|
||||
<HiMiniCheck className="h-5 w-5 text-green-600" />
|
||||
) : (
|
||||
<HiOutlineDocumentDuplicate className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 flex items-start gap-1">
|
||||
<HiInformationCircle className="mt-0.5 h-4 w-4 text-dark-900" />
|
||||
<p className="text-xs text-gray-500 dark:text-dark-900">
|
||||
{t`Anyone with this link can join your workspace`}
|
||||
{(!isEmailEnabled || (isShareInviteLinkEnabled && inviteLink)) && (
|
||||
<div className="my-4">
|
||||
<div className="relative">
|
||||
<Input
|
||||
value={inviteLink}
|
||||
className="pr-10 text-sm text-light-900 dark:text-dark-900"
|
||||
readOnly
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 flex items-center pr-3 text-light-900 hover:text-light-950 dark:text-dark-900 dark:hover:text-dark-950"
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{copied ? (
|
||||
<HiMiniCheck className="h-5 w-5 text-green-600" />
|
||||
) : (
|
||||
<HiOutlineDocumentDuplicate className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 flex items-start gap-1">
|
||||
<HiInformationCircle className="mt-0.5 h-4 w-4 text-dark-900" />
|
||||
<p className="text-xs text-gray-500 dark:text-dark-900">
|
||||
{t`Anyone with this link can join your workspace`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{env("NEXT_PUBLIC_KAN_ENV") === "cloud" && (
|
||||
<div className="mt-3 rounded-md bg-light-100 p-3 text-xs text-light-900 dark:bg-dark-200 dark:text-dark-900">
|
||||
{hasTeamSubscription || hasProSubscription ? (
|
||||
<div>
|
||||
<span className="font-medium text-emerald-500 dark:text-emerald-400">
|
||||
{hasTeamSubscription ? t`Team Plan` : t`Pro Plan ∞`}
|
||||
</span>
|
||||
<p className="mt-1">
|
||||
{unlimitedSeats
|
||||
? t`You have unlimited seats with your Pro Plan. There is no additional charge for new members!`
|
||||
: t`Adding a new member will cost an additional ${price} (${billingType}) per seat.`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{env("NEXT_PUBLIC_KAN_ENV") === "cloud" &&
|
||||
!isPartnerTier &&
|
||||
!unlimitedSeats && (
|
||||
<div className="mt-3 rounded-md bg-light-100 p-3 text-xs text-light-900 dark:bg-dark-200 dark:text-dark-900">
|
||||
{hasTeamSubscription || hasProSubscription ? (
|
||||
<div>
|
||||
<span className="font-medium text-emerald-500 dark:text-emerald-400">
|
||||
{hasTeamSubscription ? t`Team Plan` : t`Pro Plan ∞`}
|
||||
</span>
|
||||
{!isPartnerTier && (
|
||||
<p className="mt-1">
|
||||
{unlimitedSeats
|
||||
? t`You have unlimited seats with your Pro Plan. There is no additional charge for new members!`
|
||||
: t`Adding a new member will cost an additional ${price} (${billingType}) per seat.`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<span className="font-medium text-light-950 dark:text-dark-950">
|
||||
{t`Free Plan`}
|
||||
</span>
|
||||
<p className="mt-1">
|
||||
{t`Inviting members requires a Team or Pro plan. You'll be redirected to upgrade your workspace.`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<div>
|
||||
<span className="font-medium text-light-950 dark:text-dark-950">
|
||||
{t`Free Plan`}
|
||||
</span>
|
||||
<p className="mt-1">
|
||||
{t`Inviting members requires a Team Plan. You'll be redirected to upgrade your workspace.`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-12 flex items-center justify-end space-x-4 border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
||||
{!isFreePlan && (
|
||||
<Toggle
|
||||
label={
|
||||
isShareInviteLinkEnabled
|
||||
? t`Deactivate invite link`
|
||||
: t`Create invite link`
|
||||
}
|
||||
isChecked={isShareInviteLinkEnabled}
|
||||
onChange={handleInviteLinkToggle}
|
||||
/>
|
||||
)}
|
||||
<Toggle
|
||||
label={
|
||||
isShareInviteLinkEnabled
|
||||
? t`Deactivate invite link`
|
||||
: t`Create invite link`
|
||||
}
|
||||
isChecked={isShareInviteLinkEnabled}
|
||||
onChange={handleInviteLinkToggle}
|
||||
/>
|
||||
<div>
|
||||
{isFreePlan ? (
|
||||
<Button
|
||||
type="button"
|
||||
href={`/upgrade/select-plan?plan=pro&workspacePublicId=${workspace.publicId}&returnUrl=${encodeURIComponent("/members")}`}
|
||||
>
|
||||
{t`Choose plan`}
|
||||
{env("NEXT_PUBLIC_KAN_ENV") === "cloud" &&
|
||||
!hasTeamSubscription &&
|
||||
!hasProSubscription ? (
|
||||
<Button type="button" onClick={handleUpgrade}>
|
||||
{t`Start 14 day free trial`}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
|
||||
@@ -27,8 +27,8 @@ import { useWorkspace } from "~/providers/workspace";
|
||||
import { api } from "~/utils/api";
|
||||
import { getAvatarUrl } from "~/utils/helpers";
|
||||
import { DeleteMemberConfirmation } from "./components/DeleteMemberConfirmation";
|
||||
import { EditMemberPermissionsModal } from "./components/EditMemberPermissionsModal";
|
||||
import { InviteMemberForm } from "./components/InviteMemberForm";
|
||||
import { EditMemberPermissionsModal } from "./components/EditMemberPermissionsModal";
|
||||
|
||||
export default function MembersPage() {
|
||||
const { modalContentType, openModal, isOpen } = useModal();
|
||||
@@ -76,19 +76,6 @@ export default function MembersPage() {
|
||||
|
||||
const unlimitedSeats = hasUnlimitedSeats(subscriptions);
|
||||
|
||||
const isProPlan =
|
||||
!!proSubscription ||
|
||||
workspace.plan === "pro" ||
|
||||
workspace.plan === "enterprise";
|
||||
const isTeamPlan = !!teamSubscription || workspace.plan === "team";
|
||||
const isPaidPlan = isProPlan || isTeamPlan;
|
||||
|
||||
const activeMembers = data?.members.length ?? 0;
|
||||
const totalSeats =
|
||||
teamSubscription?.seats ??
|
||||
proSubscription?.seats ??
|
||||
(isPaidPlan ? null : 1);
|
||||
|
||||
const TableRow = ({
|
||||
memberPublicId,
|
||||
memberId,
|
||||
@@ -157,8 +144,8 @@ export default function MembersPage() {
|
||||
{memberName}
|
||||
</p>
|
||||
</div>
|
||||
{(workspace.role === "admin" ||
|
||||
data?.showEmailsToMembers === true ||
|
||||
{((workspace.role === "admin" ||
|
||||
data?.showEmailsToMembers === true) ||
|
||||
showSkeleton) && (
|
||||
<p
|
||||
className={twMerge(
|
||||
@@ -193,7 +180,8 @@ export default function MembersPage() {
|
||||
<div className="relative inline-flex items-center">
|
||||
<span className="inline-flex items-center gap-1 rounded-md bg-emerald-500/10 px-1.5 py-0.5 text-[10px] font-medium text-emerald-400 ring-1 ring-inset ring-emerald-500/20 sm:text-[11px]">
|
||||
{memberRole &&
|
||||
memberRole.charAt(0).toUpperCase() + memberRole.slice(1)}
|
||||
memberRole.charAt(0).toUpperCase() +
|
||||
memberRole.slice(1)}
|
||||
{canEditMember && session?.user.id !== memberId && (
|
||||
<HiChevronDown className="h-3 w-3" />
|
||||
)}
|
||||
@@ -276,42 +264,38 @@ export default function MembersPage() {
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{env("NEXT_PUBLIC_KAN_ENV") === "cloud" && !!data && (
|
||||
{env("NEXT_PUBLIC_KAN_ENV") === "cloud" && (
|
||||
<>
|
||||
{!isPaidPlan && (
|
||||
{!proSubscription && !teamSubscription && (
|
||||
<Link
|
||||
href={`/upgrade/select-plan?plan=pro&workspacePublicId=${workspace.publicId}&returnUrl=${encodeURIComponent("/members")}`}
|
||||
href="/settings/workspace?upgrade=pro"
|
||||
className="hidden items-center rounded-full border border-emerald-300 bg-emerald-50 px-3 py-1 text-center text-xs text-emerald-400 dark:border-emerald-700 dark:bg-emerald-950 dark:text-emerald-400 lg:flex"
|
||||
>
|
||||
<HiBolt />
|
||||
<span className="ml-1 font-medium">{t`Upgrade`}</span>
|
||||
<span className="ml-1 font-medium">
|
||||
{t`Launch offer: Get unlimited members with Pro`}
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex items-center rounded-full border px-3 py-1 text-center text-xs",
|
||||
isPaidPlan
|
||||
teamSubscription || proSubscription
|
||||
? "border-emerald-300 bg-emerald-50 text-emerald-400 dark:border-emerald-700 dark:bg-emerald-950 dark:text-emerald-400"
|
||||
: "border-light-300 bg-light-50 text-light-1000 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900",
|
||||
)}
|
||||
>
|
||||
<span className="font-medium">
|
||||
{isProPlan
|
||||
{proSubscription
|
||||
? t`Pro Plan`
|
||||
: isTeamPlan
|
||||
: teamSubscription
|
||||
? t`Team Plan`
|
||||
: t`Free Plan`}
|
||||
{proSubscription && unlimitedSeats && (
|
||||
<span className="ml-1 text-xs">∞</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{isPaidPlan && (unlimitedSeats || totalSeats !== null) && (
|
||||
<div className="flex items-center rounded-full border border-light-300 bg-light-50 px-3 py-1 text-center text-xs text-light-1000 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900">
|
||||
<span className="font-medium">
|
||||
{unlimitedSeats
|
||||
? t`Unlimited seats`
|
||||
: `${activeMembers}/${totalSeats} ${t`seats`}`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
@@ -400,6 +384,8 @@ export default function MembersPage() {
|
||||
isVisible={isOpen && modalContentType === "INVITE_MEMBER"}
|
||||
>
|
||||
<InviteMemberForm
|
||||
userId={session?.user.id}
|
||||
numberOfMembers={data?.members.length ?? 1}
|
||||
subscriptions={subscriptions}
|
||||
unlimitedSeats={unlimitedSeats}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { Radio, RadioGroup } from "@headlessui/react";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Radio, RadioGroup } from "@headlessui/react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import { HiUser } from "react-icons/hi2";
|
||||
@@ -48,7 +48,6 @@ export default function SelectPlanView() {
|
||||
(searchParams.get("billing") as Billing | null) ?? "annual",
|
||||
);
|
||||
const returnUrl = searchParams.get("returnUrl") ?? "/boards";
|
||||
const workspacePublicId = searchParams.get("workspacePublicId");
|
||||
const { data: workspaces } = api.workspace.all.useQuery();
|
||||
const { data: session } = authClient.useSession();
|
||||
const { data: user } = api.user.getUser.useQuery(undefined, {
|
||||
@@ -97,44 +96,18 @@ export default function SelectPlanView() {
|
||||
},
|
||||
];
|
||||
|
||||
const buildSelectPlanUrl = (plan: PlanId, b: Billing) => {
|
||||
const base = `/onboarding/select-plan?plan=${plan}&billing=${b}&returnUrl=${encodeURIComponent(returnUrl)}`;
|
||||
return workspacePublicId
|
||||
? `${base}&workspacePublicId=${workspacePublicId}`
|
||||
: base;
|
||||
};
|
||||
|
||||
const handleSelectPlan = (plan: PlanId) => {
|
||||
setSelected(plan);
|
||||
router.replace(buildSelectPlanUrl(plan, billing));
|
||||
router.replace(`/onboarding/select-plan?plan=${plan}&billing=${billing}&returnUrl=${encodeURIComponent(returnUrl)}`);
|
||||
};
|
||||
|
||||
const handleSetBilling = (b: Billing) => {
|
||||
setBilling(b);
|
||||
router.replace(buildSelectPlanUrl(selected, b));
|
||||
router.replace(`/onboarding/select-plan?plan=${selected}&billing=${b}&returnUrl=${encodeURIComponent(returnUrl)}`);
|
||||
};
|
||||
|
||||
const handleContinue = async () => {
|
||||
if (workspacePublicId && selected !== "solo") {
|
||||
const response = await fetch("/api/stripe/create_checkout_session", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
plan: selected,
|
||||
billing,
|
||||
workspacePublicId,
|
||||
successUrl: returnUrl,
|
||||
cancelUrl: returnUrl,
|
||||
}),
|
||||
});
|
||||
const { url } = (await response.json()) as { url: string };
|
||||
if (url) window.location.href = url;
|
||||
return;
|
||||
}
|
||||
router.push(
|
||||
`/onboarding/workspace?plan=${selected}&billing=${billing}&returnUrl=${encodeURIComponent(returnUrl)}`,
|
||||
);
|
||||
};
|
||||
const handleContinue = () =>
|
||||
router.push(`/onboarding/workspace?plan=${selected}&billing=${billing}&returnUrl=${encodeURIComponent(returnUrl)}`);
|
||||
|
||||
const handleCancel = () => router.push(returnUrl);
|
||||
|
||||
@@ -236,11 +209,7 @@ export default function SelectPlanView() {
|
||||
{t`Cancel`}
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={() => void handleContinue()}>
|
||||
{workspacePublicId && selected !== "solo"
|
||||
? t`Upgrade`
|
||||
: t`Continue`}
|
||||
</Button>
|
||||
<Button onClick={handleContinue}>{t`Continue`}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -39,16 +39,7 @@ export default function WorkspaceNameView() {
|
||||
const plan = searchParams.get("plan") ?? "solo";
|
||||
const billing = searchParams.get("billing") ?? "annual";
|
||||
const returnUrl = searchParams.get("returnUrl") ?? "/boards";
|
||||
const licenseKeyParam = searchParams.get("license_key");
|
||||
const isLicenseFlow = !!licenseKeyParam;
|
||||
const { showPopup } = usePopup();
|
||||
|
||||
useEffect(() => {
|
||||
if (licenseKeyParam) {
|
||||
localStorage.setItem("partnerLicenseKey", licenseKeyParam);
|
||||
}
|
||||
}, [licenseKeyParam]);
|
||||
|
||||
const [isProToggle, setIsProToggle] = useState(plan === "pro");
|
||||
const effectivePlan = isProToggle ? "pro" : plan;
|
||||
|
||||
@@ -103,15 +94,7 @@ export default function WorkspaceNameView() {
|
||||
if (!workspace.publicId) return;
|
||||
localStorage.setItem("workspacePublicId", workspace.publicId);
|
||||
void utils.workspace.all.invalidate();
|
||||
const storedLicenseKey = localStorage.getItem("partnerLicenseKey");
|
||||
if (storedLicenseKey) {
|
||||
localStorage.removeItem("partnerLicenseKey");
|
||||
router.push(
|
||||
`/api/partner/link?license_key=${encodeURIComponent(storedLicenseKey)}`,
|
||||
);
|
||||
} else {
|
||||
router.push("/boards");
|
||||
}
|
||||
router.push("/boards");
|
||||
},
|
||||
onError: () => {
|
||||
showPopup({
|
||||
@@ -127,7 +110,7 @@ export default function WorkspaceNameView() {
|
||||
const handleContinue = async () => {
|
||||
if (!name.trim()) return;
|
||||
|
||||
if (effectivePlan === "solo" || isLicenseFlow) {
|
||||
if (effectivePlan === "solo") {
|
||||
createWorkspace.mutate({
|
||||
name: name.trim(),
|
||||
...(description.trim() && { description: description.trim() }),
|
||||
@@ -210,52 +193,50 @@ export default function WorkspaceNameView() {
|
||||
maxLength={64}
|
||||
/>
|
||||
|
||||
{!isLicenseFlow && (
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
placeholder={t`your-workspace`}
|
||||
value={isProToggle ? slug : t`your-workspace`}
|
||||
onChange={(e) => {
|
||||
setSlugManuallyEdited(true);
|
||||
setSlug(
|
||||
e.target.value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, "")
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.slice(0, 60),
|
||||
);
|
||||
}}
|
||||
disabled={!isProToggle}
|
||||
prefix="kan.bn/"
|
||||
className={
|
||||
!isProToggle ? "cursor-not-allowed opacity-50" : ""
|
||||
}
|
||||
errorMessage={slugError}
|
||||
iconRight={
|
||||
!isProToggle ? (
|
||||
<Tooltip
|
||||
content={
|
||||
<span className="text-xs">{t`Custom usernames require upgrading to a Pro plan`}</span>
|
||||
}
|
||||
placement="top"
|
||||
delay={0}
|
||||
>
|
||||
<HiInformationCircle className="h-4 w-4 leading-[0] text-dark-700 dark:text-dark-700" />
|
||||
</Tooltip>
|
||||
) : isProToggle && slug.length >= 3 ? (
|
||||
isTyping || slugAvailability.isPending ? (
|
||||
<LoadingSpinner />
|
||||
) : isSlugAvailable ? (
|
||||
<HiCheck className="h-4 w-4 text-white" />
|
||||
) : null
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
placeholder={t`your-workspace`}
|
||||
value={isProToggle ? slug : t`your-workspace`}
|
||||
onChange={(e) => {
|
||||
setSlugManuallyEdited(true);
|
||||
setSlug(
|
||||
e.target.value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, "")
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.slice(0, 60),
|
||||
);
|
||||
}}
|
||||
disabled={!isProToggle}
|
||||
prefix="kan.bn/"
|
||||
className={
|
||||
!isProToggle ? "cursor-not-allowed opacity-50" : ""
|
||||
}
|
||||
errorMessage={slugError}
|
||||
iconRight={
|
||||
!isProToggle ? (
|
||||
<Tooltip
|
||||
content={
|
||||
<span className="text-xs">{t`Custom usernames require upgrading to a Pro plan`}</span>
|
||||
}
|
||||
placement="top"
|
||||
delay={0}
|
||||
>
|
||||
<HiInformationCircle className="h-4 w-4 leading-[0] text-dark-700 dark:text-dark-700" />
|
||||
</Tooltip>
|
||||
) : isProToggle && slug.length >= 3 ? (
|
||||
isTyping || slugAvailability.isPending ? (
|
||||
<LoadingSpinner />
|
||||
) : isSlugAvailable ? (
|
||||
<HiCheck className="h-4 w-4 text-white" />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isLicenseFlow && plan !== "pro" && (
|
||||
{plan !== "pro" && (
|
||||
<div className="pb-2">
|
||||
<Toggle
|
||||
isChecked={isProToggle}
|
||||
@@ -284,18 +265,16 @@ export default function WorkspaceNameView() {
|
||||
|
||||
<div className="mt-6 flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="ml-auto flex gap-2">
|
||||
{!isLicenseFlow && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
router.replace(
|
||||
`/onboarding/select-plan?plan=${effectivePlan}&billing=${billing}&returnUrl=${encodeURIComponent(returnUrl)}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
{t`Back`}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
router.replace(
|
||||
`/onboarding/select-plan?plan=${effectivePlan}&billing=${billing}&returnUrl=${encodeURIComponent(returnUrl)}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
{t`Back`}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => void handleContinue()}
|
||||
disabled={
|
||||
|
||||
@@ -140,11 +140,6 @@ 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>
|
||||
|
||||
@@ -85,19 +85,17 @@ export default function AccountSettings() {
|
||||
{isCredentialsEnabled && (
|
||||
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
|
||||
<h2 className="mb-4 mt-8 text-[14px] font-bold text-neutral-900 dark:text-dark-1000">
|
||||
{data?.hasPassword ? t`Change Password` : t`Set Password`}
|
||||
{t`Change Password`}
|
||||
</h2>
|
||||
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
|
||||
{data?.hasPassword
|
||||
? t`You are about to change your password.`
|
||||
: t`Set a password to enable password-based login.`}
|
||||
{t`You are about to change your password.`}
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => openModal("CHANGE_PASSWORD")}
|
||||
>
|
||||
{data?.hasPassword ? t`Change Password` : t`Set Password`}
|
||||
{t`Change Password`}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,7 +113,7 @@ export default function AccountSettings() {
|
||||
modalSize="sm"
|
||||
isVisible={isOpen && modalContentType === "CHANGE_PASSWORD"}
|
||||
>
|
||||
<ChangePasswordFormConfirmation hasPassword={data?.hasPassword ?? false} />
|
||||
<ChangePasswordFormConfirmation />
|
||||
</Modal>
|
||||
|
||||
{/* Global modals */}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { env } from "next-runtime-env";
|
||||
import { HiMiniArrowTopRightOnSquare } from "react-icons/hi2";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
@@ -9,49 +7,24 @@ import Modal from "~/components/modal";
|
||||
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
||||
import { PageHead } from "~/components/PageHead";
|
||||
import { useModal } from "~/providers/modal";
|
||||
import { useWorkspace } from "~/providers/workspace";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
export default function BillingSettings() {
|
||||
const { modalContentType, isOpen } = useModal();
|
||||
const router = useRouter();
|
||||
const isCloud = env("NEXT_PUBLIC_KAN_ENV") === "cloud";
|
||||
const { workspace } = useWorkspace();
|
||||
|
||||
const { data: workspaceData } = api.workspace.byId.useQuery(
|
||||
{ workspacePublicId: workspace.publicId },
|
||||
{ enabled: !!workspace.publicId && workspace.publicId.length >= 12 },
|
||||
);
|
||||
|
||||
const subscription = workspaceData?.subscriptions.find((s) =>
|
||||
["active", "trialing", "past_due"].includes(s.status),
|
||||
);
|
||||
|
||||
const planLabel = (() => {
|
||||
if (!subscription) return t`Free (1 member)`;
|
||||
if (subscription.unlimitedSeats) {
|
||||
const name =
|
||||
subscription.plan.charAt(0).toUpperCase() + subscription.plan.slice(1);
|
||||
return `${name} (${t`unlimited members`})`;
|
||||
}
|
||||
if (subscription.seats != null) {
|
||||
const name =
|
||||
subscription.plan.charAt(0).toUpperCase() + subscription.plan.slice(1);
|
||||
return `${name} (${subscription.seats} ${subscription.seats === 1 ? t`member` : t`members`})`;
|
||||
}
|
||||
return (
|
||||
subscription.plan.charAt(0).toUpperCase() + subscription.plan.slice(1)
|
||||
);
|
||||
})();
|
||||
|
||||
const handleOpenBillingPortal = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/stripe/create_billing_session", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const { url } = (await response.json()) as { url: string };
|
||||
if (url) window.location.href = url;
|
||||
|
||||
if (url) {
|
||||
window.location.href = url;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating billing session:", error);
|
||||
}
|
||||
@@ -61,28 +34,6 @@ export default function BillingSettings() {
|
||||
<>
|
||||
<PageHead title={t`Settings | Billing`} />
|
||||
|
||||
<div className="border-t border-light-300 dark:border-dark-300">
|
||||
<h2 className="mb-4 mt-8 text-[14px] font-bold text-neutral-900 dark:text-dark-1000">
|
||||
{t`Plan`}
|
||||
</h2>
|
||||
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
|
||||
{planLabel}
|
||||
</p>
|
||||
{!subscription && isCloud && (
|
||||
<div className="mb-8">
|
||||
<Button
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/upgrade/select-plan?plan=pro&workspacePublicId=${workspace.publicId}&returnUrl=${encodeURIComponent("/settings/billing")}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
{t`Choose plan`}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
|
||||
<h2 className="mb-4 mt-8 text-[14px] font-bold text-neutral-900 dark:text-dark-1000">
|
||||
{t`Billing`}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { env } from "next-runtime-env";
|
||||
import { useEffect, useState } from "react";
|
||||
import { HiBolt } from "react-icons/hi2";
|
||||
|
||||
import type { Subscription } from "@kan/shared/utils";
|
||||
import { hasActiveSubscription } from "@kan/shared/utils";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
import FeedbackModal from "~/components/FeedbackModal";
|
||||
@@ -12,21 +17,43 @@ 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() {
|
||||
const { modalContentType, openModal, isOpen } = useModal();
|
||||
const { workspace } = useWorkspace();
|
||||
const { canEditWorkspace } = usePermissions();
|
||||
const router = useRouter();
|
||||
const { data } = api.user.getUser.useQuery();
|
||||
const [hasOpenedUpgradeModal, setHasOpenedUpgradeModal] = useState(false);
|
||||
|
||||
const { data: workspaceData } = api.workspace.byId.useQuery(
|
||||
{ workspacePublicId: workspace.publicId },
|
||||
{ enabled: !!workspace.publicId && workspace.publicId.length >= 12 },
|
||||
);
|
||||
|
||||
const subscriptions = workspaceData?.subscriptions as
|
||||
| Subscription[]
|
||||
| undefined;
|
||||
|
||||
// Open upgrade modal if upgrade=pro is in URL params
|
||||
useEffect(() => {
|
||||
if (
|
||||
router.query.upgrade === "pro" &&
|
||||
env("NEXT_PUBLIC_KAN_ENV") === "cloud" &&
|
||||
!hasActiveSubscription(subscriptions, "pro") &&
|
||||
!hasOpenedUpgradeModal
|
||||
) {
|
||||
openModal("UPGRADE_TO_PRO");
|
||||
setHasOpenedUpgradeModal(true);
|
||||
}
|
||||
}, [router.query.upgrade, subscriptions, openModal, hasOpenedUpgradeModal]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={t`Settings | Workspace`} />
|
||||
@@ -80,6 +107,19 @@ export default function WorkspaceSettings() {
|
||||
disabled={!canEditWorkspace}
|
||||
/>
|
||||
|
||||
{env("NEXT_PUBLIC_KAN_ENV") === "cloud" &&
|
||||
!hasActiveSubscription(subscriptions, "pro") &&
|
||||
!hasActiveSubscription(subscriptions, "team") && (
|
||||
<div className="my-8">
|
||||
<Button
|
||||
onClick={() => openModal("UPGRADE_TO_PRO")}
|
||||
iconRight={<HiBolt />}
|
||||
>
|
||||
{t`Upgrade to Pro`}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="border-t border-light-300 dark:border-dark-300">
|
||||
<h2 className="mb-4 mt-8 text-[14px] font-bold text-neutral-900 dark:text-dark-1000">
|
||||
{t`Delete workspace`}
|
||||
@@ -106,6 +146,16 @@ export default function WorkspaceSettings() {
|
||||
>
|
||||
<DeleteWorkspaceConfirmation />
|
||||
</Modal>
|
||||
<Modal
|
||||
modalSize="sm"
|
||||
isVisible={isOpen && modalContentType === "UPGRADE_TO_PRO"}
|
||||
>
|
||||
<UpgradeToProConfirmation
|
||||
userId={data?.id ?? ""}
|
||||
workspacePublicId={workspace.publicId}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
{/* Global modals */}
|
||||
<Modal
|
||||
modalSize="md"
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useRouter } from "next/navigation";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { env } from "next-runtime-env";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -13,43 +14,27 @@ import { useModal } from "~/providers/modal";
|
||||
import { usePopup } from "~/providers/popup";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
const buildSchema = (hasPassword: boolean) => {
|
||||
const base = z.object({
|
||||
currentPassword: hasPassword
|
||||
? z.string().min(1, t`Current password is required`)
|
||||
: z.string().optional(),
|
||||
const FormSchema = z
|
||||
.object({
|
||||
currentPassword: z.string().min(1, t`Current password is required`),
|
||||
newPassword: z
|
||||
.string()
|
||||
.min(8, t`Password must be at least 8 characters`)
|
||||
.min(1, t`New password is required`),
|
||||
confirmPassword: z.string().min(1, t`Please confirm your new password`),
|
||||
})
|
||||
.refine((data) => data.newPassword === data.confirmPassword, {
|
||||
message: t`Passwords do not match`,
|
||||
path: ["confirmPassword"],
|
||||
})
|
||||
.refine((data) => data.currentPassword !== data.newPassword, {
|
||||
message: t`New password must be different from current password`,
|
||||
path: ["newPassword"],
|
||||
});
|
||||
|
||||
return base
|
||||
.refine((data) => data.newPassword === data.confirmPassword, {
|
||||
message: t`Passwords do not match`,
|
||||
path: ["confirmPassword"],
|
||||
})
|
||||
.refine(
|
||||
(data) => !hasPassword || data.currentPassword !== data.newPassword,
|
||||
{
|
||||
message: t`New password must be different from current password`,
|
||||
path: ["newPassword"],
|
||||
},
|
||||
);
|
||||
};
|
||||
type FormValues = z.infer<typeof FormSchema>;
|
||||
|
||||
interface FormValues {
|
||||
currentPassword?: string;
|
||||
newPassword: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
hasPassword: boolean;
|
||||
}
|
||||
|
||||
export function ChangePasswordFormConfirmation({ hasPassword }: Props) {
|
||||
export function ChangePasswordFormConfirmation() {
|
||||
const { closeModal } = useModal();
|
||||
const { showPopup } = usePopup();
|
||||
const router = useRouter();
|
||||
@@ -62,24 +47,15 @@ export function ChangePasswordFormConfirmation({ hasPassword }: Props) {
|
||||
reset,
|
||||
setError,
|
||||
} = useForm<FormValues>({
|
||||
resolver: zodResolver(buildSchema(hasPassword)),
|
||||
resolver: zodResolver(FormSchema),
|
||||
mode: "onChange",
|
||||
});
|
||||
|
||||
const setPasswordMutation = api.user.setPassword.useMutation();
|
||||
|
||||
const changePasswordMutation = useMutation({
|
||||
mutationFn: async (data: FormValues) => {
|
||||
if (!hasPassword) {
|
||||
await setPasswordMutation.mutateAsync({
|
||||
newPassword: data.newPassword,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await authClient.changePassword({
|
||||
newPassword: data.newPassword,
|
||||
currentPassword: data.currentPassword ?? "",
|
||||
currentPassword: data.currentPassword,
|
||||
revokeOtherSessions: true,
|
||||
});
|
||||
|
||||
@@ -90,18 +66,12 @@ export function ChangePasswordFormConfirmation({ hasPassword }: Props) {
|
||||
onSuccess: async () => {
|
||||
closeModal();
|
||||
showPopup({
|
||||
header: hasPassword ? t`Password Changed` : t`Password Set`,
|
||||
message: hasPassword
|
||||
? t`Your password has been changed.`
|
||||
: t`Your password has been set.`,
|
||||
header: t`Password Changed`,
|
||||
message: t`Your password has been changed.`,
|
||||
icon: "success",
|
||||
});
|
||||
|
||||
if (!hasPassword) {
|
||||
sessionStorage.removeItem("set_password_prompted");
|
||||
}
|
||||
|
||||
utils.user.getUser.invalidate();
|
||||
utils.invalidate();
|
||||
reset();
|
||||
router.push("/");
|
||||
},
|
||||
@@ -116,9 +86,7 @@ export function ChangePasswordFormConfirmation({ hasPassword }: Props) {
|
||||
} else {
|
||||
closeModal();
|
||||
showPopup({
|
||||
header: hasPassword
|
||||
? t`Error Changing Password`
|
||||
: t`Error Setting Password`,
|
||||
header: t`Error Changing Password`,
|
||||
message: t`An unexpected error occurred. Please try again later.`,
|
||||
icon: "error",
|
||||
});
|
||||
@@ -138,33 +106,27 @@ export function ChangePasswordFormConfirmation({ hasPassword }: Props) {
|
||||
return (
|
||||
<div className="p-5">
|
||||
<div className="flex w-full flex-col justify-between pb-4">
|
||||
<h2 className="pb-4 text-base font-medium dark:text-white">
|
||||
{hasPassword ? t`Change Password` : t`Set Password`}
|
||||
</h2>
|
||||
<h2 className="text-md pb-4 font-medium dark:text-white">{t`Change Password`}</h2>
|
||||
<p className="mb-4 text-sm text-light-900">
|
||||
{hasPassword
|
||||
? t`Enter your current password and choose a new secure password.`
|
||||
: t`You signed in without a password. Set a password to enable password-based login.`}
|
||||
{t`Enter your current password and choose a new secure password.`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="space-y-2">
|
||||
{hasPassword && (
|
||||
<div>
|
||||
<Input
|
||||
id="currentPassword"
|
||||
type="password"
|
||||
{...register("currentPassword")}
|
||||
placeholder={t`Enter your current password`}
|
||||
/>
|
||||
{errors.currentPassword && (
|
||||
<p className="mt-2 text-xs text-red-400">
|
||||
{errors.currentPassword.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Input
|
||||
id="currentPassword"
|
||||
type="password"
|
||||
{...register("currentPassword")}
|
||||
placeholder={t`Enter your current password`}
|
||||
/>
|
||||
{errors.currentPassword && (
|
||||
<p className="mt-2 text-xs text-red-400">
|
||||
{errors.currentPassword.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Input
|
||||
@@ -213,7 +175,7 @@ export function ChangePasswordFormConfirmation({ hasPassword }: Props) {
|
||||
fullWidth
|
||||
size="lg"
|
||||
>
|
||||
{hasPassword ? t`Change Password` : t`Set Password`}
|
||||
{t`Change Password`}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -13,29 +13,20 @@ 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({
|
||||
header: t`Webhook deleted`,
|
||||
message: t`Webhook deleted successfully`,
|
||||
icon: "success",
|
||||
});
|
||||
showPopup({ message: t`Webhook deleted successfully`, type: "success" });
|
||||
closeModal();
|
||||
},
|
||||
onError: (error) => {
|
||||
showPopup({
|
||||
header: t`Unable to delete webhook`,
|
||||
message: error.message || t`Failed to delete webhook`,
|
||||
icon: "error",
|
||||
type: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -44,7 +35,7 @@ export function DeleteWebhookConfirmation({
|
||||
if (!webhookPublicId) return;
|
||||
deleteWebhookMutation.mutate({
|
||||
workspacePublicId,
|
||||
webhookPublicId: webhookPublicId,
|
||||
webhookPublicId: webhookPublicId as string,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -44,12 +44,7 @@ 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);
|
||||
|
||||
@@ -106,18 +101,13 @@ export function NewWebhookModal({
|
||||
const createWebhookMutation = api.webhook.create.useMutation({
|
||||
onSuccess: () => {
|
||||
void utils.webhook.list.invalidate({ workspacePublicId });
|
||||
showPopup({
|
||||
header: t`Webhook created`,
|
||||
message: t`Webhook created successfully`,
|
||||
icon: "success",
|
||||
});
|
||||
showPopup({ message: t`Webhook created successfully`, type: "success" });
|
||||
closeModal();
|
||||
},
|
||||
onError: (error) => {
|
||||
showPopup({
|
||||
header: t`Unable to create webhook`,
|
||||
message: error.message || t`Failed to create webhook`,
|
||||
icon: "error",
|
||||
type: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -125,18 +115,13 @@ export function NewWebhookModal({
|
||||
const updateWebhookMutation = api.webhook.update.useMutation({
|
||||
onSuccess: () => {
|
||||
void utils.webhook.list.invalidate({ workspacePublicId });
|
||||
showPopup({
|
||||
header: t`Webhook updated`,
|
||||
message: t`Webhook updated successfully`,
|
||||
icon: "success",
|
||||
});
|
||||
showPopup({ message: t`Webhook updated successfully`, type: "success" });
|
||||
closeModal();
|
||||
},
|
||||
onError: (error) => {
|
||||
showPopup({
|
||||
header: t`Unable to update webhook`,
|
||||
message: error.message || t`Failed to update webhook`,
|
||||
icon: "error",
|
||||
type: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -144,25 +129,19 @@ export function NewWebhookModal({
|
||||
const testWebhookMutation = api.webhook.test.useMutation({
|
||||
onSuccess: (result) => {
|
||||
if (result.success) {
|
||||
showPopup({
|
||||
header: t`Test sent`,
|
||||
message: t`Test webhook sent successfully!`,
|
||||
icon: "success",
|
||||
});
|
||||
showPopup({ message: t`Test webhook sent successfully!`, type: "success" });
|
||||
} else {
|
||||
showPopup({
|
||||
header: t`Test failed`,
|
||||
message: result.error || t`Webhook test failed`,
|
||||
icon: "error",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
setIsTestingWebhook(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
showPopup({
|
||||
header: t`Unable to test webhook`,
|
||||
message: error.message || t`Failed to test webhook`,
|
||||
icon: "error",
|
||||
type: "error",
|
||||
});
|
||||
setIsTestingWebhook(false);
|
||||
},
|
||||
@@ -172,7 +151,7 @@ export function NewWebhookModal({
|
||||
if (isEdit && webhookPublicId) {
|
||||
updateWebhookMutation.mutate({
|
||||
workspacePublicId,
|
||||
webhookPublicId: webhookPublicId,
|
||||
webhookPublicId: webhookPublicId as string,
|
||||
name: data.name,
|
||||
url: data.url,
|
||||
secret: data.secret || undefined,
|
||||
@@ -195,12 +174,11 @@ export function NewWebhookModal({
|
||||
setIsTestingWebhook(true);
|
||||
testWebhookMutation.mutate({
|
||||
workspacePublicId,
|
||||
webhookPublicId: webhookPublicId,
|
||||
webhookPublicId: webhookPublicId as string,
|
||||
});
|
||||
};
|
||||
|
||||
const isPending =
|
||||
createWebhookMutation.isPending || updateWebhookMutation.isPending;
|
||||
const isPending = createWebhookMutation.isPending || updateWebhookMutation.isPending;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
@@ -253,11 +231,7 @@ 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}
|
||||
/>
|
||||
@@ -278,7 +252,7 @@ export function NewWebhookModal({
|
||||
{webhookEvents.map((event) => (
|
||||
<label
|
||||
key={event}
|
||||
className="flex cursor-pointer items-center space-x-2"
|
||||
className="flex items-center space-x-2 cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -288,11 +262,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="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-light-400 dark:border-dark-400"
|
||||
className="h-4 w-4 rounded border-light-400 text-primary-600 focus:ring-primary-500 dark:border-dark-400"
|
||||
/>
|
||||
<span className="text-sm text-light-900 dark:text-dark-900">
|
||||
{event}
|
||||
@@ -303,19 +277,17 @@ 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 cursor-pointer items-center space-x-2">
|
||||
<label className="flex items-center space-x-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
{...register("active")}
|
||||
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-light-400 dark:border-dark-400"
|
||||
className="h-4 w-4 rounded border-light-400 text-primary-600 focus:ring-primary-500 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 as 0 | 1 | 6,
|
||||
weekStartDay: newValue,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ const UpdateWorkspaceUrlForm = ({
|
||||
}: {
|
||||
workspacePublicId: string;
|
||||
workspaceUrl: string;
|
||||
workspacePlan: "free" | "team" | "pro" | "enterprise";
|
||||
workspacePlan: "free" | "pro" | "enterprise";
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const utils = api.useUtils();
|
||||
|
||||
@@ -171,20 +171,18 @@ export default function WebhookList({ workspacePublicId }: WebhookListProps) {
|
||||
const testWebhookMutation = api.webhook.test.useMutation({
|
||||
onSuccess: (result) => {
|
||||
if (result.success) {
|
||||
showPopup({ header: t`Test sent`, message: t`Test webhook sent successfully!`, icon: "success" });
|
||||
showPopup({ message: t`Test webhook sent successfully!`, type: "success" });
|
||||
} else {
|
||||
showPopup({
|
||||
header: t`Test failed`,
|
||||
message: result.error || t`Webhook test failed`,
|
||||
icon: "error",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
showPopup({
|
||||
header: t`Unable to test webhook`,
|
||||
message: error.message || t`Failed to test webhook`,
|
||||
icon: "error",
|
||||
type: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -99,19 +99,6 @@ services:
|
||||
- NEXT_PUBLIC_UMAMI_ID=${NEXT_PUBLIC_UMAMI_ID}
|
||||
- NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY}
|
||||
- NEXT_PUBLIC_POSTHOG_HOST=${NEXT_PUBLIC_POSTHOG_HOST}
|
||||
|
||||
# Partner config (optional)
|
||||
- NEXT_PUBLIC_PARTNER_NAME=${NEXT_PUBLIC_PARTNER_NAME}
|
||||
- PARTNER_CLIENT_ID=${PARTNER_CLIENT_ID}
|
||||
- PARTNER_CLIENT_SECRET=${PARTNER_CLIENT_SECRET}
|
||||
- PARTNER_REDIRECT_URL=${PARTNER_REDIRECT_URL}
|
||||
- PARTNER_TOKEN_URL=${PARTNER_TOKEN_URL}
|
||||
- PARTNER_OAUTH_LICENSE_URL=${PARTNER_OAUTH_LICENSE_URL}
|
||||
- PARTNER_LICENSE_API_URL=${PARTNER_LICENSE_API_URL}
|
||||
- PARTNER_API_KEY=${PARTNER_API_KEY}
|
||||
- PARTNER_API_KEY_HEADER=${PARTNER_API_KEY_HEADER}
|
||||
- PARTNER_SIGNATURE_HEADER=${PARTNER_SIGNATURE_HEADER}
|
||||
- PARTNER_TIMESTAMP_HEADER=${PARTNER_TIMESTAMP_HEADER}
|
||||
depends_on:
|
||||
migrator:
|
||||
condition: service_completed_successfully
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -5,35 +5,35 @@
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./root": {
|
||||
"types": "./src/root.ts",
|
||||
"types": "./dist/root.d.ts",
|
||||
"default": "./src/root.ts"
|
||||
},
|
||||
"./trpc": {
|
||||
"types": "./src/trpc.ts",
|
||||
"types": "./dist/trpc.d.ts",
|
||||
"default": "./src/trpc.ts"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./src/types/router.types.ts",
|
||||
"types": "./dist/types.d.ts",
|
||||
"default": "./src/types/router.types.ts"
|
||||
},
|
||||
"./openapi": {
|
||||
"types": "./src/openapi.ts",
|
||||
"types": "./dist/openapi.d.ts",
|
||||
"default": "./src/openapi.ts"
|
||||
},
|
||||
"./utils/rateLimit": {
|
||||
"types": "./src/utils/rateLimit.ts",
|
||||
"types": "./dist/utils/rateLimit.d.ts",
|
||||
"default": "./src/utils/rateLimit.ts"
|
||||
},
|
||||
"./utils/apiLogging": {
|
||||
"types": "./src/utils/apiLogging.ts",
|
||||
"types": "./dist/utils/apiLogging.d.ts",
|
||||
"default": "./src/utils/apiLogging.ts"
|
||||
},
|
||||
"./utils/permissions": {
|
||||
"types": "./src/utils/permissions.ts",
|
||||
"types": "./dist/utils/permissions.d.ts",
|
||||
"default": "./src/utils/permissions.ts"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,7 +8,6 @@ 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";
|
||||
|
||||
@@ -111,7 +110,7 @@ export const attachmentRouter = createTRPCRouter({
|
||||
size: z.number().positive(),
|
||||
}),
|
||||
)
|
||||
.output(attachmentConfirmResponseSchema)
|
||||
.output(z.custom<Awaited<ReturnType<typeof cardAttachmentRepo.create>>>())
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
|
||||
@@ -16,13 +16,6 @@ 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({
|
||||
@@ -44,7 +37,9 @@ export const boardRouter = createTRPCRouter({
|
||||
archived: z.boolean().optional(),
|
||||
}),
|
||||
)
|
||||
.output(z.array(boardListItemSchema))
|
||||
.output(
|
||||
z.custom<Awaited<ReturnType<typeof boardRepo.getAllByWorkspaceId>>>(),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -111,7 +106,7 @@ export const boardRouter = createTRPCRouter({
|
||||
type: z.enum(["regular", "template"]).optional(),
|
||||
}),
|
||||
)
|
||||
.output(boardDetailSchema)
|
||||
.output(z.custom<Awaited<ReturnType<typeof boardRepo.getByPublicId>>>())
|
||||
.query(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -251,7 +246,7 @@ export const boardRouter = createTRPCRouter({
|
||||
.optional(),
|
||||
}),
|
||||
)
|
||||
.output(boardBySlugSchema.nullable())
|
||||
.output(z.custom<Awaited<ReturnType<typeof boardRepo.getBySlug>>>())
|
||||
.query(async ({ ctx, input }) => {
|
||||
const workspace = await workspaceRepo.getBySlugWithBoards(
|
||||
ctx.db,
|
||||
@@ -304,7 +299,7 @@ export const boardRouter = createTRPCRouter({
|
||||
sourceBoardPublicId: z.string().min(12).optional(),
|
||||
}),
|
||||
)
|
||||
.output(boardCreateResponseSchema)
|
||||
.output(z.custom<Awaited<ReturnType<typeof boardRepo.create>>>())
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -474,7 +469,7 @@ export const boardRouter = createTRPCRouter({
|
||||
isArchived: z.boolean().optional(),
|
||||
}),
|
||||
)
|
||||
.output(boardUpdateResponseSchema)
|
||||
.output(z.object({ success: z.boolean() }).or(z.custom<Awaited<ReturnType<typeof boardRepo.update>>>()))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
|
||||
@@ -10,14 +10,6 @@ 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";
|
||||
@@ -50,7 +42,7 @@ export const cardRouter = createTRPCRouter({
|
||||
dueDate: z.date().nullable().optional(),
|
||||
}),
|
||||
)
|
||||
.output(cardCreateResponseSchema)
|
||||
.output(z.custom<Awaited<ReturnType<typeof cardRepo.create>>>())
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -78,7 +70,6 @@ export const cardRouter = createTRPCRouter({
|
||||
description: input.description,
|
||||
createdBy: userId,
|
||||
listId: list.id,
|
||||
workspaceId: list.workspaceId,
|
||||
position: input.position,
|
||||
dueDate: input.dueDate ?? null,
|
||||
});
|
||||
@@ -187,11 +178,10 @@ export const cardRouter = createTRPCRouter({
|
||||
"card.created",
|
||||
{
|
||||
id: String(newCard.id),
|
||||
publicId: newCard.publicId,
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
dueDate: input.dueDate ?? null,
|
||||
listId: list.publicId,
|
||||
listId: String(newCard.listId),
|
||||
},
|
||||
{
|
||||
boardId: list.boardPublicId,
|
||||
@@ -225,7 +215,7 @@ export const cardRouter = createTRPCRouter({
|
||||
comment: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.output(commentResponseSchema)
|
||||
.output(z.custom<Awaited<ReturnType<typeof cardCommentRepo.create>>>())
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -298,7 +288,7 @@ export const cardRouter = createTRPCRouter({
|
||||
comment: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.output(commentResponseSchema)
|
||||
.output(z.custom<Awaited<ReturnType<typeof cardCommentRepo.update>>>())
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -386,7 +376,7 @@ export const cardRouter = createTRPCRouter({
|
||||
commentPublicId: z.string().min(12),
|
||||
}),
|
||||
)
|
||||
.output(commentDeleteResponseSchema)
|
||||
.output(z.custom<Awaited<ReturnType<typeof cardCommentRepo.softDelete>>>())
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -445,7 +435,7 @@ export const cardRouter = createTRPCRouter({
|
||||
createdBy: userId,
|
||||
});
|
||||
|
||||
return { publicId: input.commentPublicId };
|
||||
return deletedComment;
|
||||
}),
|
||||
addOrRemoveLabel: protectedProcedure
|
||||
.meta({
|
||||
@@ -649,7 +639,25 @@ export const cardRouter = createTRPCRouter({
|
||||
},
|
||||
})
|
||||
.input(z.object({ cardPublicId: z.string().min(12) }))
|
||||
.output(cardDetailSchema)
|
||||
.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;
|
||||
}[];
|
||||
}
|
||||
>(),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const card = await cardRepo.getWorkspaceAndCardIdByCardPublicId(
|
||||
ctx.db,
|
||||
@@ -755,7 +763,15 @@ export const cardRouter = createTRPCRouter({
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
activities: z.array(activityItemSchema),
|
||||
activities: z.array(
|
||||
z.custom<
|
||||
NonNullable<
|
||||
Awaited<
|
||||
ReturnType<typeof cardActivityRepo.getPaginatedActivities>
|
||||
>
|
||||
>["activities"][number]
|
||||
>(),
|
||||
),
|
||||
hasMore: z.boolean(),
|
||||
nextCursor: z.string().datetime().nullable(),
|
||||
}),
|
||||
@@ -855,7 +871,7 @@ export const cardRouter = createTRPCRouter({
|
||||
dueDate: z.date().nullable().optional(),
|
||||
}),
|
||||
)
|
||||
.output(cardUpdateResponseSchema)
|
||||
.output(z.custom<Awaited<ReturnType<typeof cardRepo.update>>>())
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -890,18 +906,9 @@ export const cardRouter = createTRPCRouter({
|
||||
);
|
||||
|
||||
let newListId: number | undefined;
|
||||
let newList:
|
||||
| {
|
||||
id: number;
|
||||
publicId: string;
|
||||
name: string;
|
||||
boardId: number;
|
||||
index: number;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
if (input.listPublicId) {
|
||||
newList = await listRepo.getByPublicId(
|
||||
const newList = await listRepo.getByPublicId(
|
||||
ctx.db,
|
||||
input.listPublicId,
|
||||
);
|
||||
@@ -946,7 +953,7 @@ export const cardRouter = createTRPCRouter({
|
||||
);
|
||||
}
|
||||
|
||||
if (input.index !== undefined || newListId !== undefined) {
|
||||
if (input.index !== undefined) {
|
||||
result = await cardRepo.reorder(ctx.db, {
|
||||
cardId: existingCard.id,
|
||||
newIndex: input.index,
|
||||
@@ -1048,19 +1055,8 @@ export const cardRouter = createTRPCRouter({
|
||||
) {
|
||||
webhookChanges.dueDate = { from: previousDueDate, to: input.dueDate };
|
||||
}
|
||||
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!,
|
||||
};
|
||||
if (newListId && existingCard.listId !== newListId) {
|
||||
webhookChanges.listId = { from: existingCard.listId, to: newListId };
|
||||
}
|
||||
|
||||
// Fire webhooks (non-blocking)
|
||||
@@ -1068,19 +1064,20 @@ export const cardRouter = createTRPCRouter({
|
||||
ctx.db,
|
||||
card.workspaceId,
|
||||
createCardWebhookPayload(
|
||||
movedToNewList ? "card.moved" : "card.updated",
|
||||
newListId && existingCard.listId !== newListId
|
||||
? "card.moved"
|
||||
: "card.updated",
|
||||
{
|
||||
id: String(result.id),
|
||||
publicId: result.publicId,
|
||||
title: result.title,
|
||||
description: result.description,
|
||||
dueDate: result.dueDate,
|
||||
listId: currentWebhookListPublicId,
|
||||
listId: String(newListId ?? existingCard.listId),
|
||||
},
|
||||
{
|
||||
boardId: card.boardPublicId,
|
||||
boardName: card.boardName,
|
||||
listName: currentWebhookListName,
|
||||
listName: card.listName,
|
||||
user: ctx.user
|
||||
? { id: ctx.user.id, name: ctx.user.name }
|
||||
: undefined,
|
||||
@@ -1167,16 +1164,15 @@ export const cardRouter = createTRPCRouter({
|
||||
"card.deleted",
|
||||
{
|
||||
id: String(fullCard.id),
|
||||
publicId: fullCard.publicId,
|
||||
title: fullCard.title,
|
||||
description: fullCard.description,
|
||||
dueDate: fullCard.dueDate,
|
||||
listId: fullCard.list.publicId,
|
||||
listId: String(fullCard.listId),
|
||||
},
|
||||
{
|
||||
boardId: card.boardPublicId,
|
||||
boardName: card.boardName,
|
||||
listName: fullCard.list.name,
|
||||
listName: card.listName,
|
||||
user: ctx.user
|
||||
? { id: ctx.user.id, name: ctx.user.name }
|
||||
: undefined,
|
||||
@@ -1276,7 +1272,6 @@ export const cardRouter = createTRPCRouter({
|
||||
description: sourceCard.description ?? "",
|
||||
createdBy: userId,
|
||||
listId: targetList.id,
|
||||
workspaceId: targetList.workspaceId,
|
||||
position: "end",
|
||||
dueDate: sourceCard.dueDate ?? null,
|
||||
});
|
||||
|
||||
@@ -340,7 +340,6 @@ export const importRouter = createTRPCRouter({
|
||||
description: card.description,
|
||||
createdBy: userId,
|
||||
listId: newListId,
|
||||
workspaceId: workspace.id,
|
||||
index,
|
||||
importId: newImportId,
|
||||
}));
|
||||
@@ -889,7 +888,6 @@ export const importRouter = createTRPCRouter({
|
||||
description: data.description,
|
||||
createdBy: userId,
|
||||
listId: data.listId,
|
||||
workspaceId: workspace.id,
|
||||
index: index,
|
||||
importId: newImportId,
|
||||
}));
|
||||
|
||||
@@ -7,7 +7,6 @@ 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({
|
||||
@@ -28,7 +27,7 @@ export const listRouter = createTRPCRouter({
|
||||
boardPublicId: z.string().min(12),
|
||||
}),
|
||||
)
|
||||
.output(listCreateResponseSchema)
|
||||
.output(z.custom<Awaited<ReturnType<typeof listRepo.create>>>())
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -164,7 +163,12 @@ export const listRouter = createTRPCRouter({
|
||||
index: z.number().optional(),
|
||||
}),
|
||||
)
|
||||
.output(listUpdateResponseSchema)
|
||||
.output(
|
||||
z.custom<
|
||||
| Awaited<ReturnType<typeof listRepo.update>>
|
||||
| Awaited<ReturnType<typeof listRepo.reorder>>
|
||||
>(),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
import { updateSubscriptionSeats } from "@kan/stripe";
|
||||
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||
import { memberInviteResponseSchema } from "../schemas";
|
||||
import {
|
||||
assertCanManageMember,
|
||||
assertCanManageRole,
|
||||
@@ -41,7 +40,7 @@ export const memberRouter = createTRPCRouter({
|
||||
workspacePublicId: z.string().min(12),
|
||||
}),
|
||||
)
|
||||
.output(memberInviteResponseSchema)
|
||||
.output(z.custom<Awaited<ReturnType<typeof memberRepo.create>>>())
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import * as userRepo from "@kan/db/repository/user.repo";
|
||||
import { generateAvatarUrl } from "@kan/shared/utils";
|
||||
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { generateAvatarUrl } from "@kan/shared/utils";
|
||||
|
||||
export const userRouter = createTRPCRouter({
|
||||
getUser: protectedProcedure
|
||||
@@ -27,8 +27,6 @@ export const userRouter = createTRPCRouter({
|
||||
name: z.string().nullable(),
|
||||
image: z.string().nullable(),
|
||||
stripeCustomerId: z.string().nullable(),
|
||||
hasPassword: z.boolean(),
|
||||
hasMagicLinkAccount: z.boolean(),
|
||||
apiKey: z
|
||||
.object({
|
||||
id: z.number(),
|
||||
@@ -63,8 +61,6 @@ export const userRouter = createTRPCRouter({
|
||||
return {
|
||||
...result,
|
||||
image: imageUrl,
|
||||
hasPassword: result.hasPassword,
|
||||
hasMagicLinkAccount: result.hasMagicLinkAccount,
|
||||
apiKey: apiKey ? { id: apiKey.id, prefix: apiKey.prefix } : null,
|
||||
};
|
||||
}),
|
||||
@@ -118,60 +114,4 @@ export const userRouter = createTRPCRouter({
|
||||
image: imageUrl,
|
||||
};
|
||||
}),
|
||||
setPassword: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: "POST",
|
||||
path: "/users/me/password",
|
||||
summary: "Set password",
|
||||
description:
|
||||
"Sets a password for a user who signed up via magic link and has no password yet",
|
||||
tags: ["Users"],
|
||||
protect: true,
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
newPassword: z
|
||||
.string()
|
||||
.min(8, "Password must be at least 8 characters"),
|
||||
}),
|
||||
)
|
||||
.output(z.object({ success: z.boolean() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
if (!userId)
|
||||
throw new TRPCError({
|
||||
message: `User not authenticated`,
|
||||
code: "UNAUTHORIZED",
|
||||
});
|
||||
|
||||
const existing = await userRepo.getById(ctx.db, userId);
|
||||
|
||||
if (!existing) {
|
||||
throw new TRPCError({
|
||||
message: `User not found`,
|
||||
code: "NOT_FOUND",
|
||||
});
|
||||
}
|
||||
|
||||
if (existing.hasPassword) {
|
||||
throw new TRPCError({
|
||||
message: `Password already set; use change password instead`,
|
||||
code: "BAD_REQUEST",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await ctx.auth.api.setPassword({ newPassword: input.newPassword });
|
||||
} catch {
|
||||
throw new TRPCError({
|
||||
message: "Failed to set password",
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -334,7 +334,6 @@ export const webhookRouter = createTRPCRouter({
|
||||
|
||||
const testPayload = createCardWebhookPayload("card.created", {
|
||||
id: "test-card-id",
|
||||
publicId: "test-card-public-id",
|
||||
title: "Test Card",
|
||||
description: "This is a test webhook payload",
|
||||
dueDate: null,
|
||||
|
||||
@@ -7,14 +7,6 @@ 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({
|
||||
@@ -30,7 +22,9 @@ export const workspaceRouter = createTRPCRouter({
|
||||
},
|
||||
})
|
||||
.input(z.void())
|
||||
.output(z.array(workspaceListItemSchema))
|
||||
.output(
|
||||
z.custom<Awaited<ReturnType<typeof workspaceRepo.getAllByUserId>>>(),
|
||||
)
|
||||
.query(async ({ ctx }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -56,7 +50,11 @@ export const workspaceRouter = createTRPCRouter({
|
||||
},
|
||||
})
|
||||
.input(z.object({ workspacePublicId: z.string().min(12) }))
|
||||
.output(workspaceDetailSchema)
|
||||
.output(
|
||||
z.custom<
|
||||
Awaited<ReturnType<typeof workspaceRepo.getByPublicIdWithMembers>>
|
||||
>(),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -136,7 +134,7 @@ export const workspaceRouter = createTRPCRouter({
|
||||
return {
|
||||
...result,
|
||||
members: sanitizedMembers,
|
||||
};
|
||||
} as Awaited<ReturnType<typeof workspaceRepo.getByPublicIdWithMembers>>;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -164,7 +162,9 @@ export const workspaceRouter = createTRPCRouter({
|
||||
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/),
|
||||
}),
|
||||
)
|
||||
.output(workspaceWithBoardsSchema)
|
||||
.output(
|
||||
z.custom<Awaited<ReturnType<typeof workspaceRepo.getBySlugWithBoards>>>(),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -211,7 +211,7 @@ export const workspaceRouter = createTRPCRouter({
|
||||
.optional(),
|
||||
}),
|
||||
)
|
||||
.output(workspaceCreateResponseSchema)
|
||||
.output(z.custom<Awaited<ReturnType<typeof workspaceRepo.create>>>())
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
const userEmail = ctx.user?.email;
|
||||
@@ -270,14 +270,7 @@ export const workspaceRouter = createTRPCRouter({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
});
|
||||
|
||||
return {
|
||||
publicId: result.publicId,
|
||||
name: result.name!,
|
||||
slug: result.slug!,
|
||||
description: result.description ?? null,
|
||||
plan: result.plan!,
|
||||
cardPrefix: result.cardPrefix!,
|
||||
};
|
||||
return result;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
@@ -307,7 +300,7 @@ export const workspaceRouter = createTRPCRouter({
|
||||
.optional(),
|
||||
}),
|
||||
)
|
||||
.output(workspaceUpdateResponseSchema)
|
||||
.output(z.custom<Awaited<ReturnType<typeof workspaceRepo.update>>>())
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -390,7 +383,7 @@ export const workspaceRouter = createTRPCRouter({
|
||||
},
|
||||
})
|
||||
.input(z.object({ workspacePublicId: z.string().min(12) }))
|
||||
.output(workspaceDeleteResponseSchema)
|
||||
.output(z.custom<Awaited<ReturnType<typeof workspaceRepo.hardDelete>>>())
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
@@ -412,12 +405,12 @@ export const workspaceRouter = createTRPCRouter({
|
||||
});
|
||||
await assertPermission(ctx.db, userId, workspace.id, "workspace:delete");
|
||||
|
||||
await workspaceRepo.hardDelete(
|
||||
const result = await workspaceRepo.hardDelete(
|
||||
ctx.db,
|
||||
input.workspacePublicId,
|
||||
);
|
||||
|
||||
return { success: true };
|
||||
return result;
|
||||
}),
|
||||
checkSlugAvailability: publicProcedure
|
||||
.meta({
|
||||
@@ -522,7 +515,6 @@ 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"),
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
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(),
|
||||
});
|
||||
@@ -1,132 +0,0 @@
|
||||
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(),
|
||||
cardNumber: z.number().nullable(),
|
||||
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(),
|
||||
cardPrefix: 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() }),
|
||||
]);
|
||||
@@ -1,218 +0,0 @@
|
||||
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(),
|
||||
cardNumber: z.number().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(),
|
||||
cardPrefix: 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(),
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
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(),
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
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";
|
||||
@@ -1,13 +0,0 @@
|
||||
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(),
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
// ─── member.invite ───────────────────────────────────────────
|
||||
export const memberInviteResponseSchema = z.object({
|
||||
publicId: z.string(),
|
||||
});
|
||||
@@ -1,96 +0,0 @@
|
||||
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(),
|
||||
cardPrefix: z.string(),
|
||||
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(),
|
||||
partnerTier: z.number().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"]),
|
||||
cardPrefix: z.string(),
|
||||
});
|
||||
|
||||
// ─── 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(),
|
||||
});
|
||||
@@ -63,11 +63,6 @@ const createAuthWithHeaders = (
|
||||
headers,
|
||||
query: { referenceId: input.workspacePublicId },
|
||||
}),
|
||||
setPassword: (input: { newPassword: string }) =>
|
||||
auth.api.setPassword({
|
||||
headers,
|
||||
body: { newPassword: input.newPassword },
|
||||
}),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
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,
|
||||
@@ -43,7 +32,6 @@ describe("webhook utilities", () => {
|
||||
"card.created",
|
||||
{
|
||||
id: "card-123",
|
||||
publicId: "card-pub-123",
|
||||
title: "Test Card",
|
||||
listId: "list-456",
|
||||
},
|
||||
@@ -56,14 +44,12 @@ describe("webhook utilities", () => {
|
||||
expect(payload.timestamp).toBe("2024-01-15T12:00:00.000Z");
|
||||
expect(payload.data.card).toEqual({
|
||||
id: "card-123",
|
||||
publicId: "card-pub-123",
|
||||
title: "Test Card",
|
||||
description: undefined,
|
||||
dueDate: null,
|
||||
listId: "list-456",
|
||||
boardId: "board-789",
|
||||
});
|
||||
expect(payload.data.card.publicId).toBe("card-pub-123");
|
||||
});
|
||||
|
||||
it("includes optional card fields when provided", () => {
|
||||
@@ -72,7 +58,6 @@ describe("webhook utilities", () => {
|
||||
"card.updated",
|
||||
{
|
||||
id: "card-123",
|
||||
publicId: "card-pub-123",
|
||||
title: "Test Card",
|
||||
description: "A description",
|
||||
dueDate,
|
||||
@@ -92,7 +77,6 @@ describe("webhook utilities", () => {
|
||||
"card.created",
|
||||
{
|
||||
id: "card-123",
|
||||
publicId: "card-pub-123",
|
||||
title: "Test Card",
|
||||
listId: "list-456",
|
||||
},
|
||||
@@ -113,7 +97,6 @@ describe("webhook utilities", () => {
|
||||
"card.created",
|
||||
{
|
||||
id: "card-123",
|
||||
publicId: "card-pub-123",
|
||||
title: "Test Card",
|
||||
listId: "list-456",
|
||||
},
|
||||
@@ -134,7 +117,6 @@ describe("webhook utilities", () => {
|
||||
"card.created",
|
||||
{
|
||||
id: "card-123",
|
||||
publicId: "card-pub-123",
|
||||
title: "Test Card",
|
||||
listId: "list-456",
|
||||
},
|
||||
@@ -158,7 +140,6 @@ describe("webhook utilities", () => {
|
||||
"card.updated",
|
||||
{
|
||||
id: "card-123",
|
||||
publicId: "card-pub-123",
|
||||
title: "Updated Title",
|
||||
listId: "list-456",
|
||||
},
|
||||
@@ -174,34 +155,6 @@ 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",
|
||||
publicId: "card-pub-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", () => {
|
||||
@@ -221,7 +174,6 @@ describe("webhook utilities", () => {
|
||||
data: {
|
||||
card: {
|
||||
id: "card-123",
|
||||
publicId: "card-pub-123",
|
||||
title: "Test Card",
|
||||
listId: "list-456",
|
||||
boardId: "board-789",
|
||||
@@ -418,7 +370,6 @@ describe("webhook utilities", () => {
|
||||
data: {
|
||||
card: {
|
||||
id: "card-123",
|
||||
publicId: "card-pub-123",
|
||||
title: "Test Card",
|
||||
listId: "list-456",
|
||||
boardId: "board-789",
|
||||
@@ -483,6 +434,8 @@ describe("webhook utilities", () => {
|
||||
});
|
||||
|
||||
it("continues sending to other webhooks when one fails", async () => {
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
mockGetActiveByWorkspaceId.mockResolvedValueOnce([
|
||||
{
|
||||
id: 1,
|
||||
@@ -509,15 +462,11 @@ describe("webhook utilities", () => {
|
||||
await sendWebhooksForWorkspace(mockDb, 1, mockPayload);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledTimes(2);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: "https://example.com/webhook1",
|
||||
event: "card.created",
|
||||
error: "500 Error",
|
||||
statusCode: 500,
|
||||
}),
|
||||
"Webhook delivery failed",
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Webhook delivery failed"),
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("handles empty webhook list", async () => {
|
||||
@@ -529,6 +478,9 @@ 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"),
|
||||
);
|
||||
@@ -538,13 +490,12 @@ describe("webhook utilities", () => {
|
||||
sendWebhooksForWorkspace(mockDb, 1, mockPayload),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
err: expect.any(Error),
|
||||
workspaceId: 1,
|
||||
}),
|
||||
"Failed to send webhooks for workspace",
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"Failed to send webhooks for workspace:",
|
||||
expect.any(Error),
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ export interface WebhookPayload {
|
||||
data: {
|
||||
card: {
|
||||
id: string;
|
||||
publicId: string;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
dueDate?: string | null; // ISO string after JSON serialization
|
||||
@@ -222,7 +221,6 @@ export function createCardWebhookPayload(
|
||||
event: WebhookEventType,
|
||||
card: {
|
||||
id: string;
|
||||
publicId: string;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
dueDate?: Date | null;
|
||||
@@ -245,7 +243,6 @@ export function createCardWebhookPayload(
|
||||
data: {
|
||||
card: {
|
||||
id: card.id,
|
||||
publicId: card.publicId,
|
||||
title: card.title,
|
||||
description: card.description,
|
||||
dueDate: card.dueDate?.toISOString() ?? null,
|
||||
|
||||
@@ -170,7 +170,7 @@ export function createPlugins(db: dbClient) {
|
||||
if (authorization?.startsWith("Bearer ")) {
|
||||
return authorization.slice(7);
|
||||
}
|
||||
return ctx.headers?.get("x-api-key") ?? null;
|
||||
return ctx.headers?.get("x-api-key") ?? undefined;
|
||||
},
|
||||
rateLimit: {
|
||||
enabled: true,
|
||||
|
||||
@@ -92,6 +92,14 @@ 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);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
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);
|
||||
@@ -1,3 +0,0 @@
|
||||
ALTER TABLE "subscription" ADD COLUMN "partnerLicenseKey" varchar(255);--> statement-breakpoint
|
||||
ALTER TABLE "subscription" ADD COLUMN "partnerTier" integer;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "subscription_partner_license_key_idx" ON "subscription" USING btree ("partnerLicenseKey");
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -225,20 +225,6 @@
|
||||
"when": 1775170588247,
|
||||
"tag": "20260402225628_AddTeamWorkspacePlan",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 32,
|
||||
"version": "7",
|
||||
"when": 1776809379931,
|
||||
"tag": "20260421220939_AddCardNumber",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 33,
|
||||
"version": "7",
|
||||
"when": 1778617946519,
|
||||
"tag": "20260512203226_AddPartnerLicenseToSubscription",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -5,27 +5,27 @@
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./client": {
|
||||
"types": "./src/client.ts",
|
||||
"types": "./dist/client.d.ts",
|
||||
"default": "./src/client.ts"
|
||||
},
|
||||
"./schema": {
|
||||
"types": "./src/schema/index.ts",
|
||||
"types": "./dist/schema.d.ts",
|
||||
"default": "./src/schema/index.ts"
|
||||
},
|
||||
"./types/*": {
|
||||
"types": "./src/types/*",
|
||||
"types": "./dist/types/*.d.ts",
|
||||
"default": "./src/types/*"
|
||||
},
|
||||
"./repository/*": {
|
||||
"types": "./src/repository/*.ts",
|
||||
"types": "./dist/repository/*.d.ts",
|
||||
"default": "./src/repository/*.ts"
|
||||
},
|
||||
"./redis": {
|
||||
"types": "./src/redis.ts",
|
||||
"types": "./dist/redis.d.ts",
|
||||
"default": "./src/redis.ts"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -209,7 +209,6 @@ export const getByPublicId = async (
|
||||
workspace: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
cardPrefix: true,
|
||||
},
|
||||
with: {
|
||||
members: {
|
||||
@@ -221,7 +220,6 @@ export const getByPublicId = async (
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
image: true,
|
||||
@@ -256,7 +254,6 @@ export const getByPublicId = async (
|
||||
listId: true,
|
||||
index: true,
|
||||
dueDate: true,
|
||||
cardNumber: true,
|
||||
},
|
||||
with: {
|
||||
labels: {
|
||||
@@ -281,7 +278,6 @@ export const getByPublicId = async (
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
image: true,
|
||||
@@ -426,7 +422,6 @@ export const getBySlug = async (
|
||||
publicId: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
cardPrefix: true,
|
||||
},
|
||||
},
|
||||
labels: {
|
||||
@@ -453,7 +448,6 @@ export const getBySlug = async (
|
||||
listId: true,
|
||||
index: true,
|
||||
dueDate: true,
|
||||
cardNumber: true,
|
||||
},
|
||||
with: {
|
||||
labels: {
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
labels,
|
||||
lists,
|
||||
workspaceMembers,
|
||||
workspaces,
|
||||
} from "@kan/db/schema";
|
||||
import { generateUID } from "@kan/shared/utils";
|
||||
|
||||
@@ -42,7 +41,6 @@ export const create = async (
|
||||
description: string;
|
||||
createdBy: string;
|
||||
listId: number;
|
||||
workspaceId: number;
|
||||
position: "start" | "end";
|
||||
dueDate?: Date | null;
|
||||
},
|
||||
@@ -84,17 +82,6 @@ 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({
|
||||
@@ -104,10 +91,9 @@ 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, cardNumber: cards.cardNumber });
|
||||
.returning({ id: cards.id, listId: cards.listId, publicId: cards.publicId });
|
||||
|
||||
if (!result[0]) throw new Error("Unable to create card");
|
||||
|
||||
@@ -255,14 +241,6 @@ export const getByPublicId = (db: dbClient, cardPublicId: string) => {
|
||||
listId: true,
|
||||
dueDate: true,
|
||||
},
|
||||
with: {
|
||||
list: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: eq(cards.publicId, cardPublicId),
|
||||
});
|
||||
};
|
||||
@@ -287,7 +265,6 @@ export const bulkCreate = async (
|
||||
description: string;
|
||||
createdBy: string;
|
||||
listId: number;
|
||||
workspaceId: number;
|
||||
index: number;
|
||||
importId?: number;
|
||||
}[],
|
||||
@@ -303,34 +280,6 @@ 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;
|
||||
@@ -338,7 +287,6 @@ export const bulkCreate = async (
|
||||
createdBy: string;
|
||||
listId: number;
|
||||
index: number;
|
||||
cardNumber: number;
|
||||
importId?: number;
|
||||
}[] = [];
|
||||
|
||||
@@ -353,12 +301,6 @@ 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,
|
||||
@@ -366,7 +308,6 @@ export const bulkCreate = async (
|
||||
createdBy: it.createdBy,
|
||||
listId: it.listId,
|
||||
index: nextIndex++,
|
||||
cardNumber,
|
||||
importId: it.importId,
|
||||
});
|
||||
}
|
||||
@@ -485,7 +426,6 @@ export const getWithListAndMembersByPublicId = async (
|
||||
description: true,
|
||||
dueDate: true,
|
||||
createdBy: true,
|
||||
cardNumber: true,
|
||||
},
|
||||
with: {
|
||||
labels: {
|
||||
@@ -562,7 +502,6 @@ export const getWithListAndMembersByPublicId = async (
|
||||
workspace: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
cardPrefix: true,
|
||||
},
|
||||
with: {
|
||||
members: {
|
||||
@@ -1006,7 +945,7 @@ export const getWorkspaceAndCardIdByCardPublicId = async (
|
||||
where: and(eq(cards.publicId, cardPublicId), isNull(cards.deletedAt)),
|
||||
with: {
|
||||
list: {
|
||||
columns: { name: true, publicId: true },
|
||||
columns: { name: true },
|
||||
with: {
|
||||
board: {
|
||||
columns: {
|
||||
@@ -1027,7 +966,6 @@ 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,12 +209,10 @@ 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: and(eq(lists.publicId, listPublicId), isNull(lists.deletedAt)),
|
||||
where: eq(lists.publicId, listPublicId),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -435,7 +433,6 @@ export const getWorkspaceAndListIdByListPublicId = async (
|
||||
return result
|
||||
? {
|
||||
id: result.id,
|
||||
publicId: listPublicId,
|
||||
name: result.name,
|
||||
createdBy: result.createdBy,
|
||||
workspaceId: result.board.workspaceId,
|
||||
|
||||
@@ -7,14 +7,12 @@ export const updateById = async (
|
||||
db: dbClient,
|
||||
subscriptionId: number,
|
||||
updates: {
|
||||
plan?: string;
|
||||
unlimitedSeats?: boolean;
|
||||
status?: string;
|
||||
seats?: number | null;
|
||||
periodStart?: Date | null;
|
||||
periodEnd?: Date | null;
|
||||
cancelAtPeriodEnd?: boolean | null;
|
||||
stripeSubscriptionId?: string | null;
|
||||
},
|
||||
) => {
|
||||
const [result] = await db
|
||||
@@ -91,40 +89,3 @@ export const create = async (
|
||||
const [result] = await db.insert(subscription).values(data).returning();
|
||||
return result;
|
||||
};
|
||||
|
||||
export const getByPartnerLicenseKey = async (
|
||||
db: dbClient,
|
||||
partnerLicenseKey: string,
|
||||
) => {
|
||||
const result = await db.query.subscription.findFirst({
|
||||
where: eq(subscription.partnerLicenseKey, partnerLicenseKey),
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
export const upsertByPartnerLicenseKey = async (
|
||||
db: dbClient,
|
||||
partnerLicenseKey: string,
|
||||
data: {
|
||||
plan: string;
|
||||
status: string;
|
||||
partnerTier: number;
|
||||
seats: number | null;
|
||||
unlimitedSeats: boolean;
|
||||
referenceId?: string;
|
||||
},
|
||||
) => {
|
||||
const [result] = await db
|
||||
.insert(subscription)
|
||||
.values({
|
||||
partnerLicenseKey,
|
||||
...data,
|
||||
referenceId: data.referenceId ?? null,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: subscription.partnerLicenseKey,
|
||||
set: { ...data, updatedAt: new Date() },
|
||||
})
|
||||
.returning();
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { and, count, desc, eq, isNotNull } from "drizzle-orm";
|
||||
import { count, desc, eq } from "drizzle-orm";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import type { dbClient } from "@kan/db/client";
|
||||
import { account, apikey, users } from "@kan/db/schema";
|
||||
|
||||
const PROVIDER_CREDENTIAL = "credential";
|
||||
const PROVIDER_MAGIC_LINK = "magic-link";
|
||||
import { apikey, users } from "@kan/db/schema";
|
||||
|
||||
export const getCount = async (db: dbClient) => {
|
||||
const result = await db.select({ count: count() }).from(users);
|
||||
@@ -14,58 +11,27 @@ export const getCount = async (db: dbClient) => {
|
||||
};
|
||||
|
||||
export const getById = async (db: dbClient, userId: string) => {
|
||||
const [user, credentialAccount, magicLinkAccount] = await Promise.all([
|
||||
db.query.users.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
image: true,
|
||||
stripeCustomerId: true,
|
||||
},
|
||||
with: {
|
||||
apiKeys: {
|
||||
columns: {
|
||||
id: true,
|
||||
prefix: true,
|
||||
key: true,
|
||||
},
|
||||
orderBy: desc(apikey.createdAt),
|
||||
limit: 1,
|
||||
return await db.query.users.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
image: true,
|
||||
stripeCustomerId: true,
|
||||
},
|
||||
with: {
|
||||
apiKeys: {
|
||||
columns: {
|
||||
id: true,
|
||||
prefix: true,
|
||||
key: true,
|
||||
},
|
||||
orderBy: desc(apikey.createdAt),
|
||||
limit: 1,
|
||||
},
|
||||
where: eq(users.id, userId),
|
||||
}),
|
||||
db
|
||||
.select({ id: account.id })
|
||||
.from(account)
|
||||
.where(
|
||||
and(
|
||||
eq(account.userId, userId),
|
||||
eq(account.providerId, PROVIDER_CREDENTIAL),
|
||||
isNotNull(account.password),
|
||||
),
|
||||
)
|
||||
.limit(1),
|
||||
db
|
||||
.select({ id: account.id })
|
||||
.from(account)
|
||||
.where(
|
||||
and(
|
||||
eq(account.userId, userId),
|
||||
eq(account.providerId, PROVIDER_MAGIC_LINK),
|
||||
),
|
||||
)
|
||||
.limit(1),
|
||||
]);
|
||||
|
||||
if (!user) return undefined;
|
||||
|
||||
return {
|
||||
...user,
|
||||
hasPassword: credentialAccount.length > 0,
|
||||
hasMagicLinkAccount: magicLinkAccount.length > 0,
|
||||
};
|
||||
},
|
||||
where: eq(users.id, userId),
|
||||
});
|
||||
};
|
||||
|
||||
export const getByStripeCustomerId = async (
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
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,
|
||||
@@ -20,11 +19,8 @@ import {
|
||||
workspaceMembers,
|
||||
workspaces,
|
||||
} from "@kan/db/schema";
|
||||
import {
|
||||
generateUID,
|
||||
generateWorkspacePrefix,
|
||||
getDefaultPermissions,
|
||||
} from "@kan/shared";
|
||||
import type { Permission, Role } from "@kan/shared";
|
||||
import { generateUID, getDefaultPermissions } from "@kan/shared";
|
||||
|
||||
import * as permissionRepo from "./permission.repo";
|
||||
|
||||
@@ -34,22 +30,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
|
||||
@@ -79,12 +75,8 @@ 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,
|
||||
@@ -93,7 +85,6 @@ export const create = async (
|
||||
slug: workspaces.slug,
|
||||
description: workspaces.description,
|
||||
plan: workspaces.plan,
|
||||
cardPrefix: workspaces.cardPrefix,
|
||||
});
|
||||
|
||||
if (workspace) {
|
||||
@@ -239,7 +230,6 @@ export const getByPublicIdWithMembers = (
|
||||
status: true,
|
||||
seats: true,
|
||||
unlimitedSeats: true,
|
||||
partnerTier: true,
|
||||
periodStart: true,
|
||||
periodEnd: true,
|
||||
},
|
||||
@@ -268,12 +258,8 @@ 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(
|
||||
@@ -297,7 +283,6 @@ 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
|
||||
@@ -380,14 +365,6 @@ 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,
|
||||
@@ -396,8 +373,6 @@ export const searchBoardsAndCards = async (
|
||||
) => {
|
||||
const searchQuery = `%${query}%`;
|
||||
|
||||
const ticketId = parseTicketId(query.trim());
|
||||
|
||||
// Search for boards
|
||||
const boardResults = await db
|
||||
.select({
|
||||
@@ -425,29 +400,9 @@ export const searchBoardsAndCards = async (
|
||||
sql`similarity(${boards.name}, ${query}) DESC`,
|
||||
desc(boards.updatedAt),
|
||||
)
|
||||
.limit(ticketId ? 0 : Math.ceil(limit * 0.4));
|
||||
|
||||
// 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),
|
||||
sql`similarity(${cards.title}, ${query}) > 0.2`,
|
||||
),
|
||||
isNull(cards.deletedAt),
|
||||
isNull(lists.deletedAt),
|
||||
isNull(boards.deletedAt),
|
||||
);
|
||||
.limit(Math.ceil(limit * 0.4));
|
||||
|
||||
// Search for cards
|
||||
const cardResults = await db
|
||||
.select({
|
||||
publicId: cards.publicId,
|
||||
@@ -456,36 +411,35 @@ export const searchBoardsAndCards = async (
|
||||
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(
|
||||
...(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),
|
||||
]),
|
||||
.where(
|
||||
and(
|
||||
eq(boards.workspaceId, workspaceId),
|
||||
or(
|
||||
ilike(cards.title, searchQuery),
|
||||
sql`similarity(${cards.title}, ${query}) > 0.2`,
|
||||
),
|
||||
isNull(cards.deletedAt),
|
||||
isNull(lists.deletedAt),
|
||||
isNull(boards.deletedAt),
|
||||
),
|
||||
)
|
||||
.limit(ticketId ? limit : Math.floor(limit * 0.6));
|
||||
.orderBy(
|
||||
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));
|
||||
|
||||
// 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,7 +2,6 @@ import { relations } from "drizzle-orm";
|
||||
import {
|
||||
bigint,
|
||||
bigserial,
|
||||
index,
|
||||
integer,
|
||||
pgEnum,
|
||||
pgTable,
|
||||
@@ -55,36 +54,27 @@ 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(),
|
||||
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 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 cardsRelations = relations(cards, ({ one, many }) => ({
|
||||
createdBy: one(users, {
|
||||
|
||||
@@ -5,42 +5,31 @@ import {
|
||||
integer,
|
||||
pgTable,
|
||||
timestamp,
|
||||
uniqueIndex,
|
||||
varchar,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
import { workspaces } from "./workspaces";
|
||||
|
||||
export const subscription = pgTable(
|
||||
"subscription",
|
||||
{
|
||||
id: bigserial("id", { mode: "number" }).primaryKey(),
|
||||
plan: varchar("plan", { length: 255 }).notNull(),
|
||||
referenceId: varchar("referenceId", { length: 12 }).references(
|
||||
() => workspaces.publicId,
|
||||
{ onDelete: "set null" },
|
||||
),
|
||||
stripeCustomerId: varchar("stripeCustomerId", { length: 255 }),
|
||||
stripeSubscriptionId: varchar("stripeSubscriptionId", { length: 255 }),
|
||||
status: varchar("status", { length: 255 }).notNull(),
|
||||
periodStart: timestamp("periodStart"),
|
||||
periodEnd: timestamp("periodEnd"),
|
||||
cancelAtPeriodEnd: boolean("cancelAtPeriodEnd"),
|
||||
seats: integer("seats"),
|
||||
unlimitedSeats: boolean("unlimitedSeats").default(false).notNull(),
|
||||
trialStart: timestamp("trialStart"),
|
||||
trialEnd: timestamp("trialEnd"),
|
||||
partnerLicenseKey: varchar("partnerLicenseKey", { length: 255 }),
|
||||
partnerTier: integer("partnerTier"),
|
||||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("subscription_partner_license_key_idx").on(
|
||||
table.partnerLicenseKey,
|
||||
),
|
||||
],
|
||||
).enableRLS();
|
||||
export const subscription = pgTable("subscription", {
|
||||
id: bigserial("id", { mode: "number" }).primaryKey(),
|
||||
plan: varchar("plan", { length: 255 }).notNull(),
|
||||
referenceId: varchar("referenceId", { length: 12 }).references(
|
||||
() => workspaces.publicId,
|
||||
{ onDelete: "set null" },
|
||||
),
|
||||
stripeCustomerId: varchar("stripeCustomerId", { length: 255 }),
|
||||
stripeSubscriptionId: varchar("stripeSubscriptionId", { length: 255 }),
|
||||
status: varchar("status", { length: 255 }).notNull(),
|
||||
periodStart: timestamp("periodStart"),
|
||||
periodEnd: timestamp("periodEnd"),
|
||||
cancelAtPeriodEnd: boolean("cancelAtPeriodEnd"),
|
||||
seats: integer("seats"),
|
||||
unlimitedSeats: boolean("unlimitedSeats").default(false).notNull(),
|
||||
trialStart: timestamp("trialStart"),
|
||||
trialEnd: timestamp("trialEnd"),
|
||||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||
}).enableRLS();
|
||||
|
||||
export const subscriptionsRelations = relations(subscription, ({ one }) => ({
|
||||
workspace: one(workspaces, {
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
bigint,
|
||||
bigserial,
|
||||
boolean,
|
||||
index,
|
||||
integer,
|
||||
pgEnum,
|
||||
pgTable,
|
||||
@@ -39,31 +38,25 @@ 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),
|
||||
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 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 workspaceRelations = relations(workspaces, ({ one, many }) => ({
|
||||
user: one(users, {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user