mirror of https://github.com/usememos/memos.git
Merge branch 'main' into main
Signed-off-by: Tiny-Paws <69763072+Tiny-Paws@users.noreply.github.com>
This commit is contained in:
commit
ace4e26ad4
16
README.md
16
README.md
|
|
@ -1,6 +1,17 @@
|
|||
<!-- Premium Sponsors -->
|
||||
<p align="center"><strong>Special thanks to our sponsor:</strong></p>
|
||||
<div align="center">
|
||||
<a href="https://go.warp.dev/memos" target="_blank" rel="noopener">
|
||||
<img src="https://raw.githubusercontent.com/warpdotdev/brand-assets/main/Github/Sponsor/Warp-Github-LG-02.png" alt="Warp" height="256" />
|
||||
</a>
|
||||
<p>
|
||||
<a href="https://go.warp.dev/memos" target="_blank" rel="noopener">Warp is built for coding with multiple AI agents</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
# Memos
|
||||
|
||||
<img align="right" height="96px" src="https://www.usememos.com/logo-rounded.png" alt="Memos" />
|
||||
<img align="right" height="96px" src="https://raw.githubusercontent.com/usememos/.github/refs/heads/main/assets/logo-rounded.png" alt="Memos" />
|
||||
|
||||
A modern, open-source, self-hosted knowledge management and note-taking platform designed for privacy-conscious users and organizations. Memos provides a lightweight yet powerful solution for capturing, organizing, and sharing thoughts with comprehensive Markdown support and cross-platform accessibility.
|
||||
|
||||
|
|
@ -17,7 +28,7 @@ A modern, open-source, self-hosted knowledge management and note-taking platform
|
|||
|
||||
</div>
|
||||
|
||||

|
||||

|
||||
|
||||
<!-- Premium Sponsors -->
|
||||
<!--
|
||||
|
|
@ -99,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.
|
||||
|
||||
<a href="https://github.com/warpdev" target="_blank"><img src="https://avatars.githubusercontent.com/u/71840468?s=200&v=4" alt="warp" height="60" /></a>
|
||||
<a href="https://github.com/yourselfhosted" target="_blank"><img src="https://avatars.githubusercontent.com/u/140182318?v=4" alt="yourselfhosted" height="60" /></a>
|
||||
<a href="https://github.com/fixermark" target="_blank"><img src="https://avatars.githubusercontent.com/u/169982?v=4" alt="fixermark" height="60" /></a>
|
||||
<a href="https://github.com/alik-agaev" target="_blank"><img src="https://avatars.githubusercontent.com/u/2662697?v=4" alt="alik-agaev" height="60" /></a>
|
||||
|
|
|
|||
34
go.mod
34
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
|
||||
|
|
@ -23,13 +23,13 @@ require (
|
|||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/viper v1.20.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/usememos/gomark v0.0.0-20250917125604-82623ecaf218
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/mod v0.27.0
|
||||
github.com/usememos/gomark v0.0.0-20250925160223-606d7debad77
|
||||
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
|
||||
)
|
||||
|
|
|
|||
72
go.sum
72
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=
|
||||
|
|
@ -433,8 +433,8 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM
|
|||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/usememos/gomark v0.0.0-20250917125604-82623ecaf218 h1:rkH9CKI0AzWOIkwzZOs0PYepWt9fJTZZ9c5W1lL/bMQ=
|
||||
github.com/usememos/gomark v0.0.0-20250917125604-82623ecaf218/go.mod h1:7CZRoYFQyyljzplOTeyODFR26O+wr0BbnpTWVLGfKJA=
|
||||
github.com/usememos/gomark v0.0.0-20250925160223-606d7debad77 h1:eDJ/NUqDIvQbyXwqiKaUC3Y4lvbauSME+KLmpaxRytk=
|
||||
github.com/usememos/gomark v0.0.0-20250925160223-606d7debad77/go.mod h1:7CZRoYFQyyljzplOTeyODFR26O+wr0BbnpTWVLGfKJA=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -306,11 +306,9 @@ func (c *Cron) runScheduler() {
|
|||
|
||||
// startJob runs the given job in a new goroutine.
|
||||
func (c *Cron) startJob(j Job) {
|
||||
c.jobWaiter.Add(1)
|
||||
go func() {
|
||||
defer c.jobWaiter.Done()
|
||||
c.jobWaiter.Go(func() {
|
||||
j.Run()
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// now returns current time in c location.
|
||||
|
|
|
|||
|
|
@ -326,4 +326,6 @@ message SpoilerNode {
|
|||
message HTMLElementNode {
|
||||
string tag_name = 1;
|
||||
map<string, string> attributes = 2;
|
||||
repeated Node children = 3;
|
||||
bool is_self_closing = 4;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.8
|
||||
// protoc-gen-go v1.36.9
|
||||
// protoc (unknown)
|
||||
// source: api/v1/activity_service.proto
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.8
|
||||
// protoc-gen-go v1.36.9
|
||||
// protoc (unknown)
|
||||
// source: api/v1/attachment_service.proto
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.8
|
||||
// protoc-gen-go v1.36.9
|
||||
// protoc (unknown)
|
||||
// source: api/v1/auth_service.proto
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.8
|
||||
// protoc-gen-go v1.36.9
|
||||
// protoc (unknown)
|
||||
// source: api/v1/common.proto
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.8
|
||||
// protoc-gen-go v1.36.9
|
||||
// protoc (unknown)
|
||||
// source: api/v1/idp_service.proto
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.8
|
||||
// protoc-gen-go v1.36.9
|
||||
// protoc (unknown)
|
||||
// source: api/v1/inbox_service.proto
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.8
|
||||
// protoc-gen-go v1.36.9
|
||||
// protoc (unknown)
|
||||
// source: api/v1/markdown_service.proto
|
||||
|
||||
|
|
@ -2634,6 +2634,8 @@ type HTMLElementNode struct {
|
|||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
TagName string `protobuf:"bytes,1,opt,name=tag_name,json=tagName,proto3" json:"tag_name,omitempty"`
|
||||
Attributes map[string]string `protobuf:"bytes,2,rep,name=attributes,proto3" json:"attributes,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
Children []*Node `protobuf:"bytes,3,rep,name=children,proto3" json:"children,omitempty"`
|
||||
IsSelfClosing bool `protobuf:"varint,4,opt,name=is_self_closing,json=isSelfClosing,proto3" json:"is_self_closing,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
|
@ -2682,6 +2684,20 @@ func (x *HTMLElementNode) GetAttributes() map[string]string {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (x *HTMLElementNode) GetChildren() []*Node {
|
||||
if x != nil {
|
||||
return x.Children
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *HTMLElementNode) GetIsSelfClosing() bool {
|
||||
if x != nil {
|
||||
return x.IsSelfClosing
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type TableNode_Row struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Cells []*Node `protobuf:"bytes,1,rep,name=cells,proto3" json:"cells,omitempty"`
|
||||
|
|
@ -2874,12 +2890,14 @@ const file_api_v1_markdown_service_proto_rawDesc = "" +
|
|||
"\rresource_name\x18\x01 \x01(\tR\fresourceName\x12\x16\n" +
|
||||
"\x06params\x18\x02 \x01(\tR\x06params\"'\n" +
|
||||
"\vSpoilerNode\x12\x18\n" +
|
||||
"\acontent\x18\x01 \x01(\tR\acontent\"\xba\x01\n" +
|
||||
"\acontent\x18\x01 \x01(\tR\acontent\"\x92\x02\n" +
|
||||
"\x0fHTMLElementNode\x12\x19\n" +
|
||||
"\btag_name\x18\x01 \x01(\tR\atagName\x12M\n" +
|
||||
"\n" +
|
||||
"attributes\x18\x02 \x03(\v2-.memos.api.v1.HTMLElementNode.AttributesEntryR\n" +
|
||||
"attributes\x1a=\n" +
|
||||
"attributes\x12.\n" +
|
||||
"\bchildren\x18\x03 \x03(\v2\x12.memos.api.v1.NodeR\bchildren\x12&\n" +
|
||||
"\x0fis_self_closing\x18\x04 \x01(\bR\risSelfClosing\x1a=\n" +
|
||||
"\x0fAttributesEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01*\x83\x04\n" +
|
||||
|
|
@ -3039,20 +3057,21 @@ var file_api_v1_markdown_service_proto_depIdxs = []int32{
|
|||
10, // 46: memos.api.v1.ItalicNode.children:type_name -> memos.api.v1.Node
|
||||
10, // 47: memos.api.v1.LinkNode.content:type_name -> memos.api.v1.Node
|
||||
43, // 48: memos.api.v1.HTMLElementNode.attributes:type_name -> memos.api.v1.HTMLElementNode.AttributesEntry
|
||||
10, // 49: memos.api.v1.TableNode.Row.cells:type_name -> memos.api.v1.Node
|
||||
2, // 50: memos.api.v1.MarkdownService.ParseMarkdown:input_type -> memos.api.v1.ParseMarkdownRequest
|
||||
4, // 51: memos.api.v1.MarkdownService.RestoreMarkdownNodes:input_type -> memos.api.v1.RestoreMarkdownNodesRequest
|
||||
6, // 52: memos.api.v1.MarkdownService.StringifyMarkdownNodes:input_type -> memos.api.v1.StringifyMarkdownNodesRequest
|
||||
8, // 53: memos.api.v1.MarkdownService.GetLinkMetadata:input_type -> memos.api.v1.GetLinkMetadataRequest
|
||||
3, // 54: memos.api.v1.MarkdownService.ParseMarkdown:output_type -> memos.api.v1.ParseMarkdownResponse
|
||||
5, // 55: memos.api.v1.MarkdownService.RestoreMarkdownNodes:output_type -> memos.api.v1.RestoreMarkdownNodesResponse
|
||||
7, // 56: memos.api.v1.MarkdownService.StringifyMarkdownNodes:output_type -> memos.api.v1.StringifyMarkdownNodesResponse
|
||||
9, // 57: memos.api.v1.MarkdownService.GetLinkMetadata:output_type -> memos.api.v1.LinkMetadata
|
||||
54, // [54:58] is the sub-list for method output_type
|
||||
50, // [50:54] is the sub-list for method input_type
|
||||
50, // [50:50] is the sub-list for extension type_name
|
||||
50, // [50:50] is the sub-list for extension extendee
|
||||
0, // [0:50] is the sub-list for field type_name
|
||||
10, // 49: memos.api.v1.HTMLElementNode.children:type_name -> memos.api.v1.Node
|
||||
10, // 50: memos.api.v1.TableNode.Row.cells:type_name -> memos.api.v1.Node
|
||||
2, // 51: memos.api.v1.MarkdownService.ParseMarkdown:input_type -> memos.api.v1.ParseMarkdownRequest
|
||||
4, // 52: memos.api.v1.MarkdownService.RestoreMarkdownNodes:input_type -> memos.api.v1.RestoreMarkdownNodesRequest
|
||||
6, // 53: memos.api.v1.MarkdownService.StringifyMarkdownNodes:input_type -> memos.api.v1.StringifyMarkdownNodesRequest
|
||||
8, // 54: memos.api.v1.MarkdownService.GetLinkMetadata:input_type -> memos.api.v1.GetLinkMetadataRequest
|
||||
3, // 55: memos.api.v1.MarkdownService.ParseMarkdown:output_type -> memos.api.v1.ParseMarkdownResponse
|
||||
5, // 56: memos.api.v1.MarkdownService.RestoreMarkdownNodes:output_type -> memos.api.v1.RestoreMarkdownNodesResponse
|
||||
7, // 57: memos.api.v1.MarkdownService.StringifyMarkdownNodes:output_type -> memos.api.v1.StringifyMarkdownNodesResponse
|
||||
9, // 58: memos.api.v1.MarkdownService.GetLinkMetadata:output_type -> memos.api.v1.LinkMetadata
|
||||
55, // [55:59] is the sub-list for method output_type
|
||||
51, // [51:55] is the sub-list for method input_type
|
||||
51, // [51:51] is the sub-list for extension type_name
|
||||
51, // [51:51] is the sub-list for extension extendee
|
||||
0, // [0:51] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_api_v1_markdown_service_proto_init() }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.8
|
||||
// protoc-gen-go v1.36.9
|
||||
// protoc (unknown)
|
||||
// source: api/v1/memo_service.proto
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.8
|
||||
// protoc-gen-go v1.36.9
|
||||
// protoc (unknown)
|
||||
// source: api/v1/shortcut_service.proto
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.8
|
||||
// protoc-gen-go v1.36.9
|
||||
// protoc (unknown)
|
||||
// source: api/v1/user_service.proto
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.8
|
||||
// protoc-gen-go v1.36.9
|
||||
// protoc (unknown)
|
||||
// source: api/v1/workspace_service.proto
|
||||
|
||||
|
|
|
|||
|
|
@ -15,13 +15,19 @@ paths:
|
|||
parameters:
|
||||
- name: pageSize
|
||||
in: query
|
||||
description: "The maximum number of activities to return.\r\n The service may return fewer than this value.\r\n If unspecified, at most 100 activities will be returned.\r\n The maximum value is 1000; values above 1000 will be coerced to 1000."
|
||||
description: |-
|
||||
The maximum number of activities to return.
|
||||
The service may return fewer than this value.
|
||||
If unspecified, at most 100 activities will be returned.
|
||||
The maximum value is 1000; values above 1000 will be coerced to 1000.
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
- name: pageToken
|
||||
in: query
|
||||
description: "A page token, received from a previous `ListActivities` call.\r\n Provide this to retrieve the subsequent page."
|
||||
description: |-
|
||||
A page token, received from a previous `ListActivities` call.
|
||||
Provide this to retrieve the subsequent page.
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
|
|
@ -72,23 +78,35 @@ paths:
|
|||
parameters:
|
||||
- name: pageSize
|
||||
in: query
|
||||
description: "Optional. The maximum number of attachments to return.\r\n The service may return fewer than this value.\r\n If unspecified, at most 50 attachments will be returned.\r\n The maximum value is 1000; values above 1000 will be coerced to 1000."
|
||||
description: |-
|
||||
Optional. The maximum number of attachments to return.
|
||||
The service may return fewer than this value.
|
||||
If unspecified, at most 50 attachments will be returned.
|
||||
The maximum value is 1000; values above 1000 will be coerced to 1000.
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
- name: pageToken
|
||||
in: query
|
||||
description: "Optional. A page token, received from a previous `ListAttachments` call.\r\n Provide this to retrieve the subsequent page."
|
||||
description: |-
|
||||
Optional. A page token, received from a previous `ListAttachments` call.
|
||||
Provide this to retrieve the subsequent page.
|
||||
schema:
|
||||
type: string
|
||||
- name: filter
|
||||
in: query
|
||||
description: "Optional. Filter to apply to the list results.\r\n Example: \"type=image/png\" or \"filename:*.jpg\"\r\n Supported operators: =, !=, <, <=, >, >=, :\r\n Supported fields: filename, type, size, create_time, memo"
|
||||
description: |-
|
||||
Optional. Filter to apply to the list results.
|
||||
Example: "type=image/png" or "filename:*.jpg"
|
||||
Supported operators: =, !=, <, <=, >, >=, :
|
||||
Supported fields: filename, type, size, create_time, memo
|
||||
schema:
|
||||
type: string
|
||||
- name: orderBy
|
||||
in: query
|
||||
description: "Optional. The order to sort results by.\r\n Example: \"create_time desc\" or \"filename asc\""
|
||||
description: |-
|
||||
Optional. The order to sort results by.
|
||||
Example: "create_time desc" or "filename asc"
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
|
|
@ -112,7 +130,9 @@ paths:
|
|||
parameters:
|
||||
- name: attachmentId
|
||||
in: query
|
||||
description: "Optional. The attachment ID to use for this attachment.\r\n If empty, a unique ID will be generated."
|
||||
description: |-
|
||||
Optional. The attachment ID to use for this attachment.
|
||||
If empty, a unique ID will be generated.
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
|
|
@ -223,7 +243,9 @@ paths:
|
|||
post:
|
||||
tags:
|
||||
- AuthService
|
||||
description: "CreateSession authenticates a user and creates a new session.\r\n Returns the authenticated user information upon successful authentication."
|
||||
description: |-
|
||||
CreateSession authenticates a user and creates a new session.
|
||||
Returns the authenticated user information upon successful authentication.
|
||||
operationId: AuthService_CreateSession
|
||||
requestBody:
|
||||
content:
|
||||
|
|
@ -248,7 +270,9 @@ paths:
|
|||
get:
|
||||
tags:
|
||||
- AuthService
|
||||
description: "GetCurrentSession returns the current active session information.\r\n This method is idempotent and safe, suitable for checking current session state."
|
||||
description: |-
|
||||
GetCurrentSession returns the current active session information.
|
||||
This method is idempotent and safe, suitable for checking current session state.
|
||||
operationId: AuthService_GetCurrentSession
|
||||
responses:
|
||||
"200":
|
||||
|
|
@ -266,7 +290,9 @@ paths:
|
|||
delete:
|
||||
tags:
|
||||
- AuthService
|
||||
description: "DeleteSession terminates the current user session.\r\n This is an idempotent operation that invalidates the user's authentication."
|
||||
description: |-
|
||||
DeleteSession terminates the current user session.
|
||||
This is an idempotent operation that invalidates the user's authentication.
|
||||
operationId: AuthService_DeleteSession
|
||||
responses:
|
||||
"200":
|
||||
|
|
@ -305,7 +331,9 @@ paths:
|
|||
parameters:
|
||||
- name: identityProviderId
|
||||
in: query
|
||||
description: "Optional. The ID to use for the identity provider, which will become the final component of the resource name.\r\n If not provided, the system will generate one."
|
||||
description: |-
|
||||
Optional. The ID to use for the identity provider, which will become the final component of the resource name.
|
||||
If not provided, the system will generate one.
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
|
|
@ -389,7 +417,9 @@ paths:
|
|||
type: string
|
||||
- name: updateMask
|
||||
in: query
|
||||
description: "Required. The update mask applies to the resource. Only the top level fields of\r\n IdentityProvider are supported."
|
||||
description: |-
|
||||
Required. The update mask applies to the resource. Only the top level fields of
|
||||
IdentityProvider are supported.
|
||||
schema:
|
||||
type: string
|
||||
format: field-mask
|
||||
|
|
@ -481,7 +511,9 @@ paths:
|
|||
get:
|
||||
tags:
|
||||
- MarkdownService
|
||||
description: "GetLinkMetadata returns metadata for a given link.\r\n This is useful for generating link previews."
|
||||
description: |-
|
||||
GetLinkMetadata returns metadata for a given link.
|
||||
This is useful for generating link previews.
|
||||
operationId: MarkdownService_GetLinkMetadata
|
||||
parameters:
|
||||
- name: link
|
||||
|
|
@ -506,7 +538,9 @@ paths:
|
|||
post:
|
||||
tags:
|
||||
- MarkdownService
|
||||
description: "ParseMarkdown parses the given markdown content and returns a list of nodes.\r\n This is a utility method that transforms markdown text into structured nodes."
|
||||
description: |-
|
||||
ParseMarkdown parses the given markdown content and returns a list of nodes.
|
||||
This is a utility method that transforms markdown text into structured nodes.
|
||||
operationId: MarkdownService_ParseMarkdown
|
||||
requestBody:
|
||||
content:
|
||||
|
|
@ -531,7 +565,9 @@ paths:
|
|||
post:
|
||||
tags:
|
||||
- MarkdownService
|
||||
description: "RestoreMarkdownNodes restores the given nodes to markdown content.\r\n This is the inverse operation of ParseMarkdown."
|
||||
description: |-
|
||||
RestoreMarkdownNodes restores the given nodes to markdown content.
|
||||
This is the inverse operation of ParseMarkdown.
|
||||
operationId: MarkdownService_RestoreMarkdownNodes
|
||||
requestBody:
|
||||
content:
|
||||
|
|
@ -556,7 +592,9 @@ paths:
|
|||
post:
|
||||
tags:
|
||||
- MarkdownService
|
||||
description: "StringifyMarkdownNodes stringify the given nodes to plain text content.\r\n This removes all markdown formatting and returns plain text."
|
||||
description: |-
|
||||
StringifyMarkdownNodes stringify the given nodes to plain text content.
|
||||
This removes all markdown formatting and returns plain text.
|
||||
operationId: MarkdownService_StringifyMarkdownNodes
|
||||
requestBody:
|
||||
content:
|
||||
|
|
@ -586,18 +624,26 @@ paths:
|
|||
parameters:
|
||||
- name: pageSize
|
||||
in: query
|
||||
description: "Optional. The maximum number of memos to return.\r\n The service may return fewer than this value.\r\n If unspecified, at most 50 memos will be returned.\r\n The maximum value is 1000; values above 1000 will be coerced to 1000."
|
||||
description: |-
|
||||
Optional. The maximum number of memos to return.
|
||||
The service may return fewer than this value.
|
||||
If unspecified, at most 50 memos will be returned.
|
||||
The maximum value is 1000; values above 1000 will be coerced to 1000.
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
- name: pageToken
|
||||
in: query
|
||||
description: "Optional. A page token, received from a previous `ListMemos` call.\r\n Provide this to retrieve the subsequent page."
|
||||
description: |-
|
||||
Optional. A page token, received from a previous `ListMemos` call.
|
||||
Provide this to retrieve the subsequent page.
|
||||
schema:
|
||||
type: string
|
||||
- name: state
|
||||
in: query
|
||||
description: "Optional. The state of the memos to list.\r\n Default to `NORMAL`. Set to `ARCHIVED` to list archived memos."
|
||||
description: |-
|
||||
Optional. The state of the memos to list.
|
||||
Default to `NORMAL`. Set to `ARCHIVED` to list archived memos.
|
||||
schema:
|
||||
enum:
|
||||
- STATE_UNSPECIFIED
|
||||
|
|
@ -607,12 +653,18 @@ paths:
|
|||
format: enum
|
||||
- name: orderBy
|
||||
in: query
|
||||
description: "Optional. The order to sort results by.\r\n Default to \"display_time desc\".\r\n Example: \"display_time desc\" or \"create_time asc\""
|
||||
description: |-
|
||||
Optional. The order to sort results by.
|
||||
Default to "display_time desc".
|
||||
Example: "display_time desc" or "create_time asc"
|
||||
schema:
|
||||
type: string
|
||||
- name: filter
|
||||
in: query
|
||||
description: "Optional. Filter to apply to the list results.\r\n Filter is a CEL expression to filter memos.\r\n Refer to `Shortcut.filter`."
|
||||
description: |-
|
||||
Optional. Filter to apply to the list results.
|
||||
Filter is a CEL expression to filter memos.
|
||||
Refer to `Shortcut.filter`.
|
||||
schema:
|
||||
type: string
|
||||
- name: showDeleted
|
||||
|
|
@ -641,7 +693,9 @@ paths:
|
|||
parameters:
|
||||
- name: memoId
|
||||
in: query
|
||||
description: "Optional. The memo ID to use for this memo.\r\n If empty, a unique ID will be generated."
|
||||
description: |-
|
||||
Optional. The memo ID to use for this memo.
|
||||
If empty, a unique ID will be generated.
|
||||
schema:
|
||||
type: string
|
||||
- name: validateOnly
|
||||
|
|
@ -688,7 +742,9 @@ paths:
|
|||
type: string
|
||||
- name: readMask
|
||||
in: query
|
||||
description: "Optional. The fields to return in the response.\r\n If not specified, all fields are returned."
|
||||
description: |-
|
||||
Optional. The fields to return in the response.
|
||||
If not specified, all fields are returned.
|
||||
schema:
|
||||
type: string
|
||||
format: field-mask
|
||||
|
|
@ -1140,18 +1196,28 @@ paths:
|
|||
parameters:
|
||||
- name: pageSize
|
||||
in: query
|
||||
description: "Optional. The maximum number of users to return.\r\n The service may return fewer than this value.\r\n If unspecified, at most 50 users will be returned.\r\n The maximum value is 1000; values above 1000 will be coerced to 1000."
|
||||
description: |-
|
||||
Optional. The maximum number of users to return.
|
||||
The service may return fewer than this value.
|
||||
If unspecified, at most 50 users will be returned.
|
||||
The maximum value is 1000; values above 1000 will be coerced to 1000.
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
- name: pageToken
|
||||
in: query
|
||||
description: "Optional. A page token, received from a previous `ListUsers` call.\r\n Provide this to retrieve the subsequent page."
|
||||
description: |-
|
||||
Optional. A page token, received from a previous `ListUsers` call.
|
||||
Provide this to retrieve the subsequent page.
|
||||
schema:
|
||||
type: string
|
||||
- name: filter
|
||||
in: query
|
||||
description: "Optional. Filter to apply to the list results.\r\n Example: \"state=ACTIVE\" or \"role=USER\" or \"email:@example.com\"\r\n Supported operators: =, !=, <, <=, >, >=, :\r\n Supported fields: username, email, role, state, create_time, update_time"
|
||||
description: |-
|
||||
Optional. Filter to apply to the list results.
|
||||
Example: "state=ACTIVE" or "role=USER" or "email:@example.com"
|
||||
Supported operators: =, !=, <, <=, >, >=, :
|
||||
Supported fields: username, email, role, state, create_time, update_time
|
||||
schema:
|
||||
type: string
|
||||
- name: showDeleted
|
||||
|
|
@ -1180,7 +1246,10 @@ paths:
|
|||
parameters:
|
||||
- name: userId
|
||||
in: query
|
||||
description: "Optional. The user ID to use for this user.\r\n If empty, a unique ID will be generated.\r\n Must match the pattern [a-z0-9-]+"
|
||||
description: |-
|
||||
Optional. The user ID to use for this user.
|
||||
If empty, a unique ID will be generated.
|
||||
Must match the pattern [a-z0-9-]+
|
||||
schema:
|
||||
type: string
|
||||
- name: validateOnly
|
||||
|
|
@ -1190,7 +1259,9 @@ paths:
|
|||
type: boolean
|
||||
- name: requestId
|
||||
in: query
|
||||
description: "Optional. An idempotency token that can be used to ensure that multiple\r\n requests to create a user have the same result."
|
||||
description: |-
|
||||
Optional. An idempotency token that can be used to ensure that multiple
|
||||
requests to create a user have the same result.
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
|
|
@ -1227,7 +1298,9 @@ paths:
|
|||
type: string
|
||||
- name: readMask
|
||||
in: query
|
||||
description: "Optional. The fields to return in the response.\r\n If not specified, all fields are returned."
|
||||
description: |-
|
||||
Optional. The fields to return in the response.
|
||||
If not specified, all fields are returned.
|
||||
schema:
|
||||
type: string
|
||||
format: field-mask
|
||||
|
|
@ -1454,23 +1527,35 @@ paths:
|
|||
type: string
|
||||
- name: pageSize
|
||||
in: query
|
||||
description: "Optional. The maximum number of inboxes to return.\r\n The service may return fewer than this value.\r\n If unspecified, at most 50 inboxes will be returned.\r\n The maximum value is 1000; values above 1000 will be coerced to 1000."
|
||||
description: |-
|
||||
Optional. The maximum number of inboxes to return.
|
||||
The service may return fewer than this value.
|
||||
If unspecified, at most 50 inboxes will be returned.
|
||||
The maximum value is 1000; values above 1000 will be coerced to 1000.
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
- name: pageToken
|
||||
in: query
|
||||
description: "Optional. A page token, received from a previous `ListInboxes` call.\r\n Provide this to retrieve the subsequent page."
|
||||
description: |-
|
||||
Optional. A page token, received from a previous `ListInboxes` call.
|
||||
Provide this to retrieve the subsequent page.
|
||||
schema:
|
||||
type: string
|
||||
- name: filter
|
||||
in: query
|
||||
description: "Optional. Filter to apply to the list results.\r\n Example: \"status=UNREAD\" or \"type=MEMO_COMMENT\"\r\n Supported operators: =, !=\r\n Supported fields: status, type, sender, create_time"
|
||||
description: |-
|
||||
Optional. Filter to apply to the list results.
|
||||
Example: "status=UNREAD" or "type=MEMO_COMMENT"
|
||||
Supported operators: =, !=
|
||||
Supported fields: status, type, sender, create_time
|
||||
schema:
|
||||
type: string
|
||||
- name: orderBy
|
||||
in: query
|
||||
description: "Optional. The order to sort results by.\r\n Example: \"create_time desc\" or \"status asc\""
|
||||
description: |-
|
||||
Optional. The order to sort results by.
|
||||
Example: "create_time desc" or "status asc"
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
|
|
@ -1556,13 +1641,19 @@ paths:
|
|||
type: string
|
||||
- name: pageSize
|
||||
in: query
|
||||
description: "Optional. The maximum number of settings to return.\r\n The service may return fewer than this value.\r\n If unspecified, at most 50 settings will be returned.\r\n The maximum value is 1000; values above 1000 will be coerced to 1000."
|
||||
description: |-
|
||||
Optional. The maximum number of settings to return.
|
||||
The service may return fewer than this value.
|
||||
If unspecified, at most 50 settings will be returned.
|
||||
The maximum value is 1000; values above 1000 will be coerced to 1000.
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
- name: pageToken
|
||||
in: query
|
||||
description: "Optional. A page token, received from a previous `ListUserSettings` call.\r\n Provide this to retrieve the subsequent page."
|
||||
description: |-
|
||||
Optional. A page token, received from a previous `ListUserSettings` call.
|
||||
Provide this to retrieve the subsequent page.
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
|
|
@ -2117,11 +2208,15 @@ components:
|
|||
name:
|
||||
readOnly: true
|
||||
type: string
|
||||
description: "The name of the activity.\r\n Format: activities/{id}"
|
||||
description: |-
|
||||
The name of the activity.
|
||||
Format: activities/{id}
|
||||
creator:
|
||||
readOnly: true
|
||||
type: string
|
||||
description: "The name of the creator.\r\n Format: users/{user}"
|
||||
description: |-
|
||||
The name of the creator.
|
||||
Format: users/{user}
|
||||
type:
|
||||
readOnly: true
|
||||
enum:
|
||||
|
|
@ -2156,10 +2251,14 @@ components:
|
|||
properties:
|
||||
memo:
|
||||
type: string
|
||||
description: "The memo name of comment.\r\n Format: memos/{memo}"
|
||||
description: |-
|
||||
The memo name of comment.
|
||||
Format: memos/{memo}
|
||||
relatedMemo:
|
||||
type: string
|
||||
description: "The name of related memo.\r\n Format: memos/{memo}"
|
||||
description: |-
|
||||
The name of related memo.
|
||||
Format: memos/{memo}
|
||||
description: ActivityMemoCommentPayload represents the payload of a memo comment activity.
|
||||
ActivityPayload:
|
||||
type: object
|
||||
|
|
@ -2176,7 +2275,9 @@ components:
|
|||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: "The name of the attachment.\r\n Format: attachments/{attachment}"
|
||||
description: |-
|
||||
The name of the attachment.
|
||||
Format: attachments/{attachment}
|
||||
createTime:
|
||||
readOnly: true
|
||||
type: string
|
||||
|
|
@ -2202,7 +2303,9 @@ components:
|
|||
description: Output only. The size of the attachment in bytes.
|
||||
memo:
|
||||
type: string
|
||||
description: "Optional. The related memo. Refer to `Memo.name`.\r\n Format: memos/{memo}"
|
||||
description: |-
|
||||
Optional. The related memo. Refer to `Memo.name`.
|
||||
Format: memos/{memo}
|
||||
AutoLinkNode:
|
||||
type: object
|
||||
properties:
|
||||
|
|
@ -2264,10 +2367,14 @@ components:
|
|||
properties:
|
||||
username:
|
||||
type: string
|
||||
description: "The username to sign in with.\r\n Required field for password-based authentication."
|
||||
description: |-
|
||||
The username to sign in with.
|
||||
Required field for password-based authentication.
|
||||
password:
|
||||
type: string
|
||||
description: "The password to sign in with.\r\n Required field for password-based authentication."
|
||||
description: |-
|
||||
The password to sign in with.
|
||||
Required field for password-based authentication.
|
||||
description: Nested message for password-based authentication credentials.
|
||||
CreateSessionRequest_SSOCredentials:
|
||||
required:
|
||||
|
|
@ -2278,14 +2385,20 @@ components:
|
|||
properties:
|
||||
idpId:
|
||||
type: integer
|
||||
description: "The ID of the SSO provider.\r\n Required field to identify the SSO provider."
|
||||
description: |-
|
||||
The ID of the SSO provider.
|
||||
Required field to identify the SSO provider.
|
||||
format: int32
|
||||
code:
|
||||
type: string
|
||||
description: "The authorization code from the SSO provider.\r\n Required field for completing the SSO flow."
|
||||
description: |-
|
||||
The authorization code from the SSO provider.
|
||||
Required field for completing the SSO flow.
|
||||
redirectUri:
|
||||
type: string
|
||||
description: "The redirect URI used in the SSO flow.\r\n Required field for security validation."
|
||||
description: |-
|
||||
The redirect URI used in the SSO flow.
|
||||
Required field for security validation.
|
||||
description: Nested message for SSO authentication credentials.
|
||||
CreateSessionResponse:
|
||||
type: object
|
||||
|
|
@ -2296,7 +2409,9 @@ components:
|
|||
description: The authenticated user information.
|
||||
lastAccessedAt:
|
||||
type: string
|
||||
description: "Last time the session was accessed.\r\n Used for sliding expiration calculation (last_accessed_time + 2 weeks)."
|
||||
description: |-
|
||||
Last time the session was accessed.
|
||||
Used for sliding expiration calculation (last_accessed_time + 2 weeks).
|
||||
format: date-time
|
||||
DeleteMemoTagRequest:
|
||||
required:
|
||||
|
|
@ -2306,7 +2421,9 @@ components:
|
|||
properties:
|
||||
parent:
|
||||
type: string
|
||||
description: "Required. The parent, who owns the tags.\r\n Format: memos/{memo}. Use \"memos/-\" to delete all tags."
|
||||
description: |-
|
||||
Required. The parent, who owns the tags.
|
||||
Format: memos/{memo}. Use "memos/-" to delete all tags.
|
||||
tag:
|
||||
type: string
|
||||
description: Required. The tag name to delete.
|
||||
|
|
@ -2357,7 +2474,9 @@ components:
|
|||
$ref: '#/components/schemas/User'
|
||||
lastAccessedAt:
|
||||
type: string
|
||||
description: "Last time the session was accessed.\r\n Used for sliding expiration calculation (last_accessed_time + 2 weeks)."
|
||||
description: |-
|
||||
Last time the session was accessed.
|
||||
Used for sliding expiration calculation (last_accessed_time + 2 weeks).
|
||||
format: date-time
|
||||
GoogleProtobufAny:
|
||||
type: object
|
||||
|
|
@ -2376,6 +2495,12 @@ components:
|
|||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
children:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Node'
|
||||
isSelfClosing:
|
||||
type: boolean
|
||||
HeadingNode:
|
||||
type: object
|
||||
properties:
|
||||
|
|
@ -2405,7 +2530,9 @@ components:
|
|||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: "The resource name of the identity provider.\r\n Format: identityProviders/{idp}"
|
||||
description: |-
|
||||
The resource name of the identity provider.
|
||||
Format: identityProviders/{idp}
|
||||
type:
|
||||
enum:
|
||||
- TYPE_UNSPECIFIED
|
||||
|
|
@ -2440,15 +2567,21 @@ components:
|
|||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: "The resource name of the inbox.\r\n Format: inboxes/{inbox}"
|
||||
description: |-
|
||||
The resource name of the inbox.
|
||||
Format: inboxes/{inbox}
|
||||
sender:
|
||||
readOnly: true
|
||||
type: string
|
||||
description: "The sender of the inbox notification.\r\n Format: users/{user}"
|
||||
description: |-
|
||||
The sender of the inbox notification.
|
||||
Format: users/{user}
|
||||
receiver:
|
||||
readOnly: true
|
||||
type: string
|
||||
description: "The receiver of the inbox notification.\r\n Format: users/{user}"
|
||||
description: |-
|
||||
The receiver of the inbox notification.
|
||||
Format: users/{user}
|
||||
status:
|
||||
enum:
|
||||
- STATUS_UNSPECIFIED
|
||||
|
|
@ -2518,7 +2651,10 @@ components:
|
|||
description: The activities.
|
||||
nextPageToken:
|
||||
type: string
|
||||
description: "A token to retrieve the next page of results.\r\n Pass this value in the page_token field in the subsequent call to `ListActivities`\r\n method to retrieve the next page of results."
|
||||
description: |-
|
||||
A token to retrieve the next page of results.
|
||||
Pass this value in the page_token field in the subsequent call to `ListActivities`
|
||||
method to retrieve the next page of results.
|
||||
ListAllUserStatsResponse:
|
||||
type: object
|
||||
properties:
|
||||
|
|
@ -2537,7 +2673,9 @@ components:
|
|||
description: The list of attachments.
|
||||
nextPageToken:
|
||||
type: string
|
||||
description: "A token that can be sent as `page_token` to retrieve the next page.\r\n If this field is omitted, there are no subsequent pages."
|
||||
description: |-
|
||||
A token that can be sent as `page_token` to retrieve the next page.
|
||||
If this field is omitted, there are no subsequent pages.
|
||||
totalSize:
|
||||
type: integer
|
||||
description: The total count of attachments (may be approximate).
|
||||
|
|
@ -2560,7 +2698,9 @@ components:
|
|||
description: The list of inboxes.
|
||||
nextPageToken:
|
||||
type: string
|
||||
description: "A token that can be sent as `page_token` to retrieve the next page.\r\n If this field is omitted, there are no subsequent pages."
|
||||
description: |-
|
||||
A token that can be sent as `page_token` to retrieve the next page.
|
||||
If this field is omitted, there are no subsequent pages.
|
||||
totalSize:
|
||||
type: integer
|
||||
description: The total count of inboxes (may be approximate).
|
||||
|
|
@ -2635,7 +2775,9 @@ components:
|
|||
description: The list of memos.
|
||||
nextPageToken:
|
||||
type: string
|
||||
description: "A token that can be sent as `page_token` to retrieve the next page.\r\n If this field is omitted, there are no subsequent pages."
|
||||
description: |-
|
||||
A token that can be sent as `page_token` to retrieve the next page.
|
||||
If this field is omitted, there are no subsequent pages.
|
||||
totalSize:
|
||||
type: integer
|
||||
description: The total count of memos (may be approximate).
|
||||
|
|
@ -2699,7 +2841,9 @@ components:
|
|||
description: The list of user settings.
|
||||
nextPageToken:
|
||||
type: string
|
||||
description: "A token that can be sent as `page_token` to retrieve the next page.\r\n If this field is omitted, there are no subsequent pages."
|
||||
description: |-
|
||||
A token that can be sent as `page_token` to retrieve the next page.
|
||||
If this field is omitted, there are no subsequent pages.
|
||||
totalSize:
|
||||
type: integer
|
||||
description: The total count of settings (may be approximate).
|
||||
|
|
@ -2723,7 +2867,9 @@ components:
|
|||
description: The list of users.
|
||||
nextPageToken:
|
||||
type: string
|
||||
description: "A token that can be sent as `page_token` to retrieve the next page.\r\n If this field is omitted, there are no subsequent pages."
|
||||
description: |-
|
||||
A token that can be sent as `page_token` to retrieve the next page.
|
||||
If this field is omitted, there are no subsequent pages.
|
||||
totalSize:
|
||||
type: integer
|
||||
description: The total count of users (may be approximate).
|
||||
|
|
@ -2761,7 +2907,9 @@ components:
|
|||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: "The resource name of the memo.\r\n Format: memos/{memo}, memo is the user defined id or uuid."
|
||||
description: |-
|
||||
The resource name of the memo.
|
||||
Format: memos/{memo}, memo is the user defined id or uuid.
|
||||
state:
|
||||
enum:
|
||||
- STATE_UNSPECIFIED
|
||||
|
|
@ -2773,7 +2921,9 @@ components:
|
|||
creator:
|
||||
readOnly: true
|
||||
type: string
|
||||
description: "The name of the creator.\r\n Format: users/{user}"
|
||||
description: |-
|
||||
The name of the creator.
|
||||
Format: users/{user}
|
||||
createTime:
|
||||
readOnly: true
|
||||
type: string
|
||||
|
|
@ -2839,7 +2989,9 @@ components:
|
|||
parent:
|
||||
readOnly: true
|
||||
type: string
|
||||
description: "Output only. The name of the parent memo.\r\n Format: memos/{memo}"
|
||||
description: |-
|
||||
Output only. The name of the parent memo.
|
||||
Format: memos/{memo}
|
||||
snippet:
|
||||
readOnly: true
|
||||
type: string
|
||||
|
|
@ -2877,7 +3029,9 @@ components:
|
|||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: "The resource name of the memo.\r\n Format: memos/{memo}"
|
||||
description: |-
|
||||
The resource name of the memo.
|
||||
Format: memos/{memo}
|
||||
snippet:
|
||||
readOnly: true
|
||||
type: string
|
||||
|
|
@ -3063,14 +3217,21 @@ components:
|
|||
name:
|
||||
readOnly: true
|
||||
type: string
|
||||
description: "The resource name of the reaction.\r\n Format: reactions/{reaction}"
|
||||
description: |-
|
||||
The resource name of the reaction.
|
||||
Format: reactions/{reaction}
|
||||
creator:
|
||||
readOnly: true
|
||||
type: string
|
||||
description: "The resource name of the creator.\r\n Format: users/{user}"
|
||||
description: |-
|
||||
The resource name of the creator.
|
||||
Format: users/{user}
|
||||
contentId:
|
||||
type: string
|
||||
description: "The resource name of the content.\r\n For memo reactions, this should be the memo's resource name.\r\n Format: memos/{memo}"
|
||||
description: |-
|
||||
The resource name of the content.
|
||||
For memo reactions, this should be the memo's resource name.
|
||||
Format: memos/{memo}
|
||||
reactionType:
|
||||
type: string
|
||||
description: "Required. The type of reaction (e.g., \"\U0001F44D\", \"❤️\", \"\U0001F604\")."
|
||||
|
|
@ -3097,7 +3258,9 @@ components:
|
|||
properties:
|
||||
parent:
|
||||
type: string
|
||||
description: "Required. The parent, who owns the tags.\r\n Format: memos/{memo}. Use \"memos/-\" to rename all tags."
|
||||
description: |-
|
||||
Required. The parent, who owns the tags.
|
||||
Format: memos/{memo}. Use "memos/-" to rename all tags.
|
||||
oldTag:
|
||||
type: string
|
||||
description: Required. The old tag name to rename.
|
||||
|
|
@ -3128,7 +3291,9 @@ components:
|
|||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: "Required. The resource name of the memo.\r\n Format: memos/{memo}"
|
||||
description: |-
|
||||
Required. The resource name of the memo.
|
||||
Format: memos/{memo}
|
||||
attachments:
|
||||
type: array
|
||||
items:
|
||||
|
|
@ -3142,7 +3307,9 @@ components:
|
|||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: "Required. The resource name of the memo.\r\n Format: memos/{memo}"
|
||||
description: |-
|
||||
Required. The resource name of the memo.
|
||||
Format: memos/{memo}
|
||||
relations:
|
||||
type: array
|
||||
items:
|
||||
|
|
@ -3155,7 +3322,9 @@ components:
|
|||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: "The resource name of the shortcut.\r\n Format: users/{user}/shortcuts/{shortcut}"
|
||||
description: |-
|
||||
The resource name of the shortcut.
|
||||
Format: users/{user}/shortcuts/{shortcut}
|
||||
title:
|
||||
type: string
|
||||
description: The title of the shortcut.
|
||||
|
|
@ -3198,7 +3367,9 @@ components:
|
|||
type: string
|
||||
usePathStyle:
|
||||
type: boolean
|
||||
description: "S3 configuration for cloud storage backend.\r\n Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/"
|
||||
description: |-
|
||||
S3 configuration for cloud storage backend.
|
||||
Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/
|
||||
StrikethroughNode:
|
||||
type: object
|
||||
properties:
|
||||
|
|
@ -3296,7 +3467,9 @@ components:
|
|||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: "Required. The resource name of the memo.\r\n Format: memos/{memo}"
|
||||
description: |-
|
||||
Required. The resource name of the memo.
|
||||
Format: memos/{memo}
|
||||
reaction:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Reaction'
|
||||
|
|
@ -3310,7 +3483,9 @@ components:
|
|||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: "The resource name of the user.\r\n Format: users/{user}"
|
||||
description: |-
|
||||
The resource name of the user.
|
||||
Format: users/{user}
|
||||
role:
|
||||
enum:
|
||||
- ROLE_UNSPECIFIED
|
||||
|
|
@ -3362,7 +3537,9 @@ components:
|
|||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: "The resource name of the access token.\r\n Format: users/{user}/accessTokens/{access_token}"
|
||||
description: |-
|
||||
The resource name of the access token.
|
||||
Format: users/{user}/accessTokens/{access_token}
|
||||
accessToken:
|
||||
readOnly: true
|
||||
type: string
|
||||
|
|
@ -3385,7 +3562,9 @@ components:
|
|||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: "The resource name of the session.\r\n Format: users/{user}/sessions/{session}"
|
||||
description: |-
|
||||
The resource name of the session.
|
||||
Format: users/{user}/sessions/{session}
|
||||
sessionId:
|
||||
readOnly: true
|
||||
type: string
|
||||
|
|
@ -3398,7 +3577,9 @@ components:
|
|||
lastAccessedTime:
|
||||
readOnly: true
|
||||
type: string
|
||||
description: "The timestamp when the session was last accessed.\r\n Used for sliding expiration calculation (last_accessed_time + 2 weeks)."
|
||||
description: |-
|
||||
The timestamp when the session was last accessed.
|
||||
Used for sliding expiration calculation (last_accessed_time + 2 weeks).
|
||||
format: date-time
|
||||
clientInfo:
|
||||
readOnly: true
|
||||
|
|
@ -3428,7 +3609,10 @@ components:
|
|||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: "The name of the user setting.\r\n Format: users/{user}/settings/{setting}, {setting} is the key for the setting.\r\n For example, \"users/123/settings/GENERAL\" for general settings."
|
||||
description: |-
|
||||
The name of the user setting.
|
||||
Format: users/{user}/settings/{setting}, {setting} is the key for the setting.
|
||||
For example, "users/123/settings/GENERAL" for general settings.
|
||||
generalSetting:
|
||||
$ref: '#/components/schemas/UserSetting_GeneralSetting'
|
||||
sessionsSetting:
|
||||
|
|
@ -3458,7 +3642,10 @@ components:
|
|||
description: The default visibility of the memo.
|
||||
theme:
|
||||
type: string
|
||||
description: "The preferred theme of the user.\r\n This references a CSS file in the web/public/themes/ directory.\r\n If not set, the default theme will be used."
|
||||
description: |-
|
||||
The preferred theme of the user.
|
||||
This references a CSS file in the web/public/themes/ directory.
|
||||
If not set, the default theme will be used.
|
||||
description: General user settings configuration.
|
||||
UserSetting_SessionsSetting:
|
||||
type: object
|
||||
|
|
@ -3483,7 +3670,9 @@ components:
|
|||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: "The resource name of the user whose stats these are.\r\n Format: users/{user}"
|
||||
description: |-
|
||||
The resource name of the user whose stats these are.
|
||||
Format: users/{user}
|
||||
memoDisplayTimestamps:
|
||||
type: array
|
||||
items:
|
||||
|
|
@ -3531,7 +3720,9 @@ components:
|
|||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: "The name of the webhook.\r\n Format: users/{user}/webhooks/{webhook}"
|
||||
description: |-
|
||||
The name of the webhook.
|
||||
Format: users/{user}/webhooks/{webhook}
|
||||
url:
|
||||
type: string
|
||||
description: The URL to send the webhook to.
|
||||
|
|
@ -3554,7 +3745,9 @@ components:
|
|||
properties:
|
||||
owner:
|
||||
type: string
|
||||
description: "The name of instance owner.\r\n Format: users/{user}"
|
||||
description: |-
|
||||
The name of instance owner.
|
||||
Format: users/{user}
|
||||
version:
|
||||
type: string
|
||||
description: Version is the current version of instance.
|
||||
|
|
@ -3570,7 +3763,9 @@ components:
|
|||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: "The name of the workspace setting.\r\n Format: workspace/settings/{setting}"
|
||||
description: |-
|
||||
The name of the workspace setting.
|
||||
Format: workspace/settings/{setting}
|
||||
generalSetting:
|
||||
$ref: '#/components/schemas/WorkspaceSetting_GeneralSetting'
|
||||
storageSetting:
|
||||
|
|
@ -3583,7 +3778,9 @@ components:
|
|||
properties:
|
||||
theme:
|
||||
type: string
|
||||
description: "theme is the name of the selected theme.\r\n This references a CSS file in the web/public/themes/ directory."
|
||||
description: |-
|
||||
theme is the name of the selected theme.
|
||||
This references a CSS file in the web/public/themes/ directory.
|
||||
disallowUserRegistration:
|
||||
type: boolean
|
||||
description: disallow_user_registration disallows user registration.
|
||||
|
|
@ -3602,7 +3799,10 @@ components:
|
|||
description: custom_profile is the custom profile.
|
||||
weekStartDayOffset:
|
||||
type: integer
|
||||
description: "week_start_day_offset is the week start day offset from Sunday.\r\n 0: Sunday, 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday\r\n Default is Sunday."
|
||||
description: |-
|
||||
week_start_day_offset is the week start day offset from Sunday.
|
||||
0: Sunday, 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday
|
||||
Default is Sunday.
|
||||
format: int32
|
||||
disallowChangeUsername:
|
||||
type: boolean
|
||||
|
|
@ -3661,7 +3861,9 @@ components:
|
|||
format: enum
|
||||
filepathTemplate:
|
||||
type: string
|
||||
description: "The template of file path.\r\n e.g. assets/{timestamp}_{filename}"
|
||||
description: |-
|
||||
The template of file path.
|
||||
e.g. assets/{timestamp}_{filename}
|
||||
uploadSizeLimitMb:
|
||||
type: string
|
||||
description: The max upload size in megabytes.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.8
|
||||
// protoc-gen-go v1.36.9
|
||||
// protoc (unknown)
|
||||
// source: store/activity.proto
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.8
|
||||
// protoc-gen-go v1.36.9
|
||||
// protoc (unknown)
|
||||
// source: store/attachment.proto
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.8
|
||||
// protoc-gen-go v1.36.9
|
||||
// protoc (unknown)
|
||||
// source: store/idp.proto
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.8
|
||||
// protoc-gen-go v1.36.9
|
||||
// protoc (unknown)
|
||||
// source: store/inbox.proto
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.8
|
||||
// protoc-gen-go v1.36.9
|
||||
// protoc (unknown)
|
||||
// source: store/memo.proto
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.8
|
||||
// protoc-gen-go v1.36.9
|
||||
// protoc (unknown)
|
||||
// source: store/user_setting.proto
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.8
|
||||
// protoc-gen-go v1.36.9
|
||||
// protoc (unknown)
|
||||
// source: store/workspace_setting.proto
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -128,7 +128,12 @@ func convertFromASTNode(rawNode ast.Node) *v1pb.Node {
|
|||
case *ast.Spoiler:
|
||||
node.Node = &v1pb.Node_SpoilerNode{SpoilerNode: &v1pb.SpoilerNode{Content: n.Content}}
|
||||
case *ast.HTMLElement:
|
||||
node.Node = &v1pb.Node_HtmlElementNode{HtmlElementNode: &v1pb.HTMLElementNode{TagName: n.TagName, Attributes: n.Attributes}}
|
||||
node.Node = &v1pb.Node_HtmlElementNode{HtmlElementNode: &v1pb.HTMLElementNode{
|
||||
TagName: n.TagName,
|
||||
Attributes: n.Attributes,
|
||||
Children: convertFromASTNodes(n.Children),
|
||||
IsSelfClosing: n.IsSelfClosing,
|
||||
}}
|
||||
default:
|
||||
node.Node = &v1pb.Node_TextNode{TextNode: &v1pb.TextNode{}}
|
||||
}
|
||||
|
|
@ -168,7 +173,7 @@ func convertListKindFromASTNode(node ast.ListKind) v1pb.ListNode_Kind {
|
|||
return v1pb.ListNode_ORDERED
|
||||
case ast.UnorderedList:
|
||||
return v1pb.ListNode_UNORDERED
|
||||
case ast.DescrpitionList:
|
||||
case ast.DescriptionList:
|
||||
return v1pb.ListNode_DESCRIPTION
|
||||
default:
|
||||
return v1pb.ListNode_KIND_UNSPECIFIED
|
||||
|
|
@ -249,7 +254,16 @@ func convertToASTNode(node *v1pb.Node) ast.Node {
|
|||
case *v1pb.Node_SpoilerNode:
|
||||
return &ast.Spoiler{Content: n.SpoilerNode.Content}
|
||||
case *v1pb.Node_HtmlElementNode:
|
||||
return &ast.HTMLElement{TagName: n.HtmlElementNode.TagName, Attributes: n.HtmlElementNode.Attributes}
|
||||
var children []ast.Node
|
||||
if len(n.HtmlElementNode.Children) > 0 {
|
||||
children = convertToASTNodes(n.HtmlElementNode.Children)
|
||||
}
|
||||
return &ast.HTMLElement{
|
||||
TagName: n.HtmlElementNode.TagName,
|
||||
Attributes: n.HtmlElementNode.Attributes,
|
||||
Children: children,
|
||||
IsSelfClosing: n.HtmlElementNode.IsSelfClosing,
|
||||
}
|
||||
default:
|
||||
return &ast.Text{}
|
||||
}
|
||||
|
|
@ -286,9 +300,9 @@ func convertListKindToASTNode(kind v1pb.ListNode_Kind) ast.ListKind {
|
|||
case v1pb.ListNode_UNORDERED:
|
||||
return ast.UnorderedList
|
||||
case v1pb.ListNode_DESCRIPTION:
|
||||
return ast.DescrpitionList
|
||||
return ast.DescriptionList
|
||||
default:
|
||||
// Default to description list.
|
||||
return ast.DescrpitionList
|
||||
return ast.DescriptionList
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.44.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.45.0",
|
||||
"vite": "^7.1.5"
|
||||
},
|
||||
"pnpm": {
|
||||
|
|
@ -92,4 +92,4 @@
|
|||
"esbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ importers:
|
|||
version: 11.11.1
|
||||
i18next:
|
||||
specifier: ^25.5.2
|
||||
version: 25.5.2(typescript@5.9.2)
|
||||
version: 25.5.2(typescript@5.9.3)
|
||||
katex:
|
||||
specifier: ^0.16.22
|
||||
version: 0.16.22
|
||||
|
|
@ -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
|
||||
|
|
@ -130,7 +130,7 @@ importers:
|
|||
version: 2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react-i18next:
|
||||
specifier: ^15.7.3
|
||||
version: 15.7.3(i18next@25.5.2(typescript@5.9.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)
|
||||
version: 15.7.3(i18next@25.5.2(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)
|
||||
react-leaflet:
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1(leaflet@1.9.4)(react-dom@18.3.1(react@18.3.1))(react@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.2
|
||||
version: 5.9.2
|
||||
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.2)
|
||||
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,15 +3362,15 @@ 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
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
typescript@5.9.2:
|
||||
resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
|
|
@ -4808,97 +4808,97 @@ 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.2))(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2)':
|
||||
'@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.2)
|
||||
'@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.2)
|
||||
'@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2)
|
||||
'@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
|
||||
natural-compare: 1.4.0
|
||||
ts-api-utils: 2.1.0(typescript@5.9.2)
|
||||
typescript: 5.9.2
|
||||
ts-api-utils: 2.1.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2)':
|
||||
'@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.2)
|
||||
'@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.2
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/project-service@8.44.0(typescript@5.9.2)':
|
||||
'@typescript-eslint/project-service@8.45.0(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.44.0(typescript@5.9.2)
|
||||
'@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.2
|
||||
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.2)':
|
||||
'@typescript-eslint/tsconfig-utils@8.45.0(typescript@5.9.3)':
|
||||
dependencies:
|
||||
typescript: 5.9.2
|
||||
typescript: 5.9.3
|
||||
|
||||
'@typescript-eslint/type-utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2)':
|
||||
'@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.2)
|
||||
'@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2)
|
||||
'@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.2)
|
||||
typescript: 5.9.2
|
||||
ts-api-utils: 2.1.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
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.2)':
|
||||
'@typescript-eslint/typescript-estree@8.45.0(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/project-service': 8.44.0(typescript@5.9.2)
|
||||
'@typescript-eslint/tsconfig-utils': 8.44.0(typescript@5.9.2)
|
||||
'@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
|
||||
minimatch: 9.0.5
|
||||
semver: 7.7.2
|
||||
ts-api-utils: 2.1.0(typescript@5.9.2)
|
||||
typescript: 5.9.2
|
||||
ts-api-utils: 2.1.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2)':
|
||||
'@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.2)
|
||||
'@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.2
|
||||
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))':
|
||||
|
|
@ -5903,11 +5903,11 @@ snapshots:
|
|||
|
||||
hyphenate-style-name@1.1.0: {}
|
||||
|
||||
i18next@25.5.2(typescript@5.9.2):
|
||||
i18next@25.5.2(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
optionalDependencies:
|
||||
typescript: 5.9.2
|
||||
typescript: 5.9.3
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
dependencies:
|
||||
|
|
@ -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
|
||||
|
|
@ -6495,15 +6495,15 @@ snapshots:
|
|||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
react-i18next@15.7.3(i18next@25.5.2(typescript@5.9.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2):
|
||||
react-i18next@15.7.3(i18next@25.5.2(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
html-parse-stringify: 3.0.1
|
||||
i18next: 25.5.2(typescript@5.9.2)
|
||||
i18next: 25.5.2(typescript@5.9.3)
|
||||
react: 18.3.1
|
||||
optionalDependencies:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
typescript: 5.9.2
|
||||
typescript: 5.9.3
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
|
|
@ -6906,9 +6906,9 @@ snapshots:
|
|||
|
||||
toggle-selection@1.0.6: {}
|
||||
|
||||
ts-api-utils@2.1.0(typescript@5.9.2):
|
||||
ts-api-utils@2.1.0(typescript@5.9.3):
|
||||
dependencies:
|
||||
typescript: 5.9.2
|
||||
typescript: 5.9.3
|
||||
|
||||
ts-dedent@2.2.0: {}
|
||||
|
||||
|
|
@ -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,18 +6957,18 @@ 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.2):
|
||||
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.2))(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2)
|
||||
'@typescript-eslint/parser': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2)
|
||||
'@typescript-eslint/typescript-estree': 8.44.0(typescript@5.9.2)
|
||||
'@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2)
|
||||
'@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.2
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
typescript@5.9.2: {}
|
||||
typescript@5.9.3: {}
|
||||
|
||||
ufo@1.6.1: {}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,178 +1,73 @@
|
|||
import dayjs from "dayjs";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { memo, useMemo } from "react";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { workspaceStore } from "@/store";
|
||||
import type { ActivityCalendarProps, CalendarDay } from "@/types/statistics";
|
||||
import type { ActivityCalendarProps } from "@/types/statistics";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
|
||||
const getCellOpacity = (ratio: number): string => {
|
||||
if (ratio === 0) return "";
|
||||
if (ratio > 0.75) return "bg-destructive text-destructive-foreground";
|
||||
if (ratio > 0.5) return "bg-destructive/70 text-destructive-foreground";
|
||||
if (ratio > 0.25) return "bg-destructive/50 text-destructive-foreground";
|
||||
return "bg-destructive/30 text-destructive-foreground";
|
||||
};
|
||||
|
||||
const CalendarCell = memo(
|
||||
({
|
||||
dayInfo,
|
||||
count,
|
||||
maxCount,
|
||||
isToday,
|
||||
isSelected,
|
||||
onClick,
|
||||
tooltipText,
|
||||
}: {
|
||||
dayInfo: CalendarDay;
|
||||
count: number;
|
||||
maxCount: number;
|
||||
isToday: boolean;
|
||||
isSelected: boolean;
|
||||
onClick?: () => void;
|
||||
tooltipText: string;
|
||||
}) => {
|
||||
const cellContent = (
|
||||
<div
|
||||
className={cn(
|
||||
"w-6 h-6 text-xs lg:text-[13px] flex justify-center items-center cursor-default",
|
||||
"rounded-lg border-2 text-muted-foreground transition-all duration-200",
|
||||
dayInfo.isCurrentMonth && getCellOpacity(count / maxCount),
|
||||
dayInfo.isCurrentMonth && isToday && "border-border",
|
||||
dayInfo.isCurrentMonth && isSelected && "font-medium border-border",
|
||||
dayInfo.isCurrentMonth && !isToday && !isSelected && "border-transparent",
|
||||
count > 0 && "cursor-pointer hover:scale-110",
|
||||
)}
|
||||
onClick={count > 0 ? onClick : undefined}
|
||||
>
|
||||
{dayInfo.day}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!dayInfo.isCurrentMonth) {
|
||||
return (
|
||||
<div
|
||||
className={cn("w-6 h-6 text-xs lg:text-[13px] flex justify-center items-center cursor-default opacity-60 text-muted-foreground")}
|
||||
>
|
||||
{dayInfo.day}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="shrink-0">{cellContent}</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{tooltipText}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
},
|
||||
);
|
||||
import { CalendarCell } from "./CalendarCell";
|
||||
import { useCalendarMatrix } from "./useCalendarMatrix";
|
||||
|
||||
export const ActivityCalendar = memo(
|
||||
observer((props: ActivityCalendarProps) => {
|
||||
const t = useTranslate();
|
||||
const { month: monthStr, data, onClick } = props;
|
||||
const { month, selectedDate, data, onClick } = props;
|
||||
const weekStartDayOffset = workspaceStore.state.generalSetting.weekStartDayOffset;
|
||||
|
||||
const { days, weekDays, maxCount } = useMemo(() => {
|
||||
const yearValue = dayjs(monthStr).toDate().getFullYear();
|
||||
const monthValue = dayjs(monthStr).toDate().getMonth();
|
||||
const dayInMonth = new Date(yearValue, monthValue + 1, 0).getDate();
|
||||
const firstDay = (((new Date(yearValue, monthValue, 1).getDay() - weekStartDayOffset) % 7) + 7) % 7;
|
||||
const lastDay = new Date(yearValue, monthValue, dayInMonth).getDay() - weekStartDayOffset;
|
||||
const prevMonthDays = new Date(yearValue, monthValue, 0).getDate();
|
||||
|
||||
const WEEK_DAYS = [t("days.sun"), t("days.mon"), t("days.tue"), t("days.wed"), t("days.thu"), t("days.fri"), t("days.sat")];
|
||||
const weekDaysOrdered = WEEK_DAYS.slice(weekStartDayOffset).concat(WEEK_DAYS.slice(0, weekStartDayOffset));
|
||||
|
||||
const daysArray: CalendarDay[] = [];
|
||||
|
||||
// Previous month's days
|
||||
for (let i = firstDay - 1; i >= 0; i--) {
|
||||
daysArray.push({ day: prevMonthDays - i, isCurrentMonth: false });
|
||||
}
|
||||
|
||||
// Current month's days
|
||||
for (let i = 1; i <= dayInMonth; i++) {
|
||||
const date = dayjs(`${yearValue}-${monthValue + 1}-${i}`).format("YYYY-MM-DD");
|
||||
daysArray.push({ day: i, isCurrentMonth: true, date });
|
||||
}
|
||||
|
||||
// Next month's days
|
||||
for (let i = 1; i < 7 - lastDay; i++) {
|
||||
daysArray.push({ day: i, isCurrentMonth: false });
|
||||
}
|
||||
|
||||
const maxCountValue = Math.max(...Object.values(data), 1);
|
||||
|
||||
return {
|
||||
year: yearValue,
|
||||
month: monthValue,
|
||||
days: daysArray,
|
||||
weekDays: weekDaysOrdered,
|
||||
maxCount: maxCountValue,
|
||||
};
|
||||
}, [monthStr, data, weekStartDayOffset, t]);
|
||||
|
||||
const today = useMemo(() => dayjs().format("YYYY-MM-DD"), []);
|
||||
const selectedDateFormatted = useMemo(() => dayjs(props.selectedDate).format("YYYY-MM-DD"), [props.selectedDate]);
|
||||
const selectedDateFormatted = useMemo(() => dayjs(selectedDate).format("YYYY-MM-DD"), [selectedDate]);
|
||||
|
||||
const weekDaysRaw = useMemo(
|
||||
() => [t("days.sun"), t("days.mon"), t("days.tue"), t("days.wed"), t("days.thu"), t("days.fri"), t("days.sat")],
|
||||
[t],
|
||||
);
|
||||
|
||||
const { weeks, weekDays, maxCount } = useCalendarMatrix({
|
||||
month,
|
||||
data,
|
||||
weekDays: weekDaysRaw,
|
||||
weekStartDayOffset,
|
||||
today,
|
||||
selectedDate: selectedDateFormatted,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={cn("w-full h-auto shrink-0 grid grid-cols-7 grid-flow-row gap-1")}>
|
||||
{weekDays.map((day, index) => (
|
||||
<div key={index} className={cn("w-6 h-5 text-xs flex justify-center items-center cursor-default opacity-60")}>
|
||||
{day}
|
||||
<TooltipProvider>
|
||||
<div className="w-full flex flex-col gap-1">
|
||||
<div className="grid grid-cols-7 gap-1 text-xs text-muted-foreground">
|
||||
{weekDays.map((label, index) => (
|
||||
<div key={index} className="flex h-5 items-center justify-center text-muted-foreground/80">
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
{days.map((dayInfo, index) => {
|
||||
if (!dayInfo.isCurrentMonth) {
|
||||
return (
|
||||
<CalendarCell
|
||||
key={`prev-next-${index}`}
|
||||
dayInfo={dayInfo}
|
||||
count={0}
|
||||
maxCount={maxCount}
|
||||
isToday={false}
|
||||
isSelected={false}
|
||||
tooltipText=""
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const date = dayInfo.date!;
|
||||
const count = data[date] || 0;
|
||||
const isToday = today === date;
|
||||
const isSelected = selectedDateFormatted === date;
|
||||
const tooltipText =
|
||||
count === 0
|
||||
? date
|
||||
: t("memo.count-memos-in-date", {
|
||||
count: count,
|
||||
memos: count === 1 ? t("common.memo") : t("common.memos"),
|
||||
date: date,
|
||||
}).toLowerCase();
|
||||
<div className="grid grid-cols-7 gap-1">
|
||||
{weeks.map((week, weekIndex) =>
|
||||
week.days.map((day, dayIndex) => {
|
||||
const tooltipText =
|
||||
day.count === 0
|
||||
? day.date
|
||||
: t("memo.count-memos-in-date", {
|
||||
count: day.count,
|
||||
memos: day.count === 1 ? t("common.memo") : t("common.memos"),
|
||||
date: day.date,
|
||||
}).toLowerCase();
|
||||
|
||||
return (
|
||||
<CalendarCell
|
||||
key={date}
|
||||
dayInfo={dayInfo}
|
||||
count={count}
|
||||
maxCount={maxCount}
|
||||
isToday={isToday}
|
||||
isSelected={isSelected}
|
||||
onClick={() => onClick?.(date)}
|
||||
tooltipText={tooltipText}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
return (
|
||||
<CalendarCell
|
||||
key={`${weekIndex}-${dayIndex}-${day.date}`}
|
||||
day={day}
|
||||
maxCount={maxCount}
|
||||
tooltipText={tooltipText}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
import { memo } from "react";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { CalendarDayCell } from "./types";
|
||||
import { getCellIntensityClass } from "./utils";
|
||||
|
||||
interface CalendarCellProps {
|
||||
day: CalendarDayCell;
|
||||
maxCount: number;
|
||||
tooltipText: string;
|
||||
onClick?: (date: string) => void;
|
||||
}
|
||||
|
||||
export const CalendarCell = memo((props: CalendarCellProps) => {
|
||||
const { day, maxCount, tooltipText, onClick } = props;
|
||||
|
||||
const handleClick = () => {
|
||||
if (day.count > 0 && onClick) {
|
||||
onClick(day.date);
|
||||
}
|
||||
};
|
||||
|
||||
const baseClasses =
|
||||
"w-full h-7 rounded-md border text-xs flex items-center justify-center text-center transition-transform duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60 focus-visible:ring-offset-1 focus-visible:ring-offset-background select-none";
|
||||
const isInteractive = Boolean(onClick && day.count > 0);
|
||||
const ariaLabel = day.isSelected ? `${tooltipText} (selected)` : tooltipText;
|
||||
|
||||
if (!day.isCurrentMonth) {
|
||||
return (
|
||||
<div className={cn(baseClasses, "border-transparent text-muted-foreground/60 bg-transparent pointer-events-none opacity-80")}>
|
||||
{day.label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const intensityClass = getCellIntensityClass(day, maxCount);
|
||||
|
||||
const buttonClasses = cn(
|
||||
baseClasses,
|
||||
"border-transparent text-muted-foreground",
|
||||
day.isToday && "border-border",
|
||||
day.isSelected && "border-border font-medium",
|
||||
day.isWeekend && "text-muted-foreground/80",
|
||||
intensityClass,
|
||||
isInteractive ? "cursor-pointer hover:scale-105" : "cursor-default",
|
||||
);
|
||||
|
||||
const button = (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
tabIndex={isInteractive ? 0 : -1}
|
||||
aria-label={ariaLabel}
|
||||
aria-current={day.isToday ? "date" : undefined}
|
||||
aria-disabled={!isInteractive}
|
||||
className={buttonClasses}
|
||||
>
|
||||
{day.label}
|
||||
</button>
|
||||
);
|
||||
|
||||
if (!tooltipText) {
|
||||
return button;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<p>{tooltipText}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
CalendarCell.displayName = "CalendarCell";
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
export interface CalendarDayCell {
|
||||
date: string;
|
||||
label: number;
|
||||
count: number;
|
||||
isCurrentMonth: boolean;
|
||||
isToday: boolean;
|
||||
isSelected: boolean;
|
||||
isWeekend: boolean;
|
||||
}
|
||||
|
||||
export interface CalendarDayRow {
|
||||
days: CalendarDayCell[];
|
||||
}
|
||||
|
||||
export interface CalendarMatrixResult {
|
||||
weeks: CalendarDayRow[];
|
||||
weekDays: string[];
|
||||
maxCount: number;
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import dayjs from "dayjs";
|
||||
import { useMemo } from "react";
|
||||
import type { CalendarDayCell, CalendarMatrixResult } from "./types";
|
||||
|
||||
interface UseCalendarMatrixParams {
|
||||
month: string;
|
||||
data: Record<string, number>;
|
||||
weekDays: string[];
|
||||
weekStartDayOffset: number;
|
||||
today: string;
|
||||
selectedDate: string;
|
||||
}
|
||||
|
||||
export const useCalendarMatrix = ({
|
||||
month,
|
||||
data,
|
||||
weekDays,
|
||||
weekStartDayOffset,
|
||||
today,
|
||||
selectedDate,
|
||||
}: UseCalendarMatrixParams): CalendarMatrixResult => {
|
||||
return useMemo(() => {
|
||||
const monthStart = dayjs(month).startOf("month");
|
||||
const monthEnd = monthStart.endOf("month");
|
||||
const monthKey = monthStart.format("YYYY-MM");
|
||||
|
||||
const orderedWeekDays = weekDays.slice(weekStartDayOffset).concat(weekDays.slice(0, weekStartDayOffset));
|
||||
|
||||
const startOffset = (monthStart.day() - weekStartDayOffset + 7) % 7;
|
||||
const endOffset = (weekStartDayOffset + 6 - monthEnd.day() + 7) % 7;
|
||||
|
||||
const calendarStart = monthStart.subtract(startOffset, "day");
|
||||
const calendarEnd = monthEnd.add(endOffset, "day");
|
||||
const dayCount = calendarEnd.diff(calendarStart, "day") + 1;
|
||||
|
||||
const weeks: CalendarMatrixResult["weeks"] = [];
|
||||
let maxCount = 0;
|
||||
|
||||
for (let index = 0; index < dayCount; index += 1) {
|
||||
const current = calendarStart.add(index, "day");
|
||||
const isoDate = current.format("YYYY-MM-DD");
|
||||
const weekIndex = Math.floor(index / 7);
|
||||
|
||||
if (!weeks[weekIndex]) {
|
||||
weeks[weekIndex] = { days: [] };
|
||||
}
|
||||
|
||||
const isCurrentMonth = current.format("YYYY-MM") === monthKey;
|
||||
const count = data[isoDate] ?? 0;
|
||||
|
||||
const dayCell: CalendarDayCell = {
|
||||
date: isoDate,
|
||||
label: current.date(),
|
||||
count,
|
||||
isCurrentMonth,
|
||||
isToday: isoDate === today,
|
||||
isSelected: isoDate === selectedDate,
|
||||
isWeekend: [0, 6].includes(current.day()),
|
||||
};
|
||||
|
||||
weeks[weekIndex].days.push(dayCell);
|
||||
maxCount = Math.max(maxCount, count);
|
||||
}
|
||||
|
||||
return {
|
||||
weeks,
|
||||
weekDays: orderedWeekDays,
|
||||
maxCount: Math.max(maxCount, 1),
|
||||
};
|
||||
}, [month, data, weekDays, weekStartDayOffset, today, selectedDate]);
|
||||
};
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import type { CalendarDayCell } from "./types";
|
||||
|
||||
export const getCellIntensityClass = (day: CalendarDayCell, maxCount: number): string => {
|
||||
if (!day.isCurrentMonth || day.count === 0 || maxCount <= 0) {
|
||||
return "bg-transparent";
|
||||
}
|
||||
|
||||
const ratio = day.count / maxCount;
|
||||
if (ratio > 0.75) return "bg-primary text-primary-foreground border-primary";
|
||||
if (ratio > 0.5) return "bg-primary/80 text-primary-foreground border-primary/90";
|
||||
if (ratio > 0.25) return "bg-primary/60 text-primary-foreground border-primary/70";
|
||||
return "bg-primary/40 text-primary";
|
||||
};
|
||||
|
|
@ -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();
|
||||
|
||||
<ConfirmDialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
title={t("memo.delete-confirm")}
|
||||
description={t("memo.delete-confirm-description")}
|
||||
confirmLabel={t("common.delete")}
|
||||
cancelLabel={t("common.cancel")}
|
||||
onConfirm={handleDelete}
|
||||
confirmVariant="destructive"
|
||||
/>;
|
||||
```
|
||||
|
||||
## 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<void>` | 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
|
||||
<Dialog open={open} onOpenChange={(next) => !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.
|
||||
|
|
@ -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<void>;
|
||||
/** 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 (
|
||||
<Dialog open={open} onOpenChange={(o: boolean) => !loading && onOpenChange(o)}>
|
||||
<DialogContent size="sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
{description ? <DialogDescription>{description}</DialogDescription> : null}
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" disabled={loading} onClick={() => onOpenChange(false)}>
|
||||
{cancelLabel}
|
||||
</Button>
|
||||
<Button variant={confirmVariant} disabled={loading} onClick={handleConfirm} data-loading={loading}>
|
||||
{confirmLabel}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<Shortcut | undefined>();
|
||||
const [editingShortcut, setEditingShortcut] = useState<Shortcut | undefined>();
|
||||
|
||||
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}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
open={!!deleteTarget}
|
||||
onOpenChange={(open) => !open && setDeleteTarget(undefined)}
|
||||
title={t("setting.shortcut.delete-confirm", { title: deleteTarget?.title ?? "" })}
|
||||
confirmLabel={t("common.delete")}
|
||||
cancelLabel={t("common.cancel")}
|
||||
onConfirm={confirmDeleteShortcut}
|
||||
confirmVariant="destructive"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<boolean>("tag-tree-auto-expand", false);
|
||||
const renameTagDialog = useDialog();
|
||||
const [selectedTag, setSelectedTag] = useState<string>("");
|
||||
const [deleteTagName, setDeleteTagName] = useState<string | undefined>(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}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
open={!!deleteTagName}
|
||||
onOpenChange={(open) => !open && setDeleteTagName(undefined)}
|
||||
title={t("tag.delete-confirm")}
|
||||
confirmLabel={t("common.delete")}
|
||||
cancelLabel={t("common.cancel")}
|
||||
onConfirm={confirmDeleteTag}
|
||||
confirmVariant="destructive"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -38,6 +38,16 @@ const LocationMarker = (props: MarkerProps) => {
|
|||
map.locate();
|
||||
}, []);
|
||||
|
||||
// Keep marker and map in sync with external position updates
|
||||
useEffect(() => {
|
||||
if (props.position) {
|
||||
setPosition(props.position);
|
||||
map.setView(props.position);
|
||||
} else {
|
||||
setPosition(undefined);
|
||||
}
|
||||
}, [props.position, map]);
|
||||
|
||||
return position === undefined ? null : <Marker position={position} icon={markerIcon}></Marker>;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import { MasonryItem } from "./MasonryItem";
|
||||
import { MasonryColumnProps } from "./types";
|
||||
|
||||
/**
|
||||
* Column component for masonry layout
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Render a single column in the masonry grid
|
||||
* - Display prefix element in the first column (e.g., memo editor)
|
||||
* - Render all assigned memo items in order
|
||||
* - Pass render context to items (includes compact mode flag)
|
||||
*/
|
||||
export function MasonryColumn({
|
||||
memoIndices,
|
||||
memoList,
|
||||
renderer,
|
||||
renderContext,
|
||||
onHeightChange,
|
||||
isFirstColumn,
|
||||
prefixElement,
|
||||
prefixElementRef,
|
||||
}: MasonryColumnProps) {
|
||||
return (
|
||||
<div className="min-w-0 mx-auto w-full max-w-2xl">
|
||||
{/* Prefix element (like memo editor) goes in first column */}
|
||||
{isFirstColumn && prefixElement && <div ref={prefixElementRef}>{prefixElement}</div>}
|
||||
|
||||
{/* Render all memos assigned to this column */}
|
||||
{memoIndices?.map((memoIndex) => {
|
||||
const memo = memoList[memoIndex];
|
||||
return memo ? (
|
||||
<MasonryItem
|
||||
key={`${memo.name}-${memo.displayTime}`}
|
||||
memo={memo}
|
||||
renderer={renderer}
|
||||
renderContext={renderContext}
|
||||
onHeightChange={onHeightChange}
|
||||
/>
|
||||
) : null;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
import { MasonryItemProps } from "./types";
|
||||
|
||||
/**
|
||||
* Individual item wrapper component for masonry layout
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Render the memo using the provided renderer with context
|
||||
* - Measure its own height using ResizeObserver
|
||||
* - Report height changes to parent for redistribution
|
||||
*
|
||||
* The ResizeObserver automatically tracks dynamic content changes such as:
|
||||
* - Images loading
|
||||
* - Expanded/collapsed text
|
||||
* - Any other content size changes
|
||||
*/
|
||||
export function MasonryItem({ memo, renderer, renderContext, onHeightChange }: MasonryItemProps) {
|
||||
const itemRef = useRef<HTMLDivElement>(null);
|
||||
const resizeObserverRef = useRef<ResizeObserver | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!itemRef.current) return;
|
||||
|
||||
const measureHeight = () => {
|
||||
if (itemRef.current) {
|
||||
const height = itemRef.current.offsetHeight;
|
||||
onHeightChange(memo.name, height);
|
||||
}
|
||||
};
|
||||
|
||||
// Initial measurement
|
||||
measureHeight();
|
||||
|
||||
// Set up ResizeObserver to track dynamic content changes
|
||||
resizeObserverRef.current = new ResizeObserver(measureHeight);
|
||||
resizeObserverRef.current.observe(itemRef.current);
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
resizeObserverRef.current?.disconnect();
|
||||
};
|
||||
}, [memo.name, onHeightChange]);
|
||||
|
||||
return <div ref={itemRef}>{renderer(memo, renderContext)}</div>;
|
||||
}
|
||||
|
|
@ -1,156 +1,42 @@
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useMemo, useRef } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Memo } from "@/types/proto/api/v1/memo_service";
|
||||
|
||||
interface Props {
|
||||
memoList: Memo[];
|
||||
renderer: (memo: Memo) => JSX.Element;
|
||||
prefixElement?: JSX.Element;
|
||||
listMode?: boolean;
|
||||
}
|
||||
|
||||
interface MemoItemProps {
|
||||
memo: Memo;
|
||||
renderer: (memo: Memo) => JSX.Element;
|
||||
onHeightChange: (memoName: string, height: number) => void;
|
||||
}
|
||||
|
||||
// Minimum width required to show more than one column
|
||||
const MINIMUM_MEMO_VIEWPORT_WIDTH = 512;
|
||||
|
||||
const MemoItem = ({ memo, renderer, onHeightChange }: MemoItemProps) => {
|
||||
const itemRef = useRef<HTMLDivElement>(null);
|
||||
const resizeObserverRef = useRef<ResizeObserver | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!itemRef.current) return;
|
||||
|
||||
const measureHeight = () => {
|
||||
if (itemRef.current) {
|
||||
const height = itemRef.current.offsetHeight;
|
||||
onHeightChange(memo.name, height);
|
||||
}
|
||||
};
|
||||
|
||||
measureHeight();
|
||||
|
||||
// Set up ResizeObserver to track dynamic content changes (images, expanded text, etc.)
|
||||
resizeObserverRef.current = new ResizeObserver(measureHeight);
|
||||
resizeObserverRef.current.observe(itemRef.current);
|
||||
|
||||
return () => {
|
||||
resizeObserverRef.current?.disconnect();
|
||||
};
|
||||
}, [memo.name, onHeightChange]);
|
||||
|
||||
return <div ref={itemRef}>{renderer(memo)}</div>;
|
||||
};
|
||||
import { MasonryColumn } from "./MasonryColumn";
|
||||
import { MasonryViewProps, MemoRenderContext } from "./types";
|
||||
import { useMasonryLayout } from "./useMasonryLayout";
|
||||
|
||||
/**
|
||||
* Algorithm to distribute memos into columns based on height for balanced layout
|
||||
* Uses greedy approach: always place next memo in the shortest column
|
||||
* Masonry layout component for displaying memos in a balanced, multi-column grid
|
||||
*
|
||||
* Features:
|
||||
* - Responsive column count based on viewport width
|
||||
* - Longest Processing-Time First (LPT) algorithm for optimal distribution
|
||||
* - Pins editor and first memo to first column for stability
|
||||
* - Debounced redistribution for performance
|
||||
* - Automatic height tracking with ResizeObserver
|
||||
* - Auto-enables compact mode in multi-column layouts
|
||||
*
|
||||
* The layout automatically adjusts to:
|
||||
* - Window resizing
|
||||
* - Content changes (images loading, text expansion)
|
||||
* - Dynamic memo additions/removals
|
||||
*
|
||||
* Algorithm guarantee: Layout is never more than 34% longer than optimal (proven)
|
||||
*/
|
||||
const distributeMemosToColumns = (
|
||||
memos: Memo[],
|
||||
columns: number,
|
||||
itemHeights: Map<string, number>,
|
||||
prefixElementHeight: number = 0,
|
||||
): { distribution: number[][]; columnHeights: number[] } => {
|
||||
// List mode: all memos in single column
|
||||
if (columns === 1) {
|
||||
const totalHeight = memos.reduce((sum, memo) => sum + (itemHeights.get(memo.name) || 0), prefixElementHeight);
|
||||
return {
|
||||
distribution: [Array.from({ length: memos.length }, (_, i) => i)],
|
||||
columnHeights: [totalHeight],
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize columns and heights
|
||||
const distribution: number[][] = Array.from({ length: columns }, () => []);
|
||||
const columnHeights: number[] = Array(columns).fill(0);
|
||||
|
||||
// Add prefix element height to first column
|
||||
if (prefixElementHeight > 0) {
|
||||
columnHeights[0] = prefixElementHeight;
|
||||
}
|
||||
|
||||
// Distribute each memo to the shortest column
|
||||
memos.forEach((memo, index) => {
|
||||
const height = itemHeights.get(memo.name) || 0;
|
||||
|
||||
// Find column with minimum height
|
||||
const shortestColumnIndex = columnHeights.indexOf(Math.min(...columnHeights));
|
||||
|
||||
distribution[shortestColumnIndex].push(index);
|
||||
columnHeights[shortestColumnIndex] += height;
|
||||
});
|
||||
|
||||
return { distribution, columnHeights };
|
||||
};
|
||||
|
||||
const MasonryView = (props: Props) => {
|
||||
const [columns, setColumns] = useState(1);
|
||||
const [itemHeights, setItemHeights] = useState<Map<string, number>>(new Map());
|
||||
const [distribution, setDistribution] = useState<number[][]>([[]]);
|
||||
|
||||
const MasonryView = ({ memoList, renderer, prefixElement, listMode = false }: MasonryViewProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const prefixElementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Calculate optimal number of columns based on container width
|
||||
const calculateColumns = useCallback(() => {
|
||||
if (!containerRef.current || props.listMode) return 1;
|
||||
const { columns, distribution, handleHeightChange } = useMasonryLayout(memoList, listMode, containerRef, prefixElementRef);
|
||||
|
||||
const containerWidth = containerRef.current.offsetWidth;
|
||||
const scale = containerWidth / MINIMUM_MEMO_VIEWPORT_WIDTH;
|
||||
return scale >= 2 ? Math.round(scale) : 1;
|
||||
}, [props.listMode]);
|
||||
|
||||
// Recalculate memo distribution when layout changes
|
||||
const redistributeMemos = useCallback(() => {
|
||||
const prefixHeight = prefixElementRef.current?.offsetHeight || 0;
|
||||
const { distribution: newDistribution } = distributeMemosToColumns(props.memoList, columns, itemHeights, prefixHeight);
|
||||
setDistribution(newDistribution);
|
||||
}, [props.memoList, columns, itemHeights]);
|
||||
|
||||
// Handle height changes from individual memo items
|
||||
const handleHeightChange = useCallback(
|
||||
(memoName: string, height: number) => {
|
||||
setItemHeights((prevHeights) => {
|
||||
const newItemHeights = new Map(prevHeights);
|
||||
newItemHeights.set(memoName, height);
|
||||
|
||||
// Recalculate distribution with new heights
|
||||
const prefixHeight = prefixElementRef.current?.offsetHeight || 0;
|
||||
const { distribution: newDistribution } = distributeMemosToColumns(props.memoList, columns, newItemHeights, prefixHeight);
|
||||
setDistribution(newDistribution);
|
||||
|
||||
return newItemHeights;
|
||||
});
|
||||
},
|
||||
[props.memoList, columns],
|
||||
// Create render context: automatically enable compact mode when multiple columns
|
||||
const renderContext: MemoRenderContext = useMemo(
|
||||
() => ({
|
||||
compact: columns > 1,
|
||||
columns,
|
||||
}),
|
||||
[columns],
|
||||
);
|
||||
|
||||
// Handle window resize and calculate new column count
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const newColumns = calculateColumns();
|
||||
if (newColumns !== columns) {
|
||||
setColumns(newColumns);
|
||||
}
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, [calculateColumns, columns]);
|
||||
|
||||
// Redistribute memos when columns, memo list, or heights change
|
||||
useEffect(() => {
|
||||
redistributeMemos();
|
||||
}, [redistributeMemos]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
|
|
@ -160,22 +46,17 @@ const MasonryView = (props: Props) => {
|
|||
}}
|
||||
>
|
||||
{Array.from({ length: columns }).map((_, columnIndex) => (
|
||||
<div key={columnIndex} className="min-w-0 mx-auto w-full max-w-2xl">
|
||||
{/* Prefix element (like memo editor) goes in first column */}
|
||||
{props.prefixElement && columnIndex === 0 && <div ref={prefixElementRef}>{props.prefixElement}</div>}
|
||||
|
||||
{distribution[columnIndex]?.map((memoIndex) => {
|
||||
const memo = props.memoList[memoIndex];
|
||||
return memo ? (
|
||||
<MemoItem
|
||||
key={`${memo.name}-${memo.displayTime}`}
|
||||
memo={memo}
|
||||
renderer={props.renderer}
|
||||
onHeightChange={handleHeightChange}
|
||||
/>
|
||||
) : null;
|
||||
})}
|
||||
</div>
|
||||
<MasonryColumn
|
||||
key={columnIndex}
|
||||
memoIndices={distribution[columnIndex] || []}
|
||||
memoList={memoList}
|
||||
renderer={renderer}
|
||||
renderContext={renderContext}
|
||||
onHeightChange={handleHeightChange}
|
||||
isFirstColumn={columnIndex === 0}
|
||||
prefixElement={prefixElement}
|
||||
prefixElementRef={prefixElementRef}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* Minimum width required to show more than one column in masonry layout
|
||||
* When viewport is narrower, layout falls back to single column
|
||||
*/
|
||||
export const MINIMUM_MEMO_VIEWPORT_WIDTH = 512;
|
||||
|
||||
/**
|
||||
* Debounce delay for redistribution in milliseconds
|
||||
* Balances responsiveness with performance by batching rapid height changes
|
||||
*/
|
||||
export const REDISTRIBUTION_DEBOUNCE_MS = 100;
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
import { Memo } from "@/types/proto/api/v1/memo_service";
|
||||
import { DistributionResult } from "./types";
|
||||
|
||||
/**
|
||||
* Distributes memos into columns using a height-aware greedy approach.
|
||||
*
|
||||
* Algorithm steps:
|
||||
* 1. Pin editor and first memo to the first column (keep feed stable)
|
||||
* 2. Place remaining memos into the currently shortest column
|
||||
* 3. Break height ties by preferring the column with fewer items
|
||||
*
|
||||
* @param memos - Array of memos to distribute
|
||||
* @param columns - Number of columns to distribute across
|
||||
* @param itemHeights - Map of memo names to their measured heights
|
||||
* @param prefixElementHeight - Height of prefix element (e.g., editor) in first column
|
||||
* @returns Distribution result with memo indices per column and column heights
|
||||
*/
|
||||
export function distributeItemsToColumns(
|
||||
memos: Memo[],
|
||||
columns: number,
|
||||
itemHeights: Map<string, number>,
|
||||
prefixElementHeight: number = 0,
|
||||
): DistributionResult {
|
||||
// Single column mode: all memos in one column
|
||||
if (columns === 1) {
|
||||
const totalHeight = memos.reduce((sum, memo) => sum + (itemHeights.get(memo.name) || 0), prefixElementHeight);
|
||||
return {
|
||||
distribution: [Array.from({ length: memos.length }, (_, i) => i)],
|
||||
columnHeights: [totalHeight],
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize columns and their heights
|
||||
const distribution: number[][] = Array.from({ length: columns }, () => []);
|
||||
const columnHeights: number[] = Array(columns).fill(0);
|
||||
const columnCounts: number[] = Array(columns).fill(0);
|
||||
|
||||
// Add prefix element height to first column
|
||||
if (prefixElementHeight > 0) {
|
||||
columnHeights[0] = prefixElementHeight;
|
||||
}
|
||||
|
||||
let startIndex = 0;
|
||||
|
||||
// Pin the first memo to the first column to keep top-of-feed stable
|
||||
if (memos.length > 0) {
|
||||
const firstMemoHeight = itemHeights.get(memos[0].name) || 0;
|
||||
distribution[0].push(0);
|
||||
columnHeights[0] += firstMemoHeight;
|
||||
columnCounts[0] += 1;
|
||||
startIndex = 1;
|
||||
}
|
||||
|
||||
for (let i = startIndex; i < memos.length; i++) {
|
||||
const memo = memos[i];
|
||||
const height = itemHeights.get(memo.name) || 0;
|
||||
|
||||
// Find column with minimum height
|
||||
const shortestColumnIndex = findShortestColumnIndex(columnHeights, columnCounts);
|
||||
|
||||
distribution[shortestColumnIndex].push(i);
|
||||
columnHeights[shortestColumnIndex] += height;
|
||||
columnCounts[shortestColumnIndex] += 1;
|
||||
}
|
||||
|
||||
return { distribution, columnHeights };
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the index of the column with the minimum height
|
||||
* @param columnHeights - Array of column heights
|
||||
* @param columnCounts - Array of items per column (for tie-breaking)
|
||||
* @returns Index of the shortest column
|
||||
*/
|
||||
function findShortestColumnIndex(columnHeights: number[], columnCounts: number[]): number {
|
||||
let minIndex = 0;
|
||||
let minHeight = columnHeights[0];
|
||||
|
||||
for (let i = 1; i < columnHeights.length; i++) {
|
||||
const currentHeight = columnHeights[i];
|
||||
if (currentHeight < minHeight) {
|
||||
minHeight = currentHeight;
|
||||
minIndex = i;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Tie-breaker: prefer column with fewer items to avoid stacking
|
||||
if (currentHeight === minHeight && columnCounts[i] < columnCounts[minIndex]) {
|
||||
minIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return minIndex;
|
||||
}
|
||||
|
|
@ -1,3 +1,25 @@
|
|||
import MasonryView from "./MasonryView";
|
||||
// Main component
|
||||
export { default } from "./MasonryView";
|
||||
|
||||
export default MasonryView;
|
||||
// Sub-components (exported for testing or advanced usage)
|
||||
export { MasonryColumn } from "./MasonryColumn";
|
||||
export { MasonryItem } from "./MasonryItem";
|
||||
|
||||
// Hooks
|
||||
export { useMasonryLayout } from "./useMasonryLayout";
|
||||
|
||||
// Utilities
|
||||
export { distributeItemsToColumns } from "./distributeItems";
|
||||
|
||||
// Types
|
||||
export type {
|
||||
MasonryViewProps,
|
||||
MasonryItemProps,
|
||||
MasonryColumnProps,
|
||||
DistributionResult,
|
||||
MemoWithHeight,
|
||||
MemoRenderContext,
|
||||
} from "./types";
|
||||
|
||||
// Constants
|
||||
export { MINIMUM_MEMO_VIEWPORT_WIDTH, REDISTRIBUTION_DEBOUNCE_MS } from "./constants";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
import { Memo } from "@/types/proto/api/v1/memo_service";
|
||||
|
||||
/**
|
||||
* Render context passed to memo renderer
|
||||
*/
|
||||
export interface MemoRenderContext {
|
||||
/** Whether to render in compact mode (automatically enabled for multi-column layouts) */
|
||||
compact: boolean;
|
||||
/** Current number of columns in the layout */
|
||||
columns: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the main MasonryView component
|
||||
*/
|
||||
export interface MasonryViewProps {
|
||||
/** List of memos to display in masonry layout */
|
||||
memoList: Memo[];
|
||||
/** Render function for each memo. Second parameter provides layout context. */
|
||||
renderer: (memo: Memo, context?: MemoRenderContext) => JSX.Element;
|
||||
/** Optional element to display at the top of the first column (e.g., memo editor) */
|
||||
prefixElement?: JSX.Element;
|
||||
/** Force single column layout regardless of viewport width */
|
||||
listMode?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for individual MasonryItem component
|
||||
*/
|
||||
export interface MasonryItemProps {
|
||||
/** The memo to render */
|
||||
memo: Memo;
|
||||
/** Render function for the memo */
|
||||
renderer: (memo: Memo, context?: MemoRenderContext) => JSX.Element;
|
||||
/** Render context for the memo */
|
||||
renderContext: MemoRenderContext;
|
||||
/** Callback when item height changes */
|
||||
onHeightChange: (memoName: string, height: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for MasonryColumn component
|
||||
*/
|
||||
export interface MasonryColumnProps {
|
||||
/** Indices of memos in this column */
|
||||
memoIndices: number[];
|
||||
/** Full list of memos */
|
||||
memoList: Memo[];
|
||||
/** Render function for each memo */
|
||||
renderer: (memo: Memo, context?: MemoRenderContext) => JSX.Element;
|
||||
/** Render context for memos */
|
||||
renderContext: MemoRenderContext;
|
||||
/** Callback when item height changes */
|
||||
onHeightChange: (memoName: string, height: number) => void;
|
||||
/** Whether this is the first column (for prefix element) */
|
||||
isFirstColumn: boolean;
|
||||
/** Optional prefix element (only rendered in first column) */
|
||||
prefixElement?: JSX.Element;
|
||||
/** Ref for prefix element height measurement */
|
||||
prefixElementRef?: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of the distribution algorithm
|
||||
*/
|
||||
export interface DistributionResult {
|
||||
/** Array of arrays, where each inner array contains memo indices for that column */
|
||||
distribution: number[][];
|
||||
/** Height of each column after distribution */
|
||||
columnHeights: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Memo item with measured height
|
||||
*/
|
||||
export interface MemoWithHeight {
|
||||
/** Index of the memo in the original list */
|
||||
index: number;
|
||||
/** Measured height in pixels */
|
||||
height: number;
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Memo } from "@/types/proto/api/v1/memo_service";
|
||||
import { MINIMUM_MEMO_VIEWPORT_WIDTH, REDISTRIBUTION_DEBOUNCE_MS } from "./constants";
|
||||
import { distributeItemsToColumns } from "./distributeItems";
|
||||
|
||||
/**
|
||||
* Custom hook for managing masonry layout state and logic
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Calculate optimal number of columns based on viewport width
|
||||
* - Track item heights and trigger redistribution
|
||||
* - Debounce redistribution to prevent excessive reflows
|
||||
* - Handle window resize events
|
||||
*
|
||||
* @param memoList - Array of memos to layout
|
||||
* @param listMode - Force single column mode
|
||||
* @param containerRef - Reference to the container element
|
||||
* @param prefixElementRef - Reference to the prefix element
|
||||
* @returns Layout state and handlers
|
||||
*/
|
||||
export function useMasonryLayout(
|
||||
memoList: Memo[],
|
||||
listMode: boolean,
|
||||
containerRef: React.RefObject<HTMLDivElement>,
|
||||
prefixElementRef: React.RefObject<HTMLDivElement>,
|
||||
) {
|
||||
const [columns, setColumns] = useState(1);
|
||||
const [itemHeights, setItemHeights] = useState<Map<string, number>>(new Map());
|
||||
const [distribution, setDistribution] = useState<number[][]>([[]]);
|
||||
|
||||
const redistributionTimeoutRef = useRef<number | null>(null);
|
||||
const itemHeightsRef = useRef<Map<string, number>>(itemHeights);
|
||||
|
||||
// Keep ref in sync with state
|
||||
useEffect(() => {
|
||||
itemHeightsRef.current = itemHeights;
|
||||
}, [itemHeights]);
|
||||
|
||||
/**
|
||||
* Calculate optimal number of columns based on container width
|
||||
* Uses a scale factor to determine column count
|
||||
*/
|
||||
const calculateColumns = useCallback(() => {
|
||||
if (!containerRef.current || listMode) return 1;
|
||||
|
||||
const containerWidth = containerRef.current.offsetWidth;
|
||||
const scale = containerWidth / MINIMUM_MEMO_VIEWPORT_WIDTH;
|
||||
return scale >= 2 ? Math.round(scale) : 1;
|
||||
}, [containerRef, listMode]);
|
||||
|
||||
/**
|
||||
* Recalculate memo distribution when layout changes
|
||||
*/
|
||||
const redistributeMemos = useCallback(() => {
|
||||
const prefixHeight = prefixElementRef.current?.offsetHeight || 0;
|
||||
setDistribution(() => {
|
||||
const { distribution: newDistribution } = distributeItemsToColumns(memoList, columns, itemHeightsRef.current, prefixHeight);
|
||||
return newDistribution;
|
||||
});
|
||||
}, [memoList, columns, prefixElementRef]);
|
||||
|
||||
/**
|
||||
* Debounced redistribution to batch multiple height changes and prevent excessive reflows
|
||||
*/
|
||||
const debouncedRedistribute = useCallback(
|
||||
(newItemHeights: Map<string, number>) => {
|
||||
// Clear any pending redistribution
|
||||
if (redistributionTimeoutRef.current) {
|
||||
clearTimeout(redistributionTimeoutRef.current);
|
||||
}
|
||||
|
||||
// Schedule new redistribution after debounce delay
|
||||
redistributionTimeoutRef.current = window.setTimeout(() => {
|
||||
const prefixHeight = prefixElementRef.current?.offsetHeight || 0;
|
||||
setDistribution(() => {
|
||||
const { distribution: newDistribution } = distributeItemsToColumns(memoList, columns, newItemHeights, prefixHeight);
|
||||
return newDistribution;
|
||||
});
|
||||
}, REDISTRIBUTION_DEBOUNCE_MS);
|
||||
},
|
||||
[memoList, columns, prefixElementRef],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle height changes from individual memo items
|
||||
*/
|
||||
const handleHeightChange = useCallback(
|
||||
(memoName: string, height: number) => {
|
||||
setItemHeights((prevHeights) => {
|
||||
const newItemHeights = new Map(prevHeights);
|
||||
const previousHeight = prevHeights.get(memoName);
|
||||
|
||||
// Skip if height hasn't changed (avoid unnecessary updates)
|
||||
if (previousHeight === height) {
|
||||
return prevHeights;
|
||||
}
|
||||
|
||||
newItemHeights.set(memoName, height);
|
||||
|
||||
// Use debounced redistribution to batch updates
|
||||
debouncedRedistribute(newItemHeights);
|
||||
|
||||
return newItemHeights;
|
||||
});
|
||||
},
|
||||
[debouncedRedistribute],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle window resize and calculate new column count
|
||||
*/
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const newColumns = calculateColumns();
|
||||
if (newColumns !== columns) {
|
||||
setColumns(newColumns);
|
||||
}
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, [calculateColumns, columns, containerRef]);
|
||||
|
||||
/**
|
||||
* Redistribute memos when columns or memo list change
|
||||
*/
|
||||
useEffect(() => {
|
||||
redistributeMemos();
|
||||
}, [columns, memoList, redistributeMemos]);
|
||||
|
||||
/**
|
||||
* Cleanup timeout on unmount
|
||||
*/
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (redistributionTimeoutRef.current) {
|
||||
clearTimeout(redistributionTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
columns,
|
||||
distribution,
|
||||
handleHeightChange,
|
||||
};
|
||||
}
|
||||
|
|
@ -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 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 confirmDeleteMemo = async () => {
|
||||
await memoStore.deleteMemo(memo.name);
|
||||
toast.success(t("message.deleted-successfully"));
|
||||
if (isInMemoDetailPage) {
|
||||
navigateTo("/");
|
||||
}
|
||||
memoUpdatedCallback();
|
||||
};
|
||||
|
||||
const handleRemoveCompletedTaskListItemsClick = () => {
|
||||
setRemoveTasksDialogOpen(true);
|
||||
};
|
||||
|
||||
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) => {
|
|||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
{/* Delete confirmation dialog */}
|
||||
<ConfirmDialog
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
title={t("memo.delete-confirm")}
|
||||
confirmLabel={t("common.delete")}
|
||||
description={t("memo.delete-confirm-description")}
|
||||
cancelLabel={t("common.cancel")}
|
||||
onConfirm={confirmDeleteMemo}
|
||||
confirmVariant="destructive"
|
||||
/>
|
||||
{/* Remove completed tasks confirmation */}
|
||||
<ConfirmDialog
|
||||
open={removeTasksDialogOpen}
|
||||
onOpenChange={setRemoveTasksDialogOpen}
|
||||
title={t("memo.remove-completed-task-list-items-confirm")}
|
||||
confirmLabel={t("common.confirm")}
|
||||
cancelLabel={t("common.cancel")}
|
||||
onConfirm={confirmRemoveCompletedTaskListItems}
|
||||
confirmVariant="destructive"
|
||||
/>
|
||||
</DropdownMenu>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,24 @@
|
|||
import { createElement } from "react";
|
||||
import { Node } from "@/types/proto/api/v1/markdown_service";
|
||||
import Renderer from "./Renderer";
|
||||
|
||||
interface Props {
|
||||
tagName: string;
|
||||
attributes: { [key: string]: string };
|
||||
children: Node[];
|
||||
isSelfClosing: boolean;
|
||||
}
|
||||
|
||||
const HTMLElement: React.FC<Props> = ({ tagName, attributes }: Props) => {
|
||||
return createElement(tagName, attributes);
|
||||
const HTMLElement: React.FC<Props> = ({ tagName, attributes, children, isSelfClosing }: Props) => {
|
||||
if (isSelfClosing) {
|
||||
return createElement(tagName, attributes);
|
||||
}
|
||||
|
||||
return createElement(
|
||||
tagName,
|
||||
attributes,
|
||||
children.map((child, index) => <Renderer key={`${child.type}-${index}`} index={String(index)} node={child} />),
|
||||
);
|
||||
};
|
||||
|
||||
export default HTMLElement;
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ const LocationSelector = (props: Props) => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Sync lat/lng input values from position
|
||||
const newLat = String(state.position.lat);
|
||||
const newLng = String(state.position.lng);
|
||||
if (state.latInput !== newLat || state.lngInput !== newLng) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ interface Props {
|
|||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
const BrandBanner = observer((props: Props) => {
|
||||
const MemosLogo = observer((props: Props) => {
|
||||
const { collapsed } = props;
|
||||
const workspaceGeneralSetting = workspaceStore.state.generalSetting;
|
||||
const title = workspaceGeneralSetting.customProfile?.title || "Memos";
|
||||
|
|
@ -24,4 +24,4 @@ const BrandBanner = observer((props: Props) => {
|
|||
);
|
||||
});
|
||||
|
||||
export default BrandBanner;
|
||||
export default MemosLogo;
|
||||
|
|
@ -8,8 +8,8 @@ import { cn } from "@/lib/utils";
|
|||
import { Routes } from "@/router";
|
||||
import { userStore } from "@/store";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import BrandBanner from "./BrandBanner";
|
||||
import UserBanner from "./UserBanner";
|
||||
import MemosLogo from "./MemosLogo";
|
||||
import UserMenu from "./UserMenu";
|
||||
|
||||
interface NavLinkItem {
|
||||
id: string;
|
||||
|
|
@ -67,7 +67,7 @@ const Navigation = observer((props: Props) => {
|
|||
<header className={cn("w-full h-full overflow-auto flex flex-col justify-between items-start gap-4 hide-scrollbar", className)}>
|
||||
<div className="w-full px-1 py-1 flex flex-col justify-start items-start space-y-2 overflow-auto overflow-x-hidden hide-scrollbar shrink">
|
||||
<NavLink className="mb-3 cursor-default" to={currentUser ? Routes.ROOT : Routes.EXPLORE}>
|
||||
<BrandBanner collapsed={collapsed} />
|
||||
<MemosLogo collapsed={collapsed} />
|
||||
</NavLink>
|
||||
{navLinks.map((navLink) => (
|
||||
<NavLink
|
||||
|
|
@ -105,7 +105,7 @@ const Navigation = observer((props: Props) => {
|
|||
</div>
|
||||
{currentUser && (
|
||||
<div className={cn("w-full flex flex-col justify-end", props.collapsed ? "items-center" : "items-start pl-3")}>
|
||||
<UserBanner collapsed={collapsed} />
|
||||
<UserMenu collapsed={collapsed} />
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ import { State } from "@/types/proto/api/v1/common";
|
|||
import { Memo } from "@/types/proto/api/v1/memo_service";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import Empty from "../Empty";
|
||||
import MasonryView from "../MasonryView";
|
||||
import MasonryView, { MemoRenderContext } from "../MasonryView";
|
||||
import MemoEditor from "../MemoEditor";
|
||||
|
||||
interface Props {
|
||||
renderer: (memo: Memo) => JSX.Element;
|
||||
renderer: (memo: Memo, context?: MemoRenderContext) => JSX.Element;
|
||||
listSort?: (list: Memo[]) => Memo[];
|
||||
state?: State;
|
||||
orderBy?: string;
|
||||
|
|
|
|||
|
|
@ -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<UserAccessToken[]>([]);
|
||||
const createTokenDialog = useDialog();
|
||||
const [deleteTarget, setDeleteTarget] = useState<UserAccessToken | undefined>(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}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
open={!!deleteTarget}
|
||||
onOpenChange={(open) => !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"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<User | undefined>();
|
||||
const sortedUsers = sortBy(users, "id");
|
||||
const [archiveTarget, setArchiveTarget] = useState<User | undefined>(undefined);
|
||||
const [deleteTarget, setDeleteTarget] = useState<User | undefined>(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 */}
|
||||
<CreateUserDialog open={editDialog.isOpen} onOpenChange={editDialog.setOpen} user={editingUser} onSuccess={fetchUsers} />
|
||||
|
||||
<ConfirmDialog
|
||||
open={!!archiveTarget}
|
||||
onOpenChange={(open) => !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"
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
open={!!deleteTarget}
|
||||
onOpenChange={(open) => !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"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<IdentityProvider[]>([]);
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||
const [editingIdentityProvider, setEditingIdentityProvider] = useState<IdentityProvider | undefined>();
|
||||
const [deleteTarget, setDeleteTarget] = useState<IdentityProvider | undefined>(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}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
open={!!deleteTarget}
|
||||
onOpenChange={(open) => !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"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<UserSession[]>([]);
|
||||
const [revokeTarget, setRevokeTarget] = useState<UserSession | undefined>(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 = () => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
open={!!revokeTarget}
|
||||
onOpenChange={(open) => !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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<UserWebhook[]>([]);
|
||||
const [isCreateWebhookDialogOpen, setIsCreateWebhookDialogOpen] = useState(false);
|
||||
const [deleteTarget, setDeleteTarget] = useState<UserWebhook | undefined>(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}
|
||||
</td>
|
||||
<td className="relative whitespace-nowrap px-3 py-2 text-right text-sm">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
handleDeleteWebhook(webhook);
|
||||
}}
|
||||
>
|
||||
<Button variant="ghost" onClick={() => handleDeleteWebhook(webhook)}>
|
||||
<TrashIcon className="text-destructive w-4 h-auto" />
|
||||
</Button>
|
||||
</td>
|
||||
|
|
@ -118,6 +122,16 @@ const WebhookSection = () => {
|
|||
onOpenChange={setIsCreateWebhookDialogOpen}
|
||||
onSuccess={handleCreateWebhookDialogConfirm}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
open={!!deleteTarget}
|
||||
onOpenChange={(open) => !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"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,37 +1,55 @@
|
|||
import { cloneElement, isValidElement } from "react";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { StatCardProps } from "@/types/statistics";
|
||||
|
||||
export const StatCard = ({ icon, label, count, onClick, tooltip, className }: StatCardProps) => {
|
||||
const content = (
|
||||
<div
|
||||
const iconNode = isValidElement(icon)
|
||||
? cloneElement(icon, {
|
||||
className: cn("h-3.5 w-3.5", icon.props.className),
|
||||
})
|
||||
: icon;
|
||||
|
||||
const countNode = (() => {
|
||||
if (typeof count === "number" || typeof count === "string") {
|
||||
return <span className="text-foreground/80">{count}</span>;
|
||||
}
|
||||
if (isValidElement(count)) {
|
||||
return cloneElement(count, {
|
||||
className: cn("text-foreground/80", count.props.className),
|
||||
});
|
||||
}
|
||||
return <span className="text-foreground/80">{count}</span>;
|
||||
})();
|
||||
|
||||
const button = (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"w-auto border pl-1.5 pr-2 py-0.5 rounded-md flex justify-between items-center",
|
||||
"cursor-pointer hover:bg-muted transition-colors",
|
||||
"inline-flex items-center gap-1 rounded-md border border-border/40 bg-background/80 px-1 pr-2 py-0.5 text-sm leading-none text-muted-foreground transition-colors",
|
||||
"hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring/70 focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
||||
className,
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="w-auto flex justify-start items-center mr-1">
|
||||
{icon}
|
||||
<span className="block text-sm opacity-80">{label}</span>
|
||||
</div>
|
||||
<span className="text-sm truncate opacity-80">{count}</span>
|
||||
</div>
|
||||
<span className="flex h-5 w-5 items-center justify-center text-muted-foreground/80">{iconNode}</span>
|
||||
<span className="truncate text-sm text-foreground/70">{label}</span>
|
||||
<span className="ml-1 flex items-center text-sm">{countNode}</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
if (tooltip) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{content}</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{tooltip}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
if (!tooltip) {
|
||||
return button;
|
||||
}
|
||||
|
||||
return content;
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{tooltip}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -46,10 +46,10 @@ const StatisticsView = observer(() => {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="pt-1 w-full flex flex-row justify-start items-center gap-1 flex-wrap">
|
||||
<div className="pt-2 w-full flex flex-wrap items-center gap-2">
|
||||
{isRootPath && hasPinnedMemos && (
|
||||
<StatCard
|
||||
icon={<BookmarkIcon className="w-4 h-auto mr-1 opacity-70" />}
|
||||
icon={<BookmarkIcon className="opacity-70" />}
|
||||
label={t("common.pinned")}
|
||||
count={userStore.state.currentUserStats!.pinnedMemos.length}
|
||||
onClick={() => handleFilterClick("pinned")}
|
||||
|
|
@ -57,20 +57,14 @@ const StatisticsView = observer(() => {
|
|||
)}
|
||||
|
||||
<StatCard
|
||||
icon={<LinkIcon className="w-4 h-auto mr-1 opacity-70" />}
|
||||
icon={<LinkIcon className="opacity-70" />}
|
||||
label={t("memo.links")}
|
||||
count={memoTypeStats.linkCount}
|
||||
onClick={() => handleFilterClick("property.hasLink")}
|
||||
/>
|
||||
|
||||
<StatCard
|
||||
icon={
|
||||
memoTypeStats.undoCount > 0 ? (
|
||||
<ListTodoIcon className="w-4 h-auto mr-1 opacity-70" />
|
||||
) : (
|
||||
<CheckCircleIcon className="w-4 h-auto mr-1 opacity-70" />
|
||||
)
|
||||
}
|
||||
icon={memoTypeStats.undoCount > 0 ? <ListTodoIcon className="opacity-70" /> : <CheckCircleIcon className="opacity-70" />}
|
||||
label={t("memo.to-do")}
|
||||
count={
|
||||
memoTypeStats.undoCount > 0 ? (
|
||||
|
|
@ -88,7 +82,7 @@ const StatisticsView = observer(() => {
|
|||
/>
|
||||
|
||||
<StatCard
|
||||
icon={<Code2Icon className="w-4 h-auto mr-1 opacity-70" />}
|
||||
icon={<Code2Icon className="opacity-70" />}
|
||||
label={t("memo.code")}
|
||||
count={memoTypeStats.codeCount}
|
||||
onClick={() => handleFilterClick("property.hasCode")}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ interface Props {
|
|||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
const UserBanner = (props: Props) => {
|
||||
const UserMenu = (props: Props) => {
|
||||
const { collapsed } = props;
|
||||
const t = useTranslate();
|
||||
const navigateTo = useNavigateTo();
|
||||
|
|
@ -20,6 +20,21 @@ const UserBanner = (props: Props) => {
|
|||
|
||||
const handleSignOut = async () => {
|
||||
await authServiceClient.deleteSession({});
|
||||
|
||||
// Clear user-specific localStorage items (e.g., drafts)
|
||||
// Preserve app-wide settings like theme
|
||||
const keysToPreserve = ["memos-theme", "tag-view-as-tree", "tag-tree-auto-expand", "viewStore"];
|
||||
const keysToRemove: string[] = [];
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key && !keysToPreserve.includes(key)) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
keysToRemove.forEach((key) => localStorage.removeItem(key));
|
||||
|
||||
window.location.href = Routes.AUTH;
|
||||
};
|
||||
|
||||
|
|
@ -65,4 +80,4 @@ const UserBanner = (props: Props) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default UserBanner;
|
||||
export default UserMenu;
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
"new-password": "新密碼",
|
||||
"repeat-new-password": "再次輸入新密碼",
|
||||
"sign-in-tip": "已經有帳戶了嗎?",
|
||||
"sign-up-tip": "還沒有帳戶?"
|
||||
"sign-up-tip": "還沒有帳戶嗎?"
|
||||
},
|
||||
"common": {
|
||||
"about": "關於",
|
||||
|
|
@ -21,6 +21,7 @@
|
|||
"archive": "封存",
|
||||
"archived": "已封存",
|
||||
"attachments": "附件",
|
||||
"auto-expand": "自動展開",
|
||||
"avatar": "頭像",
|
||||
"basic": "基礎",
|
||||
"beta": "測試版",
|
||||
|
|
@ -114,7 +115,7 @@
|
|||
"add-your-comment-here": "在這裡添加您的評論...",
|
||||
"any-thoughts": "任何想法...",
|
||||
"save": "儲存",
|
||||
"no-changes-detected": "未檢測到變更"
|
||||
"no-changes-detected": "未發現變更"
|
||||
},
|
||||
"filters": {
|
||||
"has-code": "有程式碼",
|
||||
|
|
@ -122,9 +123,9 @@
|
|||
"has-task-list": "有待辦事項"
|
||||
},
|
||||
"inbox": {
|
||||
"failed-to-load": "載入通知失敗",
|
||||
"memo-comment": "{{user}} 對您的 {{memo}} 發表了評論。",
|
||||
"version-update": "新版本 {{version}} 現已推出!"
|
||||
"version-update": "新版本 {{version}} 現已推出!",
|
||||
"failed-to-load": "載入失敗"
|
||||
},
|
||||
"markdown": {
|
||||
"checkbox": "核取方塊",
|
||||
|
|
@ -141,8 +142,9 @@
|
|||
"write-a-comment": "寫下評論"
|
||||
},
|
||||
"copy-link": "複製連結",
|
||||
"count-memos-in-date": "{{date}}的{{count}}條備忘錄",
|
||||
"delete-confirm": "您確定要刪除此備忘錄?(此操作無法恢復)",
|
||||
"count-memos-in-date": "{{date}} 的 {{count}} 條備忘錄",
|
||||
"delete-confirm": "您確定要刪除此備忘錄嗎?",
|
||||
"delete-confirm-description": "此操作無法恢復。附件、連結和引用將一併被移除。",
|
||||
"direction": "排序",
|
||||
"direction-asc": "升序",
|
||||
"direction-desc": "降序",
|
||||
|
|
@ -154,7 +156,7 @@
|
|||
"no-memos": "無備忘錄",
|
||||
"order-by": "排序",
|
||||
"remove-completed-task-list-items": "移除已完成的待辦事項",
|
||||
"remove-completed-task-list-items-confirm": "您確定要移除所有完成的待辦事項嗎?(此操作無法恢復)",
|
||||
"remove-completed-task-list-items-confirm": "您確定要移除所有完成的待辦事項嗎?此操作無法恢復。",
|
||||
"search-placeholder": "搜尋備忘錄",
|
||||
"show-less": "顯示較少",
|
||||
"show-more": "查看更多",
|
||||
|
|
@ -173,12 +175,12 @@
|
|||
"archived-successfully": "封存成功",
|
||||
"change-memo-created-time": "變更備忘錄建立時間",
|
||||
"copied": "已複製",
|
||||
"deleted-successfully": "已成功刪除",
|
||||
"deleted-successfully": "刪除成功",
|
||||
"description-is-required": "說明必填",
|
||||
"failed-to-embed-memo": "嵌入備忘錄失敗",
|
||||
"fill-all": "請填寫所有欄位。",
|
||||
"fill-all-required-fields": "請填寫所有必填欄位",
|
||||
"maximum-upload-size-is": "最大允許檔案上傳大小 {{size}} MiB",
|
||||
"maximum-upload-size-is": "最大允許上傳大小為 {{size}} MiB",
|
||||
"memo-not-found": "未找到備忘錄",
|
||||
"new-password-not-match": "新密碼不一致。",
|
||||
"no-data": "或許尋覓虛空,或者改換選擇之軌跡。",
|
||||
|
|
@ -218,6 +220,8 @@
|
|||
},
|
||||
"delete-resource": "刪除檔案",
|
||||
"delete-selected-resources": "刪除所選的檔案",
|
||||
"delete-all-unused": "刪除所有未使用的檔案",
|
||||
"delete-all-unused-confirm": "您確定要刪除所有未使用的檔案嗎?此操作無法恢復。",
|
||||
"fetching-data": "抓取資料...",
|
||||
"file-drag-drop-prompt": "將您的檔案拖曳到此處以上傳",
|
||||
"linked-amount": "連結數量",
|
||||
|
|
@ -225,7 +229,7 @@
|
|||
"no-resources": "無檔案。",
|
||||
"no-unused-resources": "無未使用的檔案",
|
||||
"reset-link": "重設連結",
|
||||
"reset-link-prompt": "您確定要重設連結嗎?這將導致當前使用的連結失效。(此操作無法恢復)",
|
||||
"reset-link-prompt": "您確定要重設連結嗎?這將導致當前使用的連結失效。此操作無法恢復。",
|
||||
"reset-resource-link": "重設檔案連結",
|
||||
"unused-resources": "未使用的檔案"
|
||||
},
|
||||
|
|
@ -236,8 +240,11 @@
|
|||
"setting": {
|
||||
"access-token-section": {
|
||||
"access-token-copied-to-clipboard": "存取令牌已複製到剪貼簿",
|
||||
"access-token-deletion": "您確定要刪除存取令牌 {{accessToken}} 嗎?(此操作無法恢復)",
|
||||
"access-token-deletion": "您確定要刪除存取令牌 `{{description}}` 嗎?",
|
||||
"access-token-deletion-description": "此操作無法恢復。您需要更新所有使用此令牌的服務以使用新的令牌。",
|
||||
"access-token-deleted": "存取令牌 `{{description}}` 已刪除",
|
||||
"create-dialog": {
|
||||
"access-token-created": "存取令牌 `{{description}}` 已建立",
|
||||
"create-access-token": "建立存取令牌",
|
||||
"created-at": "建立於",
|
||||
"description": "說明",
|
||||
|
|
@ -261,9 +268,11 @@
|
|||
"expires": "過期時間",
|
||||
"current": "當前裝置",
|
||||
"never": "永不過期",
|
||||
"session-revocation": "您確定要撤銷 {{device}} 的工作階段嗎?(此操作無法恢復)",
|
||||
"session-revocation": "您確定要撤銷工作階段 `{{sessionId}}` 嗎?",
|
||||
"session-revocation-description": "您需要在該裝置上重新登入。",
|
||||
"session-revoked": "工作階段撤銷成功",
|
||||
"revoke-session": "撤銷工作階段",
|
||||
"revoke-session-button": "撤銷",
|
||||
"cannot-revoke-current": "無法撤銷當前裝置的工作階段",
|
||||
"no-sessions": "無工作階段"
|
||||
},
|
||||
|
|
@ -273,7 +282,7 @@
|
|||
"export-memos": "導出備忘錄",
|
||||
"nickname-note": "顯示於橫幅",
|
||||
"openapi-reset": "重設 OpenAPI 密鑰(Key)",
|
||||
"openapi-sample-post": "哈囉 來自 {{url}} 的 #memos",
|
||||
"openapi-sample-post": "哈囉,來自 {{url}} 的 #memos",
|
||||
"openapi-title": "OpenAPI",
|
||||
"reset-api": "重設 API",
|
||||
"title": "帳號資訊",
|
||||
|
|
@ -285,10 +294,15 @@
|
|||
"member-section": {
|
||||
"admin": "管理者",
|
||||
"archive-member": "封存使用者",
|
||||
"archive-warning": "您確定要封存 {{username}}?",
|
||||
"archive-warning": "您確定要封存 {{username}} 嗎?",
|
||||
"archive-warning-description": "封存會停用該帳戶。您可於日後恢復或刪除該帳戶。",
|
||||
"archive-success": "{{username}} 封存成功",
|
||||
"restore-success": "{{username}} 恢復成功",
|
||||
"create-a-member": "新增使用者",
|
||||
"delete-member": "刪除使用者",
|
||||
"delete-warning": "您確定要刪除 {{username}}?(此操作無法恢復)",
|
||||
"delete-warning": "您確定要刪除 {{username}} 嗎?",
|
||||
"delete-warning-description": "此操作無法恢復。",
|
||||
"delete-success": "{{username}} 刪除成功",
|
||||
"user": "使用者"
|
||||
},
|
||||
"memo-related": "備忘錄",
|
||||
|
|
@ -308,12 +322,16 @@
|
|||
"default-memo-visibility": "備忘錄預設瀏覽權限",
|
||||
"theme": "主題"
|
||||
},
|
||||
"shortcut": {
|
||||
"delete-confirm": "您確定要刪除快捷篩選 `{{title}}` 嗎?",
|
||||
"delete-success": "快捷篩選 `{{title}}` 刪除成功"
|
||||
},
|
||||
"sso": "SSO",
|
||||
"sso-section": {
|
||||
"authorization-endpoint": "驗證端點(Authorization Endpoint)",
|
||||
"client-id": "客戶端 ID(Client ID)",
|
||||
"client-secret": "客戶端金鑰(Client Secret)",
|
||||
"confirm-delete": "您確定要刪除 \"{{name}}\" 的單點登錄 (SSO) 配置嗎?(此操作無法恢復)",
|
||||
"confirm-delete": "您確定要刪除 `{{name}}` 的單點登錄 (SSO) 配置嗎?此操作無法恢復。",
|
||||
"create-sso": "新增 SSO",
|
||||
"custom": "自訂",
|
||||
"delete-sso": "確認刪除",
|
||||
|
|
@ -366,7 +384,7 @@
|
|||
"url-prefix-placeholder": "自訂網址前綴(選填)",
|
||||
"url-suffix": "網址後綴",
|
||||
"url-suffix-placeholder": "自訂網址後綴(選填)",
|
||||
"warning-text": "您確定要刪除存儲服務 \"{{name}}\" 嗎?(此操作無法恢復)"
|
||||
"warning-text": "您確定要刪除存儲服務 `{{name}}` 嗎?此操作無法恢復。"
|
||||
},
|
||||
"system": "系統",
|
||||
"system-section": {
|
||||
|
|
@ -383,7 +401,7 @@
|
|||
},
|
||||
"disable-markdown-shortcuts-in-editor": "停用編輯器 Markdown 快捷鍵",
|
||||
"disable-password-login": "停用密碼登入",
|
||||
"disable-password-login-final-warning": "如果您知道自己在做什麼,請輸入「CONFIRM」。",
|
||||
"disable-password-login-final-warning": "如果您知道自己在做什麼,請輸入 `CONFIRM`。",
|
||||
"disable-password-login-warning": "所有使用者將無法使用密碼登入。如果設定的身份識別提供者失效,不在資料庫中恢復此設定將無法登入。刪除身分識別提供者時也要特別小心❗",
|
||||
"disable-public-memos": "停用公共備忘錄",
|
||||
"display-with-updated-time": "顯示更新時間",
|
||||
|
|
@ -402,11 +420,17 @@
|
|||
"create-dialog": {
|
||||
"an-easy-to-remember-name": "一個容易記住的名稱",
|
||||
"create-webhook": "建立 Webhook",
|
||||
"create-webhook-success": "Webhook `{{name}}` 已建立",
|
||||
"edit-webhook": "編輯 Webhook",
|
||||
"payload-url": "URL",
|
||||
"title": "標題",
|
||||
"url-example-post-receive": "https://example.com/postreceive"
|
||||
},
|
||||
"delete-dialog": {
|
||||
"delete-webhook-description": "此操作無法恢復。",
|
||||
"delete-webhook-title": "您確定要刪除 webhook `{{name}}` 嗎?",
|
||||
"delete-webhook-success": "Webhook `{{name}}` 刪除成功"
|
||||
},
|
||||
"no-webhooks-found": "尚未建立任何 Webhook。",
|
||||
"title": "Webhook",
|
||||
"url": "網址"
|
||||
|
|
@ -428,13 +452,22 @@
|
|||
"create-tags-guide": "您可以通過輸入`#標籤`來建立標籤。",
|
||||
"delete-confirm": "您確定要刪除此標籤嗎?所有關聯的備忘錄將會被封存",
|
||||
"delete-tag": "刪除標籤",
|
||||
"no-tag-found": "未找到標籤",
|
||||
"delete-success": "標籤刪除成功",
|
||||
"new-name": "新標籤名稱",
|
||||
"no-tag-found": "未找到標籤",
|
||||
"old-name": "舊標籤名稱",
|
||||
"rename-error-empty": "標籤名稱不能為空",
|
||||
"rename-error-repeat": "標籤名稱已存在",
|
||||
"rename-success": "重新命名標籤成功",
|
||||
"rename-tag": "重新命名標籤",
|
||||
"rename-tip": "您的標籤名稱將會被更新"
|
||||
},
|
||||
"tooltip": {
|
||||
"link-memo": "連結備忘錄",
|
||||
"markdown-menu": "Markdown",
|
||||
"select-location": "位置",
|
||||
"select-visibility": "瀏覽權限",
|
||||
"tags": "標籤",
|
||||
"upload-attachment": "上傳附件"
|
||||
}
|
||||
}
|
||||
|
|
@ -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(() => (
|
||||
<>
|
||||
<RouterProvider router={router} />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import dayjs from "dayjs";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMemo } from "react";
|
||||
import { MemoRenderContext } from "@/components/MasonryView";
|
||||
import MemoView from "@/components/MemoView";
|
||||
import PagedMemoList from "@/components/PagedMemoList";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
|
|
@ -27,7 +28,9 @@ const Archived = observer(() => {
|
|||
|
||||
return (
|
||||
<PagedMemoList
|
||||
renderer={(memo: Memo) => <MemoView key={`${memo.name}-${memo.updateTime}`} memo={memo} showVisibility compact />}
|
||||
renderer={(memo: Memo, context?: MemoRenderContext) => (
|
||||
<MemoView key={`${memo.name}-${memo.updateTime}`} memo={memo} showVisibility compact={context?.compact} />
|
||||
)}
|
||||
listSort={(memos: Memo[]) =>
|
||||
memos
|
||||
.filter((memo) => memo.state === State.ARCHIVED)
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<section className="@container w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8">
|
||||
{!md && <MobileHeader />}
|
||||
|
|
@ -138,18 +126,6 @@ const Attachments = observer(() => {
|
|||
<div className="w-full flex flex-row justify-start items-center gap-2">
|
||||
<span className="text-muted-foreground">{t("resource.unused-resources")}</span>
|
||||
<span className="text-muted-foreground opacity-80">({unusedAttachments.length})</span>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="sm" onClick={handleDeleteUnusedAttachments}>
|
||||
<TrashIcon className="w-4 h-auto opacity-60" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Delete all</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
{unusedAttachments.map((attachment) => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import dayjs from "dayjs";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { MemoRenderContext } from "@/components/MasonryView";
|
||||
import MemoView from "@/components/MemoView";
|
||||
import MobileHeader from "@/components/MobileHeader";
|
||||
import PagedMemoList from "@/components/PagedMemoList";
|
||||
|
|
@ -16,7 +17,9 @@ const Explore = observer(() => {
|
|||
{!md && <MobileHeader />}
|
||||
<div className="w-full px-4 sm:px-6">
|
||||
<PagedMemoList
|
||||
renderer={(memo: Memo) => <MemoView key={`${memo.name}-${memo.updateTime}`} memo={memo} showCreator showVisibility compact />}
|
||||
renderer={(memo: Memo, context?: MemoRenderContext) => (
|
||||
<MemoView key={`${memo.name}-${memo.updateTime}`} memo={memo} showCreator showVisibility compact={context?.compact} />
|
||||
)}
|
||||
listSort={(memos: Memo[]) =>
|
||||
memos
|
||||
.filter((memo) => memo.state === State.NORMAL)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import dayjs from "dayjs";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMemo } from "react";
|
||||
import { MemoRenderContext } from "@/components/MasonryView";
|
||||
import MemoView from "@/components/MemoView";
|
||||
import PagedMemoList from "@/components/PagedMemoList";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
|
|
@ -56,7 +57,9 @@ const Home = observer(() => {
|
|||
return (
|
||||
<div className="w-full min-h-full bg-background text-foreground">
|
||||
<PagedMemoList
|
||||
renderer={(memo: Memo) => <MemoView key={`${memo.name}-${memo.displayTime}`} memo={memo} showVisibility showPinned compact />}
|
||||
renderer={(memo: Memo, context?: MemoRenderContext) => (
|
||||
<MemoView key={`${memo.name}-${memo.displayTime}`} memo={memo} showVisibility showPinned compact={context?.compact} />
|
||||
)}
|
||||
listSort={(memos: Memo[]) =>
|
||||
memos
|
||||
.filter((memo) => memo.state === State.NORMAL)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { observer } from "mobx-react-lite";
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { MemoRenderContext } from "@/components/MasonryView";
|
||||
import MemoView from "@/components/MemoView";
|
||||
import PagedMemoList from "@/components/PagedMemoList";
|
||||
import UserAvatar from "@/components/UserAvatar";
|
||||
|
|
@ -89,8 +90,8 @@ const UserProfile = observer(() => {
|
|||
</div>
|
||||
</div>
|
||||
<PagedMemoList
|
||||
renderer={(memo: Memo) => (
|
||||
<MemoView key={`${memo.name}-${memo.displayTime}`} memo={memo} showVisibility showPinned compact />
|
||||
renderer={(memo: Memo, context?: MemoRenderContext) => (
|
||||
<MemoView key={`${memo.name}-${memo.displayTime}`} memo={memo} showVisibility showPinned compact={context?.compact} />
|
||||
)}
|
||||
listSort={(memos: Memo[]) =>
|
||||
memos
|
||||
|
|
|
|||
|
|
@ -503,6 +503,8 @@ export interface SpoilerNode {
|
|||
export interface HTMLElementNode {
|
||||
tagName: string;
|
||||
attributes: { [key: string]: string };
|
||||
children: Node[];
|
||||
isSelfClosing: boolean;
|
||||
}
|
||||
|
||||
export interface HTMLElementNode_AttributesEntry {
|
||||
|
|
@ -3085,7 +3087,7 @@ export const SpoilerNode: MessageFns<SpoilerNode> = {
|
|||
};
|
||||
|
||||
function createBaseHTMLElementNode(): HTMLElementNode {
|
||||
return { tagName: "", attributes: {} };
|
||||
return { tagName: "", attributes: {}, children: [], isSelfClosing: false };
|
||||
}
|
||||
|
||||
export const HTMLElementNode: MessageFns<HTMLElementNode> = {
|
||||
|
|
@ -3096,6 +3098,12 @@ export const HTMLElementNode: MessageFns<HTMLElementNode> = {
|
|||
Object.entries(message.attributes).forEach(([key, value]) => {
|
||||
HTMLElementNode_AttributesEntry.encode({ key: key as any, value }, writer.uint32(18).fork()).join();
|
||||
});
|
||||
for (const v of message.children) {
|
||||
Node.encode(v!, writer.uint32(26).fork()).join();
|
||||
}
|
||||
if (message.isSelfClosing !== false) {
|
||||
writer.uint32(32).bool(message.isSelfClosing);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
|
|
@ -3125,6 +3133,22 @@ export const HTMLElementNode: MessageFns<HTMLElementNode> = {
|
|||
}
|
||||
continue;
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.children.push(Node.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 32) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.isSelfClosing = reader.bool();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
|
|
@ -3149,6 +3173,8 @@ export const HTMLElementNode: MessageFns<HTMLElementNode> = {
|
|||
},
|
||||
{},
|
||||
);
|
||||
message.children = object.children?.map((e) => Node.fromPartial(e)) || [];
|
||||
message.isSelfClosing = object.isSelfClosing ?? false;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue