mirror of
https://github.com/wassname/talk.git
synced 2026-07-29 11:28:24 +08:00
[CORL-1090] Sections Alpha (#2973)
* feat: added section filtering support * fix: addressed issues around filtering sections * fix: fixed issue with section filter
This commit is contained in:
@@ -2,6 +2,7 @@ import { graphql } from "react-relay";
|
||||
import { ConnectionHandler, Environment } from "relay-runtime";
|
||||
|
||||
import { getQueueConnection } from "coral-admin/helpers";
|
||||
import { SectionFilter } from "coral-common/section";
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
@@ -18,7 +19,11 @@ const FeatureCommentMutation = createMutation(
|
||||
"featureComment",
|
||||
(
|
||||
environment: Environment,
|
||||
input: MutationInput<FeatureCommentMutation> & { storyID: string },
|
||||
input: MutationInput<FeatureCommentMutation> & {
|
||||
storyID?: string | null;
|
||||
siteID?: string | null;
|
||||
section?: SectionFilter | null;
|
||||
},
|
||||
{ uuidGenerator }: CoralContext
|
||||
) =>
|
||||
commitMutationPromiseNormalized<FeatureCommentMutation>(environment, {
|
||||
@@ -69,10 +74,34 @@ const FeatureCommentMutation = createMutation(
|
||||
},
|
||||
updater: (store) => {
|
||||
const connections = [
|
||||
getQueueConnection(store, "PENDING", input.storyID),
|
||||
getQueueConnection(store, "REPORTED", input.storyID),
|
||||
getQueueConnection(store, "UNMODERATED", input.storyID),
|
||||
getQueueConnection(store, "REJECTED", input.storyID),
|
||||
getQueueConnection(
|
||||
store,
|
||||
"PENDING",
|
||||
input.storyID,
|
||||
input.siteID,
|
||||
input.section
|
||||
),
|
||||
getQueueConnection(
|
||||
store,
|
||||
"REPORTED",
|
||||
input.storyID,
|
||||
input.siteID,
|
||||
input.section
|
||||
),
|
||||
getQueueConnection(
|
||||
store,
|
||||
"UNMODERATED",
|
||||
input.storyID,
|
||||
input.siteID,
|
||||
input.section
|
||||
),
|
||||
getQueueConnection(
|
||||
store,
|
||||
"REJECTED",
|
||||
input.storyID,
|
||||
input.siteID,
|
||||
input.section
|
||||
),
|
||||
].filter((c) => c);
|
||||
connections.forEach((con) =>
|
||||
ConnectionHandler.deleteNode(con!, input.commentID)
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "coral-admin/mutations";
|
||||
import FadeInTransition from "coral-framework/components/FadeInTransition";
|
||||
import { getModerationLink } from "coral-framework/helpers";
|
||||
import parseModerationOptions from "coral-framework/helpers/parseModerationOptions";
|
||||
import {
|
||||
MutationProp,
|
||||
withFragmentContainer,
|
||||
@@ -95,10 +96,14 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const { storyID, siteID, section } = parseModerationOptions(match);
|
||||
|
||||
await approveComment({
|
||||
commentID: comment.id,
|
||||
commentRevisionID: comment.revision.id,
|
||||
storyID: match.params.storyID,
|
||||
storyID,
|
||||
siteID,
|
||||
section,
|
||||
});
|
||||
if (loadNext) {
|
||||
loadNext();
|
||||
@@ -110,10 +115,14 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const { storyID, siteID, section } = parseModerationOptions(match);
|
||||
|
||||
await rejectComment({
|
||||
commentID: comment.id,
|
||||
commentRevisionID: comment.revision.id,
|
||||
storyID: match.params.storyID,
|
||||
storyID,
|
||||
siteID,
|
||||
section,
|
||||
});
|
||||
if (loadNext) {
|
||||
loadNext();
|
||||
@@ -125,10 +134,14 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const { storyID, siteID, section } = parseModerationOptions(match);
|
||||
|
||||
featureComment({
|
||||
commentID: comment.id,
|
||||
commentRevisionID: comment.revision.id,
|
||||
storyID: match.params.storyID,
|
||||
storyID,
|
||||
siteID,
|
||||
section,
|
||||
});
|
||||
}, [featureComment, comment, match]);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import cn from "classnames";
|
||||
import { noop } from "lodash";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import AutoLoadMore from "coral-admin/components/AutoLoadMore";
|
||||
@@ -16,20 +17,20 @@ import {
|
||||
import styles from "./PaginatedSelect.css";
|
||||
|
||||
interface Props {
|
||||
onLoadMore: () => void;
|
||||
onLoadMore?: () => void;
|
||||
icon?: string;
|
||||
hasMore: boolean;
|
||||
disableLoadMore: boolean;
|
||||
loading: boolean;
|
||||
hasMore?: boolean;
|
||||
disableLoadMore?: boolean;
|
||||
loading?: boolean;
|
||||
selected: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PaginatedSelect: FunctionComponent<Props> = ({
|
||||
loading,
|
||||
onLoadMore,
|
||||
disableLoadMore,
|
||||
hasMore,
|
||||
onLoadMore = noop,
|
||||
disableLoadMore = false,
|
||||
hasMore = false,
|
||||
loading = false,
|
||||
children,
|
||||
icon,
|
||||
selected,
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
RecordSourceSelectorProxy,
|
||||
} from "relay-runtime";
|
||||
|
||||
import { SectionFilter } from "coral-common/section";
|
||||
import {
|
||||
GQLCOMMENT_STATUS,
|
||||
GQLMODERATION_QUEUE_RL,
|
||||
@@ -14,7 +15,8 @@ export default function getQueueConnection(
|
||||
store: RecordSourceSelectorProxy | RecordSourceProxy,
|
||||
queue: GQLMODERATION_QUEUE_RL | "REJECTED" | "APPROVED",
|
||||
storyID?: string | null,
|
||||
siteID?: string | null
|
||||
siteID?: string | null,
|
||||
section?: SectionFilter | null
|
||||
): RecordProxy | null | undefined {
|
||||
const root = store.getRoot();
|
||||
if (queue === "REJECTED") {
|
||||
@@ -22,6 +24,7 @@ export default function getQueueConnection(
|
||||
status: GQLCOMMENT_STATUS.REJECTED,
|
||||
storyID,
|
||||
siteID,
|
||||
section,
|
||||
});
|
||||
}
|
||||
if (queue === "APPROVED") {
|
||||
@@ -29,11 +32,13 @@ export default function getQueueConnection(
|
||||
status: GQLCOMMENT_STATUS.APPROVED,
|
||||
storyID,
|
||||
siteID,
|
||||
section,
|
||||
});
|
||||
}
|
||||
const queuesRecord = root.getLinkedRecord("moderationQueues", {
|
||||
storyID,
|
||||
siteID,
|
||||
section,
|
||||
})!;
|
||||
if (!queuesRecord) {
|
||||
return undefined;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { graphql } from "react-relay";
|
||||
import { ConnectionHandler, Environment } from "relay-runtime";
|
||||
|
||||
import { getQueueConnection } from "coral-admin/helpers";
|
||||
import { SectionFilter } from "coral-common/section";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
@@ -16,13 +17,19 @@ const ApproveCommentMutation = createMutation(
|
||||
"approveComment",
|
||||
(
|
||||
environment: Environment,
|
||||
input: MutationInput<MutationTypes> & { storyID?: string }
|
||||
input: MutationInput<MutationTypes> & {
|
||||
storyID?: string | null;
|
||||
siteID?: string | null;
|
||||
section?: SectionFilter | null;
|
||||
}
|
||||
) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation ApproveCommentMutation(
|
||||
$input: ApproveCommentInput!
|
||||
$storyID: ID
|
||||
$siteID: ID
|
||||
$section: SectionFilter
|
||||
) {
|
||||
approveComment(input: $input) {
|
||||
comment {
|
||||
@@ -42,7 +49,11 @@ const ApproveCommentMutation = createMutation(
|
||||
}
|
||||
...ModeratedByContainer_comment
|
||||
}
|
||||
moderationQueues(storyID: $storyID) {
|
||||
moderationQueues(
|
||||
storyID: $storyID
|
||||
siteID: $siteID
|
||||
section: $section
|
||||
) {
|
||||
unmoderated {
|
||||
count
|
||||
}
|
||||
@@ -64,6 +75,8 @@ const ApproveCommentMutation = createMutation(
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
storyID: input.storyID,
|
||||
siteID: input.siteID,
|
||||
section: input.section,
|
||||
},
|
||||
optimisticUpdater: (store) => {
|
||||
const proxy = store.get(input.commentID)!;
|
||||
@@ -72,10 +85,34 @@ const ApproveCommentMutation = createMutation(
|
||||
},
|
||||
updater: (store) => {
|
||||
const connections = [
|
||||
getQueueConnection(store, "REPORTED", input.storyID),
|
||||
getQueueConnection(store, "PENDING", input.storyID),
|
||||
getQueueConnection(store, "UNMODERATED", input.storyID),
|
||||
getQueueConnection(store, "REJECTED", input.storyID),
|
||||
getQueueConnection(
|
||||
store,
|
||||
"REPORTED",
|
||||
input.storyID,
|
||||
input.siteID,
|
||||
input.section
|
||||
),
|
||||
getQueueConnection(
|
||||
store,
|
||||
"PENDING",
|
||||
input.storyID,
|
||||
input.siteID,
|
||||
input.section
|
||||
),
|
||||
getQueueConnection(
|
||||
store,
|
||||
"UNMODERATED",
|
||||
input.storyID,
|
||||
input.siteID,
|
||||
input.section
|
||||
),
|
||||
getQueueConnection(
|
||||
store,
|
||||
"REJECTED",
|
||||
input.storyID,
|
||||
input.siteID,
|
||||
input.section
|
||||
),
|
||||
].filter((c) => c);
|
||||
connections.forEach((con) =>
|
||||
ConnectionHandler.deleteNode(con!, input.commentID)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { graphql } from "react-relay";
|
||||
import { ConnectionHandler, Environment } from "relay-runtime";
|
||||
|
||||
import { getQueueConnection } from "coral-admin/helpers";
|
||||
import { SectionFilter } from "coral-common/section";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
@@ -16,13 +17,19 @@ const RejectCommentMutation = createMutation(
|
||||
"rejectComment",
|
||||
(
|
||||
environment: Environment,
|
||||
input: MutationInput<MutationTypes> & { storyID?: string }
|
||||
input: MutationInput<MutationTypes> & {
|
||||
storyID?: string | null;
|
||||
siteID?: string | null;
|
||||
section?: SectionFilter | null;
|
||||
}
|
||||
) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation RejectCommentMutation(
|
||||
$input: RejectCommentInput!
|
||||
$storyID: ID
|
||||
$siteID: ID
|
||||
$section: SectionFilter
|
||||
) {
|
||||
rejectComment(input: $input) {
|
||||
comment {
|
||||
@@ -42,7 +49,11 @@ const RejectCommentMutation = createMutation(
|
||||
}
|
||||
...ModeratedByContainer_comment
|
||||
}
|
||||
moderationQueues(storyID: $storyID) {
|
||||
moderationQueues(
|
||||
storyID: $storyID
|
||||
siteID: $siteID
|
||||
section: $section
|
||||
) {
|
||||
unmoderated {
|
||||
count
|
||||
}
|
||||
@@ -64,6 +75,8 @@ const RejectCommentMutation = createMutation(
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
storyID: input.storyID,
|
||||
siteID: input.siteID,
|
||||
section: input.section,
|
||||
},
|
||||
optimisticUpdater: (store) => {
|
||||
const proxy = store.get(input.commentID)!;
|
||||
|
||||
@@ -39,7 +39,9 @@ class OIDCConfigContainer extends React.Component<Props, State> {
|
||||
}
|
||||
this.setState({ awaitingResponse: true });
|
||||
try {
|
||||
const config = await this.props.discoverOIDCConfiguration({
|
||||
const {
|
||||
discoverOIDCConfiguration: config,
|
||||
} = await this.props.discoverOIDCConfiguration({
|
||||
issuer,
|
||||
});
|
||||
if (config) {
|
||||
|
||||
@@ -8,12 +8,14 @@ import React, {
|
||||
|
||||
import MainLayout from "coral-admin/components/MainLayout";
|
||||
import { HOTKEYS } from "coral-admin/constants";
|
||||
import { SectionFilter } from "coral-common/section";
|
||||
import { PropTypesOf } from "coral-framework/types";
|
||||
import { SubBar } from "coral-ui/components/v2/SubBar";
|
||||
|
||||
import HotkeysModal from "./HotkeysModal";
|
||||
import ModerateNavigationContainer from "./ModerateNavigation";
|
||||
import ModerateSearchBarContainer from "./ModerateSearchBar";
|
||||
import { SectionSelectorContainer } from "./SectionSelector";
|
||||
import { SiteSelectorContainer } from "./SiteSelector";
|
||||
|
||||
import styles from "./Moderate.css";
|
||||
@@ -26,12 +28,14 @@ interface RouteParams {
|
||||
interface Props {
|
||||
story: PropTypesOf<typeof ModerateNavigationContainer>["story"] &
|
||||
PropTypesOf<typeof ModerateSearchBarContainer>["story"];
|
||||
query: PropTypesOf<typeof SiteSelectorContainer>["query"];
|
||||
query: PropTypesOf<typeof SiteSelectorContainer>["query"] &
|
||||
PropTypesOf<typeof SectionSelectorContainer>["query"];
|
||||
moderationQueues: PropTypesOf<
|
||||
typeof ModerateNavigationContainer
|
||||
>["moderationQueues"];
|
||||
allStories: boolean;
|
||||
siteID: string | null;
|
||||
section?: SectionFilter | null;
|
||||
settings: PropTypesOf<typeof ModerateSearchBarContainer>["settings"] | null;
|
||||
children?: React.ReactNode;
|
||||
queueName: string;
|
||||
@@ -48,6 +52,7 @@ const Moderate: FunctionComponent<Props> = ({
|
||||
routeParams,
|
||||
settings,
|
||||
siteID,
|
||||
section,
|
||||
}) => {
|
||||
const [showHotkeysModal, setShowHotkeysModal] = useState(false);
|
||||
const closeModal = useCallback(() => {
|
||||
@@ -80,12 +85,20 @@ const Moderate: FunctionComponent<Props> = ({
|
||||
siteID={routeParams.siteID || siteID || null}
|
||||
/>
|
||||
}
|
||||
sectionSelector={
|
||||
<SectionSelectorContainer
|
||||
queueName={queueName}
|
||||
query={query}
|
||||
section={section}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SubBar data-testid="moderate-tabBar-container">
|
||||
<ModerateNavigationContainer
|
||||
moderationQueues={moderationQueues}
|
||||
story={story}
|
||||
siteID={routeParams.siteID || siteID || null}
|
||||
section={section}
|
||||
/>
|
||||
</SubBar>
|
||||
<div className={styles.background} />
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Match, RouteProps, Router, withRouter } from "found";
|
||||
import React from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import parseModerationOptions from "coral-framework/helpers/parseModerationOptions";
|
||||
import { withRouteConfig } from "coral-framework/lib/router";
|
||||
import { Spinner } from "coral-ui/components/v2";
|
||||
|
||||
@@ -36,6 +37,8 @@ class ModerateContainer extends React.Component<Props> {
|
||||
].find((name) => {
|
||||
return this.props.match.location.pathname.includes(name);
|
||||
});
|
||||
const { section } = parseModerationOptions(this.props.match);
|
||||
|
||||
if (!this.props.data) {
|
||||
return (
|
||||
<Moderate
|
||||
@@ -43,6 +46,7 @@ class ModerateContainer extends React.Component<Props> {
|
||||
story={null}
|
||||
settings={null}
|
||||
siteID={null}
|
||||
section={section}
|
||||
query={this.props.data}
|
||||
routeParams={this.props.match.params}
|
||||
queueName={queueName || "default"}
|
||||
@@ -58,6 +62,7 @@ class ModerateContainer extends React.Component<Props> {
|
||||
moderationQueues={this.props.data.moderationQueues}
|
||||
story={this.props.data.story || null}
|
||||
siteID={this.props.data.story ? this.props.data.story.site.id : null}
|
||||
section={section}
|
||||
routeParams={this.props.match.params}
|
||||
query={this.props.data}
|
||||
allStories={allStories}
|
||||
@@ -76,6 +81,7 @@ const enhanced = withRouteConfig<Props>({
|
||||
$storyID: ID
|
||||
$includeStory: Boolean!
|
||||
$siteID: ID
|
||||
$section: SectionFilter
|
||||
) {
|
||||
settings {
|
||||
...ModerateSearchBarContainer_settings
|
||||
@@ -87,19 +93,23 @@ const enhanced = withRouteConfig<Props>({
|
||||
id
|
||||
}
|
||||
}
|
||||
moderationQueues(storyID: $storyID, siteID: $siteID) {
|
||||
moderationQueues(storyID: $storyID, siteID: $siteID, section: $section) {
|
||||
...ModerateNavigationContainer_moderationQueues
|
||||
}
|
||||
...SiteSelectorContainer_query
|
||||
...SectionSelectorContainer_query
|
||||
}
|
||||
`,
|
||||
cacheConfig: { force: true },
|
||||
prepareVariables: (params, match) => {
|
||||
const { storyID, siteID, section } = parseModerationOptions(match);
|
||||
|
||||
return {
|
||||
storyID: match.params.storyID,
|
||||
siteID: match.params.siteID,
|
||||
includeStory: Boolean(match.params.storyID),
|
||||
includeSite: Boolean(match.params.siteID),
|
||||
storyID,
|
||||
siteID,
|
||||
section,
|
||||
includeStory: Boolean(storyID),
|
||||
includeSite: Boolean(siteID),
|
||||
};
|
||||
},
|
||||
})(withRouter(ModerateContainer));
|
||||
|
||||
+5
-2
@@ -20,8 +20,11 @@ const ModerateCountsCommentEnteredSubscription = createSubscription(
|
||||
) =>
|
||||
requestSubscription(environment, {
|
||||
subscription: graphql`
|
||||
subscription ModerateCountsCommentEnteredSubscription($storyID: ID) {
|
||||
commentEnteredModerationQueue(storyID: $storyID) {
|
||||
subscription ModerateCountsCommentEnteredSubscription(
|
||||
$storyID: ID
|
||||
$siteID: ID
|
||||
) {
|
||||
commentEnteredModerationQueue(storyID: $storyID, siteID: $siteID) {
|
||||
queue
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -20,11 +20,11 @@ const ModerateCountsCommentLeftSubscription = createSubscription(
|
||||
) =>
|
||||
requestSubscription(environment, {
|
||||
subscription: graphql`
|
||||
subscription ModerateCountsCommentLeftSubscription($storyID: ID) {
|
||||
commentLeftModerationQueue(storyID: $storyID) {
|
||||
queue
|
||||
}
|
||||
commentLeftModerationQueue(storyID: $storyID) {
|
||||
subscription ModerateCountsCommentLeftSubscription(
|
||||
$storyID: ID
|
||||
$siteID: ID
|
||||
) {
|
||||
commentLeftModerationQueue(storyID: $storyID, siteID: $siteID) {
|
||||
queue
|
||||
}
|
||||
}
|
||||
|
||||
+10
-2
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { SectionFilter } from "coral-common/section";
|
||||
import {
|
||||
combineDisposables,
|
||||
useSubscription,
|
||||
@@ -18,6 +19,7 @@ interface Props {
|
||||
moderationQueues: ModerationQueuesData | null;
|
||||
story: StoryData | null;
|
||||
siteID: string | null;
|
||||
section?: SectionFilter | null;
|
||||
}
|
||||
|
||||
const ModerateNavigationContainer: React.FunctionComponent<Props> = (props) => {
|
||||
@@ -29,7 +31,7 @@ const ModerateNavigationContainer: React.FunctionComponent<Props> = (props) => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.moderationQueues) {
|
||||
if (!props.moderationQueues || props.section) {
|
||||
return;
|
||||
}
|
||||
const vars = {
|
||||
@@ -43,7 +45,12 @@ const ModerateNavigationContainer: React.FunctionComponent<Props> = (props) => {
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, [Boolean(props.moderationQueues), props.story, props.siteID]);
|
||||
}, [
|
||||
Boolean(props.moderationQueues),
|
||||
props.story,
|
||||
props.siteID,
|
||||
props.section,
|
||||
]);
|
||||
|
||||
if (!props.moderationQueues) {
|
||||
return <Navigation />;
|
||||
@@ -55,6 +62,7 @@ const ModerateNavigationContainer: React.FunctionComponent<Props> = (props) => {
|
||||
pendingCount={props.moderationQueues.pending.count}
|
||||
storyID={props.story && props.story.id}
|
||||
siteID={props.siteID}
|
||||
section={props.section}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import { isNumber } from "lodash";
|
||||
import React, { FunctionComponent, useEffect, useMemo } from "react";
|
||||
|
||||
import { HOTKEYS } from "coral-admin/constants";
|
||||
import { SectionFilter } from "coral-common/section";
|
||||
import { getModerationLink } from "coral-framework/helpers";
|
||||
import { Counter, Icon, SubBarNavigation } from "coral-ui/components/v2";
|
||||
|
||||
@@ -16,6 +17,7 @@ interface Props {
|
||||
pendingCount?: number | null;
|
||||
storyID?: string | null;
|
||||
siteID?: string | null;
|
||||
section?: SectionFilter | null;
|
||||
router: Router;
|
||||
match: Match;
|
||||
}
|
||||
@@ -26,16 +28,17 @@ const Navigation: FunctionComponent<Props> = ({
|
||||
pendingCount,
|
||||
storyID,
|
||||
siteID,
|
||||
section,
|
||||
router,
|
||||
match,
|
||||
}) => {
|
||||
const moderationLinks = useMemo(() => {
|
||||
return [
|
||||
getModerationLink({ queue: "reported", storyID, siteID }),
|
||||
getModerationLink({ queue: "pending", storyID, siteID }),
|
||||
getModerationLink({ queue: "unmoderated", storyID, siteID }),
|
||||
getModerationLink({ queue: "approved", storyID, siteID }),
|
||||
getModerationLink({ queue: "rejected", storyID, siteID }),
|
||||
getModerationLink({ queue: "reported", storyID, siteID, section }),
|
||||
getModerationLink({ queue: "pending", storyID, siteID, section }),
|
||||
getModerationLink({ queue: "unmoderated", storyID, siteID, section }),
|
||||
getModerationLink({ queue: "approved", storyID, siteID, section }),
|
||||
getModerationLink({ queue: "rejected", storyID, siteID, section }),
|
||||
];
|
||||
}, [storyID, siteID]);
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ interface Props {
|
||||
onSearch?: (value: string) => void;
|
||||
|
||||
siteSelector: React.ReactNode;
|
||||
sectionSelector?: React.ReactNode;
|
||||
|
||||
multisite: boolean;
|
||||
}
|
||||
@@ -48,6 +49,7 @@ const Bar: FunctionComponent<Props> = ({
|
||||
options,
|
||||
onSearch,
|
||||
siteSelector,
|
||||
sectionSelector,
|
||||
multisite,
|
||||
}) => {
|
||||
const [focused, focusHandlers] = useFocus();
|
||||
@@ -90,7 +92,7 @@ const Bar: FunctionComponent<Props> = ({
|
||||
aria-expanded={focused}
|
||||
>
|
||||
<Backdrop className={styles.bumpZIndex} active={focused} />
|
||||
{multisite ? siteSelector : null}
|
||||
{multisite ? siteSelector : sectionSelector}
|
||||
<Form onSubmit={submitHandler}>
|
||||
{({ handleSubmit }) => (
|
||||
<Localized
|
||||
|
||||
+58
-13
@@ -12,6 +12,7 @@ import { graphql } from "react-relay";
|
||||
import { getModerationLink } from "coral-framework/helpers";
|
||||
import { useEffectWhenChanged } from "coral-framework/hooks";
|
||||
import { useFetch, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { GQLFEATURE_FLAG } from "coral-framework/schema";
|
||||
import { PropTypesOf } from "coral-framework/types";
|
||||
import { Flex, Spinner } from "coral-ui/components/v2";
|
||||
import { blur } from "coral-ui/helpers";
|
||||
@@ -22,11 +23,13 @@ import {
|
||||
|
||||
import { ModerateSearchBarContainer_settings as SettingsData } from "coral-admin/__generated__/ModerateSearchBarContainer_settings.graphql";
|
||||
import { ModerateSearchBarContainer_story as ModerationQueuesData } from "coral-admin/__generated__/ModerateSearchBarContainer_story.graphql";
|
||||
import { SearchStoryFetchQueryResponse } from "coral-admin/__generated__/SearchStoryFetchQuery.graphql";
|
||||
|
||||
import Bar from "./Bar";
|
||||
import GoToAriaInfo from "./GoToAriaInfo";
|
||||
import ModerateAllOption from "./ModerateAllOption";
|
||||
import Option from "./Option";
|
||||
import OptionDetail from "./OptionDetail";
|
||||
import SearchStoryFetch from "./SearchStoryFetch";
|
||||
import SeeAllOption from "./SeeAllOption";
|
||||
|
||||
@@ -37,6 +40,7 @@ interface Props {
|
||||
settings: SettingsData | null;
|
||||
allStories: boolean;
|
||||
siteSelector: React.ReactNode;
|
||||
sectionSelector?: React.ReactNode;
|
||||
siteID: string | null;
|
||||
}
|
||||
|
||||
@@ -67,6 +71,38 @@ function useLinkNavHandler(router: Router): ListBoxOptionClickOrEnterHandler {
|
||||
);
|
||||
}
|
||||
|
||||
function getStoryDetails(
|
||||
{
|
||||
multisite,
|
||||
featureFlags,
|
||||
}: SettingsData | SearchStoryFetchQueryResponse["settings"],
|
||||
{
|
||||
site,
|
||||
metadata,
|
||||
}:
|
||||
| ModerationQueuesData
|
||||
| SearchStoryFetchQueryResponse["stories"]["edges"][0]["node"]
|
||||
) {
|
||||
return (
|
||||
<Flex itemGutter>
|
||||
{/* If we're multisite, show the site name. */}
|
||||
{multisite && <OptionDetail variant="bold">{site.name}</OptionDetail>}
|
||||
{/* If the section is available, show it on the story result */}
|
||||
{!multisite &&
|
||||
featureFlags.includes(GQLFEATURE_FLAG.SECTIONS) &&
|
||||
(metadata?.section ? (
|
||||
<OptionDetail variant="bold">{metadata.section}</OptionDetail>
|
||||
) : (
|
||||
<Localized id="moderate-section-uncategorized">
|
||||
<OptionDetail variant="muted">Uncategorized</OptionDetail>
|
||||
</Localized>
|
||||
))}
|
||||
{/* If the author is available, show it on the story result */}
|
||||
{metadata?.author && <OptionDetail>{metadata.author}</OptionDetail>}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
function getContextOptionsWhenModeratingAll(
|
||||
onClickOrEnter: ListBoxOptionClickOrEnterHandler
|
||||
): SearchBarOptions {
|
||||
@@ -88,9 +124,10 @@ function getContextOptionsWhenModeratingAll(
|
||||
|
||||
function getContextOptionsWhenModeratingStory(
|
||||
onClickOrEnter: ListBoxOptionClickOrEnterHandler,
|
||||
settings: SettingsData | null,
|
||||
story: ModerationQueuesData | null
|
||||
): SearchBarOptions {
|
||||
if (story === null) {
|
||||
if (story === null || settings === null) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
@@ -98,7 +135,7 @@ function getContextOptionsWhenModeratingStory(
|
||||
element: (
|
||||
<Option
|
||||
href={getModerationLink({ storyID: story.id })}
|
||||
details={story.metadata && story.metadata.author}
|
||||
details={getStoryDetails(settings, story)}
|
||||
>
|
||||
<GoToAriaInfo /> {story.metadata && story.metadata.title}
|
||||
</Option>
|
||||
@@ -115,6 +152,7 @@ function getContextOptionsWhenModeratingStory(
|
||||
}
|
||||
|
||||
type OnSearchCallback = (search: string) => void;
|
||||
|
||||
interface SearchParams {
|
||||
query: string;
|
||||
limit: number;
|
||||
@@ -165,7 +203,7 @@ function useSearchOptions(
|
||||
if (siteID) {
|
||||
searchParams.siteID = siteID;
|
||||
}
|
||||
const stories = await searchStory(searchParams);
|
||||
const { settings, stories } = await searchStory(searchParams);
|
||||
if (searchCount !== searchCountRef.current) {
|
||||
// This result is old, so we can discard it.
|
||||
return;
|
||||
@@ -184,12 +222,7 @@ function useSearchOptions(
|
||||
storyID: e.node.id,
|
||||
siteID: e.node.site.id,
|
||||
})}
|
||||
details={
|
||||
<Flex itemGutter>
|
||||
<strong>{e.node.site.name}</strong>
|
||||
{e.node.metadata && e.node.metadata.author}
|
||||
</Flex>
|
||||
}
|
||||
details={getStoryDetails(settings, e.node)}
|
||||
>
|
||||
<GoToAriaInfo /> {e.node.metadata && e.node.metadata.title}
|
||||
</Option>
|
||||
@@ -237,7 +270,11 @@ const ModerateSearchBarContainer: React.FunctionComponent<Props> = (props) => {
|
||||
const linkNavHandler = useLinkNavHandler(props.router);
|
||||
const contextOptions: PropTypesOf<typeof Bar>["options"] = props.allStories
|
||||
? getContextOptionsWhenModeratingAll(linkNavHandler)
|
||||
: getContextOptionsWhenModeratingStory(linkNavHandler, props.story);
|
||||
: getContextOptionsWhenModeratingStory(
|
||||
linkNavHandler,
|
||||
props.settings,
|
||||
props.story
|
||||
);
|
||||
|
||||
const [searchOptions, onSearch] = useSearchOptions(
|
||||
linkNavHandler,
|
||||
@@ -258,6 +295,7 @@ const ModerateSearchBarContainer: React.FunctionComponent<Props> = (props) => {
|
||||
<Localized id="moderate-searchBar-allStories" attrs={{ title: true }}>
|
||||
<Bar
|
||||
siteSelector={props.siteSelector}
|
||||
sectionSelector={props.sectionSelector}
|
||||
multisite={props.settings ? props.settings.multisite : false}
|
||||
title="All stories"
|
||||
{...childProps}
|
||||
@@ -270,22 +308,26 @@ const ModerateSearchBarContainer: React.FunctionComponent<Props> = (props) => {
|
||||
<Bar
|
||||
multisite={props.settings ? props.settings.multisite : false}
|
||||
siteSelector={props.siteSelector}
|
||||
sectionSelector={props.sectionSelector}
|
||||
title={""}
|
||||
{...childProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const t = props.story.metadata && props.story.metadata.title;
|
||||
if (t) {
|
||||
|
||||
const title = props.story.metadata && props.story.metadata.title;
|
||||
if (title) {
|
||||
return (
|
||||
<Bar
|
||||
multisite={props.settings ? props.settings.multisite : false}
|
||||
siteSelector={props.siteSelector}
|
||||
title={t}
|
||||
sectionSelector={props.sectionSelector}
|
||||
title={title}
|
||||
{...childProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Localized
|
||||
id="moderate-searchBar-titleNotAvailable"
|
||||
@@ -293,6 +335,7 @@ const ModerateSearchBarContainer: React.FunctionComponent<Props> = (props) => {
|
||||
>
|
||||
<Bar
|
||||
siteSelector={props.siteSelector}
|
||||
sectionSelector={props.sectionSelector}
|
||||
multisite={props.settings ? props.settings.multisite : false}
|
||||
title={"Title not available"}
|
||||
options={options}
|
||||
@@ -307,6 +350,7 @@ const enhanced = withRouter(
|
||||
settings: graphql`
|
||||
fragment ModerateSearchBarContainer_settings on Settings {
|
||||
multisite
|
||||
featureFlags
|
||||
}
|
||||
`,
|
||||
story: graphql`
|
||||
@@ -319,6 +363,7 @@ const enhanced = withRouter(
|
||||
metadata {
|
||||
title
|
||||
author
|
||||
section
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
@mixin outline;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
min-height: calc(4 * var(--mini-unit));
|
||||
padding: var(--mini-unit) calc(2.5 * var(--mini-unit));
|
||||
@@ -30,7 +31,7 @@
|
||||
}
|
||||
|
||||
.details {
|
||||
padding-top: 3px;
|
||||
padding-top: 8px;
|
||||
font-family: var(--v2-font-family-primary);
|
||||
font-weight: var(--v2-font-weight-primary-regular);
|
||||
font-size: var(--v2-font-size-2);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
.bold {
|
||||
font-weight: var(--v2-font-weight-primary-bold);
|
||||
}
|
||||
|
||||
.muted {
|
||||
font-weight: var(--v2-font-weight-primary-bold);
|
||||
color: var(--v2-colors-grey-400);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import cn from "classnames";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import styles from "./OptionDetail.css";
|
||||
|
||||
interface Props {
|
||||
variant?: "bold" | "muted";
|
||||
}
|
||||
|
||||
const OptionDetail: FunctionComponent<Props> = ({ children, variant }) => (
|
||||
<span
|
||||
className={cn({
|
||||
[styles.muted]: variant === "muted",
|
||||
[styles.bold]: variant === "bold",
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
export default OptionDetail;
|
||||
@@ -20,6 +20,10 @@ const SearchStoryFetch = createFetch(
|
||||
$limit: Int!
|
||||
$siteID: ID
|
||||
) {
|
||||
settings {
|
||||
multisite
|
||||
featureFlags
|
||||
}
|
||||
stories(query: $query, first: $limit, siteID: $siteID) {
|
||||
edges {
|
||||
node {
|
||||
@@ -30,6 +34,7 @@ const SearchStoryFetch = createFetch(
|
||||
}
|
||||
metadata {
|
||||
title
|
||||
section
|
||||
author
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { RouteProps } from "found";
|
||||
import React from "react";
|
||||
import { graphql, RelayPaginationProp } from "react-relay";
|
||||
|
||||
import parseModerationOptions from "coral-framework/helpers/parseModerationOptions";
|
||||
import { IntersectionProvider } from "coral-framework/lib/intersection";
|
||||
import { withPaginationContainer } from "coral-framework/lib/relay";
|
||||
import { resolveModule } from "coral-framework/lib/relay/helpers";
|
||||
@@ -19,6 +20,7 @@ interface ApprovedQueueRouteProps {
|
||||
relay: RelayPaginationProp;
|
||||
storyID?: string;
|
||||
siteID?: string;
|
||||
section?: string | null;
|
||||
}
|
||||
|
||||
// TODO: use generated types
|
||||
@@ -89,11 +91,13 @@ const enhanced = (withPaginationContainer<
|
||||
cursor: { type: "Cursor" }
|
||||
storyID: { type: "ID" }
|
||||
siteID: { type: "ID" }
|
||||
section: { type: "SectionFilter" }
|
||||
) {
|
||||
comments(
|
||||
status: APPROVED
|
||||
storyID: $storyID
|
||||
siteID: $siteID
|
||||
section: $section
|
||||
first: $count
|
||||
after: $cursor
|
||||
) @connection(key: "ApprovedQueue_comments") {
|
||||
@@ -135,6 +139,7 @@ const enhanced = (withPaginationContainer<
|
||||
query ApprovedQueueRoutePaginationQuery(
|
||||
$storyID: ID
|
||||
$siteID: ID
|
||||
$section: SectionFilter
|
||||
$count: Int!
|
||||
$cursor: Cursor
|
||||
) {
|
||||
@@ -142,6 +147,7 @@ const enhanced = (withPaginationContainer<
|
||||
@arguments(
|
||||
storyID: $storyID
|
||||
siteID: $siteID
|
||||
section: $section
|
||||
count: $count
|
||||
cursor: $cursor
|
||||
)
|
||||
@@ -153,18 +159,26 @@ const enhanced = (withPaginationContainer<
|
||||
enhanced.routeConfig = {
|
||||
Component: enhanced,
|
||||
query: resolveModule(graphql`
|
||||
query ApprovedQueueRouteQuery($storyID: ID, $siteID: ID) {
|
||||
...ApprovedQueueRoute_query @arguments(storyID: $storyID, siteID: $siteID)
|
||||
query ApprovedQueueRouteQuery(
|
||||
$storyID: ID
|
||||
$siteID: ID
|
||||
$section: SectionFilter
|
||||
) {
|
||||
...ApprovedQueueRoute_query
|
||||
@arguments(storyID: $storyID, siteID: $siteID, section: $section)
|
||||
}
|
||||
`),
|
||||
cacheConfig: { force: true },
|
||||
render: function RejectedRouteRender({ Component, props, match }) {
|
||||
if (Component && props) {
|
||||
const { storyID, siteID, section } = parseModerationOptions(match);
|
||||
|
||||
return (
|
||||
<Component
|
||||
query={props}
|
||||
storyID={match.params.storyID}
|
||||
siteID={match.params.siteID}
|
||||
storyID={storyID}
|
||||
siteID={siteID}
|
||||
section={section}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { graphql } from "react-relay";
|
||||
import { Environment, RecordSourceSelectorProxy } from "relay-runtime";
|
||||
|
||||
import { getQueueConnection } from "coral-admin/helpers";
|
||||
import { SectionFilter } from "coral-common/section";
|
||||
import {
|
||||
createSubscription,
|
||||
requestSubscription,
|
||||
@@ -14,7 +15,9 @@ import { QueueCommentEnteredSubscription } from "coral-admin/__generated__/Queue
|
||||
function handleCommentEnteredModerationQueue(
|
||||
store: RecordSourceSelectorProxy<unknown>,
|
||||
queue: GQLMODERATION_QUEUE_RL,
|
||||
storyID: string | null
|
||||
storyID: string | null,
|
||||
siteID: string | null,
|
||||
section?: SectionFilter | null
|
||||
) {
|
||||
const rootField = store.getRootField("commentEnteredModerationQueue");
|
||||
if (!rootField) {
|
||||
@@ -28,7 +31,7 @@ function handleCommentEnteredModerationQueue(
|
||||
);
|
||||
commentsEdge.setValue(comment.getValue("createdAt"), "cursor");
|
||||
commentsEdge.setLinkedRecord(comment, "node");
|
||||
const connection = getQueueConnection(store, queue, storyID);
|
||||
const connection = getQueueConnection(store, queue, storyID, siteID, section);
|
||||
if (connection) {
|
||||
const linked = connection.getLinkedRecords("viewNewEdges") || [];
|
||||
connection.setLinkedRecords(linked.concat(commentsEdge), "viewNewEdges");
|
||||
@@ -45,9 +48,16 @@ const QueueSubscription = createSubscription(
|
||||
subscription: graphql`
|
||||
subscription QueueCommentEnteredSubscription(
|
||||
$storyID: ID
|
||||
$siteID: ID
|
||||
$section: SectionFilter
|
||||
$queue: MODERATION_QUEUE!
|
||||
) {
|
||||
commentEnteredModerationQueue(storyID: $storyID, queue: $queue) {
|
||||
commentEnteredModerationQueue(
|
||||
storyID: $storyID
|
||||
siteID: $siteID
|
||||
section: $section
|
||||
queue: $queue
|
||||
) {
|
||||
comment {
|
||||
id
|
||||
createdAt
|
||||
@@ -61,7 +71,9 @@ const QueueSubscription = createSubscription(
|
||||
handleCommentEnteredModerationQueue(
|
||||
store,
|
||||
variables.queue,
|
||||
variables.storyID || null
|
||||
variables.storyID || null,
|
||||
variables.siteID || null,
|
||||
variables.section
|
||||
);
|
||||
},
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import { graphql } from "react-relay";
|
||||
import { Environment, RecordSourceSelectorProxy } from "relay-runtime";
|
||||
|
||||
import { getQueueConnection } from "coral-admin/helpers";
|
||||
import { SectionFilter } from "coral-common/section";
|
||||
import {
|
||||
createSubscription,
|
||||
requestSubscription,
|
||||
@@ -14,7 +15,9 @@ import { QueueCommentLeftSubscription } from "coral-admin/__generated__/QueueCom
|
||||
function handleCommentLeftModerationQueue(
|
||||
store: RecordSourceSelectorProxy<unknown>,
|
||||
queue: GQLMODERATION_QUEUE_RL,
|
||||
storyID: string | null
|
||||
storyID: string | null,
|
||||
siteID: string | null,
|
||||
section?: SectionFilter | null
|
||||
) {
|
||||
const rootField = store.getRootField("commentLeftModerationQueue");
|
||||
if (!rootField) {
|
||||
@@ -28,7 +31,7 @@ function handleCommentLeftModerationQueue(
|
||||
// Mark that the status of the comment was live updated.
|
||||
commentInStore.setValue(true, "statusLiveUpdated");
|
||||
}
|
||||
const connection = getQueueConnection(store, queue, storyID);
|
||||
const connection = getQueueConnection(store, queue, storyID, siteID, section);
|
||||
if (connection) {
|
||||
const linked = connection.getLinkedRecords("viewNewEdges") || [];
|
||||
connection.setLinkedRecords(
|
||||
@@ -50,9 +53,16 @@ const QueueSubscription = createSubscription(
|
||||
subscription: graphql`
|
||||
subscription QueueCommentLeftSubscription(
|
||||
$storyID: ID
|
||||
$siteID: ID
|
||||
$section: SectionFilter
|
||||
$queue: MODERATION_QUEUE!
|
||||
) {
|
||||
commentLeftModerationQueue(storyID: $storyID, queue: $queue) {
|
||||
commentLeftModerationQueue(
|
||||
storyID: $storyID
|
||||
siteID: $siteID
|
||||
section: $section
|
||||
queue: $queue
|
||||
) {
|
||||
comment {
|
||||
id
|
||||
status
|
||||
@@ -67,7 +77,9 @@ const QueueSubscription = createSubscription(
|
||||
handleCommentLeftModerationQueue(
|
||||
store,
|
||||
variables.queue,
|
||||
variables.storyID || null
|
||||
variables.storyID || null,
|
||||
variables.siteID || null,
|
||||
variables.section
|
||||
);
|
||||
},
|
||||
})
|
||||
|
||||
@@ -7,6 +7,8 @@ import React, {
|
||||
} from "react";
|
||||
import { graphql, GraphQLTaggedNode, RelayPaginationProp } from "react-relay";
|
||||
|
||||
import { SectionFilter } from "coral-common/section";
|
||||
import parseModerationOptions from "coral-framework/helpers/parseModerationOptions";
|
||||
import { IntersectionProvider } from "coral-framework/lib/intersection";
|
||||
import {
|
||||
combineDisposables,
|
||||
@@ -36,8 +38,9 @@ interface Props {
|
||||
settings: QueueRoute_settings | null;
|
||||
relay: RelayPaginationProp;
|
||||
emptyElement: React.ReactElement;
|
||||
storyID?: string;
|
||||
siteID?: string;
|
||||
storyID?: string | null;
|
||||
siteID?: string | null;
|
||||
section?: SectionFilter | null;
|
||||
count?: string;
|
||||
}
|
||||
|
||||
@@ -59,6 +62,7 @@ export const QueueRoute: FunctionComponent<Props> = (props) => {
|
||||
queue: props.queueName,
|
||||
storyID: props.storyID || null,
|
||||
siteID: props.siteID || null,
|
||||
section: props.section,
|
||||
});
|
||||
}, [props.queueName, props.storyID, props.siteID, viewNew]);
|
||||
useEffect(() => {
|
||||
@@ -66,6 +70,7 @@ export const QueueRoute: FunctionComponent<Props> = (props) => {
|
||||
queue: props.queueName,
|
||||
storyID: props.storyID || null,
|
||||
siteID: props.siteID || null,
|
||||
section: props.section,
|
||||
};
|
||||
const disposable = combineDisposables(
|
||||
subscribeToQueueCommentEntered(vars),
|
||||
@@ -77,6 +82,7 @@ export const QueueRoute: FunctionComponent<Props> = (props) => {
|
||||
}, [
|
||||
props.storyID,
|
||||
props.siteID,
|
||||
props.section,
|
||||
props.queueName,
|
||||
subscribeToQueueCommentEntered,
|
||||
subscribeToQueueCommentLeft,
|
||||
@@ -126,6 +132,9 @@ const createQueueRoute = (
|
||||
if (!Component) {
|
||||
throw new Error("Missing component");
|
||||
}
|
||||
|
||||
const { storyID, siteID, section } = parseModerationOptions(match);
|
||||
|
||||
if (!data || !data.moderationQueues) {
|
||||
return (
|
||||
<Component
|
||||
@@ -134,13 +143,15 @@ const createQueueRoute = (
|
||||
queue={null}
|
||||
settings={null}
|
||||
emptyElement={emptyElement}
|
||||
storyID={match.params.storyID}
|
||||
siteID={match.params.siteID}
|
||||
storyID={storyID}
|
||||
siteID={siteID}
|
||||
section={section}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const queue =
|
||||
data.moderationQueues[Object.keys(data.moderationQueues)[0]];
|
||||
|
||||
return (
|
||||
<Component
|
||||
isLoading={false}
|
||||
@@ -148,8 +159,9 @@ const createQueueRoute = (
|
||||
queue={queue}
|
||||
settings={data.settings}
|
||||
emptyElement={emptyElement}
|
||||
storyID={match.params.storyID}
|
||||
siteID={match.params.siteID}
|
||||
storyID={storyID}
|
||||
siteID={siteID}
|
||||
section={section}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -217,8 +229,13 @@ const createQueueRoute = (
|
||||
export const PendingQueueRoute = createQueueRoute(
|
||||
GQLMODERATION_QUEUE.PENDING,
|
||||
graphql`
|
||||
query QueueRoutePendingQuery($storyID: ID, $siteID: ID, $count: Int) {
|
||||
moderationQueues(storyID: $storyID, siteID: $siteID) {
|
||||
query QueueRoutePendingQuery(
|
||||
$storyID: ID
|
||||
$siteID: ID
|
||||
$count: Int
|
||||
$section: SectionFilter
|
||||
) {
|
||||
moderationQueues(storyID: $storyID, siteID: $siteID, section: $section) {
|
||||
pending {
|
||||
...QueueRoute_queue @arguments(count: $count)
|
||||
}
|
||||
@@ -234,10 +251,11 @@ export const PendingQueueRoute = createQueueRoute(
|
||||
query QueueRoutePaginationPendingQuery(
|
||||
$storyID: ID
|
||||
$siteID: ID
|
||||
$section: SectionFilter
|
||||
$count: Int!
|
||||
$cursor: Cursor
|
||||
) {
|
||||
moderationQueues(storyID: $storyID, siteID: $siteID) {
|
||||
moderationQueues(storyID: $storyID, siteID: $siteID, section: $section) {
|
||||
pending {
|
||||
...QueueRoute_queue @arguments(count: $count, cursor: $cursor)
|
||||
}
|
||||
@@ -255,8 +273,12 @@ export const PendingQueueRoute = createQueueRoute(
|
||||
export const ReportedQueueRoute = createQueueRoute(
|
||||
GQLMODERATION_QUEUE.REPORTED,
|
||||
graphql`
|
||||
query QueueRouteReportedQuery($storyID: ID, $siteID: ID) {
|
||||
moderationQueues(storyID: $storyID, siteID: $siteID) {
|
||||
query QueueRouteReportedQuery(
|
||||
$storyID: ID
|
||||
$siteID: ID
|
||||
$section: SectionFilter
|
||||
) {
|
||||
moderationQueues(storyID: $storyID, siteID: $siteID, section: $section) {
|
||||
reported {
|
||||
...QueueRoute_queue
|
||||
}
|
||||
@@ -272,10 +294,11 @@ export const ReportedQueueRoute = createQueueRoute(
|
||||
query QueueRoutePaginationReportedQuery(
|
||||
$storyID: ID
|
||||
$siteID: ID
|
||||
$section: SectionFilter
|
||||
$count: Int!
|
||||
$cursor: Cursor
|
||||
) {
|
||||
moderationQueues(storyID: $storyID, siteID: $siteID) {
|
||||
moderationQueues(storyID: $storyID, siteID: $siteID, section: $section) {
|
||||
reported {
|
||||
...QueueRoute_queue @arguments(count: $count, cursor: $cursor)
|
||||
}
|
||||
@@ -293,8 +316,12 @@ export const ReportedQueueRoute = createQueueRoute(
|
||||
export const UnmoderatedQueueRoute = createQueueRoute(
|
||||
GQLMODERATION_QUEUE.UNMODERATED,
|
||||
graphql`
|
||||
query QueueRouteUnmoderatedQuery($storyID: ID, $siteID: ID) {
|
||||
moderationQueues(storyID: $storyID, siteID: $siteID) {
|
||||
query QueueRouteUnmoderatedQuery(
|
||||
$storyID: ID
|
||||
$siteID: ID
|
||||
$section: SectionFilter
|
||||
) {
|
||||
moderationQueues(storyID: $storyID, siteID: $siteID, section: $section) {
|
||||
unmoderated {
|
||||
...QueueRoute_queue
|
||||
}
|
||||
@@ -310,10 +337,11 @@ export const UnmoderatedQueueRoute = createQueueRoute(
|
||||
query QueueRoutePaginationUnmoderatedQuery(
|
||||
$storyID: ID
|
||||
$siteID: ID
|
||||
$section: SectionFilter
|
||||
$count: Int!
|
||||
$cursor: Cursor
|
||||
) {
|
||||
moderationQueues(storyID: $storyID, siteID: $siteID) {
|
||||
moderationQueues(storyID: $storyID, siteID: $siteID, section: $section) {
|
||||
unmoderated {
|
||||
...QueueRoute_queue @arguments(count: $count, cursor: $cursor)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ConnectionHandler, Environment } from "relay-runtime";
|
||||
|
||||
import { getQueueConnection } from "coral-admin/helpers";
|
||||
import { SectionFilter } from "coral-common/section";
|
||||
import {
|
||||
commitLocalUpdatePromisified,
|
||||
createMutation,
|
||||
@@ -10,6 +11,7 @@ import { GQLMODERATION_QUEUE } from "coral-framework/schema";
|
||||
interface QueueViewNewInput {
|
||||
storyID: string | null;
|
||||
siteID: string | null;
|
||||
section?: SectionFilter | null;
|
||||
queue: GQLMODERATION_QUEUE;
|
||||
}
|
||||
|
||||
@@ -21,7 +23,8 @@ const QueueViewNewMutation = createMutation(
|
||||
store,
|
||||
input.queue,
|
||||
input.storyID,
|
||||
input.siteID
|
||||
input.siteID,
|
||||
input.section
|
||||
);
|
||||
if (!connection) {
|
||||
return;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { RouteProps } from "found";
|
||||
import React from "react";
|
||||
import { graphql, RelayPaginationProp } from "react-relay";
|
||||
|
||||
import parseModerationOptions from "coral-framework/helpers/parseModerationOptions";
|
||||
import { IntersectionProvider } from "coral-framework/lib/intersection";
|
||||
import { withPaginationContainer } from "coral-framework/lib/relay";
|
||||
import { resolveModule } from "coral-framework/lib/relay/helpers";
|
||||
@@ -19,6 +20,7 @@ interface RejectedQueueRouteProps {
|
||||
relay: RelayPaginationProp;
|
||||
storyID?: string;
|
||||
siteID?: string;
|
||||
section?: string | null;
|
||||
}
|
||||
|
||||
// TODO: use generated types
|
||||
@@ -89,11 +91,13 @@ const enhanced = (withPaginationContainer<
|
||||
cursor: { type: "Cursor" }
|
||||
storyID: { type: "ID" }
|
||||
siteID: { type: "ID" }
|
||||
section: { type: "SectionFilter" }
|
||||
) {
|
||||
comments(
|
||||
status: REJECTED
|
||||
storyID: $storyID
|
||||
siteID: $siteID
|
||||
section: $section
|
||||
first: $count
|
||||
after: $cursor
|
||||
) @connection(key: "RejectedQueue_comments") {
|
||||
@@ -135,6 +139,7 @@ const enhanced = (withPaginationContainer<
|
||||
query RejectedQueueRoutePaginationQuery(
|
||||
$storyID: ID
|
||||
$siteID: ID
|
||||
$section: SectionFilter
|
||||
$count: Int!
|
||||
$cursor: Cursor
|
||||
) {
|
||||
@@ -142,6 +147,7 @@ const enhanced = (withPaginationContainer<
|
||||
@arguments(
|
||||
storyID: $storyID
|
||||
siteID: $siteID
|
||||
section: $section
|
||||
count: $count
|
||||
cursor: $cursor
|
||||
)
|
||||
@@ -153,18 +159,26 @@ const enhanced = (withPaginationContainer<
|
||||
enhanced.routeConfig = {
|
||||
Component: enhanced,
|
||||
query: resolveModule(graphql`
|
||||
query RejectedQueueRouteQuery($storyID: ID, $siteID: ID) {
|
||||
...RejectedQueueRoute_query @arguments(storyID: $storyID, siteID: $siteID)
|
||||
query RejectedQueueRouteQuery(
|
||||
$storyID: ID
|
||||
$siteID: ID
|
||||
$section: SectionFilter
|
||||
) {
|
||||
...RejectedQueueRoute_query
|
||||
@arguments(storyID: $storyID, siteID: $siteID, section: $section)
|
||||
}
|
||||
`),
|
||||
cacheConfig: { force: true },
|
||||
render: function RejectedRouteRender({ Component, props, match }) {
|
||||
if (Component && props) {
|
||||
const { storyID, siteID, section } = parseModerationOptions(match);
|
||||
|
||||
return (
|
||||
<Component
|
||||
query={props}
|
||||
storyID={match.params.storyID}
|
||||
siteID={match.params.siteID}
|
||||
storyID={storyID}
|
||||
siteID={siteID}
|
||||
section={section}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
.button {
|
||||
height: calc(4 * var(--mini-unit));
|
||||
}
|
||||
|
||||
.buttonText {
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import PaginatedSelect from "coral-admin/components/PaginatedSelect";
|
||||
import { SectionFilter } from "coral-common/section";
|
||||
import { getModerationLink, QUEUE_NAME } from "coral-framework/helpers";
|
||||
import { Divider } from "coral-ui/components/v2/Dropdown";
|
||||
|
||||
import SectionSelectorSection from "./SectionSelectorSection";
|
||||
|
||||
import styles from "./SectionSelector.css";
|
||||
|
||||
interface Props {
|
||||
sections: ReadonlyArray<string>;
|
||||
section?: SectionFilter | null;
|
||||
queueName: QUEUE_NAME;
|
||||
}
|
||||
|
||||
const SelectedSection: FunctionComponent<{
|
||||
section?: SectionFilter | null;
|
||||
}> = ({ section }) => {
|
||||
if (!section) {
|
||||
return (
|
||||
<Localized id="moderate-section-selector-allSections">
|
||||
<span className={styles.buttonText}>All Sections</span>
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
|
||||
if (!section.name) {
|
||||
return (
|
||||
<Localized id="moderate-section-selector-uncategorized">
|
||||
<span className={styles.buttonText}>Uncategorized</span>
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
|
||||
return <span className={styles.buttonText}>{section.name}</span>;
|
||||
};
|
||||
|
||||
const SectionSelector: FunctionComponent<Props> = ({
|
||||
sections,
|
||||
section,
|
||||
queueName: queue,
|
||||
}) => {
|
||||
return (
|
||||
<PaginatedSelect
|
||||
disableLoadMore
|
||||
className={styles.button}
|
||||
selected={<SelectedSection section={section} />}
|
||||
>
|
||||
<SectionSelectorSection
|
||||
active={!section}
|
||||
link={getModerationLink({ queue })}
|
||||
/>
|
||||
<SectionSelectorSection
|
||||
section={{ name: null }}
|
||||
active={!!section && !section.name}
|
||||
link={getModerationLink({ queue, section: { name: null } })}
|
||||
/>
|
||||
{sections.length > 0 && <Divider />}
|
||||
{sections.map((name) => (
|
||||
<SectionSelectorSection
|
||||
key={name}
|
||||
section={{ name }}
|
||||
active={!!section && section.name === name}
|
||||
link={getModerationLink({ queue, section: { name } })}
|
||||
/>
|
||||
))}
|
||||
</PaginatedSelect>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionSelector;
|
||||
@@ -0,0 +1,54 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { SectionFilter } from "coral-common/section";
|
||||
import { QUEUE_NAME } from "coral-framework/helpers";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { GQLFEATURE_FLAG } from "coral-framework/schema";
|
||||
|
||||
import { SectionSelectorContainer_query } from "coral-admin/__generated__/SectionSelectorContainer_query.graphql";
|
||||
|
||||
import SectionSelector from "./SectionSelector";
|
||||
|
||||
interface Props {
|
||||
query: SectionSelectorContainer_query | null;
|
||||
section?: SectionFilter | null;
|
||||
queueName: string;
|
||||
}
|
||||
|
||||
const SectionSelectorContainer: FunctionComponent<Props> = ({
|
||||
query,
|
||||
section,
|
||||
queueName,
|
||||
}) => {
|
||||
// FEATURE_FLAG:SECTIONS
|
||||
if (
|
||||
!query ||
|
||||
!query.settings.featureFlags.includes(GQLFEATURE_FLAG.SECTIONS) ||
|
||||
!query.sections
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionSelector
|
||||
sections={query.sections}
|
||||
section={section}
|
||||
queueName={queueName as QUEUE_NAME}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
query: graphql`
|
||||
fragment SectionSelectorContainer_query on Query {
|
||||
sections
|
||||
settings {
|
||||
# FEATURE_FLAG:SECTIONS
|
||||
featureFlags
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(SectionSelectorContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,18 @@
|
||||
.root {
|
||||
font-family: var(--v2-font-family-primary);
|
||||
font-weight: var(--v2-font-weight-primary-regular);
|
||||
font-size: var(--v2-font-size-1);
|
||||
color: var(--v2-colors-mono-500);
|
||||
line-height: var(--v2-line-height-reset);
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
padding: var(--v2-spacing-2) var(--v2-spacing-4);
|
||||
}
|
||||
|
||||
.root:hover {
|
||||
background: var(--v2-colors-teal-100);
|
||||
}
|
||||
|
||||
.active {
|
||||
font-weight: var(--v2-font-weight-primary-bold);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import cn from "classnames";
|
||||
import { Link } from "found";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { SectionFilter } from "coral-common/section";
|
||||
|
||||
import styles from "./SectionSelectorSection.css";
|
||||
|
||||
interface Props {
|
||||
section?: SectionFilter;
|
||||
active?: boolean;
|
||||
link?: string;
|
||||
}
|
||||
|
||||
const SectionSelectorSection: FunctionComponent<Props> = ({
|
||||
section,
|
||||
link,
|
||||
active,
|
||||
}) => {
|
||||
return (
|
||||
<Link
|
||||
className={cn(styles.root, {
|
||||
[styles.active]: active,
|
||||
})}
|
||||
to={link || ""}
|
||||
>
|
||||
{!section && (
|
||||
<Localized id="moderate-section-selector-allSections">
|
||||
<span>All Sections</span>
|
||||
</Localized>
|
||||
)}
|
||||
{section && !section.name && (
|
||||
<Localized id="moderate-section-selector-uncategorized">
|
||||
<span>Uncategorized</span>
|
||||
</Localized>
|
||||
)}
|
||||
{section && section.name && <span>{section.name}</span>}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionSelectorSection;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as SectionSelectorContainer } from "./SectionSelectorContainer";
|
||||
@@ -9,6 +9,10 @@
|
||||
padding: var(--v2-spacing-2) var(--v2-spacing-4);
|
||||
}
|
||||
|
||||
.root:hover {
|
||||
background: var(--v2-colors-teal-100);
|
||||
}
|
||||
|
||||
.active {
|
||||
font-weight: var(--v2-font-weight-primary-bold);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,12 @@ exports[`renders correctly 1`] = `
|
||||
>
|
||||
<withRouter(Relay(ModerateSearchBarContainer))
|
||||
allStories={true}
|
||||
sectionSelector={
|
||||
<Relay(SectionSelectorContainer)
|
||||
query=""
|
||||
queueName=""
|
||||
/>
|
||||
}
|
||||
settings={
|
||||
Object {
|
||||
"multisite": false,
|
||||
|
||||
@@ -75,6 +75,7 @@ it("renders rejected queue with comments", async () => {
|
||||
status: "REJECTED",
|
||||
storyID: null,
|
||||
siteID: null,
|
||||
section: null,
|
||||
});
|
||||
return {
|
||||
edges: [
|
||||
@@ -114,6 +115,7 @@ it("shows a moderate story", async () => {
|
||||
status: "REJECTED",
|
||||
storyID: null,
|
||||
siteID: null,
|
||||
section: null,
|
||||
});
|
||||
return {
|
||||
edges: [
|
||||
@@ -158,6 +160,7 @@ it("renders rejected queue with comments and load more", async () => {
|
||||
status: GQLCOMMENT_STATUS.REJECTED,
|
||||
storyID: null,
|
||||
siteID: null,
|
||||
section: null,
|
||||
});
|
||||
return {
|
||||
edges: [
|
||||
@@ -182,6 +185,7 @@ it("renders rejected queue with comments and load more", async () => {
|
||||
status: GQLCOMMENT_STATUS.REJECTED,
|
||||
storyID: null,
|
||||
siteID: null,
|
||||
section: null,
|
||||
});
|
||||
return {
|
||||
edges: [
|
||||
@@ -275,6 +279,7 @@ it("approves comment in rejected queue", async () => {
|
||||
status: "REJECTED",
|
||||
storyID: null,
|
||||
siteID: null,
|
||||
section: null,
|
||||
});
|
||||
return {
|
||||
edges: [
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import urls from "./urls";
|
||||
|
||||
import { SectionFilter } from "coral-common/section";
|
||||
|
||||
export type QUEUE_NAME =
|
||||
| "reported"
|
||||
| "pending"
|
||||
@@ -7,16 +9,18 @@ export type QUEUE_NAME =
|
||||
| "rejected"
|
||||
| "approved";
|
||||
|
||||
interface Options {
|
||||
export interface Options {
|
||||
queue?: QUEUE_NAME;
|
||||
storyID?: string | null;
|
||||
siteID?: string | null;
|
||||
section?: SectionFilter | null;
|
||||
}
|
||||
|
||||
export default function getModerationLink({
|
||||
queue,
|
||||
storyID,
|
||||
siteID,
|
||||
section,
|
||||
}: Options = {}) {
|
||||
const parts = [urls.admin.moderate];
|
||||
|
||||
@@ -32,5 +36,16 @@ export default function getModerationLink({
|
||||
parts.push("sites", encodeURIComponent(siteID));
|
||||
}
|
||||
|
||||
return parts.join("/");
|
||||
const path = parts.join("/");
|
||||
|
||||
if (section) {
|
||||
const { name } = section;
|
||||
if (name) {
|
||||
return path + "?section=" + encodeURIComponent(name);
|
||||
}
|
||||
|
||||
return path + "?section=";
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Match } from "found";
|
||||
|
||||
import { Options } from "./getModerationLink";
|
||||
|
||||
export default function parseModerationOptions(
|
||||
match: Match
|
||||
): Omit<Options, "queue"> {
|
||||
const options: Options = {};
|
||||
|
||||
if (match.params.storyID) {
|
||||
options.storyID = match.params.storyID;
|
||||
}
|
||||
|
||||
if (match.params.siteID) {
|
||||
options.siteID = match.params.siteID;
|
||||
}
|
||||
|
||||
if (typeof match.location.query.section === "string") {
|
||||
const section = match.location.query.section;
|
||||
options.section = section
|
||||
? {
|
||||
name: section,
|
||||
}
|
||||
: {
|
||||
name: null,
|
||||
};
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
@@ -42,10 +42,13 @@ export interface ManagedSubscriptionClient {
|
||||
cacheConfig: CacheConfig,
|
||||
observer: any
|
||||
): Disposable;
|
||||
|
||||
/** Pauses all active subscriptions causing websocket connection to close. */
|
||||
pause(): void;
|
||||
|
||||
/** Resume all subscriptions eventually causing websocket to start with new connection parameters */
|
||||
resume(): void;
|
||||
|
||||
/** Sets access token and restarts the websocket connection */
|
||||
setAccessToken(accessToken?: string): void;
|
||||
}
|
||||
@@ -140,13 +143,28 @@ export default function createManagedSubscriptionClient(
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
// Register the request.
|
||||
requests.push(request as SubscriptionRequest);
|
||||
|
||||
// Start subscription if we are not paused.
|
||||
if (!paused) {
|
||||
request.subscribe();
|
||||
|
||||
// Debug subscriptions being logged. These should be kept here to help
|
||||
// with debugging subscriptions.
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
window.console.debug(
|
||||
`+1 [${requests.length - 1} + 1 = ${
|
||||
requests.length
|
||||
}] subscribe called for subscription:`,
|
||||
request.operation?.name,
|
||||
"with variables:",
|
||||
JSON.stringify(request.variables)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
dispose: () => {
|
||||
const i = requests.findIndex((r) => r === request);
|
||||
@@ -166,6 +184,19 @@ export default function createManagedSubscriptionClient(
|
||||
closeClient();
|
||||
}
|
||||
}
|
||||
|
||||
// Debug subscriptions being logged. These should be kept here to help
|
||||
// with debugging subscriptions.
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
window.console.debug(
|
||||
`-1 [${requests.length + 1} - 1 = ${
|
||||
requests.length
|
||||
}] dispose called for subscription:`,
|
||||
request.operation?.name,
|
||||
"with variables:",
|
||||
JSON.stringify(request.variables)
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ function createSubscriptionFunction(
|
||||
): SubscribeFunction {
|
||||
const fn: SubscribeFunction = (operation, variables, cacheConfig) => {
|
||||
return Observable.create<GraphQLResponse>((sink) => {
|
||||
subscriptionClient.subscribe(
|
||||
const subscription = subscriptionClient.subscribe(
|
||||
operation as any,
|
||||
variables,
|
||||
{
|
||||
@@ -36,6 +36,10 @@ function createSubscriptionFunction(
|
||||
onCompleted: sink.complete,
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
subscription.dispose();
|
||||
};
|
||||
});
|
||||
};
|
||||
return fn;
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
} from "relay-runtime";
|
||||
|
||||
import { CoralContext, useCoralContext, withContext } from "../bootstrap";
|
||||
import extractPayload from "./extractPayload";
|
||||
|
||||
export interface Fetch<N, V, R> {
|
||||
name: N;
|
||||
@@ -49,14 +48,8 @@ export async function fetchQuery<T extends { response: any }>(
|
||||
taggedNode: GraphQLTaggedNode,
|
||||
variables: Variables,
|
||||
cacheConfig?: CacheConfig
|
||||
): Promise<T["response"][keyof T["response"]]> {
|
||||
const result = await relayFetchQuery(
|
||||
environment,
|
||||
taggedNode,
|
||||
variables,
|
||||
cacheConfig
|
||||
);
|
||||
return extractPayload(result);
|
||||
): Promise<T["response"]> {
|
||||
return relayFetchQuery(environment, taggedNode, variables, cacheConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+14
-10
@@ -5,6 +5,7 @@ import { graphql, RelayPaginationProp } from "react-relay";
|
||||
import FadeInTransition from "coral-framework/components/FadeInTransition";
|
||||
import { useViewerNetworkEvent } from "coral-framework/lib/events";
|
||||
import {
|
||||
combineDisposables,
|
||||
useLoadMore,
|
||||
useLocal,
|
||||
useMutation,
|
||||
@@ -86,17 +87,20 @@ export const AllCommentsTabContainer: FunctionComponent<Props> = (props) => {
|
||||
// Only chronological sort supports top level live updates of incoming comments.
|
||||
return;
|
||||
}
|
||||
const newCommentDisposable = subscribeToCommentCreated({
|
||||
storyID: props.story.id,
|
||||
orderBy: commentsOrderBy,
|
||||
});
|
||||
const releasedCommentDisposable = subscribeToCommentReleased({
|
||||
storyID: props.story.id,
|
||||
orderBy: commentsOrderBy,
|
||||
});
|
||||
|
||||
const disposable = combineDisposables(
|
||||
subscribeToCommentCreated({
|
||||
storyID: props.story.id,
|
||||
orderBy: commentsOrderBy,
|
||||
}),
|
||||
subscribeToCommentReleased({
|
||||
storyID: props.story.id,
|
||||
orderBy: commentsOrderBy,
|
||||
})
|
||||
);
|
||||
|
||||
return () => {
|
||||
newCommentDisposable.dispose();
|
||||
releasedCommentDisposable.dispose();
|
||||
disposable.dispose();
|
||||
};
|
||||
}, [
|
||||
commentsOrderBy,
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface SectionFilter {
|
||||
/**
|
||||
* name when null or not provided indicates a filter for comments/stories
|
||||
* without a section. When name is provided as a string, it indicates a filter
|
||||
* for comments/stories with the specified section.
|
||||
*/
|
||||
name?: string | null;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import DataLoader from "dataloader";
|
||||
import { defaultTo, isNil, omitBy } from "lodash";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
import { SectionFilter } from "coral-common/section";
|
||||
import Context from "coral-server/graph/context";
|
||||
import { retrieveManyUserActionPresence } from "coral-server/models/action/comment";
|
||||
import {
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
import { retrieveSharedModerationQueueQueuesCounts } from "coral-server/models/comment/counts/shared";
|
||||
import { hasPublishedStatus } from "coral-server/models/comment/helpers";
|
||||
import { Connection } from "coral-server/models/helpers";
|
||||
import { hasFeatureFlag, Tenant } from "coral-server/models/tenant";
|
||||
import { User } from "coral-server/models/user";
|
||||
|
||||
import {
|
||||
@@ -28,6 +30,7 @@ import {
|
||||
CommentToRepliesArgs,
|
||||
GQLActionPresence,
|
||||
GQLCOMMENT_SORT,
|
||||
GQLFEATURE_FLAG,
|
||||
GQLTAG,
|
||||
GQLUSER_ROLE,
|
||||
QueryToCommentsArgs,
|
||||
@@ -39,6 +42,18 @@ import {
|
||||
|
||||
import { SingletonResolver } from "./util";
|
||||
|
||||
/**
|
||||
* requiredPropertyFilter will remove those properties that are nil from the
|
||||
* object as they are not nilable on the database model. If we didn't do this,
|
||||
* then any time that the property is nil, we'd be querying for comments that
|
||||
* can't possibly exist!
|
||||
*
|
||||
* @param props properties that if nil should be removed from the return object
|
||||
*/
|
||||
const requiredPropertyFilter = (
|
||||
props: CommentConnectionInput["filter"]
|
||||
): CommentConnectionInput["filter"] => omitBy(props, isNil);
|
||||
|
||||
const tagFilter = (tag?: GQLTAG): CommentConnectionInput["filter"] => {
|
||||
if (tag) {
|
||||
return {
|
||||
@@ -57,6 +72,22 @@ const queryFilter = (query?: string): CommentConnectionInput["filter"] => {
|
||||
return {};
|
||||
};
|
||||
|
||||
const sectionFilter = (
|
||||
tenant: Pick<Tenant, "featureFlags">,
|
||||
section?: SectionFilter
|
||||
): CommentConnectionInput["filter"] => {
|
||||
// Don't filter by section if the feature flag is disabled.
|
||||
if (!hasFeatureFlag(tenant, GQLFEATURE_FLAG.SECTIONS)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (section) {
|
||||
return { section: section.name || null };
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
/**
|
||||
* primeCommentsFromConnection will prime a given context with the comments
|
||||
* retrieved via a connection.
|
||||
@@ -139,6 +170,7 @@ export default (ctx: Context) => ({
|
||||
after,
|
||||
storyID,
|
||||
siteID,
|
||||
section,
|
||||
status,
|
||||
tag,
|
||||
query,
|
||||
@@ -147,16 +179,15 @@ export default (ctx: Context) => ({
|
||||
first: defaultTo(first, 10),
|
||||
after,
|
||||
orderBy: GQLCOMMENT_SORT.CREATED_AT_DESC,
|
||||
filter: omitBy(
|
||||
{
|
||||
...queryFilter(query),
|
||||
...tagFilter(tag),
|
||||
storyID,
|
||||
siteID,
|
||||
status,
|
||||
},
|
||||
isNil
|
||||
),
|
||||
filter: {
|
||||
...queryFilter(query),
|
||||
...tagFilter(tag),
|
||||
...sectionFilter(ctx.tenant, section),
|
||||
// If these properties are not provided or are null, remove them from
|
||||
// the filter because they do not exist in a nullable state on the
|
||||
// database model.
|
||||
...requiredPropertyFilter({ storyID, siteID, status }),
|
||||
},
|
||||
}).then(primeCommentsFromConnection(ctx)),
|
||||
retrieveMyActionPresence: new DataLoader<string, GQLActionPresence>(
|
||||
(commentIDs: string[]) => {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
findOrCreate,
|
||||
FindOrCreateStory,
|
||||
FindStory,
|
||||
retrieveSections,
|
||||
} from "coral-server/services/stories";
|
||||
import { scraper } from "coral-server/services/stories/scraper";
|
||||
|
||||
@@ -185,6 +186,7 @@ export default (ctx: GraphContext) => ({
|
||||
cache: !ctx.disableCaching,
|
||||
}
|
||||
),
|
||||
sections: () => retrieveSections(ctx.mongo, ctx.tenant),
|
||||
story: new DataLoader<string, Story | null>(
|
||||
(ids) => retrieveManyStories(ctx.mongo, ctx.tenant.id, ids),
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
import { FilterQuery } from "coral-server/models/helpers";
|
||||
import { Site } from "coral-server/models/site";
|
||||
import { Story } from "coral-server/models/story";
|
||||
import { hasFeatureFlag } from "coral-server/models/tenant";
|
||||
import {
|
||||
PENDING_STATUS,
|
||||
REPORTED_STATUS,
|
||||
@@ -12,7 +13,9 @@ import {
|
||||
} from "coral-server/services/comments/moderation/counts";
|
||||
|
||||
import {
|
||||
GQLFEATURE_FLAG,
|
||||
GQLModerationQueuesTypeResolver,
|
||||
GQLSectionFilter,
|
||||
QueryToModerationQueuesResolver,
|
||||
} from "coral-server/graph/schema/__generated__/types";
|
||||
|
||||
@@ -21,7 +24,7 @@ import { ModerationQueueInput } from "./ModerationQueue";
|
||||
|
||||
interface ModerationQueuesInput {
|
||||
connection: Partial<CommentConnectionInput>;
|
||||
counts: CommentModerationCountsPerQueue;
|
||||
counts?: CommentModerationCountsPerQueue;
|
||||
}
|
||||
|
||||
const mergeModerationInputFilters = (
|
||||
@@ -80,6 +83,25 @@ export const storyModerationInputResolver = (
|
||||
counts: story.commentCounts.moderationQueue.queues,
|
||||
});
|
||||
|
||||
/**
|
||||
* sectionModerationInputResolver can be used to retrieve the moderationQueue for
|
||||
* a specific Story.
|
||||
*
|
||||
* @param section the section that will be used to base the comment moderation
|
||||
* queues on
|
||||
*/
|
||||
export const sectionModerationInputResolver = async (
|
||||
section: GQLSectionFilter
|
||||
): Promise<ModerationQueuesInput> => ({
|
||||
connection: {
|
||||
filter: {
|
||||
// This moderationQueues is being sourced from the section, so require
|
||||
// that all the comments for theses queues are also for this section.
|
||||
section: section.name || null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* sharedModerationInputResolver implements the resolver function style which
|
||||
* allows it to be used in a type resolver.
|
||||
@@ -122,6 +144,10 @@ export const moderationQueuesResolver: QueryToModerationQueuesResolver = async (
|
||||
return storyModerationInputResolver(story);
|
||||
}
|
||||
|
||||
if (args.section && hasFeatureFlag(ctx.tenant, GQLFEATURE_FLAG.SECTIONS)) {
|
||||
return sectionModerationInputResolver(args.section);
|
||||
}
|
||||
|
||||
if (args.siteID) {
|
||||
const site = await ctx.loaders.Sites.site.load(args.siteID);
|
||||
if (!site) {
|
||||
|
||||
@@ -24,6 +24,7 @@ export const Query: Required<GQLQueryTypeResolver<void>> = {
|
||||
debugScrapeStoryMetadata: (source, { url }, ctx) =>
|
||||
ctx.loaders.Stories.debugScrapeMetadata.load(url),
|
||||
moderationQueues: moderationQueuesResolver,
|
||||
sections: (source, args, ctx) => ctx.loaders.Stories.sections(),
|
||||
activeStories: (source, { limit = 10 }, ctx) =>
|
||||
ctx.loaders.Stories.activeStories(limit),
|
||||
sites: (source, args, ctx) => ctx.loaders.Sites.connection(args),
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { hasFeatureFlag } from "coral-server/models/tenant";
|
||||
|
||||
import {
|
||||
GQLFEATURE_FLAG,
|
||||
GQLMODERATION_QUEUE,
|
||||
SubscriptionToCommentEnteredModerationQueueResolver,
|
||||
} from "coral-server/graph/schema/__generated__/types";
|
||||
@@ -15,6 +18,8 @@ export interface CommentEnteredModerationQueueInput
|
||||
queue: GQLMODERATION_QUEUE;
|
||||
commentID: string;
|
||||
storyID: string;
|
||||
siteID: string;
|
||||
section?: string;
|
||||
}
|
||||
|
||||
export type CommentEnteredModerationQueueSubscription = SubscriptionType<
|
||||
@@ -25,7 +30,7 @@ export type CommentEnteredModerationQueueSubscription = SubscriptionType<
|
||||
export const commentEnteredModerationQueue: SubscriptionToCommentEnteredModerationQueueResolver<CommentEnteredModerationQueueInput> = createIterator(
|
||||
SUBSCRIPTION_CHANNELS.COMMENT_ENTERED_MODERATION_QUEUE,
|
||||
{
|
||||
filter: (source, { storyID, queue }) => {
|
||||
filter: (source, { storyID, siteID, section, queue }, ctx) => {
|
||||
// If we're filtering by storyID, then only send back comments with the
|
||||
// specific storyID.
|
||||
if (storyID && source.storyID !== storyID) {
|
||||
@@ -38,6 +43,25 @@ export const commentEnteredModerationQueue: SubscriptionToCommentEnteredModerati
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we're filtering by siteID, then only send back comments from the
|
||||
// specific site.
|
||||
if (siteID && source.siteID !== siteID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we're filtering by section, then only send back comments from the
|
||||
// specific section. If the source has a section, if it's not equal to the
|
||||
// filter then return false. If the source does not have a section, then
|
||||
// the filter must also be null/undefined, otherwise return false.
|
||||
if (
|
||||
section &&
|
||||
((source.section && section.name !== source.section) ||
|
||||
(!source.section && section.name)) &&
|
||||
hasFeatureFlag(ctx.tenant, GQLFEATURE_FLAG.SECTIONS)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { hasFeatureFlag } from "coral-server/models/tenant";
|
||||
|
||||
import {
|
||||
GQLFEATURE_FLAG,
|
||||
GQLMODERATION_QUEUE,
|
||||
SubscriptionToCommentLeftModerationQueueResolver,
|
||||
} from "coral-server/graph/schema/__generated__/types";
|
||||
@@ -14,6 +17,8 @@ export interface CommentLeftModerationQueueInput extends SubscriptionPayload {
|
||||
queue: GQLMODERATION_QUEUE;
|
||||
commentID: string;
|
||||
storyID: string;
|
||||
siteID: string;
|
||||
section?: string;
|
||||
}
|
||||
|
||||
export type CommentLeftModerationQueueSubscription = SubscriptionType<
|
||||
@@ -24,7 +29,7 @@ export type CommentLeftModerationQueueSubscription = SubscriptionType<
|
||||
export const commentLeftModerationQueue: SubscriptionToCommentLeftModerationQueueResolver<CommentLeftModerationQueueInput> = createIterator(
|
||||
SUBSCRIPTION_CHANNELS.COMMENT_LEFT_MODERATION_QUEUE,
|
||||
{
|
||||
filter: (source, { storyID, queue }) => {
|
||||
filter: (source, { storyID, siteID, section, queue }, ctx) => {
|
||||
// If we're filtering by storyID, then only send back comments with the
|
||||
// specific storyID.
|
||||
if (storyID && source.storyID !== storyID) {
|
||||
@@ -37,6 +42,25 @@ export const commentLeftModerationQueue: SubscriptionToCommentLeftModerationQueu
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we're filtering by siteID, then only send back comments from the
|
||||
// specific site.
|
||||
if (siteID && source.siteID !== siteID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we're filtering by section, then only send back comments from the
|
||||
// specific section. If the source has a section, if it's not equal to the
|
||||
// filter then return false. If the source does not have a section, then
|
||||
// the filter must also be null/undefined, otherwise return false.
|
||||
if (
|
||||
section &&
|
||||
((source.section && section.name !== source.section) ||
|
||||
(!source.section && section.name)) &&
|
||||
hasFeatureFlag(ctx.tenant, GQLFEATURE_FLAG.SECTIONS)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
}
|
||||
|
||||
@@ -91,6 +91,21 @@ Locale represents a language code in the BCP 47 format.
|
||||
"""
|
||||
scalar Locale
|
||||
|
||||
################################################################################
|
||||
## Custom Input Types
|
||||
################################################################################
|
||||
|
||||
input SectionFilter {
|
||||
"""
|
||||
name when provided will filter only those comments/stories that have this
|
||||
section. When name is not provided or is null, only comments/stories without
|
||||
a section (uncategorized) will be returned. In order to return all
|
||||
comments/stories regardless of section, specify null or undefined for this
|
||||
filter option.
|
||||
"""
|
||||
name: String
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## Actions
|
||||
################################################################################
|
||||
@@ -343,6 +358,12 @@ type ActionPresence {
|
||||
################################################################################
|
||||
|
||||
enum FEATURE_FLAG {
|
||||
"""
|
||||
SECTIONS when enabled will allow filtering comments in the moderation queue
|
||||
by section.
|
||||
"""
|
||||
SECTIONS
|
||||
|
||||
"""
|
||||
DISABLE_WARN_USER_OF_TOXIC_COMMENT when enabled will turn off warnings for
|
||||
toxic comments.
|
||||
@@ -3150,6 +3171,7 @@ type Query {
|
||||
storyID: ID
|
||||
siteID: ID
|
||||
status: COMMENT_STATUS
|
||||
section: SectionFilter
|
||||
tag: TAG
|
||||
query: String
|
||||
): CommentsConnection! @auth(roles: [ADMIN, MODERATOR])
|
||||
@@ -3236,8 +3258,16 @@ type Query {
|
||||
moderationQueues returns the set of ModerationQueues that are available for
|
||||
all stories or if given the story identified by the `storyID`.
|
||||
"""
|
||||
moderationQueues(storyID: ID, siteID: ID): ModerationQueues!
|
||||
@auth(roles: [ADMIN, MODERATOR])
|
||||
moderationQueues(
|
||||
storyID: ID
|
||||
siteID: ID
|
||||
section: SectionFilter
|
||||
): ModerationQueues! @auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
sections will return the unique sections used by this Tenant.
|
||||
"""
|
||||
sections: [String!] @auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
activeStories is the list of most recently commented on stories identified
|
||||
@@ -4888,7 +4918,11 @@ type ApproveCommentPayload {
|
||||
is provided, it will filter the moderation queues for only comments in that
|
||||
Story.
|
||||
"""
|
||||
moderationQueues(storyID: ID): ModerationQueues
|
||||
moderationQueues(
|
||||
storyID: ID
|
||||
siteID: ID
|
||||
section: SectionFilter
|
||||
): ModerationQueues
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
@@ -4928,7 +4962,11 @@ type RejectCommentPayload {
|
||||
is provided, it will filter the moderation queues for only comments in that
|
||||
Story.
|
||||
"""
|
||||
moderationQueues(storyID: ID): ModerationQueues
|
||||
moderationQueues(
|
||||
storyID: ID
|
||||
siteID: ID
|
||||
section: SectionFilter
|
||||
): ModerationQueues
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
@@ -7281,6 +7319,8 @@ type Subscription {
|
||||
"""
|
||||
commentEnteredModerationQueue(
|
||||
storyID: ID
|
||||
siteID: ID
|
||||
section: SectionFilter
|
||||
queue: MODERATION_QUEUE
|
||||
): CommentEnteredModerationQueuePayload! @auth(roles: [MODERATOR, ADMIN])
|
||||
|
||||
@@ -7290,6 +7330,8 @@ type Subscription {
|
||||
"""
|
||||
commentLeftModerationQueue(
|
||||
storyID: ID
|
||||
siteID: ID
|
||||
section: SectionFilter
|
||||
queue: MODERATION_QUEUE
|
||||
): CommentLeftModerationQueuePayload! @auth(roles: [MODERATOR, ADMIN])
|
||||
|
||||
|
||||
@@ -80,6 +80,13 @@ export interface Comment extends TenantResource {
|
||||
*/
|
||||
siteID: string;
|
||||
|
||||
/**
|
||||
* section is the section of the story that this comment was left on. If the
|
||||
* section was not available when the comment was authored, the section will
|
||||
* be null here.
|
||||
*/
|
||||
section?: string;
|
||||
|
||||
/**
|
||||
* revisions stores all the revisions of the Comment body including the most
|
||||
* recent revision, the last revision is the most recent.
|
||||
|
||||
@@ -661,3 +661,23 @@ export async function setStoryMode(
|
||||
|
||||
return result.value || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieveStorySections will return the sections used by stories in the
|
||||
* database for a given Tenant sorted alphabetically.
|
||||
*
|
||||
* @param mongo the database connection to use to retrieve the data
|
||||
* @param tenantID the ID of the Tenant that we're retrieving data
|
||||
*/
|
||||
export async function retrieveStorySections(
|
||||
mongo: Db,
|
||||
tenantID: string
|
||||
): Promise<string[]> {
|
||||
const results: Array<string | null> = await collection(
|
||||
mongo
|
||||
).distinct("metadata.section", { tenantID });
|
||||
|
||||
// We perform the type assertion here because we know that after filtering out
|
||||
// the null entries, the resulting array can not contain null.
|
||||
return results.filter((section) => section !== null).sort() as string[];
|
||||
}
|
||||
|
||||
@@ -90,19 +90,23 @@ export async function publishCommentFeatured(
|
||||
export async function publishModerationQueueChanges(
|
||||
broker: CoralEventPublisherBroker,
|
||||
moderationQueue: Pick<CommentModerationQueueCounts, "queues">,
|
||||
comment: Pick<Comment, "id" | "storyID">
|
||||
comment: Pick<Comment, "id" | "storyID" | "siteID" | "section">
|
||||
) {
|
||||
if (moderationQueue.queues.pending === 1) {
|
||||
await CommentEnteredModerationQueueCoralEvent.publish(broker, {
|
||||
queue: GQLMODERATION_QUEUE.PENDING,
|
||||
commentID: comment.id,
|
||||
storyID: comment.storyID,
|
||||
siteID: comment.siteID,
|
||||
section: comment.section,
|
||||
});
|
||||
} else if (moderationQueue.queues.pending === -1) {
|
||||
await CommentLeftModerationQueueCoralEvent.publish(broker, {
|
||||
queue: GQLMODERATION_QUEUE.PENDING,
|
||||
commentID: comment.id,
|
||||
storyID: comment.storyID,
|
||||
siteID: comment.siteID,
|
||||
section: comment.section,
|
||||
});
|
||||
}
|
||||
if (moderationQueue.queues.reported === 1) {
|
||||
@@ -110,12 +114,16 @@ export async function publishModerationQueueChanges(
|
||||
queue: GQLMODERATION_QUEUE.REPORTED,
|
||||
commentID: comment.id,
|
||||
storyID: comment.storyID,
|
||||
siteID: comment.siteID,
|
||||
section: comment.section,
|
||||
});
|
||||
} else if (moderationQueue.queues.reported === -1) {
|
||||
await CommentLeftModerationQueueCoralEvent.publish(broker, {
|
||||
queue: GQLMODERATION_QUEUE.REPORTED,
|
||||
commentID: comment.id,
|
||||
storyID: comment.storyID,
|
||||
siteID: comment.siteID,
|
||||
section: comment.section,
|
||||
});
|
||||
}
|
||||
if (moderationQueue.queues.unmoderated === 1) {
|
||||
@@ -123,12 +131,16 @@ export async function publishModerationQueueChanges(
|
||||
queue: GQLMODERATION_QUEUE.UNMODERATED,
|
||||
commentID: comment.id,
|
||||
storyID: comment.storyID,
|
||||
siteID: comment.siteID,
|
||||
section: comment.section,
|
||||
});
|
||||
} else if (moderationQueue.queues.unmoderated === -1) {
|
||||
await CommentLeftModerationQueueCoralEvent.publish(broker, {
|
||||
queue: GQLMODERATION_QUEUE.UNMODERATED,
|
||||
commentID: comment.id,
|
||||
storyID: comment.storyID,
|
||||
siteID: comment.siteID,
|
||||
section: comment.section,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
removeStory,
|
||||
retrieveManyStories,
|
||||
retrieveStory,
|
||||
retrieveStorySections,
|
||||
setStoryMode,
|
||||
Story,
|
||||
updateStory,
|
||||
@@ -42,13 +43,16 @@ import {
|
||||
updateStorySettings,
|
||||
UpdateStorySettingsInput,
|
||||
} from "coral-server/models/story";
|
||||
import { Tenant } from "coral-server/models/tenant";
|
||||
import { hasFeatureFlag, Tenant } from "coral-server/models/tenant";
|
||||
import { retrieveUser } from "coral-server/models/user";
|
||||
import { ScraperQueue } from "coral-server/queue/tasks/scraper";
|
||||
import { findSiteByURL } from "coral-server/services/sites";
|
||||
import { scrape } from "coral-server/services/stories/scraper";
|
||||
|
||||
import { GQLSTORY_MODE } from "coral-server/graph/schema/__generated__/types";
|
||||
import {
|
||||
GQLFEATURE_FLAG,
|
||||
GQLSTORY_MODE,
|
||||
} from "coral-server/graph/schema/__generated__/types";
|
||||
|
||||
export type FindStory = FindStoryInput;
|
||||
|
||||
@@ -412,3 +416,11 @@ export async function updateStoryMode(
|
||||
) {
|
||||
return setStoryMode(mongo, tenant.id, storyID, mode);
|
||||
}
|
||||
|
||||
export async function retrieveSections(mongo: Db, tenant: Tenant) {
|
||||
if (!hasFeatureFlag(tenant, GQLFEATURE_FLAG.SECTIONS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return retrieveStorySections(mongo, tenant.id);
|
||||
}
|
||||
|
||||
@@ -222,6 +222,8 @@ export default async function create(
|
||||
{
|
||||
...input,
|
||||
siteID: story.siteID,
|
||||
// Copy the current story section into the comment if it exists.
|
||||
section: story.metadata?.section,
|
||||
// Remap the tags to include the createdAt.
|
||||
tags: tags.map((tag) => ({ type: tag, createdAt: now })),
|
||||
body,
|
||||
|
||||
@@ -475,6 +475,12 @@ stories-column-site = Site
|
||||
site-table-siteName = Site name
|
||||
stories-filter-sites = Site
|
||||
|
||||
### Sections
|
||||
|
||||
moderate-section-selector-allSections = All Sections
|
||||
moderate-section-selector-uncategorized = Uncategorized
|
||||
moderate-section-uncategorized = Uncategorized
|
||||
|
||||
### Email
|
||||
|
||||
configure-email = Email settings
|
||||
|
||||
Reference in New Issue
Block a user