[CORL-159, CORL-160] Stream Config Tab (#2219)

* feat: Implement stream configuration tab

* feat: split profile & configure into separate bundles

* chore: better role logic

* fix+chore: add test cases, implement expectAndFail, refactor tests

* chore: add some comments

* chore: Update src/core/client/framework/lib/form/helpers.tsx

Co-Authored-By: cvle <vinh@wikiwi.io>

* feat: support new graphql mutations/schema

* fix: ci fixes

* fix: improvement to revision loading

* fix: updated some tests

* fix: adapt client to changes

* fix: remove obsolote isClosed in UpdateStory

* ci: increase no_output_timeout for build
This commit is contained in:
Kiwi
2019-03-18 22:07:52 +01:00
committed by GitHub
parent c738d9a355
commit 9eb5afbb2b
128 changed files with 2249 additions and 1081 deletions
@@ -8,8 +8,9 @@ import Configure from "./Configure";
it("renders correctly", () => {
const props: PropTypesOf<typeof Configure> = {
onSave: noop,
onSubmit: noop,
onChange: noop,
children: <span />,
};
const renderer = createRenderer();
renderer.render(<Configure {...props} />);
@@ -12,17 +12,18 @@ import { Link, Navigation } from "./Navigation";
import SideBar from "./SideBar";
interface Props {
onSave: (settings: any, form: FormApi) => void;
onSubmit: (settings: any, form: FormApi) => void;
onChange: (formState: FormState) => void;
children: React.ReactElement;
}
const Configure: StatelessComponent<Props> = ({
onSave,
onSubmit,
onChange,
children,
}) => (
<MainLayout data-testid="configure-container">
<Form onSubmit={onSave}>
<Form onSubmit={onSubmit}>
{({ handleSubmit, submitting, pristine, form, submitError }) => (
<form autoComplete="off" onSubmit={handleSubmit} id="configure-form">
<FormSpy onChange={onChange} />
@@ -1,5 +1,4 @@
import { FormApi, FormState } from "final-form";
import { Router } from "found";
import React from "react";
import {
@@ -7,117 +6,54 @@ import {
UpdateSettingsMutation,
withUpdateSettingsMutation,
} from "talk-admin/mutations";
import { TalkContext, withContext } from "talk-framework/lib/bootstrap";
import { InvalidRequestError } from "talk-framework/lib/errors";
import { getMessage } from "talk-framework/lib/i18n";
import { SubmitHookHandler } from "talk-framework/lib/form";
import Configure from "../components/Configure";
import {
AddSubmitHook,
SubmitHook,
SubmitHookContextProvider,
} from "../submitHook";
import NavigationWarningContainer from "./NavigationWarningContainer";
interface Props {
localeBundles: TalkContext["localeBundles"];
router: Router;
updateSettings: UpdateSettingsMutation;
children: React.ReactNode;
children: React.ReactElement;
}
class ConfigureContainer extends React.Component<Props> {
private dirty = false;
private removeTransitionHook: () => void;
private submitHooks: SubmitHook[] = [];
interface State {
dirty: boolean;
}
constructor(props: Props) {
super(props);
class ConfigureContainer extends React.Component<Props, State> {
public state: State = {
dirty: false,
};
this.dirty = false;
const warningMessage = getMessage(
props.localeBundles,
"configure-unsavedInputWarning",
"You have unsaved input. Are you sure you want to leave this page?"
);
this.removeTransitionHook = props.router.addTransitionHook(
() => (this.dirty ? warningMessage : true)
);
}
public componentWillUnmount() {
this.removeTransitionHook();
}
private handleSave = async (
private handleExecute = async (
data: UpdateSettingsInput["settings"],
form: FormApi
) => {
let cancelled = false;
let formErrors: Record<string, React.ReactNode> = {};
const executeCallbacks: Array<() => Promise<any>> = [];
const cancel = (errors: Record<string, React.ReactNode>) => {
cancelled = true;
formErrors = { ...errors, ...formErrors };
};
const onExecute = (cb: () => Promise<any>) => {
executeCallbacks.push(cb);
};
try {
// Call submit hooks, that can manipulate what
// we send as the mutation.
let nextData = data;
for (const hook of this.submitHooks) {
const result = await hook(nextData, { cancel, onExecute });
if (result) {
nextData = result;
}
}
if (cancelled) {
return formErrors;
}
executeCallbacks.push(() =>
this.props.updateSettings({ settings: nextData })
);
for (const c of executeCallbacks.map(cb => cb())) {
await c;
}
form.initialize(data);
} catch (error) {
if (error instanceof InvalidRequestError) {
return error.invalidArgs;
}
// tslint:disable-next-line:no-console
console.error(error);
}
return undefined;
await this.props.updateSettings({ settings: data });
form.initialize(data);
};
private handleChange = ({ dirty }: FormState) => {
this.dirty = dirty;
};
private addSubmitHook: AddSubmitHook = hook => {
this.submitHooks.push(hook);
return () => {
this.submitHooks = this.submitHooks.filter(h => h !== hook);
};
if (dirty !== this.state.dirty) {
this.setState({ dirty });
}
};
public render() {
return (
<SubmitHookContextProvider value={this.addSubmitHook}>
<Configure onChange={this.handleChange} onSave={this.handleSave}>
{this.props.children}
</Configure>
</SubmitHookContextProvider>
<>
<NavigationWarningContainer active={this.state.dirty} />
<SubmitHookHandler onExecute={this.handleExecute}>
{({ onSubmit }) => (
<Configure onChange={this.handleChange} onSubmit={onSubmit}>
{this.props.children}
</Configure>
)}
</SubmitHookHandler>
</>
);
}
}
const enhanced = withContext(({ localeBundles }) => ({ localeBundles }))(
withUpdateSettingsMutation(ConfigureContainer)
);
const enhanced = withUpdateSettingsMutation(ConfigureContainer);
export default enhanced;
@@ -0,0 +1,43 @@
import { Match, Router, withRouter } from "found";
import React from "react";
import { TalkContext, withContext } from "talk-framework/lib/bootstrap";
import { getMessage } from "talk-framework/lib/i18n";
interface Props {
localeBundles: TalkContext["localeBundles"];
router: Router;
active: boolean;
match: Match;
}
class NavigationWarningContainer extends React.Component<Props> {
private removeTransitionHook: () => void;
constructor(props: Props) {
super(props);
const warningMessage = getMessage(
props.localeBundles,
"configure-unsavedInputWarning",
"You have unsaved input. Are you sure you want to leave this page?"
);
this.removeTransitionHook = props.router.addTransitionHook(
() => (this.props.active ? warningMessage : true)
);
}
public componentWillUnmount() {
this.removeTransitionHook();
}
public render() {
return null;
}
}
const enhanced = withContext(({ localeBundles }) => ({ localeBundles }))(
withRouter(NavigationWarningContainer)
);
export default enhanced;
@@ -1,6 +1,7 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field } from "react-final-form";
import { parseBool } from "talk-framework/lib/form";
import { CheckBox, FormField } from "talk-ui/components";
@@ -14,8 +15,6 @@ interface Props {
children: (disabledInside: boolean) => React.ReactNode;
}
const bool = (v: any) => !!v;
const ConfigBoxWithToggleField: StatelessComponent<Props> = ({
id,
name,
@@ -24,7 +23,7 @@ const ConfigBoxWithToggleField: StatelessComponent<Props> = ({
children,
...rest
}) => (
<Field name={name} type="checkbox" parse={bool}>
<Field name={name} type="checkbox" parse={parseBool}>
{({ input }) => (
<ConfigBox
{...rest}
@@ -1,11 +1,10 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field } from "react-final-form";
import { parseBool } from "talk-framework/lib/form";
import { CheckBox, Flex, FormField, InputLabel } from "talk-ui/components";
const bool = (v: any) => !!v;
interface Props {
label: React.ReactNode;
name: string;
@@ -20,7 +19,7 @@ const TargetFilterField: StatelessComponent<Props> = ({
<FormField>
<InputLabel>{label}</InputLabel>
<Flex direction="row" itemGutter="double">
<Field name={`${name}.admin`} type="checkbox" parse={bool}>
<Field name={`${name}.admin`} type="checkbox" parse={parseBool}>
{({ input, meta }) => (
<Localized id="configure-auth-targetFilterTalkAdmin">
<CheckBox
@@ -35,7 +34,7 @@ const TargetFilterField: StatelessComponent<Props> = ({
</Localized>
)}
</Field>
<Field name={`${name}.stream`} type="checkbox" parse={bool}>
<Field name={`${name}.stream`} type="checkbox" parse={parseBool}>
{({ input }) => (
<Localized id="configure-auth-targetFilterCommentStream">
<CheckBox
@@ -7,15 +7,15 @@ import { graphql } from "react-relay";
import { AuthConfigContainer_auth as AuthData } from "talk-admin/__generated__/AuthConfigContainer_auth.graphql";
import { TalkContext, withContext } from "talk-framework/lib/bootstrap";
import { getMessage } from "talk-framework/lib/i18n";
import { withFragmentContainer } from "talk-framework/lib/relay";
import {
AddSubmitHook,
RemoveSubmitHook,
SubmitHook,
withSubmitHookContext,
} from "../../../submitHook";
} from "talk-framework/lib/form";
import { getMessage } from "talk-framework/lib/i18n";
import { withFragmentContainer } from "talk-framework/lib/relay";
import AuthConfig from "../components/AuthConfig";
interface Props {
@@ -35,7 +35,7 @@ const ClosedStreamMessageConfig: StatelessComponent<Props> = ({ disabled }) => (
Write a message to appear after a story is closed for commenting.
</Typography>
</Localized>
<Field name="closedMessage">
<Field name="closeCommenting.message">
{({ input, meta }) => (
<>
<Suspense fallback={<Spinner />}>
@@ -45,7 +45,7 @@ const ClosingCommentStreamsConfig: StatelessComponent<Props> = ({
<Localized id="configure-general-closingCommentStreams-closeCommentsAutomatically">
<InputLabel container="legend">Close Comments Automatically</InputLabel>
</Localized>
<OnOffField name="autoCloseStream" disabled={disabled} />
<OnOffField name="closeCommenting.auto" disabled={disabled} />
</FormField>
<FormField container={<FieldSet />}>
<Localized id="configure-general-closingCommentStreams-closeCommentsAfter">
@@ -53,7 +53,7 @@ const ClosingCommentStreamsConfig: StatelessComponent<Props> = ({
</Localized>
<Field
name="closedTimeout"
name="closeCommenting.timeout"
validate={composeValidators(
required,
validateWholeNumberGreaterThan(0)
@@ -27,7 +27,9 @@ class ClosedStreamMessageConfigContainer extends React.Component<Props> {
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment ClosedStreamMessageConfigContainer_settings on Settings {
closedMessage
closeCommenting {
message
}
}
`,
})(ClosedStreamMessageConfigContainer);
@@ -27,8 +27,10 @@ class ClosingCommentStreamsConfigContainer extends React.Component<Props> {
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment ClosingCommentStreamsConfigContainer_settings on Settings {
autoCloseStream
closedTimeout
closeCommenting {
auto
timeout
}
}
`,
})(ClosingCommentStreamsConfigContainer);
@@ -23,7 +23,7 @@ const OrganizationNameConfig: StatelessComponent<Props> = ({ disabled }) => (
<Localized id="configure-organization-email">
<Header
container={
<label htmlFor="configure-organization-organizationContactEmail" />
<label htmlFor="configure-organization-organization.contactEmail" />
}
>
Organization Email
@@ -35,7 +35,7 @@ const OrganizationNameConfig: StatelessComponent<Props> = ({ disabled }) => (
>
<Typography variant="detail">This E-Mail will be used</Typography>
</Localized>
<Field name="organizationContactEmail" validate={required}>
<Field name="organization.contactEmail" validate={required}>
{({ input, meta }) => (
<>
<TextField
@@ -23,7 +23,7 @@ const OrganizationNameConfig: StatelessComponent<Props> = ({ disabled }) => (
<Localized id="configure-organization-name">
<Header
container={
<label htmlFor="configure-organization-organizationName" />
<label htmlFor="configure-organization-organization.name" />
}
>
Organization Name
@@ -38,7 +38,7 @@ const OrganizationNameConfig: StatelessComponent<Props> = ({ disabled }) => (
community and organization members
</Typography>
</Localized>
<Field name="organizationName" validate={required}>
<Field name="organization.name" validate={required}>
{({ input, meta }) => (
<>
<TextField
@@ -27,7 +27,9 @@ class OrganizationContactEmailConfigContainer extends React.Component<Props> {
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment OrganizationContactEmailConfigContainer_settings on Settings {
organizationContactEmail
organization {
contactEmail
}
}
`,
})(OrganizationContactEmailConfigContainer);
@@ -27,7 +27,9 @@ class OrganizationNameConfigContainer extends React.Component<Props> {
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment OrganizationNameConfigContainer_settings on Settings {
organizationName
organization {
name
}
}
`,
})(OrganizationNameConfigContainer);
@@ -1,22 +0,0 @@
import { noop } from "lodash";
import React from "react";
export type SubmitHook = (
data: any,
actions: {
// Callback will be called after all validations has passed and
// the submit has not been cancelled.
onExecute: (cb: () => Promise<any>) => void;
cancel: (errors?: Record<string, React.ReactNode>) => void;
}
) => Promise<any> | any;
export type RemoveSubmitHook = () => void;
export type AddSubmitHook = (hook: SubmitHook) => RemoveSubmitHook;
export type SubmitHookContext = AddSubmitHook;
const { Provider, Consumer } = React.createContext<SubmitHookContext>(
() => noop
);
export const SubmitHookContextProvider = Provider;
export const SubmitHookContextConsumer = Consumer;
@@ -1,9 +0,0 @@
export {
AddSubmitHook,
SubmitHook,
RemoveSubmitHook,
SubmitHookContext,
SubmitHookContextConsumer,
SubmitHookContextProvider,
} from "./SubmitHookContext";
export { default as withSubmitHookContext } from "./withSubmitHookContext";
@@ -1,12 +0,0 @@
import { createContextHOC } from "talk-framework/helpers";
import {
SubmitHookContext,
SubmitHookContextConsumer,
} from "./SubmitHookContext";
const withSubmitHookContext = createContextHOC<SubmitHookContext>(
"withSubmitHookContext",
SubmitHookContextConsumer
);
export default withSubmitHookContext;