mirror of
https://github.com/wassname/talk.git
synced 2026-08-19 12:40:16 +08:00
[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:
@@ -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 <Facebook>”.
|
||||
>
|
||||
<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 <Facebook>”.
|
||||
>
|
||||
<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 <Facebook>”.
|
||||
>
|
||||
<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);
|
||||
|
||||
|
||||
+5
-5
@@ -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>
|
||||
|
||||
@@ -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"]
|
||||
>;
|
||||
@@ -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;
|
||||
+22
-76
@@ -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
|
||||
|
||||
+4
@@ -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]}
|
||||
/>
|
||||
|
||||
+4
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user