[CORL-155] User Suspending and Banning (#2247)

* feat: suspending, banning, now propogation

* feat: adapting to `now`

* feat: support auth for suspension/banned

* feat: added trace-id to requests

* feat: new mutation api with hooks support

* feat: added user status filtering, current field

* feat: Implement filter by status, adapt to new USER_STATUS type, add lookup helper <3

* fix: typo

* fix: tests

* chore: rename banned status to ban status

* test: feature test + lots of test helper improvements e.g. types

* fix: add translation to ban user modal

* fix: translation

* fix: test
This commit is contained in:
Wyatt Johnson
2019-04-22 22:57:32 +00:00
committed by GitHub
parent b63c00f26f
commit dbbc1af42e
147 changed files with 4609 additions and 1468 deletions
@@ -14,6 +14,14 @@ interface Props {
localeBundles: TalkContext["localeBundles"];
}
interface InjectedProps {
getMessage: GetMessage;
}
interface Props {
localeBundles: TalkContext["localeBundles"];
}
/**
* withGetMessage provides a property `getMessage: (id: string) => string`
* that'll provide a translated string associated with `id`.
@@ -22,13 +30,17 @@ const withGetMessage: DefaultingInferableComponentEnhancer<
InjectedProps
> = hoistStatics<InjectedProps>(
<T extends InjectedProps>(BaseComponent: React.ComponentType<T>) => {
// TODO: (cvle) This is a workaround for a typescript bug
// https://github.com/Microsoft/TypeScript/issues/30762
const Workaround = BaseComponent as React.ComponentType<InjectedProps>;
class WithGetMessage extends React.Component<Props> {
private getMessage = (id: string, defaultTo?: string) => {
return getMessage(this.props.localeBundles, id, defaultTo);
};
public render() {
const { localeBundles: _, ...rest } = this.props;
return <BaseComponent {...rest} getMessage={this.getMessage} />;
return <Workaround {...rest} getMessage={this.getMessage} />;
}
}
@@ -25,6 +25,10 @@ const withInView: DefaultingInferableComponentEnhancer<
InjectedProps
> = hoistStatics<InjectedProps>(
<T extends InjectedProps>(BaseComponent: React.ComponentType<T>) => {
// TODO: (cvle) This is a workaround for a typescript bug
// https://github.com/Microsoft/TypeScript/issues/30762
const Workaround = BaseComponent as React.ComponentType<InjectedProps>;
class WithInView extends React.Component<Props, State> {
private unobserve: (() => void) | null = null;
@@ -62,7 +66,7 @@ const withInView: DefaultingInferableComponentEnhancer<
public render() {
return (
<BaseComponent
<Workaround
{...this.props}
inView={this.state.inView}
intersectionRef={this.changeRef}
@@ -1,7 +1,7 @@
import { commitMutation } from "react-relay";
import { Environment, MutationConfig, OperationBase } from "relay-runtime";
import { Omit } from "talk-framework/types";
import { DeepPartial, Omit } from "talk-framework/types";
import extractPayload from "./extractPayload";
@@ -11,8 +11,8 @@ import extractPayload from "./extractPayload";
*/
export type MutationPromiseConfig<T extends OperationBase> = Omit<
MutationConfig<T>,
"onCompleted" | "onError"
>;
"onCompleted" | "onError" | "optimisticResponse"
> & { optimisticResponse?: DeepPartial<T["response"]> };
/**
* Normalizes response and error from `commitMutationPromise`.
@@ -6,24 +6,9 @@ import {
wrapDisplayName,
} from "recompose";
import { Environment } from "relay-runtime";
import { Omit } from "talk-framework/types";
import { TalkContext, withContext } from "../bootstrap";
export type MutationInput<
T extends { variables: { input: { clientMutationId: string } } }
> = Omit<T["variables"]["input"], "clientMutationId">;
export type MutationResponse<
T extends { response: { [P in U]: any } },
U extends string
> = Exclude<T["response"][U], null>;
export type MutationResponsePromise<
T extends { response: { [P in U]: any } },
U extends string
> = Promise<MutationResponse<T, U>>;
/**
* createMutationContainer creates a HOC that
* injects a property with the name specified in `propName`
+7 -2
View File
@@ -5,12 +5,16 @@ export { default as withLocalStateContainer } from "./withLocalStateContainer";
export * from "./withLocalStateContainer";
export { default as QueryRenderer } from "./QueryRenderer";
export * from "./QueryRenderer";
export { default as createMutationContainer } from "./createMutationContainer";
export {
default as createMutationContainer,
createMutation,
useMutation,
withMutation,
MutationInput,
MutationResponse,
MutationResponsePromise,
} from "./createMutationContainer";
MutationProp,
} from "./mutation";
export { default as createFetchContainer } from "./createFetchContainer";
export { default as createAndRetain } from "./createAndRetain";
export { default as wrapFetchWithLogger } from "./wrapFetchWithLogger";
@@ -26,3 +30,4 @@ export { initLocalBaseState, setAccessTokenInLocalState } from "./localState";
export { default as fetchQuery } from "./fetchQuery";
export { default as useRefetch } from "./useRefetch";
export { default as useLoadMore } from "./useLoadMore";
export { default as lookup } from "./lookup";
@@ -0,0 +1,49 @@
import { Environment, RelayInMemoryRecordSource } from "relay-runtime";
/**
* RecordSourceProxy has the same shape as the underlying Schema Type, but
* makes all fields optional and readonly.
*/
type RecordSourceProxy<T> = T extends object
? {
readonly [P in keyof T]?: T[P] extends Array<infer U>
? ReadonlyArray<RecordSourceProxy<U>>
: T[P] extends ReadonlyArray<infer V>
? ReadonlyArray<RecordSourceProxy<V>>
: RecordSourceProxy<T[P]>
}
: T;
/**
* createProxy returns a proxy for `recordSource`, that automatically
* resolves references to other record sources.
*/
const createProxy = <T = any>(
environment: Environment,
recordSource: RelayInMemoryRecordSource
) => {
const proxy: ProxyHandler<any> = {
get(_, prop) {
if ((recordSource as any)[prop].__ref) {
return lookup(environment, (recordSource as any)[prop].__ref);
}
return (recordSource as any)[prop];
},
};
return new Proxy({}, proxy) as RecordSourceProxy<T>;
};
/**
* Lookup the Relay Cache with given object id. Returns a `RecordSourceProxy``
* for easy traversing through the Relay Cache.
*/
export default function lookup<T = any>(environment: Environment, id: string) {
const recordSource = environment
.getStore()
.getSource()
.get(id);
if (!recordSource) {
return null;
}
return createProxy<T>(environment, recordSource);
}
@@ -0,0 +1,118 @@
import React, { useCallback } from "react";
import {
compose,
hoistStatics,
InferableComponentEnhancer,
wrapDisplayName,
} from "recompose";
import { Environment } from "relay-runtime";
import { Omit } from "talk-framework/types";
import { TalkContext, useTalkContext, withContext } from "../bootstrap";
export interface Mutation<N, I, R> {
name: N;
commit: (environment: Environment, input: I, context: TalkContext) => R;
}
export type MutationInput<
T extends { variables: { input: { clientMutationId: string } } }
> = Omit<T["variables"]["input"], "clientMutationId">;
export type MutationResponse<
T extends { response: { [P in U]: any } },
U extends string | number | symbol
> = Exclude<T["response"][U], null>;
export type MutationResponsePromise<
T extends { response: { [P in U]: any } },
U extends string | number | symbol
> = Promise<MutationResponse<T, U>>;
export type MutationProp<
T extends Mutation<any, any, any>
> = T extends Mutation<any, infer I, infer R>
? Parameters<T["commit"]>[1] extends undefined
? () => R
: keyof Parameters<T["commit"]>[1] extends never ? () => R : (input: I) => R
: never;
type RemoveClientMutationID<T> = T extends Promise<infer U>
? Promise<
U extends { clientMutationId: any } ? Omit<U, "clientMutationId"> : U
>
: T extends { clientMutationId: any } ? Omit<T, "clientMutationId"> : T;
export function createMutation<N extends string, I, R>(
name: N,
commit: (environment: Environment, input: I, context: TalkContext) => R
// (cvle) We remove `clientMutationId` from the response, so we don't use it inside our app.
// It is a Relay implementation detail that is pending for removal.
// https://github.com/facebook/relay/pull/2349
): Mutation<N, I, RemoveClientMutationID<R>> {
return {
name,
commit,
} as any;
}
/**
* useMutation is a React Hook that
* returns a callback to call the mutation.
*/
export function useMutation<I, R>(
mutation: Mutation<any, I, R>
): MutationProp<typeof mutation> {
const context = useTalkContext();
return useCallback<MutationProp<typeof mutation>>(
((input: I) => {
context.eventEmitter.emit(`mutation.${mutation.name}`, input);
return mutation.commit(context.relayEnvironment, input, context);
}) as any,
[context]
);
}
/**
* withMutation creates a HOC that injects the mutation as
* a property.
*/
export function withMutation<N extends string, I, R>(
mutation: Mutation<N, I, R>
): InferableComponentEnhancer<{ [P in N]: MutationProp<typeof mutation> }> {
return compose(
withContext(context => ({ context })),
hoistStatics((BaseComponent: React.ComponentType<any>) => {
class WithMutation extends React.Component<{
context: TalkContext;
}> {
public static displayName = wrapDisplayName(
BaseComponent,
"withMutation"
);
private commit = (input: I) => {
this.props.context.eventEmitter.emit(
`mutation.${mutation.name}`,
input
);
return mutation.commit(
this.props.context.relayEnvironment,
input,
this.props.context
);
};
public render() {
const { context: _, ...rest } = this.props;
const inject = {
[mutation.name]: this.commit,
};
return <BaseComponent {...rest} {...inject} />;
}
}
return WithMutation as React.ComponentType<any>;
})
);
}
@@ -15,9 +15,11 @@ import {
GQLSTORY_STATUS,
GQLUSER_AUTH_CONDITIONS,
GQLUSER_ROLE,
GQLUSER_STATUS,
} from "./__generated__/types";
export type GQLUSER_ROLE_RL = RelayEnumLiteral<typeof GQLUSER_ROLE>;
export type GQLUSER_STATUS_RL = RelayEnumLiteral<typeof GQLUSER_STATUS>;
export type GQLCOMMENT_FLAG_DETECTED_REASON_RL = RelayEnumLiteral<
typeof GQLCOMMENT_FLAG_DETECTED_REASON
>;
@@ -0,0 +1,34 @@
import { merge } from "lodash";
/**
* Fixture prepares schema type to be used in fixtures.
* It adds an optional `__typename` to the schema type and
* marks fields as optional.
*/
export type Fixture<T> = T extends object
? {
// (cvle): We don't use & { __typename?: string } because for some reason
// typescript would allow field names that are not defined!
[P in keyof T | "__typename"]?: P extends keyof T
? T[P] extends Array<infer U>
? Array<Fixture<U>>
: T[P] extends ReadonlyArray<infer V>
? ReadonlyArray<Fixture<V>>
: Fixture<T[P]>
: string
}
: T;
/**
* createFixture lets you input the data of a schema object as deep partial
* including it's `__typename`, merged it with `base` and return it as the
* schema object's type. In the future the result could be trimmed down
* to only include fields that exists in `data` and `base` though to
* type this it seems we need partial generic inferation support.
*/
export default function createFixture<T>(data: Fixture<T>, base?: T): T {
if (base) {
return merge({}, base, data);
}
return data as T;
}
@@ -0,0 +1,15 @@
import createFixture, { Fixture } from "./createFixture";
/**
* createFixtures lets you input an array of data of a schema object as deep partial
* including it's `__typename`, merged it with `base` and return it as any array of the
* schema object's type. In the future the result could be trimmed down
* to only include fields that exists in `data` and `base` though to
* type this it seems we need partial generic inferation support.
*/
export default function createFixtures<T>(
data: Array<Fixture<T>>,
base?: T
): T[] {
return data.map(d => createFixture(d, base)) as T[];
}
@@ -0,0 +1,27 @@
import sinon from "sinon";
import { Mutation } from "talk-framework/lib/relay/mutation";
export default function createMutationResolverStub<
T extends Mutation<any, any, any>
>(
callback: (
variables: T extends Mutation<any, infer I, any> ? I : never,
callCount: number
) => T extends Mutation<any, any, infer R>
? R extends Promise<infer U> ? U | R : R | Promise<R>
: never
) {
let callCount = 0;
const lastClientMutationIds: any[] = [];
const resolver = async (_: any, data: any) => {
const clientMutationId = data.input.clientMutationId;
expectAndFail(clientMutationId).toBeTruthy();
expectAndFail(lastClientMutationIds).not.toContain(clientMutationId);
lastClientMutationIds.push(clientMutationId);
const result = await callback(data.input, callCount++);
expectAndFail(result.clientMutationId).toBeUndefined();
result.clientMutationId = clientMutationId;
return result;
};
return sinon.stub().callsFake(resolver);
}
@@ -0,0 +1,17 @@
import sinon from "sinon";
type Resolver<V, R> = (parent: any, args: V, context: any, info: any) => R;
export default function createQueryResolverStub<T extends Resolver<any, any>>(
callback: (
variables: T extends Resolver<infer V, any> ? V : never,
callCount: number
) => T extends Resolver<any, infer R>
? R extends Promise<infer U> ? U | R : R | Promise<R>
: never
) {
let callCount = 0;
return sinon
.stub()
.callsFake((_: any, data: any) => callback(data, callCount++));
}
@@ -22,3 +22,9 @@ export { default as replaceHistoryLocation } from "./replaceHistoryLocation";
export { default as createAccessToken } from "./createAccessToken";
export { default as findParentsWithType } from "./findParentsWithType";
export { default as findParentWithType } from "./findParentWithType";
export { default as createFixture } from "./createFixture";
export { default as createFixtures } from "./createFixtures";
export {
default as createMutationResolverStub,
} from "./createMutationResolverStub";
export { default as createQueryResolverStub } from "./createQueryResolverStub";