mirror of
https://github.com/wassname/talk.git
synced 2026-09-10 12:43:11 +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 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])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user