mirror of
https://github.com/wassname/talk.git
synced 2026-07-19 11:28:50 +08:00
[next] Admin Configure (#2076)
* feat: Add RadioButton and CheckBox * feat: configure facebook and google auth * feat: configure sso, localAuth and displayName + some tests * test: add integration tests for configure auth * test: more integration tests * feat: add oidc support * test: add oidc integration test * feat: generate sso key initially * fix: import fetchQuery from correct package * fix: admin url * fix: set timezone to utc when testing * refactor: improve route config * fix: remove obsolete line * fix: clientMutationId increment * fix: oidc only create when enabled * fix: copy * test: update snapshots * feat: fixed graphql logging extension * Update src/locales/en-US/admin.ftl Co-Authored-By: cvle <vinh@wikiwi.io> * Apply suggestions from code review Co-Authored-By: cvle <vinh@wikiwi.io> * test: update snapshots * fix: change Local Auth to Email Authentication * fix: copy updates
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React from "react";
|
||||
import CopyToClipboard from "react-copy-to-clipboard";
|
||||
|
||||
import { Button } from "talk-ui/components";
|
||||
import { PropTypesOf } from "talk-ui/types";
|
||||
|
||||
interface InnerProps extends PropTypesOf<typeof Button> {
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
copied: boolean;
|
||||
}
|
||||
|
||||
class PermalinkPopover extends React.Component<InnerProps> {
|
||||
private timeout: any = null;
|
||||
|
||||
public state: State = {
|
||||
copied: false,
|
||||
};
|
||||
|
||||
public componentWillUnmount() {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
|
||||
private handleCopy = () => {
|
||||
this.setCopied(true);
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = setTimeout(() => {
|
||||
this.setCopied(false);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
private setCopied = (b: boolean) => {
|
||||
this.setState({
|
||||
copied: b,
|
||||
});
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { text, ...rest } = this.props;
|
||||
const { copied } = this.state;
|
||||
return (
|
||||
<CopyToClipboard text={text} onCopy={this.handleCopy}>
|
||||
<Button color="primary" variant="filled" size="small" {...rest}>
|
||||
{copied ? (
|
||||
<Localized id="framework-copyButton-copied">
|
||||
<span>Copied!</span>
|
||||
</Localized>
|
||||
) : (
|
||||
<Localized id="framework-copyButton-copy">
|
||||
<span>Copy</span>
|
||||
</Localized>
|
||||
)}
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PermalinkPopover;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./CopyButton";
|
||||
@@ -0,0 +1 @@
|
||||
export { default as CopyButton } from "./CopyButton";
|
||||
@@ -0,0 +1,36 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
hoistStatics,
|
||||
InferableComponentEnhancer,
|
||||
wrapDisplayName,
|
||||
} from "recompose";
|
||||
|
||||
/**
|
||||
* withContext is a HOC wrapper around `TalkContextConsumer`.
|
||||
* `propsCallback` must be provided which accepts the `TalkContext`
|
||||
* and returns the props the should be injected.
|
||||
*/
|
||||
function createContextHOC<Context>(
|
||||
displayName: string,
|
||||
Consumer: React.ComponentType<React.ConsumerProps<Context>>
|
||||
) {
|
||||
return function withContext<T>(
|
||||
propsCallback: (context: Context) => T
|
||||
): InferableComponentEnhancer<T> {
|
||||
return hoistStatics<T>(
|
||||
<U extends T>(WrappedComponent: React.ComponentType<U>) => {
|
||||
const Component: React.StatelessComponent<any> = props => (
|
||||
<Consumer>
|
||||
{context => (
|
||||
<WrappedComponent {...props} {...propsCallback(context)} />
|
||||
)}
|
||||
</Consumer>
|
||||
);
|
||||
Component.displayName = wrapDisplayName(WrappedComponent, displayName);
|
||||
return Component;
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default createContextHOC;
|
||||
@@ -2,3 +2,4 @@ export { default as getMe } from "./getMe";
|
||||
export { default as getMeSourceID } from "./getMeSourceID";
|
||||
export { default as getURLWithCommentID } from "./getURLWithCommentID";
|
||||
export { default as urls } from "./urls";
|
||||
export { default as createContextHOC } from "./createContextHOC";
|
||||
|
||||
@@ -7,7 +7,7 @@ export default (process.env.NODE_ENV !== "development"
|
||||
},
|
||||
}
|
||||
: {
|
||||
admin: "/admin.html",
|
||||
admin: "/admin",
|
||||
embed: {
|
||||
stream: "/stream.html",
|
||||
auth: "/auth.html",
|
||||
|
||||
@@ -1,33 +1,9 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
hoistStatics,
|
||||
InferableComponentEnhancer,
|
||||
wrapDisplayName,
|
||||
} from "recompose";
|
||||
|
||||
import { createContextHOC } from "talk-framework/helpers";
|
||||
import { TalkContext, TalkContextConsumer } from "./TalkContext";
|
||||
|
||||
/**
|
||||
* withContext is a HOC wrapper around `TalkContextConsumer`.
|
||||
* `propsCallback` must be provided which accepts the `TalkContext`
|
||||
* and returns the props the should be injected.
|
||||
*/
|
||||
function withContext<T>(
|
||||
propsCallback: (context: TalkContext) => T
|
||||
): InferableComponentEnhancer<T> {
|
||||
return hoistStatics<T>(
|
||||
<U extends T>(WrappedComponent: React.ComponentType<U>) => {
|
||||
const Component: React.StatelessComponent<any> = props => (
|
||||
<TalkContextConsumer>
|
||||
{context => (
|
||||
<WrappedComponent {...props} {...propsCallback(context)} />
|
||||
)}
|
||||
</TalkContextConsumer>
|
||||
);
|
||||
Component.displayName = wrapDisplayName(WrappedComponent, "withContext");
|
||||
return Component;
|
||||
}
|
||||
);
|
||||
}
|
||||
const withContext = createContextHOC<TalkContext>(
|
||||
"withContext",
|
||||
TalkContextConsumer
|
||||
);
|
||||
|
||||
export default withContext;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { FluentBundle } from "fluent/compat";
|
||||
|
||||
export default function getMessage(
|
||||
bundles: FluentBundle[],
|
||||
key: string,
|
||||
defaultTo = ""
|
||||
): string {
|
||||
const res = bundles.reduce((val, bundle) => {
|
||||
const got = bundle.getMessage(key);
|
||||
if (!got && process.env.NODE_ENV !== "production") {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.warn(`Translation ${key} was not found for ${bundle.locales}`);
|
||||
}
|
||||
return val || got;
|
||||
}, "");
|
||||
return res || defaultTo;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export { default as generateBundles } from "./generateBundles";
|
||||
export { default as negotiateLanguages } from "./negotiateLanguages";
|
||||
export { BundledLocales, LoadableLocales, LocalesData } from "./locales";
|
||||
export { default as getMessage } from "./getMessage";
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Environment, MutationConfig, OperationBase } from "relay-runtime";
|
||||
|
||||
import { Omit } from "talk-framework/types";
|
||||
|
||||
import extractPayload from "./extractPayload";
|
||||
|
||||
/**
|
||||
* Like `MutationConfig` but omits `onCompleted` and `onError`
|
||||
* because we are going to use a Promise API.
|
||||
@@ -12,15 +14,6 @@ export type MutationPromiseConfig<T extends OperationBase> = Omit<
|
||||
"onCompleted" | "onError"
|
||||
>;
|
||||
|
||||
// Extract the payload from the response,
|
||||
function getPayload(response: { [key: string]: any }): any {
|
||||
const keys = Object.keys(response);
|
||||
if (keys.length !== 1) {
|
||||
return response;
|
||||
}
|
||||
return response[keys[0]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes response and error from `commitMutationPromise`.
|
||||
* Meaning `response` will directly contain the payload
|
||||
@@ -33,7 +26,7 @@ export async function commitMutationPromiseNormalized<T extends OperationBase>(
|
||||
): Promise<T["response"][keyof T["response"]]> {
|
||||
try {
|
||||
const response = await commitMutationPromise(environment, config);
|
||||
return getPayload(response);
|
||||
return extractPayload(response);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
@@ -57,7 +50,7 @@ export function commitMutationPromise<T extends OperationBase>(
|
||||
reject(errors);
|
||||
return;
|
||||
}
|
||||
resolve(getPayload(response));
|
||||
resolve(extractPayload(response));
|
||||
},
|
||||
onError: error => {
|
||||
reject(error);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
compose,
|
||||
hoistStatics,
|
||||
InferableComponentEnhancer,
|
||||
wrapDisplayName,
|
||||
} from "recompose";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { TalkContext, withContext } from "../bootstrap";
|
||||
|
||||
/**
|
||||
* createFetchContainer creates a HOC that
|
||||
* injects a property with the name specified in `propName`
|
||||
* and the signature (input: I) => Promise<R>. Calling
|
||||
* this will start a one off query.
|
||||
*/
|
||||
function createFetchContainer<T extends string, V, R>(
|
||||
propName: T,
|
||||
fetch: (
|
||||
environment: Environment,
|
||||
variables: V,
|
||||
context: TalkContext
|
||||
) => Promise<R>
|
||||
): InferableComponentEnhancer<{ [P in T]: (variables: V) => Promise<R> }> {
|
||||
return compose(
|
||||
withContext(context => ({ context })),
|
||||
hoistStatics((BaseComponent: React.ComponentType<any>) => {
|
||||
class CreateFetchContainer extends React.Component<any> {
|
||||
public static displayName = wrapDisplayName(
|
||||
BaseComponent,
|
||||
"createFetchContainer"
|
||||
);
|
||||
|
||||
private fetch = (variables: V) => {
|
||||
return fetch(
|
||||
this.props.context.relayEnvironment,
|
||||
variables,
|
||||
this.props.context
|
||||
);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { context: _, ...rest } = this.props;
|
||||
const inject = {
|
||||
[propName]: this.fetch,
|
||||
};
|
||||
return <BaseComponent {...rest} {...inject} />;
|
||||
}
|
||||
}
|
||||
return CreateFetchContainer as React.ComponentType<any>;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export default createFetchContainer;
|
||||
@@ -0,0 +1,8 @@
|
||||
// Extract the payload from the response,
|
||||
export default function extractPayload(response: { [key: string]: any }): any {
|
||||
const keys = Object.keys(response);
|
||||
if (keys.length !== 1) {
|
||||
return response;
|
||||
}
|
||||
return response[keys[0]];
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export * from "./withLocalStateContainer";
|
||||
export { default as QueryRenderer } from "./QueryRenderer";
|
||||
export * from "./QueryRenderer";
|
||||
export { default as createMutationContainer } from "./createMutationContainer";
|
||||
export { default as createFetchContainer } from "./createFetchContainer";
|
||||
export { default as createAndRetain } from "./createAndRetain";
|
||||
export { default as wrapFetchWithLogger } from "./wrapFetchWithLogger";
|
||||
export {
|
||||
@@ -17,3 +18,4 @@ export {
|
||||
default as commitLocalUpdatePromisified,
|
||||
} from "./commitLocalUpdatePromisified";
|
||||
export { initLocalBaseState, setAuthTokenInLocalState } from "./localState";
|
||||
export { default as fetchQuery } from "./fetchQuery";
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
VALIDATION_REQUIRED,
|
||||
} from "./messages";
|
||||
|
||||
type Validator<T, V> = (v: T, values: V) => ReactNode;
|
||||
export type Validator<T = any, V = any> = (v: T, values: V) => ReactNode;
|
||||
|
||||
/**
|
||||
* createValidator returns a Validator that returns given `error` when `condition` is falsey.
|
||||
@@ -62,7 +62,7 @@ export const validateUsernameCharacters = createValidator(
|
||||
*/
|
||||
export const validateURL = createValidator(
|
||||
v =>
|
||||
/^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/.test(
|
||||
/^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,5}(:[0-9]{1,5})?(\/.*)?$/.test(
|
||||
v
|
||||
),
|
||||
INVALID_URL()
|
||||
|
||||
@@ -11,3 +11,5 @@ export {
|
||||
export { default as createUUIDGenerator } from "./createUUIDGenerator";
|
||||
export * from "./denormalize";
|
||||
export { default as replaceHistoryLocation } from "./replaceHistoryLocation";
|
||||
export { default as limitSnapshotTo } from "./limitSnapshotTo";
|
||||
export { default as inputPredicate } from "./inputPredicate";
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ReactTestInstance } from "react-test-renderer";
|
||||
|
||||
const inputPredicate = (nameOrID: string) => (n: ReactTestInstance) => {
|
||||
return (
|
||||
[n.props.name, n.props.id].indexOf(nameOrID) > -1 &&
|
||||
["input", "button"].indexOf(n.type) > -1
|
||||
);
|
||||
};
|
||||
|
||||
export default inputPredicate;
|
||||
@@ -0,0 +1,14 @@
|
||||
export default function limitSnapshotTo(id: string, node: any) {
|
||||
if (node.props && node.props.id === id) {
|
||||
return node;
|
||||
}
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
const result: any = limitSnapshotTo(id, child);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
Reference in New Issue
Block a user