mirror of
https://github.com/wassname/talk.git
synced 2026-07-25 13:30:59 +08:00
[CORL-469] keyboard shortcuts (#2588)
* make comments selectable in queue * manage focus * switching focus works * keyboard navigation sort of works * scroll into view * prev, next, approve, reject * shortcuts for queue switching * ban modal? * focus search bar with ctrl f * keyboards shortcuts modal * zen mode * single comment view * ban users from moderate cards * clean up ts and lints * update snaps * add strings * update smaps * remove single comment view shortcut * add single comment view to moderation queue * clean up types * fix deps for useEffect/useCallback hooks * feat: added toggable help
This commit is contained in:
committed by
Wyatt Johnson
parent
e56793f2fb
commit
33487be0ce
Generated
+5
@@ -20678,6 +20678,11 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"keymaster": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/keymaster/-/keymaster-1.6.2.tgz",
|
||||
"integrity": "sha1-4a5U0OqUiPn2C2a2aPAumhlGxus="
|
||||
},
|
||||
"killable": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
"jsonwebtoken": "^8.3.0",
|
||||
"juice": "^5.2.0",
|
||||
"jwks-rsa": "^1.3.0",
|
||||
"keymaster": "^1.6.2",
|
||||
"linkifyjs": "^2.1.8",
|
||||
"lodash": "^4.17.15",
|
||||
"lru-cache": "^5.1.1",
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { BanCommentUserMutation as MutationTypes } from "coral-admin/__generated__/BanCommentUserMutation.graphql";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
const clientMutationId = 0;
|
||||
|
||||
const BanCommentUserMutation = createMutation(
|
||||
"banUser",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) => {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation BanCommentUserMutation($input: BanUserInput!) {
|
||||
banUser(input: $input) {
|
||||
user {
|
||||
id
|
||||
status {
|
||||
current
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
updater: store => {
|
||||
const user = store.get(input.userID);
|
||||
if (user) {
|
||||
const comments = user.getLinkedRecords("comments");
|
||||
if (comments) {
|
||||
comments.forEach(comment => {
|
||||
if (comment) {
|
||||
comment.setLinkedRecord(user, "author");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default BanCommentUserMutation;
|
||||
@@ -0,0 +1,3 @@
|
||||
.authorStatus {
|
||||
padding-right: var(--spacing-2);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { CommentAuthorContainer_comment as CommentData } from "coral-admin/__generated__/CommentAuthorContainer_comment.graphql";
|
||||
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { Tag } from "coral-ui/components";
|
||||
|
||||
import styles from "./CommentAuthorContainer.css";
|
||||
|
||||
interface Props {
|
||||
comment: CommentData;
|
||||
}
|
||||
|
||||
const CommentAuthorContainer: FunctionComponent<Props> = ({ comment }) => {
|
||||
if (!comment.author || !comment.author.status.ban.active) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Localized id="commentAuthor-status-banned">
|
||||
<div className={styles.authorStatus}>
|
||||
<Tag color="error">BANNED</Tag>
|
||||
</div>
|
||||
</Localized>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
comment: graphql`
|
||||
fragment CommentAuthorContainer_comment on Comment {
|
||||
author {
|
||||
id
|
||||
username
|
||||
status {
|
||||
ban {
|
||||
active
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(CommentAuthorContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -8,7 +8,7 @@
|
||||
}
|
||||
|
||||
.username {
|
||||
margin-right: var(--mini-unit);
|
||||
margin-right: var(--spacing-1);
|
||||
padding: var(--spacing-1);
|
||||
margin-left: calc(-1 * var(--spacing-1));
|
||||
line-height: calc(16rem / var(--rem-base));
|
||||
@@ -89,6 +89,10 @@
|
||||
transition: background 100ms, box-shadow 100ms;
|
||||
}
|
||||
|
||||
.root:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dangling {
|
||||
background-color: var(--palette-grey-lightest);
|
||||
box-shadow: none;
|
||||
@@ -164,4 +168,13 @@
|
||||
.edited {
|
||||
color: var(--palette-grey-lighter);
|
||||
padding-left: var(--spacing-2)
|
||||
}
|
||||
}
|
||||
|
||||
.selected {
|
||||
box-shadow: 1px 4px 15px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.authorStatus {
|
||||
padding-right: var(--spacing-2);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ const baseProps: PropTypesOf<typeof ModerateCardN> = {
|
||||
body: "content",
|
||||
edited: false,
|
||||
inReplyTo: null,
|
||||
comment: {},
|
||||
settings: {},
|
||||
comment: {},
|
||||
status: "undecided",
|
||||
featured: false,
|
||||
viewContextHref: "http://localhost/comment",
|
||||
@@ -26,7 +26,9 @@ const baseProps: PropTypesOf<typeof ModerateCardN> = {
|
||||
onApprove: noop,
|
||||
onReject: noop,
|
||||
onFeature: noop,
|
||||
onBan: noop,
|
||||
onUsernameClick: noop,
|
||||
onFocusOrClick: noop,
|
||||
showStory: false,
|
||||
moderatedBy: null,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import cn from "classnames";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import key from "keymaster";
|
||||
import { noop } from "lodash";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
|
||||
import { HOTKEYS } from "coral-admin/constants";
|
||||
import { PropTypesOf } from "coral-framework/types";
|
||||
import {
|
||||
BaseButton,
|
||||
@@ -14,6 +22,7 @@ import {
|
||||
} from "coral-ui/components";
|
||||
|
||||
import ApproveButton from "./ApproveButton";
|
||||
import CommentAuthorContainer from "./CommentAuthorContainer";
|
||||
import CommentContent from "./CommentContent";
|
||||
import FeatureButton from "./FeatureButton";
|
||||
import InReplyTo from "./InReplyTo";
|
||||
@@ -32,7 +41,8 @@ interface Props {
|
||||
id: string;
|
||||
username: string | null;
|
||||
} | null;
|
||||
comment: PropTypesOf<typeof MarkersContainer>["comment"];
|
||||
comment: PropTypesOf<typeof MarkersContainer>["comment"] &
|
||||
PropTypesOf<typeof CommentAuthorContainer>["comment"];
|
||||
settings: PropTypesOf<typeof MarkersContainer>["settings"];
|
||||
status: "approved" | "rejected" | "undecided";
|
||||
featured: boolean;
|
||||
@@ -48,8 +58,10 @@ interface Props {
|
||||
onReject: () => void;
|
||||
onFeature: () => void;
|
||||
onUsernameClick: (id?: string) => void;
|
||||
onFocusOrClick: () => void;
|
||||
mini?: boolean;
|
||||
hideUsername?: boolean;
|
||||
selected?: boolean;
|
||||
/**
|
||||
* If set to true, it means this comment is about to be removed
|
||||
* from the queue. This will trigger some styling changes to
|
||||
@@ -58,6 +70,9 @@ interface Props {
|
||||
dangling?: boolean;
|
||||
deleted?: boolean;
|
||||
edited: boolean;
|
||||
selectPrev?: () => void;
|
||||
selectNext?: () => void;
|
||||
onBan: () => void;
|
||||
}
|
||||
|
||||
const ModerateCard: FunctionComponent<Props> = ({
|
||||
@@ -83,11 +98,52 @@ const ModerateCard: FunctionComponent<Props> = ({
|
||||
storyHref,
|
||||
onModerateStory,
|
||||
moderatedBy,
|
||||
selected,
|
||||
onFocusOrClick,
|
||||
mini = false,
|
||||
hideUsername = false,
|
||||
deleted = false,
|
||||
edited,
|
||||
selectNext,
|
||||
selectPrev,
|
||||
onBan,
|
||||
}) => {
|
||||
const div = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (selected) {
|
||||
if (selectNext) {
|
||||
key(HOTKEYS.NEXT, id, selectNext);
|
||||
}
|
||||
if (selectPrev) {
|
||||
key(HOTKEYS.PREV, id, selectPrev);
|
||||
}
|
||||
if (onBan) {
|
||||
key(HOTKEYS.BAN, id, onBan);
|
||||
}
|
||||
key(HOTKEYS.APPROVE, id, onApprove);
|
||||
key(HOTKEYS.REJECT, id, onReject);
|
||||
|
||||
// The the scope such that only events attached to the ${id} scope will
|
||||
// be honored.
|
||||
key.setScope(id);
|
||||
|
||||
return () => {
|
||||
// Remove all events that are set in the ${id} scope.
|
||||
key.deleteScope(id);
|
||||
};
|
||||
} else {
|
||||
// Remove all events that were set in the ${id} scope.
|
||||
key.deleteScope(id);
|
||||
}
|
||||
|
||||
return noop;
|
||||
}, [selected, id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selected && div && div.current) {
|
||||
div.current.focus();
|
||||
}
|
||||
}, [selected]);
|
||||
const commentBody = deleted ? (
|
||||
<Localized id="moderate-comment-deleted-body">
|
||||
<Typography>
|
||||
@@ -112,9 +168,14 @@ const ModerateCard: FunctionComponent<Props> = ({
|
||||
styles.root,
|
||||
{ [styles.borderless]: mini },
|
||||
{ [styles.dangling]: dangling },
|
||||
{ [styles.deleted]: deleted }
|
||||
{ [styles.deleted]: deleted },
|
||||
{ [styles.selected]: selected }
|
||||
)}
|
||||
ref={div}
|
||||
tabIndex={0}
|
||||
data-testid={`moderate-comment-${id}`}
|
||||
id={`moderate-comment-${id}`}
|
||||
onClick={onFocusOrClick}
|
||||
>
|
||||
<Flex>
|
||||
<div className={styles.mainContainer}>
|
||||
@@ -132,7 +193,8 @@ const ModerateCard: FunctionComponent<Props> = ({
|
||||
<Username>{username}</Username>
|
||||
</BaseButton>
|
||||
)}
|
||||
<Timestamp className={styles.timestamp}>{createdAt}</Timestamp>
|
||||
<CommentAuthorContainer comment={comment} />
|
||||
<Timestamp>{createdAt}</Timestamp>
|
||||
{edited && (
|
||||
<Localized id="moderate-comment-edited">
|
||||
<Typography variant="timestamp" className={styles.edited}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Match, Router, withRouter } from "found";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import {
|
||||
@@ -18,8 +18,11 @@ import {
|
||||
withFragmentContainer,
|
||||
withMutation,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { GQLUSER_STATUS } from "coral-framework/schema";
|
||||
import { GQLTAG } from "coral-framework/schema";
|
||||
|
||||
import BanModal from "coral-admin/components/UserStatus/BanModal";
|
||||
import BanCommentUserMutation from "./BanCommentUserMutation";
|
||||
import FeatureCommentMutation from "./FeatureCommentMutation";
|
||||
import ModerateCard from "./ModerateCard";
|
||||
import ModeratedByContainer from "./ModeratedByContainer";
|
||||
@@ -33,6 +36,7 @@ interface Props {
|
||||
rejectComment: MutationProp<typeof RejectCommentMutation>;
|
||||
featureComment: MutationProp<typeof FeatureCommentMutation>;
|
||||
unfeatureComment: MutationProp<typeof UnfeatureCommentMutation>;
|
||||
banUser: MutationProp<typeof BanCommentUserMutation>;
|
||||
danglingLogic: (status: COMMENT_STATUS) => boolean;
|
||||
match: Match;
|
||||
router: Router;
|
||||
@@ -40,6 +44,11 @@ interface Props {
|
||||
mini?: boolean;
|
||||
hideUsername?: boolean;
|
||||
onUsernameClicked?: (userID: string) => void;
|
||||
onSetSelected?: () => void;
|
||||
selected?: boolean;
|
||||
selectPrev?: () => void;
|
||||
selectNext?: () => void;
|
||||
loadNext?: (() => void) | null;
|
||||
}
|
||||
|
||||
function getStatus(comment: ModerateCardContainer_comment) {
|
||||
@@ -71,30 +80,43 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
unfeatureComment,
|
||||
mini,
|
||||
hideUsername,
|
||||
selected,
|
||||
selectPrev,
|
||||
selectNext,
|
||||
onUsernameClicked: usernameClicked,
|
||||
onSetSelected: setSelected,
|
||||
banUser,
|
||||
loadNext,
|
||||
}) => {
|
||||
const handleApprove = useCallback(() => {
|
||||
const [showBanModal, setShowBanModal] = useState(false);
|
||||
const handleApprove = useCallback(async () => {
|
||||
if (!comment.revision) {
|
||||
return;
|
||||
}
|
||||
|
||||
approveComment({
|
||||
await approveComment({
|
||||
commentID: comment.id,
|
||||
commentRevisionID: comment.revision.id,
|
||||
storyID: match.params.storyID,
|
||||
});
|
||||
if (loadNext) {
|
||||
loadNext();
|
||||
}
|
||||
}, [approveComment, comment, match]);
|
||||
|
||||
const handleReject = useCallback(() => {
|
||||
const handleReject = useCallback(async () => {
|
||||
if (!comment.revision) {
|
||||
return;
|
||||
}
|
||||
|
||||
rejectComment({
|
||||
await rejectComment({
|
||||
commentID: comment.id,
|
||||
commentRevisionID: comment.revision.id,
|
||||
storyID: match.params.storyID,
|
||||
});
|
||||
if (loadNext) {
|
||||
loadNext();
|
||||
}
|
||||
}, [rejectComment, comment, match]);
|
||||
|
||||
const handleFeature = useCallback(() => {
|
||||
@@ -146,6 +168,35 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
[router, comment]
|
||||
);
|
||||
|
||||
const onFocusOrClick = useCallback(() => {
|
||||
if (setSelected) {
|
||||
setSelected();
|
||||
}
|
||||
}, [selected, comment]);
|
||||
|
||||
const handleBanModalClose = useCallback(() => {
|
||||
setShowBanModal(false);
|
||||
}, []);
|
||||
|
||||
const openBanModal = useCallback(() => {
|
||||
if (
|
||||
!comment.author ||
|
||||
comment.author.status.current.includes(GQLUSER_STATUS.BANNED)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setShowBanModal(true);
|
||||
}, [comment]);
|
||||
|
||||
const handleBanConfirm = useCallback(
|
||||
async (message: string) => {
|
||||
if (comment.author) {
|
||||
await banUser({ userID: comment.author.id, message });
|
||||
}
|
||||
setShowBanModal(false);
|
||||
},
|
||||
[comment]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<FadeInTransition active={Boolean(comment.enteredLive)}>
|
||||
@@ -171,6 +222,10 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
onReject={handleReject}
|
||||
onFeature={onFeature}
|
||||
onUsernameClick={onUsernameClicked}
|
||||
selected={selected}
|
||||
selectPrev={selectPrev}
|
||||
selectNext={selectNext}
|
||||
onBan={openBanModal}
|
||||
moderatedBy={
|
||||
<ModeratedByContainer
|
||||
onUsernameClicked={onUsernameClicked}
|
||||
@@ -178,6 +233,7 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
comment={comment}
|
||||
/>
|
||||
}
|
||||
onFocusOrClick={onFocusOrClick}
|
||||
showStory={showStoryInfo}
|
||||
storyTitle={
|
||||
(comment.story.metadata && comment.story.metadata.title) || (
|
||||
@@ -192,6 +248,16 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
edited={comment.editing.edited}
|
||||
/>
|
||||
</FadeInTransition>
|
||||
<BanModal
|
||||
username={
|
||||
comment.author && comment.author.username
|
||||
? comment.author.username
|
||||
: ""
|
||||
}
|
||||
open={showBanModal}
|
||||
onClose={handleBanModalClose}
|
||||
onConfirm={handleBanConfirm}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -203,6 +269,9 @@ const enhanced = withFragmentContainer<Props>({
|
||||
author {
|
||||
id
|
||||
username
|
||||
status {
|
||||
current
|
||||
}
|
||||
}
|
||||
statusLiveUpdated
|
||||
createdAt
|
||||
@@ -234,6 +303,7 @@ const enhanced = withFragmentContainer<Props>({
|
||||
deleted
|
||||
...MarkersContainer_comment
|
||||
...ModeratedByContainer_comment
|
||||
...CommentAuthorContainer_comment
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
@@ -252,10 +322,12 @@ const enhanced = withFragmentContainer<Props>({
|
||||
`,
|
||||
})(
|
||||
withRouter(
|
||||
withMutation(ApproveCommentMutation)(
|
||||
withMutation(RejectCommentMutation)(
|
||||
withMutation(FeatureCommentMutation)(
|
||||
withMutation(UnfeatureCommentMutation)(ModerateCardContainer)
|
||||
withMutation(BanCommentUserMutation)(
|
||||
withMutation(ApproveCommentMutation)(
|
||||
withMutation(RejectCommentMutation)(
|
||||
withMutation(FeatureCommentMutation)(
|
||||
withMutation(UnfeatureCommentMutation)(ModerateCardContainer)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
+56
-21
@@ -1,9 +1,12 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders approved correctly 1`] = `
|
||||
<withPropsOnChange(Card)
|
||||
<ForwardRef(forwardRef)
|
||||
className="ModerateCard-root"
|
||||
data-testid="moderate-comment-comment-id"
|
||||
id="moderate-comment-comment-id"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<ForwardRef(forwardRef)>
|
||||
<div
|
||||
@@ -23,8 +26,10 @@ exports[`renders approved correctly 1`] = `
|
||||
Theon
|
||||
</Username>
|
||||
</ForwardRef(forwardRef)>
|
||||
<Relay(CommentAuthorContainer)
|
||||
comment={Object {}}
|
||||
/>
|
||||
<Timestamp
|
||||
className="ModerateCard-timestamp"
|
||||
toggleAbsolute={true}
|
||||
>
|
||||
2018-11-29T16:01:51.897Z
|
||||
@@ -112,13 +117,16 @@ exports[`renders approved correctly 1`] = `
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</withPropsOnChange(Card)>
|
||||
</ForwardRef(forwardRef)>
|
||||
`;
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<withPropsOnChange(Card)
|
||||
<ForwardRef(forwardRef)
|
||||
className="ModerateCard-root"
|
||||
data-testid="moderate-comment-comment-id"
|
||||
id="moderate-comment-comment-id"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<ForwardRef(forwardRef)>
|
||||
<div
|
||||
@@ -138,8 +146,10 @@ exports[`renders correctly 1`] = `
|
||||
Theon
|
||||
</Username>
|
||||
</ForwardRef(forwardRef)>
|
||||
<Relay(CommentAuthorContainer)
|
||||
comment={Object {}}
|
||||
/>
|
||||
<Timestamp
|
||||
className="ModerateCard-timestamp"
|
||||
toggleAbsolute={true}
|
||||
>
|
||||
2018-11-29T16:01:51.897Z
|
||||
@@ -227,13 +237,16 @@ exports[`renders correctly 1`] = `
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</withPropsOnChange(Card)>
|
||||
</ForwardRef(forwardRef)>
|
||||
`;
|
||||
|
||||
exports[`renders dangling correctly 1`] = `
|
||||
<withPropsOnChange(Card)
|
||||
<ForwardRef(forwardRef)
|
||||
className="ModerateCard-root ModerateCard-dangling"
|
||||
data-testid="moderate-comment-comment-id"
|
||||
id="moderate-comment-comment-id"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<ForwardRef(forwardRef)>
|
||||
<div
|
||||
@@ -253,8 +266,10 @@ exports[`renders dangling correctly 1`] = `
|
||||
Theon
|
||||
</Username>
|
||||
</ForwardRef(forwardRef)>
|
||||
<Relay(CommentAuthorContainer)
|
||||
comment={Object {}}
|
||||
/>
|
||||
<Timestamp
|
||||
className="ModerateCard-timestamp"
|
||||
toggleAbsolute={true}
|
||||
>
|
||||
2018-11-29T16:01:51.897Z
|
||||
@@ -342,13 +357,16 @@ exports[`renders dangling correctly 1`] = `
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</withPropsOnChange(Card)>
|
||||
</ForwardRef(forwardRef)>
|
||||
`;
|
||||
|
||||
exports[`renders rejected correctly 1`] = `
|
||||
<withPropsOnChange(Card)
|
||||
<ForwardRef(forwardRef)
|
||||
className="ModerateCard-root"
|
||||
data-testid="moderate-comment-comment-id"
|
||||
id="moderate-comment-comment-id"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<ForwardRef(forwardRef)>
|
||||
<div
|
||||
@@ -368,8 +386,10 @@ exports[`renders rejected correctly 1`] = `
|
||||
Theon
|
||||
</Username>
|
||||
</ForwardRef(forwardRef)>
|
||||
<Relay(CommentAuthorContainer)
|
||||
comment={Object {}}
|
||||
/>
|
||||
<Timestamp
|
||||
className="ModerateCard-timestamp"
|
||||
toggleAbsolute={true}
|
||||
>
|
||||
2018-11-29T16:01:51.897Z
|
||||
@@ -457,13 +477,16 @@ exports[`renders rejected correctly 1`] = `
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</withPropsOnChange(Card)>
|
||||
</ForwardRef(forwardRef)>
|
||||
`;
|
||||
|
||||
exports[`renders reply correctly 1`] = `
|
||||
<withPropsOnChange(Card)
|
||||
<ForwardRef(forwardRef)
|
||||
className="ModerateCard-root"
|
||||
data-testid="moderate-comment-comment-id"
|
||||
id="moderate-comment-comment-id"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<ForwardRef(forwardRef)>
|
||||
<div
|
||||
@@ -483,8 +506,10 @@ exports[`renders reply correctly 1`] = `
|
||||
Theon
|
||||
</Username>
|
||||
</ForwardRef(forwardRef)>
|
||||
<Relay(CommentAuthorContainer)
|
||||
comment={Object {}}
|
||||
/>
|
||||
<Timestamp
|
||||
className="ModerateCard-timestamp"
|
||||
toggleAbsolute={true}
|
||||
>
|
||||
2018-11-29T16:01:51.897Z
|
||||
@@ -582,13 +607,16 @@ exports[`renders reply correctly 1`] = `
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</withPropsOnChange(Card)>
|
||||
</ForwardRef(forwardRef)>
|
||||
`;
|
||||
|
||||
exports[`renders story info 1`] = `
|
||||
<withPropsOnChange(Card)
|
||||
<ForwardRef(forwardRef)
|
||||
className="ModerateCard-root"
|
||||
data-testid="moderate-comment-comment-id"
|
||||
id="moderate-comment-comment-id"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<ForwardRef(forwardRef)>
|
||||
<div
|
||||
@@ -608,8 +636,10 @@ exports[`renders story info 1`] = `
|
||||
Theon
|
||||
</Username>
|
||||
</ForwardRef(forwardRef)>
|
||||
<Relay(CommentAuthorContainer)
|
||||
comment={Object {}}
|
||||
/>
|
||||
<Timestamp
|
||||
className="ModerateCard-timestamp"
|
||||
toggleAbsolute={true}
|
||||
>
|
||||
2018-11-29T16:01:51.897Z
|
||||
@@ -726,13 +756,16 @@ exports[`renders story info 1`] = `
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</withPropsOnChange(Card)>
|
||||
</ForwardRef(forwardRef)>
|
||||
`;
|
||||
|
||||
exports[`renders tombstoned when comment is deleted 1`] = `
|
||||
<withPropsOnChange(Card)
|
||||
<ForwardRef(forwardRef)
|
||||
className="ModerateCard-root ModerateCard-deleted"
|
||||
data-testid="moderate-comment-comment-id"
|
||||
id="moderate-comment-comment-id"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<ForwardRef(forwardRef)>
|
||||
<div
|
||||
@@ -752,8 +785,10 @@ exports[`renders tombstoned when comment is deleted 1`] = `
|
||||
Theon
|
||||
</Username>
|
||||
</ForwardRef(forwardRef)>
|
||||
<Relay(CommentAuthorContainer)
|
||||
comment={Object {}}
|
||||
/>
|
||||
<Timestamp
|
||||
className="ModerateCard-timestamp"
|
||||
toggleAbsolute={true}
|
||||
>
|
||||
2018-11-29T16:01:51.897Z
|
||||
@@ -847,5 +882,5 @@ exports[`renders tombstoned when comment is deleted 1`] = `
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</withPropsOnChange(Card)>
|
||||
</ForwardRef(forwardRef)>
|
||||
`;
|
||||
|
||||
@@ -1 +1,12 @@
|
||||
export const REDIRECT_PATH_KEY = "adminRedirectPath";
|
||||
export const HOTKEYS = {
|
||||
NEXT: "j",
|
||||
PREV: "k",
|
||||
APPROVE: "d",
|
||||
REJECT: "f",
|
||||
SWITCH_QUEUE: "t",
|
||||
ZEN: "z",
|
||||
BAN: "b",
|
||||
SEARCH: "ctrl+f",
|
||||
GUIDE: "shift+/",
|
||||
};
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
.root {
|
||||
min-width: 475px;
|
||||
}
|
||||
|
||||
.hotKeyContainer {
|
||||
width: 65px;
|
||||
}
|
||||
|
||||
.hotKey {
|
||||
composes: bodyCopy from "coral-ui/shared/typography.css";
|
||||
background-color: #f2f2f2;
|
||||
border: 1px solid #bbbebf;
|
||||
border-radius: 3px;
|
||||
padding: var(--spacing-1);
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import {
|
||||
Card,
|
||||
CardCloseButton,
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
Modal,
|
||||
Typography,
|
||||
} from "coral-ui/components";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import styles from "./HotkeysModal.css";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const HotkeysModal: FunctionComponent<Props> = ({ open, onClose }) => {
|
||||
return (
|
||||
<Modal open={open} onClose={onClose} aria-labelledby="banModal-title">
|
||||
{({ firstFocusableRef }) => (
|
||||
<Card className={styles.root}>
|
||||
<CardCloseButton onClick={onClose} ref={firstFocusableRef} />
|
||||
<HorizontalGutter size="double">
|
||||
<Localized id="hotkeysModal-title">
|
||||
<Typography variant="header1">Keyboard shortcuts</Typography>
|
||||
</Localized>
|
||||
<Flex itemGutter>
|
||||
<HorizontalGutter>
|
||||
<Localized id="hotkeysModal-navigation-shortcuts">
|
||||
<Typography variant="header3">
|
||||
Navigation shortcuts
|
||||
</Typography>
|
||||
</Localized>
|
||||
<Flex>
|
||||
<div className={styles.hotKeyContainer}>
|
||||
<div className={styles.hotKey}>j</div>
|
||||
</div>
|
||||
<Localized id="hotkeysModal-shortcuts-next">
|
||||
<Typography>Next comment</Typography>
|
||||
</Localized>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<div className={styles.hotKeyContainer}>
|
||||
<div className={styles.hotKey}>k</div>
|
||||
</div>
|
||||
<Localized id="hotkeysModal-shortcuts-prev">
|
||||
<Typography>Previous comment</Typography>
|
||||
</Localized>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<div className={styles.hotKeyContainer}>
|
||||
<div className={styles.hotKey}>ctrl+f</div>
|
||||
</div>
|
||||
<Localized id="hotkeysModal-shortcuts-search">
|
||||
<Typography>Open search</Typography>
|
||||
</Localized>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<div className={styles.hotKeyContainer}>
|
||||
<div className={styles.hotKey}>1..4</div>
|
||||
</div>
|
||||
<Localized id="hotkeysModal-shortcuts-jump">
|
||||
<Typography>Jump to specific queue</Typography>
|
||||
</Localized>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<div className={styles.hotKeyContainer}>
|
||||
<div className={styles.hotKey}>t</div>
|
||||
</div>
|
||||
<Localized id="hotkeysModal-shortcuts-switch">
|
||||
<Typography>Switch queues</Typography>
|
||||
</Localized>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<div className={styles.hotKeyContainer}>
|
||||
<div className={styles.hotKey}>z</div>
|
||||
</div>
|
||||
<Localized id="hotkeysModal-shortcuts-zen">
|
||||
<Typography>Toggle single-comment view</Typography>
|
||||
</Localized>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<div className={styles.hotKeyContainer}>
|
||||
<div className={styles.hotKey}>?</div>
|
||||
</div>
|
||||
<Localized id="hotkeysModal-shortcuts-toggle">
|
||||
<Typography>Toggle shortcuts help</Typography>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</HorizontalGutter>
|
||||
<HorizontalGutter>
|
||||
<Localized id="hotkeysModal-moderation-decisions">
|
||||
<Typography variant="header3">
|
||||
Moderation decisions
|
||||
</Typography>
|
||||
</Localized>
|
||||
<Flex>
|
||||
<div className={styles.hotKeyContainer}>
|
||||
<div className={styles.hotKey}>d</div>
|
||||
</div>
|
||||
<Localized id="hotkeysModal-shortcuts-approve">
|
||||
<Typography>Approve</Typography>
|
||||
</Localized>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<div className={styles.hotKeyContainer}>
|
||||
<div className={styles.hotKey}>f</div>
|
||||
</div>
|
||||
<Localized id="hotkeysModal-shortcuts-reject">
|
||||
<Typography>Reject</Typography>
|
||||
</Localized>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<div className={styles.hotKeyContainer}>
|
||||
<div className={styles.hotKey}>b</div>
|
||||
</div>
|
||||
<Localized id="hotkeysModal-shortcuts-ban">
|
||||
<Typography>Ban comment author</Typography>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</HorizontalGutter>
|
||||
</Flex>
|
||||
</HorizontalGutter>
|
||||
</Card>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default HotkeysModal;
|
||||
@@ -1,9 +1,17 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import key from "keymaster";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import MainLayout from "coral-admin/components/MainLayout";
|
||||
import { HOTKEYS } from "coral-admin/constants";
|
||||
import { PropTypesOf } from "coral-framework/types";
|
||||
import { SubBar } from "coral-ui/components/SubBar";
|
||||
|
||||
import HotkeysModal from "./HotkeysModal";
|
||||
import ModerateNavigationContainer from "./ModerateNavigation";
|
||||
import ModerateSearchBarContainer from "./ModerateSearchBar";
|
||||
|
||||
@@ -24,20 +32,40 @@ const Moderate: FunctionComponent<Props> = ({
|
||||
story,
|
||||
allStories,
|
||||
children,
|
||||
}) => (
|
||||
<div data-testid="moderate-container">
|
||||
<ModerateSearchBarContainer story={story} allStories={allStories} />
|
||||
<SubBar data-testid="moderate-tabBar-container">
|
||||
<ModerateNavigationContainer
|
||||
moderationQueues={moderationQueues}
|
||||
story={story}
|
||||
/>
|
||||
</SubBar>
|
||||
<div className={styles.background} />
|
||||
<MainLayout data-testid="moderate-main-container">
|
||||
<main className={styles.main}>{children}</main>
|
||||
</MainLayout>
|
||||
</div>
|
||||
);
|
||||
}) => {
|
||||
const [showHotkeysModal, setShowHotkeysModal] = useState(false);
|
||||
const closeModal = useCallback(() => {
|
||||
setShowHotkeysModal(false);
|
||||
}, []);
|
||||
const toggleModal = useCallback(() => {
|
||||
setShowHotkeysModal(!showHotkeysModal);
|
||||
}, [showHotkeysModal]);
|
||||
|
||||
useEffect(() => {
|
||||
// Attach the modal toggle when the GUIDE button is pressed.
|
||||
key(HOTKEYS.GUIDE, toggleModal);
|
||||
return () => {
|
||||
// Detach the modal toggle if we have to rebind it.
|
||||
key.unbind(HOTKEYS.GUIDE);
|
||||
};
|
||||
}, [toggleModal]);
|
||||
|
||||
return (
|
||||
<div data-testid="moderate-container">
|
||||
<ModerateSearchBarContainer story={story} allStories={allStories} />
|
||||
<SubBar data-testid="moderate-tabBar-container">
|
||||
<ModerateNavigationContainer
|
||||
moderationQueues={moderationQueues}
|
||||
story={story}
|
||||
/>
|
||||
</SubBar>
|
||||
<div className={styles.background} />
|
||||
<MainLayout data-testid="moderate-main-container">
|
||||
<main className={styles.main}>{children}</main>
|
||||
</MainLayout>
|
||||
<HotkeysModal open={showHotkeysModal} onClose={closeModal} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Moderate;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { HOTKEYS } from "coral-admin/constants";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Match, Router, withRouter } from "found";
|
||||
import key from "keymaster";
|
||||
import React, { FunctionComponent, useEffect, useMemo } from "react";
|
||||
|
||||
import { getModerationLink } from "coral-admin/helpers";
|
||||
import { Counter, Icon, SubBarNavigation } from "coral-ui/components";
|
||||
@@ -11,6 +14,8 @@ interface Props {
|
||||
reportedCount?: number;
|
||||
pendingCount?: number;
|
||||
storyID?: string | null;
|
||||
router: Router;
|
||||
match: Match;
|
||||
}
|
||||
|
||||
const Navigation: FunctionComponent<Props> = ({
|
||||
@@ -18,48 +23,86 @@ const Navigation: FunctionComponent<Props> = ({
|
||||
reportedCount,
|
||||
pendingCount,
|
||||
storyID,
|
||||
}) => (
|
||||
<SubBarNavigation>
|
||||
<NavigationLink to={getModerationLink("reported", storyID)}>
|
||||
<Icon>flag</Icon>
|
||||
<Localized id="moderate-navigation-reported">
|
||||
<span>Reported</span>
|
||||
</Localized>
|
||||
{reportedCount !== undefined && (
|
||||
<Counter data-testid="moderate-navigation-reported-count">
|
||||
{reportedCount}
|
||||
</Counter>
|
||||
)}
|
||||
</NavigationLink>
|
||||
<NavigationLink to={getModerationLink("pending", storyID)}>
|
||||
<Icon>access_time</Icon>
|
||||
<Localized id="moderate-navigation-pending">
|
||||
<span>Pending</span>
|
||||
</Localized>
|
||||
{pendingCount !== undefined && (
|
||||
<Counter data-testid="moderate-navigation-pending-count">
|
||||
{pendingCount}
|
||||
</Counter>
|
||||
)}
|
||||
</NavigationLink>
|
||||
<NavigationLink to={getModerationLink("unmoderated", storyID)}>
|
||||
<Icon>forum</Icon>
|
||||
<Localized id="moderate-navigation-unmoderated">
|
||||
<span>Unmoderated</span>
|
||||
</Localized>
|
||||
{unmoderatedCount !== undefined && (
|
||||
<Counter data-testid="moderate-navigation-unmoderated-count">
|
||||
{unmoderatedCount}
|
||||
</Counter>
|
||||
)}
|
||||
</NavigationLink>
|
||||
<NavigationLink to={getModerationLink("rejected", storyID)}>
|
||||
<Icon>cancel</Icon>
|
||||
<Localized id="moderate-navigation-rejected">
|
||||
<span>Rejected</span>
|
||||
</Localized>
|
||||
</NavigationLink>
|
||||
</SubBarNavigation>
|
||||
);
|
||||
router,
|
||||
match,
|
||||
}) => {
|
||||
const moderationLinks = useMemo(() => {
|
||||
return [
|
||||
getModerationLink("reported", storyID),
|
||||
getModerationLink("pending", storyID),
|
||||
getModerationLink("unmoderated", storyID),
|
||||
getModerationLink("rejected", storyID),
|
||||
];
|
||||
}, [storyID]);
|
||||
|
||||
export default Navigation;
|
||||
useEffect(() => {
|
||||
key(HOTKEYS.SWITCH_QUEUE, () => {
|
||||
const current = match.location.pathname;
|
||||
const index = moderationLinks.indexOf(current);
|
||||
if (index >= 0) {
|
||||
if (index === moderationLinks.length - 1) {
|
||||
router.replace(moderationLinks[0]);
|
||||
} else {
|
||||
router.replace(moderationLinks[index + 1]);
|
||||
}
|
||||
}
|
||||
});
|
||||
for (let i = 0; i < moderationLinks.length; i++) {
|
||||
key(`${i + 1}`, () => {
|
||||
router.replace(moderationLinks[i]);
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
key.unbind(HOTKEYS.SWITCH_QUEUE);
|
||||
for (let i = 0; i < moderationLinks.length; i++) {
|
||||
key.unbind(`${i + 1}`);
|
||||
}
|
||||
};
|
||||
}, [match, moderationLinks]);
|
||||
|
||||
return (
|
||||
<SubBarNavigation>
|
||||
<NavigationLink to={moderationLinks[0]}>
|
||||
<Icon>flag</Icon>
|
||||
<Localized id="moderate-navigation-reported">
|
||||
<span>Reported</span>
|
||||
</Localized>
|
||||
{reportedCount !== undefined && (
|
||||
<Counter data-testid="moderate-navigation-reported-count">
|
||||
{reportedCount}
|
||||
</Counter>
|
||||
)}
|
||||
</NavigationLink>
|
||||
<NavigationLink to={moderationLinks[1]}>
|
||||
<Icon>access_time</Icon>
|
||||
<Localized id="moderate-navigation-pending">
|
||||
<span>Pending</span>
|
||||
</Localized>
|
||||
{pendingCount !== undefined && (
|
||||
<Counter data-testid="moderate-navigation-pending-count">
|
||||
{pendingCount}
|
||||
</Counter>
|
||||
)}
|
||||
</NavigationLink>
|
||||
<NavigationLink to={moderationLinks[2]}>
|
||||
<Icon>forum</Icon>
|
||||
<Localized id="moderate-navigation-unmoderated">
|
||||
<span>Unmoderated</span>
|
||||
</Localized>
|
||||
{unmoderatedCount !== undefined && (
|
||||
<Counter data-testid="moderate-navigation-unmoderated-count">
|
||||
{unmoderatedCount}
|
||||
</Counter>
|
||||
)}
|
||||
</NavigationLink>
|
||||
<NavigationLink to={moderationLinks[3]}>
|
||||
<Icon>cancel</Icon>
|
||||
<Localized id="moderate-navigation-rejected">
|
||||
<span>Rejected</span>
|
||||
</Localized>
|
||||
</NavigationLink>
|
||||
</SubBarNavigation>
|
||||
);
|
||||
};
|
||||
|
||||
export default withRouter(Navigation);
|
||||
|
||||
+6
-131
@@ -1,138 +1,13 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<withPropsOnChange(Navigation)>
|
||||
<NavigationLink
|
||||
to="/admin/moderate/reported"
|
||||
>
|
||||
<ForwardRef(forwardRef)>
|
||||
flag
|
||||
</ForwardRef(forwardRef)>
|
||||
<Localized
|
||||
id="moderate-navigation-reported"
|
||||
>
|
||||
<span>
|
||||
Reported
|
||||
</span>
|
||||
</Localized>
|
||||
</NavigationLink>
|
||||
<NavigationLink
|
||||
to="/admin/moderate/pending"
|
||||
>
|
||||
<ForwardRef(forwardRef)>
|
||||
access_time
|
||||
</ForwardRef(forwardRef)>
|
||||
<Localized
|
||||
id="moderate-navigation-pending"
|
||||
>
|
||||
<span>
|
||||
Pending
|
||||
</span>
|
||||
</Localized>
|
||||
</NavigationLink>
|
||||
<NavigationLink
|
||||
to="/admin/moderate/unmoderated"
|
||||
>
|
||||
<ForwardRef(forwardRef)>
|
||||
forum
|
||||
</ForwardRef(forwardRef)>
|
||||
<Localized
|
||||
id="moderate-navigation-unmoderated"
|
||||
>
|
||||
<span>
|
||||
Unmoderated
|
||||
</span>
|
||||
</Localized>
|
||||
</NavigationLink>
|
||||
<NavigationLink
|
||||
to="/admin/moderate/rejected"
|
||||
>
|
||||
<ForwardRef(forwardRef)>
|
||||
cancel
|
||||
</ForwardRef(forwardRef)>
|
||||
<Localized
|
||||
id="moderate-navigation-rejected"
|
||||
>
|
||||
<span>
|
||||
Rejected
|
||||
</span>
|
||||
</Localized>
|
||||
</NavigationLink>
|
||||
</withPropsOnChange(Navigation)>
|
||||
<Context.Consumer>
|
||||
[Function]
|
||||
</Context.Consumer>
|
||||
`;
|
||||
|
||||
exports[`renders correctly with counts 1`] = `
|
||||
<withPropsOnChange(Navigation)>
|
||||
<NavigationLink
|
||||
to="/admin/moderate/reported"
|
||||
>
|
||||
<ForwardRef(forwardRef)>
|
||||
flag
|
||||
</ForwardRef(forwardRef)>
|
||||
<Localized
|
||||
id="moderate-navigation-reported"
|
||||
>
|
||||
<span>
|
||||
Reported
|
||||
</span>
|
||||
</Localized>
|
||||
<withPropsOnChange(Counter)
|
||||
data-testid="moderate-navigation-reported-count"
|
||||
>
|
||||
4
|
||||
</withPropsOnChange(Counter)>
|
||||
</NavigationLink>
|
||||
<NavigationLink
|
||||
to="/admin/moderate/pending"
|
||||
>
|
||||
<ForwardRef(forwardRef)>
|
||||
access_time
|
||||
</ForwardRef(forwardRef)>
|
||||
<Localized
|
||||
id="moderate-navigation-pending"
|
||||
>
|
||||
<span>
|
||||
Pending
|
||||
</span>
|
||||
</Localized>
|
||||
<withPropsOnChange(Counter)
|
||||
data-testid="moderate-navigation-pending-count"
|
||||
>
|
||||
0
|
||||
</withPropsOnChange(Counter)>
|
||||
</NavigationLink>
|
||||
<NavigationLink
|
||||
to="/admin/moderate/unmoderated"
|
||||
>
|
||||
<ForwardRef(forwardRef)>
|
||||
forum
|
||||
</ForwardRef(forwardRef)>
|
||||
<Localized
|
||||
id="moderate-navigation-unmoderated"
|
||||
>
|
||||
<span>
|
||||
Unmoderated
|
||||
</span>
|
||||
</Localized>
|
||||
<withPropsOnChange(Counter)
|
||||
data-testid="moderate-navigation-unmoderated-count"
|
||||
>
|
||||
3
|
||||
</withPropsOnChange(Counter)>
|
||||
</NavigationLink>
|
||||
<NavigationLink
|
||||
to="/admin/moderate/rejected"
|
||||
>
|
||||
<ForwardRef(forwardRef)>
|
||||
cancel
|
||||
</ForwardRef(forwardRef)>
|
||||
<Localized
|
||||
id="moderate-navigation-rejected"
|
||||
>
|
||||
<span>
|
||||
Rejected
|
||||
</span>
|
||||
</Localized>
|
||||
</NavigationLink>
|
||||
</withPropsOnChange(Navigation)>
|
||||
<Context.Consumer>
|
||||
[Function]
|
||||
</Context.Consumer>
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { HOTKEYS } from "coral-admin/constants";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import key from "keymaster";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { Form } from "react-final-form";
|
||||
|
||||
import { Backdrop, Icon, Popover, SubBar } from "coral-ui/components";
|
||||
@@ -39,6 +46,7 @@ const Bar: FunctionComponent<Props> = ({ title, options, onSearch }) => {
|
||||
({ search }: { search: string }) => onSearch && onSearch(search),
|
||||
[onSearch]
|
||||
);
|
||||
const searchInput = useRef<HTMLInputElement>(null);
|
||||
const blurOnEscProps = useBlurOnEsc(focused);
|
||||
const [
|
||||
mappedOptions,
|
||||
@@ -46,6 +54,13 @@ const Bar: FunctionComponent<Props> = ({ title, options, onSearch }) => {
|
||||
keyboardNavigationHandlers,
|
||||
] = useComboBox("moderate-searchBar-listBoxOption", options);
|
||||
|
||||
useEffect(() => {
|
||||
key(HOTKEYS.SEARCH, () => {
|
||||
if (searchInput && searchInput.current) {
|
||||
searchInput.current.focus();
|
||||
}
|
||||
});
|
||||
}, [searchInput.current]);
|
||||
const contextOptions = mappedOptions
|
||||
.filter(o => o.group === "CONTEXT")
|
||||
.map(o => o.element);
|
||||
@@ -148,6 +163,7 @@ const Bar: FunctionComponent<Props> = ({ title, options, onSearch }) => {
|
||||
<div ref={ref}>
|
||||
<Field
|
||||
title={title}
|
||||
ref={searchInput}
|
||||
{...combineEventHandlers(
|
||||
focusHandlers,
|
||||
blurOnEscProps,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import cn from "classnames";
|
||||
import { withForwardRef } from "coral-ui/hocs";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, HTMLAttributes } from "react";
|
||||
import React, { FunctionComponent, HTMLAttributes, Ref } from "react";
|
||||
import { Field as FormField } from "react-final-form";
|
||||
|
||||
import { BaseButton, Flex, Icon } from "coral-ui/components";
|
||||
@@ -12,6 +13,7 @@ interface Props extends HTMLAttributes<HTMLInputElement> {
|
||||
title: string;
|
||||
className?: string;
|
||||
focused?: boolean;
|
||||
forwardRef?: Ref<HTMLInputElement>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,6 +25,7 @@ const Field: FunctionComponent<Props> = ({
|
||||
className,
|
||||
onBlur,
|
||||
onChange,
|
||||
forwardRef,
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
@@ -63,6 +66,7 @@ const Field: FunctionComponent<Props> = ({
|
||||
aria-label="Search or jump to story..."
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
ref={forwardRef}
|
||||
onBlur={evt => {
|
||||
// Reset value when blurring.
|
||||
input.onChange("");
|
||||
@@ -92,4 +96,4 @@ const Field: FunctionComponent<Props> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default Field;
|
||||
export default withForwardRef(Field);
|
||||
|
||||
@@ -3,22 +3,11 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.exitTransition {
|
||||
opacity: 1;
|
||||
transition: 300ms opacity;
|
||||
}
|
||||
|
||||
.exitTransitionActive {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.exitTransitionDone {
|
||||
opacity: 0;
|
||||
}
|
||||
.viewNewButtonContainer {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.viewNewButton {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { HOTKEYS } from "coral-admin/constants";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
import { CSSTransition, TransitionGroup } from "react-transition-group";
|
||||
|
||||
import AutoLoadMore from "coral-admin/components/AutoLoadMore";
|
||||
import ModerateCardContainer from "coral-admin/components/ModerateCard";
|
||||
import UserHistoryDrawer from "coral-admin/components/UserHistoryDrawer";
|
||||
import { Button, Flex, HorizontalGutter } from "coral-ui/components";
|
||||
import { useHotkey } from "coral-ui/hooks";
|
||||
import { PropTypesOf } from "coral-ui/types";
|
||||
import QueueWrapper from "./QueueWrapper";
|
||||
|
||||
import styles from "./Queue.css";
|
||||
|
||||
@@ -41,18 +43,55 @@ const Queue: FunctionComponent<Props> = ({
|
||||
}) => {
|
||||
const [userDrawerVisible, setUserDrawerVisible] = useState(false);
|
||||
const [userDrawerId, setUserDrawerID] = useState("");
|
||||
const [selectedComment, setSelectedComment] = useState<number | null>(0);
|
||||
const [singleView, setSingleView] = useState(false);
|
||||
|
||||
const toggleView = useCallback(() => {
|
||||
if (!singleView) {
|
||||
setSelectedComment(0);
|
||||
}
|
||||
setSingleView(!singleView);
|
||||
}, [singleView]);
|
||||
|
||||
useHotkey(HOTKEYS.ZEN, toggleView);
|
||||
|
||||
const selectNext = useCallback(() => {
|
||||
const index = selectedComment || 0;
|
||||
const nextComment = comments[index + 1];
|
||||
if (nextComment) {
|
||||
setSelectedComment(index + 1);
|
||||
const container: HTMLElement | null = document.getElementById(
|
||||
`moderate-comment-${nextComment.id}`
|
||||
);
|
||||
if (container) {
|
||||
container.scrollIntoView();
|
||||
}
|
||||
}
|
||||
}, [comments, selectedComment]);
|
||||
|
||||
const selectPrev = useCallback(() => {
|
||||
const index = selectedComment || 0;
|
||||
const prevComment = comments[index - 1];
|
||||
if (prevComment) {
|
||||
setSelectedComment(index - 1);
|
||||
const container: HTMLElement | null = document.getElementById(
|
||||
`moderate-comment-${prevComment.id}`
|
||||
);
|
||||
if (container) {
|
||||
container.scrollIntoView();
|
||||
}
|
||||
}
|
||||
}, [comments, selectedComment]);
|
||||
|
||||
const onShowUserDrawer = useCallback((userID: string) => {
|
||||
setUserDrawerID(userID);
|
||||
setUserDrawerVisible(true);
|
||||
}, []);
|
||||
|
||||
const onShowUserDrawer = useCallback(
|
||||
(userID: string) => {
|
||||
setUserDrawerID(userID);
|
||||
setUserDrawerVisible(true);
|
||||
},
|
||||
[setUserDrawerVisible, setUserDrawerID]
|
||||
);
|
||||
const onHideUserDrawer = useCallback(() => {
|
||||
setUserDrawerVisible(false);
|
||||
setUserDrawerID("");
|
||||
}, [setUserDrawerVisible, setUserDrawerID]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<HorizontalGutter className={styles.root} size="double">
|
||||
@@ -70,31 +109,27 @@ const Queue: FunctionComponent<Props> = ({
|
||||
</Localized>
|
||||
</Flex>
|
||||
)}
|
||||
<TransitionGroup component={null} appear={false} enter={false} exit>
|
||||
{comments
|
||||
// FIXME (Nick/Wyatt): Investigate why comments are coming back null
|
||||
.filter(c => Boolean(c))
|
||||
.map(c => (
|
||||
<CSSTransition
|
||||
key={c.id}
|
||||
timeout={400}
|
||||
classNames={{
|
||||
exit: styles.exitTransition,
|
||||
exitActive: styles.exitTransitionActive,
|
||||
exitDone: styles.exitTransitionDone,
|
||||
}}
|
||||
>
|
||||
<ModerateCardContainer
|
||||
settings={settings}
|
||||
viewer={viewer}
|
||||
comment={c}
|
||||
danglingLogic={danglingLogic}
|
||||
showStoryInfo={Boolean(allStories)}
|
||||
onUsernameClicked={onShowUserDrawer}
|
||||
/>
|
||||
</CSSTransition>
|
||||
))}
|
||||
</TransitionGroup>
|
||||
|
||||
<QueueWrapper
|
||||
comments={comments}
|
||||
singleView={singleView}
|
||||
card={(comment, i) => (
|
||||
<ModerateCardContainer
|
||||
key={comment.id}
|
||||
settings={settings}
|
||||
viewer={viewer}
|
||||
comment={comment}
|
||||
danglingLogic={danglingLogic}
|
||||
showStoryInfo={Boolean(allStories)}
|
||||
onUsernameClicked={onShowUserDrawer}
|
||||
onSetSelected={() => setSelectedComment(i)}
|
||||
selected={selectedComment === i}
|
||||
selectPrev={selectPrev}
|
||||
selectNext={selectNext}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{hasMore && (
|
||||
<Flex justifyContent="center">
|
||||
<AutoLoadMore
|
||||
|
||||
@@ -33,6 +33,7 @@ interface Props {
|
||||
relay: RelayPaginationProp;
|
||||
emptyElement: React.ReactElement;
|
||||
storyID?: string;
|
||||
count?: string;
|
||||
}
|
||||
|
||||
// TODO: use generated types
|
||||
@@ -208,10 +209,10 @@ const createQueueRoute = (
|
||||
export const PendingQueueRoute = createQueueRoute(
|
||||
GQLMODERATION_QUEUE.PENDING,
|
||||
graphql`
|
||||
query QueueRoutePendingQuery($storyID: ID) {
|
||||
query QueueRoutePendingQuery($storyID: ID, $count: Int) {
|
||||
moderationQueues(storyID: $storyID) {
|
||||
pending {
|
||||
...QueueRoute_queue
|
||||
...QueueRoute_queue @arguments(count: $count)
|
||||
}
|
||||
}
|
||||
settings {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
.exitTransition {
|
||||
opacity: 1;
|
||||
transition: 300ms opacity;
|
||||
}
|
||||
|
||||
.exitTransitionSingle {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.exitTransitionActive {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.exitTransitionDone {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import ModerateCardContainer from "coral-admin/components/ModerateCard";
|
||||
import { PropTypesOf } from "coral-ui/types";
|
||||
import React, { FunctionComponent, ReactNode } from "react";
|
||||
import { CSSTransition, TransitionGroup } from "react-transition-group";
|
||||
|
||||
import styles from "./QueueWrapper.css";
|
||||
|
||||
interface Props {
|
||||
singleView: boolean;
|
||||
comments: Array<
|
||||
{ id: string } & PropTypesOf<typeof ModerateCardContainer>["comment"]
|
||||
>;
|
||||
card: (
|
||||
c: { id: string } & PropTypesOf<typeof ModerateCardContainer>["comment"],
|
||||
i: number
|
||||
) => ReactNode;
|
||||
}
|
||||
|
||||
const QueueWrapper: FunctionComponent<Props> = ({
|
||||
singleView,
|
||||
card,
|
||||
comments,
|
||||
}) => {
|
||||
const commentsQueue = singleView ? [comments[0]] : comments;
|
||||
if (singleView) {
|
||||
return (
|
||||
<>
|
||||
{commentsQueue
|
||||
// FIXME (Nick/Wyatt): Investigate why comments are coming back null
|
||||
.filter(c => Boolean(c))
|
||||
.map((c, i) => card(c, i))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TransitionGroup component={null} appear={false} enter={false} exit>
|
||||
{commentsQueue
|
||||
// FIXME (Nick/Wyatt): Investigate why comments are coming back null
|
||||
.filter(c => Boolean(c))
|
||||
.map((c, i) => (
|
||||
<CSSTransition
|
||||
key={c.id}
|
||||
timeout={400}
|
||||
classNames={{
|
||||
exit: singleView
|
||||
? styles.exitTransitionSingle
|
||||
: styles.exitTransition,
|
||||
exitActive: styles.exitTransitionActive,
|
||||
exitDone: styles.exitTransitionDone,
|
||||
}}
|
||||
>
|
||||
{card(c, i)}
|
||||
</CSSTransition>
|
||||
))}
|
||||
</TransitionGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default QueueWrapper;
|
||||
@@ -5,11 +5,10 @@ exports[`renders correctly with load more 1`] = `
|
||||
className="Queue-root"
|
||||
size="double"
|
||||
>
|
||||
<TransitionGroup
|
||||
appear={false}
|
||||
component={null}
|
||||
enter={false}
|
||||
exit={true}
|
||||
<QueueWrapper
|
||||
card={[Function]}
|
||||
comments={Array []}
|
||||
singleView={false}
|
||||
/>
|
||||
<ForwardRef(forwardRef)
|
||||
justifyContent="center"
|
||||
@@ -32,11 +31,10 @@ exports[`renders correctly without load more 1`] = `
|
||||
className="Queue-root"
|
||||
size="double"
|
||||
>
|
||||
<TransitionGroup
|
||||
appear={false}
|
||||
component={null}
|
||||
enter={false}
|
||||
exit={true}
|
||||
<QueueWrapper
|
||||
card={[Function]}
|
||||
comments={Array []}
|
||||
singleView={false}
|
||||
/>
|
||||
<UserHistoryDrawer
|
||||
onClose={[Function]}
|
||||
|
||||
@@ -26,5 +26,9 @@ exports[`renders correctly 1`] = `
|
||||
className="Moderate-main"
|
||||
/>
|
||||
</MainLayout>
|
||||
<HotkeysModal
|
||||
onClose={[Function]}
|
||||
open={false}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -15,8 +15,11 @@ exports[`approves comment in reported queue: count should be 1 1`] = `
|
||||
|
||||
exports[`approves comment in reported queue: dangling 1`] = `
|
||||
<div
|
||||
className="Card-root ModerateCard-root ModerateCard-dangling"
|
||||
className="Card-root ModerateCard-root ModerateCard-dangling ModerateCard-selected"
|
||||
data-testid="moderate-comment-comment-0"
|
||||
id="moderate-comment-comment-0"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
@@ -57,7 +60,7 @@ exports[`approves comment in reported queue: dangling 1`] = `
|
||||
type="button"
|
||||
>
|
||||
<time
|
||||
className="RelativeTime-root Timestamp-text ModerateCard-timestamp"
|
||||
className="RelativeTime-root Timestamp-text"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
@@ -254,8 +257,11 @@ exports[`rejects comment in reported queue: count should be 1 1`] = `
|
||||
|
||||
exports[`rejects comment in reported queue: dangling 1`] = `
|
||||
<div
|
||||
className="Card-root ModerateCard-root ModerateCard-dangling"
|
||||
className="Card-root ModerateCard-root ModerateCard-dangling ModerateCard-selected"
|
||||
data-testid="moderate-comment-comment-0"
|
||||
id="moderate-comment-comment-0"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
@@ -296,7 +302,7 @@ exports[`rejects comment in reported queue: dangling 1`] = `
|
||||
type="button"
|
||||
>
|
||||
<time
|
||||
className="RelativeTime-root Timestamp-text ModerateCard-timestamp"
|
||||
className="RelativeTime-root Timestamp-text"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
@@ -490,8 +496,11 @@ exports[`renders reported queue with comments 1`] = `
|
||||
className="Box-root HorizontalGutter-root Queue-root HorizontalGutter-double"
|
||||
>
|
||||
<div
|
||||
className="Card-root ModerateCard-root"
|
||||
className="Card-root ModerateCard-root ModerateCard-selected"
|
||||
data-testid="moderate-comment-comment-0"
|
||||
id="moderate-comment-comment-0"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
@@ -532,7 +541,7 @@ exports[`renders reported queue with comments 1`] = `
|
||||
type="button"
|
||||
>
|
||||
<time
|
||||
className="RelativeTime-root Timestamp-text ModerateCard-timestamp"
|
||||
className="RelativeTime-root Timestamp-text"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
@@ -715,6 +724,9 @@ exports[`renders reported queue with comments 1`] = `
|
||||
<div
|
||||
className="Card-root ModerateCard-root"
|
||||
data-testid="moderate-comment-comment-1"
|
||||
id="moderate-comment-comment-1"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
@@ -755,7 +767,7 @@ exports[`renders reported queue with comments 1`] = `
|
||||
type="button"
|
||||
>
|
||||
<time
|
||||
className="RelativeTime-root Timestamp-text ModerateCard-timestamp"
|
||||
className="RelativeTime-root Timestamp-text"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
@@ -952,8 +964,11 @@ exports[`renders reported queue with comments 2`] = `
|
||||
className="Box-root HorizontalGutter-root Queue-root HorizontalGutter-double"
|
||||
>
|
||||
<div
|
||||
className="Card-root ModerateCard-root"
|
||||
className="Card-root ModerateCard-root ModerateCard-selected"
|
||||
data-testid="moderate-comment-comment-0"
|
||||
id="moderate-comment-comment-0"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
@@ -994,7 +1009,7 @@ exports[`renders reported queue with comments 2`] = `
|
||||
type="button"
|
||||
>
|
||||
<time
|
||||
className="RelativeTime-root Timestamp-text ModerateCard-timestamp"
|
||||
className="RelativeTime-root Timestamp-text"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
@@ -1177,6 +1192,9 @@ exports[`renders reported queue with comments 2`] = `
|
||||
<div
|
||||
className="Card-root ModerateCard-root"
|
||||
data-testid="moderate-comment-comment-1"
|
||||
id="moderate-comment-comment-1"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
@@ -1217,7 +1235,7 @@ exports[`renders reported queue with comments 2`] = `
|
||||
type="button"
|
||||
>
|
||||
<time
|
||||
className="RelativeTime-root Timestamp-text ModerateCard-timestamp"
|
||||
className="RelativeTime-root Timestamp-text"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
@@ -1406,6 +1424,9 @@ exports[`renders reported queue with comments and load more 1`] = `
|
||||
<div
|
||||
className="Card-root ModerateCard-root"
|
||||
data-testid="moderate-comment-comment-2"
|
||||
id="moderate-comment-comment-2"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
@@ -1446,7 +1467,7 @@ exports[`renders reported queue with comments and load more 1`] = `
|
||||
type="button"
|
||||
>
|
||||
<time
|
||||
className="RelativeTime-root Timestamp-text ModerateCard-timestamp"
|
||||
className="RelativeTime-root Timestamp-text"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
|
||||
@@ -15,8 +15,11 @@ exports[`approves comment in rejected queue: count should be 1 1`] = `
|
||||
|
||||
exports[`approves comment in rejected queue: dangling 1`] = `
|
||||
<div
|
||||
className="Card-root ModerateCard-root ModerateCard-dangling"
|
||||
className="Card-root ModerateCard-root ModerateCard-dangling ModerateCard-selected"
|
||||
data-testid="moderate-comment-comment-0"
|
||||
id="moderate-comment-comment-0"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
@@ -57,7 +60,7 @@ exports[`approves comment in rejected queue: dangling 1`] = `
|
||||
type="button"
|
||||
>
|
||||
<time
|
||||
className="RelativeTime-root Timestamp-text ModerateCard-timestamp"
|
||||
className="RelativeTime-root Timestamp-text"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
@@ -251,8 +254,11 @@ exports[`renders rejected queue with comments 1`] = `
|
||||
className="Box-root HorizontalGutter-root Queue-root HorizontalGutter-double"
|
||||
>
|
||||
<div
|
||||
className="Card-root ModerateCard-root"
|
||||
className="Card-root ModerateCard-root ModerateCard-selected"
|
||||
data-testid="moderate-comment-comment-0"
|
||||
id="moderate-comment-comment-0"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
@@ -293,7 +299,7 @@ exports[`renders rejected queue with comments 1`] = `
|
||||
type="button"
|
||||
>
|
||||
<time
|
||||
className="RelativeTime-root Timestamp-text ModerateCard-timestamp"
|
||||
className="RelativeTime-root Timestamp-text"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
@@ -476,6 +482,9 @@ exports[`renders rejected queue with comments 1`] = `
|
||||
<div
|
||||
className="Card-root ModerateCard-root"
|
||||
data-testid="moderate-comment-comment-1"
|
||||
id="moderate-comment-comment-1"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
@@ -516,7 +525,7 @@ exports[`renders rejected queue with comments 1`] = `
|
||||
type="button"
|
||||
>
|
||||
<time
|
||||
className="RelativeTime-root Timestamp-text ModerateCard-timestamp"
|
||||
className="RelativeTime-root Timestamp-text"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
@@ -705,6 +714,9 @@ exports[`renders rejected queue with comments and load more 1`] = `
|
||||
<div
|
||||
className="Card-root ModerateCard-root"
|
||||
data-testid="moderate-comment-comment-2"
|
||||
id="moderate-comment-comment-2"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
@@ -745,7 +757,7 @@ exports[`renders rejected queue with comments and load more 1`] = `
|
||||
type="button"
|
||||
>
|
||||
<time
|
||||
className="RelativeTime-root Timestamp-text ModerateCard-timestamp"
|
||||
className="RelativeTime-root Timestamp-text"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
exports[`approves single comment 1`] = `
|
||||
<div
|
||||
className="Card-root ModerateCard-root"
|
||||
className="Card-root ModerateCard-root ModerateCard-selected"
|
||||
data-testid="moderate-comment-comment-0"
|
||||
id="moderate-comment-comment-0"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
@@ -44,7 +47,7 @@ exports[`approves single comment 1`] = `
|
||||
type="button"
|
||||
>
|
||||
<time
|
||||
className="RelativeTime-root Timestamp-text ModerateCard-timestamp"
|
||||
className="RelativeTime-root Timestamp-text"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
@@ -207,8 +210,11 @@ exports[`approves single comment 1`] = `
|
||||
|
||||
exports[`rejects single comment 1`] = `
|
||||
<div
|
||||
className="Card-root ModerateCard-root"
|
||||
className="Card-root ModerateCard-root ModerateCard-selected"
|
||||
data-testid="moderate-comment-comment-0"
|
||||
id="moderate-comment-comment-0"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
@@ -249,7 +255,7 @@ exports[`rejects single comment 1`] = `
|
||||
type="button"
|
||||
>
|
||||
<time
|
||||
className="RelativeTime-root Timestamp-text ModerateCard-timestamp"
|
||||
className="RelativeTime-root Timestamp-text"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
@@ -447,8 +453,11 @@ exports[`renders single comment view 1`] = `
|
||||
className="Box-root HorizontalGutter-root Queue-root HorizontalGutter-double"
|
||||
>
|
||||
<div
|
||||
className="Card-root ModerateCard-root"
|
||||
className="Card-root ModerateCard-root ModerateCard-selected"
|
||||
data-testid="moderate-comment-comment-0"
|
||||
id="moderate-comment-comment-0"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex"
|
||||
@@ -489,7 +498,7 @@ exports[`renders single comment view 1`] = `
|
||||
type="button"
|
||||
>
|
||||
<time
|
||||
className="RelativeTime-root Timestamp-text ModerateCard-timestamp"
|
||||
className="RelativeTime-root Timestamp-text"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import cn from "classnames";
|
||||
import { withStyles } from "coral-ui/hocs";
|
||||
import React from "react";
|
||||
import { withForwardRef } from "coral-ui/hocs";
|
||||
import React, { Ref } from "react";
|
||||
import { FunctionComponent, ReactNode } from "react";
|
||||
|
||||
import styles from "./Card.css";
|
||||
@@ -18,19 +19,22 @@ export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
* Override or extend the styles applied to the component.
|
||||
*/
|
||||
classes: typeof styles;
|
||||
|
||||
tabIndex?: number;
|
||||
forwardRef?: Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
const Card: FunctionComponent<CardProps> = props => {
|
||||
const { className, classes, children, ...rest } = props;
|
||||
const { className, classes, children, forwardRef, ...rest } = props;
|
||||
|
||||
const rootClassName = cn(classes.root, className);
|
||||
|
||||
return (
|
||||
<div className={rootClassName} {...rest}>
|
||||
<div className={rootClassName} {...rest} ref={forwardRef}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withStyles(styles)(Card);
|
||||
const enhanced = withForwardRef(withStyles(styles)(Card));
|
||||
export default enhanced;
|
||||
|
||||
@@ -2,3 +2,4 @@ export { default as useFocus } from "./useFocus";
|
||||
export { default as usePreventFocusLoss } from "./usePreventFocusLoss";
|
||||
export { default as useBlurOnEsc } from "./useBlurOnEsc";
|
||||
export { default as useComboBox } from "./useComboBox";
|
||||
export { default as useHotkey } from "./useHotkey";
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import key from "keymaster";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
type CallbackFn = (event: KeyboardEvent) => void;
|
||||
interface Ref {
|
||||
current: any;
|
||||
}
|
||||
|
||||
export default function useHotkey(trigger: string, handler: CallbackFn) {
|
||||
const savedHandler: Ref = useRef();
|
||||
useEffect(() => {
|
||||
savedHandler.current = handler;
|
||||
}, [handler]);
|
||||
|
||||
useEffect(() => {
|
||||
const eventListener = () => savedHandler.current();
|
||||
|
||||
key(trigger, eventListener);
|
||||
|
||||
return () => {
|
||||
key.unbind(trigger);
|
||||
};
|
||||
}, [trigger]);
|
||||
}
|
||||
@@ -825,3 +825,23 @@ forgotPassword-emailAddressLabel = Email address
|
||||
forgotPassword-emailAddressTextField =
|
||||
.placeholder = Email Address
|
||||
forgotPassword-sendEmailButton = Send email
|
||||
|
||||
commentAuthor-status-banned = Banned
|
||||
|
||||
hotkeysModal-title = Keyboard shortcuts
|
||||
hotkeysModal-navigation-shortcuts = Navigation shortcuts
|
||||
hotkeysModal-shortcuts-next = Next comment
|
||||
hotkeysModal-shortcuts-prev = Previous comment
|
||||
hotkeysModal-shortcuts-search = Open search
|
||||
hotkeysModal-shortcuts-jump = Jump to specific queue
|
||||
hotkeysModal-shortcuts-switch = Switch queues
|
||||
hotkeysModal-shortcuts-toggle = Toggle shortcuts help
|
||||
hotkeysModal-shortcuts-single-view = Single comment view
|
||||
hotkeysModal-moderation-decisions = Moderation decisions
|
||||
hotkeysModal-shortcuts-approve = Approve
|
||||
hotkeysModal-shortcuts-reject = Reject
|
||||
hotkeysModal-shortcuts-ban = Ban comment author
|
||||
hotkeysModal-shortcuts-zen = Toggle single-comment view
|
||||
|
||||
|
||||
|
||||
|
||||
Vendored
+52
@@ -0,0 +1,52 @@
|
||||
interface KeymasterEvent {
|
||||
key: string;
|
||||
method: KeyHandler;
|
||||
mods: number[];
|
||||
scope: string;
|
||||
shortcut: string;
|
||||
}
|
||||
|
||||
type KeyHandler = (
|
||||
keyboardEvent: KeyboardEvent,
|
||||
keymasterEvent: KeymasterEvent
|
||||
) => void;
|
||||
|
||||
interface FilterEvent {
|
||||
target?: {
|
||||
tagName?: string;
|
||||
};
|
||||
srcElement?: {
|
||||
tagName?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface Keymaster {
|
||||
(key: string, callback: KeyHandler): void;
|
||||
(key: string, scope: string, callback: KeyHandler): void;
|
||||
|
||||
shift: boolean;
|
||||
alt: boolean;
|
||||
option: boolean;
|
||||
ctrl: boolean;
|
||||
control: boolean;
|
||||
command: boolean;
|
||||
|
||||
setScope(scopeName: string): void;
|
||||
getScope(): string;
|
||||
deleteScope(scopeName: string): void;
|
||||
|
||||
noConflict(): void;
|
||||
|
||||
unbind(key: string, scopeName?: string): void;
|
||||
|
||||
isPressed(keyCode: number): boolean;
|
||||
getPressedKeyCodes(): number[];
|
||||
|
||||
filter(event: FilterEvent): void;
|
||||
}
|
||||
|
||||
declare let key: Keymaster;
|
||||
|
||||
declare module "keymaster" {
|
||||
export = key;
|
||||
}
|
||||
Reference in New Issue
Block a user