[CORL-294] Moderate a single story + quick search (#2286)

* feat: allow passing a `storyID` to `Query.moderationQueues`

* feat: moderate by story

* feat: implement search story combobox

* feat: add translations

* fix: tests

* fix: duplicate id

* fix: rename file

* chore: add more comments

* fix: add missing translation

* review: use query parameter "q" instead of url path

* chore: move placeholder logic inside, maybe this makes it clearer :-D
This commit is contained in:
Kiwi
2019-04-26 14:23:46 +00:00
committed by Wyatt Johnson
parent a91de05af9
commit ab938985e4
86 changed files with 1934 additions and 319 deletions
+2
View File
@@ -1 +1,3 @@
export { default as useEffectAfterMount } from "./useEffectAfterMount";
export { default as usePrevious } from "./usePrevious";
export { default as useEffectWhenChanged } from "./useEffectWhenChanged";
@@ -0,0 +1,21 @@
import equals from "shallow-equals";
import useEffectAfterMount from "./useEffectAfterMount";
import usePrevious from "./usePrevious";
/**
* useEffectWhenChanged is a react hook that will run effects
* when value changed.
*/
export default function useEffectWhenChanged(
callback: () => void,
deps: ReadonlyArray<any>
) {
const previous = usePrevious(deps);
// We use `useEffectAfterMount` to make sure `previous` has an assigned value.
useEffectAfterMount(() => {
if (!equals(deps, previous)) {
callback();
}
}, deps);
}
@@ -0,0 +1,13 @@
import { useEffect, useRef } from "react";
/**
* usePrevious is a react hook that will return the
* previous value.
*/
export default function usePrevious<T>(value: T): T {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
});
return ref.current as T;
}
@@ -0,0 +1,117 @@
import React, { useCallback } from "react";
import { fetchQuery as relayFetchQuery } from "react-relay";
import {
compose,
hoistStatics,
InferableComponentEnhancer,
wrapDisplayName,
} from "recompose";
import {
CacheConfig,
Environment,
GraphQLTaggedNode,
Variables,
} from "relay-runtime";
import { TalkContext, useTalkContext, withContext } from "../bootstrap";
import extractPayload from "./extractPayload";
export interface Fetch<N, V, R> {
name: N;
fetch: (environment: Environment, variables: V, context: TalkContext) => R;
}
export type FetchVariables<T extends { variables: any }> = T["variables"];
export type FetchProp<T extends Fetch<any, any, any>> = T extends Fetch<
any,
infer V,
infer R
>
? Parameters<T["fetch"]>[1] extends undefined
? () => R
: keyof Parameters<T["fetch"]>[1] extends never
? () => R
: (variables: V) => R
: never;
export function createFetch<N extends string, V, R>(
name: N,
fetch: (environment: Environment, variables: V, context: TalkContext) => R
): Fetch<N, V, R> {
return {
name,
fetch,
} as any;
}
export async function fetchQuery<T extends { response: any }>(
environment: Environment,
taggedNode: GraphQLTaggedNode,
variables: Variables,
cacheConfig?: CacheConfig
): Promise<T["response"][keyof T["response"]]> {
const result = await relayFetchQuery(
environment,
taggedNode,
variables,
cacheConfig
);
return extractPayload(result);
}
/**
* useFetch is a React Hook that
* returns a callback to call the fetch.
*/
export function useFetch<V, R>(
fetch: Fetch<any, V, R>
): FetchProp<typeof fetch> {
const context = useTalkContext();
return useCallback<FetchProp<typeof fetch>>(
((variables: V) => {
context.eventEmitter.emit(`fetch.${fetch.name}`, variables);
return fetch.fetch(context.relayEnvironment, variables, context);
}) as any,
[context]
);
}
/**
* withFetch creates a HOC that injects the fetch as
* a property.
*/
export function withFetch<N extends string, V, R>(
fetch: Fetch<N, V, R>
): InferableComponentEnhancer<{ [P in N]: FetchProp<typeof fetch> }> {
return compose(
withContext(context => ({ context })),
hoistStatics((BaseComponent: React.ComponentType<any>) => {
class WithFetch extends React.Component<{
context: TalkContext;
}> {
public static displayName = wrapDisplayName(BaseComponent, "withFetch");
private fetch = (variables: V) => {
this.props.context.eventEmitter.emit(
`fetch.${fetch.name}`,
variables
);
return fetch.fetch(
this.props.context.relayEnvironment,
variables,
this.props.context
);
};
public render() {
const { context: _, ...rest } = this.props;
const inject = {
[fetch.name]: this.fetch,
};
return <BaseComponent {...rest} {...inject} />;
}
}
return WithFetch as React.ComponentType<any>;
})
);
}
@@ -1,24 +0,0 @@
import { fetchQuery as relayFetchQuery } from "react-relay";
import {
CacheConfig,
Environment,
GraphQLTaggedNode,
Variables,
} from "relay-runtime";
import extractPayload from "./extractPayload";
export default async function fetchQuery<R = any>(
environment: Environment,
taggedNode: GraphQLTaggedNode,
variables: Variables,
cacheConfig?: CacheConfig
): Promise<R> {
const result = await relayFetchQuery(
environment,
taggedNode,
variables,
cacheConfig
);
return extractPayload(result);
}
+8 -1
View File
@@ -27,7 +27,14 @@ export {
default as commitLocalUpdatePromisified,
} from "./commitLocalUpdatePromisified";
export { initLocalBaseState, setAccessTokenInLocalState } from "./localState";
export { default as fetchQuery } from "./fetchQuery";
export {
fetchQuery,
createFetch,
FetchVariables,
FetchProp,
withFetch,
useFetch,
} from "./fetch";
export { default as useRefetch } from "./useRefetch";
export { default as useLoadMore } from "./useLoadMore";
export { default as lookup } from "./lookup";
@@ -2,7 +2,7 @@ import { useState } from "react";
import { RelayPaginationProp } from "react-relay";
import { Variables } from "relay-runtime";
import { useEffectAfterMount } from "talk-framework/hooks";
import { useEffectWhenChanged } from "talk-framework/hooks";
/**
* useRefetch is a react hook that returns a `refetch` callback
@@ -15,7 +15,7 @@ export default function useRefetch<V = Variables>(
): [() => void, boolean] {
const [manualRefetchCount, setManualRefetchCount] = useState(0);
const [refetching, setRefetching] = useState(false);
useEffectAfterMount(() => {
useEffectWhenChanged(() => {
setRefetching(true);
const disposable = relay.refetchConnection(
10,
@@ -34,7 +34,7 @@ export default function useRefetch<V = Variables>(
}
};
}, [
relay,
relay.environment,
manualRefetchCount,
...Object.keys(variables).reduce<any[]>((a, k) => {
a.push((variables as any)[k]);
@@ -1,4 +1,4 @@
import { RouteProps } from "found";
import { RouteMatch, RouteProps } from "found";
import * as React from "react";
interface InjectedProps<T> {
@@ -7,25 +7,48 @@ interface InjectedProps<T> {
retry?: Error | null;
}
type RouteConfig = Partial<Pick<RouteProps, "query" | "getQuery">> &
type RouteConfig<QueryResponse> = Partial<
Pick<RouteProps, "query" | "getQuery">
> &
Partial<Pick<RouteProps, "data" | "getData" | "defer">> & {
cacheConfig?: {
force?: boolean;
};
prepareVariables?: (
params: Record<string, string>,
match: RouteMatch
) => Record<string, any>;
render?: (args: {
error: Error;
data: QueryResponse | null;
retry: () => void;
match: RouteMatch;
Component: React.ComponentType<any>;
}) => React.ReactElement;
};
function withRouteConfig<QueryResponse>(config: RouteConfig) {
function withRouteConfig<QueryResponse>(config: RouteConfig<QueryResponse>) {
const hoc = <T extends InjectedProps<QueryResponse>>(
component: React.ComponentType<T>
) => {
(component as any).routeConfig = {
...config,
Component: component,
render: ({ error, props, retry, Component }: any) => {
return React.createElement(Component, { error, data: props, retry });
render: ({ error, props: data, retry, match, Component }: any) => {
if (config.render) {
return config.render({ error, data, retry, match, Component });
}
return React.createElement(Component, {
error,
data,
retry,
match,
});
},
};
return component as React.ComponentClass<T> & { routeConfig: RouteConfig };
return component as React.ComponentClass<T> & {
routeConfig: RouteConfig<QueryResponse>;
};
};
return hoc;
}