diff --git a/package-lock.json b/package-lock.json index d347dd3da..be57c7c13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24256,9 +24256,9 @@ "dev": true }, "typescript": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.0.1.tgz", - "integrity": "sha512-zQIMOmC+372pC/CCVLqnQ0zSBiY7HHodU7mpQdjiZddek4GMj31I3dUJ7gAs9o65X7mnRma6OokOkc6f9jjfBg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.0.3.tgz", + "integrity": "sha512-kk80vLW9iGtjMnIv11qyxLqZm20UklzuR2tL0QAnDIygIUIemcZMxlMWudl9OOt76H3ntVzcTiddQ1/pAAJMYg==", "dev": true }, "ua-parser-js": { diff --git a/package.json b/package.json index 9eaedca62..c56144867 100644 --- a/package.json +++ b/package.json @@ -222,7 +222,7 @@ "typed-css-modules": "^0.3.4", "typeface-manuale": "0.0.54", "typeface-source-sans-pro": "0.0.54", - "typescript": "^3.0.0", + "typescript": "^3.0.3", "uglifyjs-webpack-plugin": "^1.2.5", "webpack": "4.12.0", "webpack-cli": "^3.0.2", diff --git a/src/core/client/auth/containers/AppContainer.tsx b/src/core/client/auth/containers/AppContainer.tsx index b100bfc74..7d424672e 100644 --- a/src/core/client/auth/containers/AppContainer.tsx +++ b/src/core/client/auth/containers/AppContainer.tsx @@ -14,7 +14,7 @@ const AppContainer: StatelessComponent = ({ local: { view } }) => { return ; }; -const enhanced = withLocalStateContainer( +const enhanced = withLocalStateContainer( graphql` fragment AppContainerLocal on Local { view diff --git a/src/core/client/framework/lib/relay/types.ts b/src/core/client/framework/lib/relay/types.ts new file mode 100644 index 000000000..07125e251 --- /dev/null +++ b/src/core/client/framework/lib/relay/types.ts @@ -0,0 +1,7 @@ +import { _RefType } from "react-relay"; + +export type FragmentKeys = { + [P in keyof T]: T[P] extends _RefType | null ? P : never +}[keyof T]; + +export type FragmentKeysNoLocal = Exclude, "local">; diff --git a/src/core/client/framework/lib/relay/withFragmentContainer.ts b/src/core/client/framework/lib/relay/withFragmentContainer.ts index 2541b2001..728c375b5 100644 --- a/src/core/client/framework/lib/relay/withFragmentContainer.ts +++ b/src/core/client/framework/lib/relay/withFragmentContainer.ts @@ -1,12 +1,22 @@ -import { createFragmentContainer, GraphQLTaggedNode } from "react-relay"; +import { + _RefType, + createFragmentContainer, + FragmentOrRegularProp, + GraphQLTaggedNode, +} from "react-relay"; import { InferableComponentEnhancerWithProps } from "recompose"; +import { FragmentKeysNoLocal } from "./types"; /** * withFragmentContainer is a curried version of `createFragmentContainers` * from Relay. */ export default ( - fragmentSpec: { [P in keyof T]: GraphQLTaggedNode } -): InferableComponentEnhancerWithProps => ( - component: React.ComponentType -) => createFragmentContainer(component, fragmentSpec) as any; + fragmentSpec: { [P in FragmentKeysNoLocal]: GraphQLTaggedNode } & { + _?: never; + } +): InferableComponentEnhancerWithProps< + { [P in FragmentKeysNoLocal]: T[P] }, + { [P in FragmentKeysNoLocal]: FragmentOrRegularProp } +> => (component: React.ComponentType) => + createFragmentContainer(component, fragmentSpec) as any; diff --git a/src/core/client/framework/lib/relay/withLocalStateContainer.tsx b/src/core/client/framework/lib/relay/withLocalStateContainer.tsx index 421eb7d71..444b67fa2 100644 --- a/src/core/client/framework/lib/relay/withLocalStateContainer.tsx +++ b/src/core/client/framework/lib/relay/withLocalStateContainer.tsx @@ -13,6 +13,7 @@ import { GraphQLTaggedNode, } from "relay-runtime"; +import { _RefType } from "react-relay"; import { withContext } from "../bootstrap"; interface Props { @@ -35,9 +36,9 @@ export const LOCAL_ID = "client:root.local"; * The `fragmentSpec` must be a `Fragment` on the `LOCAL_TYPE` which * must have the `LOCAL_ID`. */ -function withLocalStateContainer( +function withLocalStateContainer( fragmentSpec: GraphQLTaggedNode -): InferableComponentEnhancer<{ local: T }> { +): InferableComponentEnhancer<{ local: _RefType }> { return compose( withContext(({ relayEnvironment }) => ({ relayEnvironment })), hoistStatics((BaseComponent: React.ComponentType) => { diff --git a/src/core/client/framework/lib/relay/withPaginationContainer.ts b/src/core/client/framework/lib/relay/withPaginationContainer.ts index bb2740d01..143c05b44 100644 --- a/src/core/client/framework/lib/relay/withPaginationContainer.ts +++ b/src/core/client/framework/lib/relay/withPaginationContainer.ts @@ -1,24 +1,24 @@ import { ConnectionConfig, createPaginationContainer, + FragmentOrRegularProp, GraphQLTaggedNode, RelayPaginationProp, } from "react-relay"; import { InferableComponentEnhancerWithProps } from "recompose"; +import { FragmentKeysNoLocal } from "./types"; /** * withPaginationContainer is a curried version of `createPaginationContainers` * from Relay. */ -export default ( - fragmentSpec: { [P in keyof T]: GraphQLTaggedNode }, - connectionConfig: ConnectionConfig< - InnerProps, - FragmentVariables, - QueryVariables - > +export default ( + fragmentSpec: { [P in FragmentKeysNoLocal]: GraphQLTaggedNode } & { + _?: never; + }, + connectionConfig: ConnectionConfig ): InferableComponentEnhancerWithProps< - T & { relay: RelayPaginationProp }, - { [P in keyof T]: any } + { [P in FragmentKeysNoLocal]: T[P] } & { relay: RelayPaginationProp }, + { [P in FragmentKeysNoLocal]: FragmentOrRegularProp } > => (component: React.ComponentType) => createPaginationContainer(component, fragmentSpec, connectionConfig) as any; diff --git a/src/core/client/framework/lib/relay/withRefetchContainer.ts b/src/core/client/framework/lib/relay/withRefetchContainer.ts index 9e9989758..9f0618f55 100644 --- a/src/core/client/framework/lib/relay/withRefetchContainer.ts +++ b/src/core/client/framework/lib/relay/withRefetchContainer.ts @@ -1,19 +1,23 @@ import { createRefetchContainer, + FragmentOrRegularProp, GraphQLTaggedNode, RelayRefetchProp, } from "react-relay"; import { InferableComponentEnhancerWithProps } from "recompose"; +import { FragmentKeysNoLocal } from "./types"; /** * withRefetchContainer is a curried version of `createRefetchContainers` * from Relay. */ export default ( - fragmentSpec: { [P in keyof T]: GraphQLTaggedNode }, + fragmentSpec: { [P in FragmentKeysNoLocal]: GraphQLTaggedNode } & { + _?: never; + }, refetchQuery: GraphQLTaggedNode ): InferableComponentEnhancerWithProps< - T & { relay: RelayRefetchProp }, - { [P in keyof T]: any } + { [P in FragmentKeysNoLocal]: T[P] } & { relay: RelayRefetchProp }, + { [P in FragmentKeysNoLocal]: FragmentOrRegularProp } > => (component: React.ComponentType) => createRefetchContainer(component, fragmentSpec, refetchQuery) as any; diff --git a/src/core/client/stream/components/PermalinkView.tsx b/src/core/client/stream/components/PermalinkView.tsx index 362efa562..1747152d7 100644 --- a/src/core/client/stream/components/PermalinkView.tsx +++ b/src/core/client/stream/components/PermalinkView.tsx @@ -1,13 +1,14 @@ import { Localized } from "fluent-react/compat"; import React, { MouseEvent, StatelessComponent } from "react"; +import { PropTypesOf } from "talk-framework/types"; import { Button, Typography } from "talk-ui/components"; import CommentContainer from "../containers/CommentContainer"; import * as styles from "./PermalinkView.css"; export interface PermalinkViewProps { - comment: {} | null; + comment: PropTypesOf["data"] | null; showAllCommentsHref: string | null; onShowAllComments: (e: MouseEvent) => void; } diff --git a/src/core/client/stream/components/ReplyList.spec.tsx b/src/core/client/stream/components/ReplyList.spec.tsx index 76750c8a0..04038673f 100644 --- a/src/core/client/stream/components/ReplyList.spec.tsx +++ b/src/core/client/stream/components/ReplyList.spec.tsx @@ -3,24 +3,27 @@ import { noop } from "lodash"; import React from "react"; import sinon, { SinonSpy } from "sinon"; +import { removeFragmentRefs } from "talk-framework/testHelpers"; import { PropTypesOf } from "talk-framework/types"; import ReplyList from "./ReplyList"; +const ReplyListN = removeFragmentRefs(ReplyList); + it("renders correctly", () => { - const props: PropTypesOf = { + const props: PropTypesOf = { commentID: "comment-id", comments: [{ id: "comment-1" }, { id: "comment-2" }], onShowAll: noop, hasMore: false, disableShowAll: false, }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); describe("when there is more", () => { - const props: PropTypesOf = { + const props: PropTypesOf = { commentID: "comment-id", comments: [{ id: "comment-1" }, { id: "comment-2" }], onShowAll: sinon.spy(), @@ -28,7 +31,7 @@ describe("when there is more", () => { disableShowAll: false, }; - const wrapper = shallow(); + const wrapper = shallow(); it("renders a load more button", () => { expect(wrapper).toMatchSnapshot(); }); @@ -41,7 +44,7 @@ describe("when there is more", () => { }); const wrapperDisabledButton = shallow( - + ); it("disables load more button", () => { expect(wrapperDisabledButton).toMatchSnapshot(); diff --git a/src/core/client/stream/components/ReplyList.tsx b/src/core/client/stream/components/ReplyList.tsx index c35e0c32c..cd2be4660 100644 --- a/src/core/client/stream/components/ReplyList.tsx +++ b/src/core/client/stream/components/ReplyList.tsx @@ -2,6 +2,7 @@ import { Localized } from "fluent-react/compat"; import * as React from "react"; import { StatelessComponent } from "react"; +import { PropTypesOf } from "talk-framework/types"; import { Button, HorizontalGutter } from "talk-ui/components"; import CommentContainer from "../containers/CommentContainer"; @@ -9,7 +10,9 @@ import Indent from "./Indent"; export interface ReplyListProps { commentID: string; - comments: ReadonlyArray<{ id: string }>; + comments: ReadonlyArray< + { id: string } & PropTypesOf["data"] + >; onShowAll: () => void; hasMore: boolean; disableShowAll: boolean; diff --git a/src/core/client/stream/components/Stream.spec.tsx b/src/core/client/stream/components/Stream.spec.tsx index 14ad6dec5..ff327d798 100644 --- a/src/core/client/stream/components/Stream.spec.tsx +++ b/src/core/client/stream/components/Stream.spec.tsx @@ -3,12 +3,15 @@ import { noop } from "lodash"; import React from "react"; import sinon, { SinonSpy } from "sinon"; +import { removeFragmentRefs } from "talk-framework/testHelpers"; import { PropTypesOf } from "talk-framework/types"; import Stream from "./Stream"; +const StreamN = removeFragmentRefs(Stream); + it("renders correctly", () => { - const props: PropTypesOf = { + const props: PropTypesOf = { assetID: "asset-id", isClosed: false, comments: [{ id: "comment-1" }, { id: "comment-2" }], @@ -17,13 +20,13 @@ it("renders correctly", () => { hasMore: false, user: null, }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); describe("when use is logged in", () => { it("renders correctly", () => { - const props: PropTypesOf = { + const props: PropTypesOf = { assetID: "asset-id", isClosed: false, comments: [{ id: "comment-1" }, { id: "comment-2" }], @@ -32,13 +35,13 @@ describe("when use is logged in", () => { hasMore: false, user: {}, }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); }); describe("when there is more", () => { - const props: PropTypesOf = { + const props: PropTypesOf = { assetID: "asset-id", isClosed: false, comments: [{ id: "comment-1" }, { id: "comment-2" }], @@ -48,7 +51,7 @@ describe("when there is more", () => { user: null, }; - const wrapper = shallow(); + const wrapper = shallow(); it("renders a load more button", () => { expect(wrapper).toMatchSnapshot(); }); @@ -58,7 +61,7 @@ describe("when there is more", () => { expect((props.onLoadMore as SinonSpy).calledOnce).toBe(true); }); - const wrapperDisabledButton = shallow(); + const wrapperDisabledButton = shallow(); it("disables load more button", () => { expect(wrapperDisabledButton).toMatchSnapshot(); }); diff --git a/src/core/client/stream/components/Stream.tsx b/src/core/client/stream/components/Stream.tsx index 884157e36..2d1358f64 100644 --- a/src/core/client/stream/components/Stream.tsx +++ b/src/core/client/stream/components/Stream.tsx @@ -2,6 +2,7 @@ import { Localized } from "fluent-react/compat"; import * as React from "react"; import { StatelessComponent } from "react"; +import { PropTypesOf } from "talk-framework/types"; import { Button, HorizontalGutter } from "talk-ui/components"; import CommentContainer from "../containers/CommentContainer"; @@ -14,11 +15,14 @@ import * as styles from "./Stream.css"; export interface StreamProps { assetID: string; isClosed?: boolean; - comments: ReadonlyArray<{ id: string }>; + comments: ReadonlyArray< + { id: string } & PropTypesOf["data"] & + PropTypesOf["comment"] + >; onLoadMore?: () => void; hasMore?: boolean; disableLoadMore?: boolean; - user: {} | null; + user: PropTypesOf["user"] | null; } const Stream: StatelessComponent = props => { diff --git a/src/core/client/stream/containers/AppContainer.tsx b/src/core/client/stream/containers/AppContainer.tsx index 7fbc7f6d8..533e8e37f 100644 --- a/src/core/client/stream/containers/AppContainer.tsx +++ b/src/core/client/stream/containers/AppContainer.tsx @@ -16,7 +16,7 @@ const AppContainer: StatelessComponent = ({ return ; }; -const enhanced = withLocalStateContainer( +const enhanced = withLocalStateContainer( graphql` fragment AppContainerLocal on Local { commentID diff --git a/src/core/client/stream/containers/CommentContainer.tsx b/src/core/client/stream/containers/CommentContainer.tsx index 0316112d1..4c0a97744 100644 --- a/src/core/client/stream/containers/CommentContainer.tsx +++ b/src/core/client/stream/containers/CommentContainer.tsx @@ -28,7 +28,7 @@ export const CommentContainer: StatelessComponent = props => { return ; }; -const enhanced = withFragmentContainer<{ data: Data }>({ +const enhanced = withFragmentContainer({ data: graphql` fragment CommentContainer on Comment { ...CommentContainer_comment @relay(mask: false) diff --git a/src/core/client/stream/containers/PermalinkButtonContainer.tsx b/src/core/client/stream/containers/PermalinkButtonContainer.tsx index fd1509117..babb4197f 100644 --- a/src/core/client/stream/containers/PermalinkButtonContainer.tsx +++ b/src/core/client/stream/containers/PermalinkButtonContainer.tsx @@ -19,7 +19,7 @@ export const PermalinkContainer: StatelessComponent = ({ ) : null; }; -const enhanced = withLocalStateContainer( +const enhanced = withLocalStateContainer( graphql` fragment PermalinkButtonContainerLocal on Local { assetURL diff --git a/src/core/client/stream/containers/PermalinkViewContainer.tsx b/src/core/client/stream/containers/PermalinkViewContainer.tsx index 096785e27..29e08c61e 100644 --- a/src/core/client/stream/containers/PermalinkViewContainer.tsx +++ b/src/core/client/stream/containers/PermalinkViewContainer.tsx @@ -52,9 +52,7 @@ const enhanced = withContext(ctx => ({ pym: ctx.pym, }))( withSetCommentIDMutation( - withFragmentContainer<{ - comment: CommentData | null; - }>({ + withFragmentContainer({ comment: graphql` fragment PermalinkViewContainer_comment on Comment { ...CommentContainer diff --git a/src/core/client/stream/containers/ReplyListContainer.tsx b/src/core/client/stream/containers/ReplyListContainer.tsx index 4ee7165e6..e2d409a6d 100644 --- a/src/core/client/stream/containers/ReplyListContainer.tsx +++ b/src/core/client/stream/containers/ReplyListContainer.tsx @@ -67,7 +67,6 @@ interface FragmentVariables { } const enhanced = withPaginationContainer< - { comment: Data }, InnerProps, FragmentVariables, ReplyListContainerPaginationQueryVariables diff --git a/src/core/client/stream/containers/StreamContainer.tsx b/src/core/client/stream/containers/StreamContainer.tsx index d629c35d1..dc5f1409f 100644 --- a/src/core/client/stream/containers/StreamContainer.tsx +++ b/src/core/client/stream/containers/StreamContainer.tsx @@ -64,7 +64,6 @@ interface FragmentVariables { } const enhanced = withPaginationContainer< - { asset: AssetData; user: UserData | null }, InnerProps, FragmentVariables, StreamContainerPaginationQueryVariables diff --git a/src/core/client/stream/containers/UserBoxContainer.tsx b/src/core/client/stream/containers/UserBoxContainer.tsx index cc05e3872..6a390c63b 100644 --- a/src/core/client/stream/containers/UserBoxContainer.tsx +++ b/src/core/client/stream/containers/UserBoxContainer.tsx @@ -78,7 +78,7 @@ export class UserBoxContainer extends Component { const enhanced = withSignOutMutation( withSetAuthPopupStateMutation( withShowAuthPopupMutation( - withLocalStateContainer( + withLocalStateContainer( graphql` fragment UserBoxContainerLocal on Local { authPopup { @@ -89,7 +89,7 @@ const enhanced = withSignOutMutation( } ` )( - withFragmentContainer<{ user: UserData | null }>({ + withFragmentContainer({ user: graphql` fragment UserBoxContainer_user on User { username diff --git a/src/core/client/stream/queries/PermalinkViewQuery.tsx b/src/core/client/stream/queries/PermalinkViewQuery.tsx index 67bfb98a0..32943ea17 100644 --- a/src/core/client/stream/queries/PermalinkViewQuery.tsx +++ b/src/core/client/stream/queries/PermalinkViewQuery.tsx @@ -48,7 +48,7 @@ const PermalinkViewQuery: StatelessComponent = ({ /> ); -const enhanced = withLocalStateContainer( +const enhanced = withLocalStateContainer( graphql` fragment PermalinkViewQueryLocal on Local { commentID diff --git a/src/core/client/stream/queries/StreamQuery.tsx b/src/core/client/stream/queries/StreamQuery.tsx index 1d0830992..67a5ae012 100644 --- a/src/core/client/stream/queries/StreamQuery.tsx +++ b/src/core/client/stream/queries/StreamQuery.tsx @@ -1,3 +1,4 @@ +import { Localized } from "fluent-react/compat"; import React, { StatelessComponent } from "react"; import { ReadyState } from "react-relay"; import { @@ -23,6 +24,13 @@ export const render = ({ } if (props) { + if (!props.asset) { + return ( + +
Asset not found
+
+ ); + } return ; } @@ -54,7 +62,7 @@ const StreamQuery: StatelessComponent = ({ /> ); -const enhanced = withLocalStateContainer( +const enhanced = withLocalStateContainer( graphql` fragment StreamQueryLocal on Local { assetID diff --git a/src/locales/en-US/stream.ftl b/src/locales/en-US/stream.ftl index 159ea0b3b..009c52e99 100644 --- a/src/locales/en-US/stream.ftl +++ b/src/locales/en-US/stream.ftl @@ -2,6 +2,8 @@ ## Comments Tab +comments-streamQuery-assetNotFound = Asset not found + comments-postCommentForm-submit = Submit comments-stream-loadMore = Load more comments-replyList-showAll = Show all