mirror of
https://github.com/wassname/talk.git
synced 2026-09-11 12:51:33 +08:00
[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:
@@ -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;
|
||||
+2
-3
@@ -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}
|
||||
|
||||
+3
-4
@@ -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
|
||||
|
||||
+4
-4
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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 />}>
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
+3
-1
@@ -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);
|
||||
|
||||
+4
-2
@@ -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);
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+3
-1
@@ -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);
|
||||
|
||||
+3
-1
@@ -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;
|
||||
Reference in New Issue
Block a user