diff --git a/README.md b/README.md index 3a9a60b68..e88162910 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ Access Memos at `http://localhost:5230` and complete the initial setup. Memos is made possible by the generous support of our sponsors. Their contributions help ensure the project's continued development, maintenance, and growth. +warp yourselfhosted fixermark alik-agaev diff --git a/go.mod b/go.mod index ca6273c55..d0d058c9a 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module github.com/usememos/memos go 1.25 require ( - github.com/aws/aws-sdk-go-v2 v1.38.3 - github.com/aws/aws-sdk-go-v2/config v1.31.6 - github.com/aws/aws-sdk-go-v2/credentials v1.18.10 + github.com/aws/aws-sdk-go-v2 v1.39.2 + github.com/aws/aws-sdk-go-v2/config v1.31.12 + github.com/aws/aws-sdk-go-v2/credentials v1.18.16 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.19.4 github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3 github.com/go-sql-driver/mysql v1.9.3 @@ -24,12 +24,12 @@ require ( github.com/spf13/viper v1.20.1 github.com/stretchr/testify v1.10.0 github.com/usememos/gomark v0.0.0-20250925160223-606d7debad77 - golang.org/x/crypto v0.41.0 - golang.org/x/mod v0.27.0 + golang.org/x/crypto v0.42.0 + golang.org/x/mod v0.28.0 golang.org/x/net v0.43.0 golang.org/x/oauth2 v0.30.0 google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 - google.golang.org/grpc v1.75.0 + google.golang.org/grpc v1.75.1 modernc.org/sqlite v1.38.2 ) @@ -68,18 +68,18 @@ require ( require ( github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.29.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.38.6 // indirect github.com/aws/smithy-go v1.23.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/disintegration/imaging v1.6.2 @@ -91,9 +91,9 @@ require ( github.com/soheilhy/cmux v0.1.5 github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.28.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect golang.org/x/time v0.12.0 // indirect - google.golang.org/protobuf v1.36.8 + google.golang.org/protobuf v1.36.9 gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2b0059054..ccd23bb15 100644 --- a/go.sum +++ b/go.sum @@ -28,22 +28,22 @@ github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6l github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= -github.com/aws/aws-sdk-go-v2 v1.38.3 h1:B6cV4oxnMs45fql4yRH+/Po/YU+597zgWqvDpYMturk= -github.com/aws/aws-sdk-go-v2 v1.38.3/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= +github.com/aws/aws-sdk-go-v2 v1.39.2 h1:EJLg8IdbzgeD7xgvZ+I8M1e0fL0ptn/M47lianzth0I= +github.com/aws/aws-sdk-go-v2 v1.39.2/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00= -github.com/aws/aws-sdk-go-v2/config v1.31.6 h1:a1t8fXY4GT4xjyJExz4knbuoxSCacB5hT/WgtfPyLjo= -github.com/aws/aws-sdk-go-v2/config v1.31.6/go.mod h1:5ByscNi7R+ztvOGzeUaIu49vkMk2soq5NaH5PYe33MQ= -github.com/aws/aws-sdk-go-v2/credentials v1.18.10 h1:xdJnXCouCx8Y0NncgoptztUocIYLKeQxrCgN6x9sdhg= -github.com/aws/aws-sdk-go-v2/credentials v1.18.10/go.mod h1:7tQk08ntj914F/5i9jC4+2HQTAuJirq7m1vZVIhEkWs= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 h1:wbjnrrMnKew78/juW7I2BtKQwa1qlf6EjQgS69uYY14= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6/go.mod h1:AtiqqNrDioJXuUgz3+3T0mBWN7Hro2n9wll2zRUc0ww= +github.com/aws/aws-sdk-go-v2/config v1.31.12 h1:pYM1Qgy0dKZLHX2cXslNacbcEFMkDMl+Bcj5ROuS6p8= +github.com/aws/aws-sdk-go-v2/config v1.31.12/go.mod h1:/MM0dyD7KSDPR+39p9ZNVKaHDLb9qnfDurvVS2KAhN8= +github.com/aws/aws-sdk-go-v2/credentials v1.18.16 h1:4JHirI4zp958zC026Sm+V4pSDwW4pwLefKrc0bF2lwI= +github.com/aws/aws-sdk-go-v2/credentials v1.18.16/go.mod h1:qQMtGx9OSw7ty1yLclzLxXCRbrkjWAM7JnObZjmCB7I= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9 h1:Mv4Bc0mWmv6oDuSWTKnk+wgeqPL5DRFu5bQL9BGPQ8Y= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9/go.mod h1:IKlKfRppK2a1y0gy1yH6zD+yX5uplJ6UuPlgd48dJiQ= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.19.4 h1:BTl+TXrpnrpPWb/J3527GsJ/lMkn7z3GO12j6OlsbRg= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.19.4/go.mod h1:cG2tenc/fscpChiZE29a2crG9uo2t6nQGflFllFL8M8= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 h1:uF68eJA6+S9iVr9WgX1NaRGyQ/6MdIyc4JNUo6TN1FA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6/go.mod h1:qlPeVZCGPiobx8wb1ft0GHT5l+dc6ldnwInDFaMvC7Y= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 h1:pa1DEC6JoI0zduhZePp3zmhWvk/xxm4NB8Hy/Tlsgos= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6/go.mod h1:gxEjPebnhWGJoaDdtDkA0JX46VRg1wcTHYe63OfX5pE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 h1:se2vOWGD3dWQUtfn4wEjRQJb1HK1XsNIt825gskZ970= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9/go.mod h1:hijCGH2VfbZQxqCDN7bwz/4dzxV+hkyhjawAtdPWKZA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 h1:6RBnKZLkJM4hQ+kN6E7yWFveOTg8NLPHAkqrs4ZPlTU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9/go.mod h1:V9rQKRmK7AWuEsOMnHzKj8WyrIir1yUJbZxDuZLFvXI= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6 h1:R0tNFJqfjHL3900cqhXuwQ+1K4G0xc9Yf8EDbFXCKEw= @@ -52,18 +52,18 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebP github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6 h1:hncKj/4gR+TPauZgTAsxOxNcvBayhUlYZ6LO/BYiQ30= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6/go.mod h1:OiIh45tp6HdJDDJGnja0mw8ihQGz3VGrUflLqSL0SmM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 h1:LHS1YAIJXJ4K9zS+1d/xa9JAA9sL2QyXIQCQFQW/X08= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6/go.mod h1:c9PCiTEuh0wQID5/KqA32J+HAgZxN9tOGXKCiYJjTZI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9 h1:5r34CgVOD4WZudeEKZ9/iKpiT6cM1JyEROpXjOcdWv8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9/go.mod h1:dB12CEbNWPbzO2uC6QSWHteqOg4JfBVJOojbAoAUb5I= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6 h1:nEXUSAwyUfLTgnc9cxlDWy637qsq4UWwp3sNAfl0Z3Y= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6/go.mod h1:HGzIULx4Ge3Do2V0FaiYKcyKzOqwrhUZgCI77NisswQ= github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3 h1:ETkfWcXP2KNPLecaDa++5bsQhCRa5M5sLUJa5DWYIIg= github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3/go.mod h1:+/3ZTqoYb3Ur7DObD00tarKMLMuKg8iqz5CHEanqTnw= -github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 h1:8OLZnVJPvjnrxEwHFg9hVUof/P4sibH+Ea4KKuqAGSg= -github.com/aws/aws-sdk-go-v2/service/sso v1.29.1/go.mod h1:27M3BpVi0C02UiQh1w9nsBEit6pLhlaH3NHna6WUbDE= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 h1:gKWSTnqudpo8dAxqBqZnDoDWCiEh/40FziUjr/mo6uA= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2/go.mod h1:x7+rkNmRoEN1U13A6JE2fXne9EWyJy54o3n6d4mGaXQ= -github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 h1:YZPjhyaGzhDQEvsffDEcpycq49nl7fiGcfJTIo8BszI= -github.com/aws/aws-sdk-go-v2/service/sts v1.38.2/go.mod h1:2dIN8qhQfv37BdUYGgEC8Q3tteM3zFxTI1MLO2O3J3c= +github.com/aws/aws-sdk-go-v2/service/sso v1.29.6 h1:A1oRkiSQOWstGh61y4Wc/yQ04sqrQZr1Si/oAXj20/s= +github.com/aws/aws-sdk-go-v2/service/sso v1.29.6/go.mod h1:5PfYspyCU5Vw1wNPsxi15LZovOnULudOQuVxphSflQA= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1 h1:5fm5RTONng73/QA73LhCNR7UT9RpFH3hR6HWL6bIgVY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1/go.mod h1:xBEjWD13h+6nq+z4AkqSfSvqRKFgDIQeaMguAJndOWo= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.6 h1:p3jIvqYwUZgu/XYeI48bJxOhvm47hZb5HUQ0tn6Q9kA= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.6/go.mod h1:WtKK+ppze5yKPkZ0XwqIVWD4beCwv056ZbPQNoeHqM8= github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE= github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -482,8 +482,8 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= @@ -505,8 +505,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -542,8 +542,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -574,15 +574,15 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= @@ -642,8 +642,8 @@ google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= -google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -654,8 +654,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/server/router/api/v1/attachment_service.go b/server/router/api/v1/attachment_service.go index b774680c8..2a99b8351 100644 --- a/server/router/api/v1/attachment_service.go +++ b/server/router/api/v1/attachment_service.go @@ -53,6 +53,9 @@ func (s *APIV1Service) CreateAttachment(ctx context.Context, request *v1pb.Creat if err != nil { return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) } + if user == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } // Validate required fields if request.Attachment == nil { @@ -124,6 +127,9 @@ func (s *APIV1Service) ListAttachments(ctx context.Context, request *v1pb.ListAt if err != nil { return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) } + if user == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } // Set default page size pageSize := int(request.PageSize) @@ -364,6 +370,9 @@ func (s *APIV1Service) DeleteAttachment(ctx context.Context, request *v1pb.Delet if err != nil { return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) } + if user == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } attachment, err := s.Store.GetAttachment(ctx, &store.FindAttachment{ UID: &attachmentUID, CreatorID: &user.ID, diff --git a/server/router/api/v1/memo_service.go b/server/router/api/v1/memo_service.go index 244059e60..aa111338f 100644 --- a/server/router/api/v1/memo_service.go +++ b/server/router/api/v1/memo_service.go @@ -29,6 +29,9 @@ func (s *APIV1Service) CreateMemo(ctx context.Context, request *v1pb.CreateMemoR if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user") } + if user == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } create := &store.Memo{ UID: shortuuid.New(), @@ -318,6 +321,9 @@ func (s *APIV1Service) UpdateMemo(ctx context.Context, request *v1pb.UpdateMemoR if err != nil { return nil, status.Errorf(codes.Internal, "failed to get current user") } + if user == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } // Only the creator or admin can update the memo. if memo.CreatorID != user.ID && !isSuperUser(user) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") @@ -453,6 +459,9 @@ func (s *APIV1Service) DeleteMemo(ctx context.Context, request *v1pb.DeleteMemoR if err != nil { return nil, status.Errorf(codes.Internal, "failed to get current user") } + if user == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } // Only the creator or admin can update the memo. if memo.CreatorID != user.ID && !isSuperUser(user) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") @@ -689,6 +698,9 @@ func (s *APIV1Service) RenameMemoTag(ctx context.Context, request *v1pb.RenameMe if err != nil { return nil, status.Errorf(codes.Internal, "failed to get current user") } + if user == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } memoFind := &store.FindMemo{ CreatorID: &user.ID, @@ -739,6 +751,9 @@ func (s *APIV1Service) DeleteMemoTag(ctx context.Context, request *v1pb.DeleteMe if err != nil { return nil, status.Errorf(codes.Internal, "failed to get current user") } + if user == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } memoFind := &store.FindMemo{ CreatorID: &user.ID, diff --git a/server/router/api/v1/reaction_service.go b/server/router/api/v1/reaction_service.go index 561d776ad..7dd007d8f 100644 --- a/server/router/api/v1/reaction_service.go +++ b/server/router/api/v1/reaction_service.go @@ -37,6 +37,9 @@ func (s *APIV1Service) UpsertMemoReaction(ctx context.Context, request *v1pb.Ups if err != nil { return nil, status.Errorf(codes.Internal, "failed to get current user") } + if user == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } reaction, err := s.Store.UpsertReaction(ctx, &store.Reaction{ CreatorID: user.ID, ContentID: request.Reaction.ContentId, diff --git a/server/router/api/v1/user_service.go b/server/router/api/v1/user_service.go index 207427611..b1f0a96ec 100644 --- a/server/router/api/v1/user_service.go +++ b/server/router/api/v1/user_service.go @@ -36,6 +36,9 @@ func (s *APIV1Service) ListUsers(ctx context.Context, request *v1pb.ListUsersReq if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) } + if currentUser == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } if currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } @@ -322,6 +325,9 @@ func (s *APIV1Service) GetUserSetting(ctx context.Context, request *v1pb.GetUser if err != nil { return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) } + if currentUser == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } // Only allow user to get their own settings if currentUser.ID != userID { @@ -356,6 +362,9 @@ func (s *APIV1Service) UpdateUserSetting(ctx context.Context, request *v1pb.Upda if err != nil { return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) } + if currentUser == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } // Only allow user to update their own settings if currentUser.ID != userID { @@ -442,6 +451,9 @@ func (s *APIV1Service) ListUserSettings(ctx context.Context, request *v1pb.ListU if err != nil { return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) } + if currentUser == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } // Only allow user to list their own settings if currentUser.ID != userID { @@ -500,7 +512,7 @@ func (s *APIV1Service) ListUserAccessTokens(ctx context.Context, request *v1pb.L return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) } if currentUser == nil { - return nil, status.Errorf(codes.PermissionDenied, "permission denied") + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") } if currentUser.ID != userID { return nil, status.Errorf(codes.PermissionDenied, "permission denied") @@ -562,7 +574,7 @@ func (s *APIV1Service) CreateUserAccessToken(ctx context.Context, request *v1pb. return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) } if currentUser == nil { - return nil, status.Errorf(codes.PermissionDenied, "permission denied") + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") } if currentUser.ID != userID { return nil, status.Errorf(codes.PermissionDenied, "permission denied") @@ -630,7 +642,7 @@ func (s *APIV1Service) DeleteUserAccessToken(ctx context.Context, request *v1pb. return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) } if currentUser == nil { - return nil, status.Errorf(codes.PermissionDenied, "permission denied") + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") } if currentUser.ID != userID { return nil, status.Errorf(codes.PermissionDenied, "permission denied") @@ -673,7 +685,7 @@ func (s *APIV1Service) ListUserSessions(ctx context.Context, request *v1pb.ListU return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) } if currentUser == nil { - return nil, status.Errorf(codes.PermissionDenied, "permission denied") + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") } if currentUser.ID != userID { return nil, status.Errorf(codes.PermissionDenied, "permission denied") @@ -736,7 +748,7 @@ func (s *APIV1Service) RevokeUserSession(ctx context.Context, request *v1pb.Revo return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) } if currentUser == nil { - return nil, status.Errorf(codes.PermissionDenied, "permission denied") + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") } if currentUser.ID != userID { return nil, status.Errorf(codes.PermissionDenied, "permission denied") @@ -796,6 +808,9 @@ func (s *APIV1Service) ListUserWebhooks(ctx context.Context, request *v1pb.ListU if err != nil { return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) } + if currentUser == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } if currentUser.ID != userID && currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } @@ -825,6 +840,9 @@ func (s *APIV1Service) CreateUserWebhook(ctx context.Context, request *v1pb.Crea if err != nil { return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) } + if currentUser == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } if currentUser.ID != userID && currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } @@ -862,6 +880,9 @@ func (s *APIV1Service) UpdateUserWebhook(ctx context.Context, request *v1pb.Upda if err != nil { return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) } + if currentUser == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } if currentUser.ID != userID && currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } @@ -931,6 +952,9 @@ func (s *APIV1Service) DeleteUserWebhook(ctx context.Context, request *v1pb.Dele if err != nil { return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) } + if currentUser == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } if currentUser.ID != userID && currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } diff --git a/server/router/api/v1/workspace_service.go b/server/router/api/v1/workspace_service.go index 2279245ca..0af794c62 100644 --- a/server/router/api/v1/workspace_service.go +++ b/server/router/api/v1/workspace_service.go @@ -83,6 +83,9 @@ func (s *APIV1Service) UpdateWorkspaceSetting(ctx context.Context, request *v1pb if err != nil { return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) } + if user == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } if user.Role != store.RoleHost { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } diff --git a/web/package.json b/web/package.json index e2612cdfd..4f0f18e66 100644 --- a/web/package.json +++ b/web/package.json @@ -39,11 +39,11 @@ "katex": "^0.16.22", "leaflet": "^1.9.4", "lodash-es": "^4.17.21", - "lucide-react": "^0.486.0", + "lucide-react": "^0.544.0", "mermaid": "^11.11.0", "mime": "^4.1.0", "mobx": "^6.13.7", - "mobx-react-lite": "^4.1.0", + "mobx-react-lite": "^4.1.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-force-graph-2d": "^1.29.0", @@ -82,9 +82,9 @@ "nice-grpc-web": "^3.3.8", "prettier": "^3.6.2", "terser": "^5.44.0", - "tw-animate-css": "^1.3.8", + "tw-animate-css": "^1.4.0", "typescript": "^5.9.3", - "typescript-eslint": "^8.44.0", + "typescript-eslint": "^8.45.0", "vite": "^7.1.5" }, "pnpm": { @@ -92,4 +92,4 @@ "esbuild" ] } -} \ No newline at end of file +} diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index e5e947706..714119a0d 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -102,8 +102,8 @@ importers: specifier: ^4.17.21 version: 4.17.21 lucide-react: - specifier: ^0.486.0 - version: 0.486.0(react@18.3.1) + specifier: ^0.544.0 + version: 0.544.0(react@18.3.1) mermaid: specifier: ^11.11.0 version: 11.11.0 @@ -114,8 +114,8 @@ importers: specifier: ^6.13.7 version: 6.13.7 mobx-react-lite: - specifier: ^4.1.0 - version: 4.1.0(mobx@6.13.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^4.1.1 + version: 4.1.1(mobx@6.13.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18.3.1 version: 18.3.1 @@ -226,14 +226,14 @@ importers: specifier: ^5.44.0 version: 5.44.0 tw-animate-css: - specifier: ^1.3.8 - version: 1.3.8 + specifier: ^1.4.0 + version: 1.4.0 typescript: specifier: ^5.9.3 version: 5.9.3 typescript-eslint: - specifier: ^8.44.0 - version: 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3) + specifier: ^8.45.0 + version: 8.45.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3) vite: specifier: ^7.1.5 version: 7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0) @@ -1521,63 +1521,63 @@ packages: '@types/uuid@10.0.0': resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} - '@typescript-eslint/eslint-plugin@8.44.0': - resolution: {integrity: sha512-EGDAOGX+uwwekcS0iyxVDmRV9HX6FLSM5kzrAToLTsr9OWCIKG/y3lQheCq18yZ5Xh78rRKJiEpP0ZaCs4ryOQ==} + '@typescript-eslint/eslint-plugin@8.45.0': + resolution: {integrity: sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.44.0 + '@typescript-eslint/parser': ^8.45.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.44.0': - resolution: {integrity: sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==} + '@typescript-eslint/parser@8.45.0': + resolution: {integrity: sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.44.0': - resolution: {integrity: sha512-ZeaGNraRsq10GuEohKTo4295Z/SuGcSq2LzfGlqiuEvfArzo/VRrT0ZaJsVPuKZ55lVbNk8U6FcL+ZMH8CoyVA==} + '@typescript-eslint/project-service@8.45.0': + resolution: {integrity: sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.44.0': - resolution: {integrity: sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA==} + '@typescript-eslint/scope-manager@8.45.0': + resolution: {integrity: sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.44.0': - resolution: {integrity: sha512-x5Y0+AuEPqAInc6yd0n5DAcvtoQ/vyaGwuX5HE9n6qAefk1GaedqrLQF8kQGylLUb9pnZyLf+iEiL9fr8APDtQ==} + '@typescript-eslint/tsconfig-utils@8.45.0': + resolution: {integrity: sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.44.0': - resolution: {integrity: sha512-9cwsoSxJ8Sak67Be/hD2RNt/fsqmWnNE1iHohG8lxqLSNY8xNfyY7wloo5zpW3Nu9hxVgURevqfcH6vvKCt6yg==} + '@typescript-eslint/type-utils@8.45.0': + resolution: {integrity: sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.44.0': - resolution: {integrity: sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA==} + '@typescript-eslint/types@8.45.0': + resolution: {integrity: sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.44.0': - resolution: {integrity: sha512-lqNj6SgnGcQZwL4/SBJ3xdPEfcBuhCG8zdcwCPgYcmiPLgokiNDKlbPzCwEwu7m279J/lBYWtDYL+87OEfn8Jw==} + '@typescript-eslint/typescript-estree@8.45.0': + resolution: {integrity: sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.44.0': - resolution: {integrity: sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg==} + '@typescript-eslint/utils@8.45.0': + resolution: {integrity: sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.44.0': - resolution: {integrity: sha512-zaz9u8EJ4GBmnehlrpoKvj/E3dNbuQ7q0ucyZImm3cLqJ8INTc970B1qEqDX/Rzq65r3TvVTN7kHWPBoyW7DWw==} + '@typescript-eslint/visitor-keys@8.45.0': + resolution: {integrity: sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@vitejs/plugin-react@4.7.0': @@ -2715,8 +2715,8 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lucide-react@0.486.0: - resolution: {integrity: sha512-xWop/wMsC1ikiEVLZrxXjPKw4vU/eAip33G2mZHgbWnr4Nr5Rt4Vx4s/q1D3B/rQVbxjOuqASkEZcUxDEKzecw==} + lucide-react@0.544.0: + resolution: {integrity: sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -2774,8 +2774,8 @@ packages: mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} - mobx-react-lite@4.1.0: - resolution: {integrity: sha512-QEP10dpHHBeQNv1pks3WnHRCem2Zp636lq54M2nKO2Sarr13pL4u6diQXf65yzXUn0mkk18SyIDCm9UOJYTi1w==} + mobx-react-lite@4.1.1: + resolution: {integrity: sha512-iUxiMpsvNraCKXU+yPotsOncNNmyeS2B5DKL+TL6Tar/xm+wwNJAubJmtRSeAoYawdZqwv8Z/+5nPRHeQxTiXg==} peerDependencies: mobx: ^6.9.0 react: ^16.8.0 || ^17 || ^18 || ^19 @@ -3339,8 +3339,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tw-animate-css@1.3.8: - resolution: {integrity: sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw==} + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} @@ -3362,8 +3362,8 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript-eslint@8.44.0: - resolution: {integrity: sha512-ib7mCkYuIzYonCq9XWF5XNw+fkj2zg629PSa9KNIQ47RXFF763S5BIX4wqz1+FLPogTZoiw8KmCiRPRa8bL3qw==} + typescript-eslint@8.45.0: + resolution: {integrity: sha512-qzDmZw/Z5beNLUrXfd0HIW6MzIaAV5WNDxmMs9/3ojGOpYavofgNAAD/nC6tGV2PczIi0iw8vot2eAe/sBn7zg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -4808,14 +4808,14 @@ snapshots: '@types/uuid@10.0.0': {} - '@typescript-eslint/eslint-plugin@8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3))(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3))(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.44.0 - '@typescript-eslint/type-utils': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.44.0 + '@typescript-eslint/parser': 8.45.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/type-utils': 8.45.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.45.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.45.0 eslint: 9.35.0(jiti@2.5.1) graphemer: 1.4.0 ignore: 7.0.5 @@ -4825,41 +4825,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.45.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.44.0 - '@typescript-eslint/types': 8.44.0 - '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.44.0 + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.45.0 debug: 4.4.3 eslint: 9.35.0(jiti@2.5.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.44.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.45.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.44.0(typescript@5.9.3) - '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/tsconfig-utils': 8.45.0(typescript@5.9.3) + '@typescript-eslint/types': 8.45.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.44.0': + '@typescript-eslint/scope-manager@8.45.0': dependencies: - '@typescript-eslint/types': 8.44.0 - '@typescript-eslint/visitor-keys': 8.44.0 + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/visitor-keys': 8.45.0 - '@typescript-eslint/tsconfig-utils@8.44.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.45.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.45.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.44.0 - '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.45.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3) debug: 4.4.3 eslint: 9.35.0(jiti@2.5.1) ts-api-utils: 2.1.0(typescript@5.9.3) @@ -4867,14 +4867,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.44.0': {} + '@typescript-eslint/types@8.45.0': {} - '@typescript-eslint/typescript-estree@8.44.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.45.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.44.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.44.0(typescript@5.9.3) - '@typescript-eslint/types': 8.44.0 - '@typescript-eslint/visitor-keys': 8.44.0 + '@typescript-eslint/project-service': 8.45.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.45.0(typescript@5.9.3) + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/visitor-keys': 8.45.0 debug: 4.4.3 fast-glob: 3.3.3 is-glob: 4.0.3 @@ -4885,20 +4885,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.45.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.5.1)) - '@typescript-eslint/scope-manager': 8.44.0 - '@typescript-eslint/types': 8.44.0 - '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3) eslint: 9.35.0(jiti@2.5.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.44.0': + '@typescript-eslint/visitor-keys@8.45.0': dependencies: - '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/types': 8.45.0 eslint-visitor-keys: 4.2.1 '@vitejs/plugin-react@4.7.0(vite@7.1.5(@types/node@24.5.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0))': @@ -6214,7 +6214,7 @@ snapshots: dependencies: yallist: 3.1.1 - lucide-react@0.486.0(react@18.3.1): + lucide-react@0.544.0(react@18.3.1): dependencies: react: 18.3.1 @@ -6285,7 +6285,7 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.1 - mobx-react-lite@4.1.0(mobx@6.13.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + mobx-react-lite@4.1.1(mobx@6.13.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: mobx: 6.13.7 react: 18.3.1 @@ -6918,7 +6918,7 @@ snapshots: tslib@2.8.1: {} - tw-animate-css@1.3.8: {} + tw-animate-css@1.4.0: {} type-check@0.4.0: dependencies: @@ -6957,12 +6957,12 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3): + typescript-eslint@8.45.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3))(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3))(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.45.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.45.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.3) eslint: 9.35.0(jiti@2.5.1) typescript: 5.9.3 transitivePeerDependencies: diff --git a/web/src/components/ConfirmDialog/README.md b/web/src/components/ConfirmDialog/README.md new file mode 100644 index 000000000..98d41f8df --- /dev/null +++ b/web/src/components/ConfirmDialog/README.md @@ -0,0 +1,131 @@ +# ConfirmDialog - Accessible Confirmation Dialog + +## Overview + +`ConfirmDialog` standardizes confirmation flows across the app. It replaces ad‑hoc `window.confirm` usage with an accessible, themeable dialog that supports asynchronous operations. + +## Key Features + +### 1. Accessibility & UX +- Uses shared `Dialog` primitives (focus trap, ARIA roles) +- Blocks dismissal while async confirm is pending +- Clear separation of title (action) vs description (context) + +### 2. Async-Aware +- Accepts sync or async `onConfirm` +- Auto-closes on resolve; remains open on error for retry / toast + +### 3. Internationalization Ready +- All labels / text provided by caller through i18n hook +- Supports interpolation for dynamic context + +### 4. Minimal Surface, Easy Extension +- Lightweight API (few required props) +- Style hook via `.container` class (SCSS module) + +## Architecture + +``` +ConfirmDialog +├── State: loading (tracks pending confirm action) +├── Dialog primitives: Header (title + description), Footer (buttons) +└── External control: parent owns open state via onOpenChange +``` + +## Usage + +```tsx +import { useTranslate } from "@/utils/i18n"; +import ConfirmDialog from "@/components/ConfirmDialog"; + +const t = useTranslate(); + +; +``` + +## Props + +| Prop | Type | Required | Acceptable Values | +|------|------|----------|------------------| +| `open` | `boolean` | Yes | `true` (visible) / `false` (hidden) | +| `onOpenChange` | `(open: boolean) => void` | Yes | Callback receiving next state; should update parent state | +| `title` | `React.ReactNode` | Yes | Short localized action summary (text / node) | +| `description` | `React.ReactNode` | No | Optional contextual message | +| `confirmLabel` | `string` | Yes | Non-empty localized action text (1–2 words) | +| `cancelLabel` | `string` | Yes | Localized cancel label | +| `onConfirm` | `() => void | Promise` | Yes | Sync or async handler; resolve = close, reject = stay open | +| `confirmVariant` | `"default" | "destructive"` | No | Defaults to `"default"`; use `"destructive"` for irreversible actions | + +## Benefits vs Previous Implementation + +### Before (window.confirm / ad‑hoc dialogs) +- Blocking native prompt, inconsistent styling +- No async progress handling +- No rich formatting +- Hard to localize consistently + +### After (ConfirmDialog) +- Unified styling + accessibility semantics +- Async-safe with loading state shielding +- Plain description flexibility +- i18n-first via externalized labels + +## Technical Implementation Details + +### Async Handling +```tsx +const handleConfirm = async () => { + setLoading(true); + try { + await onConfirm(); // resolve -> close + onOpenChange(false); + } catch (e) { + console.error(e); // remain open for retry + } finally { + setLoading(false); + } +}; +``` + +### Close Guard +```tsx + !loading && onOpenChange(next)} /> +``` + +## Browser / Environment Support +- Works anywhere the existing `Dialog` primitives work (modern browsers) +- No ResizeObserver / layout dependencies + +## Performance Considerations +1. Minimal renders: loading state toggles once per confirm attempt +2. No portal churn—relies on underlying dialog infra + +## Future Enhancements +1. Severity icon / header accent +2. Auto-focus destructive button toggle +3. Secondary action (e.g. "Archive" vs "Delete") +4. Built-in retry / error slot +5. Optional checkbox confirmation ("I understand the consequences") +6. Motion/animation tokens integration + +## Styling +The `ConfirmDialog.module.scss` file provides a `.container` hook. It currently only hosts a harmless custom property so the stylesheet is non-empty. Add real layout or variant tokens there instead of inline styles. + +## Internationalization +All visible strings must come from the translation system. Use `useTranslate()` and pass localized values into props. Separate keys for title/description. + +## Error Handling +Errors thrown in `onConfirm` are caught and logged. The dialog stays open so the caller can surface a toast or inline message and allow retry. (Consider routing serious errors to a higher-level handler.) + +--- + +If you extend this component, update this README to keep usage discoverable. diff --git a/web/src/components/ConfirmDialog/index.tsx b/web/src/components/ConfirmDialog/index.tsx new file mode 100644 index 000000000..495d68ceb --- /dev/null +++ b/web/src/components/ConfirmDialog/index.tsx @@ -0,0 +1,73 @@ +import * as React from "react"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; + +export interface ConfirmDialogProps { + /** Whether the dialog is open */ + open: boolean; + /** Open state change callback (closing disabled while loading) */ + onOpenChange: (open: boolean) => void; + /** Title content (plain text or React nodes) */ + title: React.ReactNode; + /** Optional description (plain text or React nodes) */ + description?: React.ReactNode; + /** Confirm / primary action button label */ + confirmLabel: string; + /** Cancel button label */ + cancelLabel: string; + /** Async or sync confirm handler. Dialog auto-closes on resolve, stays open on reject */ + onConfirm: () => void | Promise; + /** Variant style of confirm button */ + confirmVariant?: "default" | "destructive"; +} + +/** + * Accessible confirmation dialog. + * - Renders optional description content + * - Prevents closing while async confirm action is in-flight + * - Minimal opinionated styling; leverages existing UI primitives + */ +export default function ConfirmDialog({ + open, + onOpenChange, + title, + description, + confirmLabel, + cancelLabel, + onConfirm, + confirmVariant = "default", +}: ConfirmDialogProps) { + const [loading, setLoading] = React.useState(false); + + const handleConfirm = async () => { + try { + setLoading(true); + await onConfirm(); + onOpenChange(false); + } catch (e) { + // Intentionally swallow errors so user can retry; surface via caller's toast/logging + console.error("ConfirmDialog error for action:", title, e); + } finally { + setLoading(false); + } + }; + + return ( + !loading && onOpenChange(o)}> + + + {title} + {description ? {description} : null} + + + + + + + + ); +} diff --git a/web/src/components/CreateAccessTokenDialog.tsx b/web/src/components/CreateAccessTokenDialog.tsx index 2e8d22dcb..8af13e509 100644 --- a/web/src/components/CreateAccessTokenDialog.tsx +++ b/web/src/components/CreateAccessTokenDialog.tsx @@ -8,12 +8,13 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { userServiceClient } from "@/grpcweb"; import useCurrentUser from "@/hooks/useCurrentUser"; import useLoading from "@/hooks/useLoading"; +import { UserAccessToken } from "@/types/proto/api/v1/user_service"; import { useTranslate } from "@/utils/i18n"; interface Props { open: boolean; onOpenChange: (open: boolean) => void; - onSuccess: () => void; + onSuccess: (created: UserAccessToken) => void; } interface State { @@ -72,7 +73,7 @@ function CreateAccessTokenDialog({ open, onOpenChange, onSuccess }: Props) { try { requestState.setLoading(); - await userServiceClient.createUserAccessToken({ + const created = await userServiceClient.createUserAccessToken({ parent: currentUser.name, accessToken: { description: state.description, @@ -81,7 +82,7 @@ function CreateAccessTokenDialog({ open, onOpenChange, onSuccess }: Props) { }); requestState.setFinish(); - onSuccess(); + onSuccess(created); onOpenChange(false); } catch (error: any) { toast.error(error.details); diff --git a/web/src/components/HomeSidebar/ShortcutsSection.tsx b/web/src/components/HomeSidebar/ShortcutsSection.tsx index f36c3a7f4..1b3d18bd3 100644 --- a/web/src/components/HomeSidebar/ShortcutsSection.tsx +++ b/web/src/components/HomeSidebar/ShortcutsSection.tsx @@ -1,6 +1,8 @@ import { Edit3Icon, MoreVerticalIcon, TrashIcon, PlusIcon } from "lucide-react"; import { observer } from "mobx-react-lite"; import { useState } from "react"; +import toast from "react-hot-toast"; +import ConfirmDialog from "@/components/ConfirmDialog"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { shortcutServiceClient } from "@/grpcweb"; import useAsyncEffect from "@/hooks/useAsyncEffect"; @@ -25,6 +27,7 @@ const ShortcutsSection = observer(() => { const t = useTranslate(); const shortcuts = userStore.state.shortcuts; const [isCreateShortcutDialogOpen, setIsCreateShortcutDialogOpen] = useState(false); + const [deleteTarget, setDeleteTarget] = useState(); const [editingShortcut, setEditingShortcut] = useState(); useAsyncEffect(async () => { @@ -32,11 +35,15 @@ const ShortcutsSection = observer(() => { }, []); const handleDeleteShortcut = async (shortcut: Shortcut) => { - const confirmed = window.confirm("Are you sure you want to delete this shortcut?"); - if (confirmed) { - await shortcutServiceClient.deleteShortcut({ name: shortcut.name }); - await userStore.fetchUserSettings(); - } + setDeleteTarget(shortcut); + }; + + const confirmDeleteShortcut = async () => { + if (!deleteTarget) return; + await shortcutServiceClient.deleteShortcut({ name: deleteTarget.name }); + await userStore.fetchUserSettings(); + toast.success(t("setting.shortcut.delete-success", { title: deleteTarget.title })); + setDeleteTarget(undefined); }; const handleCreateShortcut = () => { @@ -113,6 +120,15 @@ const ShortcutsSection = observer(() => { shortcut={editingShortcut} onSuccess={handleShortcutDialogSuccess} /> + !open && setDeleteTarget(undefined)} + title={t("setting.shortcut.delete-confirm", { title: deleteTarget?.title ?? "" })} + confirmLabel={t("common.delete")} + cancelLabel={t("common.cancel")} + onConfirm={confirmDeleteShortcut} + confirmVariant="destructive" + /> ); }); diff --git a/web/src/components/HomeSidebar/TagsSection.tsx b/web/src/components/HomeSidebar/TagsSection.tsx index 2eaeaf3d4..bed64fe2a 100644 --- a/web/src/components/HomeSidebar/TagsSection.tsx +++ b/web/src/components/HomeSidebar/TagsSection.tsx @@ -3,6 +3,7 @@ import { observer } from "mobx-react-lite"; import { useState } from "react"; import toast from "react-hot-toast"; import useLocalStorage from "react-use/lib/useLocalStorage"; +import ConfirmDialog from "@/components/ConfirmDialog"; import { Switch } from "@/components/ui/switch"; import { memoServiceClient } from "@/grpcweb"; import { useDialog } from "@/hooks/useDialog"; @@ -25,6 +26,7 @@ const TagsSection = observer((props: Props) => { const [treeAutoExpand, setTreeAutoExpand] = useLocalStorage("tag-tree-auto-expand", false); const renameTagDialog = useDialog(); const [selectedTag, setSelectedTag] = useState(""); + const [deleteTagName, setDeleteTagName] = useState(undefined); const tags = Object.entries(userStore.state.tagCount) .sort((a, b) => a[0].localeCompare(b[0])) .sort((a, b) => b[1] - a[1]); @@ -52,14 +54,17 @@ const TagsSection = observer((props: Props) => { }; const handleDeleteTag = async (tag: string) => { - const confirmed = window.confirm(t("tag.delete-confirm")); - if (confirmed) { - await memoServiceClient.deleteMemoTag({ - parent: "memos/-", - tag: tag, - }); - toast.success(t("message.deleted-successfully")); - } + setDeleteTagName(tag); + }; + + const confirmDeleteTag = async () => { + if (!deleteTagName) return; + await memoServiceClient.deleteMemoTag({ + parent: "memos/-", + tag: deleteTagName, + }); + toast.success(t("tag.delete-success")); + setDeleteTagName(undefined); }; return ( @@ -139,6 +144,15 @@ const TagsSection = observer((props: Props) => { tag={selectedTag} onSuccess={handleRenameSuccess} /> + !open && setDeleteTagName(undefined)} + title={t("tag.delete-confirm")} + confirmLabel={t("common.delete")} + cancelLabel={t("common.cancel")} + onConfirm={confirmDeleteTag} + confirmVariant="destructive" + /> ); }); diff --git a/web/src/components/MemoActionMenu.tsx b/web/src/components/MemoActionMenu.tsx index 051a9a8bc..a07401699 100644 --- a/web/src/components/MemoActionMenu.tsx +++ b/web/src/components/MemoActionMenu.tsx @@ -11,8 +11,10 @@ import { SquareCheckIcon, } from "lucide-react"; import { observer } from "mobx-react-lite"; +import { useState } from "react"; import toast from "react-hot-toast"; import { useLocation } from "react-router-dom"; +import ConfirmDialog from "@/components/ConfirmDialog"; import { markdownServiceClient } from "@/grpcweb"; import useNavigateTo from "@/hooks/useNavigateTo"; import { memoStore, userStore } from "@/store"; @@ -49,6 +51,8 @@ const MemoActionMenu = observer((props: Props) => { const t = useTranslate(); const location = useLocation(); const navigateTo = useNavigateTo(); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [removeTasksDialogOpen, setRemoveTasksDialogOpen] = useState(false); const hasCompletedTaskList = checkHasCompletedTaskList(memo); const isInMemoDetailPage = location.pathname.startsWith(`/${memo.name}`); const isComment = Boolean(memo.parent); @@ -101,7 +105,7 @@ const MemoActionMenu = observer((props: Props) => { }, ["state"], ); - toast(message); + toast.success(message); } catch (error: any) { toast.error(error.details); console.error(error); @@ -123,48 +127,50 @@ const MemoActionMenu = observer((props: Props) => { toast.success(t("message.succeed-copy-link")); }; - const handleDeleteMemoClick = async () => { - const confirmed = window.confirm(t("memo.delete-confirm")); - if (confirmed) { - await memoStore.deleteMemo(memo.name); - toast.success(t("message.deleted-successfully")); - if (isInMemoDetailPage) { - navigateTo("/"); - } - memoUpdatedCallback(); + const handleDeleteMemoClick = () => { + setDeleteDialogOpen(true); + }; + + const confirmDeleteMemo = async () => { + await memoStore.deleteMemo(memo.name); + toast.success(t("message.deleted-successfully")); + if (isInMemoDetailPage) { + navigateTo("/"); } + memoUpdatedCallback(); + }; + + const handleRemoveCompletedTaskListItemsClick = () => { + setRemoveTasksDialogOpen(true); }; - const handleRemoveCompletedTaskListItemsClick = async () => { - const confirmed = window.confirm(t("memo.remove-completed-task-list-items-confirm")); - if (confirmed) { - const newNodes = JSON.parse(JSON.stringify(memo.nodes)); - for (const node of newNodes) { - if (node.type === NodeType.LIST && node.listNode?.children?.length > 0) { - const children = node.listNode.children; - for (let i = 0; i < children.length; i++) { - if (children[i].type === NodeType.TASK_LIST_ITEM && children[i].taskListItemNode?.complete) { - // Remove completed taskList item and next line breaks + const confirmRemoveCompletedTaskListItems = async () => { + const newNodes = JSON.parse(JSON.stringify(memo.nodes)); + for (const node of newNodes) { + if (node.type === NodeType.LIST && node.listNode?.children?.length > 0) { + const children = node.listNode.children; + for (let i = 0; i < children.length; i++) { + if (children[i].type === NodeType.TASK_LIST_ITEM && children[i].taskListItemNode?.complete) { + // Remove completed taskList item and next line breaks + children.splice(i, 1); + if (children[i]?.type === NodeType.LINE_BREAK) { children.splice(i, 1); - if (children[i]?.type === NodeType.LINE_BREAK) { - children.splice(i, 1); - } - i--; } + i--; } } } - const { markdown } = await markdownServiceClient.restoreMarkdownNodes({ nodes: newNodes }); - await memoStore.updateMemo( - { - name: memo.name, - content: markdown, - }, - ["content"], - ); - toast.success(t("message.remove-completed-task-list-items-successfully")); - memoUpdatedCallback(); } + const { markdown } = await markdownServiceClient.restoreMarkdownNodes({ nodes: newNodes }); + await memoStore.updateMemo( + { + name: memo.name, + content: markdown, + }, + ["content"], + ); + toast.success(t("message.remove-completed-task-list-items-successfully")); + memoUpdatedCallback(); }; return ( @@ -216,6 +222,27 @@ const MemoActionMenu = observer((props: Props) => { )} + {/* Delete confirmation dialog */} + + {/* Remove completed tasks confirmation */} + ); }); diff --git a/web/src/components/Settings/AccessTokenSection.tsx b/web/src/components/Settings/AccessTokenSection.tsx index 60c9f0e16..e4cf3f6da 100644 --- a/web/src/components/Settings/AccessTokenSection.tsx +++ b/web/src/components/Settings/AccessTokenSection.tsx @@ -2,6 +2,7 @@ import copy from "copy-to-clipboard"; import { ClipboardIcon, TrashIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; +import ConfirmDialog from "@/components/ConfirmDialog"; import { Button } from "@/components/ui/button"; import { userServiceClient } from "@/grpcweb"; import useCurrentUser from "@/hooks/useCurrentUser"; @@ -20,6 +21,7 @@ const AccessTokenSection = () => { const currentUser = useCurrentUser(); const [userAccessTokens, setUserAccessTokens] = useState([]); const createTokenDialog = useDialog(); + const [deleteTarget, setDeleteTarget] = useState(undefined); useEffect(() => { listAccessTokens(currentUser.name).then((accessTokens) => { @@ -27,9 +29,10 @@ const AccessTokenSection = () => { }); }, []); - const handleCreateAccessTokenDialogConfirm = async () => { + const handleCreateAccessTokenDialogConfirm = async (created: UserAccessToken) => { const accessTokens = await listAccessTokens(currentUser.name); setUserAccessTokens(accessTokens); + toast.success(t("setting.access-token-section.create-dialog.access-token-created", { description: created.description })); }; const handleCreateToken = () => { @@ -42,12 +45,17 @@ const AccessTokenSection = () => { }; const handleDeleteAccessToken = async (userAccessToken: UserAccessToken) => { - const formatedAccessToken = getFormatedAccessToken(userAccessToken.accessToken); - const confirmed = window.confirm(t("setting.access-token-section.access-token-deletion", { accessToken: formatedAccessToken })); - if (confirmed) { - await userServiceClient.deleteUserAccessToken({ name: userAccessToken.name }); - setUserAccessTokens(userAccessTokens.filter((token) => token.accessToken !== userAccessToken.accessToken)); - } + setDeleteTarget(userAccessToken); + }; + + const confirmDeleteAccessToken = async () => { + if (!deleteTarget) return; + const { name: tokenName, description } = deleteTarget; + await userServiceClient.deleteUserAccessToken({ name: tokenName }); + // Filter by stable resource name to avoid ambiguity with duplicate token strings + setUserAccessTokens((prev) => prev.filter((token) => token.name !== tokenName)); + setDeleteTarget(undefined); + toast.success(t("setting.access-token-section.access-token-deleted", { description })); }; const getFormatedAccessToken = (accessToken: string) => { @@ -134,6 +142,16 @@ const AccessTokenSection = () => { onOpenChange={createTokenDialog.setOpen} onSuccess={handleCreateAccessTokenDialogConfirm} /> + !open && setDeleteTarget(undefined)} + title={deleteTarget ? t("setting.access-token-section.access-token-deletion", { description: deleteTarget.description }) : ""} + description={t("setting.access-token-section.access-token-deletion-description")} + confirmLabel={t("common.delete")} + cancelLabel={t("common.cancel")} + onConfirm={confirmDeleteAccessToken} + confirmVariant="destructive" + /> ); }; diff --git a/web/src/components/Settings/MemberSection.tsx b/web/src/components/Settings/MemberSection.tsx index 7734063bf..f6ee2137e 100644 --- a/web/src/components/Settings/MemberSection.tsx +++ b/web/src/components/Settings/MemberSection.tsx @@ -2,6 +2,8 @@ import { sortBy } from "lodash-es"; import { MoreVerticalIcon, PlusIcon } from "lucide-react"; import { observer } from "mobx-react-lite"; import React, { useEffect, useState } from "react"; +import toast from "react-hot-toast"; +import ConfirmDialog from "@/components/ConfirmDialog"; import { Button } from "@/components/ui/button"; import { userServiceClient } from "@/grpcweb"; import useCurrentUser from "@/hooks/useCurrentUser"; @@ -21,6 +23,8 @@ const MemberSection = observer(() => { const editDialog = useDialog(); const [editingUser, setEditingUser] = useState(); const sortedUsers = sortBy(users, "id"); + const [archiveTarget, setArchiveTarget] = useState(undefined); + const [deleteTarget, setDeleteTarget] = useState(undefined); useEffect(() => { fetchUsers(); @@ -52,20 +56,26 @@ const MemberSection = observer(() => { }; const handleArchiveUserClick = async (user: User) => { - const confirmed = window.confirm(t("setting.member-section.archive-warning", { username: user.displayName })); - if (confirmed) { - await userServiceClient.updateUser({ - user: { - name: user.name, - state: State.ARCHIVED, - }, - updateMask: ["state"], - }); - fetchUsers(); - } + setArchiveTarget(user); + }; + + const confirmArchiveUser = async () => { + if (!archiveTarget) return; + const username = archiveTarget.username; + await userServiceClient.updateUser({ + user: { + name: archiveTarget.name, + state: State.ARCHIVED, + }, + updateMask: ["state"], + }); + setArchiveTarget(undefined); + toast.success(t("setting.member-section.archive-success", { username })); + await fetchUsers(); }; const handleRestoreUserClick = async (user: User) => { + const { username } = user; await userServiceClient.updateUser({ user: { name: user.name, @@ -73,15 +83,21 @@ const MemberSection = observer(() => { }, updateMask: ["state"], }); - fetchUsers(); + toast.success(t("setting.member-section.restore-success", { username })); + await fetchUsers(); }; const handleDeleteUserClick = async (user: User) => { - const confirmed = window.confirm(t("setting.member-section.delete-warning", { username: user.displayName })); - if (confirmed) { - await userStore.deleteUser(user.name); - fetchUsers(); - } + setDeleteTarget(user); + }; + + const confirmDeleteUser = async () => { + if (!deleteTarget) return; + const { username, name } = deleteTarget; + await userStore.deleteUser(name); + setDeleteTarget(undefined); + toast.success(t("setting.member-section.delete-success", { username })); + await fetchUsers(); }; return ( @@ -169,6 +185,28 @@ const MemberSection = observer(() => { {/* Edit User Dialog */} + + !open && setArchiveTarget(undefined)} + title={archiveTarget ? t("setting.member-section.archive-warning", { username: archiveTarget.username }) : ""} + description={archiveTarget ? t("setting.member-section.archive-warning-description") : ""} + confirmLabel={t("common.confirm")} + cancelLabel={t("common.cancel")} + onConfirm={confirmArchiveUser} + confirmVariant="default" + /> + + !open && setDeleteTarget(undefined)} + title={deleteTarget ? t("setting.member-section.delete-warning", { username: deleteTarget.username }) : ""} + description={deleteTarget ? t("setting.member-section.delete-warning-description") : ""} + confirmLabel={t("common.delete")} + cancelLabel={t("common.cancel")} + onConfirm={confirmDeleteUser} + confirmVariant="destructive" + /> ); }); diff --git a/web/src/components/Settings/SSOSection.tsx b/web/src/components/Settings/SSOSection.tsx index 0dfd7b94e..8cc31690c 100644 --- a/web/src/components/Settings/SSOSection.tsx +++ b/web/src/components/Settings/SSOSection.tsx @@ -1,6 +1,7 @@ import { MoreVerticalIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; +import ConfirmDialog from "@/components/ConfirmDialog"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { Separator } from "@/components/ui/separator"; @@ -15,6 +16,7 @@ const SSOSection = () => { const [identityProviderList, setIdentityProviderList] = useState([]); const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [editingIdentityProvider, setEditingIdentityProvider] = useState(); + const [deleteTarget, setDeleteTarget] = useState(undefined); useEffect(() => { fetchIdentityProviderList(); @@ -26,16 +28,19 @@ const SSOSection = () => { }; const handleDeleteIdentityProvider = async (identityProvider: IdentityProvider) => { - const confirmed = window.confirm(t("setting.sso-section.confirm-delete", { name: identityProvider.title })); - if (confirmed) { - try { - await identityProviderServiceClient.deleteIdentityProvider({ name: identityProvider.name }); - } catch (error: any) { - console.error(error); - toast.error(error.details); - } - await fetchIdentityProviderList(); + setDeleteTarget(identityProvider); + }; + + const confirmDeleteIdentityProvider = async () => { + if (!deleteTarget) return; + try { + await identityProviderServiceClient.deleteIdentityProvider({ name: deleteTarget.name }); + } catch (error: any) { + console.error(error); + toast.error(error.details); } + await fetchIdentityProviderList(); + setDeleteTarget(undefined); }; const handleCreateIdentityProvider = () => { @@ -112,6 +117,16 @@ const SSOSection = () => { identityProvider={editingIdentityProvider} onSuccess={handleDialogSuccess} /> + + !open && setDeleteTarget(undefined)} + title={deleteTarget ? t("setting.sso-section.confirm-delete", { name: deleteTarget.title }) : ""} + confirmLabel={t("common.delete")} + cancelLabel={t("common.cancel")} + onConfirm={confirmDeleteIdentityProvider} + confirmVariant="destructive" + /> ); }; diff --git a/web/src/components/Settings/UserSessionsSection.tsx b/web/src/components/Settings/UserSessionsSection.tsx index 8e5d00e59..292e09140 100644 --- a/web/src/components/Settings/UserSessionsSection.tsx +++ b/web/src/components/Settings/UserSessionsSection.tsx @@ -1,6 +1,7 @@ import { ClockIcon, MonitorIcon, SmartphoneIcon, TabletIcon, TrashIcon, WifiIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; +import ConfirmDialog from "@/components/ConfirmDialog"; import { Button } from "@/components/ui/button"; import { userServiceClient } from "@/grpcweb"; import useCurrentUser from "@/hooks/useCurrentUser"; @@ -16,6 +17,7 @@ const UserSessionsSection = () => { const t = useTranslate(); const currentUser = useCurrentUser(); const [userSessions, setUserSessions] = useState([]); + const [revokeTarget, setRevokeTarget] = useState(undefined); useEffect(() => { listUserSessions(currentUser.name).then((sessions) => { @@ -24,13 +26,15 @@ const UserSessionsSection = () => { }, []); const handleRevokeSession = async (userSession: UserSession) => { - const formattedSessionId = getFormattedSessionId(userSession.sessionId); - const confirmed = window.confirm(t("setting.user-sessions-section.session-revocation", { sessionId: formattedSessionId })); - if (confirmed) { - await userServiceClient.revokeUserSession({ name: userSession.name }); - setUserSessions(userSessions.filter((session) => session.sessionId !== userSession.sessionId)); - toast.success(t("setting.user-sessions-section.session-revoked")); - } + setRevokeTarget(userSession); + }; + + const confirmRevokeSession = async () => { + if (!revokeTarget) return; + await userServiceClient.revokeUserSession({ name: revokeTarget.name }); + setUserSessions(userSessions.filter((session) => session.sessionId !== revokeTarget.sessionId)); + toast.success(t("setting.user-sessions-section.session-revoked")); + setRevokeTarget(undefined); }; const getFormattedSessionId = (sessionId: string) => { @@ -148,6 +152,22 @@ const UserSessionsSection = () => { + !open && setRevokeTarget(undefined)} + title={ + revokeTarget + ? t("setting.user-sessions-section.session-revocation", { + sessionId: getFormattedSessionId(revokeTarget.sessionId), + }) + : "" + } + description={revokeTarget ? t("setting.user-sessions-section.session-revocation-description") : ""} + confirmLabel={t("setting.user-sessions-section.revoke-session-button")} + cancelLabel={t("common.cancel")} + onConfirm={confirmRevokeSession} + confirmVariant="destructive" + /> ); diff --git a/web/src/components/Settings/WebhookSection.tsx b/web/src/components/Settings/WebhookSection.tsx index 78e22a148..817db5c99 100644 --- a/web/src/components/Settings/WebhookSection.tsx +++ b/web/src/components/Settings/WebhookSection.tsx @@ -1,6 +1,8 @@ import { ExternalLinkIcon, TrashIcon } from "lucide-react"; import { useEffect, useState } from "react"; +import toast from "react-hot-toast"; import { Link } from "react-router-dom"; +import ConfirmDialog from "@/components/ConfirmDialog"; import { Button } from "@/components/ui/button"; import { userServiceClient } from "@/grpcweb"; import useCurrentUser from "@/hooks/useCurrentUser"; @@ -13,6 +15,7 @@ const WebhookSection = () => { const currentUser = useCurrentUser(); const [webhooks, setWebhooks] = useState([]); const [isCreateWebhookDialogOpen, setIsCreateWebhookDialogOpen] = useState(false); + const [deleteTarget, setDeleteTarget] = useState(undefined); const listWebhooks = async () => { if (!currentUser) return []; @@ -30,16 +33,22 @@ const WebhookSection = () => { const handleCreateWebhookDialogConfirm = async () => { const webhooks = await listWebhooks(); + const name = webhooks[webhooks.length - 1]?.displayName || ""; setWebhooks(webhooks); setIsCreateWebhookDialogOpen(false); + toast.success(t("setting.webhook-section.create-dialog.create-webhook-success", { name })); }; const handleDeleteWebhook = async (webhook: UserWebhook) => { - const confirmed = window.confirm(`Are you sure to delete webhook \`${webhook.displayName}\`? You cannot undo this action.`); - if (confirmed) { - await userServiceClient.deleteUserWebhook({ name: webhook.name }); - setWebhooks(webhooks.filter((item) => item.name !== webhook.name)); - } + setDeleteTarget(webhook); + }; + + const confirmDeleteWebhook = async () => { + if (!deleteTarget) return; + await userServiceClient.deleteUserWebhook({ name: deleteTarget.name }); + setWebhooks(webhooks.filter((item) => item.name !== deleteTarget.name)); + setDeleteTarget(undefined); + toast.success(t("setting.webhook-section.delete-dialog.delete-webhook-success", { name: deleteTarget.displayName })); }; return ( @@ -79,12 +88,7 @@ const WebhookSection = () => { {webhook.url} - @@ -118,6 +122,16 @@ const WebhookSection = () => { onOpenChange={setIsCreateWebhookDialogOpen} onSuccess={handleCreateWebhookDialogConfirm} /> + !open && setDeleteTarget(undefined)} + title={t("setting.webhook-section.delete-dialog.delete-webhook-title", { name: deleteTarget?.displayName || "" })} + description={t("setting.webhook-section.delete-dialog.delete-webhook-description")} + confirmLabel={t("common.delete")} + cancelLabel={t("common.cancel")} + onConfirm={confirmDeleteWebhook} + confirmVariant="destructive" + /> ); }; diff --git a/web/src/locales/en.json b/web/src/locales/en.json index d3833b9a6..7f001d02e 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -143,7 +143,8 @@ }, "copy-link": "Copy Link", "count-memos-in-date": "{{count}} {{memos}} in {{date}}", - "delete-confirm": "Are you sure you want to delete this memo? THIS ACTION IS IRREVERSIBLE", + "delete-confirm": "Are you sure you want to delete this memo?", + "delete-confirm-description": "This action is irreversible. Attachments, links, and references will also be removed.", "direction": "Direction", "direction-asc": "Ascending", "direction-desc": "Descending", @@ -174,7 +175,7 @@ "archived-successfully": "Archived successfully", "change-memo-created-time": "Change memo created time", "copied": "Copied", - "deleted-successfully": "Deleted successfully", + "deleted-successfully": "Memo deleted successfully", "description-is-required": "Description is required", "failed-to-embed-memo": "Failed to embed memo", "fill-all": "Please fill in all fields.", @@ -219,6 +220,8 @@ }, "delete-resource": "Delete Resource", "delete-selected-resources": "Delete Selected Resources", + "delete-all-unused": "Delete all unused", + "delete-all-unused-confirm": "Are you sure you want to delete all unused resources? THIS ACTION IS IRREVERSIBLE", "fetching-data": "Fetching data…", "file-drag-drop-prompt": "Drag and drop your file here to upload file", "linked-amount": "Linked amount", @@ -226,7 +229,7 @@ "no-resources": "No resources.", "no-unused-resources": "No unused resources", "reset-link": "Reset Link", - "reset-link-prompt": "Are you sure to reset the link? This will break all current link usages. THIS ACTION IS IRREVERSIBLE", + "reset-link-prompt": "Are you sure you want to reset the link? This will break all current link usages. THIS ACTION IS IRREVERSIBLE", "reset-resource-link": "Reset Resource Link", "unused-resources": "Unused resources" }, @@ -237,8 +240,11 @@ "setting": { "access-token-section": { "access-token-copied-to-clipboard": "Access token copied to clipboard", - "access-token-deletion": "Are you sure to delete access token {{accessToken}}? THIS ACTION IS IRREVERSIBLE.", + "access-token-deletion": "Are you sure you want to delete access token `{{description}}`?", + "access-token-deletion-description": "This action is irreversible. You will need to update any services using this token to use a new token.", + "access-token-deleted": "Access token `{{description}}` deleted", "create-dialog": { + "access-token-created": "Access token `{{description}}` created", "create-access-token": "Create Access Token", "created-at": "Created At", "description": "Description", @@ -262,9 +268,11 @@ "expires": "Expires", "current": "Current", "never": "Never", - "session-revocation": "Are you sure to revoke session {{sessionId}}? You will need to sign in again on that device.", + "session-revocation": "Are you sure you want to revoke session `{{sessionId}}`?", + "session-revocation-description": "You will need to sign in again on that device.", "session-revoked": "Session revoked successfully", "revoke-session": "Revoke session", + "revoke-session-button": "Revoke", "cannot-revoke-current": "Cannot revoke current session", "no-sessions": "No active sessions found" }, @@ -286,10 +294,15 @@ "member-section": { "admin": "Admin", "archive-member": "Archive member", - "archive-warning": "Are you sure to archive {{username}}?", + "archive-warning": "Are you sure you want to archive {{username}}?", + "archive-warning-description": "Archiving disables the account. You can restore or delete it later.", + "archive-success": "{{username}} archived successfully", + "restore-success": "{{username}} restored successfully", "create-a-member": "Create a member", "delete-member": "Delete Member", - "delete-warning": "Are you sure to delete {{username}}? THIS ACTION IS IRREVERSIBLE", + "delete-warning": "Are you sure you want to delete {{username}}?", + "delete-warning-description": "THIS ACTION IS IRREVERSIBLE", + "delete-success": "{{username}} deleted successfully", "user": "User" }, "memo-related": "Memo", @@ -309,12 +322,16 @@ "default-memo-visibility": "Default memo visibility", "theme": "Theme" }, + "shortcut": { + "delete-confirm": "Are you sure you want to delete shortcut `{{title}}`?", + "delete-success": "Shortcut `{{title}}` deleted successfully" + }, "sso": "SSO", "sso-section": { "authorization-endpoint": "Authorization endpoint", "client-id": "Client ID", "client-secret": "Client secret", - "confirm-delete": "Are you sure to delete \"{{name}}\" SSO configuration? THIS ACTION IS IRREVERSIBLE", + "confirm-delete": "Are you sure you want to delete `{{name}}` SSO configuration? THIS ACTION IS IRREVERSIBLE", "create-sso": "Create SSO", "custom": "Custom", "delete-sso": "Confirm delete", @@ -367,7 +384,7 @@ "url-prefix-placeholder": "Custom URL prefix, optional", "url-suffix": "URL suffix", "url-suffix-placeholder": "Custom URL suffix, optional", - "warning-text": "Are you sure to delete storage service \"{{name}}\"? THIS ACTION IS IRREVERSIBLE" + "warning-text": "Are you sure you want to delete storage service `{{name}}`? THIS ACTION IS IRREVERSIBLE" }, "system": "System", "system-section": { @@ -384,7 +401,7 @@ }, "disable-markdown-shortcuts-in-editor": "Disable Markdown shortcuts in editor", "disable-password-login": "Disable password login", - "disable-password-login-final-warning": "Please type \"CONFIRM\" if you know what you are doing.", + "disable-password-login-final-warning": "Please type `CONFIRM` if you know what you are doing.", "disable-password-login-warning": "This will disable password login for all users. It is not possible to log in without reverting this setting in the database if your configured identity providers fail. You’ll also have to be extra careful when removing an identity provider", "disable-public-memos": "Disable public memos", "display-with-updated-time": "Display with updated time", @@ -403,11 +420,17 @@ "create-dialog": { "an-easy-to-remember-name": "An easy-to-remember name", "create-webhook": "Create webhook", + "create-webhook-success": "Webhook `{{name}}` created", "edit-webhook": "Edit webhook", "payload-url": "Payload URL", "title": "Title", "url-example-post-receive": "https://example.com/postreceive" }, + "delete-dialog": { + "delete-webhook-description": "This action is irreversible.", + "delete-webhook-title": "Are you sure you want to delete webhook `{{name}}`?", + "delete-webhook-success": "Webhook `{{name}}` deleted successfully" + }, "no-webhooks-found": "No webhooks found.", "title": "Webhooks", "url": "URL" @@ -427,8 +450,9 @@ "all-tags": "All Tags", "create-tag": "Create Tag", "create-tags-guide": "You can create tags by inputting `#tag`.", - "delete-confirm": "Are you sure to delete this tag? All related memos will be archived.", + "delete-confirm": "Are you sure you want to delete this tag? All related memos will be archived.", "delete-tag": "Delete Tag", + "delete-success": "Tag deleted successfully", "new-name": "New Name", "no-tag-found": "No tag found", "old-name": "Old Name", diff --git a/web/src/main.tsx b/web/src/main.tsx index 8884e8495..038d4ee6c 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -8,8 +8,12 @@ import "./index.css"; import router from "./router"; import { initialUserStore } from "./store/user"; import { initialWorkspaceStore } from "./store/workspace"; +import { applyThemeEarly } from "./utils/theme"; import "leaflet/dist/leaflet.css"; +// Apply theme early to prevent flash of wrong theme +applyThemeEarly(); + const Main = observer(() => ( <> diff --git a/web/src/pages/Attachments.tsx b/web/src/pages/Attachments.tsx index f8bafcdd5..7e6d33e40 100644 --- a/web/src/pages/Attachments.tsx +++ b/web/src/pages/Attachments.tsx @@ -1,15 +1,13 @@ import dayjs from "dayjs"; import { includes } from "lodash-es"; -import { PaperclipIcon, SearchIcon, TrashIcon } from "lucide-react"; +import { PaperclipIcon, SearchIcon } from "lucide-react"; import { observer } from "mobx-react-lite"; import { useEffect, useState } from "react"; import AttachmentIcon from "@/components/AttachmentIcon"; import Empty from "@/components/Empty"; import MobileHeader from "@/components/MobileHeader"; -import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { attachmentServiceClient } from "@/grpcweb"; import useLoading from "@/hooks/useLoading"; import useResponsiveWidth from "@/hooks/useResponsiveWidth"; @@ -56,16 +54,6 @@ const Attachments = observer(() => { }); }, []); - const handleDeleteUnusedAttachments = async () => { - const confirmed = window.confirm("Are you sure to delete all unused attachments? This action cannot be undone."); - if (confirmed) { - for (const attachment of unusedAttachments) { - await attachmentServiceClient.deleteAttachment({ name: attachment.name }); - } - setAttachments(attachments.filter((attachment) => attachment.memo)); - } - }; - return (
{!md && } @@ -138,18 +126,6 @@ const Attachments = observer(() => {
{t("resource.unused-resources")} ({unusedAttachments.length}) - - - - - - -

Delete all

-
-
-
{unusedAttachments.map((attachment) => { return ( diff --git a/web/src/utils/theme.ts b/web/src/utils/theme.ts index 75570b0a6..83a8ffac7 100644 --- a/web/src/utils/theme.ts +++ b/web/src/utils/theme.ts @@ -16,6 +16,43 @@ const validateTheme = (theme: string): ValidTheme => { return VALID_THEMES.includes(theme as ValidTheme) ? (theme as ValidTheme) : "default"; }; +/** + * Detects system theme preference + */ +export const getSystemTheme = (): "default" | "default-dark" => { + if (typeof window !== "undefined" && window.matchMedia) { + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "default-dark" : "default"; + } + return "default"; +}; + +/** + * Gets the theme that should be applied on initial load + * Priority: stored user preference -> system preference -> default + */ +export const getInitialTheme = (): ValidTheme => { + // Try to get stored theme from localStorage (where user settings might be cached) + try { + const storedTheme = localStorage.getItem("memos-theme"); + if (storedTheme && VALID_THEMES.includes(storedTheme as ValidTheme)) { + return storedTheme as ValidTheme; + } + } catch { + // localStorage might not be available + } + + // Fall back to system preference + return getSystemTheme(); +}; + +/** + * Applies the theme early to prevent flash of wrong theme + */ +export const applyThemeEarly = (): void => { + const theme = getInitialTheme(); + loadTheme(theme); +}; + export const loadTheme = (themeName: string): void => { const validTheme = validateTheme(themeName); @@ -35,4 +72,11 @@ export const loadTheme = (themeName: string): void => { // Set data attribute document.documentElement.setAttribute("data-theme", validTheme); + + // Store theme preference for future loads + try { + localStorage.setItem("memos-theme", validTheme); + } catch { + // localStorage might not be available + } };