From 41db413bea2501f521c8452e4a578d8f01c3c0b2 Mon Sep 17 00:00:00 2001 From: Kiwi Date: Tue, 19 Mar 2019 18:30:44 +0100 Subject: [PATCH] [CORL-158] Open/Close stream inside of configure tab (#2223) * feat: Open or Close stream inside of configure tab * feat: default disable/close commenting message * fix: adjusted tests --- .../__snapshots__/general.spec.tsx.snap | 4 +- src/core/client/admin/test/fixtures.ts | 2 + .../client/framework/testHelpers/byText.ts | 14 +- .../stream/mutations/CloseStoryMutation.ts | 47 +++ .../stream/mutations/OpenStoryMutation.ts | 47 +++ src/core/client/stream/mutations/index.ts | 10 + .../tabs/configure/components/CloseStream.css | 7 + .../tabs/configure/components/CloseStream.tsx | 43 ++ .../tabs/configure/components/Configure.tsx | 19 +- ...eCommentStream.css => ConfigureStream.css} | 0 ...eCommentStream.tsx => ConfigureStream.tsx} | 10 +- .../configure/components/HorizontalRule.css | 7 + .../configure/components/HorizontalRule.d.ts | 1 + .../components/HorizontalRule.spec.tsx | 10 + .../configure/components/HorizontalRule.tsx | 9 + .../tabs/configure/components/OpenStream.css | 7 + .../tabs/configure/components/OpenStream.tsx | 42 ++ .../HorizontalRule.spec.tsx.snap | 7 + .../containers/ConfigureContainer.tsx | 3 +- ...ainer.tsx => ConfigureStreamContainer.tsx} | 12 +- .../containers/OpenOrCloseStreamContainer.tsx | 65 +++ .../renderConfigure.spec.tsx.snap | 382 ++++++++++-------- .../test/configure/openOrCloseStream.spec.tsx | 102 +++++ src/core/client/ui/components/Flex/Flex.css | 17 + src/core/client/ui/components/Flex/Flex.tsx | 3 +- .../HorizontalGutter/HorizontalGutter.css | 8 + .../HorizontalGutter/HorizontalGutter.tsx | 2 +- .../graph/tenant/resolvers/CloseCommenting.ts | 23 ++ .../tenant/resolvers/DisableCommenting.ts | 23 ++ .../server/graph/tenant/resolvers/index.ts | 4 + .../server/graph/tenant/schema/schema.graphql | 4 +- src/core/server/locales/en-US/common.ftl | 2 + src/core/server/models/settings.ts | 38 +- src/locales/en-US/stream.ftl | 17 +- 34 files changed, 783 insertions(+), 208 deletions(-) create mode 100644 src/core/client/stream/mutations/CloseStoryMutation.ts create mode 100644 src/core/client/stream/mutations/OpenStoryMutation.ts create mode 100644 src/core/client/stream/tabs/configure/components/CloseStream.css create mode 100644 src/core/client/stream/tabs/configure/components/CloseStream.tsx rename src/core/client/stream/tabs/configure/components/{ConfigureCommentStream.css => ConfigureStream.css} (100%) rename src/core/client/stream/tabs/configure/components/{ConfigureCommentStream.tsx => ConfigureStream.tsx} (90%) create mode 100644 src/core/client/stream/tabs/configure/components/HorizontalRule.css create mode 100644 src/core/client/stream/tabs/configure/components/HorizontalRule.d.ts create mode 100644 src/core/client/stream/tabs/configure/components/HorizontalRule.spec.tsx create mode 100644 src/core/client/stream/tabs/configure/components/HorizontalRule.tsx create mode 100644 src/core/client/stream/tabs/configure/components/OpenStream.css create mode 100644 src/core/client/stream/tabs/configure/components/OpenStream.tsx create mode 100644 src/core/client/stream/tabs/configure/components/__snapshots__/HorizontalRule.spec.tsx.snap rename src/core/client/stream/tabs/configure/containers/{ConfigureCommentStreamContainer.tsx => ConfigureStreamContainer.tsx} (73%) create mode 100644 src/core/client/stream/tabs/configure/containers/OpenOrCloseStreamContainer.tsx create mode 100644 src/core/client/stream/test/configure/openOrCloseStream.spec.tsx create mode 100644 src/core/server/graph/tenant/resolvers/CloseCommenting.ts create mode 100644 src/core/server/graph/tenant/resolvers/DisableCommenting.ts create mode 100644 src/core/server/locales/en-US/common.ftl diff --git a/src/core/client/admin/test/configure/__snapshots__/general.spec.tsx.snap b/src/core/client/admin/test/configure/__snapshots__/general.spec.tsx.snap index deea371e0..809804602 100644 --- a/src/core/client/admin/test/configure/__snapshots__/general.spec.tsx.snap +++ b/src/core/client/admin/test/configure/__snapshots__/general.spec.tsx.snap @@ -204,7 +204,7 @@ shared. id="configure-general-sitewideCommenting-message" name="disableCommenting.message" onChange={[Function]} - value="" + value="Comments are closed on this story." /> @@ -705,7 +705,7 @@ moderation panel. id="configure-general-closedStreamMessage-content" name="closeCommenting.message" onChange={[Function]} - value="" + value="Comments are closed on this story." /> diff --git a/src/core/client/admin/test/fixtures.ts b/src/core/client/admin/test/fixtures.ts index 593d6d064..f38fdebc4 100644 --- a/src/core/client/admin/test/fixtures.ts +++ b/src/core/client/admin/test/fixtures.ts @@ -15,10 +15,12 @@ export const settings = { }, disableCommenting: { enabled: false, + message: "Comments are closed on this story.", }, closeCommenting: { auto: false, timeout: 604800, + message: "Comments are closed on this story.", }, customCSSURL: null, domains: ["localhost:8080"], diff --git a/src/core/client/framework/testHelpers/byText.ts b/src/core/client/framework/testHelpers/byText.ts index 60e3f96db..19880040e 100644 --- a/src/core/client/framework/testHelpers/byText.ts +++ b/src/core/client/framework/testHelpers/byText.ts @@ -2,7 +2,6 @@ import React from "react"; import { ReactTestInstance } from "react-test-renderer"; import findParentsWithType from "./findParentsWithType"; -import findParentWithType from "./findParentWithType"; import matchText, { TextMatchOptions, TextMatchPattern } from "./matchText"; const matcher = (pattern: TextMatchPattern, options?: TextMatchOptions) => ( @@ -39,14 +38,17 @@ export function getByText( pattern: TextMatchPattern, options?: TextMatchOptions & SelectorOptions ) { - const result = findParentWithType( - container.find(matcher(pattern, options)), + const results = findParentsWithType( + container.findAll(matcher(pattern, options)), options && options.selector ); - if (!result) { - throw new Error(`Couldn't find text ${pattern}`); + if (results.length === 1) { + return results[0]; } - return result; + if (results.length === 0) { + throw new Error(`Could't find element with text ${pattern}`); + } + throw new Error(`Found multiple elements with text ${pattern}`); } export function getAllByText( diff --git a/src/core/client/stream/mutations/CloseStoryMutation.ts b/src/core/client/stream/mutations/CloseStoryMutation.ts new file mode 100644 index 000000000..5f1729a56 --- /dev/null +++ b/src/core/client/stream/mutations/CloseStoryMutation.ts @@ -0,0 +1,47 @@ +import { graphql } from "react-relay"; +import { Environment } from "relay-runtime"; + +import { + commitMutationPromiseNormalized, + createMutationContainer, + MutationInput, + MutationResponsePromise, +} from "talk-framework/lib/relay"; + +import { CloseStoryMutation as MutationTypes } from "talk-stream/__generated__/CloseStoryMutation.graphql"; + +export type CloseStoryInput = MutationInput; + +const mutation = graphql` + mutation CloseStoryMutation($input: CloseStoryInput!) { + closeStory(input: $input) { + story { + ...ConfigureContainer_story + } + clientMutationId + } + } +`; + +let clientMutationId = 0; + +function commit(environment: Environment, input: CloseStoryInput) { + return commitMutationPromiseNormalized(environment, { + mutation, + variables: { + input: { + ...input, + clientMutationId: (clientMutationId++).toString(), + }, + }, + }); +} + +export const withCloseStoryMutation = createMutationContainer( + "closeStory", + commit +); + +export type CloseStoryMutation = ( + input: CloseStoryInput +) => MutationResponsePromise; diff --git a/src/core/client/stream/mutations/OpenStoryMutation.ts b/src/core/client/stream/mutations/OpenStoryMutation.ts new file mode 100644 index 000000000..5d808c654 --- /dev/null +++ b/src/core/client/stream/mutations/OpenStoryMutation.ts @@ -0,0 +1,47 @@ +import { graphql } from "react-relay"; +import { Environment } from "relay-runtime"; + +import { + commitMutationPromiseNormalized, + createMutationContainer, + MutationInput, + MutationResponsePromise, +} from "talk-framework/lib/relay"; + +import { OpenStoryMutation as MutationTypes } from "talk-stream/__generated__/OpenStoryMutation.graphql"; + +export type OpenStoryInput = MutationInput; + +const mutation = graphql` + mutation OpenStoryMutation($input: OpenStoryInput!) { + openStory(input: $input) { + story { + ...ConfigureContainer_story + } + clientMutationId + } + } +`; + +let clientMutationId = 0; + +function commit(environment: Environment, input: OpenStoryInput) { + return commitMutationPromiseNormalized(environment, { + mutation, + variables: { + input: { + ...input, + clientMutationId: (clientMutationId++).toString(), + }, + }, + }); +} + +export const withOpenStoryMutation = createMutationContainer( + "openStory", + commit +); + +export type OpenStoryMutation = ( + input: OpenStoryInput +) => MutationResponsePromise; diff --git a/src/core/client/stream/mutations/index.ts b/src/core/client/stream/mutations/index.ts index e095f5f89..a97638706 100644 --- a/src/core/client/stream/mutations/index.ts +++ b/src/core/client/stream/mutations/index.ts @@ -60,3 +60,13 @@ export { withUpdateStorySettingsMutation, UpdateStorySettingsMutation, } from "./UpdateStorySettingsMutation"; +export { + OpenStoryInput, + withOpenStoryMutation, + OpenStoryMutation, +} from "./OpenStoryMutation"; +export { + CloseStoryInput, + withCloseStoryMutation, + CloseStoryMutation, +} from "./CloseStoryMutation"; diff --git a/src/core/client/stream/tabs/configure/components/CloseStream.css b/src/core/client/stream/tabs/configure/components/CloseStream.css new file mode 100644 index 000000000..6d38a289e --- /dev/null +++ b/src/core/client/stream/tabs/configure/components/CloseStream.css @@ -0,0 +1,7 @@ +.heading { + padding-bottom: calc(1.5 * var(--spacing-unit)); +} + +.button { + flex-shrink: 0; +} diff --git a/src/core/client/stream/tabs/configure/components/CloseStream.tsx b/src/core/client/stream/tabs/configure/components/CloseStream.tsx new file mode 100644 index 000000000..d15d1dfb2 --- /dev/null +++ b/src/core/client/stream/tabs/configure/components/CloseStream.tsx @@ -0,0 +1,43 @@ +import { Localized } from "fluent-react/compat"; +import React, { StatelessComponent } from "react"; + +import { Button, Flex, Typography } from "talk-ui/components"; + +import styles from "./CloseStream.css"; + +interface Props { + onClick: () => void; + disableButton?: boolean; +} + +const CloseStream: StatelessComponent = ({ onClick, disableButton }) => ( +
+ + + Close Comment Stream + + + + + + This comment stream is currently open. By closing this comment stream, + no new comments may be submitted and all previously submitted comments + will still be displayed. + + + + + + +
+); + +export default CloseStream; diff --git a/src/core/client/stream/tabs/configure/components/Configure.tsx b/src/core/client/stream/tabs/configure/components/Configure.tsx index cba7d8683..f42b4926a 100644 --- a/src/core/client/stream/tabs/configure/components/Configure.tsx +++ b/src/core/client/stream/tabs/configure/components/Configure.tsx @@ -4,20 +4,27 @@ import { PropTypesOf } from "talk-framework/types"; import UserBoxContainer from "talk-stream/containers/UserBoxContainer"; import { HorizontalGutter } from "talk-ui/components"; -import ConfigureCommentStreamContainer from "../containers/ConfigureCommentStreamContainer"; +import ConfigureStreamContainer from "../containers/ConfigureStreamContainer"; +import OpenOrCloseStreamContainer from "../containers/OpenOrCloseStreamContainer"; +import HorizontalRule from "./HorizontalRule"; export interface Props { me: PropTypesOf["me"]; settings: PropTypesOf["settings"]; - story: PropTypesOf["story"]; + story: PropTypesOf["story"] & + PropTypesOf["story"]; } const Configure: StatelessComponent = props => { return ( - - - - +
+ + + + + + +
); }; diff --git a/src/core/client/stream/tabs/configure/components/ConfigureCommentStream.css b/src/core/client/stream/tabs/configure/components/ConfigureStream.css similarity index 100% rename from src/core/client/stream/tabs/configure/components/ConfigureCommentStream.css rename to src/core/client/stream/tabs/configure/components/ConfigureStream.css diff --git a/src/core/client/stream/tabs/configure/components/ConfigureCommentStream.tsx b/src/core/client/stream/tabs/configure/components/ConfigureStream.tsx similarity index 90% rename from src/core/client/stream/tabs/configure/components/ConfigureCommentStream.tsx rename to src/core/client/stream/tabs/configure/components/ConfigureStream.tsx index 133719221..fbdba9ee3 100644 --- a/src/core/client/stream/tabs/configure/components/ConfigureCommentStream.tsx +++ b/src/core/client/stream/tabs/configure/components/ConfigureStream.tsx @@ -17,7 +17,7 @@ import MessageBoxConfigContainer from "../containers/MessageBoxConfigContainer"; import PremodConfigContainer from "../containers/PremodConfigContainer"; import PremodLinksConfigContainer from "../containers/PremodLinksConfigContainer"; -import styles from "./ConfigureCommentStream.css"; +import styles from "./ConfigureStream.css"; interface Props { onSubmit: (settings: any, form: FormApi) => void; @@ -26,7 +26,7 @@ interface Props { PropTypesOf["storySettings"]; } -const ConfigureCommentStream: StatelessComponent = ({ +const ConfigureStream: StatelessComponent = ({ onSubmit, storySettings, }) => ( @@ -40,12 +40,12 @@ const ConfigureCommentStream: StatelessComponent = ({ alignItems="flex-start" itemGutter > - + Configure this Comment Stream - + + + + +); + +export default OpenStream; diff --git a/src/core/client/stream/tabs/configure/components/__snapshots__/HorizontalRule.spec.tsx.snap b/src/core/client/stream/tabs/configure/components/__snapshots__/HorizontalRule.spec.tsx.snap new file mode 100644 index 000000000..f72e5ddab --- /dev/null +++ b/src/core/client/stream/tabs/configure/components/__snapshots__/HorizontalRule.spec.tsx.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +
+`; diff --git a/src/core/client/stream/tabs/configure/containers/ConfigureContainer.tsx b/src/core/client/stream/tabs/configure/containers/ConfigureContainer.tsx index 87b313cd0..cd361cd8b 100644 --- a/src/core/client/stream/tabs/configure/containers/ConfigureContainer.tsx +++ b/src/core/client/stream/tabs/configure/containers/ConfigureContainer.tsx @@ -28,7 +28,8 @@ export class StreamContainer extends React.Component { const enhanced = withFragmentContainer({ story: graphql` fragment ConfigureContainer_story on Story { - ...ConfigureCommentStreamContainer_story + ...ConfigureStreamContainer_story + ...OpenOrCloseStreamContainer_story } `, me: graphql` diff --git a/src/core/client/stream/tabs/configure/containers/ConfigureCommentStreamContainer.tsx b/src/core/client/stream/tabs/configure/containers/ConfigureStreamContainer.tsx similarity index 73% rename from src/core/client/stream/tabs/configure/containers/ConfigureCommentStreamContainer.tsx rename to src/core/client/stream/tabs/configure/containers/ConfigureStreamContainer.tsx index fc42e1e3a..1cc453136 100644 --- a/src/core/client/stream/tabs/configure/containers/ConfigureCommentStreamContainer.tsx +++ b/src/core/client/stream/tabs/configure/containers/ConfigureStreamContainer.tsx @@ -3,21 +3,21 @@ import React from "react"; import { SubmitHookHandler } from "talk-framework/lib/form"; import { graphql, withFragmentContainer } from "talk-framework/lib/relay"; -import { ConfigureCommentStreamContainer_story as StoryData } from "talk-stream/__generated__/ConfigureCommentStreamContainer_story.graphql"; +import { ConfigureStreamContainer_story as StoryData } from "talk-stream/__generated__/ConfigureStreamContainer_story.graphql"; import { UpdateStorySettingsInput, UpdateStorySettingsMutation, withUpdateStorySettingsMutation, } from "talk-stream/mutations"; -import ConfigureCommentStream from "../components/ConfigureCommentStream"; +import ConfigureStream from "../components/ConfigureStream"; interface Props { story: StoryData; updateStorySettings: UpdateStorySettingsMutation; } -class ConfigureCommentStreamContainer extends React.Component { +class ConfigureStreamContainer extends React.Component { private handleExecute = async ( data: UpdateStorySettingsInput["settings"], form: FormApi @@ -33,7 +33,7 @@ class ConfigureCommentStreamContainer extends React.Component { return ( {({ onSubmit }) => ( - @@ -45,7 +45,7 @@ class ConfigureCommentStreamContainer extends React.Component { const enhanced = withFragmentContainer({ story: graphql` - fragment ConfigureCommentStreamContainer_story on Story { + fragment ConfigureStreamContainer_story on Story { id settings { ...PremodConfigContainer_storySettings @@ -54,5 +54,5 @@ const enhanced = withFragmentContainer({ } } `, -})(withUpdateStorySettingsMutation(ConfigureCommentStreamContainer)); +})(withUpdateStorySettingsMutation(ConfigureStreamContainer)); export default enhanced; diff --git a/src/core/client/stream/tabs/configure/containers/OpenOrCloseStreamContainer.tsx b/src/core/client/stream/tabs/configure/containers/OpenOrCloseStreamContainer.tsx new file mode 100644 index 000000000..e0e99159a --- /dev/null +++ b/src/core/client/stream/tabs/configure/containers/OpenOrCloseStreamContainer.tsx @@ -0,0 +1,65 @@ +import React from "react"; + +import { graphql, withFragmentContainer } from "talk-framework/lib/relay"; +import { OpenOrCloseStreamContainer_story as StoryData } from "talk-stream/__generated__/OpenOrCloseStreamContainer_story.graphql"; +import { + CloseStoryMutation, + OpenStoryMutation, + withCloseStoryMutation, + withOpenStoryMutation, +} from "talk-stream/mutations"; + +import CloseStream from "../components/CloseStream"; +import OpenStream from "../components/OpenStream"; + +interface Props { + story: StoryData; + openStory: OpenStoryMutation; + closeStory: CloseStoryMutation; +} + +interface State { + waitingForResponse: boolean; +} + +class OpenOrCloseStreamContainer extends React.Component { + public state: State = { + waitingForResponse: false, + }; + + private handleOnClick = async () => { + if (!this.state.waitingForResponse) { + this.setState({ waitingForResponse: true }); + if (this.props.story.isClosed) { + await this.props.openStory({ id: this.props.story.id }); + } else { + await this.props.closeStory({ id: this.props.story.id }); + } + this.setState({ waitingForResponse: false }); + } + }; + + public render() { + return this.props.story.isClosed ? ( + + ) : ( + + ); + } +} + +const enhanced = withFragmentContainer({ + story: graphql` + fragment OpenOrCloseStreamContainer_story on Story { + id + isClosed + } + `, +})(withOpenStoryMutation(withCloseStoryMutation(OpenOrCloseStreamContainer))); +export default enhanced; diff --git a/src/core/client/stream/test/configure/__snapshots__/renderConfigure.spec.tsx.snap b/src/core/client/stream/test/configure/__snapshots__/renderConfigure.spec.tsx.snap index 2ba59514a..44742785b 100644 --- a/src/core/client/stream/test/configure/__snapshots__/renderConfigure.spec.tsx.snap +++ b/src/core/client/stream/test/configure/__snapshots__/renderConfigure.spec.tsx.snap @@ -8,209 +8,245 @@ exports[`renders configure 1`] = ` id="tabPane-CONFIGURE" role="tabpanel" > -
+
- Signed in as - - Moderator - - . + Signed in as + + Moderator + + . +
+
+ + Not you?  + + +
+
+
- - Not you?  - +

+ Configure this Comment Stream +

-
-
- -
-

- Configure this Comment Stream -

- -
-
-
-
- - -
-
-

- Moderators must approve any comment before it is published to this stream. -

-
-
-
-
- - -
-
-

- Moderators must approve any comment that contains a link before it is published to this stream. -

-
-
-
-
- - -
-
+
+ + +
+

- Add a message to the top of the comment box for your readers. Use this to pose a topic, -ask a question or make announcements relating to this story. + Moderators must approve any comment before it is published to this stream.

+
+
+ + +
+
+

+ Moderators must approve any comment that contains a link before it is published to this stream. +

+
+
+
+
+ + +
+
+
+

+ Add a message to the top of the comment box for your readers. Use this to pose a topic, +ask a question or make announcements relating to this story. +

+
+
+
+ +
+
+
+

+ Close Comment Stream +

+
+

+ This comment stream is currently open. By closing this comment stream, +no new comments may be submitted and all previously submitted comments +will still be displayed. +

+
- +
`; diff --git a/src/core/client/stream/test/configure/openOrCloseStream.spec.tsx b/src/core/client/stream/test/configure/openOrCloseStream.spec.tsx new file mode 100644 index 000000000..3e19af324 --- /dev/null +++ b/src/core/client/stream/test/configure/openOrCloseStream.spec.tsx @@ -0,0 +1,102 @@ +import sinon from "sinon"; + +import { waitForElement, within } from "talk-framework/testHelpers"; + +import { meAsModerator, settings, stories } from "../fixtures"; +import create from "./create"; + +async function createTestRenderer( + resolver: any = {}, + options: { muteNetworkErrors?: boolean; status?: string } = {} +) { + const resolvers = { + ...resolver, + Query: { + settings: sinon.stub().returns(settings), + story: sinon.stub().returns(stories[0]), + me: sinon.stub().returns(meAsModerator), + ...resolver.Query, + }, + }; + + const { testRenderer } = create({ + // Set this to true, to see graphql responses. + logNetwork: false, + muteNetworkErrors: options.muteNetworkErrors, + resolvers, + initLocalState: localRecord => { + localRecord.setValue(stories[0].id, "storyID"); + }, + }); + + const tabPane = await waitForElement(() => + within(testRenderer.root).getByTestID("current-tab-pane") + ); + + return { testRenderer, tabPane }; +} + +it("close stream", async () => { + const closeStoryStub = sinon.stub().callsFake((_: any, data: any) => { + expectAndFail(data.input.id).toBe(stories[0].id); + return { + story: { ...stories[0], isClosed: true }, + clientMutationId: data.input.clientMutationId, + }; + }); + const { tabPane } = await createTestRenderer({ + Mutation: { + closeStory: closeStoryStub, + }, + }); + + const button = within(tabPane).getByText("Close Stream", { + selector: "button", + }); + button.props.onClick(); + + expect(button.props.disabled).toBe(true); + + // Stream should then appear closed. + await waitForElement(() => + within(tabPane).getByText("Open Stream", { + selector: "button", + }) + ); + + // Should have successfully sent with server. + expect(closeStoryStub.called).toBe(true); +}); + +it("opens stream", async () => { + const openStoryStub = sinon.stub().callsFake((_: any, data: any) => { + expectAndFail(data.input.id).toBe(stories[0].id); + return { + story: { ...stories[0], isClosed: false }, + clientMutationId: data.input.clientMutationId, + }; + }); + const { tabPane } = await createTestRenderer({ + Query: { + story: sinon.stub().returns({ ...stories[0], isClosed: true }), + }, + Mutation: { + openStory: openStoryStub, + }, + }); + + const button = within(tabPane).getByText("Open Stream", { + selector: "button", + }); + button.props.onClick(); + + // Stream should then appear open. + await waitForElement(() => + within(tabPane).getByText("Close Stream", { + selector: "button", + }) + ); + + // Should have successfully sent with server. + expect(openStoryStub.called).toBe(true); +}); diff --git a/src/core/client/ui/components/Flex/Flex.css b/src/core/client/ui/components/Flex/Flex.css index 9f20d8414..2618ec2e4 100644 --- a/src/core/client/ui/components/Flex/Flex.css +++ b/src/core/client/ui/components/Flex/Flex.css @@ -137,6 +137,23 @@ margin-left: calc(2 * var(--spacing-unit)) !important; } } + + &:not(.directionColumn).tripleItemGutter { + &:not(:empty) { + margin-top: calc(-3 * var(--spacing-unit)) !important; + } + & > * { + margin-top: calc(3 * var(--spacing-unit)) !important; + } + } + &.directionColumn.tripleItemGutter { + &:not(:empty) { + margin-left: calc(-3 * var(--spacing-unit)) !important; + } + &.tripleItemGutter > * { + margin-left: calc(3 * var(--spacing-unit)) !important; + } + } } .justifyFlexStart { diff --git a/src/core/client/ui/components/Flex/Flex.tsx b/src/core/client/ui/components/Flex/Flex.tsx index 3f7a20a4a..53f75d3c1 100644 --- a/src/core/client/ui/components/Flex/Flex.tsx +++ b/src/core/client/ui/components/Flex/Flex.tsx @@ -25,7 +25,7 @@ interface Props { | "space-evenly"; alignItems?: "flex-start" | "flex-end" | "center" | "baseline" | "stretch"; direction?: "row" | "column" | "row-reverse" | "column-reverse"; - itemGutter?: boolean | "half" | "double"; + itemGutter?: boolean | "half" | "double" | "triple"; className?: string; wrap?: boolean | "reverse"; @@ -51,6 +51,7 @@ const Flex: StatelessComponent = props => { [classes.itemGutter]: itemGutter === true, [classes.halfItemGutter]: itemGutter === "half", [classes.doubleItemGutter]: itemGutter === "double", + [classes.tripleItemGutter]: itemGutter === "triple", [classes.wrap]: wrap === true, [classes.wrapReverse]: wrap === "reverse", }; diff --git a/src/core/client/ui/components/HorizontalGutter/HorizontalGutter.css b/src/core/client/ui/components/HorizontalGutter/HorizontalGutter.css index f6751ce9a..dfaabe567 100644 --- a/src/core/client/ui/components/HorizontalGutter/HorizontalGutter.css +++ b/src/core/client/ui/components/HorizontalGutter/HorizontalGutter.css @@ -36,3 +36,11 @@ margin: 0 !important; } } +.triple { + & > * { + margin: 0 0 calc(3 * var(--spacing-unit)) 0 !important; + } + & > *:last-child { + margin: 0 !important; + } +} diff --git a/src/core/client/ui/components/HorizontalGutter/HorizontalGutter.tsx b/src/core/client/ui/components/HorizontalGutter/HorizontalGutter.tsx index 912c3a2b5..a77e3e2e1 100644 --- a/src/core/client/ui/components/HorizontalGutter/HorizontalGutter.tsx +++ b/src/core/client/ui/components/HorizontalGutter/HorizontalGutter.tsx @@ -13,7 +13,7 @@ interface Props extends HTMLAttributes { */ classes: typeof styles; - size?: "half" | "full" | "double" | "oneAndAHalf"; + size?: "half" | "full" | "double" | "triple" | "oneAndAHalf"; /** The name of the HorizontalGutter to render */ children?: React.ReactNode; diff --git a/src/core/server/graph/tenant/resolvers/CloseCommenting.ts b/src/core/server/graph/tenant/resolvers/CloseCommenting.ts new file mode 100644 index 000000000..a1ff76e58 --- /dev/null +++ b/src/core/server/graph/tenant/resolvers/CloseCommenting.ts @@ -0,0 +1,23 @@ +import { GQLCloseCommentingTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types"; +import * as settings from "talk-server/models/settings"; +import { translate } from "talk-server/services/i18n"; + +export const CloseCommenting: GQLCloseCommentingTypeResolver< + settings.CloseCommenting +> = { + message: (closeCommenting, input, ctx) => { + if (closeCommenting.message) { + return closeCommenting.message; + } + + // Get the translation bundle. + const bundle = ctx.i18n.getBundle(ctx.lang); + + // Translate the default close message. + return translate( + bundle, + "Comments are closed on this story.", + "closeCommentingDefaultMessage" + ); + }, +}; diff --git a/src/core/server/graph/tenant/resolvers/DisableCommenting.ts b/src/core/server/graph/tenant/resolvers/DisableCommenting.ts new file mode 100644 index 000000000..ff99e1976 --- /dev/null +++ b/src/core/server/graph/tenant/resolvers/DisableCommenting.ts @@ -0,0 +1,23 @@ +import { GQLDisableCommentingTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types"; +import * as settings from "talk-server/models/settings"; +import { translate } from "talk-server/services/i18n"; + +export const DisableCommenting: GQLDisableCommentingTypeResolver< + settings.DisableCommenting +> = { + message: (disableCommenting, input, ctx) => { + if (disableCommenting.message) { + return disableCommenting.message; + } + + // Get the translation bundle. + const bundle = ctx.i18n.getBundle(ctx.lang); + + // Translate the default close message. + return translate( + bundle, + "Comments are closed on this story.", + "disableCommentingDefaultMessage" + ); + }, +}; diff --git a/src/core/server/graph/tenant/resolvers/index.ts b/src/core/server/graph/tenant/resolvers/index.ts index be7d0fec9..fa2788d04 100644 --- a/src/core/server/graph/tenant/resolvers/index.ts +++ b/src/core/server/graph/tenant/resolvers/index.ts @@ -4,10 +4,12 @@ import { GQLResolver } from "talk-server/graph/tenant/schema/__generated__/types import { AcceptCommentPayload } from "./AcceptCommentPayload"; import { AuthIntegrations } from "./AuthIntegrations"; +import { CloseCommenting } from "./CloseCommenting"; import { Comment } from "./Comment"; import { CommentCounts } from "./CommentCounts"; import { CommentModerationAction } from "./CommentModerationAction"; import { CommentRevision } from "./CommentRevision"; +import { DisableCommenting } from "./DisableCommenting"; import { FacebookAuthIntegration } from "./FacebookAuthIntegration"; import { GoogleAuthIntegration } from "./GoogleAuthIntegration"; import { ModerationQueue } from "./ModerationQueue"; @@ -24,11 +26,13 @@ import { User } from "./User"; const Resolvers: GQLResolver = { AcceptCommentPayload, AuthIntegrations, + CloseCommenting, Comment, CommentCounts, CommentModerationAction, CommentRevision, Cursor, + DisableCommenting, FacebookAuthIntegration, GoogleAuthIntegration, ModerationQueue, diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index 4dbff211c..1f0f79f79 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -780,7 +780,7 @@ type DisableCommenting { message will be shown above the comment stream while commenting is disabled site-wide. """ - message: String + message: String! } ################################################################################ @@ -912,7 +912,7 @@ type CloseCommenting { message when provided will be the message that shows when the comment stream is closed for commenting. """ - message: String + message: String! } """ diff --git a/src/core/server/locales/en-US/common.ftl b/src/core/server/locales/en-US/common.ftl new file mode 100644 index 000000000..596998a56 --- /dev/null +++ b/src/core/server/locales/en-US/common.ftl @@ -0,0 +1,2 @@ +closeCommentingDefaultMessage = Comments are closed on this story. +disableCommentingDefaultMessage = Comments are closed on this story. diff --git a/src/core/server/models/settings.ts b/src/core/server/models/settings.ts index e1c436ca5..0febadf4e 100644 --- a/src/core/server/models/settings.ts +++ b/src/core/server/models/settings.ts @@ -30,6 +30,10 @@ export type FacebookAuthIntegration = Omit< "callbackURL" | "redirectURL" >; +/** + * AuthIntegrations are the set of configurations for the variations of + * authentication solutions. + */ export interface AuthIntegrations { local: GQLLocalAuthIntegration; sso: GQLSSOAuthIntegration; @@ -38,6 +42,9 @@ export interface AuthIntegrations { facebook: FacebookAuthIntegration; } +/** + * Auth is the set of configured authentication integrations. + */ export type Auth = Omit & { /** * integrations are the set of configurations for the variations of @@ -46,11 +53,25 @@ export type Auth = Omit & { integrations: AuthIntegrations; }; +/** + * CloseCommenting contains settings related to the automatic closing of commenting on + * Stories. + */ +export type CloseCommenting = Omit & + Partial>; + +/** + * DisableCommenting will disable commenting site-wide. + */ +export type DisableCommenting = Omit< + GQLSettings["disableCommenting"], + "message" +> & + Partial>; + export type Settings = GlobalModerationSettings & Pick< GQLSettings, - | "closeCommenting" - | "disableCommenting" | "charCount" | "email" | "karma" @@ -62,7 +83,18 @@ export type Settings = GlobalModerationSettings & | "communityGuidelines" > & { /** - * Set of configured authentication integrations. + * auth is the set of configured authentication integrations. */ auth: Auth; + + /** + * closeCommenting contains settings related to the automatic closing of commenting on + * Stories. + */ + closeCommenting: CloseCommenting; + + /** + * disableCommenting will disable commenting site-wide. + */ + disableCommenting: DisableCommenting; }; diff --git a/src/locales/en-US/stream.ftl b/src/locales/en-US/stream.ftl index 5838c3beb..b69b85ec4 100644 --- a/src/locales/en-US/stream.ftl +++ b/src/locales/en-US/stream.ftl @@ -135,8 +135,8 @@ configure-configureQuery-errorLoadingProfile = Error loading configure configure-configureQuery-storyNotFound = Story not found ## Comment Stream -configure-commentStream-title = Configure this Comment Stream -configure-commentStream-apply = Apply +configure-stream-title = Configure this Comment Stream +configure-stream-apply = Apply configure-premod-title = Enable Pre-Moderation configure-premod-description = @@ -154,3 +154,16 @@ configure-messageBox-preview = Preview configure-messageBox-selectAnIcon = Select an Icon configure-messageBox-noIcon = No Icon configure-messageBox-writeAMessage = Write a Message + +configure-closeStream-title = Close Comment Stream +configure-closeStream-description = + This comment stream is currently open. By closing this comment stream, + no new comments may be submitted and all previously submitted comments + will still be displayed. +configure-closeStream-closeStream = Close Stream + +configure-openStream-title = Open Stream +configure-openStream-description = + This comment stream is currently closed. By opening this comment + stream new comments may be submitted and displayed. +configure-openStream-openStream = Open Stream