mirror of
https://github.com/penpot/penpot.git
synced 2025-01-20 05:34:23 -05:00
Merge branch 'develop' of github.com:penpot/penpot into develop
This commit is contained in:
commit
e5f8650994
139 changed files with 16970 additions and 2076 deletions
|
@ -11,6 +11,7 @@
|
|||
### :sparkles: New features
|
||||
|
||||
- New gradients UI with multi-stop support.
|
||||
- Shareable link pointing to an specific board.
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
|
@ -46,6 +47,8 @@
|
|||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem with some texts desynchronization [Taiga #9379](https://tree.taiga.io/project/penpot/issue/9379)
|
||||
- Fix problem with reoder grid layers [#5446](https://github.com/penpot/penpot/issues/5446)
|
||||
- Fix problem with swap component style [#9542](https://tree.taiga.io/project/penpot/issue/9542)
|
||||
|
||||
## 2.3.3
|
||||
|
||||
|
|
244
backend/resources/app/email/comment-mention/en.html
Normal file
244
backend/resources/app/email/comment-mention/en.html
Normal file
|
@ -0,0 +1,244 @@
|
|||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
</title>
|
||||
<!--[if !mso]><!-- -->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" rel="stylesheet" type="text/css">
|
||||
<style type="text/css">
|
||||
@import url(https://fonts.googleapis.com/css?family=Source%20Sans%20Pro);
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.mj-column-px-425 {
|
||||
width: 425px !important;
|
||||
max-width: 425px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
@media only screen and (max-width:480px) {
|
||||
table.mj-full-width-mobile {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
td.mj-full-width-mobile {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="background-color:#E5E5E5;">
|
||||
<div style="background-color:#E5E5E5;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:97px;">
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
|
||||
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
||||
width="97" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
|
||||
Hello {{name|abbreviate:25}}!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
<span style="font-weight:bold;">{{ source-user }}</span> has mentioned you on a comment at "{{ comment-reference }}".</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;font-style:italic;line-height:150%;text-align:left;color:#212426;
|
||||
border-top: 1px solid #bfbfbf; border-bottom: 1px solid #bfbfbf; padding: 32px 0px;">
|
||||
{{ comment-content }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#6911d4#31EFB8" role="presentation"
|
||||
style="border:none;border-radius:8px;cursor:auto;mso-padding-alt:10px 25px;background:#6911d4;"
|
||||
valign="middle">
|
||||
<a href="{{ comment-url }}"
|
||||
style="display:inline-block;background:#6911d4;color:#FFFFFF;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:8px;"
|
||||
target="_blank"> GO TO THE COMMENT </a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
1
backend/resources/app/email/comment-mention/en.subj
Normal file
1
backend/resources/app/email/comment-mention/en.subj
Normal file
|
@ -0,0 +1 @@
|
|||
Mentioned in comment
|
13
backend/resources/app/email/comment-mention/en.txt
Normal file
13
backend/resources/app/email/comment-mention/en.txt
Normal file
|
@ -0,0 +1,13 @@
|
|||
Hello {{name|abbreviate:25}}!
|
||||
|
||||
{{ source-user }} has mentioned you on a comment at "{{ comment-reference }}".
|
||||
|
||||
--
|
||||
|
||||
{{ comment-content }}
|
||||
|
||||
--
|
||||
|
||||
{{ comment-url }}
|
||||
|
||||
The Penpot team.
|
244
backend/resources/app/email/comment-notification/en.html
Normal file
244
backend/resources/app/email/comment-notification/en.html
Normal file
|
@ -0,0 +1,244 @@
|
|||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
</title>
|
||||
<!--[if !mso]><!-- -->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" rel="stylesheet" type="text/css">
|
||||
<style type="text/css">
|
||||
@import url(https://fonts.googleapis.com/css?family=Source%20Sans%20Pro);
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.mj-column-px-425 {
|
||||
width: 425px !important;
|
||||
max-width: 425px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
@media only screen and (max-width:480px) {
|
||||
table.mj-full-width-mobile {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
td.mj-full-width-mobile {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="background-color:#E5E5E5;">
|
||||
<div style="background-color:#E5E5E5;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:97px;">
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
|
||||
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
||||
width="97" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
|
||||
Hello {{name|abbreviate:25}}!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
<span style="font-weight:bold;">{{ source-user }}</span> has commented at "{{ comment-reference }}".</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;font-style:italic;line-height:150%;text-align:left;color:#212426;
|
||||
border-top: 1px solid #bfbfbf; border-bottom: 1px solid #bfbfbf; padding: 32px 0px;">
|
||||
{{ comment-content }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#6911d4#31EFB8" role="presentation"
|
||||
style="border:none;border-radius:8px;cursor:auto;mso-padding-alt:10px 25px;background:#6911d4;"
|
||||
valign="middle">
|
||||
<a href="{{ comment-url }}"
|
||||
style="display:inline-block;background:#6911d4;color:#FFFFFF;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:8px;"
|
||||
target="_blank"> GO TO THE COMMENT </a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
1
backend/resources/app/email/comment-notification/en.subj
Normal file
1
backend/resources/app/email/comment-notification/en.subj
Normal file
|
@ -0,0 +1 @@
|
|||
New comment
|
13
backend/resources/app/email/comment-notification/en.txt
Normal file
13
backend/resources/app/email/comment-notification/en.txt
Normal file
|
@ -0,0 +1,13 @@
|
|||
Hello {{name|abbreviate:25}}!
|
||||
|
||||
{{ source-user }} has commented at "{{ comment-reference }}".
|
||||
|
||||
--
|
||||
|
||||
{{ comment-content }}
|
||||
|
||||
--
|
||||
|
||||
{{ comment-url }}
|
||||
|
||||
The Penpot team.
|
244
backend/resources/app/email/comment-thread/en.html
Normal file
244
backend/resources/app/email/comment-thread/en.html
Normal file
|
@ -0,0 +1,244 @@
|
|||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
</title>
|
||||
<!--[if !mso]><!-- -->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" rel="stylesheet" type="text/css">
|
||||
<style type="text/css">
|
||||
@import url(https://fonts.googleapis.com/css?family=Source%20Sans%20Pro);
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.mj-column-px-425 {
|
||||
width: 425px !important;
|
||||
max-width: 425px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
@media only screen and (max-width:480px) {
|
||||
table.mj-full-width-mobile {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
td.mj-full-width-mobile {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="background-color:#E5E5E5;">
|
||||
<div style="background-color:#E5E5E5;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:97px;">
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
|
||||
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
||||
width="97" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
|
||||
Hello {{name|abbreviate:25}}!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
<span style="font-weight:bold;">{{ source-user }}</span> has created a comment in a thread you've been mentioned at "{{ comment-reference }}".</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;font-style:italic;line-height:150%;text-align:left;color:#212426;
|
||||
border-top: 1px solid #bfbfbf; border-bottom: 1px solid #bfbfbf; padding: 32px 0px;">
|
||||
{{ comment-content }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#6911d4#31EFB8" role="presentation"
|
||||
style="border:none;border-radius:8px;cursor:auto;mso-padding-alt:10px 25px;background:#6911d4;"
|
||||
valign="middle">
|
||||
<a href="{{ comment-url }}"
|
||||
style="display:inline-block;background:#6911d4;color:#FFFFFF;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:8px;"
|
||||
target="_blank"> GO TO THE COMMENT </a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
1
backend/resources/app/email/comment-thread/en.subj
Normal file
1
backend/resources/app/email/comment-thread/en.subj
Normal file
|
@ -0,0 +1 @@
|
|||
New response in comment
|
13
backend/resources/app/email/comment-thread/en.txt
Normal file
13
backend/resources/app/email/comment-thread/en.txt
Normal file
|
@ -0,0 +1,13 @@
|
|||
Hello {{name|abbreviate:25}}!
|
||||
|
||||
{{ source-user }} has created a comment in a thread you've been mentioned at "{{ comment-reference }}".
|
||||
|
||||
--
|
||||
|
||||
{{ comment-content }}
|
||||
|
||||
--
|
||||
|
||||
{{ comment-url }}
|
||||
|
||||
The Penpot team.
|
|
@ -449,6 +449,45 @@
|
|||
:id ::request-team-access
|
||||
:schema schema:request-team-access))
|
||||
|
||||
(def ^:private schema:comment-mention
|
||||
[:map
|
||||
[:name ::sm/text]
|
||||
[:source-user ::sm/text]
|
||||
[:comment-reference ::sm/text]
|
||||
[:comment-content ::sm/text]
|
||||
[:comment-url ::sm/text]])
|
||||
|
||||
(def comment-mention
|
||||
(template-factory
|
||||
:id ::comment-mention
|
||||
:schema schema:comment-mention))
|
||||
|
||||
(def ^:private schema:comment-thread
|
||||
[:map
|
||||
[:name ::sm/text]
|
||||
[:source-user ::sm/text]
|
||||
[:comment-reference ::sm/text]
|
||||
[:comment-content ::sm/text]
|
||||
[:comment-url ::sm/text]])
|
||||
|
||||
(def comment-thread
|
||||
(template-factory
|
||||
:id ::comment-thread
|
||||
:schema schema:comment-thread))
|
||||
|
||||
(def ^:private schema:comment-notification
|
||||
[:map
|
||||
[:name ::sm/text]
|
||||
[:source-user ::sm/text]
|
||||
[:comment-reference ::sm/text]
|
||||
[:comment-content ::sm/text]
|
||||
[:comment-url ::sm/text]])
|
||||
|
||||
(def comment-notification
|
||||
(template-factory
|
||||
:id ::comment-notification
|
||||
:schema schema:comment-notification))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; BOUNCE/COMPLAINS HELPERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
|
|
@ -426,7 +426,10 @@
|
|||
:fn (mg/resource "app/migrations/sql/0134-mod-file-change-table.sql")}
|
||||
|
||||
{:name "0135-mod-team-invitation-table.sql"
|
||||
:fn (mg/resource "app/migrations/sql/0135-mod-team-invitation-table.sql")}])
|
||||
:fn (mg/resource "app/migrations/sql/0135-mod-team-invitation-table.sql")}
|
||||
|
||||
{:name "0136-mod-comments-mentions.sql"
|
||||
:fn (mg/resource "app/migrations/sql/0136-mod-comments-mentions.sql")}])
|
||||
|
||||
(defn apply-migrations!
|
||||
[pool name migrations]
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
ALTER TABLE comment ADD COLUMN mentions uuid[] NULL DEFAULT '{}';
|
||||
|
||||
ALTER TABLE comment_thread ADD COLUMN mentions uuid[] NULL DEFAULT '{}';
|
|
@ -273,7 +273,8 @@
|
|||
(merge {:viewed-tutorial? false
|
||||
:viewed-walkthrough? false
|
||||
:nudge {:big 10 :small 1}
|
||||
:v2-info-shown true})
|
||||
:v2-info-shown true
|
||||
:release-notes-viewed (:main cf/version)})
|
||||
(db/tjson))
|
||||
|
||||
password (or (:password params) "!")
|
||||
|
|
|
@ -6,40 +6,165 @@
|
|||
|
||||
(ns app.rpc.commands.comments
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.uri :as uri]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
[app.email :as eml]
|
||||
[app.features.fdata :as feat.fdata]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.rpc.retry :as rtry]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]))
|
||||
[app.util.time :as dt]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;; --- GENERAL PURPOSE INTERNAL HELPERS
|
||||
|
||||
(def r-mentions-split #"@\[[^\]]*\]\([^\)]*\)")
|
||||
(def r-mentions #"@\[([^\]]*)\]\(([^\)]*)\)")
|
||||
|
||||
(defn- format-comment
|
||||
[{:keys [content]}]
|
||||
(->> (d/interleave-all
|
||||
(str/split content r-mentions-split)
|
||||
(->> (re-seq r-mentions content)
|
||||
(map (fn [[_ user _]] user))))
|
||||
(str/join "")))
|
||||
|
||||
(defn- format-comment-url
|
||||
[{:keys [team-id file-id page-id]}]
|
||||
(str/ffmt "%/#/workspace?%"
|
||||
(cf/get :public-uri)
|
||||
(uri/map->query-string
|
||||
{:file-id file-id
|
||||
:page-id page-id
|
||||
:team-id team-id})))
|
||||
|
||||
(defn- format-comment-ref
|
||||
[{:keys [seqn]} {:keys [file-name page-name]}]
|
||||
(str/ffmt "#%, %, %" seqn file-name page-name))
|
||||
|
||||
(defn get-team-users
|
||||
[conn team-id]
|
||||
(->> (teams/get-users+props conn team-id)
|
||||
(map profile/decode-row)
|
||||
(d/index-by :id)))
|
||||
|
||||
(defn- resolve-profile-name
|
||||
[conn profile-id]
|
||||
(-> (db/get conn :profile {:id profile-id}
|
||||
{::sql/columns [:fullname]})
|
||||
(get :fullname)))
|
||||
|
||||
(defn- notification-email?
|
||||
[profile-id owner-id props]
|
||||
(if (= profile-id owner-id)
|
||||
(not= :none (-> props :notifications :email-comments))
|
||||
(= :all (-> props :notifications :email-comments))))
|
||||
|
||||
(defn- mention-email?
|
||||
[props]
|
||||
(not= :none (-> props :notifications :email-comments)))
|
||||
|
||||
(defn send-comment-emails!
|
||||
[conn {:keys [profile-id team-id] :as params} comment thread]
|
||||
|
||||
(let [team-users (get-team-users conn team-id)
|
||||
source-user (resolve-profile-name conn profile-id)
|
||||
|
||||
comment-reference (format-comment-ref thread params)
|
||||
comment-content (format-comment comment)
|
||||
comment-url (format-comment-url params)
|
||||
|
||||
;; Users mentioned in this comment
|
||||
comment-mentions
|
||||
(-> (:mentions comment)
|
||||
(set/difference #{profile-id}))
|
||||
|
||||
;; Users mentioned in this thread
|
||||
thread-mentions
|
||||
(-> (:mentions thread)
|
||||
;; Remove the mentions in the thread because we're already sending a
|
||||
;; notification
|
||||
(set/difference comment-mentions)
|
||||
(disj profile-id))
|
||||
|
||||
;; All users
|
||||
notificate-users-ids
|
||||
(-> (set (keys team-users))
|
||||
(set/difference comment-mentions)
|
||||
(set/difference thread-mentions)
|
||||
(disj profile-id))]
|
||||
|
||||
(doseq [mention comment-mentions]
|
||||
(let [{:keys [fullname email props]} (get team-users mention)]
|
||||
(when (mention-email? props)
|
||||
(eml/send!
|
||||
{::eml/conn conn
|
||||
::eml/factory eml/comment-mention
|
||||
:to email
|
||||
:name fullname
|
||||
:source-user source-user
|
||||
:comment-reference comment-reference
|
||||
:comment-content comment-content
|
||||
:comment-url comment-url}))))
|
||||
|
||||
;; Send to the thread users
|
||||
(doseq [mention thread-mentions]
|
||||
(let [{:keys [fullname email props]} (get team-users mention)]
|
||||
(when (mention-email? props)
|
||||
(eml/send!
|
||||
{::eml/conn conn
|
||||
::eml/factory eml/comment-thread
|
||||
:to email
|
||||
:name fullname
|
||||
:source-user source-user
|
||||
:comment-reference comment-reference
|
||||
:comment-content comment-content
|
||||
:comment-url comment-url}))))
|
||||
|
||||
;; Send to users with the "all" flag activated
|
||||
(doseq [user-id notificate-users-ids]
|
||||
(let [{:keys [id fullname email props]} (get team-users user-id)]
|
||||
(when (notification-email? id (:owner-id thread) props)
|
||||
(eml/send!
|
||||
{::eml/conn conn
|
||||
::eml/factory eml/comment-notification
|
||||
:to email
|
||||
:name fullname
|
||||
:source-user source-user
|
||||
:comment-reference comment-reference
|
||||
:comment-content comment-content
|
||||
:comment-url comment-url}))))))
|
||||
|
||||
(defn- decode-row
|
||||
[{:keys [participants position] :as row}]
|
||||
[{:keys [participants position mentions] :as row}]
|
||||
(cond-> row
|
||||
(db/pgpoint? position) (assoc :position (db/decode-pgpoint position))
|
||||
(db/pgobject? participants) (assoc :participants (db/decode-transit-pgobject participants))))
|
||||
(db/pgobject? participants) (assoc :participants (db/decode-transit-pgobject participants))
|
||||
(db/pgarray? mentions) (assoc :mentions (db/decode-pgarray mentions #{}))))
|
||||
|
||||
(def xf-decode-row
|
||||
(map decode-row))
|
||||
|
||||
(def ^:privateqpage-name
|
||||
(def ^:private
|
||||
sql:get-file
|
||||
"select f.id, f.modified_at, f.revn, f.features,
|
||||
"select f.id, f.modified_at, f.revn, f.features, f.name,
|
||||
f.project_id, p.team_id, f.data
|
||||
from file as f
|
||||
join project as p on (p.id = f.project_id)
|
||||
|
@ -91,7 +216,7 @@
|
|||
|
||||
(defn upsert-comment-thread-status!
|
||||
([conn profile-id thread-id]
|
||||
(upsert-comment-thread-status! conn profile-id thread-id (dt/now)))
|
||||
(upsert-comment-thread-status! conn profile-id thread-id (dt/in-future "1s")))
|
||||
([conn profile-id thread-id mod-at]
|
||||
(db/exec-one! conn [sql:upsert-comment-thread-status thread-id profile-id mod-at mod-at])))
|
||||
|
||||
|
@ -116,36 +241,38 @@
|
|||
{::doc/added "1.15"
|
||||
::sm/params schema:get-comment-threads}
|
||||
[cfg {:keys [::rpc/profile-id file-id share-id] :as params}]
|
||||
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(get-comment-threads conn profile-id file-id))))
|
||||
|
||||
(def ^:private sql:comment-threads
|
||||
"select distinct on (ct.id)
|
||||
"SELECT DISTINCT ON (ct.id)
|
||||
ct.*,
|
||||
f.name as file_name,
|
||||
f.project_id as project_id,
|
||||
first_value(c.content) over w as content,
|
||||
(select count(1)
|
||||
from comment as c
|
||||
where c.thread_id = ct.id) as count_comments,
|
||||
(select count(1)
|
||||
from comment as c
|
||||
where c.thread_id = ct.id
|
||||
and c.created_at >= coalesce(cts.modified_at, ct.created_at)) as count_unread_comments
|
||||
from comment_thread as ct
|
||||
inner join comment as c on (c.thread_id = ct.id)
|
||||
inner join file as f on (f.id = ct.file_id)
|
||||
left join comment_thread_status as cts
|
||||
on (cts.thread_id = ct.id and
|
||||
cts.profile_id = ?)
|
||||
where ct.file_id = ?
|
||||
window w as (partition by c.thread_id order by c.created_at asc)")
|
||||
p.team_id AS team_id,
|
||||
f.name AS file_name,
|
||||
f.project_id AS project_id,
|
||||
first_value(c.content) OVER w AS content,
|
||||
(SELECT count(1)
|
||||
FROM comment AS c
|
||||
WHERE c.thread_id = ct.id) AS count_comments,
|
||||
(SELECT count(1)
|
||||
FROM comment AS c
|
||||
WHERE c.thread_id = ct.id
|
||||
AND c.created_at >= coalesce(cts.modified_at, ct.created_at)) AS count_unread_comments
|
||||
FROM comment_thread AS ct
|
||||
INNER JOIN comment AS c ON (c.thread_id = ct.id)
|
||||
INNER JOIN file AS f ON (f.id = ct.file_id)
|
||||
INNER JOIN project AS p ON (p.id = f.project_id)
|
||||
LEFT JOIN comment_thread_status AS cts ON (cts.thread_id = ct.id AND cts.profile_id = ?)
|
||||
WINDOW w AS (PARTITION BY c.thread_id ORDER BY c.created_at ASC)")
|
||||
|
||||
(def ^:private sql:comment-threads-by-file-id
|
||||
(str "WITH threads AS (" sql:comment-threads ")"
|
||||
"SELECT * FROM threads WHERE file_id = ?"))
|
||||
|
||||
(defn- get-comment-threads
|
||||
[conn profile-id file-id]
|
||||
(->> (db/exec! conn [sql:comment-threads profile-id file-id])
|
||||
(->> (db/exec! conn [sql:comment-threads-by-file-id profile-id file-id])
|
||||
(into [] xf-decode-row)))
|
||||
|
||||
;; --- COMMAND: Get Unread Comment Threads
|
||||
|
@ -161,41 +288,41 @@
|
|||
{::doc/added "1.15"
|
||||
::sm/params schema:get-unread-comment-threads}
|
||||
[cfg {:keys [::rpc/profile-id team-id] :as params}]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(teams/check-read-permissions! conn profile-id team-id)
|
||||
(get-unread-comment-threads conn profile-id team-id))))
|
||||
(db/run!
|
||||
cfg
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(teams/check-read-permissions! conn profile-id team-id)
|
||||
(get-unread-comment-threads conn profile-id team-id))))
|
||||
|
||||
(def sql:comment-threads-by-team
|
||||
"select distinct on (ct.id)
|
||||
ct.*,
|
||||
f.name as file_name,
|
||||
f.project_id as project_id,
|
||||
first_value(c.content) over w as content,
|
||||
(select count(1)
|
||||
from comment as c
|
||||
where c.thread_id = ct.id) as count_comments,
|
||||
(select count(1)
|
||||
from comment as c
|
||||
where c.thread_id = ct.id
|
||||
and c.created_at >= coalesce(cts.modified_at, ct.created_at)) as count_unread_comments
|
||||
from comment_thread as ct
|
||||
inner join comment as c on (c.thread_id = ct.id)
|
||||
inner join file as f on (f.id = ct.file_id)
|
||||
inner join project as p on (p.id = f.project_id)
|
||||
left join comment_thread_status as cts
|
||||
on (cts.thread_id = ct.id and
|
||||
cts.profile_id = ?)
|
||||
where p.team_id = ?
|
||||
window w as (partition by c.thread_id order by c.created_at asc)")
|
||||
(def sql:unread-all-comment-threads-by-team
|
||||
(str "WITH threads AS (" sql:comment-threads ")"
|
||||
"SELECT * FROM threads WHERE count_unread_comments > 0 AND team_id = ?"))
|
||||
|
||||
(def sql:unread-comment-threads-by-team
|
||||
(str "with threads as (" sql:comment-threads-by-team ")"
|
||||
"select * from threads where count_unread_comments > 0"))
|
||||
;; The partial configuration will retrieve only comments created by the user and
|
||||
;; threads that have a mention to the user.
|
||||
(def sql:unread-partial-comment-threads-by-team
|
||||
(str "WITH threads AS (" sql:comment-threads ")"
|
||||
"SELECT * FROM threads
|
||||
WHERE count_unread_comments > 0
|
||||
AND team_id = ?
|
||||
AND (owner_id = ? OR ? = ANY(mentions))"))
|
||||
|
||||
(defn- get-unread-comment-threads
|
||||
[conn profile-id team-id]
|
||||
(->> (db/exec! conn [sql:unread-comment-threads-by-team profile-id team-id])
|
||||
(into [] xf-decode-row)))
|
||||
(let [profile (-> (db/get conn :profile {:id profile-id})
|
||||
(profile/decode-row))
|
||||
notify (or (-> profile :props :notifications :dashboard-comments) :all)]
|
||||
|
||||
(case notify
|
||||
:all
|
||||
(->> (db/exec! conn [sql:unread-all-comment-threads-by-team profile-id team-id])
|
||||
(into [] xf-decode-row))
|
||||
|
||||
:partial
|
||||
(->> (db/exec! conn [sql:unread-partial-comment-threads-by-team profile-id team-id profile-id profile-id])
|
||||
(into [] xf-decode-row))
|
||||
|
||||
[])))
|
||||
|
||||
;; --- COMMAND: Get Single Comment Thread
|
||||
|
||||
|
@ -212,9 +339,9 @@
|
|||
[cfg {:keys [::rpc/profile-id file-id id share-id] :as params}]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(let [sql (str "with threads as (" sql:comment-threads ")"
|
||||
"select * from threads where id = ?")]
|
||||
(-> (db/exec-one! conn [sql profile-id file-id id])
|
||||
(let [sql (str "WITH threads AS (" sql:comment-threads ")"
|
||||
"SELECT * FROM threads WHERE id = ? AND file_id = ?")]
|
||||
(-> (db/exec-one! conn [sql profile-id id file-id])
|
||||
(decode-row))))))
|
||||
|
||||
;; --- COMMAND: Retrieve Comments
|
||||
|
@ -300,7 +427,8 @@
|
|||
[:content [:string {:max 750}]]
|
||||
[:page-id ::sm/uuid]
|
||||
[:frame-id ::sm/uuid]
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]
|
||||
[:mentions {:optional true} [:vector ::sm/uuid]]])
|
||||
|
||||
(sv/defmethod ::create-comment-thread
|
||||
{::doc/added "1.15"
|
||||
|
@ -308,11 +436,10 @@
|
|||
::rtry/enabled true
|
||||
::rtry/when rtry/conflict-exception?
|
||||
::sm/params schema:create-comment-thread}
|
||||
[cfg {:keys [::rpc/profile-id ::rpc/request-at file-id page-id share-id position content frame-id]}]
|
||||
[cfg {:keys [::rpc/profile-id ::rpc/request-at file-id page-id share-id mentions position content frame-id]}]
|
||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
|
||||
(let [{:keys [team-id project-id page-name]} (get-file cfg file-id page-id)]
|
||||
|
||||
(let [{:keys [team-id project-id page-name name]} (get-file cfg file-id page-id)]
|
||||
(-> cfg
|
||||
(assoc ::quotes/profile-id profile-id)
|
||||
(assoc ::quotes/team-id team-id)
|
||||
|
@ -324,18 +451,23 @@
|
|||
(let [params {:created-at request-at
|
||||
:profile-id profile-id
|
||||
:file-id file-id
|
||||
:file-name name
|
||||
:page-id page-id
|
||||
:page-name page-name
|
||||
:position position
|
||||
:content content
|
||||
:frame-id frame-id}
|
||||
thread (db/tx-run! cfg create-comment-thread params)]
|
||||
:frame-id frame-id
|
||||
:team-id team-id
|
||||
:project-id project-id
|
||||
:mentions mentions}
|
||||
thread (-> (db/tx-run! cfg create-comment-thread params)
|
||||
(decode-row))]
|
||||
|
||||
(vary-meta thread assoc ::audit/props thread))))
|
||||
|
||||
(defn- create-comment-thread
|
||||
[{:keys [::db/conn] :as cfg}
|
||||
{:keys [profile-id file-id page-id page-name created-at position content frame-id]}]
|
||||
{:keys [profile-id file-id page-id page-name created-at position content mentions frame-id] :as params}]
|
||||
|
||||
(let [;; NOTE: we take the next seq number from a separate query
|
||||
;; because we need to lock the file for avoid race conditions
|
||||
|
@ -348,25 +480,29 @@
|
|||
|
||||
seqn (get-next-seqn conn file-id)
|
||||
thread-id (uuid/next)
|
||||
thread (db/insert! conn :comment-thread
|
||||
{:id thread-id
|
||||
:file-id file-id
|
||||
:owner-id profile-id
|
||||
:participants (db/tjson #{profile-id})
|
||||
:page-name page-name
|
||||
:page-id page-id
|
||||
:created-at created-at
|
||||
:modified-at created-at
|
||||
:seqn seqn
|
||||
:position (db/pgpoint position)
|
||||
:frame-id frame-id})
|
||||
comment (db/insert! conn :comment
|
||||
{:id (uuid/next)
|
||||
:thread-id thread-id
|
||||
:owner-id profile-id
|
||||
:created-at created-at
|
||||
:modified-at created-at
|
||||
:content content})]
|
||||
thread (-> (db/insert! conn :comment-thread
|
||||
{:id thread-id
|
||||
:file-id file-id
|
||||
:owner-id profile-id
|
||||
:participants (db/tjson #{profile-id})
|
||||
:page-name page-name
|
||||
:page-id page-id
|
||||
:created-at created-at
|
||||
:modified-at created-at
|
||||
:seqn seqn
|
||||
:position (db/pgpoint position)
|
||||
:frame-id frame-id
|
||||
:mentions (db/encode-pgarray mentions conn "uuid")})
|
||||
(decode-row))
|
||||
comment (-> (db/insert! conn :comment
|
||||
{:id (uuid/next)
|
||||
:thread-id thread-id
|
||||
:owner-id profile-id
|
||||
:created-at created-at
|
||||
:modified-at created-at
|
||||
:mentions (db/encode-pgarray mentions conn "uuid")
|
||||
:content content})
|
||||
(decode-row))]
|
||||
|
||||
;; Make the current thread as read.
|
||||
(upsert-comment-thread-status! conn profile-id thread-id created-at)
|
||||
|
@ -377,8 +513,11 @@
|
|||
{:id file-id}
|
||||
{::db/return-keys false})
|
||||
|
||||
;; Send mentions emails
|
||||
(send-comment-emails! conn params comment thread)
|
||||
|
||||
(-> thread
|
||||
(select-keys [:id :file-id :page-id])
|
||||
(select-keys [:id :file-id :page-id :mentions])
|
||||
(assoc :comment-id (:id comment)))))
|
||||
|
||||
;; --- COMMAND: Update Comment Thread Status
|
||||
|
@ -391,12 +530,12 @@
|
|||
|
||||
(sv/defmethod ::update-comment-thread-status
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:update-comment-thread-status}
|
||||
[cfg {:keys [::rpc/profile-id id share-id]}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(upsert-comment-thread-status! conn profile-id id)))))
|
||||
::sm/params schema:update-comment-thread-status
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id id share-id]}]
|
||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(upsert-comment-thread-status! conn profile-id id)))
|
||||
|
||||
;; --- COMMAND: Update Comment Thread
|
||||
|
||||
|
@ -409,16 +548,15 @@
|
|||
|
||||
(sv/defmethod ::update-comment-thread
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:update-comment-thread}
|
||||
[cfg {:keys [::rpc/profile-id id is-resolved share-id]}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(db/update! conn :comment-thread
|
||||
{:is-resolved is-resolved}
|
||||
{:id id})
|
||||
nil))))
|
||||
|
||||
::sm/params schema:update-comment-thread
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id id is-resolved share-id]}]
|
||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(db/update! conn :comment-thread
|
||||
{:is-resolved is-resolved}
|
||||
{:id id})
|
||||
nil))
|
||||
|
||||
;; --- COMMAND: Add Comment
|
||||
|
||||
|
@ -429,56 +567,74 @@
|
|||
[:map {:title "create-comment"}
|
||||
[:thread-id ::sm/uuid]
|
||||
[:content [:string {:max 250}]]
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]
|
||||
[:mentions {:optional true} [:vector ::sm/uuid]]])
|
||||
|
||||
(sv/defmethod ::create-comment
|
||||
{::doc/added "1.15"
|
||||
::webhooks/event? true
|
||||
::sm/params schema:create-comment}
|
||||
[cfg {:keys [::rpc/profile-id ::rpc/request-at thread-id share-id content]}]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(let [{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id ::sql/for-update true)
|
||||
{:keys [team-id project-id page-name] :as file} (get-file cfg file-id page-id)]
|
||||
::sm/params schema:create-comment
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at thread-id share-id content mentions]}]
|
||||
(let [{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id ::sql/for-update true)
|
||||
{file-name :name :keys [team-id project-id page-name] :as file} (get-file cfg file-id page-id)]
|
||||
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(quotes/check! cfg {::quotes/id ::quotes/comments-per-file
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id
|
||||
::quotes/project-id project-id
|
||||
::quotes/file-id file-id})
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(quotes/check! cfg {::quotes/id ::quotes/comments-per-file
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id
|
||||
::quotes/project-id project-id
|
||||
::quotes/file-id file-id})
|
||||
|
||||
;; Update the page-name cached attribute on comment thread table.
|
||||
(when (not= page-name (:page-name thread))
|
||||
(db/update! conn :comment-thread
|
||||
{:page-name page-name}
|
||||
{:id thread-id}))
|
||||
;; Update the page-name cached attribute on comment thread table.
|
||||
(when (not= page-name (:page-name thread))
|
||||
(db/update! conn :comment-thread
|
||||
{:page-name page-name}
|
||||
{:id thread-id}))
|
||||
|
||||
(let [comment (db/insert! conn :comment
|
||||
{:id (uuid/next)
|
||||
:created-at request-at
|
||||
:modified-at request-at
|
||||
:thread-id thread-id
|
||||
:owner-id profile-id
|
||||
:content content})
|
||||
props {:file-id file-id
|
||||
:share-id nil}]
|
||||
(let [comment (-> (db/insert!
|
||||
conn :comment
|
||||
{:id (uuid/next)
|
||||
:created-at request-at
|
||||
:modified-at request-at
|
||||
:thread-id thread-id
|
||||
:owner-id profile-id
|
||||
:content content
|
||||
:mentions
|
||||
(-> mentions
|
||||
(set)
|
||||
(db/encode-pgarray conn "uuid"))})
|
||||
(decode-row))
|
||||
props {:file-id file-id
|
||||
:share-id nil}]
|
||||
|
||||
;; Update thread modified-at attribute and assoc the current
|
||||
;; profile to the participant set.
|
||||
(db/update! conn :comment-thread
|
||||
{:modified-at request-at
|
||||
:participants (-> (:participants thread #{})
|
||||
(conj profile-id)
|
||||
(db/tjson))}
|
||||
{:id thread-id})
|
||||
;; Update thread modified-at attribute and assoc the current
|
||||
;; profile to the participant set.
|
||||
(db/update! conn :comment-thread
|
||||
{:modified-at request-at
|
||||
:participants (-> (:participants thread #{})
|
||||
(conj profile-id)
|
||||
(db/tjson))
|
||||
:mentions (-> (:mentions thread)
|
||||
(set)
|
||||
(into mentions)
|
||||
(db/encode-pgarray conn "uuid"))}
|
||||
{:id thread-id})
|
||||
|
||||
;; Update the current profile status in relation to the
|
||||
;; current thread.
|
||||
(upsert-comment-thread-status! conn profile-id thread-id request-at)
|
||||
;; Update the current profile status in relation to the
|
||||
;; current thread.
|
||||
(upsert-comment-thread-status! conn profile-id thread-id)
|
||||
|
||||
(vary-meta comment assoc ::audit/props props))))))
|
||||
(let [params {:project-id project-id
|
||||
:profile-id profile-id
|
||||
:team-id team-id
|
||||
:file-id (:file-id thread)
|
||||
:page-id (:page-id thread)
|
||||
:file-name file-name
|
||||
:page-name page-name}]
|
||||
(send-comment-emails! conn params comment thread))
|
||||
|
||||
(vary-meta comment assoc ::audit/props props))))
|
||||
|
||||
;; --- COMMAND: Update Comment
|
||||
|
||||
|
@ -487,35 +643,42 @@
|
|||
[:map {:title "update-comment"}
|
||||
[:id ::sm/uuid]
|
||||
[:content [:string {:max 250}]]
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]
|
||||
[:mentions {:optional true} [:vector ::sm/uuid]]])
|
||||
|
||||
;; TODO Check if there are new mentions, if there are send the new emails.
|
||||
(sv/defmethod ::update-comment
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:update-comment}
|
||||
[cfg {:keys [::rpc/profile-id ::rpc/request-at id share-id content]}]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(let [{:keys [thread-id owner-id] :as comment} (get-comment conn id ::sql/for-update true)
|
||||
{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id ::sql/for-update true)]
|
||||
::sm/params schema:update-comment
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id share-id content mentions]}]
|
||||
(let [{:keys [thread-id owner-id] :as comment} (get-comment conn id ::sql/for-update true)
|
||||
{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id ::sql/for-update true)]
|
||||
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
|
||||
;; Don't allow edit comments to not owners
|
||||
(when-not (= owner-id profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed))
|
||||
;; Don't allow edit comments to not owners
|
||||
(when-not (= owner-id profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed))
|
||||
|
||||
(let [{:keys [page-name]} (get-file cfg file-id page-id)]
|
||||
(db/update! conn :comment
|
||||
{:content content
|
||||
:modified-at request-at}
|
||||
{:id id})
|
||||
(let [{:keys [page-name]} (get-file cfg file-id page-id)]
|
||||
(db/update! conn :comment
|
||||
{:content content
|
||||
:modified-at request-at
|
||||
:mentions (db/encode-pgarray mentions conn "uuid")}
|
||||
{:id id})
|
||||
|
||||
(db/update! conn :comment-thread
|
||||
{:modified-at request-at
|
||||
:page-name page-name}
|
||||
{:id thread-id})
|
||||
nil)))))
|
||||
(db/update! conn :comment-thread
|
||||
{:modified-at request-at
|
||||
:page-name page-name
|
||||
:mentions
|
||||
(-> (:mentions thread)
|
||||
(set)
|
||||
(into mentions)
|
||||
(db/encode-pgarray conn "uuid"))}
|
||||
{:id thread-id})
|
||||
nil)))
|
||||
|
||||
;; --- COMMAND: Delete Comment Thread
|
||||
|
||||
|
@ -527,17 +690,17 @@
|
|||
|
||||
(sv/defmethod ::delete-comment-thread
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:delete-comment-thread}
|
||||
[cfg {:keys [::rpc/profile-id id share-id]}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [{:keys [owner-id file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(when-not (= owner-id profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed))
|
||||
::sm/params schema:delete-comment-thread
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id id share-id]}]
|
||||
(let [{:keys [owner-id file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(when-not (= owner-id profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed))
|
||||
|
||||
(db/delete! conn :comment-thread {:id id})
|
||||
nil))))
|
||||
(db/delete! conn :comment-thread {:id id})
|
||||
nil))
|
||||
|
||||
;; --- COMMAND: Delete comment
|
||||
|
||||
|
@ -549,17 +712,17 @@
|
|||
|
||||
(sv/defmethod ::delete-comment
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:delete-comment}
|
||||
[cfg {:keys [::rpc/profile-id id share-id]}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [{:keys [owner-id thread-id] :as comment} (get-comment conn id ::sql/for-update true)
|
||||
{:keys [file-id] :as thread} (get-comment-thread conn thread-id)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(when-not (= owner-id profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed))
|
||||
(db/delete! conn :comment {:id id})
|
||||
nil))))
|
||||
::sm/params schema:delete-comment
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id id share-id]}]
|
||||
(let [{:keys [owner-id thread-id] :as comment} (get-comment conn id ::sql/for-update true)
|
||||
{:keys [file-id] :as thread} (get-comment-thread conn thread-id)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(when-not (= owner-id profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed))
|
||||
(db/delete! conn :comment {:id id})
|
||||
nil))
|
||||
|
||||
;; --- COMMAND: Update comment thread position
|
||||
|
||||
|
@ -573,17 +736,17 @@
|
|||
|
||||
(sv/defmethod ::update-comment-thread-position
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:update-comment-thread-position}
|
||||
[cfg {:keys [::rpc/profile-id ::rpc/request-at id position frame-id share-id]}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(db/update! conn :comment-thread
|
||||
{:modified-at request-at
|
||||
:position (db/pgpoint position)
|
||||
:frame-id frame-id}
|
||||
{:id (:id thread)})
|
||||
nil))))
|
||||
::sm/params schema:update-comment-thread-position
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id ::rpc/request-at id position frame-id share-id]}]
|
||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(db/update! conn :comment-thread
|
||||
{:modified-at request-at
|
||||
:position (db/pgpoint position)
|
||||
:frame-id frame-id}
|
||||
{:id (:id thread)})
|
||||
nil))
|
||||
|
||||
;; --- COMMAND: Update comment frame
|
||||
|
||||
|
@ -596,13 +759,13 @@
|
|||
|
||||
(sv/defmethod ::update-comment-thread-frame
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:update-comment-thread-frame}
|
||||
[cfg {:keys [::rpc/profile-id ::rpc/request-at id frame-id share-id]}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(db/update! conn :comment-thread
|
||||
{:modified-at request-at
|
||||
:frame-id frame-id}
|
||||
{:id id})
|
||||
nil))))
|
||||
::sm/params schema:update-comment-thread-frame
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id ::rpc/request-at id frame-id share-id]}]
|
||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(db/update! conn :comment-thread
|
||||
{:modified-at request-at
|
||||
:frame-id frame-id}
|
||||
{:id id})
|
||||
nil))
|
||||
|
|
|
@ -402,7 +402,10 @@
|
|||
|
||||
[cfg {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
;; TODO For now we check read permissions instead of write,
|
||||
;; to allow viewer users to update thumbnails. We might
|
||||
;; review this approach on the future.
|
||||
(files/check-read-permissions! conn profile-id file-id)
|
||||
(when-not (db/read-only? conn)
|
||||
(let [media (create-file-thumbnail! cfg params)]
|
||||
{:uri (files/resolve-public-uri (:id media))
|
||||
|
|
|
@ -41,6 +41,12 @@
|
|||
(declare strip-private-attrs)
|
||||
(declare verify-password)
|
||||
|
||||
(def schema:props-notifications
|
||||
[:map {:title "props-notifications"}
|
||||
[:dashboard-comments [::sm/one-of #{:all :partial :none}]]
|
||||
[:email-comments [::sm/one-of #{:all :partial :none}]]
|
||||
[:email-invites [::sm/one-of #{:all :none}]]])
|
||||
|
||||
(def schema:props
|
||||
[:map {:title "ProfileProps"}
|
||||
[:plugins {:optional true} schema:plugin-registry]
|
||||
|
@ -51,7 +57,8 @@
|
|||
[:v2-info-shown {:optional true} ::sm/boolean]
|
||||
[:welcome-file-id {:optional true} [:maybe ::sm/boolean]]
|
||||
[:release-notes-viewed {:optional true}
|
||||
[::sm/text {:max 100}]]])
|
||||
[::sm/text {:max 100}]]
|
||||
[:notifications {:optional true} schema:props-notifications]])
|
||||
|
||||
(def schema:profile
|
||||
[:map {:title "Profile"}
|
||||
|
@ -200,6 +207,44 @@
|
|||
{:id id})
|
||||
nil))
|
||||
|
||||
|
||||
;; --- MUTATION: Update notifications
|
||||
|
||||
(def ^:private
|
||||
schema:update-profile-notifications
|
||||
[:map {:title "update-profile-notifications"}
|
||||
[:dashboard-comments [::sm/one-of #{:all :partial :none}]]
|
||||
[:email-comments [::sm/one-of #{:all :partial :none}]]
|
||||
[:email-invites [::sm/one-of #{:all :none}]]])
|
||||
|
||||
(declare update-notifications!)
|
||||
|
||||
(sv/defmethod ::update-profile-notifications
|
||||
{::doc/added "2.4.0"
|
||||
::sm/params schema:update-profile-notifications
|
||||
::climit/id :auth/global}
|
||||
[cfg {:keys [::rpc/profile-id] :as params}]
|
||||
(db/tx-run! cfg update-notifications! (assoc params :profile-id profile-id)))
|
||||
|
||||
(defn- update-notifications!
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id dashboard-comments email-comments email-invites]}]
|
||||
(let [profile (get-profile conn profile-id)
|
||||
|
||||
notifications
|
||||
{:dashboard-comments dashboard-comments
|
||||
:email-comments email-comments
|
||||
:email-invites email-invites}]
|
||||
|
||||
(db/update!
|
||||
conn :profile
|
||||
{:props
|
||||
(-> (:props profile)
|
||||
(assoc :notifications notifications)
|
||||
(db/tjson))}
|
||||
{:id (:id profile)})
|
||||
|
||||
nil))
|
||||
|
||||
;; --- MUTATION: Update Photo
|
||||
|
||||
(declare upload-photo)
|
||||
|
|
|
@ -286,18 +286,18 @@
|
|||
;; implemented in UI)
|
||||
|
||||
(def sql:team-users
|
||||
"select pf.id, pf.fullname, pf.photo_id
|
||||
"select pf.id, pf.fullname, pf.photo_id, pf.email
|
||||
from profile as pf
|
||||
inner join team_profile_rel as tpr on (tpr.profile_id = pf.id)
|
||||
where tpr.team_id = ?
|
||||
union
|
||||
select pf.id, pf.fullname, pf.photo_id
|
||||
select pf.id, pf.fullname, pf.photo_id, pf.email
|
||||
from profile as pf
|
||||
inner join project_profile_rel as ppr on (ppr.profile_id = pf.id)
|
||||
inner join project as p on (ppr.project_id = p.id)
|
||||
where p.team_id = ?
|
||||
union
|
||||
select pf.id, pf.fullname, pf.photo_id
|
||||
select pf.id, pf.fullname, pf.photo_id, pf.email
|
||||
from profile as pf
|
||||
inner join file_profile_rel as fpr on (fpr.profile_id = pf.id)
|
||||
inner join file as f on (fpr.file_id = f.id)
|
||||
|
@ -308,6 +308,30 @@
|
|||
[conn team-id]
|
||||
(db/exec! conn [sql:team-users team-id team-id team-id]))
|
||||
|
||||
;; Get the users but add the props property
|
||||
(def sql:team-users+props
|
||||
"select pf.id, pf.fullname, pf.photo_id, pf.email, pf.props
|
||||
from profile as pf
|
||||
inner join team_profile_rel as tpr on (tpr.profile_id = pf.id)
|
||||
where tpr.team_id = ?
|
||||
union
|
||||
select pf.id, pf.fullname, pf.photo_id, pf.email, pf.props
|
||||
from profile as pf
|
||||
inner join project_profile_rel as ppr on (ppr.profile_id = pf.id)
|
||||
inner join project as p on (ppr.project_id = p.id)
|
||||
where p.team_id = ?
|
||||
union
|
||||
select pf.id, pf.fullname, pf.photo_id, pf.email, pf.props
|
||||
from profile as pf
|
||||
inner join file_profile_rel as fpr on (fpr.profile_id = pf.id)
|
||||
inner join file as f on (fpr.file_id = f.id)
|
||||
inner join project as p on (f.project_id = p.id)
|
||||
where p.team_id = ?")
|
||||
|
||||
(defn get-users+props
|
||||
[conn team-id]
|
||||
(db/exec! conn [sql:team-users+props team-id team-id team-id]))
|
||||
|
||||
(def sql:get-team-by-file
|
||||
"SELECT t.*
|
||||
FROM team AS t
|
||||
|
|
|
@ -166,23 +166,26 @@
|
|||
;; invited team.
|
||||
(let [props {:team-id (:team-id claims)
|
||||
:role (:role claims)
|
||||
:invitation-id (:id invitation)}
|
||||
:invitation-id (:id invitation)}]
|
||||
|
||||
accept-invitation-event
|
||||
(-> (audit/event-from-rpc-params params)
|
||||
(assoc ::audit/name "accept-team-invitation")
|
||||
(assoc ::audit/props props))
|
||||
(audit/submit!
|
||||
cfg
|
||||
(-> (audit/event-from-rpc-params params)
|
||||
(assoc ::audit/name "accept-team-invitation")
|
||||
(assoc ::audit/props props)))
|
||||
|
||||
accept-invitation-from-event
|
||||
(-> (audit/event-from-rpc-params params)
|
||||
(assoc ::audit/profile-id (:created-by invitation))
|
||||
(assoc ::audit/name "accept-team-invitation-from")
|
||||
(assoc ::audit/props (assoc props
|
||||
:profile-id (:id profile)
|
||||
:email (:email profile))))]
|
||||
|
||||
(audit/submit! cfg accept-invitation-event)
|
||||
(audit/submit! cfg accept-invitation-from-event)
|
||||
;; NOTE: Backward compatibility; old invitations can
|
||||
;; have the `created-by` to be nil; so in this case we
|
||||
;; don't submit this event to the audit-log
|
||||
(when-let [created-by (:created-by invitation)]
|
||||
(audit/submit!
|
||||
cfg
|
||||
(-> (audit/event-from-rpc-params params)
|
||||
(assoc ::audit/profile-id created-by)
|
||||
(assoc ::audit/name "accept-team-invitation-from")
|
||||
(assoc ::audit/props (assoc props
|
||||
:profile-id (:id profile)
|
||||
:email (:email profile))))))
|
||||
|
||||
(accept-invitation cfg claims invitation profile)
|
||||
(assoc claims :state :created))
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.comments :as comments]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.cond :as-alias cond]
|
||||
|
@ -38,10 +37,10 @@
|
|||
team (-> (db/get conn :team {:id (:team-id project)})
|
||||
(teams/decode-row))
|
||||
|
||||
members (into #{} (->> (teams/get-team-members conn (:team-id project))
|
||||
(map :id)))
|
||||
members (teams/get-team-members conn (:team-id project))
|
||||
member-ids (into #{} (map :id) members)
|
||||
|
||||
perms (assoc perms :in-team (contains? members profile-id))
|
||||
perms (assoc perms :in-team (contains? member-ids profile-id))
|
||||
|
||||
_ (-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params))
|
||||
|
@ -55,7 +54,6 @@
|
|||
(update :data select-keys [:id :options :pages :pages-index :components]))
|
||||
|
||||
libs (files/get-file-libraries conn file-id)
|
||||
users (comments/get-file-comments-users conn file-id profile-id)
|
||||
links (->> (db/query conn :share-link {:file-id file-id})
|
||||
(mapv (fn [row]
|
||||
(-> row
|
||||
|
@ -71,7 +69,7 @@
|
|||
{:team-id (:id team)
|
||||
:deleted-at nil})]
|
||||
|
||||
{:users users
|
||||
{:users members
|
||||
:fonts fonts
|
||||
:project project
|
||||
:share-links links
|
||||
|
|
|
@ -177,7 +177,7 @@
|
|||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
(let [[thread :as result] (:result out)]
|
||||
(t/is (= 1 (count result)))))
|
||||
(t/is (= 0 (count result)))))
|
||||
|
||||
(let [data {::th/type :update-comment-thread-status
|
||||
::rpc/profile-id (:id profile-1)
|
||||
|
|
|
@ -653,6 +653,28 @@
|
|||
(into new-elems)
|
||||
(into (drop index coll))))
|
||||
|
||||
(defn interleave-all
|
||||
"Like interleave, but stops when the longest seq is done, instead of the shortest."
|
||||
([] ())
|
||||
([c1] (lazy-seq c1))
|
||||
([c1 c2]
|
||||
(lazy-seq
|
||||
(let [s1 (seq c1) s2 (seq c2)]
|
||||
(cond
|
||||
;; Interleave as it
|
||||
(and s1 s2)
|
||||
(cons (first s1)
|
||||
(cons (first s2)
|
||||
(interleave-all (rest s1) (rest s2))))
|
||||
;; s2 is empty, we return s1
|
||||
s1 s1
|
||||
;; s1 is empty
|
||||
s2 s2))))
|
||||
([c1 c2 & colls]
|
||||
(lazy-seq
|
||||
(let [ss (filter identity (map seq (conj colls c2 c1)))]
|
||||
(c/concat (map first ss) (apply interleave-all (map rest ss)))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Data Parsing / Conversion
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
|
|
@ -304,7 +304,9 @@
|
|||
(->> ids
|
||||
(mapcat #(ctn/get-child-heads objects %))
|
||||
(map :id)))
|
||||
cell (or cell (ctl/get-cell-by-index parent to-index))]
|
||||
|
||||
index-cell-data (when to-index (ctl/get-cell-by-index parent to-index))
|
||||
cell (or cell (and index-cell-data [(:row index-cell-data) (:column index-cell-data)]))]
|
||||
|
||||
(-> changes
|
||||
(pcb/with-page-id page-id)
|
||||
|
|
|
@ -1479,7 +1479,7 @@
|
|||
(defn get-cell-by-index
|
||||
[parent to-index]
|
||||
(let [cells (get-cells parent {:sort? true :remove-empty? true})
|
||||
to-index (- (count cells) to-index)]
|
||||
to-index (- (count cells) to-index 1)]
|
||||
(nth cells to-index nil)))
|
||||
|
||||
(defn add-children-to-index
|
||||
|
|
|
@ -90,6 +90,7 @@ http {
|
|||
proxy_hide_header x-amz-meta-server-side-encryption;
|
||||
proxy_hide_header x-amz-server-side-encryption;
|
||||
proxy_pass $redirect_uri;
|
||||
proxy_ssl_server_name on;
|
||||
|
||||
add_header x-internal-redirect "$redirect_uri";
|
||||
add_header x-cache-control "$redirect_cache_control";
|
||||
|
|
|
@ -92,6 +92,7 @@ http {
|
|||
proxy_hide_header x-amz-request-id;
|
||||
proxy_hide_header x-amz-meta-server-side-encryption;
|
||||
proxy_hide_header x-amz-server-side-encryption;
|
||||
proxy_ssl_server_name on;
|
||||
proxy_pass $redirect_uri;
|
||||
|
||||
add_header x-internal-redirect "$redirect_uri";
|
||||
|
|
|
@ -216,3 +216,9 @@ Success! - Published to example-plugin-penpot.surge.sh
|
|||
```
|
||||
|
||||
5. Done!
|
||||
|
||||
## 3.5. Submitting to Penpot
|
||||
|
||||
To make your finished plugin available in our catalog, submit in on the [plugin submission page](https://penpot.app/penpothub/plugins/create-plugin). Once it becomes available any Penpot user will be able to install and use it.
|
||||
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@ However, we might want to control the aspect of the icons, or limit which icons
|
|||
[{:keys [icon children] :rest props}]
|
||||
(assert (or (nil? icon) (contains? valid-icon-list icon) "expected valid icon id"))
|
||||
[:> "button" props
|
||||
(when icon [:> icon* {:id icon :size "m"}])
|
||||
(when icon [:> icon* {:icon-id icon :size "m"}])
|
||||
children])
|
||||
```
|
||||
|
||||
|
@ -160,7 +160,7 @@ Nested styles for DOM elements that are not instantiated by our component should
|
|||
[{:keys [icon children class] :rest props}]
|
||||
(let [props (mf/spread-props props {:class (stl/css :button)})]
|
||||
[:> "button" props
|
||||
(when icon [:> icon* {:id icon :size "m"}])
|
||||
(when icon [:> icon* {:icon-id icon :size "m"}])
|
||||
[:span {:class (stl/css :label-wrapper)} children]]))
|
||||
|
||||
;; later in code
|
||||
|
|
|
@ -250,11 +250,6 @@ geometric structure. In Penpot there are three types of guides:
|
|||
<h4>Navigate actions</h4>
|
||||
<p>To navigate through the actions press <kbd>Ctrl/⌘</kbd> + <kbd>Z</kbd> to go backwards and <kbd>Ctrl/⌘</kbd> + <kbd>Shift/⇧</kbd> + <kbd>Z</kbd> to go forward.</p>
|
||||
<p>You can also press any item of the actions list to get to this specific state.</p>
|
||||
<figure>
|
||||
<video title="Navigate history" muted="" playsinline="" controls="" width="auto" poster="/img/workspace-basics/history-navigate.webp" height="auto">
|
||||
<source src="/img/workspace-basics/history-navigate.mp4" type="video/mp4">
|
||||
</video>
|
||||
</figure>
|
||||
|
||||
<h2 id="comments">Comments</h2>
|
||||
<p>Comments allow the team to have one priceless conversation getting and providing feedback right over the designs and prototypes.<p>
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
funcool/okulary {:mvn/version "2022.04.11-16"}
|
||||
|
||||
funcool/potok2
|
||||
{:git/tag "v2.1"
|
||||
:git/sha "84c97b9"
|
||||
{:git/tag "v2.2"
|
||||
:git/sha "0f7e15a"
|
||||
:git/url "https://github.com/funcool/potok.git"
|
||||
:exclusions [funcool/beicon2]}
|
||||
|
||||
|
@ -20,8 +20,8 @@
|
|||
:git/url "https://github.com/funcool/beicon.git"}
|
||||
|
||||
funcool/rumext
|
||||
{:git/tag "v2.15"
|
||||
:git/sha "28783a7"
|
||||
{:git/tag "v2.18"
|
||||
:git/sha "b6e8f45"
|
||||
:git/url "https://github.com/funcool/rumext.git"}
|
||||
|
||||
instaparse/instaparse {:mvn/version "1.5.0"}
|
||||
|
|
3392
frontend/externs/main.txt
Normal file
3392
frontend/externs/main.txt
Normal file
File diff suppressed because it is too large
Load diff
1
frontend/externs/worker.txt
Symbolic link
1
frontend/externs/worker.txt
Symbolic link
|
@ -0,0 +1 @@
|
|||
main.txt
|
|
@ -104,8 +104,7 @@ export class ViewerPage extends BaseWebSocketPage {
|
|||
|
||||
async showCommentsThread(number, clickOptions = {}) {
|
||||
await this.page
|
||||
.getByTestId("floating-thread-bubble")
|
||||
.filter({ hasText: number.toString() })
|
||||
.getByTestId(`floating-thread-bubble-${number.toString()}`)
|
||||
.click(clickOptions);
|
||||
}
|
||||
|
||||
|
|
|
@ -19,12 +19,8 @@ test("Comment is shown with scroll and valid position", async ({ page }) => {
|
|||
});
|
||||
await viewer.showComments();
|
||||
await viewer.showCommentsThread(1);
|
||||
await expect(
|
||||
viewer.page.getByRole("textbox", { name: "Reply" }),
|
||||
).toBeVisible();
|
||||
await expect(viewer.page.getByRole("textbox")).toBeVisible();
|
||||
await viewer.showCommentsThread(1);
|
||||
await viewer.showCommentsThread(2);
|
||||
await expect(
|
||||
viewer.page.getByRole("textbox", { name: "Reply" }),
|
||||
).toBeVisible();
|
||||
await expect(viewer.page.getByRole("textbox")).toBeVisible();
|
||||
});
|
||||
|
|
1
frontend/resources/images/icons/at.svg
Normal file
1
frontend/resources/images/icons/at.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="14.499" height="14.507" viewBox="700.734 827.762 14.499 14.507"><path d="M707.948 827.762c-.193 0-.386.007-.578.022-2.311.181-4.574 1.452-5.839 3.982-1.686 3.372-.52 6.821 1.835 8.787 2.354 1.966 5.955 2.497 8.974.233a.566.566 0 1 0-.68-.906c-2.611 1.959-5.563 1.478-7.568-.196-2.004-1.675-3.005-4.495-1.546-7.412 1.458-2.917 4.314-3.808 6.856-3.208 2.543.599 4.698 2.672 4.698 5.936v.667c0 .525-.176.847-.435 1.076-.258.23-.624.357-.998.357s-.741-.127-.999-.357c-.258-.229-.435-.551-.435-1.076v-3.334a.567.567 0 0 0-1.133 0v.215a3.215 3.215 0 0 0-2.1-.781 3.241 3.241 0 0 0-3.233 3.233 3.241 3.241 0 0 0 3.233 3.233 3.23 3.23 0 0 0 2.482-1.168c.122.199.267.377.433.525a2.63 2.63 0 0 0 1.752.643c.626 0 1.259-.206 1.751-.643.492-.437.815-1.115.815-1.923V835c0-3.773-2.586-6.336-5.572-7.04a7.405 7.405 0 0 0-1.713-.198ZM708 832.9c1.167 0 2.1.933 2.1 2.1a2.09 2.09 0 0 1-2.1 2.1 2.09 2.09 0 0 1-2.1-2.1c0-1.167.933-2.1 2.1-2.1Z"/></svg>
|
After Width: | Height: | Size: 982 B |
|
@ -810,28 +810,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.comment-bubbles {
|
||||
@include bodySmallTypography;
|
||||
@include flexCenter;
|
||||
height: $s-32;
|
||||
width: $s-32;
|
||||
border-radius: $br-circle;
|
||||
background-color: var(--comment-bullet-background-color-rest);
|
||||
border: $s-1 solid var(--comment-bullet-border-color-rest);
|
||||
color: var(--comment-bullet-foreground-color-rest);
|
||||
}
|
||||
|
||||
.resolved-comment-bubble {
|
||||
background-color: var(--comment-bullet-background-color-resolved);
|
||||
border: $s-1 solid var(--comment-bullet-border-color-resolved);
|
||||
color: var(--comment-bullet-foreground-color-resolved);
|
||||
}
|
||||
.unread-comment-bubble {
|
||||
background-color: var(--comment-bullet-background-color-unread);
|
||||
border: $s-1 solid var(--comment-bullet-border-color-unread);
|
||||
color: var(--comment-bullet-foreground-color-unread);
|
||||
}
|
||||
|
||||
// SELECTS AND DROPDOWNS
|
||||
.menu-dropdown {
|
||||
@include menuShadow;
|
||||
|
|
|
@ -143,6 +143,11 @@
|
|||
(let [f (obj/get global "externalSessionId")]
|
||||
(when (fn? f) (f))))
|
||||
|
||||
(defn external-context-info
|
||||
[]
|
||||
(let [f (obj/get global "externalContextInfo")]
|
||||
(when (fn? f) (f))))
|
||||
|
||||
;; --- Helper Functions
|
||||
|
||||
(defn ^boolean check-browser? [candidate]
|
||||
|
|
|
@ -39,24 +39,26 @@
|
|||
accepting invitation, or third party auth signup or singin."
|
||||
[{:keys [props] :as profile}]
|
||||
(letfn [(get-redirect-events [teams]
|
||||
(if-let [redirect-href (:login-redirect storage/session)]
|
||||
(binding [storage/*sync* true]
|
||||
(swap! storage/session dissoc :login-redirect)
|
||||
(if (= redirect-href (rt/get-current-href))
|
||||
(rx/of (rt/reload true))
|
||||
(rx/of (rt/nav-raw :href redirect-href))))
|
||||
(if-let [file-id (get props :welcome-file-id)]
|
||||
(rx/of (dcm/go-to-workspace
|
||||
:file-id file-id
|
||||
:team-id (:default-team-id profile))
|
||||
(dp/update-profile-props {:welcome-file-id nil}))
|
||||
(if-let [token (:invitation-token profile)]
|
||||
(rx/of (rt/nav :auth-verify-token {:token token}))
|
||||
(if-let [redirect-href (:login-redirect storage/session)]
|
||||
(binding [storage/*sync* true]
|
||||
(swap! storage/session dissoc :login-redirect)
|
||||
(if (= redirect-href (rt/get-current-href))
|
||||
(rx/of (rt/reload true))
|
||||
(rx/of (rt/nav-raw :href redirect-href))))
|
||||
(if-let [file-id (get props :welcome-file-id)]
|
||||
(rx/of (dcm/go-to-workspace
|
||||
:file-id file-id
|
||||
:team-id (:default-team-id profile))
|
||||
(dp/update-profile-props {:welcome-file-id nil}))
|
||||
|
||||
(let [teams (into #{} (map :id) teams)
|
||||
team-id (dtm/get-last-team-id)
|
||||
team-id (if (and team-id (contains? teams team-id))
|
||||
team-id
|
||||
(:default-team-id profile))]
|
||||
(rx/of (dcm/go-to-dashboard-recent {:team-id team-id}))))))]
|
||||
(let [teams (into #{} (map :id) teams)
|
||||
team-id (dtm/get-last-team-id)
|
||||
team-id (if (and team-id (contains? teams team-id))
|
||||
team-id
|
||||
(:default-team-id profile))]
|
||||
(rx/of (dcm/go-to-dashboard-recent {:team-id team-id})))))))]
|
||||
|
||||
(ptk/reify ::logged-in
|
||||
ev/Event
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
[:file-id ::sm/uuid]
|
||||
[:project-id ::sm/uuid]
|
||||
[:owner-id ::sm/uuid]
|
||||
[:page-name :string]
|
||||
[:page-name {:optional true} :string]
|
||||
[:file-name :string]
|
||||
[:seqn :int]
|
||||
[:content :string]
|
||||
|
@ -55,6 +55,19 @@
|
|||
(declare retrieve-comment-threads)
|
||||
(declare refresh-comment-thread)
|
||||
|
||||
(def r-mentions #"@\[([^\]]*)\]\(([^\)]*)\)")
|
||||
|
||||
(defn extract-mentions
|
||||
"Retrieves the mentions in the content as an array of uuids"
|
||||
[content]
|
||||
(->> (re-seq r-mentions content)
|
||||
(mapv (fn [[_ _ id]] (uuid/uuid id)))))
|
||||
|
||||
(defn update-mentions
|
||||
"Updates the params object with the mentiosn"
|
||||
[{:keys [content] :as props}]
|
||||
(assoc props :mentions (extract-mentions content)))
|
||||
|
||||
(defn created-thread-on-workspace
|
||||
([params]
|
||||
(created-thread-on-workspace params true))
|
||||
|
@ -103,7 +116,9 @@
|
|||
(let [page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state page-id)
|
||||
frame-id (ctst/get-frame-id-by-position objects (:position params))
|
||||
params (assoc params :frame-id frame-id)]
|
||||
params (-> params
|
||||
(update-mentions)
|
||||
(assoc :frame-id frame-id))]
|
||||
(->> (rp/cmd! :create-comment-thread params)
|
||||
(rx/mapcat #(rp/cmd! :get-comment-thread {:file-id (:file-id %) :id (:id %)}))
|
||||
(rx/tap on-thread-created)
|
||||
|
@ -156,7 +171,9 @@
|
|||
(watch [_ state _]
|
||||
(let [share-id (-> state :viewer-local :share-id)
|
||||
frame-id (:frame-id params)
|
||||
params (assoc params :share-id share-id :frame-id frame-id)]
|
||||
params (-> params
|
||||
(update-mentions)
|
||||
(assoc :share-id share-id :frame-id frame-id))]
|
||||
(->> (rp/cmd! :create-comment-thread params)
|
||||
(rx/mapcat #(rp/cmd! :get-comment-thread {:file-id (:file-id %) :id (:id %) :share-id share-id}))
|
||||
(rx/map created-thread-on-viewer)
|
||||
|
@ -228,9 +245,15 @@
|
|||
(watch [_ state _]
|
||||
(let [share-id (-> state :viewer-local :share-id)
|
||||
created (fn [comment state]
|
||||
(update-in state [:comments (:id thread)] assoc (:id comment) comment))]
|
||||
(update-in state [:comments (:id thread)] assoc (:id comment) comment))
|
||||
|
||||
params
|
||||
(-> {:thread-id (:id thread)
|
||||
:content content
|
||||
:share-id share-id}
|
||||
(update-mentions))]
|
||||
(rx/concat
|
||||
(->> (rp/cmd! :create-comment {:thread-id (:id thread) :content content :share-id share-id})
|
||||
(->> (rp/cmd! :create-comment params)
|
||||
(rx/map (fn [comment] (partial created comment)))
|
||||
(rx/catch (fn [{:keys [type code] :as cause}]
|
||||
(if (and (= type :restriction)
|
||||
|
@ -260,8 +283,10 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [file-id (:current-file-id state)
|
||||
share-id (-> state :viewer-local :share-id)]
|
||||
(->> (rp/cmd! :update-comment {:id id :content content :share-id share-id})
|
||||
share-id (-> state :viewer-local :share-id)
|
||||
params (-> {:id id :content content :share-id share-id}
|
||||
(update-mentions))]
|
||||
(->> (rp/cmd! :update-comment params)
|
||||
(rx/catch #(rx/throw {:type :comment-error}))
|
||||
(rx/map #(retrieve-comment-threads file-id)))))))
|
||||
|
||||
|
@ -502,11 +527,11 @@
|
|||
(d/update-in-when [:comments-local :draft] merge data)))))
|
||||
|
||||
(defn toggle-comment-options
|
||||
[comment]
|
||||
[comment-id]
|
||||
(ptk/reify ::toggle-comment-options
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:comments-local :options] #(if (= (:id comment) %) nil (:id comment))))))
|
||||
(update-in state [:comments-local :options] #(if (= comment-id %) nil comment-id)))))
|
||||
|
||||
(defn hide-comment-options
|
||||
[]
|
||||
|
@ -559,7 +584,10 @@
|
|||
(filter (comp not :is-resolved))
|
||||
|
||||
(= :yours mode)
|
||||
(filter #(contains? (:participants %) (:id profile))))))
|
||||
(filter #(contains? (:participants %) (:id profile)))
|
||||
|
||||
(= :mentions mode)
|
||||
(filter #(contains? (set (:mentions %)) (:id profile))))))
|
||||
|
||||
(defn update-comment-thread-frame
|
||||
([thread]
|
||||
|
|
|
@ -307,7 +307,7 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (or team-id (:current-team-id state))]
|
||||
(rx/of (rt/nav :dashboard-libraries {:team-id team-id}))))))
|
||||
(rx/of (rt/nav :dashboard-fonts {:team-id team-id}))))))
|
||||
|
||||
(defn go-to-dashboard-recent
|
||||
[& {:keys [team-id] :as options}]
|
||||
|
@ -367,14 +367,14 @@
|
|||
(watch [_ state _]
|
||||
(let [team-id (or team-id (:current-team-id state))
|
||||
file-id (or file-id (:current-file-id state))
|
||||
;: FIXME: why not :current-page-id
|
||||
page-id (or page-id
|
||||
page-id (or page-id (:current-page-id state)
|
||||
(dm/get-in state [:workspace-data :pages 0]))
|
||||
|
||||
params (-> (rt/get-params state)
|
||||
(assoc :team-id team-id)
|
||||
(assoc :file-id file-id)
|
||||
(assoc :page-id page-id)
|
||||
(assoc :layout layout)
|
||||
(update :layout #(or layout %))
|
||||
(d/without-nils))]
|
||||
(rx/of (rt/nav :workspace params options))))))
|
||||
|
||||
|
|
|
@ -361,7 +361,7 @@
|
|||
;; --- EVENT: delete-file
|
||||
|
||||
(defn file-deleted
|
||||
[_team-id project-id]
|
||||
[project-id]
|
||||
(ptk/reify ::file-deleted
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
|
@ -378,10 +378,9 @@
|
|||
(d/update-when :recent-files dissoc id)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (uuid/uuid (get-in state [:route :path-params :team-id]))]
|
||||
(->> (rp/cmd! :delete-file {:id id})
|
||||
(rx/map #(file-deleted team-id project-id)))))))
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! :delete-file {:id id})
|
||||
(rx/map (partial file-deleted project-id))))))
|
||||
|
||||
;; --- Rename File
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
(:require
|
||||
["ua-parser-js" :as ua]
|
||||
[app.common.data :as d]
|
||||
[app.common.json :as json]
|
||||
[app.common.logging :as l]
|
||||
[app.config :as cf]
|
||||
[app.main.repo :as rp]
|
||||
|
@ -93,6 +94,11 @@
|
|||
data
|
||||
data))
|
||||
|
||||
(defn add-external-context-info
|
||||
[context]
|
||||
(let [external-context-info (json/->clj (cf/external-context-info))]
|
||||
(merge context external-context-info)))
|
||||
|
||||
(defn- process-event-by-proto
|
||||
[event]
|
||||
(let [data (d/deep-merge (-data event) (meta event))
|
||||
|
@ -102,6 +108,7 @@
|
|||
(assoc :event-origin (::origin data))
|
||||
(assoc :event-namespace (namespace type))
|
||||
(assoc :event-symbol ev-name)
|
||||
(add-external-context-info)
|
||||
(d/without-nils))
|
||||
props (-> data d/without-qualified simplify-props)]
|
||||
|
||||
|
@ -119,6 +126,7 @@
|
|||
(let [type (::type data "action")
|
||||
context (-> (::context data)
|
||||
(assoc :event-origin (::origin data))
|
||||
(add-external-context-info)
|
||||
(d/without-nils))
|
||||
props (-> data d/without-qualified simplify-props)]
|
||||
{:type type
|
||||
|
|
|
@ -208,7 +208,6 @@
|
|||
;; Social registered users don't have old-password
|
||||
[:password-old {:optional true} [:maybe :string]]])
|
||||
|
||||
|
||||
(defn update-password
|
||||
[data]
|
||||
(dm/assert!
|
||||
|
@ -233,6 +232,32 @@
|
|||
(rx/empty)))
|
||||
(rx/ignore))))))
|
||||
|
||||
(def ^:private schema:update-notifications
|
||||
[:map {:title "NotificationsForm"}
|
||||
[:dashboard-comments [::sm/one-of #{:all :partial :none}]]
|
||||
[:email-comments [::sm/one-of #{:all :partial :none}]]
|
||||
[:email-invites [::sm/one-of #{:all :none}]]])
|
||||
|
||||
(defn update-notifications
|
||||
[data]
|
||||
(dm/assert!
|
||||
"expected valid parameters"
|
||||
(sm/check schema:update-notifications data))
|
||||
|
||||
(ptk/reify ::update-notifications
|
||||
ev/Event
|
||||
(-data [_] {})
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [{:keys [on-error on-success]
|
||||
:or {on-error identity
|
||||
on-success identity}} (meta data)]
|
||||
(->> (rp/cmd! :update-profile-notifications data)
|
||||
(rx/tap on-success)
|
||||
(rx/catch #(do (on-error %) (rx/empty)))
|
||||
(rx/ignore))))))
|
||||
|
||||
(defn update-profile-props
|
||||
[props]
|
||||
(ptk/reify ::update-profile-props
|
||||
|
|
|
@ -254,6 +254,22 @@
|
|||
(dwsl/initialize-shape-layout)
|
||||
(fetch-libraries file-id))))))
|
||||
|
||||
(defn zoom-to-frame
|
||||
[]
|
||||
(ptk/reify ::zoom-to-frame
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [params (rt/get-params state)
|
||||
board-id (get params :board-id)
|
||||
board-id (cond
|
||||
(vector? board-id) board-id
|
||||
(string? board-id) [board-id])
|
||||
frames-id (->> board-id
|
||||
(map uuid/uuid)
|
||||
(into (d/ordered-set)))]
|
||||
(rx/of (dws/select-shapes frames-id)
|
||||
dwz/zoom-to-selected-shape)))))
|
||||
|
||||
(defn- fetch-bundle
|
||||
"Multi-stage file bundle fetch coordinator"
|
||||
[file-id]
|
||||
|
@ -290,7 +306,6 @@
|
|||
:features features
|
||||
:thumbnails thumbnails})))))
|
||||
(rx/map bundle-fetched)))
|
||||
|
||||
(rx/take-until stopper-s))))))
|
||||
|
||||
(defn initialize-workspace
|
||||
|
@ -334,6 +349,13 @@
|
|||
(rx/take 1)
|
||||
(rx/map #(dwl/go-to-local-component :id component-id))))
|
||||
|
||||
(when (:board-id rparams)
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::workspace-initialized))
|
||||
(rx/observe-on :async)
|
||||
(rx/take 1)
|
||||
(rx/map zoom-to-frame)))
|
||||
|
||||
(->> stream
|
||||
(rx/filter dch/commit?)
|
||||
(rx/map deref)
|
||||
|
@ -1913,6 +1935,13 @@
|
|||
(update [_ state]
|
||||
(assoc-in state [:workspace-global :show-distances?] value))))
|
||||
|
||||
(defn copy-link-to-clipboard
|
||||
[]
|
||||
(ptk/reify ::copy-link-to-clipboard
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(wapi/write-to-clipboard (rt/get-current-href)))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Interactions
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
|
|
@ -151,11 +151,13 @@
|
|||
(pcb/with-page page)
|
||||
(pcb/set-comment-thread-position thread))]
|
||||
|
||||
(rx/merge
|
||||
(rx/of (dch/commit-changes changes))
|
||||
(->> (rp/cmd! :update-comment-thread-position thread)
|
||||
(rx/catch #(rx/throw {:type :update-comment-thread-position}))
|
||||
(rx/ignore))))))))
|
||||
(rx/concat
|
||||
(rx/merge
|
||||
(rx/of (dch/commit-changes changes))
|
||||
(->> (rp/cmd! :update-comment-thread-position thread)
|
||||
(rx/catch #(rx/throw {:type :update-comment-thread-position}))
|
||||
(rx/ignore)))
|
||||
(rx/of (dcmt/refresh-comment-thread thread))))))))
|
||||
|
||||
;; Move comment threads that are inside a frame when that frame is moved"
|
||||
(defmethod ptk/resolve ::move-frame-comment-threads
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.data.workspace.zoom :as dwz]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.router :as rt]
|
||||
[app.main.streams :as ms]
|
||||
[app.main.worker :as uw]
|
||||
[app.util.mouse :as mse]
|
||||
|
@ -138,12 +139,25 @@
|
|||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state page-id)]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state page-id)
|
||||
selected-id (wsh/lookup-selected state)
|
||||
selected (wsh/lookup-shapes state selected-id)
|
||||
frame-ids (map (fn [item] (let [parent (cfh/get-frame objects (:id item))]
|
||||
(:id parent))) selected)
|
||||
params-without-board (-> (rt/get-params state)
|
||||
(dissoc :board-id))
|
||||
params-board (-> (rt/get-params state)
|
||||
(assoc :board-id frame-ids))]
|
||||
|
||||
(rx/of
|
||||
(dwc/expand-all-parents [id] objects)
|
||||
:interrupt
|
||||
::dwsp/interrupt))))))
|
||||
::dwsp/interrupt)
|
||||
|
||||
(if (some #(= % uuid/zero) frame-ids)
|
||||
(rx/of (rt/nav :workspace params-without-board {::rt/replace true}))
|
||||
(rx/of (rt/nav :workspace params-board {::rt/replace true}))))))))
|
||||
|
||||
(defn select-prev-shape
|
||||
([]
|
||||
|
@ -290,8 +304,11 @@
|
|||
([check-modal]
|
||||
(ptk/reify ::deselect-all
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of ::dwsp/interrupt))
|
||||
(watch [_ state _]
|
||||
(let [params-without-board (-> (rt/get-params state)
|
||||
(dissoc :board-id))]
|
||||
(rx/of ::dwsp/interrupt)
|
||||
(rx/of (rt/nav :workspace params-without-board {::rt/replace true}))))
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
|
||||
|
|
|
@ -85,6 +85,11 @@
|
|||
:subsections [:edit]
|
||||
:fn #(st/emit! (dw/copy-selected))}
|
||||
|
||||
:copy-link {:tooltip (ds/meta (ds/alt "C"))
|
||||
:command (ds/c-mod "alt+c")
|
||||
:subsections [:edit]
|
||||
:fn #(st/emit! (dw/copy-link-to-clipboard))}
|
||||
|
||||
:cut {:tooltip (ds/meta "X")
|
||||
:command (ds/c-mod "x")
|
||||
:subsections [:edit]
|
||||
|
|
|
@ -52,6 +52,11 @@
|
|||
(defonce queue
|
||||
(q/create find-request (/ 1000 30)))
|
||||
|
||||
(defn clear-queue!
|
||||
[]
|
||||
(l/dbg :hint "clearing thumbnail queue")
|
||||
(q/clear! queue))
|
||||
|
||||
;; This function first renders the HTML calling `render/render-frame` that
|
||||
;; returns HTML as a string, then we send that data to the iframe rasterizer
|
||||
;; that returns the image as a Blob. Finally we create a URI for that blob.
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
[app.main.data.event :as ev]
|
||||
[app.main.data.persistence :as dwp]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.thumbnails :as th]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.repo :as rp]
|
||||
[app.util.time :as dt]
|
||||
|
@ -100,19 +101,23 @@
|
|||
(rx/concat
|
||||
(rx/of ::dwp/force-persist
|
||||
(dw/remove-layout-flag :document-history))
|
||||
|
||||
;; FIXME: we should abstract this
|
||||
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
|
||||
(rx/filter #(or (nil? %) (= :saved %)))
|
||||
(rx/take 1)
|
||||
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
|
||||
(rx/tap #(th/clear-queue!))
|
||||
(rx/map #(dw/initialize-workspace file-id)))
|
||||
(case origin
|
||||
:version
|
||||
(rx/of (ptk/event ::ev/event {::ev/name "restore-pin-version"}))
|
||||
|
||||
(when-let [name (case origin
|
||||
:version "restore-pin-version"
|
||||
:snapshot "restore-autosave"
|
||||
nil)]
|
||||
(rx/of (ptk/event ::ev/event {::ev/name name}))))))))
|
||||
:snapshot
|
||||
(rx/of (ptk/event ::ev/event {::ev/name "restore-autosave"}))
|
||||
|
||||
:plugin
|
||||
(rx/of (ptk/event ::ev/event {::ev/name "restore-version-plugin"}))
|
||||
|
||||
(rx/empty)))))))
|
||||
|
||||
(defn delete-version
|
||||
[id]
|
||||
|
|
|
@ -267,9 +267,9 @@
|
|||
(def workspace-page-flows
|
||||
(l/derived #(-> % :flows not-empty) workspace-page))
|
||||
|
||||
(defn workspace-page-objects-by-id
|
||||
[page-id]
|
||||
(l/derived #(wsh/lookup-page-objects % page-id) st/state =))
|
||||
(defn workspace-page-object-by-id
|
||||
[page-id shape-id]
|
||||
(l/derived #(wsh/lookup-shape % page-id shape-id) st/state =))
|
||||
|
||||
;; TODO: Looks like using the `=` comparator can be pretty expensive
|
||||
;; on large pages, we are using this for some reason?
|
||||
|
|
|
@ -149,6 +149,7 @@
|
|||
(let [{:keys [data params]} route
|
||||
props (get profile :props)
|
||||
section (get data :name)
|
||||
team (mf/deref refs/team)
|
||||
|
||||
|
||||
show-question-modal?
|
||||
|
@ -166,10 +167,12 @@
|
|||
(and (contains? cf/flags :onboarding)
|
||||
(not (:onboarding-viewed props))
|
||||
(not (contains? props :onboarding-team-id))
|
||||
(contains? props :newsletter-updates))
|
||||
(contains? props :newsletter-updates)
|
||||
(:is-default team))
|
||||
|
||||
show-release-modal?
|
||||
(and (contains? cf/flags :onboarding)
|
||||
(not (contains? cf/flags :hide-release-modal))
|
||||
(:onboarding-viewed props)
|
||||
(not= (:release-notes-viewed props) (:main cf/version))
|
||||
(not= "0.0" (:main cf/version)))]
|
||||
|
@ -191,7 +194,8 @@
|
|||
:settings-password
|
||||
:settings-options
|
||||
:settings-feedback
|
||||
:settings-access-tokens)
|
||||
:settings-access-tokens
|
||||
:settings-notifications)
|
||||
[:? [:& settings-page {:route route}]]
|
||||
|
||||
:debug-icons-preview
|
||||
|
|
|
@ -120,17 +120,10 @@
|
|||
:else
|
||||
(reset! error (tr "errors.generic")))))
|
||||
|
||||
on-success-default
|
||||
(mf/use-fn
|
||||
(fn [data]
|
||||
(when-let [token (:invitation-token data)]
|
||||
(st/emit! (rt/nav :auth-verify-token {:token token})))))
|
||||
|
||||
on-success
|
||||
(fn [data]
|
||||
(if (nil? on-success-callback)
|
||||
(on-success-default data)
|
||||
(on-success-callback)))
|
||||
(when (fn? on-success-callback)
|
||||
(on-success-callback data)))
|
||||
|
||||
on-submit
|
||||
(mf/use-callback
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -6,103 +6,98 @@
|
|||
|
||||
@import "refactor/common-refactor.scss";
|
||||
|
||||
// Comment-thread-group
|
||||
.thread-group {
|
||||
padding: 0 $s-12;
|
||||
cursor: pointer;
|
||||
border-radius: $br-8;
|
||||
padding: $s-8 $s-16;
|
||||
|
||||
&:hover {
|
||||
background: var(--comment-thread-background-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
@include bodySmallTypography;
|
||||
height: $s-32;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: $s-8;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
@include textEllipsis;
|
||||
.grayed-text {
|
||||
color: var(--comment-subtitle-color);
|
||||
}
|
||||
|
||||
.page-name {
|
||||
@include textEllipsis;
|
||||
.location {
|
||||
color: var(--comment-subtitle-color);
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 $s-6 0 $s-4;
|
||||
width: $s-24;
|
||||
height: $s-32;
|
||||
margin-left: $s-6;
|
||||
svg {
|
||||
@extend .button-icon-small;
|
||||
stroke: var(--icon-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.threads {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-24;
|
||||
}
|
||||
|
||||
// Comment-thread
|
||||
.comment {
|
||||
@include bodySmallTypography;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-12;
|
||||
}
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
gap: $s-8;
|
||||
}
|
||||
|
||||
.thread-bubble {
|
||||
@extend .comment-bubbles;
|
||||
&.resolved {
|
||||
@extend .resolved-comment-bubble;
|
||||
}
|
||||
&.unread {
|
||||
@extend .unread-comment-bubble;
|
||||
}
|
||||
.location-icon {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.location-text {
|
||||
@include textEllipsis;
|
||||
}
|
||||
|
||||
.author {
|
||||
@include bodySmallTypography;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $s-8;
|
||||
}
|
||||
|
||||
.author-identity {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.author-fullname {
|
||||
@include textEllipsis;
|
||||
color: var(--comment-title-color);
|
||||
}
|
||||
|
||||
.author-timeago {
|
||||
@include textEllipsis;
|
||||
color: var(--comment-subtitle-color);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
position: relative;
|
||||
height: $s-24;
|
||||
width: $s-24;
|
||||
border-radius: $br-circle;
|
||||
}
|
||||
|
||||
.avatar-lg {
|
||||
height: $s-32;
|
||||
width: $s-32;
|
||||
}
|
||||
|
||||
.avatar-read {
|
||||
border: $s-2 solid var(--color-background-tertiary);
|
||||
}
|
||||
|
||||
.avatar-unread {
|
||||
border: $s-2 solid var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.avatar-solved {
|
||||
border: $s-2 solid var(--color-background-tertiary);
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
border-radius: $br-circle;
|
||||
img {
|
||||
border-radius: $br-circle;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
flex-grow: 1;
|
||||
.fullname {
|
||||
@include textEllipsis;
|
||||
color: var(--comment-title-color);
|
||||
}
|
||||
.timeago {
|
||||
@include textEllipsis;
|
||||
color: var(--comment-subtitle-color);
|
||||
}
|
||||
.avatar-mask {
|
||||
border-radius: $br-circle;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
.avatar-darken {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.cover {
|
||||
@include bodySmallTypography;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-8;
|
||||
padding: $s-20;
|
||||
border-bottom: $s-1 solid var(--color-background-quaternary);
|
||||
}
|
||||
|
||||
.item {
|
||||
@include bodySmallTypography;
|
||||
color: var(--color-foreground-primary);
|
||||
word-wrap: break-word;
|
||||
|
@ -112,119 +107,128 @@
|
|||
}
|
||||
|
||||
.replies {
|
||||
@include bodySmallTypography;
|
||||
display: flex;
|
||||
gap: $s-8;
|
||||
}
|
||||
|
||||
.total-replies {
|
||||
.replies-total {
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
.new-replies {
|
||||
.replies-unread {
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
// Thread-bubble
|
||||
|
||||
.floating-thread-bubble {
|
||||
@extend .comment-bubbles;
|
||||
.floating-preview-wrapper {
|
||||
z-index: $z-index-1;
|
||||
position: absolute;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
transform: translate(calc(-1 * $s-16), calc(-1 * $s-16));
|
||||
|
||||
&.resolved {
|
||||
@extend .resolved-comment-bubble;
|
||||
}
|
||||
&.unread {
|
||||
@extend .unread-comment-bubble;
|
||||
}
|
||||
}
|
||||
|
||||
// thread-content
|
||||
.thread-content {
|
||||
position: absolute;
|
||||
overflow-y: auto;
|
||||
width: $s-284;
|
||||
padding: $s-12;
|
||||
padding-inline-end: $s-8;
|
||||
.floating-preview-bubble {
|
||||
z-index: initial;
|
||||
}
|
||||
|
||||
.floating-preview-displacement {
|
||||
margin-left: calc(-1 * ($s-12 + $s-2));
|
||||
margin-top: calc(-1 * ($s-8 + $s-2));
|
||||
}
|
||||
|
||||
.grabbing {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.floating-thread-wrapper {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-12;
|
||||
width: $s-284;
|
||||
padding: $s-8 $s-12 $s-8 $s-12;
|
||||
pointer-events: auto;
|
||||
user-select: text;
|
||||
border-radius: $br-8;
|
||||
border: $s-2 solid var(--modal-border-color);
|
||||
background-color: var(--comment-modal-background-color);
|
||||
--translate-x: 0%;
|
||||
--translate-y: 0%;
|
||||
transform: translate(var(--translate-x), var(--translate-y));
|
||||
.comments {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-24;
|
||||
&.left {
|
||||
--translate-x: -100%;
|
||||
}
|
||||
&.top {
|
||||
--translate-y: -100%;
|
||||
}
|
||||
}
|
||||
|
||||
.thread-content-left {
|
||||
--translate-x: -100%;
|
||||
}
|
||||
.thread-content-top {
|
||||
--translate-y: -100%;
|
||||
}
|
||||
|
||||
// comment-item
|
||||
|
||||
.comment-container {
|
||||
.floating-thread-header {
|
||||
position: relative;
|
||||
.comment {
|
||||
@include bodySmallTypography;
|
||||
.author {
|
||||
display: flex;
|
||||
gap: $s-8;
|
||||
.avatar {
|
||||
height: $s-32;
|
||||
width: $s-32;
|
||||
border-radius: $br-circle;
|
||||
img {
|
||||
border-radius: $br-circle;
|
||||
}
|
||||
}
|
||||
.name {
|
||||
flex-grow: 1;
|
||||
.fullname {
|
||||
@include textEllipsis;
|
||||
color: var(--comment-title-color);
|
||||
}
|
||||
.timeago {
|
||||
@include textEllipsis;
|
||||
color: var(--comment-subtitle-color);
|
||||
}
|
||||
}
|
||||
.options-resolve-wrapper {
|
||||
@include flexCenter;
|
||||
width: $s-16;
|
||||
height: $s-32;
|
||||
.options-resolve {
|
||||
@extend .checkbox-icon;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.comment-options-dropdown {
|
||||
@extend .dropdown-wrapper;
|
||||
position: absolute;
|
||||
width: fit-content;
|
||||
max-width: $s-200;
|
||||
right: 0;
|
||||
left: unset;
|
||||
.context-menu-option {
|
||||
@extend .dropdown-element-base;
|
||||
}
|
||||
}
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: $s-32;
|
||||
}
|
||||
|
||||
// edit-form & reply-form
|
||||
.floating-thread-header-left {
|
||||
@include bodySmallTypography;
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.edit-form,
|
||||
.reply-form {
|
||||
.floating-thread-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.floating-thread-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-16;
|
||||
overflow-y: auto;
|
||||
padding-bottom: $s-16;
|
||||
}
|
||||
|
||||
.floating-thread-item-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.floating-thread-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-8;
|
||||
@include bodySmallTypography;
|
||||
}
|
||||
|
||||
.checkbox-wrapper {
|
||||
@include flexCenter;
|
||||
width: $s-16;
|
||||
height: $s-24;
|
||||
margin-right: $s-8;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
@extend .checkbox-icon;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
@extend .dropdown-wrapper;
|
||||
position: absolute;
|
||||
width: fit-content;
|
||||
max-width: $s-200;
|
||||
right: $s-32;
|
||||
top: 0;
|
||||
left: unset;
|
||||
}
|
||||
|
||||
.dropdown-menu-option {
|
||||
@extend .dropdown-element-base;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-8;
|
||||
textarea {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
|
@ -232,8 +236,8 @@
|
|||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: $s-260;
|
||||
margin-bottom: $s-8;
|
||||
padding: $s-8;
|
||||
margin-top: $s-4;
|
||||
color: var(--input-foreground-color-active);
|
||||
resize: vertical;
|
||||
&:focus {
|
||||
|
@ -241,21 +245,119 @@
|
|||
outline: none;
|
||||
}
|
||||
}
|
||||
.buttons-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: $s-4;
|
||||
.post-btn {
|
||||
@extend .button-primary;
|
||||
height: $s-32;
|
||||
width: $s-92;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.cancel-btn {
|
||||
@extend .button-secondary;
|
||||
height: $s-32;
|
||||
width: $s-92;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.form-buttons-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
justify-content: flex-end;
|
||||
gap: $s-8;
|
||||
}
|
||||
|
||||
.open-mentions-button {
|
||||
cursor: pointer;
|
||||
stroke: none;
|
||||
fill: var(--color-foreground-secondary);
|
||||
|
||||
&.is-toggled {
|
||||
fill: var(--color-accent-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.comments-mentions-choice {
|
||||
background: var(--color-background-tertiary);
|
||||
border-radius: $s-8;
|
||||
border: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
left: calc(-1 * $s-2);
|
||||
margin-top: $s-8;
|
||||
overflow: hidden;
|
||||
padding: $s-2;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
width: calc(100% + $s-4);
|
||||
}
|
||||
|
||||
.comments-mentions-entry {
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"avatar name"
|
||||
"avatar email";
|
||||
grid-template-columns: $s-32 1fr;
|
||||
column-gap: $s-8;
|
||||
margin: $s-4 $s-8;
|
||||
padding: 0 $s-4;
|
||||
border-radius: $br-8;
|
||||
border: $s-1 solid transparent;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-background-quaternary);
|
||||
}
|
||||
|
||||
.comments-mentions-avatar {
|
||||
grid-area: avatar;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.comments-mentions-name {
|
||||
grid-area: name;
|
||||
font-size: $fs-12;
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.comments-mentions-email {
|
||||
grid-area: email;
|
||||
font-size: $fs-12;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
border: 1px solid var(--color-accent-primary-muted);
|
||||
background: var(--color-background-quaternary);
|
||||
}
|
||||
}
|
||||
|
||||
.comment-input {
|
||||
@include bodySmallTypography;
|
||||
white-space: pre;
|
||||
background: var(--input-background-color);
|
||||
border-radius: $br-8;
|
||||
border: $s-1 solid var(--input-border-color);
|
||||
color: var(--input-foreground-color);
|
||||
height: $s-36;
|
||||
margin-bottom: $s-8;
|
||||
max-width: $s-260;
|
||||
overflow-y: auto;
|
||||
padding: $s-8;
|
||||
resize: vertical;
|
||||
width: 100%;
|
||||
|
||||
&:focus {
|
||||
border: $s-1 solid var(--input-border-color-active);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-type="mention"] {
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
[data-type="text"] {
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: var(--placeholder);
|
||||
}
|
||||
}
|
||||
|
||||
.comment-mention {
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.comments-mentions-empty {
|
||||
font-size: $fs-12;
|
||||
color: var(--color-foreground-secondary);
|
||||
padding: $s-6 $s-8;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
[app.common.data.macros :as dm]
|
||||
[app.common.schema :as sm]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.ui.components.dropdown :refer [dropdown']]
|
||||
[app.main.ui.components.dropdown :refer [dropdown-content*]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
|
@ -52,8 +52,6 @@
|
|||
(sm/lazy-validator schema:option))
|
||||
|
||||
(mf/defc context-menu*
|
||||
{::mf/props :obj}
|
||||
|
||||
[{:keys [show on-close options selectable selected
|
||||
top left fixed min-width origin width]
|
||||
:as props}]
|
||||
|
@ -90,7 +88,7 @@
|
|||
(on-close)))
|
||||
|
||||
props
|
||||
(mf/spread props :on-close on-local-close)
|
||||
(mf/spread-props props {:on-close on-local-close})
|
||||
|
||||
ids
|
||||
(mf/with-memo [levels]
|
||||
|
@ -221,7 +219,7 @@
|
|||
#(dom/focus! (dom/get-element (first ids)))))
|
||||
|
||||
(when (and show (some? levels))
|
||||
[:> dropdown' props
|
||||
[:> dropdown-content* props
|
||||
(let [level (peek levels)
|
||||
options (:options level)
|
||||
parent (:parent level)]
|
||||
|
|
|
@ -12,17 +12,13 @@
|
|||
[app.util.keyboard :as kbd]
|
||||
[app.util.timers :as tm]
|
||||
[goog.events :as events]
|
||||
[goog.object :as gobj]
|
||||
[rumext.v2 :as mf])
|
||||
(:import goog.events.EventType))
|
||||
|
||||
(mf/defc dropdown'
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [children (gobj/get props "children")
|
||||
on-close (gobj/get props "on-close")
|
||||
container-ref (gobj/get props "container")
|
||||
listening-ref (mf/use-ref nil)
|
||||
(mf/defc dropdown-content*
|
||||
[{:keys [children on-close container]}]
|
||||
(let [listening-ref (mf/use-ref nil)
|
||||
container-ref container
|
||||
|
||||
on-click
|
||||
(fn [event]
|
||||
|
@ -57,10 +53,13 @@
|
|||
children))
|
||||
|
||||
(mf/defc dropdown
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(assert (fn? (gobj/get props "on-close")) "missing `on-close` prop")
|
||||
(assert (boolean? (gobj/get props "show")) "missing `show` prop")
|
||||
{::mf/props :obj}
|
||||
[{:keys [on-close show children container]}]
|
||||
(assert (fn? on-close) "missing `on-close` prop")
|
||||
(assert (boolean? show) "missing `show` prop")
|
||||
|
||||
(when (gobj/get props "show")
|
||||
(mf/element dropdown' props)))
|
||||
(when ^boolean show
|
||||
[:> dropdown-content*
|
||||
{:on-close on-close
|
||||
:container container
|
||||
:children children}]))
|
||||
|
|
|
@ -87,10 +87,16 @@
|
|||
(dom/blur! input))))
|
||||
|
||||
context-value
|
||||
(mf/spread props
|
||||
:on-change on-change'
|
||||
:encode-fn encode-fn
|
||||
:decode-fn decode-fn)]
|
||||
(mf/spread-object props
|
||||
;; We pass a special metadata for disable
|
||||
;; key casing transformation in this
|
||||
;; concrete case, because this component
|
||||
;; uses legacy mode and props are in
|
||||
;; kebab-case style
|
||||
^{::mf/transform false}
|
||||
{:on-change on-change'
|
||||
:encode-fn encode-fn
|
||||
:decode-fn decode-fn})]
|
||||
|
||||
[:& (mf/provider context) {:value context-value}
|
||||
[:div {:class (dm/str class " " (stl/css :radio-btn-wrapper))
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
[:*
|
||||
[:div {:ref ref :class (stl/css :reorder)}
|
||||
[:> icon*
|
||||
{:id ic/reorder
|
||||
{:icon-id ic/reorder
|
||||
:class (stl/css :reorder-icon)
|
||||
:aria-hidden true}]]
|
||||
[:hr {:class (stl/css :reorder-separator-top)}]
|
||||
|
|
|
@ -14,25 +14,18 @@
|
|||
[app.main.store :as st]
|
||||
[app.main.ui.comments :as cmt]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[potok.v2.core :as ptk]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
|
||||
(def ^:private close-icon
|
||||
(i/icon-xref :close (stl/css :close-icon)))
|
||||
|
||||
(def ^:private comments-icon-svg
|
||||
(i/icon-xref :comments (stl/css :comments-icon)))
|
||||
|
||||
|
||||
(def ^:private comments-icon-small
|
||||
(i/icon-xref :comments (stl/css :comments-icon-small)))
|
||||
|
||||
(mf/defc comments-icon
|
||||
[{:keys [profile show? on-show-comments]}]
|
||||
(mf/defc comments-icon*
|
||||
{::mf/props :obj}
|
||||
[{:keys [profile on-show-comments]}]
|
||||
|
||||
(let [threads-map (mf/deref refs/comment-threads)
|
||||
|
||||
|
@ -41,24 +34,18 @@
|
|||
(sort-by :modified-at)
|
||||
(reverse)
|
||||
(dcm/apply-filters {} profile)
|
||||
(dcm/group-threads-by-file-and-page))
|
||||
|
||||
handle-keydown
|
||||
(mf/use-callback
|
||||
(mf/deps on-show-comments)
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-show-comments event))))]
|
||||
(dcm/group-threads-by-file-and-page))]
|
||||
|
||||
[:div {:class (stl/css :dashboard-comments-section)}
|
||||
[:button {:tab-index "0"
|
||||
:on-click on-show-comments
|
||||
:on-key-down handle-keydown
|
||||
:data-testid "open-comments"
|
||||
:class (stl/css-case :comment-button true
|
||||
:open show?
|
||||
:unread (boolean (seq tgroups)))}
|
||||
comments-icon-small]]))
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:tab-index "0"
|
||||
:class (stl/css :comment-button)
|
||||
:data-testid "open-comments"
|
||||
:aria-label (tr "dashboard.notifications.view")
|
||||
:on-click on-show-comments
|
||||
:icon "comments"}
|
||||
(when (seq tgroups)
|
||||
[:div {:class (stl/css :unread)}])]]))
|
||||
|
||||
(mf/defc comments-section
|
||||
[{:keys [profile team show? on-hide-comments]}]
|
||||
|
@ -72,13 +59,6 @@
|
|||
(dcm/apply-filters {} profile)
|
||||
(dcm/group-threads-by-file-and-page))
|
||||
|
||||
handle-keydown
|
||||
(mf/use-callback
|
||||
(mf/deps on-hide-comments)
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-hide-comments event))))
|
||||
|
||||
on-navigate
|
||||
(mf/use-callback
|
||||
(fn [thread]
|
||||
|
@ -101,22 +81,22 @@
|
|||
[:& dropdown {:show show? :on-close on-hide-comments}
|
||||
[:div {:class (stl/css :dropdown :comments-section :comment-threads-section)}
|
||||
[:div {:class (stl/css :header)}
|
||||
[:h3 {:class (stl/css :header-title)} (tr "labels.comments")]
|
||||
[:button {:class (stl/css :close-btn)
|
||||
:tab-index (if show? "0" "-1")
|
||||
:on-click on-hide-comments
|
||||
:on-key-down handle-keydown}
|
||||
close-icon]]
|
||||
[:h3 {:class (stl/css :header-title)} (tr "dashboard.notifications")]
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:tab-index (if show? "0" "-1")
|
||||
:aria-label (tr "labels.close")
|
||||
:on-click on-hide-comments
|
||||
:icon "close"}]]
|
||||
|
||||
(if (seq tgroups)
|
||||
[:div {:class (stl/css :thread-groups)}
|
||||
[:& cmt/comment-thread-group
|
||||
[:> cmt/comment-dashboard-thread-group*
|
||||
{:group (first tgroups)
|
||||
:on-thread-click on-navigate
|
||||
:show-file-name true
|
||||
:profiles profiles}]
|
||||
(for [tgroup (rest tgroups)]
|
||||
[:& cmt/comment-thread-group
|
||||
[:> cmt/comment-dashboard-thread-group*
|
||||
{:group tgroup
|
||||
:on-thread-click on-navigate
|
||||
:show-file-name true
|
||||
|
|
|
@ -44,21 +44,16 @@
|
|||
}
|
||||
|
||||
.comment-button {
|
||||
@include buttonStyle;
|
||||
@include flexCenter;
|
||||
border-radius: $br-8;
|
||||
height: $s-32;
|
||||
width: $s-32;
|
||||
--comment-icon-small-foreground-color: var(--icon-foreground);
|
||||
|
||||
&.unread,
|
||||
&.open {
|
||||
--comment-icon-small-foreground-color: var(--icon-foreground-selected);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-quaternary);
|
||||
--comment-icon-small-foreground-color: var(--icon-foreground-active);
|
||||
position: relative;
|
||||
.unread {
|
||||
position: absolute;
|
||||
width: $s-8;
|
||||
height: $s-8;
|
||||
border: $s-2 solid var(--color-background-tertiary);
|
||||
border-radius: 50%;
|
||||
background: red;
|
||||
top: $s-6;
|
||||
right: $s-6;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,13 +95,3 @@
|
|||
flex-grow: 1;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
@include buttonStyle;
|
||||
@include flexCenter;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
@extend .button-icon;
|
||||
stroke: var(--icon-foreground);
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
[app.common.media :as cm]
|
||||
[app.main.data.fonts :as df]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.context-menu-a11y :refer [context-menu*]]
|
||||
|
@ -109,6 +110,8 @@
|
|||
(swap! uploading* disj id)
|
||||
(st/emit! (df/add-font font)))
|
||||
(fn [error]
|
||||
(st/emit! (ntf/error (tr "errors.bad-font" (first (:names item)))))
|
||||
(swap! fonts* dissoc id)
|
||||
(js/console.log "error" error))))))
|
||||
|
||||
on-upload
|
||||
|
|
|
@ -189,7 +189,7 @@
|
|||
[:& bc/color-bullet {:color {:color (:color color)
|
||||
:id (:id color)
|
||||
:opacity (:opacity color)}
|
||||
:mini? true}]
|
||||
:mini true}]
|
||||
[:div {:class (stl/css :name-block)}
|
||||
[:span {:class (stl/css :color-name)} (:name color)]
|
||||
(when-not (= (:name color) default-name)
|
||||
|
@ -331,6 +331,15 @@
|
|||
client-position)]
|
||||
(st/emit! (dd/show-file-menu-with-position file-id position)))))
|
||||
|
||||
on-context-menu
|
||||
(mf/use-fn
|
||||
(mf/deps is-library-view)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(dom/prevent-default event)
|
||||
(when-not is-library-view
|
||||
(on-menu-click event))))
|
||||
|
||||
edit
|
||||
(mf/use-fn
|
||||
(mf/deps file)
|
||||
|
@ -373,7 +382,7 @@
|
|||
:on-key-down handle-key-down
|
||||
:on-double-click on-navigate
|
||||
:on-drag-start on-drag-start
|
||||
:on-context-menu on-menu-click}
|
||||
:on-context-menu on-context-menu}
|
||||
|
||||
[:div {:class (stl/css :overlay)}]
|
||||
|
||||
|
@ -392,31 +401,32 @@
|
|||
[:h3 (:name file)])
|
||||
[:& grid-item-metadata {:modified-at (:modified-at file)}]]
|
||||
|
||||
[:div {:class (stl/css-case :project-th-actions true :force-display (:menu-open dashboard-local))}
|
||||
[:div
|
||||
{:class (stl/css :project-th-icon :menu)
|
||||
:tab-index "0"
|
||||
:ref menu-ref
|
||||
:id (str file-id "-action-menu")
|
||||
:on-click on-menu-click
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(dom/stop-propagation event)
|
||||
(on-menu-click event)))}
|
||||
menu-icon
|
||||
(when (and selected? file-menu-open?)
|
||||
(when-not is-library-view
|
||||
[:div {:class (stl/css-case :project-th-actions true :force-display (:menu-open dashboard-local))}
|
||||
[:div
|
||||
{:class (stl/css :project-th-icon :menu)
|
||||
:tab-index "0"
|
||||
:ref menu-ref
|
||||
:id (str file-id "-action-menu")
|
||||
:on-click on-menu-click
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(dom/stop-propagation event)
|
||||
(on-menu-click event)))}
|
||||
menu-icon
|
||||
(when (and selected? file-menu-open?)
|
||||
;; When the menu is open we disable events in the dashboard. We need to force pointer events
|
||||
;; so the menu can be handled
|
||||
[:div {:style {:pointer-events "all"}}
|
||||
[:> file-menu* {:files (vals selected-files)
|
||||
:left (+ 24 (:x (:menu-pos dashboard-local)))
|
||||
:top (:y (:menu-pos dashboard-local))
|
||||
:can-edit can-edit
|
||||
:navigate true
|
||||
:on-edit on-edit
|
||||
:on-menu-close on-menu-close
|
||||
:origin origin
|
||||
:parent-id (dm/str file-id "-action-menu")}]])]]]]]))
|
||||
[:div {:style {:pointer-events "all"}}
|
||||
[:> file-menu* {:files (vals selected-files)
|
||||
:left (+ 24 (:x (:menu-pos dashboard-local)))
|
||||
:top (:y (:menu-pos dashboard-local))
|
||||
:can-edit can-edit
|
||||
:navigate true
|
||||
:on-edit on-edit
|
||||
:on-menu-close on-menu-close
|
||||
:origin origin
|
||||
:parent-id (dm/str file-id "-action-menu")}]])]])]]]))
|
||||
|
||||
(mf/defc grid
|
||||
{::mf/props :obj}
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown-menu :refer [dropdown-menu dropdown-menu-item*]]
|
||||
[app.main.ui.components.link :refer [link]]
|
||||
[app.main.ui.dashboard.comments :refer [comments-icon comments-section]]
|
||||
[app.main.ui.dashboard.comments :refer [comments-icon* comments-section]]
|
||||
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
|
||||
[app.main.ui.dashboard.project-menu :refer [project-menu*]]
|
||||
[app.main.ui.dashboard.team-form]
|
||||
|
@ -1071,9 +1071,8 @@
|
|||
(tr "labels.logout")]]
|
||||
|
||||
(when (and team profile)
|
||||
[:& comments-icon
|
||||
[:> comments-icon*
|
||||
{:profile profile
|
||||
:show? show-comments?
|
||||
:on-show-comments handle-show-comments}])]]))
|
||||
|
||||
(mf/defc sidebar*
|
||||
|
|
|
@ -412,7 +412,7 @@
|
|||
border: $b-1 solid var(--color-background-quaternary);
|
||||
border-radius: var(--sp-s);
|
||||
padding: var(--sp-m);
|
||||
margin: var(--sp-m) var(--sp-s) var(--sp-m) var(--sp-m);
|
||||
margin: var(--sp-m);
|
||||
color: var(--color-foreground-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
@ -31,7 +31,6 @@
|
|||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private arrow-icon
|
||||
|
@ -743,15 +742,12 @@
|
|||
[:> i18n/tr-html* {:content (tr "labels.no-invitations-hint")
|
||||
:tag-name "span"}])])
|
||||
|
||||
(def ^:private ref:invitations
|
||||
(l/derived :invitations st/state))
|
||||
|
||||
(mf/defc invitation-section*
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [team]}]
|
||||
(let [permissions (get team :permissions)
|
||||
invitations (mf/deref ref:invitations)
|
||||
invitations (get team :invitations)
|
||||
|
||||
team-id (get team :id)
|
||||
|
||||
|
@ -957,7 +953,7 @@
|
|||
|
||||
[:span {:title (tr "dashboard.webhooks.cant-edit")
|
||||
:class (stl/css :menu-disabled)}
|
||||
[:> icon* {:id "menu"}]])))
|
||||
[:> icon* {:icon-id "menu"}]])))
|
||||
|
||||
(mf/defc webhook-item*
|
||||
{::mf/wrap [mf/memo]
|
||||
|
@ -1037,13 +1033,10 @@
|
|||
:key (dm/str (:id webhook))
|
||||
:permissions permissions}])])
|
||||
|
||||
(def ^:private ref:webhooks
|
||||
(l/derived :webhooks st/state))
|
||||
|
||||
(mf/defc webhooks-page*
|
||||
{::mf/props :obj}
|
||||
[{:keys [team]}]
|
||||
(let [webhooks (mf/deref ref:webhooks)]
|
||||
(let [webhooks (:webhooks team)]
|
||||
|
||||
(mf/with-effect [team]
|
||||
(dom/set-html-title
|
||||
|
|
|
@ -32,5 +32,5 @@
|
|||
:button-destructive (= variant "destructive")))
|
||||
props (mf/spread-props props {:class class})]
|
||||
[:> "button" props
|
||||
(when icon [:> icon* {:id icon :size "m"}])
|
||||
(when icon [:> icon* {:icon-id icon :size "m"}])
|
||||
[:span {:class (stl/css :label-wrapper)} children]]))
|
|
@ -34,4 +34,4 @@
|
|||
:icon-button-action (= variant "action")
|
||||
:icon-button-destructive (= variant "destructive")))
|
||||
props (mf/spread-props props {:class class :title aria-label})]
|
||||
[:> "button" props [:> icon* {:id icon :aria-label aria-label :class icon-class}] children]))
|
||||
[:> "button" props [:> icon* {:icon-id icon :aria-label aria-label :class icon-class}] children]))
|
||||
|
|
|
@ -215,7 +215,7 @@
|
|||
[:span {:class (stl/css-case :combobox-header true
|
||||
:header-icon (some? icon))}
|
||||
(when icon
|
||||
[:> icon* {:id icon
|
||||
[:> icon* {:icon-id icon
|
||||
:size "s"
|
||||
:aria-hidden true}])
|
||||
[:input {:type "text"
|
||||
|
@ -236,7 +236,7 @@
|
|||
:aria-controls listbox-id
|
||||
:class (stl/css :button-toggle-list)
|
||||
:on-click on-click}
|
||||
[:> icon* {:id i/arrow
|
||||
[:> icon* {:icon-id i/arrow
|
||||
:class (stl/css :arrow)
|
||||
:size "s"
|
||||
:aria-hidden true
|
||||
|
|
|
@ -30,11 +30,11 @@
|
|||
(let [ref (or ref (mf/use-ref))
|
||||
type (d/nilv type "text")
|
||||
props (mf/spread-props props
|
||||
:class (stl/css-case
|
||||
:input true
|
||||
:input-with-icon (some? icon))
|
||||
:ref ref
|
||||
:type type)
|
||||
{:class (stl/css-case
|
||||
:input true
|
||||
:input-with-icon (some? icon))
|
||||
:ref ref
|
||||
:type type})
|
||||
|
||||
on-icon-click
|
||||
(mf/use-fn
|
||||
|
@ -46,5 +46,5 @@
|
|||
|
||||
[:> :span {:class (dm/str class " " (stl/css :container))}
|
||||
(when (some? icon)
|
||||
[:> icon* {:id icon :class (stl/css :icon) :on-click on-icon-click}])
|
||||
[:> icon* {:icon-id icon :class (stl/css :icon) :on-click on-icon-click}])
|
||||
[:> :input props]]))
|
||||
|
|
|
@ -171,12 +171,12 @@
|
|||
[:span {:class (stl/css-case :select-header true
|
||||
:header-icon (some? icon))}
|
||||
(when icon
|
||||
[:> icon* {:id icon
|
||||
[:> icon* {:icon-id icon
|
||||
:size "s"
|
||||
:aria-hidden true}])
|
||||
[:span {:class (stl/css :header-label)}
|
||||
label]]
|
||||
[:> icon* {:id i/arrow
|
||||
[:> icon* {:icon-id i/arrow
|
||||
:class (stl/css :arrow)
|
||||
:size "s"
|
||||
:aria-hidden true}]]
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
|
||||
(when (some? icon)
|
||||
[:> icon*
|
||||
{:id icon
|
||||
{:icon-id icon
|
||||
:size "s"
|
||||
:class (stl/css :option-icon)
|
||||
:aria-hidden (when label true)
|
||||
|
@ -40,7 +40,7 @@
|
|||
[:span {:class (stl/css :option-text)} label]
|
||||
(when selected
|
||||
[:> icon*
|
||||
{:id i/tick
|
||||
{:icon-id i/tick
|
||||
:size "s"
|
||||
:class (stl/css :option-check)
|
||||
:aria-hidden (when label true)}])])
|
||||
|
|
|
@ -55,6 +55,7 @@
|
|||
(def ^:icon-id arrow-right "arrow-right")
|
||||
(def ^:icon-id arrow-up "arrow-up")
|
||||
(def ^:icon-id asc-sort "asc-sort")
|
||||
(def ^:icon-id at "at")
|
||||
(def ^:icon-id board "board")
|
||||
(def ^:icon-id boards-thumbnail "boards-thumbnail")
|
||||
(def ^:icon-id boolean-difference "boolean-difference")
|
||||
|
@ -287,17 +288,17 @@
|
|||
(def ^:private schema:icon
|
||||
[:map
|
||||
[:class {:optional true} [:maybe :string]]
|
||||
[:id [:and :string [:fn #(contains? icon-list %)]]]
|
||||
[:icon-id [:and :string [:fn #(contains? icon-list %)]]]
|
||||
[:size {:optional true}
|
||||
[:maybe [:enum "s" "m"]]]])
|
||||
|
||||
(mf/defc icon*
|
||||
{::mf/props :obj
|
||||
::mf/schema schema:icon}
|
||||
[{:keys [id size class] :rest props}]
|
||||
[{:keys [icon-id size class] :rest props}]
|
||||
(let [class (dm/str (or class "") " " (stl/css :icon))
|
||||
props (mf/spread-props props {:class class :width icon-size-m :height icon-size-m})
|
||||
size-px (cond (= size "s") icon-size-s :else icon-size-m)
|
||||
offset (/ (- icon-size-m size-px) 2)]
|
||||
[:> "svg" props
|
||||
[:use {:href (dm/str "#icon-" id) :width size-px :height size-px :x offset :y offset}]]))
|
||||
[:use {:href (dm/str "#icon-" icon-id) :width size-px :height size-px :x offset :y offset}]]))
|
||||
|
|
|
@ -47,7 +47,7 @@ Assuming the namespace is required as `i`:
|
|||
You can now use the icon IDs defined in the namespace:
|
||||
|
||||
```clj
|
||||
[:> i/icon* {:id i/pin}]
|
||||
[:> i/icon* {:icon-id i/pin}]
|
||||
```
|
||||
|
||||
### Customizing colors
|
||||
|
@ -59,7 +59,7 @@ If you need to override this behavior, you can use a `class` in the `<Icon>`
|
|||
component and set `color` to whatever value you prefer:
|
||||
|
||||
```clj
|
||||
[:> i/icon* {:id i/add :class (stl/css :toolbar-icon)}]
|
||||
[:> i/icon* {:icon-id i/add :class (stl/css :toolbar-icon)}]
|
||||
```
|
||||
|
||||
```scss
|
||||
|
@ -74,7 +74,7 @@ By default, icons do not have any accessible text attached to them. You should
|
|||
add an `aria-label` attribute to set a proper text:
|
||||
|
||||
```clj
|
||||
[:> i/icon* {:id i/add :aria-label (tr "foo.bar")}]
|
||||
[:> i/icon* {:icon-id i/add :aria-label (tr "foo.bar")}]
|
||||
```
|
||||
|
||||
## Usage guidelines for design
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
[:> :button props
|
||||
(when (some? icon)
|
||||
[:> icon*
|
||||
{:id icon
|
||||
{:icon-id icon
|
||||
:aria-hidden (when label true)
|
||||
:aria-label (when (not label) aria-label)}])
|
||||
(when (string? label)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
@ -16,9 +15,9 @@
|
|||
{::mf/props :obj}
|
||||
[{:keys [children size style] :rest other}]
|
||||
(let [class (stl/css :story-grid)
|
||||
size (or size 16)
|
||||
style (or style {})
|
||||
style (mf/spread style :--component-grid-size (dm/str size "px"))
|
||||
size (or size 16)
|
||||
style (or style #js {})
|
||||
style (mf/spread-props style {"--component-grid-size" (dm/str size "px")})
|
||||
props (mf/spread-props other {:class class :style style})]
|
||||
[:> "article" props children]))
|
||||
|
||||
|
@ -41,4 +40,4 @@
|
|||
[{:keys [children] :rest other}]
|
||||
(let [class (stl/css :story-grid-row)
|
||||
props (mf/spread-props other {:class class})]
|
||||
[:> "article" props children]))
|
||||
[:> "article" props children]))
|
||||
|
|
|
@ -33,7 +33,8 @@
|
|||
["/password" :settings-password]
|
||||
["/feedback" :settings-feedback]
|
||||
["/options" :settings-options]
|
||||
["/access-tokens" :settings-access-tokens]]
|
||||
["/access-tokens" :settings-access-tokens]
|
||||
["/notifications" :settings-notifications]]
|
||||
|
||||
["/frame-preview" :frame-preview]
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
[app.main.ui.settings.change-email]
|
||||
[app.main.ui.settings.delete-account]
|
||||
[app.main.ui.settings.feedback :refer [feedback-page]]
|
||||
[app.main.ui.settings.notifications :refer [notifications-page]]
|
||||
[app.main.ui.settings.options :refer [options-page]]
|
||||
[app.main.ui.settings.password :refer [password-page]]
|
||||
[app.main.ui.settings.profile :refer [profile-page]]
|
||||
|
@ -67,4 +68,7 @@
|
|||
[:& options-page]
|
||||
|
||||
:settings-access-tokens
|
||||
[:& access-tokens-page])]]]]))
|
||||
[:& access-tokens-page]
|
||||
|
||||
:settings-notifications
|
||||
[:& notifications-page])]]]]))
|
||||
|
|
106
frontend/src/app/main/ui/settings/notifications.cljs
Normal file
106
frontend/src/app/main/ui/settings/notifications.cljs
Normal file
|
@ -0,0 +1,106 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.settings.notifications
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.profile :as dp]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.forms :as fm]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def default-notification-settings
|
||||
{:dashboard-comments :all
|
||||
:email-comments :partial
|
||||
:email-invites :all})
|
||||
|
||||
(def notification-settings-ref
|
||||
(l/derived
|
||||
(fn [profile]
|
||||
(-> (merge default-notification-settings
|
||||
(-> profile :props :notifications))
|
||||
(d/update-vals d/name)))
|
||||
refs/profile))
|
||||
|
||||
(defn- on-error
|
||||
[form _]
|
||||
(reset! form nil)
|
||||
(st/emit! (ntf/error (tr "generic.error"))))
|
||||
|
||||
(defn- on-success
|
||||
[_]
|
||||
(st/emit! (ntf/success (tr "dashboard.notifications.notifications-saved"))))
|
||||
|
||||
(defn- on-submit
|
||||
[form event]
|
||||
(dom/prevent-default event)
|
||||
(let [params (with-meta (:clean-data @form)
|
||||
{:on-success (partial on-success form)
|
||||
:on-error (partial on-error form)})]
|
||||
(st/emit! (dp/update-notifications params))))
|
||||
|
||||
(def ^:private schema:notifications-form
|
||||
[:map {:title "NotificationsForm"}
|
||||
[:dashboard-comments [::sm/one-of #{:all :partial :none}]]
|
||||
[:email-comments [::sm/one-of #{:all :partial :none}]]
|
||||
[:email-invites [::sm/one-of #{:all :partial :none}]]])
|
||||
|
||||
(mf/defc notifications-page
|
||||
[]
|
||||
(let [settings (mf/deref notification-settings-ref)
|
||||
form (fm/use-form :schema schema:notifications-form
|
||||
:initial settings)]
|
||||
(mf/with-effect []
|
||||
(dom/set-html-title (tr "title.settings.notifications")))
|
||||
|
||||
[:section {:class (stl/css :notifications-page)}
|
||||
[:& fm/form {:class (stl/css :notifications-form)
|
||||
:on-submit on-submit
|
||||
:form form}
|
||||
[:div {:class (stl/css :form-container)}
|
||||
[:h2 (tr "dashboard.settings.notifications.title")]
|
||||
[:h3 (tr "dashboard.settings.notifications.dashboard.title")]
|
||||
[:h4 (tr "dashboard.settings.notifications.dashboard-comments.title")]
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:& fm/radio-buttons
|
||||
{:options [{:label (tr "dashboard.settings.notifications.dashboard-comments.all") :value "all"}
|
||||
{:label (tr "dashboard.settings.notifications.dashboard-comments.partial") :value "partial"}
|
||||
{:label (tr "dashboard.settings.notifications.dashboard-comments.none") :value "none"}]
|
||||
:name :dashboard-comments
|
||||
:class (stl/css :radio-btns)}]]
|
||||
|
||||
[:h3 (tr "dashboard.settings.notifications.email.title")]
|
||||
[:h4 (tr "dashboard.settings.notifications.email-comments.title")]
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:& fm/radio-buttons
|
||||
{:options [{:label (tr "dashboard.settings.notifications.email-comments.all") :value "all"}
|
||||
{:label (tr "dashboard.settings.notifications.email-comments.partial") :value "partial"}
|
||||
{:label (tr "dashboard.settings.notifications.email-comments.none") :value "none"}]
|
||||
:name :email-comments
|
||||
:class (stl/css :radio-btns)}]]
|
||||
|
||||
[:h4 (tr "dashboard.settings.notifications.email-invites.title")]
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:& fm/radio-buttons
|
||||
{:options [{:label (tr "dashboard.settings.notifications.email-invites.all") :value "all"}
|
||||
;; This type of notifications doesnt't exist yet
|
||||
;; {:label "Only invites and requests that my response" :value "partial"}
|
||||
{:label (tr "dashboard.settings.notifications.email-invites.none") :value "none"}]
|
||||
:name :email-invites
|
||||
:class (stl/css :radio-btns)}]]
|
||||
|
||||
[:> fm/submit-button*
|
||||
{:label (tr "dashboard.settings.notifications.submit")
|
||||
:data-testid "submit-settings"
|
||||
:class (stl/css :update-btn)}]]]]))
|
||||
|
42
frontend/src/app/main/ui/settings/notifications.scss
Normal file
42
frontend/src/app/main/ui/settings/notifications.scss
Normal file
|
@ -0,0 +1,42 @@
|
|||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
@use "common/refactor/common-refactor.scss" as *;
|
||||
@use "./profile" as *;
|
||||
|
||||
.update-btn {
|
||||
margin-top: $s-16;
|
||||
@extend .button-primary;
|
||||
height: $s-36;
|
||||
}
|
||||
|
||||
.notifications-form {
|
||||
width: $s-400;
|
||||
}
|
||||
|
||||
.notifications-page {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.radio-btns {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
h3 {
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: $fs-11;
|
||||
color: var(--color-foreground-primary);
|
||||
text-transform: uppercase;
|
||||
margin: $s-12;
|
||||
}
|
||||
}
|
|
@ -43,6 +43,9 @@
|
|||
(def ^:private go-settings-access-tokens
|
||||
#(st/emit! (rt/nav :settings-access-tokens)))
|
||||
|
||||
(def ^:private go-settings-notifications
|
||||
#(st/emit! (rt/nav :settings-notifications)))
|
||||
|
||||
(defn- show-release-notes
|
||||
[event]
|
||||
(let [version (:main cf/version)]
|
||||
|
@ -60,6 +63,7 @@
|
|||
options? (= section :settings-options)
|
||||
feedback? (= section :settings-feedback)
|
||||
access-tokens? (= section :settings-access-tokens)
|
||||
notifications? (= section :settings-notifications)
|
||||
team-id (or (dtm/get-last-team-id)
|
||||
(:default-team-id profile))
|
||||
|
||||
|
@ -89,6 +93,11 @@
|
|||
:on-click go-settings-password}
|
||||
[:span {:class (stl/css :element-title)} (tr "labels.password")]]
|
||||
|
||||
[:li {:class (stl/css-case :current notifications?
|
||||
:settings-item true)
|
||||
:on-click go-settings-notifications}
|
||||
[:span {:class (stl/css :element-title)} (tr "labels.notifications")]]
|
||||
|
||||
[:li {:class (stl/css-case :current options?
|
||||
:settings-item true)
|
||||
:on-click go-settings-options
|
||||
|
|
|
@ -47,7 +47,8 @@
|
|||
:on-click on-nav-root}
|
||||
[:> raw-svg* {:id "penpot-logo-icon" :class (stl/css :penpot-logo)}]
|
||||
(when profile-id
|
||||
[:div {:class (stl/css :go-back-wrapper)} [:> icon* {:id "arrow" :class (stl/css :back-arrow)}] [:span (tr "not-found.no-permission.go-dashboard")]])]
|
||||
[:div {:class (stl/css :go-back-wrapper)}
|
||||
[:> icon* {:icon-id "arrow" :class (stl/css :back-arrow)}] [:span (tr "not-found.no-permission.go-dashboard")]])]
|
||||
[:div {:class (stl/css :deco-before)} i/logo-error-screen]
|
||||
(when-not profile-id
|
||||
[:button {:class (stl/css :login-header)
|
||||
|
|
|
@ -206,26 +206,28 @@
|
|||
[:div {:class (stl/css :viewer-comments-container)}
|
||||
[:div {:class (stl/css :threads)}
|
||||
(for [item threads]
|
||||
[:& cmt/thread-bubble
|
||||
[:> cmt/comment-floating-bubble*
|
||||
{:thread item
|
||||
:profiles users
|
||||
:position-modifier modifier1
|
||||
:zoom zoom
|
||||
:on-click on-bubble-click
|
||||
:open? (= (:id item) (:open local))
|
||||
:is-open (= (:id item) (:open local))
|
||||
:key (:seqn item)
|
||||
:origin :viewer}])
|
||||
|
||||
(when-let [thread (get threads-map open-thread-id)]
|
||||
[:& cmt/thread-comments
|
||||
[:> cmt/comment-floating-thread*
|
||||
{:thread thread
|
||||
:profiles users
|
||||
:position-modifier modifier1
|
||||
:viewport {:offset-x 0 :offset-y 0 :width (:width vsize) :height (:height vsize)}
|
||||
:profiles users
|
||||
:zoom zoom}])
|
||||
|
||||
(when-let [draft (:draft local)]
|
||||
[:& cmt/draft-thread
|
||||
[:> cmt/comment-floating-thread-draft*
|
||||
{:draft draft
|
||||
:profiles users
|
||||
:position-modifier modifier1
|
||||
:on-cancel on-draft-cancel
|
||||
:on-submit on-draft-submit
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
[:div {:class (stl/css :bullet-wrapper)
|
||||
:style #js {"--bullet-size" "16px"}}
|
||||
[:& cb/color-bullet {:color color
|
||||
:mini? true}]]
|
||||
:mini true}]]
|
||||
|
||||
[:div {:class (stl/css :format-wrapper)}
|
||||
[:div {:class (stl/css :image-format)}
|
||||
|
@ -102,7 +102,7 @@
|
|||
[:div {:class (stl/css :bullet-wrapper)
|
||||
:style #js {"--bullet-size" "16px"}}
|
||||
[:& cb/color-bullet {:color color
|
||||
:mini? true}]]
|
||||
:mini true}]]
|
||||
|
||||
[:div {:class (stl/css :format-wrapper)}
|
||||
(when-not (and on-change-format (or (:gradient color) image))
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
[app.main.ui.workspace.plugins]
|
||||
[app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]]
|
||||
[app.main.ui.workspace.sidebar.collapsable-button :refer [collapsed-button]]
|
||||
[app.main.ui.workspace.sidebar.history :refer [history-toolbox]]
|
||||
[app.main.ui.workspace.sidebar.history :refer [history-toolbox*]]
|
||||
[app.main.ui.workspace.tokens.modals]
|
||||
[app.main.ui.workspace.tokens.modals.themes]
|
||||
[app.main.ui.workspace.viewport :refer [viewport]]
|
||||
|
@ -100,7 +100,7 @@
|
|||
(when (dbg/enabled? :history-overlay)
|
||||
[:div {:class (stl/css :history-debug-overlay)}
|
||||
[:button {:on-click #(st/emit! dw/reinitialize-undo)} "CLEAR"]
|
||||
[:& history-toolbox]])
|
||||
[:> history-toolbox*]])
|
||||
|
||||
[:& viewport {:file file
|
||||
:wlocal wlocal
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
:style #js {"--bullet-size" "20px"}}
|
||||
(for [[i {:keys [color id gradient]}] (map-indexed vector (take 7 colors))]
|
||||
[:& cb/color-bullet {:key (dm/str "color-" i)
|
||||
:mini? true
|
||||
:mini true
|
||||
:color {:color color :id id :gradient gradient}}])]]]))
|
||||
|
||||
[:li {:class (stl/css-case :file-library true
|
||||
|
@ -68,7 +68,7 @@
|
|||
:style #js {"--bullet-size" "20px"}}
|
||||
(for [[i color] (map-indexed vector (take 7 (vals file-colors)))]
|
||||
[:& cb/color-bullet {:key (dm/str "color-" i)
|
||||
:mini? true
|
||||
:mini true
|
||||
:color color}])]]]
|
||||
|
||||
[:li {:class (stl/css :recent-colors true
|
||||
|
@ -90,5 +90,5 @@
|
|||
:style #js {"--bullet-size" "20px"}}
|
||||
(for [[idx color] (map-indexed vector (take 7 (reverse recent-colors)))]
|
||||
[:& cb/color-bullet {:key (str "color-" idx)
|
||||
:mini? true
|
||||
:mini true
|
||||
:color color}])]]]]]))
|
||||
|
|
|
@ -63,6 +63,12 @@
|
|||
:on-click update-mode}
|
||||
[:span {:class (stl/css :label)} (tr "labels.show-your-comments")]
|
||||
[:span {:class (stl/css :icon)} i/tick]]
|
||||
[:li {:class (stl/css-case :dropdown-item true
|
||||
:selected (= :mentions cmode))
|
||||
:data-value "mentions"
|
||||
:on-click update-mode}
|
||||
[:span {:class (stl/css :label)} (tr "labels.show-mentions")]
|
||||
[:span {:class (stl/css :icon)} i/tick]]
|
||||
[:li {:class (stl/css :separator)}]
|
||||
[:li {:class (stl/css-case :dropdown-item true
|
||||
:selected (= :pending cshow))
|
||||
|
@ -137,9 +143,11 @@
|
|||
[:button {:class (stl/css :mode-dropdown-wrapper)
|
||||
:on-click toggle-mode-selector}
|
||||
|
||||
[:span {:class (stl/css :mode-label)} (case (:mode local)
|
||||
(nil :all) (tr "labels.show-all-comments")
|
||||
:yours (tr "labels.show-your-comments"))]
|
||||
[:span {:class (stl/css :mode-label)}
|
||||
(case (:mode local)
|
||||
(nil :all) (tr "labels.show-all-comments")
|
||||
:yours (tr "labels.show-your-comments")
|
||||
:mentions (tr "labels.show-mentions"))]
|
||||
[:div {:class (stl/css :arrow-icon)} i/arrow]]
|
||||
|
||||
[:& dropdown {:show options?
|
||||
|
@ -150,12 +158,12 @@
|
|||
|
||||
(if (seq tgroups)
|
||||
[:div {:class (stl/css :thread-groups)}
|
||||
[:& cmt/comment-thread-group
|
||||
[:> cmt/comment-sidebar-thread-group*
|
||||
{:group (first tgroups)
|
||||
:on-thread-click on-thread-click
|
||||
:profiles profiles}]
|
||||
(for [tgroup (rest tgroups)]
|
||||
[:& cmt/comment-thread-group
|
||||
[:> cmt/comment-sidebar-thread-group*
|
||||
{:group tgroup
|
||||
:on-thread-click on-thread-click
|
||||
:profiles profiles
|
||||
|
|
|
@ -125,7 +125,6 @@
|
|||
.thread-groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-24;
|
||||
}
|
||||
|
||||
.thread-group-placeholder {
|
||||
|
|
|
@ -138,6 +138,8 @@
|
|||
::mf/private true}
|
||||
[]
|
||||
(let [do-copy #(st/emit! (dw/copy-selected))
|
||||
do-copy-link #(st/emit! (dw/copy-link-to-clipboard))
|
||||
|
||||
do-cut #(st/emit! (dw/copy-selected)
|
||||
(dw/delete-selected))
|
||||
do-paste #(st/emit! (dw/paste-from-clipboard))
|
||||
|
@ -146,6 +148,9 @@
|
|||
[:> menu-entry* {:title (tr "workspace.shape.menu.copy")
|
||||
:shortcut (sc/get-tooltip :copy)
|
||||
:on-click do-copy}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.copy_link")
|
||||
:shortcut (sc/get-tooltip :copy-link)
|
||||
:on-click do-copy-link}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.cut")
|
||||
:shortcut (sc/get-tooltip :cut)
|
||||
:on-click do-cut}]
|
||||
|
@ -531,7 +536,7 @@
|
|||
[{:keys [mdata]}]
|
||||
(let [{:keys [disable-booleans disable-flatten]} mdata
|
||||
shapes (mf/deref refs/selected-objects)
|
||||
props (mf/spread-props
|
||||
props (mf/props
|
||||
{:shapes shapes
|
||||
:disable-booleans disable-booleans
|
||||
:disable-flatten disable-flatten})]
|
||||
|
|
|
@ -141,8 +141,7 @@
|
|||
|
||||
;; --- Header Component
|
||||
|
||||
(mf/defc right-header
|
||||
{::mf/wrap-props false}
|
||||
(mf/defc right-header*
|
||||
[{:keys [file layout page-id]}]
|
||||
(let [file-id (:id file)
|
||||
|
||||
|
|
|
@ -14,21 +14,22 @@
|
|||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*]]
|
||||
[app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]]
|
||||
[app.main.ui.hooks.resize :refer [use-resize-hook]]
|
||||
[app.main.ui.workspace.comments :refer [comments-sidebar*]]
|
||||
[app.main.ui.workspace.left-header :refer [left-header]]
|
||||
[app.main.ui.workspace.right-header :refer [right-header]]
|
||||
[app.main.ui.workspace.right-header :refer [right-header*]]
|
||||
[app.main.ui.workspace.sidebar.assets :refer [assets-toolbox]]
|
||||
[app.main.ui.workspace.sidebar.debug :refer [debug-panel]]
|
||||
[app.main.ui.workspace.sidebar.debug-shape-info :refer [debug-shape-info]]
|
||||
[app.main.ui.workspace.sidebar.history :refer [history-toolbox]]
|
||||
[app.main.ui.workspace.sidebar.history :refer [history-toolbox*]]
|
||||
[app.main.ui.workspace.sidebar.layers :refer [layers-toolbox]]
|
||||
[app.main.ui.workspace.sidebar.options :refer [options-toolbox]]
|
||||
[app.main.ui.workspace.sidebar.options :refer [options-toolbox*]]
|
||||
[app.main.ui.workspace.sidebar.shortcuts :refer [shortcuts-container]]
|
||||
[app.main.ui.workspace.sidebar.sitemap :refer [sitemap]]
|
||||
[app.main.ui.workspace.sidebar.versions :refer [versions-toolbox]]
|
||||
[app.main.ui.workspace.sidebar.versions :refer [versions-toolbox*]]
|
||||
[app.main.ui.workspace.tokens.sidebar :refer [tokens-sidebar-tab]]
|
||||
[app.util.debug :as dbg]
|
||||
[app.util.i18n :refer [tr]]
|
||||
|
@ -43,7 +44,7 @@
|
|||
;; NOTE: This custom button may be replace by an action button when this variant is designed
|
||||
[:button {:class (stl/css :collapse-sidebar-button)
|
||||
:on-click on-click}
|
||||
[:& icon* {:id "arrow"
|
||||
[:> icon* {:icon-id "arrow"
|
||||
:size "s"
|
||||
:aria-label (tr "workspace.sidebar.collapse")}]])
|
||||
|
||||
|
@ -188,74 +189,99 @@
|
|||
is-history? (contains? layout :document-history)
|
||||
is-inspect? (= section :inspect)
|
||||
|
||||
dbg-shape-panel? (dbg/enabled? :shape-panel)
|
||||
|
||||
current-section* (mf/use-state :info)
|
||||
current-section (deref current-section*)
|
||||
|
||||
can-be-expanded? (or (dbg/enabled? :shape-panel)
|
||||
(and (not is-comments?)
|
||||
(not is-history?)
|
||||
is-inspect?
|
||||
(= current-section :code)))
|
||||
can-be-expanded?
|
||||
(or dbg-shape-panel?
|
||||
(and (not is-comments?)
|
||||
(not is-history?)
|
||||
is-inspect?
|
||||
(= current-section :code)))
|
||||
|
||||
{:keys [on-pointer-down on-lost-pointer-capture on-pointer-move set-size size]}
|
||||
(use-resize-hook :code 276 276 768 :x true :right)
|
||||
|
||||
handle-change-section
|
||||
(mf/use-callback
|
||||
on-change-section
|
||||
(mf/use-fn
|
||||
(fn [section]
|
||||
(reset! current-section* section)))
|
||||
|
||||
handle-expand
|
||||
(mf/use-callback
|
||||
on-close-history
|
||||
(mf/use-fn #(st/emit! (dw/remove-layout-flag :document-history)))
|
||||
|
||||
on-expand
|
||||
(mf/use-fn
|
||||
(mf/deps size)
|
||||
(fn []
|
||||
(set-size (if (> size 276) 276 768))))
|
||||
|
||||
props
|
||||
(mf/spread props
|
||||
:on-change-section handle-change-section
|
||||
:on-expand handle-expand)
|
||||
(mf/spread-props props
|
||||
{:on-change-section on-change-section
|
||||
:on-expand on-expand})]
|
||||
|
||||
history-tab
|
||||
(mf/html
|
||||
[:article {:class (stl/css :history-tab)}
|
||||
[:& history-toolbox {}]])
|
||||
[:> (mf/provider muc/sidebar) {:value :right}
|
||||
[:aside
|
||||
{:class (stl/css-case :right-settings-bar true
|
||||
:not-expand (not can-be-expanded?)
|
||||
:expanded (> size 276))
|
||||
|
||||
versions-tab
|
||||
(mf/html
|
||||
[:article {:class (stl/css :versions-tab)}
|
||||
[:& versions-toolbox {}]])]
|
||||
:id "right-sidebar-aside"
|
||||
:data-testid "right-sidebar"
|
||||
:data-size (str size)
|
||||
:style {"--width" (if can-be-expanded? (dm/str size "px") "276px")}}
|
||||
|
||||
[:& (mf/provider muc/sidebar) {:value :right}
|
||||
[:aside {:class (stl/css-case :right-settings-bar true
|
||||
:not-expand (not can-be-expanded?)
|
||||
:expanded (> size 276))
|
||||
|
||||
:id "right-sidebar-aside"
|
||||
:data-testid "right-sidebar"
|
||||
:data-size (str size)
|
||||
:style #js {"--width" (if can-be-expanded? (dm/str size "px") "276px")}}
|
||||
(when can-be-expanded?
|
||||
[:div {:class (stl/css :resize-area)
|
||||
:on-pointer-down on-pointer-down
|
||||
:on-lost-pointer-capture on-lost-pointer-capture
|
||||
:on-pointer-move on-pointer-move}])
|
||||
[:& right-header {:file file :layout layout :page-id page-id}]
|
||||
|
||||
[:> right-header*
|
||||
{:file file
|
||||
:layout layout
|
||||
:page-id page-id}]
|
||||
|
||||
[:div {:class (stl/css :settings-bar-inside)}
|
||||
(cond
|
||||
(dbg/enabled? :shape-panel)
|
||||
dbg-shape-panel?
|
||||
[:& debug-shape-info]
|
||||
|
||||
(true? is-comments?)
|
||||
is-comments?
|
||||
[:> comments-sidebar* {}]
|
||||
|
||||
(true? is-history?)
|
||||
[:> tab-switcher*
|
||||
{:tabs #js [#js {:label (tr "workspace.versions.tab.history") :id "history" :content versions-tab}
|
||||
#js {:label (tr "workspace.versions.tab.actions") :id "actions" :content history-tab}]
|
||||
:default-selected "history"
|
||||
:class (stl/css :left-sidebar-tabs)}]
|
||||
is-history?
|
||||
(let [history-tab
|
||||
(mf/html
|
||||
[:article {:class (stl/css :history-tab)}
|
||||
[:> history-toolbox*]])
|
||||
|
||||
versions-tab
|
||||
(mf/html
|
||||
[:article {:class (stl/css :versions-tab)}
|
||||
[:> versions-toolbox*]])
|
||||
|
||||
button
|
||||
(mf/html
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:aria-label (tr "labels.close")
|
||||
:on-click on-close-history
|
||||
:icon "close"}])]
|
||||
|
||||
[:> tab-switcher*
|
||||
{:tabs [{:label (tr "workspace.versions.tab.history")
|
||||
:id "history"
|
||||
:content versions-tab}
|
||||
{:label (tr "workspace.versions.tab.actions")
|
||||
:id "actions"
|
||||
:content history-tab}]
|
||||
:default-selected "history"
|
||||
:class (stl/css :left-sidebar-tabs)
|
||||
:action-button-position "end"
|
||||
:action-button button}])
|
||||
|
||||
:else
|
||||
[:> options-toolbox props])]]]))
|
||||
[:> options-toolbox* props])]]]))
|
||||
|
|
|
@ -90,6 +90,9 @@
|
|||
reverse-sort? (= :desc ordering)
|
||||
num-libs (count (mf/deref refs/libraries))
|
||||
|
||||
show-templates-02-test?
|
||||
(and (cf/external-feature-flag "templates-02" "test") (zero? num-libs))
|
||||
|
||||
toggle-ordering
|
||||
(mf/use-fn
|
||||
(mf/deps ordering)
|
||||
|
@ -158,8 +161,7 @@
|
|||
[:article {:class (stl/css :assets-bar)}
|
||||
[:div {:class (stl/css :assets-header)}
|
||||
(when-not ^boolean read-only?
|
||||
(if (and (cf/external-feature-flag "templates-02" "test")
|
||||
(zero? num-libs))
|
||||
(if show-templates-02-test?
|
||||
[:button {:class (stl/css :add-library-button)
|
||||
:on-click show-libraries-dialog
|
||||
:data-testid "libraries"}
|
||||
|
@ -171,31 +173,32 @@
|
|||
i/library]
|
||||
(tr "workspace.assets.libraries")]))
|
||||
|
||||
[:div {:class (stl/css :search-wrapper)}
|
||||
[:& search-bar {:on-change on-search-term-change
|
||||
:value term
|
||||
:placeholder (tr "workspace.assets.search")}
|
||||
[:button
|
||||
{:on-click on-open-menu
|
||||
:title (tr "workspace.assets.filter")
|
||||
:class (stl/css-case :section-button true
|
||||
:opened menu-open?)}
|
||||
i/filter-icon]]
|
||||
[:> context-menu*
|
||||
{:on-close on-menu-close
|
||||
:selectable true
|
||||
:selected section
|
||||
:show menu-open?
|
||||
:fixed true
|
||||
:min-width true
|
||||
:width size
|
||||
:top 158
|
||||
:left 18
|
||||
:options options}]
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:aria-label (tr "workspace.assets.sort")
|
||||
:on-click toggle-ordering
|
||||
:icon (if reverse-sort? "asc-sort" "desc-sort")}]]]
|
||||
(when-not show-templates-02-test?
|
||||
[:div {:class (stl/css :search-wrapper)}
|
||||
[:& search-bar {:on-change on-search-term-change
|
||||
:value term
|
||||
:placeholder (tr "workspace.assets.search")}
|
||||
[:button
|
||||
{:on-click on-open-menu
|
||||
:title (tr "workspace.assets.filter")
|
||||
:class (stl/css-case :section-button true
|
||||
:opened menu-open?)}
|
||||
i/filter-icon]]
|
||||
[:> context-menu*
|
||||
{:on-close on-menu-close
|
||||
:selectable true
|
||||
:selected section
|
||||
:show menu-open?
|
||||
:fixed true
|
||||
:min-width true
|
||||
:width size
|
||||
:top 158
|
||||
:left 18
|
||||
:options options}]
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:aria-label (tr "workspace.assets.sort")
|
||||
:on-click toggle-ordering
|
||||
:icon (if reverse-sort? "asc-sort" "desc-sort")}]])]
|
||||
|
||||
[:& (mf/provider cmm/assets-filters) {:value filters}
|
||||
[:& (mf/provider cmm/assets-toggle-ordering) {:value toggle-ordering}
|
||||
|
|
|
@ -216,7 +216,7 @@
|
|||
|
||||
[:div {:class (stl/css :bullet-block)}
|
||||
[:& cb/color-bullet {:color color
|
||||
:mini? true}]]
|
||||
:mini true}]]
|
||||
|
||||
(if ^boolean editing?
|
||||
[:input
|
||||
|
|
|
@ -320,7 +320,7 @@
|
|||
(when @show-detail?
|
||||
[:& history-entry-details {:entry entry}])]))
|
||||
|
||||
(mf/defc history-toolbox
|
||||
(mf/defc history-toolbox*
|
||||
[]
|
||||
(let [objects (mf/deref refs/workspace-page-objects)
|
||||
{:keys [items index]} (mf/deref workspace-undo)
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
[{:keys [selected] :as props}]
|
||||
(let [pending-selected (mf/use-var selected)
|
||||
current-selected (mf/use-state selected)
|
||||
props (mf/spread props :selected @current-selected)
|
||||
props (mf/spread-object props {:selected @current-selected})
|
||||
|
||||
set-selected
|
||||
(mf/use-memo
|
||||
|
|
|
@ -129,9 +129,9 @@
|
|||
:file-id file-id
|
||||
:shared-libs shared-libs}])]))
|
||||
|
||||
(mf/defc options-content
|
||||
(mf/defc options-content*
|
||||
{::mf/memo true
|
||||
::mf/props :obj}
|
||||
::mf/private true}
|
||||
[{:keys [selected shapes shapes-with-children page-id file-id on-change-section on-expand]}]
|
||||
(let [objects (mf/deref refs/workspace-page-objects)
|
||||
permissions (mf/use-ctx ctx/permissions)
|
||||
|
@ -202,20 +202,19 @@
|
|||
;; selected-objects-with-children are derefed always but they only
|
||||
;; need on multiple selection in majority of cases
|
||||
|
||||
(mf/defc options-toolbox
|
||||
{::mf/memo true
|
||||
::mf/props :obj}
|
||||
(mf/defc options-toolbox*
|
||||
{::mf/memo true}
|
||||
[{:keys [section selected on-change-section on-expand]}]
|
||||
(let [page-id (mf/use-ctx ctx/current-page-id)
|
||||
file-id (mf/use-ctx ctx/current-file-id)
|
||||
shapes (mf/deref refs/selected-objects)
|
||||
shapes-with-children (mf/deref refs/selected-shapes-with-children)]
|
||||
|
||||
[:& options-content {:shapes shapes
|
||||
:selected selected
|
||||
:shapes-with-children shapes-with-children
|
||||
:file-id file-id
|
||||
:page-id page-id
|
||||
:section section
|
||||
:on-change-section on-change-section
|
||||
:on-expand on-expand}]))
|
||||
[:> options-content* {:shapes shapes
|
||||
:selected selected
|
||||
:shapes-with-children shapes-with-children
|
||||
:file-id file-id
|
||||
:page-id page-id
|
||||
:section section
|
||||
:on-change-section on-change-section
|
||||
:on-expand on-expand}]))
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
(if (not radius-expanded)
|
||||
[:div {:class (stl/css :radius-1)
|
||||
:title (tr "workspace.options.radius")}
|
||||
[:> icon* {:id "corner-radius"
|
||||
[:> icon* {:icon-id "corner-radius"
|
||||
:size "s"
|
||||
:class (stl/css :icon)}]
|
||||
[:> numeric-input*
|
||||
|
@ -116,8 +116,7 @@
|
|||
[:> icon-button* {:class (stl/css-case :selected radius-expanded)
|
||||
:variant "ghost"
|
||||
:on-click toggle-radius-mode
|
||||
:aria-label (tr "workspace.options.radius")
|
||||
:title (if radius-expanded
|
||||
(tr "workspace.options.radius.all-corners")
|
||||
(tr "workspace.options.radius.single-corners"))
|
||||
:aria-label (if radius-expanded
|
||||
(tr "workspace.options.radius.all-corners")
|
||||
(tr "workspace.options.radius.single-corners"))
|
||||
:icon "corner-radius"}]]))
|
|
@ -402,6 +402,7 @@
|
|||
|
||||
.component-swap {
|
||||
padding-top: $s-12;
|
||||
max-width: $s-248;
|
||||
}
|
||||
|
||||
.component-swap-content {
|
||||
|
|
|
@ -456,7 +456,7 @@
|
|||
type (if (= type "multiple") :simple :multiple)]
|
||||
(on-type-change type))))
|
||||
|
||||
props (mf/spread props {:on-change on-change})]
|
||||
props (mf/spread-object props {:on-change on-change})]
|
||||
|
||||
(mf/with-effect []
|
||||
;; on destroy component
|
||||
|
|
|
@ -43,8 +43,7 @@
|
|||
[prop]
|
||||
(select-margins (= prop :m1) (= prop :m2) (= prop :m3) (= prop :m4)))
|
||||
|
||||
(mf/defc margin-simple
|
||||
{::mf/props :obj}
|
||||
(mf/defc margin-simple*
|
||||
[{:keys [value on-change on-blur]}]
|
||||
(let [m1 (:m1 value)
|
||||
m2 (:m2 value)
|
||||
|
@ -103,8 +102,7 @@
|
|||
:nillable true
|
||||
:value m2}]]]))
|
||||
|
||||
(mf/defc margin-multiple
|
||||
{::mf/props :obj}
|
||||
(mf/defc margin-multiple*
|
||||
[{:keys [value on-change on-blur]}]
|
||||
(let [m1 (:m1 value)
|
||||
m2 (:m2 value)
|
||||
|
@ -182,14 +180,13 @@
|
|||
:value m4}]]]))
|
||||
|
||||
|
||||
(mf/defc margin-section
|
||||
{::mf/props :obj
|
||||
::mf/private true
|
||||
(mf/defc margin-section*
|
||||
{::mf/private true
|
||||
::mf/expect-props #{:value :type :on-type-change :on-change}}
|
||||
[{:keys [type on-type-change] :as props}]
|
||||
(let [type (d/nilv type :simple)
|
||||
on-blur (mf/use-fn #(select-margins false false false false))
|
||||
props (mf/spread props :on-blur on-blur)
|
||||
props (mf/spread-props props {:on-blur on-blur})
|
||||
|
||||
on-type-change'
|
||||
(mf/use-fn
|
||||
|
@ -206,10 +203,10 @@
|
|||
[:div {:class (stl/css :inputs-wrapper)}
|
||||
(cond
|
||||
(= type :simple)
|
||||
[:> margin-simple props]
|
||||
[:> margin-simple* props]
|
||||
|
||||
(= type :multiple)
|
||||
[:> margin-multiple props])]
|
||||
[:> margin-multiple* props])]
|
||||
|
||||
[:button {:class (stl/css-case
|
||||
:margin-mode true
|
||||
|
@ -500,10 +497,10 @@
|
|||
|
||||
(when is-layout-child?
|
||||
[:div {:class (stl/css :row)}
|
||||
[:& margin-section {:value (:layout-item-margin values)
|
||||
:type (:layout-item-margin-type values)
|
||||
:on-type-change on-margin-type-change
|
||||
:on-change on-margin-change}]])
|
||||
[:> margin-section* {:value (:layout-item-margin values)
|
||||
:type (:layout-item-margin-type values)
|
||||
:on-type-change on-margin-type-change
|
||||
:on-change on-margin-change}]])
|
||||
|
||||
(when (or (= h-sizing :fill)
|
||||
(= v-sizing :fill))
|
||||
|
|
|
@ -212,7 +212,7 @@
|
|||
(nil? color-name) (assoc
|
||||
:id nil
|
||||
:file-id nil))
|
||||
:mini? true
|
||||
:mini true
|
||||
:on-click handle-click-color}]]
|
||||
(cond
|
||||
;; Rendering a color with ID
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue