[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>;
})
);
}