[CORL 212] Implement CharCount Restriction feature (#2204)

* feat: adhere to char count settings for posting top level comments

* fix: remove bidi characters during test

* test: add feature tests for charCount

* test: update snapshots

* test: add feature test for updating settings

* feat: use red color and warn icon (from review)

* feat: Implement charCount for reply

* feat: charCount for edit comment

* test: update snapshots
This commit is contained in:
Kiwi
2019-03-04 22:49:04 +01:00
committed by GitHub
parent 60f5b7e3c0
commit 625809b42c
61 changed files with 1488 additions and 607 deletions
@@ -65,7 +65,7 @@ exports[`accepts valid email 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -208,7 +208,7 @@ exports[`checks for invalid email 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -245,7 +245,7 @@ exports[`checks for invalid email 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -422,7 +422,7 @@ exports[`shows error when submitting empty form 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -459,7 +459,7 @@ exports[`shows error when submitting empty form 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -26,7 +26,7 @@ we require users to create a password.
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
@@ -65,12 +65,12 @@ we require users to create a password.
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
<span>
Password must contain at least 8 characters.
Password must contain at least 8 characters.
</span>
</div>
</div>
@@ -145,7 +145,7 @@ we require users to create a password.
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
@@ -227,7 +227,7 @@ we require users to create a password.
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
@@ -266,7 +266,7 @@ we require users to create a password.
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -329,7 +329,7 @@ GraphQL request (4:3)
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
@@ -406,7 +406,7 @@ we require users to create a password.
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
@@ -104,12 +104,12 @@ exports[`checks for invalid username 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
<span>
Username must contain at least 3 characters.
Username must contain at least 3 characters.
</span>
</div>
</div>
@@ -267,7 +267,7 @@ exports[`shows error when submitting empty form 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -83,7 +83,7 @@ exports[`show restricted screen for commenters 1`] = `
>
<span
aria-hidden="true"
className="Icon-root Restricted-lockIcon Icon-lg"
className="Icon-root Icon-lg Restricted-lockIcon"
>
lock
</span>
@@ -36,7 +36,7 @@ exports[`accepts correct password 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -99,7 +99,7 @@ exports[`accepts correct password 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -189,7 +189,7 @@ exports[`accepts valid email 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -210,7 +210,7 @@ exports[`accepts valid email 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -258,7 +258,7 @@ exports[`checks for invalid email 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -313,7 +313,7 @@ exports[`checks for invalid email 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -334,7 +334,7 @@ exports[`checks for invalid email 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -509,7 +509,7 @@ exports[`renders sign in form 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -565,7 +565,7 @@ exports[`shows error when submitting empty form 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -620,7 +620,7 @@ exports[`shows error when submitting empty form 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -641,7 +641,7 @@ exports[`shows error when submitting empty form 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -744,7 +744,7 @@ exports[`shows server error 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -1289,7 +1289,7 @@ needs to be displayed, e.g. “Log in with &lt;Facebook&gt;”.
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -1328,7 +1328,7 @@ needs to be displayed, e.g. “Log in with &lt;Facebook&gt;”.
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -1367,7 +1367,7 @@ needs to be displayed, e.g. “Log in with &lt;Facebook&gt;”.
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -1430,7 +1430,7 @@ the remaining fields. You may also enter the information manually.
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -1469,7 +1469,7 @@ the remaining fields. You may also enter the information manually.
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -1508,7 +1508,7 @@ the remaining fields. You may also enter the information manually.
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -1547,7 +1547,7 @@ the remaining fields. You may also enter the information manually.
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -1728,11 +1728,11 @@ exports[`regenerate sso key 1`] = `
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary SSOKeyField-keyGenerated"
>
KEY GENERATED AT:
11/12/2018, 11:26 PM
11/12/2018, 11:26 PM
</p>
<span
aria-hidden="true"
className="Icon-root SSOKeyField-warnIcon Icon-sm"
className="Icon-root Icon-sm SSOKeyField-warnIcon"
>
warning
</span>
@@ -2621,11 +2621,11 @@ integration to register for a new account.
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary SSOKeyField-keyGenerated"
>
KEY GENERATED AT:
Invalid Date
Invalid Date
</p>
<span
aria-hidden="true"
className="Icon-root SSOKeyField-warnIcon Icon-sm"
className="Icon-root Icon-sm SSOKeyField-warnIcon"
>
warning
</span>
@@ -202,7 +202,7 @@ the
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
This value can be set a percentage between 0 and 100. This number represents the likelihood that a
comment is toxic, according to Perspective API. By default the treshold is set to 80.
comment is toxic, according to Perspective API. By default the treshold is set to 80.
</p>
<div
className="TextField-root PerspectiveConfig-thresholdTextField"
@@ -351,7 +351,7 @@ improve the API over time
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
By default the endpoint is set to https://commentanalyzer.googleapis.com/v1alpha1. You may override this here.
By default the endpoint is set to https://commentanalyzer.googleapis.com/v1alpha1. You may override this here.
</p>
<div
className="TextField-root"
@@ -357,7 +357,7 @@ it("change comment length limitations", async () => {
expect(
within(generalContainer).queryAllByText(
"Please enter a whole number greater than 0"
"Please enter a whole number greater than 0"
).length
).toBe(2);
@@ -240,7 +240,7 @@ it("change perspective settings", async () => {
).toBe(1);
expect(
within(perspectiveContainer).queryAllByText(
"Please enter a whole number between 0 and 100."
"Please enter a whole number between 0 and 100."
).length
).toBe(1);
@@ -39,7 +39,7 @@ exports[`loads more 1`] = `
>
<span
aria-hidden="true"
className="Icon-root AcceptedIcon-root Icon-sm"
className="Icon-root Icon-sm AcceptedIcon-root"
>
check
</span>
@@ -103,7 +103,7 @@ exports[`loads more 1`] = `
>
<span
aria-hidden="true"
className="Icon-root RejectedIcon-root Icon-sm"
className="Icon-root Icon-sm RejectedIcon-root"
>
cancel
</span>
@@ -167,7 +167,7 @@ exports[`loads more 1`] = `
>
<span
aria-hidden="true"
className="Icon-root AcceptedIcon-root Icon-sm"
className="Icon-root Icon-sm AcceptedIcon-root"
>
check
</span>
@@ -296,7 +296,7 @@ exports[`render popover content 1`] = `
>
<span
aria-hidden="true"
className="Icon-root AcceptedIcon-root Icon-sm"
className="Icon-root Icon-sm AcceptedIcon-root"
>
check
</span>
@@ -360,7 +360,7 @@ exports[`render popover content 1`] = `
>
<span
aria-hidden="true"
className="Icon-root RejectedIcon-root Icon-sm"
className="Icon-root Icon-sm RejectedIcon-root"
>
cancel
</span>
@@ -252,7 +252,7 @@ exports[`rejected queue accepts comment in rejected queue: dangling 1`] = `
>
<span
aria-hidden="true"
className="Icon-root RejectButton-icon Icon-lg"
className="Icon-root Icon-lg RejectButton-icon"
>
close
</span>
@@ -271,7 +271,7 @@ exports[`rejected queue accepts comment in rejected queue: dangling 1`] = `
>
<span
aria-hidden="true"
className="Icon-root AcceptButton-icon Icon-lg"
className="Icon-root Icon-lg AcceptButton-icon"
>
done
</span>
@@ -405,7 +405,7 @@ exports[`rejected queue renders rejected queue with comments 1`] = `
>
<span
aria-hidden="true"
className="Icon-root RejectButton-icon Icon-lg"
className="Icon-root Icon-lg RejectButton-icon"
>
close
</span>
@@ -424,7 +424,7 @@ exports[`rejected queue renders rejected queue with comments 1`] = `
>
<span
aria-hidden="true"
className="Icon-root AcceptButton-icon Icon-lg"
className="Icon-root Icon-lg AcceptButton-icon"
>
done
</span>
@@ -545,7 +545,7 @@ exports[`rejected queue renders rejected queue with comments 1`] = `
>
<span
aria-hidden="true"
className="Icon-root RejectButton-icon Icon-lg"
className="Icon-root Icon-lg RejectButton-icon"
>
close
</span>
@@ -564,7 +564,7 @@ exports[`rejected queue renders rejected queue with comments 1`] = `
>
<span
aria-hidden="true"
className="Icon-root AcceptButton-icon Icon-lg"
className="Icon-root Icon-lg AcceptButton-icon"
>
done
</span>
@@ -705,7 +705,7 @@ exports[`rejected queue renders rejected queue with comments and load more 1`] =
>
<span
aria-hidden="true"
className="Icon-root RejectButton-icon Icon-lg"
className="Icon-root Icon-lg RejectButton-icon"
>
close
</span>
@@ -724,7 +724,7 @@ exports[`rejected queue renders rejected queue with comments and load more 1`] =
>
<span
aria-hidden="true"
className="Icon-root AcceptButton-icon Icon-lg"
className="Icon-root Icon-lg AcceptButton-icon"
>
done
</span>
@@ -861,7 +861,7 @@ exports[`reported queue accepts comment in reported queue: dangling 1`] = `
>
<span
aria-hidden="true"
className="Icon-root RejectButton-icon Icon-lg"
className="Icon-root Icon-lg RejectButton-icon"
>
close
</span>
@@ -880,7 +880,7 @@ exports[`reported queue accepts comment in reported queue: dangling 1`] = `
>
<span
aria-hidden="true"
className="Icon-root AcceptButton-icon Icon-lg"
className="Icon-root Icon-lg AcceptButton-icon"
>
done
</span>
@@ -1017,7 +1017,7 @@ exports[`reported queue rejects comment in reported queue: dangling 1`] = `
>
<span
aria-hidden="true"
className="Icon-root RejectButton-icon Icon-lg"
className="Icon-root Icon-lg RejectButton-icon"
>
close
</span>
@@ -1036,7 +1036,7 @@ exports[`reported queue rejects comment in reported queue: dangling 1`] = `
>
<span
aria-hidden="true"
className="Icon-root AcceptButton-icon Icon-lg"
className="Icon-root Icon-lg AcceptButton-icon"
>
done
</span>
@@ -1185,7 +1185,7 @@ exports[`reported queue renders reported queue with comments 1`] = `
>
<span
aria-hidden="true"
className="Icon-root RejectButton-icon Icon-lg"
className="Icon-root Icon-lg RejectButton-icon"
>
close
</span>
@@ -1204,7 +1204,7 @@ exports[`reported queue renders reported queue with comments 1`] = `
>
<span
aria-hidden="true"
className="Icon-root AcceptButton-icon Icon-lg"
className="Icon-root Icon-lg AcceptButton-icon"
>
done
</span>
@@ -1325,7 +1325,7 @@ exports[`reported queue renders reported queue with comments 1`] = `
>
<span
aria-hidden="true"
className="Icon-root RejectButton-icon Icon-lg"
className="Icon-root Icon-lg RejectButton-icon"
>
close
</span>
@@ -1344,7 +1344,7 @@ exports[`reported queue renders reported queue with comments 1`] = `
>
<span
aria-hidden="true"
className="Icon-root AcceptButton-icon Icon-lg"
className="Icon-root Icon-lg AcceptButton-icon"
>
done
</span>
@@ -1489,7 +1489,7 @@ exports[`reported queue renders reported queue with comments and load more 1`] =
>
<span
aria-hidden="true"
className="Icon-root RejectButton-icon Icon-lg"
className="Icon-root Icon-lg RejectButton-icon"
>
close
</span>
@@ -1508,7 +1508,7 @@ exports[`reported queue renders reported queue with comments and load more 1`] =
>
<span
aria-hidden="true"
className="Icon-root AcceptButton-icon Icon-lg"
className="Icon-root Icon-lg AcceptButton-icon"
>
done
</span>
@@ -1632,7 +1632,7 @@ exports[`single comment view accepts single comment 1`] = `
>
<span
aria-hidden="true"
className="Icon-root RejectButton-icon Icon-lg"
className="Icon-root Icon-lg RejectButton-icon"
>
close
</span>
@@ -1651,7 +1651,7 @@ exports[`single comment view accepts single comment 1`] = `
>
<span
aria-hidden="true"
className="Icon-root AcceptButton-icon Icon-lg"
className="Icon-root Icon-lg AcceptButton-icon"
>
done
</span>
@@ -1775,7 +1775,7 @@ exports[`single comment view rejects single comment 1`] = `
>
<span
aria-hidden="true"
className="Icon-root RejectButton-icon Icon-lg"
className="Icon-root Icon-lg RejectButton-icon"
>
close
</span>
@@ -1794,7 +1794,7 @@ exports[`single comment view rejects single comment 1`] = `
>
<span
aria-hidden="true"
className="Icon-root AcceptButton-icon Icon-lg"
className="Icon-root Icon-lg AcceptButton-icon"
>
done
</span>
@@ -1953,7 +1953,7 @@ exports[`single comment view renders single comment view 1`] = `
>
<span
aria-hidden="true"
className="Icon-root RejectButton-icon Icon-lg"
className="Icon-root Icon-lg RejectButton-icon"
>
close
</span>
@@ -1972,7 +1972,7 @@ exports[`single comment view renders single comment view 1`] = `
>
<span
aria-hidden="true"
className="Icon-root AcceptButton-icon Icon-lg"
className="Icon-root Icon-lg AcceptButton-icon"
>
done
</span>
@@ -134,7 +134,7 @@ Your email address will be used to:
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -415,7 +415,7 @@ Your email address will be used to:
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -452,7 +452,7 @@ Your email address will be used to:
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -755,7 +755,7 @@ Your email address will be used to:
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -792,7 +792,7 @@ Your email address will be used to:
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -26,7 +26,7 @@ we require users to create a password.
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
@@ -65,12 +65,12 @@ we require users to create a password.
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
<span>
Password must contain at least 8 characters.
Password must contain at least 8 characters.
</span>
</div>
</div>
@@ -135,7 +135,7 @@ we require users to create a password.
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
@@ -215,7 +215,7 @@ we require users to create a password.
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
@@ -254,7 +254,7 @@ we require users to create a password.
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -317,7 +317,7 @@ GraphQL request (4:3)
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
@@ -394,7 +394,7 @@ we require users to create a password.
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
@@ -104,12 +104,12 @@ exports[`checks for invalid username 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
<span>
Username must contain at least 3 characters.
Username must contain at least 3 characters.
</span>
</div>
</div>
@@ -255,7 +255,7 @@ exports[`shows error when submitting empty form 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -36,7 +36,7 @@ exports[`accepts correct password 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -117,7 +117,7 @@ exports[`accepts correct password 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -207,7 +207,7 @@ exports[`accepts valid email 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -246,7 +246,7 @@ exports[`accepts valid email 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -369,7 +369,7 @@ exports[`auth configuration renders all auth enabled 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -457,7 +457,7 @@ exports[`auth configuration renders all auth enabled 1`] = `
type="button"
>
<span>
Sign in with OIDC
Sign in with OIDC
</span>
</button>
</div>
@@ -569,7 +569,7 @@ exports[`checks for invalid email 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -624,7 +624,7 @@ exports[`checks for invalid email 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -663,7 +663,7 @@ exports[`checks for invalid email 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -832,7 +832,7 @@ exports[`renders sign in view 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -1012,7 +1012,7 @@ exports[`renders sign in view with error 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -1066,7 +1066,7 @@ exports[`shows error when submitting empty form 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -1121,7 +1121,7 @@ exports[`shows error when submitting empty form 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -1160,7 +1160,7 @@ exports[`shows error when submitting empty form 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -1281,7 +1281,7 @@ exports[`shows server error 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -1397,7 +1397,7 @@ exports[`submits form successfully 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -43,7 +43,7 @@ exports[`accepts correct password 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -85,7 +85,7 @@ exports[`accepts correct password 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -106,7 +106,7 @@ exports[`accepts correct password 1`] = `
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
@@ -153,7 +153,7 @@ exports[`accepts correct password 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -242,7 +242,7 @@ exports[`accepts valid email 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -263,7 +263,7 @@ exports[`accepts valid email 1`] = `
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
@@ -302,7 +302,7 @@ exports[`accepts valid email 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -323,7 +323,7 @@ exports[`accepts valid email 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -383,7 +383,7 @@ exports[`accepts valid username 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -433,7 +433,7 @@ exports[`accepts valid username 1`] = `
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
@@ -472,7 +472,7 @@ exports[`accepts valid username 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -493,7 +493,7 @@ exports[`accepts valid username 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -590,7 +590,7 @@ exports[`auth configuration renders all auth enabled 1`] = `
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
@@ -637,7 +637,7 @@ exports[`auth configuration renders all auth enabled 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -725,7 +725,7 @@ exports[`auth configuration renders all auth enabled 1`] = `
type="button"
>
<span>
Sign up with OIDC
Sign up with OIDC
</span>
</button>
</div>
@@ -844,7 +844,7 @@ exports[`checks for invalid characters in username 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -886,7 +886,7 @@ exports[`checks for invalid characters in username 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -907,7 +907,7 @@ exports[`checks for invalid characters in username 1`] = `
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
@@ -946,7 +946,7 @@ exports[`checks for invalid characters in username 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -967,7 +967,7 @@ exports[`checks for invalid characters in username 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -1027,7 +1027,7 @@ exports[`checks for invalid email 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -1069,7 +1069,7 @@ exports[`checks for invalid email 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -1090,7 +1090,7 @@ exports[`checks for invalid email 1`] = `
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
@@ -1129,7 +1129,7 @@ exports[`checks for invalid email 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -1150,7 +1150,7 @@ exports[`checks for invalid email 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -1210,7 +1210,7 @@ exports[`checks for too long username 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -1252,12 +1252,12 @@ exports[`checks for too long username 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
<span>
Usernames cannot be longer than 30 characters.
Usernames cannot be longer than 30 characters.
</span>
</div>
</div>
@@ -1273,7 +1273,7 @@ exports[`checks for too long username 1`] = `
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
@@ -1312,7 +1312,7 @@ exports[`checks for too long username 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -1333,7 +1333,7 @@ exports[`checks for too long username 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -1393,7 +1393,7 @@ exports[`checks for too short password 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -1435,7 +1435,7 @@ exports[`checks for too short password 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -1456,7 +1456,7 @@ exports[`checks for too short password 1`] = `
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
@@ -1495,12 +1495,12 @@ exports[`checks for too short password 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
<span>
Password must contain at least 8 characters.
Password must contain at least 8 characters.
</span>
</div>
</div>
@@ -1516,7 +1516,7 @@ exports[`checks for too short password 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -1576,7 +1576,7 @@ exports[`checks for too short username 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -1618,12 +1618,12 @@ exports[`checks for too short username 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
<span>
Username must contain at least 3 characters.
Username must contain at least 3 characters.
</span>
</div>
</div>
@@ -1639,7 +1639,7 @@ exports[`checks for too short username 1`] = `
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
@@ -1678,7 +1678,7 @@ exports[`checks for too short username 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -1699,7 +1699,7 @@ exports[`checks for too short username 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -1842,7 +1842,7 @@ exports[`renders sign up form 1`] = `
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
@@ -1889,7 +1889,7 @@ exports[`renders sign up form 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -1951,7 +1951,7 @@ exports[`shows error when submitting empty form 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -1993,7 +1993,7 @@ exports[`shows error when submitting empty form 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -2014,7 +2014,7 @@ exports[`shows error when submitting empty form 1`] = `
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
@@ -2053,7 +2053,7 @@ exports[`shows error when submitting empty form 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -2074,7 +2074,7 @@ exports[`shows error when submitting empty form 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -2176,7 +2176,7 @@ exports[`shows server error 1`] = `
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
@@ -2223,7 +2223,7 @@ exports[`shows server error 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
@@ -2320,7 +2320,7 @@ exports[`submits form successfully 1`] = `
<p
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
Must be at least 8 characters
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
@@ -2367,7 +2367,7 @@ exports[`submits form successfully 1`] = `
>
<span
aria-hidden="true"
className="Icon-root ButtonIcon-root Icon-md"
className="Icon-root Icon-md ButtonIcon-root"
>
email
</span>
+10 -4
View File
@@ -88,18 +88,24 @@ export const validateURL = createValidator(
/**
* validateMinLength is a Validator that checks that the field has a min length of characters
*/
export const validateMinLength = (minLength: number) =>
export const validateMinLength = (
minLength: number,
getLength: (v: any) => number = v => v.length
) =>
createValidator(
v => !v || v.length >= minLength,
v => !v || getLength(v) >= minLength,
VALIDATION_TOO_SHORT(minLength)
);
/**
* validateMaxLength is a Validator that checks that the field has max length of characters
*/
export const validateMaxLength = (maxLength: number) =>
export const validateMaxLength = (
maxLength: number,
getLength: (v: any) => number = v => v.length
) =>
createValidator(
v => !v || v.length <= maxLength,
v => !v || getLength(v) <= maxLength,
VALIDATION_TOO_LONG(maxLength)
);
@@ -38,7 +38,10 @@ function createFluentBundle(
target: string,
pathToLocale: string
): FluentBundle {
const bundle = new FluentBundle("en-US", { functions });
// `useIsolating: false` will remove bidi characterse.
// See https://github.com/projectfluent/fluent.js/commit/41e5445d2e399f090306c5b9a084bcce5111bbd3
// And https://www.w3.org/International/questions/qa-bidi-unicode-controls
const bundle = new FluentBundle("en-US", { functions, useIsolating: false });
const files = fs.readdirSync(pathToLocale);
const prefixes = commonPrefixes.concat(target);
files.forEach(f => {
@@ -25,6 +25,7 @@ export default function matchText(
a = a.replace(/\s+/g, " ");
b = b.replace(/\s+/g, " ");
}
if (options.exact || options.exact === undefined) {
return a === b;
}
@@ -0,0 +1,31 @@
import { graphql } from "react-relay";
import { Environment } from "relay-runtime";
import { createFetchContainer, fetchQuery } from "talk-framework/lib/relay";
import { RefreshSettingsQuery as QueryTypes } from "talk-stream/__generated__/RefreshSettingsQuery.graphql";
const query = graphql`
query RefreshSettingsQuery {
settings {
...StreamContainer_settings
}
}
`;
function fetch(environment: Environment) {
return fetchQuery<QueryTypes["response"]["settings"]>(
environment,
query,
{},
{ force: true }
);
}
export const withRefreshSettingsFetch = createFetchContainer(
"refreshSettings",
fetch
);
export type RefreshSettingsFetch = () => Promise<
QueryTypes["response"]["settings"]
>;
+4
View File
@@ -0,0 +1,4 @@
export {
withRefreshSettingsFetch,
RefreshSettingsFetch,
} from "./RefreshSettingsQuery";
@@ -9,7 +9,6 @@ import React, {
import { Field, Form } from "react-final-form";
import { OnSubmit } from "talk-framework/lib/form";
import { required } from "talk-framework/lib/validation";
import Timestamp from "talk-stream/components/Timestamp";
import {
AriaInfo,
@@ -23,6 +22,8 @@ import {
ValidationMessage,
} from "talk-ui/components";
import RemainingCharactersContainer from "../containers/RemainingCharactersContainer";
import { cleanupRTEEmptyHTML, getCommentBodyValidators } from "../helpers";
import { TopBarLeft, Username } from "./Comment";
import RTE from "./RTE";
@@ -44,19 +45,15 @@ export interface EditCommentFormProps {
initialValues?: FormProps;
rteRef?: Ref<CoralRTE>;
expired?: boolean;
min: number | null;
max: number | null;
}
const EditCommentForm: StatelessComponent<EditCommentFormProps> = props => {
const inputID = `comments-editCommentForm-rte-${props.id}`;
return (
<Form onSubmit={props.onSubmit} initialValues={props.initialValues}>
{({
handleSubmit,
submitting,
hasValidationErrors,
pristine,
submitError,
}) => (
{({ handleSubmit, submitting, pristine, submitError }) => (
<form
className={props.className}
autoComplete="off"
@@ -72,102 +69,125 @@ const EditCommentForm: StatelessComponent<EditCommentFormProps> = props => {
<Timestamp>{props.createdAt}</Timestamp>
</TopBarLeft>
</div>
<Field name="body" validate={required}>
<Field
name="body"
validate={getCommentBodyValidators(props.min, props.max)}
>
{({ input, meta }) => (
<HorizontalGutter size="half">
<Localized id="comments-editCommentForm-rteLabel">
<AriaInfo component="label" htmlFor={inputID}>
Edit comment
</AriaInfo>
</Localized>
<Localized
id="comments-editCommentForm-rte"
attrs={{ placeholder: true }}
>
<RTE
inputId={inputID}
onChange={({ html }) => input.onChange(html)}
value={input.value}
placeholder="Edit comment"
forwardRef={props.rteRef}
disabled={submitting || props.expired}
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
<>
<HorizontalGutter size="half">
<Localized id="comments-editCommentForm-rteLabel">
<AriaInfo component="label" htmlFor={inputID}>
Edit comment
</AriaInfo>
</Localized>
<Localized
id="comments-editCommentForm-rte"
attrs={{ placeholder: true }}
>
<RTE
inputId={inputID}
onChange={({ html }) =>
input.onChange(cleanupRTEEmptyHTML(html))
}
value={input.value}
placeholder="Edit comment"
forwardRef={props.rteRef}
disabled={submitting || props.expired}
/>
</Localized>
{props.expired ? (
<Localized id="comments-editCommentForm-editTimeExpired">
<ValidationMessage fullWidth>
Edit time has expired. You can no longer edit this
comment. Why not post another one?
</ValidationMessage>
</Localized>
) : (
<>
<Message fullWidth>
<MessageIcon>alarm</MessageIcon>
<Localized
id="comments-editCommentForm-editRemainingTime"
time={
<RelativeTime date={props.editableUntil} live />
}
>
<span>{"Edit: <time></time> remaining"}</span>
</Localized>
</Message>
{meta.touched &&
(meta.error ||
(meta.submitError &&
!meta.dirtySinceLastSubmit)) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{submitError && (
<ValidationMessage fullWidth>
{submitError}
</ValidationMessage>
)}
{props.max && (
<RemainingCharactersContainer
value={input.value}
max={props.max}
/>
)}
</>
)}
{submitError && (
<ValidationMessage fullWidth>
{submitError}
</ValidationMessage>
)}
</HorizontalGutter>
)}
</Field>
{props.expired ? (
<Localized id="comments-editCommentForm-editTimeExpired">
<ValidationMessage fullWidth>
Edit time has expired. You can no longer edit this comment.
Why not post another one?
</ValidationMessage>
</Localized>
) : (
<Message fullWidth>
<MessageIcon>alarm</MessageIcon>
<Localized
id="comments-editCommentForm-editRemainingTime"
time={<RelativeTime date={props.editableUntil} live />}
>
<span>{"Edit: <time></time> remaining"}</span>
</Localized>
</Message>
)}
<Flex direction="row" justifyContent="flex-end" itemGutter="half">
{props.expired ? (
<Localized id="comments-editCommentForm-close">
<Button
variant="outlined"
disabled={submitting}
onClick={props.onClose}
</HorizontalGutter>
<Flex
direction="row"
justifyContent="flex-end"
itemGutter="half"
>
Close
</Button>
</Localized>
) : (
<MatchMedia ltWidth="sm">
{matches => (
<>
<Localized id="comments-editCommentForm-cancel">
{props.expired ? (
<Localized id="comments-editCommentForm-close">
<Button
variant="outlined"
disabled={submitting}
onClick={props.onCancel}
fullWidth={matches}
onClick={props.onClose}
>
Cancel
Close
</Button>
</Localized>
<Localized id="comments-editCommentForm-saveChanges">
<Button
color="primary"
variant="filled"
disabled={
submitting || hasValidationErrors || pristine
}
type="submit"
fullWidth={matches}
>
Save Changes
</Button>
</Localized>
</>
)}
</MatchMedia>
) : (
<MatchMedia ltWidth="sm">
{matches => (
<>
<Localized id="comments-editCommentForm-cancel">
<Button
variant="outlined"
disabled={submitting}
onClick={props.onCancel}
fullWidth={matches}
>
Cancel
</Button>
</Localized>
<Localized id="comments-editCommentForm-saveChanges">
<Button
color="primary"
variant="filled"
disabled={
submitting || !input.value || pristine
}
type="submit"
fullWidth={matches}
>
Save Changes
</Button>
</Localized>
</>
)}
</MatchMedia>
)}
</Flex>
</>
)}
</Flex>
</Field>
</HorizontalGutter>
</form>
)}
@@ -1,16 +1,16 @@
import { FormState } from "final-form";
import { FormApi, FormState } from "final-form";
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field, Form, FormSpy } from "react-final-form";
import ValidationMessage from "talk-admin/routes/configure/components/ValidationMessage";
import { OnSubmit } from "talk-framework/lib/form";
import { required } from "talk-framework/lib/validation";
import { AriaInfo, Button, Flex, HorizontalGutter } from "talk-ui/components";
import PoweredBy from "./PoweredBy";
import RemainingCharactersContainer from "../containers/RemainingCharactersContainer";
import { cleanupRTEEmptyHTML, getCommentBodyValidators } from "../helpers";
import RTE from "./RTE";
import ValidationMessage from "talk-admin/routes/configure/components/ValidationMessage";
import styles from "./PostCommentForm.css";
interface FormProps {
@@ -19,73 +19,88 @@ interface FormProps {
export interface PostCommentFormProps {
onSubmit: OnSubmit<FormProps>;
onChange?: (state: FormState) => void;
onChange?: (state: FormState, form: FormApi) => void;
initialValues?: FormProps;
min: number | null;
max: number | null;
}
const PostCommentForm: StatelessComponent<PostCommentFormProps> = props => (
<Form onSubmit={props.onSubmit} initialValues={props.initialValues}>
{({ handleSubmit, submitting, hasValidationErrors, submitError }) => (
{({ handleSubmit, submitting, submitError, form }) => (
<form
autoComplete="off"
onSubmit={handleSubmit}
className={styles.root}
id="comments-postCommentForm-form"
>
<FormSpy onChange={props.onChange} />
<FormSpy
onChange={state => props.onChange && props.onChange(state, form)}
/>
<HorizontalGutter>
<Field name="body" validate={required}>
<Field
name="body"
validate={getCommentBodyValidators(props.min, props.max)}
>
{({ input, meta }) => (
<HorizontalGutter size="half">
<Localized id="comments-postCommentForm-rteLabel">
<AriaInfo
component="label"
htmlFor="comments-postCommentForm-field"
<>
<HorizontalGutter size="half">
<Localized id="comments-postCommentForm-rteLabel">
<AriaInfo
component="label"
htmlFor="comments-postCommentForm-field"
>
Post a comment
</AriaInfo>
</Localized>
<Localized
id="comments-postCommentForm-rte"
attrs={{ placeholder: true }}
>
Post a comment
</AriaInfo>
</Localized>
<Localized
id="comments-postCommentForm-rte"
attrs={{ placeholder: true }}
>
<RTE
inputId="comments-postCommentForm-field"
onChange={({ html }) => input.onChange(html)}
value={input.value}
placeholder="Post a comment"
disabled={submitting}
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<RTE
inputId="comments-postCommentForm-field"
onChange={({ html }) =>
input.onChange(cleanupRTEEmptyHTML(html))
}
value={input.value}
placeholder="Post a comment"
disabled={submitting}
/>
</Localized>
{meta.touched &&
(meta.error ||
(meta.submitError && !meta.dirtySinceLastSubmit)) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{submitError && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
{submitError}
</ValidationMessage>
)}
{submitError && (
<ValidationMessage fullWidth>{submitError}</ValidationMessage>
)}
</HorizontalGutter>
{props.max && (
<RemainingCharactersContainer
value={input.value}
max={props.max}
/>
)}
</HorizontalGutter>
<Flex direction="column" alignItems="flex-end">
<Localized id="comments-postCommentForm-submit">
<Button
color="primary"
variant="filled"
disabled={submitting || !input.value}
type="submit"
>
Submit
</Button>
</Localized>
</Flex>
</>
)}
</Field>
<Flex
direction="row"
justifyContent="space-between"
alignItems="flex-start"
>
<PoweredBy className={styles.poweredBy} />
<Localized id="comments-postCommentForm-submit">
<Button
color="primary"
variant="filled"
disabled={submitting || hasValidationErrors}
type="submit"
>
Submit
</Button>
</Localized>
</Flex>
</HorizontalGutter>
</form>
)}
@@ -0,0 +1,34 @@
import cn from "classnames";
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Flex, Icon, Typography } from "talk-ui/components";
interface Props {
className?: string;
remaining: number;
}
const RemainingCharacters: StatelessComponent<Props> = props => {
const belowZero = props.remaining < 0;
return (
<Flex
className={cn(props.className)}
alignItems="center"
justifyContent="flex-end"
itemGutter="half"
>
{belowZero && <Icon color="error">warning</Icon>}
<Localized id="comments-remainingCharacters" $remaining={props.remaining}>
<Typography
variant="detail"
container="div"
color={belowZero ? "error" : "textSecondary"}
>
{"X characters remaining"}
</Typography>
</Localized>
</Flex>
);
};
export default RemainingCharacters;
@@ -1,5 +1,5 @@
import { CoralRTE } from "@coralproject/rte";
import { FormState } from "final-form";
import { FormApi, FormState } from "final-form";
import { Localized } from "fluent-react/compat";
import React, {
EventHandler,
@@ -10,7 +10,6 @@ import React, {
import { Field, Form, FormSpy } from "react-final-form";
import { OnSubmit } from "talk-framework/lib/form";
import { required } from "talk-framework/lib/validation";
import {
AriaInfo,
Button,
@@ -20,6 +19,8 @@ import {
ValidationMessage,
} from "talk-ui/components";
import RemainingCharactersContainer from "../containers/RemainingCharactersContainer";
import { cleanupRTEEmptyHTML, getCommentBodyValidators } from "../helpers";
import ReplyTo from "./ReplyTo";
import RTE from "./RTE";
@@ -32,96 +33,115 @@ export interface ReplyCommentFormProps {
className?: string;
onSubmit: OnSubmit<FormProps>;
onCancel?: EventHandler<MouseEvent<any>>;
onChange?: (state: FormState) => void;
onChange?: (state: FormState, form: FormApi) => void;
initialValues?: FormProps;
rteRef?: Ref<CoralRTE>;
parentUsername: string | null;
min: number | null;
max: number | null;
}
const ReplyCommentForm: StatelessComponent<ReplyCommentFormProps> = props => {
const inputID = `comments-replyCommentForm-rte-${props.id}`;
return (
<Form onSubmit={props.onSubmit} initialValues={props.initialValues}>
{({ handleSubmit, submitting, hasValidationErrors, submitError }) => (
{({ handleSubmit, submitting, form, submitError }) => (
<form
className={props.className}
autoComplete="off"
onSubmit={handleSubmit}
id={`comments-replyCommentForm-form-${props.id}`}
>
<FormSpy onChange={props.onChange} />
<FormSpy
onChange={state => props.onChange && props.onChange(state, form)}
/>
<HorizontalGutter>
<Field name="body" validate={required}>
<Field
name="body"
validate={getCommentBodyValidators(props.min, props.max)}
>
{({ input, meta }) => (
<HorizontalGutter size="half">
<div>
<Localized id="comments-replyCommentForm-rteLabel">
<AriaInfo component="label" htmlFor={inputID}>
Write a reply
</AriaInfo>
</Localized>
{props.parentUsername && (
<ReplyTo username={props.parentUsername} />
)}
<Localized
id="comments-replyCommentForm-rte"
attrs={{ placeholder: true }}
>
<RTE
inputId={inputID}
onChange={({ html }) => input.onChange(html)}
value={input.value}
placeholder="Write a reply"
forwardRef={props.rteRef}
disabled={submitting}
/>
</Localized>
</div>
{meta.touched &&
(meta.error || meta.submitError) && (
<>
<HorizontalGutter size="half">
<div>
<Localized id="comments-replyCommentForm-rteLabel">
<AriaInfo component="label" htmlFor={inputID}>
Write a reply
</AriaInfo>
</Localized>
{props.parentUsername && (
<ReplyTo username={props.parentUsername} />
)}
<Localized
id="comments-replyCommentForm-rte"
attrs={{ placeholder: true }}
>
<RTE
inputId={inputID}
onChange={({ html }) =>
input.onChange(cleanupRTEEmptyHTML(html))
}
value={input.value}
placeholder="Write a reply"
forwardRef={props.rteRef}
disabled={submitting}
/>
</Localized>
</div>
{meta.touched &&
(meta.error ||
(meta.submitError && !meta.dirtySinceLastSubmit)) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{submitError && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
{submitError}
</ValidationMessage>
)}
{submitError && (
<ValidationMessage fullWidth>
{submitError}
</ValidationMessage>
)}
</HorizontalGutter>
{props.max && (
<RemainingCharactersContainer
value={input.value}
max={props.max}
/>
)}
</HorizontalGutter>
<MatchMedia ltWidth="sm">
{matches => (
<Flex
direction="row"
justifyContent="flex-end"
itemGutter="half"
>
<Localized id="comments-replyCommentForm-cancel">
<Button
variant="outlined"
disabled={submitting}
onClick={props.onCancel}
fullWidth={matches}
>
Cancel
</Button>
</Localized>
<Localized id="comments-replyCommentForm-submit">
<Button
color="primary"
variant="filled"
disabled={submitting || !input.value}
type="submit"
fullWidth={matches}
>
Submit
</Button>
</Localized>
</Flex>
)}
</MatchMedia>
</>
)}
</Field>
<MatchMedia ltWidth="sm">
{matches => (
<Flex
direction="row"
justifyContent="flex-end"
itemGutter="half"
>
<Localized id="comments-replyCommentForm-cancel">
<Button
variant="outlined"
disabled={submitting}
onClick={props.onCancel}
fullWidth={matches}
>
Cancel
</Button>
</Localized>
<Localized id="comments-replyCommentForm-submit">
<Button
color="primary"
variant="filled"
disabled={submitting || hasValidationErrors}
type="submit"
fullWidth={matches}
>
Submit
</Button>
</Localized>
</Flex>
)}
</MatchMedia>
</HorizontalGutter>
</form>
)}
@@ -24,7 +24,8 @@ export interface StreamProps {
settings: PropTypesOf<typeof CommentContainer>["settings"] &
PropTypesOf<typeof ReplyListContainer>["settings"] &
PropTypesOf<typeof UserBoxContainer>["settings"] &
PropTypesOf<typeof CommunityGuidelinesContainer>["settings"];
PropTypesOf<typeof CommunityGuidelinesContainer>["settings"] &
PropTypesOf<typeof PostCommentFormContainer>["settings"];
comments: ReadonlyArray<
{ id: string } & PropTypesOf<typeof CommentContainer>["comment"] &
PropTypesOf<typeof ReplyListContainer>["comment"]
@@ -49,7 +50,10 @@ const Stream: StatelessComponent<StreamProps> = props => {
<UserBoxContainer me={props.me} settings={props.settings} />
<CommunityGuidelinesContainer settings={props.settings} />
{props.me ? (
<PostCommentFormContainer storyID={props.story.id} />
<PostCommentFormContainer
storyID={props.story.id}
settings={props.settings}
/>
) : (
<PostCommentFormFake />
)}
@@ -472,7 +472,15 @@ exports[`when use is logged in renders correctly 1`] = `
}
}
/>
<withContext(withContext(createMutationContainer(PostCommentFormContainer)))
<withContext(withContext(createMutationContainer(withContext(createFetchContainer(Relay(PostCommentFormContainer))))))
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
}
storyID="story-id"
/>
</ForwardRef(forwardRef)>
@@ -151,6 +151,7 @@ export class CommentContainer extends Component<Props, State> {
return (
<div data-testid={`comment-${comment.id}`}>
<EditCommentFormContainer
settings={settings}
comment={comment}
onClose={this.closeEditDialog}
/>
@@ -231,6 +232,7 @@ export class CommentContainer extends Component<Props, State> {
/>
{showReplyDialog && (
<ReplyCommentFormContainer
settings={settings}
comment={comment}
story={story}
onClose={this.closeReplyDialog}
@@ -288,6 +290,8 @@ const enhanced = withSetCommentIDMutation(
settings: graphql`
fragment CommentContainer_settings on Settings {
...ReactionButtonContainer_settings
...ReplyCommentFormContainer_settings
...EditCommentFormContainer_settings
}
`,
})(CommentContainer)
@@ -7,8 +7,13 @@ import { withContext } from "talk-framework/lib/bootstrap";
import { InvalidRequestError } from "talk-framework/lib/errors";
import { withFragmentContainer } from "talk-framework/lib/relay";
import { PropTypesOf } from "talk-framework/types";
import {
RefreshSettingsFetch,
withRefreshSettingsFetch,
} from "talk-stream/fetches";
import { EditCommentFormContainer_comment as CommentData } from "talk-stream/__generated__/EditCommentFormContainer_comment.graphql";
import { EditCommentFormContainer_settings as SettingsData } from "talk-stream/__generated__/EditCommentFormContainer_settings.graphql";
import {
EditCommentMutation,
withEditCommentMutation,
@@ -17,12 +22,15 @@ import {
import EditCommentForm, {
EditCommentFormProps,
} from "../components/EditCommentForm";
import { shouldTriggerSettingsRefresh } from "../helpers";
interface Props {
editComment: EditCommentMutation;
comment: CommentData;
settings: SettingsData;
onClose?: () => void;
autofocus: boolean;
refreshSettings: RefreshSettingsFetch;
}
interface State {
@@ -83,6 +91,9 @@ export class EditCommentFormContainer extends Component<Props, State> {
}
} catch (error) {
if (error instanceof InvalidRequestError) {
if (shouldTriggerSettingsRefresh(error.code)) {
await this.props.refreshSettings();
}
return error.invalidArgs;
}
// tslint:disable-next-line:no-console
@@ -104,6 +115,16 @@ export class EditCommentFormContainer extends Component<Props, State> {
createdAt={this.props.comment.createdAt}
editableUntil={this.props.comment.editing.editableUntil}
expired={this.state.expired}
min={
(this.props.settings.charCount.enabled &&
this.props.settings.charCount.min) ||
null
}
max={
(this.props.settings.charCount.enabled &&
this.props.settings.charCount.max) ||
null
}
/>
);
}
@@ -112,22 +133,33 @@ const enhanced = withContext(({ sessionStorage, browserInfo }) => ({
// Disable autofocus on ios and enable for the rest.
autofocus: !browserInfo.ios,
}))(
withEditCommentMutation(
withFragmentContainer<Props>({
comment: graphql`
fragment EditCommentFormContainer_comment on Comment {
id
body
createdAt
author {
username
withRefreshSettingsFetch(
withEditCommentMutation(
withFragmentContainer<Props>({
comment: graphql`
fragment EditCommentFormContainer_comment on Comment {
id
body
createdAt
author {
username
}
editing {
editableUntil
}
}
editing {
editableUntil
`,
settings: graphql`
fragment EditCommentFormContainer_settings on Settings {
charCount {
enabled
min
max
}
}
}
`,
})(EditCommentFormContainer)
`,
})(EditCommentFormContainer)
)
)
);
export type PostCommentFormContainerProps = PropTypesOf<typeof enhanced>;
@@ -5,52 +5,62 @@ import sinon from "sinon";
import { timeout } from "talk-common/utils";
import { createPromisifiedStorage } from "talk-framework/lib/storage";
import { removeFragmentRefs } from "talk-framework/testHelpers";
import { PropTypesOf } from "talk-framework/types";
import { PostCommentFormContainer } from "./PostCommentFormContainer";
const contextKey = "postCommentFormBody";
const PostCommentFormContainerN = removeFragmentRefs(PostCommentFormContainer);
it("renders correctly", async () => {
const props: PropTypesOf<typeof PostCommentFormContainer> = {
// tslint:disable-next-line:no-empty
createComment: (() => {}) as any,
function createDefaultProps(): PropTypesOf<typeof PostCommentFormContainerN> {
return {
createComment: noop as any,
refreshSettings: noop as any,
storyID: "story-id",
sessionStorage: createPromisifiedStorage(),
settings: {
charCount: {
enabled: true,
min: 3,
max: 100,
},
},
};
}
it("renders correctly", async () => {
const props: PropTypesOf<typeof PostCommentFormContainerN> = {
...createDefaultProps(),
};
const wrapper = shallow(<PostCommentFormContainer {...props} />);
const wrapper = shallow(<PostCommentFormContainerN {...props} />);
await timeout();
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
it("renders with initialValues", async () => {
const props: PropTypesOf<typeof PostCommentFormContainer> = {
// tslint:disable-next-line:no-empty
createComment: (() => {}) as any,
storyID: "story-id",
sessionStorage: createPromisifiedStorage(),
const props: PropTypesOf<typeof PostCommentFormContainerN> = {
...createDefaultProps(),
};
await props.sessionStorage.setItem(contextKey, "Hello World!");
const wrapper = shallow(<PostCommentFormContainer {...props} />);
const wrapper = shallow(<PostCommentFormContainerN {...props} />);
await timeout();
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
it("save values", async () => {
const props: PropTypesOf<typeof PostCommentFormContainer> = {
// tslint:disable-next-line:no-empty
createComment: (() => {}) as any,
storyID: "story-id",
sessionStorage: createPromisifiedStorage(),
const props: PropTypesOf<typeof PostCommentFormContainerN> = {
...createDefaultProps(),
};
await props.sessionStorage.setItem(contextKey, "Hello World!");
const wrapper = shallow(<PostCommentFormContainer {...props} />);
const wrapper = shallow(<PostCommentFormContainerN {...props} />);
await timeout();
wrapper.update();
wrapper
@@ -71,16 +81,15 @@ it("creates a comment", async () => {
.withArgs({})
.once();
const props: PropTypesOf<typeof PostCommentFormContainer> = {
// tslint:disable-next-line:no-empty
const props: PropTypesOf<typeof PostCommentFormContainerN> = {
...createDefaultProps(),
createComment: createCommentStub,
storyID,
sessionStorage: createPromisifiedStorage(),
};
await props.sessionStorage.setItem(contextKey, "Hello World!");
const wrapper = shallow(<PostCommentFormContainer {...props} />);
const wrapper = shallow(<PostCommentFormContainerN {...props} />);
await timeout();
wrapper.update();
wrapper
@@ -2,21 +2,30 @@ import React, { Component } from "react";
import { withContext } from "talk-framework/lib/bootstrap";
import { InvalidRequestError } from "talk-framework/lib/errors";
import { graphql, withFragmentContainer } from "talk-framework/lib/relay";
import { PromisifiedStorage } from "talk-framework/lib/storage";
import { PropTypesOf } from "talk-framework/types";
import { PostCommentFormContainer_settings as SettingsData } from "talk-stream/__generated__/PostCommentFormContainer_settings.graphql";
import {
RefreshSettingsFetch,
withRefreshSettingsFetch,
} from "talk-stream/fetches";
import {
CreateCommentMutation,
withCreateCommentMutation,
} from "talk-stream/mutations";
import PostCommentForm, {
PostCommentFormProps,
} from "../components/PostCommentForm";
import { shouldTriggerSettingsRefresh } from "../helpers";
interface Props {
createComment: CreateCommentMutation;
refreshSettings: RefreshSettingsFetch;
storyID: string;
sessionStorage: PromisifiedStorage;
settings: SettingsData;
}
interface State {
@@ -60,6 +69,9 @@ export class PostCommentFormContainer extends Component<Props, State> {
form.reset({});
} catch (error) {
if (error instanceof InvalidRequestError) {
if (shouldTriggerSettingsRefresh(error.code)) {
await this.props.refreshSettings();
}
return error.invalidArgs;
}
// tslint:disable-next-line:no-console
@@ -68,12 +80,16 @@ export class PostCommentFormContainer extends Component<Props, State> {
return undefined;
};
private handleOnChange: PostCommentFormProps["onChange"] = state => {
private handleOnChange: PostCommentFormProps["onChange"] = (state, form) => {
if (state.values.body) {
this.props.sessionStorage.setItem(contextKey, state.values.body);
} else {
this.props.sessionStorage.removeItem(contextKey);
}
// Reset errors whenever user clears the form.
if (state.touched && state.touched.body && !state.values.body) {
form.reset({});
}
};
public render() {
@@ -85,6 +101,16 @@ export class PostCommentFormContainer extends Component<Props, State> {
onSubmit={this.handleOnSubmit}
onChange={this.handleOnChange}
initialValues={this.state.initialValues}
min={
(this.props.settings.charCount.enabled &&
this.props.settings.charCount.min) ||
null
}
max={
(this.props.settings.charCount.enabled &&
this.props.settings.charCount.max) ||
null
}
/>
);
}
@@ -92,6 +118,22 @@ export class PostCommentFormContainer extends Component<Props, State> {
const enhanced = withContext(({ sessionStorage }) => ({
sessionStorage,
}))(withCreateCommentMutation(PostCommentFormContainer));
}))(
withCreateCommentMutation(
withRefreshSettingsFetch(
withFragmentContainer<Props>({
settings: graphql`
fragment PostCommentFormContainer_settings on Settings {
charCount {
enabled
min
max
}
}
`,
})(PostCommentFormContainer)
)
)
);
export type PostCommentFormContainerProps = PropTypesOf<typeof enhanced>;
export default enhanced;
@@ -0,0 +1,21 @@
import React, { StatelessComponent } from "react";
import RemainingCharacters from "../components/RemainingCharacters";
import { getHTMLCharacterLength } from "../helpers";
interface Props {
className?: string;
max: number;
value: string | undefined;
}
const RemainingCharactersContainer: StatelessComponent<Props> = props => {
return (
<RemainingCharacters
className={props.className}
remaining={props.max - getHTMLCharacterLength(props.value)}
/>
);
};
export default RemainingCharactersContainer;
@@ -18,9 +18,10 @@ function getContextKey(commentID: string) {
return `replyCommentFormBody-${commentID}`;
}
it("renders correctly", async () => {
const props: PropTypesOf<typeof ReplyCommentFormContainerN> = {
function createDefaultProps(): PropTypesOf<typeof ReplyCommentFormContainerN> {
return {
createCommentReply: noop as any,
refreshSettings: noop as any,
story: {
id: "story-id",
},
@@ -35,6 +36,19 @@ it("renders correctly", async () => {
},
sessionStorage: createPromisifiedStorage(),
autofocus: false,
settings: {
charCount: {
enabled: true,
min: 3,
max: 100,
},
},
};
}
it("renders correctly", async () => {
const props: PropTypesOf<typeof ReplyCommentFormContainerN> = {
...createDefaultProps(),
};
const renderer = createRenderer();
@@ -45,21 +59,7 @@ it("renders correctly", async () => {
it("renders with initialValues", async () => {
const props: PropTypesOf<typeof ReplyCommentFormContainerN> = {
createCommentReply: noop as any,
story: {
id: "story-id",
},
comment: {
id: "comment-id",
author: {
username: "Joe",
},
revision: {
id: "revision-id",
},
},
sessionStorage: createPromisifiedStorage(),
autofocus: false,
...createDefaultProps(),
};
await props.sessionStorage.setItem(
@@ -75,21 +75,7 @@ it("renders with initialValues", async () => {
it("save values", async () => {
const props: PropTypesOf<typeof ReplyCommentFormContainerN> = {
createCommentReply: noop as any,
story: {
id: "story-id",
},
comment: {
id: "comment-id",
author: {
username: "Joe",
},
revision: {
id: "revision-id",
},
},
sessionStorage: createPromisifiedStorage(),
autofocus: false,
...createDefaultProps(),
};
await props.sessionStorage.setItem(
@@ -117,22 +103,9 @@ it("creates a comment", async () => {
const onCloseStub = sinon.stub();
const props: PropTypesOf<typeof ReplyCommentFormContainerN> = {
createCommentReply: createCommentStub,
story: {
id: "story-id",
},
comment: {
id: "comment-id",
author: {
username: "Joe",
},
revision: {
id: "revision-id",
},
},
sessionStorage: createPromisifiedStorage(),
...createDefaultProps(),
onClose: onCloseStub,
autofocus: false,
createCommentReply: createCommentStub,
};
await props.sessionStorage.setItem(
@@ -162,22 +135,8 @@ it("creates a comment", async () => {
it("closes on cancel", async () => {
const onCloseStub = sinon.stub();
const props: PropTypesOf<typeof ReplyCommentFormContainerN> = {
createCommentReply: noop as any,
story: {
id: "story-id",
},
comment: {
id: "comment-id",
author: {
username: "Joe",
},
revision: {
id: "revision-id",
},
},
sessionStorage: createPromisifiedStorage(),
...createDefaultProps(),
onClose: onCloseStub,
autofocus: false,
};
await props.sessionStorage.setItem(
@@ -203,20 +162,7 @@ it("autofocuses", async () => {
const focusStub = sinon.stub();
const rte = { focus: focusStub };
const props: PropTypesOf<typeof ReplyCommentFormContainerN> = {
createCommentReply: noop as any,
story: {
id: "story-id",
},
comment: {
id: "comment-id",
author: {
username: "Joe",
},
revision: {
id: "revision-id",
},
},
sessionStorage: createPromisifiedStorage(),
...createDefaultProps(),
autofocus: true,
};
@@ -8,7 +8,12 @@ import { withFragmentContainer } from "talk-framework/lib/relay";
import { PromisifiedStorage } from "talk-framework/lib/storage";
import { PropTypesOf } from "talk-framework/types";
import { ReplyCommentFormContainer_comment as CommentData } from "talk-stream/__generated__/ReplyCommentFormContainer_comment.graphql";
import { ReplyCommentFormContainer_settings as SettingsData } from "talk-stream/__generated__/ReplyCommentFormContainer_settings.graphql";
import { ReplyCommentFormContainer_story as StoryData } from "talk-stream/__generated__/ReplyCommentFormContainer_story.graphql";
import {
RefreshSettingsFetch,
withRefreshSettingsFetch,
} from "talk-stream/fetches";
import {
CreateCommentReplyMutation,
withCreateCommentReplyMutation,
@@ -17,15 +22,18 @@ import {
import ReplyCommentForm, {
ReplyCommentFormProps,
} from "../components/ReplyCommentForm";
import { shouldTriggerSettingsRefresh } from "../helpers";
interface Props {
createCommentReply: CreateCommentReplyMutation;
sessionStorage: PromisifiedStorage;
comment: CommentData;
settings: SettingsData;
story: StoryData;
onClose?: () => void;
autofocus: boolean;
localReply?: boolean;
refreshSettings: RefreshSettingsFetch;
}
interface State {
@@ -88,6 +96,9 @@ export class ReplyCommentFormContainer extends Component<Props, State> {
}
} catch (error) {
if (error instanceof InvalidRequestError) {
if (shouldTriggerSettingsRefresh(error.code)) {
await this.props.refreshSettings();
}
return error.invalidArgs;
}
// tslint:disable-next-line:no-console
@@ -96,12 +107,16 @@ export class ReplyCommentFormContainer extends Component<Props, State> {
return undefined;
};
private handleOnChange: ReplyCommentFormProps["onChange"] = state => {
private handleOnChange: ReplyCommentFormProps["onChange"] = (state, form) => {
if (state.values.body) {
this.props.sessionStorage.setItem(this.contextKey, state.values.body);
} else {
this.props.sessionStorage.removeItem(this.contextKey);
}
// Reset errors whenever user clears the form.
if (state.touched && state.touched.body && !state.values.body) {
form.reset({});
}
};
public render() {
@@ -119,6 +134,16 @@ export class ReplyCommentFormContainer extends Component<Props, State> {
parentUsername={
this.props.comment.author && this.props.comment.author.username
}
min={
(this.props.settings.charCount.enabled &&
this.props.settings.charCount.min) ||
null
}
max={
(this.props.settings.charCount.enabled &&
this.props.settings.charCount.max) ||
null
}
/>
);
}
@@ -128,25 +153,36 @@ const enhanced = withContext(({ sessionStorage, browserInfo }) => ({
// Disable autofocus on ios and enable for the rest.
autofocus: !browserInfo.ios,
}))(
withCreateCommentReplyMutation(
withFragmentContainer<Props>({
story: graphql`
fragment ReplyCommentFormContainer_story on Story {
id
}
`,
comment: graphql`
fragment ReplyCommentFormContainer_comment on Comment {
id
author {
username
withRefreshSettingsFetch(
withCreateCommentReplyMutation(
withFragmentContainer<Props>({
settings: graphql`
fragment ReplyCommentFormContainer_settings on Settings {
charCount {
enabled
min
max
}
}
revision {
`,
story: graphql`
fragment ReplyCommentFormContainer_story on Story {
id
}
}
`,
})(ReplyCommentFormContainer)
`,
comment: graphql`
fragment ReplyCommentFormContainer_comment on Comment {
id
author {
username
}
revision {
id
}
}
`,
})(ReplyCommentFormContainer)
)
)
);
export type PostCommentFormContainerProps = PropTypesOf<typeof enhanced>;
@@ -135,6 +135,7 @@ const enhanced = withPaginationContainer<
`,
settings: graphql`
fragment StreamContainer_settings on Settings {
...PostCommentFormContainer_settings
...ReplyListContainer1_settings
...CommentContainer_settings
...UserBoxContainer_settings
@@ -2,6 +2,8 @@
exports[`renders correctly 1`] = `
<PostCommentForm
max={100}
min={3}
onChange={[Function]}
onSubmit={[Function]}
/>
@@ -14,6 +16,8 @@ exports[`renders with initialValues 1`] = `
"body": "Hello World!",
}
}
max={100}
min={3}
onChange={[Function]}
onSubmit={[Function]}
/>
@@ -3,6 +3,8 @@
exports[`renders correctly 1`] = `
<ReplyCommentForm
id="comment-id"
max={100}
min={3}
onCancel={[Function]}
onChange={[Function]}
onSubmit={[Function]}
@@ -19,6 +21,8 @@ exports[`renders with initialValues 1`] = `
"body": "Hello World!",
}
}
max={100}
min={3}
onCancel={[Function]}
onChange={[Function]}
onSubmit={[Function]}
@@ -0,0 +1,21 @@
import getHTMLText from "./getHTMLText";
/**
* cleanupRTEEmptyHTML will try to figure out if given html only contains
* dead tags like `<b></b>` or `<br />` or `<i></i><br />` which basically
* means renders nothing and return a standardized `""` instead.
* @param html the html to be cleaned up
*/
export default function cleanupRTEEmptyHTML(html: string) {
if (html.includes("blockquote")) {
return html;
}
const innerText = getHTMLText(html);
if (
(innerText !== "\n" && innerText.includes("\n")) ||
innerText.trim() !== ""
) {
return html;
}
return "";
}
@@ -0,0 +1,27 @@
import {
composeValidators,
required,
validateMaxLength,
validateMinLength,
} from "talk-framework/lib/validation";
import getHTMLCharacterLength from "./getHTMLCharacterLength";
/**
* getBodyValidators will return validators based on given min & max parameters.
* @param min minimum length or null
* @param max maximum length or null
*/
export default function getBodyValidators(
min: number | null,
max: number | null
) {
const validators = [required];
if (min) {
validators.push(validateMinLength(min, getHTMLCharacterLength));
}
if (max) {
validators.push(validateMaxLength(max, getHTMLCharacterLength));
}
return composeValidators(...validators);
}
@@ -0,0 +1,14 @@
import getHTMLText from "./getHTMLText";
/**
* getHTMLCharacterLength will strip all tags and return remaining
* character length.
* @param html the html which length should be determined
*/
export default function getHTMLCharacterLength(html: string | undefined) {
if (!html) {
return 0;
}
const innerText = getHTMLText(html);
return innerText.trim().replace(/\n/g, "").length;
}
@@ -0,0 +1,15 @@
/**
* getHTMLText returns text representation of html.
* Includes a different implementation during test that works.
* @param html
*/
export default function getHTMLText(html: string) {
if (process.env.NODE_ENV === "test") {
// innerText is not implement in JSDOM, so we use `striptags` instead.
const striptags = require("striptags");
return striptags(html.replace(/<br *\/?>/, "\n"));
}
const divElement = document.createElement("div");
divElement.innerHTML = html;
return divElement.innerText;
}
@@ -0,0 +1,9 @@
export { default as getHTMLCharacterLength } from "./getHTMLCharacterLength";
export {
default as getCommentBodyValidators,
} from "./getCommentBodyValidators";
export { default as cleanupRTEEmptyHTML } from "./cleanupRTEEmptyHTML";
export {
default as shouldTriggerSettingsRefresh,
} from "./shouldTriggerSettingsRefresh";
export { default as getHTMLText } from "./getHTMLText";
@@ -0,0 +1,17 @@
import { ERROR_CODES } from "talk-common/errors";
const triggers = [
ERROR_CODES.COMMENT_BODY_TOO_SHORT,
ERROR_CODES.COMMENT_BODY_EXCEEDS_MAX_LENGTH,
];
/**
* shouldTriggerSettingsRefresh will indicate whether the settings
* needs to refresh based on a recently received error code. Some
* error codes signify that the settings on the client currently
* mismatches with the newest settings on the server, and thus
* e.g. validations fail.
* @param code the error code to check for
*/
export default function shouldTriggerSettingsRefresh(code: ERROR_CODES) {
return triggers.includes(code);
}
@@ -290,27 +290,27 @@ exports[`edit a comment and handle server error: edit form 1`] = `
</div>
</div>
</div>
</div>
<div
className="Message-root Message-colorGrey Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
<div
className="Message-root Message-colorGrey Message-fullWidth"
>
alarm
</span>
<span>
Edit:
<time
className="RelativeTime-root"
dateTime="2018-07-06T18:24:30.000Z"
title="2018-07-06T18:24:30.000Z"
<span
aria-hidden="true"
className="Icon-root Icon-sm MessageIcon-root"
>
2018-07-06T18:24:30.000Z
</time>
remaining
</span>
alarm
</span>
<span>
Edit:
<time
className="RelativeTime-root"
dateTime="2018-07-06T18:24:30.000Z"
title="2018-07-06T18:24:30.000Z"
>
2018-07-06T18:24:30.000Z
</time>
remaining
</span>
</div>
</div>
<div
className="Flex-root Flex-flex Flex-halfItemGutter Flex-justifyFlexEnd Flex-directionRow"
@@ -456,27 +456,27 @@ exports[`edit a comment: edit form 1`] = `
</div>
</div>
</div>
</div>
<div
className="Message-root Message-colorGrey Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
<div
className="Message-root Message-colorGrey Message-fullWidth"
>
alarm
</span>
<span>
Edit:
<time
className="RelativeTime-root"
dateTime="2018-07-06T18:24:30.000Z"
title="2018-07-06T18:24:30.000Z"
<span
aria-hidden="true"
className="Icon-root Icon-sm MessageIcon-root"
>
2018-07-06T18:24:30.000Z
</time>
remaining
</span>
alarm
</span>
<span>
Edit:
<time
className="RelativeTime-root"
dateTime="2018-07-06T18:24:30.000Z"
title="2018-07-06T18:24:30.000Z"
>
2018-07-06T18:24:30.000Z
</time>
remaining
</span>
</div>
</div>
<div
className="Flex-root Flex-flex Flex-halfItemGutter Flex-justifyFlexEnd Flex-directionRow"
@@ -622,27 +622,27 @@ exports[`edit a comment: optimistic response 1`] = `
</div>
</div>
</div>
</div>
<div
className="Message-root Message-colorGrey Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
<div
className="Message-root Message-colorGrey Message-fullWidth"
>
alarm
</span>
<span>
Edit:
<time
className="RelativeTime-root"
dateTime="2018-07-06T18:24:30.000Z"
title="2018-07-06T18:24:30.000Z"
<span
aria-hidden="true"
className="Icon-root Icon-sm MessageIcon-root"
>
2018-07-06T18:24:30.000Z
</time>
remaining
</span>
alarm
</span>
<span>
Edit:
<time
className="RelativeTime-root"
dateTime="2018-07-06T18:24:30.000Z"
title="2018-07-06T18:24:30.000Z"
>
2018-07-06T18:24:30.000Z
</time>
remaining
</span>
</div>
</div>
<div
className="Flex-root Flex-flex Flex-halfItemGutter Flex-justifyFlexEnd Flex-directionRow"
@@ -1337,17 +1337,17 @@ exports[`shows expiry message: edit time expired 1`] = `
</div>
</div>
</div>
</div>
<div
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
<div
className="Message-root Message-colorError Message-fullWidth"
>
warning
</span>
Edit time has expired. You can no longer edit this comment. Why not post another one?
<span
aria-hidden="true"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
Edit time has expired. You can no longer edit this comment. Why not post another one?
</div>
</div>
<div
className="Flex-root Flex-flex Flex-halfItemGutter Flex-justifyFlexEnd Flex-directionRow"
@@ -27,7 +27,7 @@ exports[`renders app with comment stream 1`] = `
type="button"
>
<span>
2 Comments
2 Comments
</span>
</button>
</li>
@@ -380,7 +380,7 @@ exports[`render popup expanded 1`] = `
<p
className="Typography-root Typography-detail Typography-colorTextSecondary ReportCommentForm-textareaInfo"
>
Max. 500 Characters
Max. 500 Characters
</p>
</div>
</div>
@@ -0,0 +1,165 @@
import RTE from "@coralproject/rte";
import sinon from "sinon";
import timekeeper from "timekeeper";
import { ERROR_CODES } from "talk-common/errors";
import { InvalidRequestError } from "talk-framework/lib/errors";
import {
createSinonStub,
findParentWithType,
waitForElement,
within,
} from "talk-framework/testHelpers";
import { settings, stories, users } from "../fixtures";
import create from "./create";
beforeAll(() => {
timekeeper.freeze(stories[0].comments.edges[0].node.createdAt);
});
afterAll(() => {
timekeeper.reset();
});
const settingsWithCharCount = {
...settings,
charCount: {
enabled: true,
min: 3,
max: 10,
},
};
async function createTestRenderer(
resolver: any = {},
options: { muteNetworkErrors?: boolean } = {}
) {
const resolvers = {
...resolver,
Query: {
settings: sinon.stub().returns(settingsWithCharCount),
me: sinon.stub().returns(users[0]),
story: sinon.stub().returns(stories[0]),
...resolver.Query,
},
};
const { testRenderer, context } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
muteNetworkErrors: options.muteNetworkErrors,
resolvers,
initLocalState: localRecord => {
localRecord.setValue(stories[0].id, "storyID");
localRecord.setValue(true, "loggedIn");
},
});
const comment = await waitForElement(() =>
within(testRenderer.root).getByTestID("comment-comment-0")
);
// Open edit form.
within(comment)
.getByText("Edit")
.props.onClick();
const rte = await waitForElement(
() =>
findParentWithType(
within(comment).getByLabelText("Edit comment"),
// We'll use the RTE component here as an exception because the
// jsdom does not support all of what is needed for rendering the
// Rich Text Editor.
RTE
)!
);
const form = findParentWithType(rte, "form")!;
return {
testRenderer,
context,
comment,
rte,
form,
};
}
it("validate min", async () => {
const { rte, form } = await createTestRenderer();
const text = "Please enter at least 3 characters.";
rte.props.onChange({ html: "ab" });
form.props.onSubmit();
within(form).getByText(text);
});
it("validate max", async () => {
const { rte, form } = await createTestRenderer();
const text = "Please enter at max 10 characters.";
rte.props.onChange({ html: "abcdefghijklmnopqrst" });
form.props.onSubmit();
within(form).getByText(text);
});
it("show remaining characters", async () => {
const { rte, form } = await createTestRenderer();
rte.props.onChange({ html: "abc" });
within(form).getByText("7 characters remaining");
rte.props.onChange({ html: "abcdefghijkl" });
within(form).getByText("-2 characters remaining");
});
it("update from server upon specific char count error", async () => {
for (const errorCode of [
ERROR_CODES.COMMENT_BODY_EXCEEDS_MAX_LENGTH,
ERROR_CODES.COMMENT_BODY_TOO_SHORT,
]) {
const { rte, form } = await createTestRenderer(
{
Mutation: {
editComment: sinon.stub().callsFake(() => {
throw new InvalidRequestError({
code: errorCode,
param: "input.body",
});
}),
},
Query: {
settings: createSinonStub(
s => s.onFirstCall().returns(settingsWithCharCount),
s =>
s.onSecondCall().returns({
...settingsWithCharCount,
charCount: {
enabled: true,
min: 3,
max: 5,
},
})
),
},
},
{ muteNetworkErrors: true }
);
rte.props.onChange({ html: "abc" });
within(form).getByText("7 characters remaining");
rte.props.onChange({ html: "abcdefgh" });
within(form).getByText("2 characters remaining");
form.props.onSubmit();
await waitForElement(() =>
within(form).getByText("-3 characters remaining")
);
// Body submit error should be displayed.
within(form).getByText(errorCode);
rte.props.onChange({ html: "abcde" });
// Body submit error should disappear when form gets dirty.
expect(within(form).queryByText(errorCode)).toBeNull();
}
});
@@ -0,0 +1,160 @@
import sinon from "sinon";
import RTE from "@coralproject/rte";
import { ERROR_CODES } from "talk-common/errors";
import { InvalidRequestError } from "talk-framework/lib/errors";
import {
createSinonStub,
findParentWithType,
waitForElement,
within,
} from "talk-framework/testHelpers";
import { settings, stories, users } from "../fixtures";
import create from "./create";
const settingsWithCharCount = {
...settings,
charCount: {
enabled: true,
min: 3,
max: 10,
},
};
async function createTestRenderer(
resolver: any = {},
options: { muteNetworkErrors?: boolean } = {}
) {
const resolvers = {
...resolver,
Query: {
settings: sinon.stub().returns(settingsWithCharCount),
me: sinon.stub().returns(users[0]),
story: sinon.stub().returns(stories[0]),
...resolver.Query,
},
};
const { testRenderer, context } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
muteNetworkErrors: options.muteNetworkErrors,
resolvers,
initLocalState: localRecord => {
localRecord.setValue(stories[0].id, "storyID");
localRecord.setValue(true, "loggedIn");
},
});
const rte = await waitForElement(
() =>
findParentWithType(
within(testRenderer.root).getByLabelText("Post a comment"),
// We'll use the RTE component here as an exception because the
// jsdom does not support all of what is needed for rendering the
// Rich Text Editor.
RTE
)!
);
const form = findParentWithType(rte, "form")!;
return {
testRenderer,
context,
rte,
form,
};
}
it("validate min", async () => {
const { rte, form } = await createTestRenderer();
const text = "Please enter at least 3 characters.";
rte.props.onChange({ html: "ab" });
form.props.onSubmit();
within(form).getByText(text);
// Reset validation when erasing all content.
rte.props.onChange({ html: "" });
expect(within(form).queryByText(text)).toBeNull();
rte.props.onChange({ html: "ab" });
expect(within(form).queryByText(text)).toBeNull();
});
it("validate max", async () => {
const { rte, form } = await createTestRenderer();
const text = "Please enter at max 10 characters.";
rte.props.onChange({ html: "abcdefghijklmnopqrst" });
form.props.onSubmit();
within(form).getByText(text);
// Reset validation when erasing all content.
rte.props.onChange({ html: "" });
expect(within(form).queryByText(text)).toBeNull();
rte.props.onChange({ html: "abcdefghijklmnopqrst" });
expect(within(form).queryByText(text)).toBeNull();
});
it("show remaining characters", async () => {
const { rte, form } = await createTestRenderer();
rte.props.onChange({ html: "abc" });
within(form).getByText("7 characters remaining");
rte.props.onChange({ html: "abcdefghijkl" });
within(form).getByText("-2 characters remaining");
});
it("update from server upon specific char count error", async () => {
for (const errorCode of [
ERROR_CODES.COMMENT_BODY_EXCEEDS_MAX_LENGTH,
ERROR_CODES.COMMENT_BODY_TOO_SHORT,
]) {
const { rte, form } = await createTestRenderer(
{
Mutation: {
createComment: sinon.stub().callsFake(() => {
throw new InvalidRequestError({
code: errorCode,
param: "input.body",
});
}),
},
Query: {
settings: createSinonStub(
s => s.onFirstCall().returns(settingsWithCharCount),
s =>
s.onSecondCall().returns({
...settingsWithCharCount,
charCount: {
enabled: true,
min: 3,
max: 5,
},
})
),
},
},
{ muteNetworkErrors: true }
);
rte.props.onChange({ html: "abc" });
within(form).getByText("7 characters remaining");
rte.props.onChange({ html: "abcdefgh" });
within(form).getByText("2 characters remaining");
form.props.onSubmit();
await waitForElement(() =>
within(form).getByText("-3 characters remaining")
);
// Body submit error should be displayed.
within(form).getByText(errorCode);
rte.props.onChange({ html: "abcde" });
// Body submit error should disappear when form gets dirty.
expect(within(form).queryByText(errorCode)).toBeNull();
}
});
@@ -0,0 +1,171 @@
import sinon from "sinon";
import RTE from "@coralproject/rte";
import { ERROR_CODES } from "talk-common/errors";
import { InvalidRequestError } from "talk-framework/lib/errors";
import {
createSinonStub,
findParentWithType,
waitForElement,
within,
} from "talk-framework/testHelpers";
import { settings, stories, users } from "../fixtures";
import create from "./create";
const settingsWithCharCount = {
...settings,
charCount: {
enabled: true,
min: 3,
max: 10,
},
};
async function createTestRenderer(
resolver: any = {},
options: { muteNetworkErrors?: boolean } = {}
) {
const resolvers = {
...resolver,
Query: {
settings: sinon.stub().returns(settingsWithCharCount),
me: sinon.stub().returns(users[0]),
story: sinon.stub().returns(stories[0]),
...resolver.Query,
},
};
const { testRenderer, context } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
muteNetworkErrors: options.muteNetworkErrors,
resolvers,
initLocalState: localRecord => {
localRecord.setValue(stories[0].id, "storyID");
localRecord.setValue(true, "loggedIn");
},
});
const comment = await waitForElement(() =>
within(testRenderer.root).getByTestID("comment-comment-0")
);
// Open reply form.
within(comment)
.getByText("Reply", { selector: "button" })
.props.onClick();
const rte = await waitForElement(
() =>
findParentWithType(
within(comment).getByLabelText("Write a reply"),
// We'll use the RTE component here as an exception because the
// jsdom does not support all of what is needed for rendering the
// Rich Text Editor.
RTE
)!
);
const form = findParentWithType(rte, "form")!;
return {
testRenderer,
context,
comment,
rte,
form,
};
}
it("validate min", async () => {
const { rte, form } = await createTestRenderer();
const text = "Please enter at least 3 characters.";
rte.props.onChange({ html: "ab" });
form.props.onSubmit();
within(form).getByText(text);
// Reset validation when erasing all content.
rte.props.onChange({ html: "" });
expect(within(form).queryByText(text)).toBeNull();
rte.props.onChange({ html: "ab" });
expect(within(form).queryByText(text)).toBeNull();
});
it("validate max", async () => {
const { rte, form } = await createTestRenderer();
const text = "Please enter at max 10 characters.";
rte.props.onChange({ html: "abcdefghijklmnopqrst" });
form.props.onSubmit();
within(form).getByText(text);
// Reset validation when erasing all content.
rte.props.onChange({ html: "" });
expect(within(form).queryByText(text)).toBeNull();
rte.props.onChange({ html: "abcdefghijklmnopqrst" });
expect(within(form).queryByText(text)).toBeNull();
});
it("show remaining characters", async () => {
const { rte, form } = await createTestRenderer();
rte.props.onChange({ html: "abc" });
within(form).getByText("7 characters remaining");
rte.props.onChange({ html: "abcdefghijkl" });
within(form).getByText("-2 characters remaining");
});
it("update from server upon specific char count error", async () => {
for (const errorCode of [
ERROR_CODES.COMMENT_BODY_EXCEEDS_MAX_LENGTH,
ERROR_CODES.COMMENT_BODY_TOO_SHORT,
]) {
const { rte, form } = await createTestRenderer(
{
Mutation: {
createCommentReply: sinon.stub().callsFake(() => {
throw new InvalidRequestError({
code: errorCode,
param: "input.body",
});
}),
},
Query: {
settings: createSinonStub(
s => s.onFirstCall().returns(settingsWithCharCount),
s =>
s.onSecondCall().returns({
...settingsWithCharCount,
charCount: {
enabled: true,
min: 3,
max: 5,
},
})
),
},
},
{ muteNetworkErrors: true }
);
rte.props.onChange({ html: "abc" });
within(form).getByText("7 characters remaining");
rte.props.onChange({ html: "abcdefgh" });
within(form).getByText("2 characters remaining");
form.props.onSubmit();
await waitForElement(() =>
within(form).getByText("-3 characters remaining")
);
// Body submit error should be displayed.
within(form).getByText(errorCode);
rte.props.onChange({ html: "abcde" });
// Body submit error should disappear when form gets dirty.
expect(within(form).queryByText(errorCode)).toBeNull();
}
});
+3
View File
@@ -55,6 +55,9 @@ export const settings = {
label: "Respect",
labelActive: "Respected",
},
charCount: {
enabled: false,
},
};
export const users = [
@@ -17,7 +17,7 @@ exports[`renders profile 1`] = `
<h1
className="Typography-root Typography-heading4 Typography-colorTextPrimary"
>
Story: title
Story: title
</h1>
<time
className="Timestamp-root RelativeTime-root"
@@ -72,7 +72,7 @@ exports[`renders profile 1`] = `
<h1
className="Typography-root Typography-heading4 Typography-colorTextPrimary"
>
Story: title
Story: title
</h1>
<time
className="Timestamp-root RelativeTime-root"
@@ -65,7 +65,7 @@ exports[`renders profile 1`] = `
<h1
className="Typography-root Typography-heading4 Typography-colorTextPrimary"
>
Story: title
Story: title
</h1>
<time
className="Timestamp-root RelativeTime-root"
@@ -120,7 +120,7 @@ exports[`renders profile 1`] = `
<h1
className="Typography-root Typography-heading4 Typography-colorTextPrimary"
>
Story: title
Story: title
</h1>
<time
className="Timestamp-root RelativeTime-root"
@@ -3,11 +3,11 @@ import React, { HTMLAttributes, Ref, StatelessComponent } from "react";
import Icon, { IconProps } from "talk-ui/components/Icon";
import { withForwardRef, withStyles } from "talk-ui/hocs";
import { PropTypesOf } from "talk-ui/types";
import { Omit, PropTypesOf } from "talk-ui/types";
import styles from "./ButtonIcon.css";
interface Props extends HTMLAttributes<HTMLSpanElement> {
interface Props extends Omit<HTMLAttributes<HTMLSpanElement>, "color"> {
/**
* This prop can be used to add custom classnames.
* It is handled by the `withStyles `HOC.
@@ -41,3 +41,14 @@
font-size: 36px;
width: 36px;
}
.colorPrimary {
color: var(--palette-primary-main);
}
.colorError {
color: var(--palette-error-main);
}
.colorSuccess {
color: var(--palette-success-main);
}
+18 -2
View File
@@ -15,6 +15,11 @@ interface Props extends HTMLAttributes<HTMLSpanElement> {
size?: "sm" | "md" | "lg" | "xl";
/**
* The color of the component. It supports those theme colors that make sense for this component.
*/
color?: "inherit" | "primary" | "error" | "success";
/** The name of the icon to render */
children: string;
@@ -23,8 +28,18 @@ interface Props extends HTMLAttributes<HTMLSpanElement> {
}
const Icon: StatelessComponent<Props> = props => {
const { classes, className, size, forwardRef, ...rest } = props;
const rootClassName = cn(classes.root, className, classes[size!]);
const { classes, className, size, color, forwardRef, ...rest } = props;
const rootClassName = cn(
classes.root,
classes[size!],
{
[classes.colorPrimary]: color === "primary",
[classes.colorError]: color === "error",
[classes.colorSuccess]: color === "success",
},
className
);
return (
<span
className={rootClassName}
@@ -37,6 +52,7 @@ const Icon: StatelessComponent<Props> = props => {
Icon.defaultProps = {
size: "sm",
color: "inherit",
} as Partial<Props>;
const enhanced = withForwardRef(withStyles(styles)(Icon));
@@ -3,10 +3,11 @@ import React, { HTMLAttributes, Ref, StatelessComponent } from "react";
import Icon, { IconProps } from "talk-ui/components/Icon";
import { withForwardRef, withStyles } from "talk-ui/hocs";
import { Omit } from "talk-ui/types";
import styles from "./MessageIcon.css";
interface Props extends HTMLAttributes<HTMLSpanElement> {
interface Props extends Omit<HTMLAttributes<HTMLSpanElement>, "color"> {
/**
* This prop can be used to add custom classnames.
* It is handled by the `withStyles `HOC.
@@ -14,7 +14,7 @@ exports[`renders icon 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
alert
</span>
@@ -82,10 +82,6 @@
margin-bottom: var(--spacing-unit);
}
.colorInherit {
color: inherit;
}
.colorPrimary {
color: var(--palette-primary-main);
}
@@ -6,7 +6,7 @@ exports[`renders correctly 1`] = `
>
<span
aria-hidden="true"
className="Icon-root MessageIcon-root Icon-sm"
className="Icon-root Icon-sm MessageIcon-root"
>
warning
</span>
@@ -45,16 +45,24 @@ export const Comment = (ctx: TenantContext) => ({
}
),
edit: ({ commentID, body }: GQLEditCommentInput) =>
edit(
ctx.mongo,
ctx.redis,
ctx.tenant,
ctx.user!,
mapFieldsetToErrorCodes(
edit(
ctx.mongo,
ctx.redis,
ctx.tenant,
ctx.user!,
{
id: commentID,
body,
},
ctx.req
),
{
id: commentID,
body,
},
ctx.req
"input.body": [
ERROR_CODES.COMMENT_BODY_EXCEEDS_MAX_LENGTH,
ERROR_CODES.COMMENT_BODY_TOO_SHORT,
],
}
),
createReaction: ({
commentID,
+2
View File
@@ -42,6 +42,8 @@ comments-rte-blockquote =
comments-poweredBy = Powered by <logo>{ -brand-name }</logo>
comments-remainingCharacters = { $remaining } characters remaining
comments-postCommentFormFake-signInAndJoin = Sign in and Join the Conversation
comments-postCommentForm-rteLabel = Post a comment