[CORL 1031] Story actions (#2975)

* add story actions dropdown with rescrape button

* move open/close story mutation to story actions dropdown

* remove unused props

* fix tests
This commit is contained in:
Tessa Thornton
2020-06-08 14:14:22 -06:00
committed by GitHub
parent eee909f05b
commit a949d7e881
20 changed files with 412 additions and 296 deletions
@@ -0,0 +1,40 @@
import { graphql } from "react-relay";
import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutation,
MutationInput,
} from "coral-framework/lib/relay";
import { RescrapeStoryMutation as MutationTypes } from "coral-admin/__generated__/RescrapeStoryMutation.graphql";
let clientMutationId = 0;
const RescrapeStoryMutation = createMutation(
"scrapeStory",
(environment: Environment, input: MutationInput<MutationTypes>) =>
commitMutationPromiseNormalized<MutationTypes>(environment, {
mutation: graphql`
mutation RescrapeStoryMutation($input: ScrapeStoryInput!) {
scrapeStory(input: $input) {
story {
id
metadata {
title
}
}
clientMutationId
}
}
`,
variables: {
input: {
...input,
clientMutationId: (clientMutationId++).toString(),
},
},
})
);
export default RescrapeStoryMutation;
@@ -0,0 +1,94 @@
import { Localized } from "@fluent/react/compat";
import React, { FunctionComponent } from "react";
import {
Button,
ButtonIcon,
ClickOutside,
Dropdown,
DropdownButton,
Popover,
} from "coral-ui/components/v2";
interface Props {
onRescrape: () => void;
onOpen: () => void;
onClose: () => void;
canOpen: boolean;
canClose: boolean;
}
const StoryActions: FunctionComponent<Props> = ({
onRescrape,
onOpen,
onClose,
canOpen,
canClose,
}) => {
return (
<Localized id="stories-actions-popover" attrs={{ description: true }}>
<Popover
id="stories-Actions"
placement="bottom-start"
description="A dropdown to select story actions"
body={({ toggleVisibility }) => (
<ClickOutside onClickOutside={toggleVisibility}>
<Dropdown>
<Localized id="stories-actions-rescrape">
<DropdownButton
onClick={() => {
onRescrape();
toggleVisibility();
}}
>
Re-scrape
</DropdownButton>
</Localized>
{canOpen && (
<Localized id="stories-actions-open">
<DropdownButton
onClick={() => {
onOpen();
toggleVisibility();
}}
>
Open story
</DropdownButton>
</Localized>
)}
{canClose && (
<Localized id="stories-actions-close">
<DropdownButton
onClick={() => {
onClose();
toggleVisibility();
}}
>
Close story
</DropdownButton>
</Localized>
)}
</Dropdown>
</ClickOutside>
)}
>
{({ toggleVisibility, ref, visible }) => (
<Localized id="stories-actionsButton" attrs={{ "aria-label": true }}>
<Button
aria-label="Select action"
onClick={toggleVisibility}
ref={ref}
color="mono"
variant="text"
uppercase={false}
>
{<ButtonIcon size="lg">more_horiz</ButtonIcon>}
</Button>
</Localized>
)}
</Popover>
</Localized>
);
};
export default StoryActions;
@@ -0,0 +1,61 @@
import React, { FunctionComponent, useCallback } from "react";
import { graphql } from "react-relay";
import { Ability, can } from "coral-admin/permissions";
import { useMutation, withFragmentContainer } from "coral-framework/lib/relay";
import { GQLSTORY_STATUS } from "coral-framework/schema";
import { StoryActionsContainer_story } from "coral-admin/__generated__/StoryActionsContainer_story.graphql";
import { StoryActionsContainer_viewer } from "coral-admin/__generated__/StoryActionsContainer_viewer.graphql";
import CloseStoryMutation from "./CloseStoryMutation";
import OpenStoryMutation from "./OpenStoryMutation";
import RescrapeStoryMutation from "./RescrapeStoryMutation";
import StoryActions from "./StoryActions";
interface Props {
story: StoryActionsContainer_story;
viewer: StoryActionsContainer_viewer;
}
const StoryActionsContainer: FunctionComponent<Props> = (props) => {
const rescrape = useMutation(RescrapeStoryMutation);
const closeStory = useMutation(CloseStoryMutation);
const openStory = useMutation(OpenStoryMutation);
const onRescrape = useCallback(() => {
rescrape({ id: props.story.id });
}, [props.story.id]);
const onClose = useCallback(() => {
closeStory({ id: props.story.id });
}, [props.story.id]);
const onOpen = useCallback(() => {
openStory({ id: props.story.id });
}, [props.story.id]);
const canChangeStatus = can(props.viewer, Ability.CHANGE_STORY_STATUS);
return (
<StoryActions
onRescrape={onRescrape}
onClose={onClose}
onOpen={onOpen}
canClose={props.story.status === GQLSTORY_STATUS.OPEN && canChangeStatus}
canOpen={props.story.status === GQLSTORY_STATUS.CLOSED && canChangeStatus}
/>
);
};
const enhanced = withFragmentContainer<Props>({
viewer: graphql`
fragment StoryActionsContainer_viewer on User {
id
role
}
`,
story: graphql`
fragment StoryActionsContainer_story on Story {
id
status
}
`,
})(StoryActionsContainer);
export default enhanced;
@@ -0,0 +1,4 @@
export {
default,
default as StoryActionsContainer,
} from "./StoryActionsContainer";
@@ -19,7 +19,8 @@
.reportedCountColumn,
.pendingCountColumn,
.totalCountColumn {
.totalCountColumn,
.actionsColumn {
text-align: center;
}
@@ -12,7 +12,8 @@ import {
TextLink,
} from "coral-ui/components/v2";
import StoryStatus from "./StoryStatus";
import StoryActionsContainer from "./StoryActions";
import StoryStatusContainer from "./StoryStatus";
import styles from "./StoryRow.css";
@@ -21,8 +22,9 @@ interface Props {
title: string | null;
author: string | null;
publishDate: string | null;
story: PropTypesOf<typeof StoryStatus>["story"];
viewer: PropTypesOf<typeof StoryStatus>["viewer"];
story: PropTypesOf<typeof StoryActionsContainer>["story"] &
PropTypesOf<typeof StoryStatusContainer>["story"];
viewer: PropTypesOf<typeof StoryActionsContainer>["viewer"];
siteName: string;
siteID: string;
multisite: boolean;
@@ -73,7 +75,10 @@ const UserRow: FunctionComponent<Props> = (props) => (
{props.totalCount}
</TableCell>
<TableCell className={styles.statusColumn}>
<StoryStatus story={props.story} viewer={props.viewer} />
<StoryStatusContainer story={props.story} />
</TableCell>
<TableCell className={styles.actionsColumn}>
<StoryActionsContainer story={props.story} viewer={props.viewer} />
</TableCell>
</TableRow>
);
@@ -53,7 +53,7 @@ const enhanced = withFragmentContainer<Props>({
viewer: graphql`
fragment StoryRowContainer_viewer on User {
id
...StoryStatusChangeContainer_viewer
...StoryActionsContainer_viewer
}
`,
story: graphql`
@@ -80,7 +80,8 @@ const enhanced = withFragmentContainer<Props>({
id
}
isClosed
...StoryStatusChangeContainer_story
...StoryActionsContainer_story
...StoryStatusContainer_story
}
`,
})(StoryRowContainer);
@@ -1,9 +0,0 @@
.button {
width: 100%;
justify-content: space-between;
padding: 0;
}
.dropdownButton {
min-width: 80px;
}
@@ -1,82 +0,0 @@
import { Localized } from "@fluent/react/compat";
import React, { FunctionComponent } from "react";
import TranslatedStoryStatus from "coral-admin/components/TranslatedStoryStatus";
import { GQLSTORY_STATUS, GQLSTORY_STATUS_RL } from "coral-framework/schema";
import {
Button,
ButtonIcon,
ClickOutside,
Dropdown,
DropdownButton,
Popover,
} from "coral-ui/components/v2";
import StoryStatusText from "./StoryStatusText";
import styles from "./StoryStatusChange.css";
interface Props {
onChangeStatus: (status: GQLSTORY_STATUS_RL) => void;
status: GQLSTORY_STATUS_RL;
}
const StoryStatusChange: FunctionComponent<Props> = (props) => (
<Localized id="stories-status-popover" attrs={{ description: true }}>
<Popover
id="stories-statusChange"
placement="bottom-start"
description="A dropdown to change the user status"
body={({ toggleVisibility }) => (
<ClickOutside onClickOutside={toggleVisibility}>
<Dropdown>
{Object.keys(GQLSTORY_STATUS).map((r: GQLSTORY_STATUS_RL) => (
<TranslatedStoryStatus
key={r}
container={
<DropdownButton
className={styles.dropdownButton}
onClick={() => {
props.onChangeStatus(r);
toggleVisibility();
}}
>
dummy
</DropdownButton>
}
>
{r}
</TranslatedStoryStatus>
))}
</Dropdown>
</ClickOutside>
)}
>
{({ toggleVisibility, ref, visible }) => (
<Localized
id="stories-changeStatusButton"
attrs={{ "aria-label": true }}
>
<Button
aria-label="Change status"
className={styles.button}
onClick={toggleVisibility}
ref={ref}
color="mono"
uppercase={false}
variant="text"
>
<StoryStatusText>{props.status}</StoryStatusText>
{
<ButtonIcon size="lg">
{visible ? "arrow_drop_up" : "arrow_drop_down"}
</ButtonIcon>
}
</Button>
</Localized>
)}
</Popover>
</Localized>
);
export default StoryStatusChange;
@@ -1,75 +0,0 @@
import React, { FunctionComponent, useCallback } from "react";
import { graphql } from "react-relay";
import { Ability, can } from "coral-admin/permissions";
import {
MutationProp,
withFragmentContainer,
withMutation,
} from "coral-framework/lib/relay";
import { GQLSTORY_STATUS, GQLSTORY_STATUS_RL } from "coral-framework/schema";
import { StoryStatusChangeContainer_story } from "coral-admin/__generated__/StoryStatusChangeContainer_story.graphql";
import { StoryStatusChangeContainer_viewer } from "coral-admin/__generated__/StoryStatusChangeContainer_viewer.graphql";
import CloseStoryMutation from "./CloseStoryMutation";
import OpenStoryMutation from "./OpenStoryMutation";
import StoryStatusChange from "./StoryStatusChange";
import StoryStatusText from "./StoryStatusText";
interface Props {
openStory: MutationProp<typeof OpenStoryMutation>;
closeStory: MutationProp<typeof CloseStoryMutation>;
viewer: StoryStatusChangeContainer_viewer;
story: StoryStatusChangeContainer_story;
}
const StoryStatusChangeContainer: FunctionComponent<Props> = (props) => {
const handleChangeStatus = useCallback(
(status: GQLSTORY_STATUS_RL) => {
if (props.story.status === status) {
return;
}
if (status === GQLSTORY_STATUS.CLOSED) {
props.closeStory({ id: props.story.id });
} else if (status === GQLSTORY_STATUS.OPEN) {
props.openStory({ id: props.story.id });
}
},
[props.story.id, props.closeStory, props.openStory, props.story.status]
);
const canChangeStatus = can(props.viewer, Ability.CHANGE_STORY_STATUS);
if (!canChangeStatus) {
return <StoryStatusText>{props.story.status}</StoryStatusText>;
}
return (
<StoryStatusChange
onChangeStatus={handleChangeStatus}
status={props.story.status}
/>
);
};
const enhanced = withMutation(OpenStoryMutation)(
withMutation(CloseStoryMutation)(
withFragmentContainer<Props>({
viewer: graphql`
fragment StoryStatusChangeContainer_viewer on User {
id
role
}
`,
story: graphql`
fragment StoryStatusChangeContainer_story on Story {
id
status
}
`,
})(StoryStatusChangeContainer)
)
);
export default enhanced;
@@ -0,0 +1,27 @@
import React, { FunctionComponent } from "react";
import { graphql } from "react-relay";
import { withFragmentContainer } from "coral-framework/lib/relay";
import { StoryStatusContainer_story } from "coral-admin/__generated__/StoryStatusContainer_story.graphql";
import StoryStatusText from "./StoryStatusText";
interface Props {
story: StoryStatusContainer_story;
}
const StoryStatusContainer: FunctionComponent<Props> = (props) => {
return <StoryStatusText>{props.story.status}</StoryStatusText>;
};
const enhanced = withFragmentContainer<Props>({
story: graphql`
fragment StoryStatusContainer_story on Story {
id
status
}
`,
})(StoryStatusContainer);
export default enhanced;
@@ -1,4 +1,4 @@
export {
default,
default as StoryStatusChangeContainer,
} from "./StoryStatusChangeContainer";
default as StoryStatusContainer,
} from "./StoryStatusContainer";
@@ -10,14 +10,19 @@ $tableHeaderAltTextColor: var(--v2-colors-mono-100);
width: 14%;
}
.reportedCountColumn {
width: 9%;
width: 8%;
}
.pendingCountColumn {
width: 9%;
width: 8%;
}
.totalCountColumn {
width: 9%;
width: 8%;
}
.actionsColumn {
width: 8%;
}
.clickToModerate {
font-size: var(--v2-font-size-2);
font-weight: var(--v2-font-weight-primary-semi-bold);
@@ -68,6 +68,9 @@ const StoryTable: FunctionComponent<Props> = (props) => (
<Localized id="stories-column-status">
<TableCell className={styles.statusColumn}>Status</TableCell>
</Localized>
<Localized id="stories-column-actions">
<TableCell className={styles.actionsColumn}>Actions</TableCell>
</Localized>
</TableRow>
</TableHead>
<TableBody>
@@ -174,6 +174,11 @@ exports[`renders empty stories 1`] = `
>
Status
</th>
<th
className="TableCell-root StoryTable-actionsColumn TableCell-header"
>
Actions
</th>
</tr>
</thead>
<tbody
@@ -227,13 +232,22 @@ exports[`renders empty stories 1`] = `
</td>
<td
className="TableCell-root StoryRow-statusColumn TableCell-body"
>
<span
className="StoryStatusText-root StoryStatusText-open"
>
Open
</span>
</td>
<td
className="TableCell-root StoryRow-actionsColumn TableCell-body"
>
<div
className="Popover-root"
>
<button
aria-label="Change status"
className="BaseButton-root Button-root Button-sizeRegular Button-colorMono Button-variantText StoryStatusChange-button"
aria-label="Select action"
className="BaseButton-root Button-root Button-sizeRegular Button-colorMono Button-variantText"
data-color="mono"
data-variant="text"
onBlur={[Function]}
@@ -244,29 +258,24 @@ exports[`renders empty stories 1`] = `
onTouchEnd={[Function]}
type="button"
>
<span
className="StoryStatusText-root StoryStatusText-open"
>
Open
</span>
<i
aria-hidden="true"
className="Icon-root Icon-lg ButtonIcon-root"
>
arrow_drop_down
more_horiz
</i>
</button>
<div
aria-hidden={true}
aria-labelledby="stories-statusChange-ariainfo"
id="stories-statusChange"
aria-labelledby="stories-Actions-ariainfo"
id="stories-Actions"
role="dialog"
>
<div
className="AriaInfo-root"
id="stories-statusChange-ariainfo"
id="stories-Actions-ariainfo"
>
A dropdown to change the story status
A dropdown to select story actions
</div>
</div>
</div>
@@ -320,13 +329,22 @@ exports[`renders empty stories 1`] = `
</td>
<td
className="TableCell-root StoryRow-statusColumn TableCell-body"
>
<span
className="StoryStatusText-root StoryStatusText-open"
>
Open
</span>
</td>
<td
className="TableCell-root StoryRow-actionsColumn TableCell-body"
>
<div
className="Popover-root"
>
<button
aria-label="Change status"
className="BaseButton-root Button-root Button-sizeRegular Button-colorMono Button-variantText StoryStatusChange-button"
aria-label="Select action"
className="BaseButton-root Button-root Button-sizeRegular Button-colorMono Button-variantText"
data-color="mono"
data-variant="text"
onBlur={[Function]}
@@ -337,29 +355,24 @@ exports[`renders empty stories 1`] = `
onTouchEnd={[Function]}
type="button"
>
<span
className="StoryStatusText-root StoryStatusText-open"
>
Open
</span>
<i
aria-hidden="true"
className="Icon-root Icon-lg ButtonIcon-root"
>
arrow_drop_down
more_horiz
</i>
</button>
<div
aria-hidden={true}
aria-labelledby="stories-statusChange-ariainfo"
id="stories-statusChange"
aria-labelledby="stories-Actions-ariainfo"
id="stories-Actions"
role="dialog"
>
<div
className="AriaInfo-root"
id="stories-statusChange-ariainfo"
id="stories-Actions-ariainfo"
>
A dropdown to change the story status
A dropdown to select story actions
</div>
</div>
</div>
@@ -546,6 +559,11 @@ exports[`renders stories 1`] = `
>
Status
</th>
<th
className="TableCell-root StoryTable-actionsColumn TableCell-header"
>
Actions
</th>
</tr>
</thead>
<tbody
@@ -599,13 +617,22 @@ exports[`renders stories 1`] = `
</td>
<td
className="TableCell-root StoryRow-statusColumn TableCell-body"
>
<span
className="StoryStatusText-root StoryStatusText-open"
>
Open
</span>
</td>
<td
className="TableCell-root StoryRow-actionsColumn TableCell-body"
>
<div
className="Popover-root"
>
<button
aria-label="Change status"
className="BaseButton-root Button-root Button-sizeRegular Button-colorMono Button-variantText StoryStatusChange-button"
aria-label="Select action"
className="BaseButton-root Button-root Button-sizeRegular Button-colorMono Button-variantText"
data-color="mono"
data-variant="text"
onBlur={[Function]}
@@ -616,29 +643,24 @@ exports[`renders stories 1`] = `
onTouchEnd={[Function]}
type="button"
>
<span
className="StoryStatusText-root StoryStatusText-open"
>
Open
</span>
<i
aria-hidden="true"
className="Icon-root Icon-lg ButtonIcon-root"
>
arrow_drop_down
more_horiz
</i>
</button>
<div
aria-hidden={true}
aria-labelledby="stories-statusChange-ariainfo"
id="stories-statusChange"
aria-labelledby="stories-Actions-ariainfo"
id="stories-Actions"
role="dialog"
>
<div
className="AriaInfo-root"
id="stories-statusChange-ariainfo"
id="stories-Actions-ariainfo"
>
A dropdown to change the story status
A dropdown to select story actions
</div>
</div>
</div>
@@ -692,13 +714,22 @@ exports[`renders stories 1`] = `
</td>
<td
className="TableCell-root StoryRow-statusColumn TableCell-body"
>
<span
className="StoryStatusText-root StoryStatusText-open"
>
Open
</span>
</td>
<td
className="TableCell-root StoryRow-actionsColumn TableCell-body"
>
<div
className="Popover-root"
>
<button
aria-label="Change status"
className="BaseButton-root Button-root Button-sizeRegular Button-colorMono Button-variantText StoryStatusChange-button"
aria-label="Select action"
className="BaseButton-root Button-root Button-sizeRegular Button-colorMono Button-variantText"
data-color="mono"
data-variant="text"
onBlur={[Function]}
@@ -709,29 +740,24 @@ exports[`renders stories 1`] = `
onTouchEnd={[Function]}
type="button"
>
<span
className="StoryStatusText-root StoryStatusText-open"
>
Open
</span>
<i
aria-hidden="true"
className="Icon-root Icon-lg ButtonIcon-root"
>
arrow_drop_down
more_horiz
</i>
</button>
<div
aria-hidden={true}
aria-labelledby="stories-statusChange-ariainfo"
id="stories-statusChange"
aria-labelledby="stories-Actions-ariainfo"
id="stories-Actions"
role="dialog"
>
<div
className="AriaInfo-root"
id="stories-statusChange-ariainfo"
id="stories-Actions-ariainfo"
>
A dropdown to change the story status
A dropdown to select story actions
</div>
</div>
</div>
@@ -201,9 +201,9 @@ it("change story status", async () => {
selector: "tr",
});
const changeStatusButton = within(storyRow).getByLabelText("Change status");
const changeStatusButton = within(storyRow).getByLabelText("Select action");
const popup = within(storyRow).getByLabelText(
"A dropdown to change the story status"
"A dropdown to select story actions"
);
/** CLOSE STORY */
@@ -211,7 +211,9 @@ it("change story status", async () => {
changeStatusButton.props.onClick();
});
act(() => {
within(popup).getByText("Closed", { selector: "button" }).props.onClick();
within(popup)
.getByText("Close story", { selector: "button" })
.props.onClick();
});
within(storyRow).getByText("Closed");
@@ -223,7 +225,9 @@ it("change story status", async () => {
});
act(() => {
within(popup).getByText("Open", { selector: "button" }).props.onClick();
within(popup)
.getByText("Open story", { selector: "button" })
.props.onClick();
});
within(storyRow).getByText("Open");
+11
View File
@@ -475,6 +475,17 @@ stories-column-site = Site
site-table-siteName = Site name
stories-filter-sites = Site
stories-column-actions = Actions
stories-column-rescrape = Re-scrape
stories-actionsButton =
.aria-label = Select action
stories-actions-popover =
.description = A dropdown to select story actions
stories-actions-rescrape = Re-scrape
stories-actions-close = Close story
stories-actions-open = Open story
### Sections
moderate-section-selector-allSections = All Sections