mirror of
https://github.com/wassname/talk.git
synced 2026-08-04 13:23:35 +08:00
[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:
@@ -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`
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user