mirror of
https://github.com/wassname/talk.git
synced 2026-07-26 13:37:38 +08:00
[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:
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user