[CORL-381] Featured Comments (#2335)

* feat: initial serverside featuring support

* Update schema.graphql

* feat: add feature comment to moderation dropdown

* feat: feature comments on the stream embed

* fix: tests

* fix: optimize loading and fix tests

* feat: hide featured tab when empty

* feat: introduced flattening

* fix: snapshots

* fix: spacing

* feat: added a dark variant to popover

* feat: add featured comments tooltip

* fix: better tests

* feat: added tag counts

* chore: changed string to enum

* fix: removed unused translation

* fix: changed schema for String -> TAG

* feat: split comments -> comments, featuredComments

* fix: adapt client to new endpoints

* feat: use featured comment counts

* test: featured count handling

* fix: snapshots and optimistically approve comment during feature

* fix: remove unnecessary assertion

* feat: approve featured comments

* fix: make optimistic update less reliant on existing data
This commit is contained in:
Wyatt Johnson
2019-06-14 16:27:25 +00:00
committed by GitHub
parent f8cf34e34d
commit 9d1f03115f
213 changed files with 7744 additions and 5387 deletions
@@ -25,6 +25,9 @@ const matcher = (pattern: TextMatchPattern, options?: TextMatchOptions) => (
if (typeof c === "string" && matchText(pattern, c, options)) {
return true;
}
if (typeof c === "number" && matchText(pattern, c.toString(), options)) {
return true;
}
}
return false;
};
@@ -1,4 +1,10 @@
import { GQLComment, GQLCommentEdge, GQLStory } from "coral-framework/schema";
import {
GQLComment,
GQLCommentEdge,
GQLStory,
GQLTAG,
GQLTag,
} from "coral-framework/schema";
import createFixture, { Fixture } from "./createFixture";
@@ -52,11 +58,19 @@ export function denormalizeStory(story: Fixture<GQLStory>) {
endCursor: null,
hasNextPage: false,
};
const featuredCommentsCount = commentNodes.filter(
n => n.tags && n.tags.some((t: GQLTag) => t.code === GQLTAG.FEATURED)
).length;
return createFixture<GQLStory>({
...story,
comments: { edges: commentNodes, pageInfo: commentsPageInfo },
commentCounts: {
...story.commentCounts,
totalVisible: commentNodes.length,
tags: {
...(story.commentCounts && story.commentCounts.tags),
FEATURED: featuredCommentsCount,
},
},
});
}
@@ -41,3 +41,7 @@ export {
TransitionControlData,
default as TransitionControl,
} from "./TransitionControl";
export {
default as overwriteQueryResolver,
createQueryResolverOverwrite,
} from "./overwriteQueryResolver";
@@ -0,0 +1,116 @@
import createQueryResolverStub, {
QueryResolverCallback,
} from "./createQueryResolverStub";
import { Resolver, Resolvers, TestResolvers } from "./createTestRenderer";
type ValueOrCallbackRecursive<T> = {
[P in keyof T]?: ValueOrCallbackRecursive<T[P]> | (() => void)
};
type ResolverResult<T extends Resolver<any, any>> = T extends Resolver<
any,
infer R
>
? R
: never;
type OverwriteQueryResolverTemplate<T extends Resolvers = any> = {
[P in keyof Required<T>["Query"]]: ValueOrCallbackRecursive<
ResolverResult<Required<T>["Query"][P]>
>
};
/**
* overwriteRecursive will loop over the original resolver object
* and creates a resolver function of each of its field. The returned
* value of the resolvers will bet the value returned by the overwrite
* or if undefined, the original value.
*
* @param original original resolver object
* @param overwrite overwrite resolver object or value
*/
function overwriteRecursive(original: any, overwrite: any) {
let ret = original;
Object.keys(overwrite).forEach(k => {
ret = {
...original,
[k]: (...args: any[]) => {
// Only resolve original resolver when needed.
const resolve = () =>
typeof original[k] === "function"
? original[k](...args)
: original[k];
// There is an overwrite defined!
if (overwrite[k]) {
// It's a resolver function, we reached a leaf.
if (typeof overwrite[k] === "function") {
// Resolve overwrite resolver and return it's value
// or if undefined the original resolved value.
const result = (overwrite as any)[k](...args);
return result !== undefined ? result : resolve();
}
// The overwrite is an Object, so we will recurse into it,
// until we find a leaf.
if (overwrite[k] instanceof Object && !Array.isArray(overwrite[k])) {
return overwriteRecursive(resolve(), overwrite[k]);
}
// The overwrite is a fixed value, we use that.
return overwrite[k];
}
// No overwrite defined, just resolve and return.
return resolve();
},
};
});
return ret;
}
/**
* `createQueryResolverOverwrite` is like `createQueryResolverStub`
* but allows you to return `void` which would then fallback to the original resolver.
*
* Given a `ResolverType` from the Schema it'll provide types as well!.
* @param callback resolver callback
*/
export function createQueryResolverOverwrite<T extends Resolver<any, any>>(
callback: QueryResolverCallback<Resolver<any, ResolverResult<T> | void>>
) {
return createQueryResolverStub(callback);
}
/**
* overwriteQueryResolver allows you to conveniently overwrite the Query resolvers,
* even if you just want to overwrite a deeply nested field.
*
* When your overwrite resolver returns no value, it will fallback to the
* original resolver.
*
* Example usage:
* ```ts
* {
* resolver: overwriteQueryResolver<GQLResolver>(params.resolvers || {}, {
* story: {
* comments: createQueryResolverOverwrite<StoryToCommentsResolver>(
* ({ variables }) => {
* if (variables.tag === "FEATURED") {
* return storyWithNoComments.comments;
* }
* return;
* }
* ),
* },
* }),
* }
* ```
*/
export default function overwriteQueryResolver<T extends Resolvers<any, any>>(
original: TestResolvers<any>,
overwriteQuery: OverwriteQueryResolverTemplate<T>
): TestResolvers<any> {
return {
...original,
Query: overwriteRecursive(original.Query || {}, overwriteQuery),
};
}