[CORL-420] Upgrade Relay (#2346)

* chore: upgrade Relay

* fix: fix errors

* fix: snapshot

* fix: relay prefix

* fix: fragment spec error
This commit is contained in:
Vinh
2019-06-07 23:42:26 +02:00
committed by Wyatt Johnson
parent ed4e5fa2a8
commit d4b99a2a57
38 changed files with 682 additions and 929 deletions
+360 -627
View File
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -74,8 +74,8 @@
"farce": "^0.2.6",
"fluent": "^0.10.0",
"fluent-dom": "^0.4.1",
"found": "^0.3.21",
"found-relay": "^0.3.1",
"found": "^0.4.0-alpha.17",
"found-relay": "^0.4.0-alpha.8",
"fs-extra": "^6.0.1",
"graphql": "^0.13.2",
"graphql-config": "^2.0.1",
@@ -118,7 +118,7 @@
"prom-client": "^11.3.0",
"proxy-agent": "^3.1.0",
"querystringify": "^2.1.0",
"react-relay-network-modern": "^2.4.0",
"react-relay-network-modern": "^3.0.4",
"source-map-support": "^0.5.12",
"stack-utils": "^1.0.2",
"striptags": "^3.1.1",
@@ -191,12 +191,12 @@
"@types/react": "^16.8.15",
"@types/react-copy-to-clipboard": "^4.2.6",
"@types/react-dom": "^16.8.4",
"@types/react-relay": "^1.3.9",
"@types/react-relay": "^1.3.14",
"@types/react-responsive": "^3.0.1",
"@types/react-test-renderer": "^16.8.1",
"@types/react-transition-group": "^2.0.14",
"@types/recompose": "^0.26.5",
"@types/relay-runtime": "^1.3.6",
"@types/relay-runtime": "^1.3.12",
"@types/sane": "^2.0.0",
"@types/shallow-equals": "^1.0.0",
"@types/simplemde": "^1.11.7",
@@ -222,7 +222,7 @@
"babel-plugin-dynamic-import-node": "^2.2.0",
"babel-plugin-lodash": "^3.3.4",
"babel-plugin-module-resolver": "^3.2.0",
"babel-plugin-relay": "^1.7.0",
"babel-plugin-relay": "^4.0.0",
"babel-plugin-use-lodash-es": "^0.2.0",
"babel-preset-react-optimize": "^1.0.1",
"bowser": "^1.9.4",
@@ -293,17 +293,17 @@
"react-error-overlay": "^5.1.6",
"react-final-form": "4.0.2",
"react-popper": "^1.3.2",
"react-relay": "^1.7.0-rc.1",
"react-relay": "^4.0.0",
"react-responsive": "^5.0.0",
"react-test-renderer": "^16.8.6",
"react-timeago": "^4.1.9",
"react-transition-group": "^2.5.0",
"react-with-state-props": "^2.0.4",
"recompose": "0.27.1",
"relay-compiler": "^1.7.0-rc.1",
"relay-compiler-language-typescript": "^1.1.0",
"relay-local-schema": "^0.7.0",
"relay-runtime": "^1.7.0-rc.1",
"relay-compiler": "^4.0.0",
"relay-compiler-language-typescript": "^4.1.0",
"relay-local-schema": "^0.8.0",
"relay-runtime": "^4.0.0",
"sane": "^4.1.0",
"shallow-equals": "^1.0.0",
"simplemde": "^1.11.2",
+2
View File
@@ -57,6 +57,8 @@ const args = [
`${program.src}/__generated__`,
"--schema",
config.projects[program.schema].schemaPath,
// "--persist-output",
// `${program.src}/persisted-queries.json`,
];
spawn.sync("relay-compiler", args, { stdio: "inherit" });
@@ -9,7 +9,7 @@ interface Props {
}
const NavigationLink: FunctionComponent<Props> = props => (
<Link to={props.to} Component={AppBarNavigationItem} activePropName="active">
<Link to={props.to} as={AppBarNavigationItem} activePropName="active">
{props.children}
</Link>
);
@@ -1,11 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<Link
Component={[Function]}
<ForwardRef(render)
activePropName="active"
as={[Function]}
to="/moderate"
>
link
</Link>
</ForwardRef(render)>
`;
@@ -5,6 +5,7 @@ import { AuthCheckContainerQueryResponse } from "coral-admin/__generated__/AuthC
import { SetRedirectPathMutation } from "coral-admin/mutations";
import { AbilityType, can } from "coral-admin/permissions";
import RestrictedContainer from "coral-admin/views/restricted/containers/RestrictedContainer";
import { roleIsAtLeast } from "coral-framework/helpers";
import { graphql, MutationProp, withMutation } from "coral-framework/lib/relay";
import { withRouteConfig } from "coral-framework/lib/router";
import { GQLUSER_ROLE } from "coral-framework/schema";
@@ -13,114 +14,115 @@ interface Props {
match: Match;
router: Router;
setRedirectPath: MutationProp<typeof SetRedirectPathMutation>;
data:
| AuthCheckContainerQueryResponse & {
route: {
// An AbilityType can be passed in as the Route data
// to perform a permission check.
data?: AbilityType;
};
}
| null;
data: AuthCheckContainerQueryResponse;
}
class AuthCheckContainer extends React.Component<Props> {
private wasLoggedIn = false;
constructor(props: Props) {
super(props);
this.redirectIfNotLoggedIn();
}
public componentWillReceiveProps(nextProps: Props) {
if (nextProps.data && nextProps.data.viewer) {
this.wasLoggedIn = true;
type CheckParams =
| {
role: GQLUSER_ROLE;
ability?: AbilityType;
}
this.redirectIfNotLoggedIn(nextProps, this.props);
if (nextProps.data && !nextProps.data.viewer) {
this.wasLoggedIn = false;
}
}
| {
role?: GQLUSER_ROLE;
ability: AbilityType;
};
private shouldRedirectTo(props: Props = this.props) {
if (!props.data || props.data.viewer) {
return false;
function createAuthCheckContainer(check: CheckParams) {
class AuthCheckContainer extends React.Component<Props> {
private wasLoggedIn = false;
constructor(props: Props) {
super(props);
this.redirectIfNotLoggedIn();
}
return true;
}
private hasAccess(props: Props = this.props) {
const { viewer } = props.data!;
if (viewer) {
if (
viewer.role === GQLUSER_ROLE.COMMENTER ||
viewer.role === GQLUSER_ROLE.STAFF ||
(props.data &&
props.data.route.data &&
// Perform permission check on the ability passed in by the route data
!can(viewer, props.data.route.data))
) {
public componentWillReceiveProps(nextProps: Props) {
if (nextProps.data && nextProps.data.viewer) {
this.wasLoggedIn = true;
}
this.redirectIfNotLoggedIn(nextProps, this.props);
if (nextProps.data && !nextProps.data.viewer) {
this.wasLoggedIn = false;
}
}
private shouldRedirectTo(props: Props = this.props) {
if (!props.data || props.data.viewer) {
return false;
}
return true;
}
return false;
}
private async redirectIfNotLoggedIn(
props: Props = this.props,
prevProps: Props | null = null
) {
if (!this.shouldRedirectTo(props)) {
return;
}
// If I was previously logged in then logged out, we don't need to set the redirect path.
if (!this.wasLoggedIn) {
const location = props.match.location;
await props.setRedirectPath({
path: location.pathname + location.search + location.hash,
});
}
props.router.replace("/admin/login");
}
public render() {
if (!this.props.data || this.shouldRedirectTo()) {
return null;
}
if (this.hasAccess()) {
return this.props.children;
}
return <RestrictedContainer viewer={this.props.data.viewer!} />;
}
}
const enhanced = withRouteConfig<Props>({
query: graphql`
query AuthCheckContainerQuery {
viewer {
...RestrictedContainer_viewer
username
email
profiles {
__typename
private hasAccess(props: Props = this.props) {
const { viewer } = props.data!;
if (viewer) {
if (
(check.role && !roleIsAtLeast(viewer.role, check.role)) ||
(check.ability && !can(viewer, check.ability))
) {
return false;
}
role
return true;
}
settings {
auth {
integrations {
local {
enabled
targetFilter {
admin
stream
return false;
}
private async redirectIfNotLoggedIn(
props: Props = this.props,
prevProps: Props | null = null
) {
if (!this.shouldRedirectTo(props)) {
return;
}
// If I was previously logged in then logged out, we don't need to set the redirect path.
if (!this.wasLoggedIn) {
const location = props.match.location;
await props.setRedirectPath({
path: location.pathname + location.search + location.hash,
});
}
props.router.replace("/admin/login");
}
public render() {
if (!this.props.data || this.shouldRedirectTo()) {
return null;
}
if (this.hasAccess()) {
return this.props.children;
}
return <RestrictedContainer viewer={this.props.data.viewer!} />;
}
}
const enhanced = withRouteConfig<Props>({
query: graphql`
query AuthCheckContainerQuery {
viewer {
...RestrictedContainer_viewer
username
email
profiles {
__typename
}
role
}
settings {
auth {
integrations {
local {
enabled
targetFilter {
admin
stream
}
}
}
}
}
}
}
`,
})(withRouter(withMutation(SetRedirectPathMutation)(AuthCheckContainer)));
`,
})(withRouter(withMutation(SetRedirectPathMutation)(AuthCheckContainer)));
return enhanced;
}
export default enhanced;
export default createAuthCheckContainer;
@@ -47,18 +47,12 @@ const ApproveCommentMutation = createMutation(
input: {
commentID: input.commentID,
commentRevisionID: input.commentRevisionID,
clientMutationId: clientMutationId.toString(),
},
},
optimisticResponse: {
approveComment: {
comment: {
id: input.commentID,
status: "APPROVED",
},
clientMutationId: (clientMutationId++).toString(),
},
},
optimisticUpdater: store => {
store.get(input.commentID)!.setValue("APPROVED", "status");
},
updater: store => {
const connections = [
getQueueConnection(store, "reported", input.storyID),
@@ -46,7 +46,7 @@ const BanUserMutation = createMutation(
current: lookup<GQLUser>(
environment,
input.userID
)!.status.current.concat([GQLUSER_STATUS.BANNED]),
)!.status.current.concat(GQLUSER_STATUS.BANNED),
ban: {
active: true,
},
@@ -47,18 +47,12 @@ const RejectCommentMutation = createMutation(
input: {
commentID: input.commentID,
commentRevisionID: input.commentRevisionID,
clientMutationId: clientMutationId.toString(),
},
},
optimisticResponse: {
rejectComment: {
comment: {
id: input.commentID,
status: "REJECTED",
},
clientMutationId: (clientMutationId++).toString(),
},
},
optimisticUpdater: store => {
store.get(input.commentID)!.setValue("REJECTED", "status");
},
updater: store => {
const connections = [
getQueueConnection(store, "reported", input.storyID),
+9 -4
View File
@@ -1,8 +1,9 @@
import { makeRouteConfig, Redirect, Route } from "found";
import React from "react";
import { GQLUSER_ROLE } from "coral-framework/schema";
import AppContainer from "./containers/AppContainer";
import AuthCheckContainer from "./containers/AuthCheckContainer";
import createAuthCheckContainer from "./containers/AuthCheckContainer";
import { Ability } from "./permissions";
import CommunityContainer from "./routes/community/containers/CommunityContainer";
import ConfigureContainer from "./routes/configure/containers/ConfigureContainer";
@@ -26,7 +27,10 @@ import StoriesContainer from "./routes/stories/containers/StoriesContainer";
export default makeRouteConfig(
<Route path="admin">
<Route {...AuthCheckContainer.routeConfig}>
<Route
{...createAuthCheckContainer({ role: GQLUSER_ROLE.MODERATOR })
.routeConfig}
>
<Route {...AppContainer.routeConfig}>
<Redirect from="/" to="/admin/moderate" />
<Route
@@ -64,8 +68,9 @@ export default makeRouteConfig(
<Route path="community" {...CommunityContainer.routeConfig} />
<Route path="stories" Component={Stories} />
<Route
{...AuthCheckContainer.routeConfig}
data={Ability.CHANGE_CONFIGURATION}
{...createAuthCheckContainer({
ability: Ability.CHANGE_CONFIGURATION,
}).routeConfig}
>
<Route path="configure" Component={ConfigureContainer}>
<Redirect from="/" to="/admin/configure/general" />
@@ -4,12 +4,12 @@ exports[`renders correctly 1`] = `
<li
className="customClassName"
>
<Link
<ForwardRef(render)
activeClassName="Link-linkActive"
className="Link-link"
to="/admin"
>
child
</Link>
</ForwardRef(render)>
</li>
`;
@@ -1,4 +1,4 @@
import { withRouter, WithRouter } from "found";
import { RouterState, withRouter } from "found";
import * as React from "react";
import { Component } from "react";
@@ -25,7 +25,7 @@ type Props = {
auth: AuthData;
viewer: UserData | null;
setRedirectPath: MutationProp<typeof SetRedirectPathMutation>;
} & WithRouter;
} & RouterState;
function handleAccountCompletion(props: Props) {
const {
@@ -9,7 +9,7 @@ interface Props {
}
const NavigationLink: FunctionComponent<Props> = props => (
<Link to={props.to} Component={SubBarNavigationItem} activePropName="active">
<Link to={props.to} as={SubBarNavigationItem} activePropName="active">
{props.children}
</Link>
);
@@ -4,7 +4,7 @@ exports[`renders correctly 1`] = `
<div
data-testid="moderate-container"
>
<withRouter(Relay(ModerateSearchBarContainer))
<ForwardRef(render)
allStories={true}
story={Object {}}
/>
@@ -1,11 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<Link
Component={[Function]}
<ForwardRef(render)
activePropName="active"
as={[Function]}
to="/moderate"
>
link
</Link>
</ForwardRef(render)>
`;
@@ -12,12 +12,12 @@ exports[`renders correctly 1`] = `
<Localized
id="moderate-single-goToModerationQueues"
>
<Link
<ForwardRef(render)
className="SingleModerate-subBarBegin"
to="/admin/moderate/"
>
Go to moderation queues
</Link>
</ForwardRef(render)>
</Localized>
<Localized
id="moderate-single-singleCommentView"
@@ -20,11 +20,12 @@ export class FlagDetailsContainer extends React.Component<Props> {
const nodes = this.props.comment.flags.nodes;
const offensiveList: React.ReactElement[] = [];
const spamList: React.ReactElement[] = [];
nodes.forEach(n => {
nodes.forEach((n, i) => {
switch (n.reason) {
case GQLCOMMENT_FLAG_REASON.COMMENT_REPORTED_OFFENSIVE:
offensiveList.push(
<FlagDetailsEntry
key={i}
user={n.flagger ? n.flagger.username : <NotAvailable />}
details={n.additionalDetails}
/>
@@ -33,6 +34,7 @@ export class FlagDetailsContainer extends React.Component<Props> {
case GQLCOMMENT_FLAG_REASON.COMMENT_REPORTED_SPAM:
spamList.push(
<FlagDetailsEntry
key={i}
user={n.flagger ? n.flagger.username : <NotAvailable />}
details={n.additionalDetails}
/>
@@ -23,10 +23,7 @@ interface Props {
const UserRow: FunctionComponent<Props> = props => (
<TableRow>
<TableCell className={styles.titleColumn}>
<Link
to={getModerationLink("default", props.storyID)}
Component={TextLink}
>
<Link to={getModerationLink("default", props.storyID)} as={TextLink}>
{props.title || <NotAvailable />}
</Link>
</TableCell>
@@ -14,7 +14,7 @@ interface Props {
const GoToCommentLink: FunctionComponent<Props> = props => {
return (
<Link
Component={TextLink}
as={TextLink}
className={styles.root}
to={props.href}
onClick={props.onClick}
@@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<Link
Component={[Function]}
<ForwardRef(render)
as={[Function]}
className="GoToCommentLink-root"
onClick={[Function]}
to="#"
@@ -18,5 +18,5 @@ exports[`renders correctly 1`] = `
<ForwardRef(forwardRef)>
chevron_right
</ForwardRef(forwardRef)>
</Link>
</ForwardRef(render)>
`;
@@ -7,9 +7,6 @@ exports[`get auth token from url 1`] = `
\\"accessToken\\": \\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIzMWIyNjU5MS00ZTlhLTQzODgtYTdmZi1lMWJkYzVkOTdjY2UifQ==\\",
\\"accessTokenJTI\\": \\"31b26591-4e9a-4388-a7ff-e1bdc5d97cce\\",
\\"loggedIn\\": true,
\\"network\\": {
\\"__ref\\": \\"client:root.local.network\\"
},
\\"view\\": \\"SIGN_IN\\",
\\"error\\": null
}"
@@ -31,16 +28,8 @@ exports[`init local state 1`] = `
\\"accessTokenExp\\": null,
\\"accessTokenJTI\\": null,
\\"loggedIn\\": false,
\\"network\\": {
\\"__ref\\": \\"client:root.local.network\\"
},
\\"view\\": \\"SIGN_IN\\",
\\"error\\": null
},
\\"client:root.local.network\\": {
\\"__id\\": \\"client:root.local.network\\",
\\"__typename\\": \\"Network\\",
\\"isOffline\\": false
}
}"
`;
@@ -1,9 +1,9 @@
import { Environment, RecordProxy, RecordSourceProxy } from "relay-runtime";
/**
* Creates a Record and retain it forever.
* This means that the garbage collector will
* not remove the record on the next run.
* Creates a Record and retain it forever. Useful for local state.
* This means that the garbage collector will not remove the record
* on the next run.
*
* See https://github.com/facebook/relay/issues/1656#issuecomment-380519761
*/
@@ -18,9 +18,6 @@ export const LOCAL_TYPE = "Local";
*/
export const LOCAL_ID = "client:root.local";
export const NETWORK_TYPE = "Network";
export const NETWORK_ID = "client:root.local.network";
export function setAccessTokenInLocalState(
accessToken: string | null,
source: RecordSourceProxy
@@ -57,15 +54,5 @@ export async function initLocalBaseState(
// Set auth token
setAccessTokenInLocalState(accessToken || null, s);
// Create network Record
const networkRecord = createAndRetain(
environment,
s,
NETWORK_ID,
NETWORK_TYPE
);
networkRecord.setValue(false, "isOffline");
localRecord.setLinkedRecord(networkRecord, "network");
});
}
+13 -1
View File
@@ -23,8 +23,20 @@ const createProxy = <T = any>(
recordSource: RelayInMemoryRecordSource
) => {
const proxy: ProxyHandler<any> = {
ownKeys() {
return Object.keys(recordSource);
},
getOwnPropertyDescriptor() {
return {
enumerable: true,
configurable: true,
};
},
has(_, prop) {
return prop in recordSource;
},
get(_, prop) {
if ((recordSource as any)[prop].__ref) {
if (prop in recordSource && (recordSource as any)[prop].__ref) {
return lookup(environment, (recordSource as any)[prop].__ref);
}
return (recordSource as any)[prop];
@@ -83,7 +83,10 @@ function applySimplified(
function useLocal<T>(
fragmentSpec: GraphQLTaggedNode
): [OmitFragments<T>, (update: LocalUpdater<OmitFragments<T>>) => void] {
const fragment = (fragmentSpec as any).data().default;
const fragment =
typeof fragmentSpec === "function"
? fragmentSpec().default
: (fragmentSpec as any).data().default;
if (fragment.kind !== "Fragment") {
throw new Error("Expected fragment");
}
@@ -106,7 +109,7 @@ function useLocal<T>(
if (isAdvancedUpdater(update)) {
update(record);
} else {
applySimplified(record, fragment.selections, update);
applySimplified(record, fragment.selections[0].selections, update);
}
});
return;
@@ -29,7 +29,10 @@ interface Props {
function withLocalStateContainer(
fragmentSpec: GraphQLTaggedNode
): InferableComponentEnhancer<{ local: _RefType<any> }> {
const fragment = (fragmentSpec as any).data().default;
const fragment =
typeof fragmentSpec === "function"
? fragmentSpec().default
: (fragmentSpec as any).data().default;
return compose(
withContext(({ relayEnvironment }) => ({ relayEnvironment })),
hoistStatics((BaseComponent: React.ComponentType<any>) => {
@@ -16,9 +16,6 @@ exports[`init local state 1`] = `
\\"accessTokenExp\\": null,
\\"accessTokenJTI\\": null,
\\"loggedIn\\": false,
\\"network\\": {
\\"__ref\\": \\"client:root.local.network\\"
},
\\"defaultStreamOrderBy\\": \\"CREATED_AT_DESC\\",
\\"authPopup\\": {
\\"__ref\\": \\"client:root.local.authPopup\\"
@@ -26,11 +23,6 @@ exports[`init local state 1`] = `
\\"activeTab\\": \\"COMMENTS\\",
\\"profileTab\\": \\"MY_COMMENTS\\"
},
\\"client:root.local.network\\": {
\\"__id\\": \\"client:root.local.network\\",
\\"__typename\\": \\"Network\\",
\\"isOffline\\": false
},
\\"client:root.local.authPopup\\": {
\\"__id\\": \\"client:root.local.authPopup\\",
\\"__typename\\": \\"AuthPopup\\",
@@ -48,6 +48,7 @@ interface State {
export class EditCommentFormContainer extends Component<Props, State> {
private expiredTimer: any;
private intitialValues = { body: this.props.comment.body || "" };
public state: State = {
initialized: false,
@@ -129,7 +130,7 @@ export class EditCommentFormContainer extends Component<Props, State> {
<EditCommentForm
id={this.props.comment.id}
onSubmit={this.handleOnSubmit}
initialValues={{ body: this.props.comment.body || "" }}
initialValues={this.intitialValues}
onCancel={this.handleOnCancelOrClose}
onClose={this.handleOnCancelOrClose}
rteRef={this.handleRTERef}
@@ -5,10 +5,13 @@ import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
lookup,
MutationInput,
MutationResponsePromise,
} from "coral-framework/lib/relay";
import { CoralContext } from "coral-framework/lib/bootstrap";
import { GQLComment } from "coral-framework/schema";
import { EditCommentMutation as MutationTypes } from "coral-stream/__generated__/EditCommentMutation.graphql";
export type EditCommentInput = MutationInput<MutationTypes>;
@@ -33,7 +36,11 @@ const mutation = graphql`
let clientMutationId = 0;
function commit(environment: Environment, input: EditCommentInput) {
function commit(
environment: Environment,
input: EditCommentInput,
{ uuidGenerator }: CoralContext
) {
return commitMutationPromiseNormalized<MutationTypes>(environment, {
mutation,
variables: {
@@ -44,10 +51,16 @@ function commit(environment: Environment, input: EditCommentInput) {
},
optimisticResponse: {
editComment: {
id: input.commentID,
body: input.body,
editing: {
edited: true,
comment: {
id: input.commentID,
body: input.body,
status: lookup<GQLComment>(environment, input.commentID)!.status,
revision: {
id: uuidGenerator(),
},
editing: {
edited: true,
},
},
clientMutationId: (clientMutationId++).toString(),
},
@@ -6,6 +6,7 @@ import {
createMutation,
MutationInput,
} from "coral-framework/lib/relay";
import { GQLCOMMENT_STATUS } from "coral-framework/schema";
import { ApproveCommentMutation as MutationTypes } from "coral-stream/__generated__/ApproveCommentMutation.graphql";
let clientMutationId = 0;
@@ -27,8 +28,10 @@ const ApproveCommentMutation = createMutation(
optimisticResponse: {
approveComment: {
comment: {
status: "APPROVED",
id: input.commentID,
status: GQLCOMMENT_STATUS.APPROVED,
},
clientMutationId: clientMutationId.toString(),
},
},
variables: {
@@ -6,6 +6,7 @@ import {
createMutation,
MutationInput,
} from "coral-framework/lib/relay";
import { GQLCOMMENT_STATUS } from "coral-framework/schema";
import { RejectCommentMutation as MutationTypes } from "coral-stream/__generated__/RejectCommentMutation.graphql";
let clientMutationId = 0;
@@ -27,8 +28,10 @@ const RejectCommentMutation = createMutation(
optimisticResponse: {
rejectComment: {
comment: {
status: "REJECTED",
id: input.commentID,
status: GQLCOMMENT_STATUS.REJECTED,
},
clientMutationId: clientMutationId.toString(),
},
},
variables: {
@@ -5,10 +5,12 @@ import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
lookup,
MutationInput,
MutationResponsePromise,
} from "coral-framework/lib/relay";
import { GQLComment } from "coral-framework/schema";
import { CreateCommentReactionMutation as MutationTypes } from "coral-stream/__generated__/CreateCommentReactionMutation.graphql";
export type CreateCommentReactionInput = MutationInput<MutationTypes>;
@@ -47,6 +49,9 @@ function commit(environment: Environment, input: CreateCommentReactionInput) {
viewerActionPresence: {
reaction: true,
},
revision: {
id: lookup<GQLComment>(environment, input.commentID)!.revision.id,
},
actionCounts: {
reaction: {
total: currentCount + 1,
@@ -5,10 +5,12 @@ import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
lookup,
MutationInput,
MutationResponsePromise,
} from "coral-framework/lib/relay";
import { GQLComment } from "coral-framework/schema";
import { RemoveCommentReactionMutation as MutationTypes } from "coral-stream/__generated__/RemoveCommentReactionMutation.graphql";
export type RemoveCommentReactionInput = MutationInput<MutationTypes>;
@@ -46,6 +48,9 @@ function commit(environment: Environment, input: RemoveCommentReactionInput) {
viewerActionPresence: {
reaction: false,
},
revision: {
id: lookup<GQLComment>(environment, input.commentID)!.revision.id,
},
actionCounts: {
reaction: {
total: currentCount - 1,
@@ -16,9 +16,10 @@ import {
MutationInput,
MutationResponsePromise,
} from "coral-framework/lib/relay";
import { GQLStory, GQLUSER_ROLE } from "coral-framework/schema";
import { GQLComment, GQLStory, GQLUSER_ROLE } from "coral-framework/schema";
import { CreateCommentReplyMutation as MutationTypes } from "coral-stream/__generated__/CreateCommentReplyMutation.graphql";
import { pick } from "lodash";
import {
incrementStoryCommentCounts,
isVisible,
@@ -108,6 +109,9 @@ graphql`
moderation
}
}
`;
// tslint:disable-next-line:no-unused-expression
graphql`
fragment CreateCommentReplyMutation_viewer on User {
role
}
@@ -135,6 +139,7 @@ function commit(
input: CreateCommentReplyInput,
{ uuidGenerator, relayEnvironment }: CoralContext
) {
const parentComment = lookup<GQLComment>(environment, input.parentID)!;
const viewer = getViewer(environment)!;
const currentDate = new Date().toISOString();
const id = uuidGenerator();
@@ -172,17 +177,39 @@ function commit(
author: {
id: viewer.id,
username: viewer.username,
createdAt: viewer.createdAt,
},
body: input.body,
revision: {
id: uuidGenerator(),
},
parent: {
id: parentComment.id,
author: parentComment.author
? pick(parentComment.author, "username", "id")
: null,
},
editing: {
editableUntil: new Date(Date.now() + 10000),
editableUntil: new Date(Date.now() + 10000).toISOString(),
edited: false,
},
actionCounts: {
reaction: {
total: 0,
},
},
tags: [],
tags: roleIsAtLeast(viewer.role, GQLUSER_ROLE.STAFF)
? [{ name: "Staff" }]
: [],
viewerActionPresence: {
reaction: false,
dontAgree: false,
flag: false,
},
replies: {
edges: [],
pageInfo: { endCursor: null, hasNextPage: false },
},
},
},
clientMutationId: (clientMutationId++).toString(),
@@ -74,15 +74,18 @@ function addCommentToStory(
/** These are needed to be included when querying for the stream. */
// tslint:disable-next-line:no-unused-expression
graphql`
fragment CreateCommentMutation_viewer on User {
role
}
`;
// tslint:disable-next-line:no-unused-expression
graphql`
fragment CreateCommentMutation_story on Story {
settings {
moderation
}
}
fragment CreateCommentMutation_viewer on User {
role
}
`;
/** end */
@@ -108,7 +111,7 @@ function commit(
input: CreateCommentInput,
{ uuidGenerator, relayEnvironment }: CoralContext
) {
const me = getViewer(environment)!;
const viewer = getViewer(environment)!;
const currentDate = new Date().toISOString();
const id = uuidGenerator();
@@ -120,7 +123,7 @@ function commit(
// TODO: Generate and use schema types.
const expectPremoderation =
!roleIsAtLeast(me.role, GQLUSER_ROLE.STAFF) &&
!roleIsAtLeast(viewer.role, GQLUSER_ROLE.STAFF) &&
storySettings.moderation === "PRE";
return commitMutationPromiseNormalized<MutationTypes>(environment, {
@@ -142,19 +145,36 @@ function commit(
createdAt: currentDate,
status: "NONE",
author: {
id: me.id,
username: me.username,
id: viewer.id,
username: viewer.username,
createdAt: viewer.createdAt,
},
revision: {
id: uuidGenerator(),
},
parent: null,
body: input.body,
editing: {
editableUntil: new Date(Date.now() + 10000),
editableUntil: new Date(Date.now() + 10000).toISOString(),
edited: false,
},
actionCounts: {
reaction: {
total: 0,
},
},
tags: [],
tags: roleIsAtLeast(viewer.role, GQLUSER_ROLE.STAFF)
? [{ name: "Staff" }]
: [],
viewerActionPresence: {
reaction: false,
dontAgree: false,
flag: false,
},
replies: {
edges: [],
pageInfo: { endCursor: null, hasNextPage: false },
},
},
},
clientMutationId: (clientMutationId++).toString(),
@@ -478,6 +478,33 @@ exports[`post a reply: optimistic response 1`] = `
</div>
</div>
</div>
<div
className="Comment-subBar"
>
<div
className="Flex-root Flex-flex Flex-alignCenter"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm InReplyTo-icon"
>
reply
</span>
<span>
 
</span>
<span
className="Typography-root Typography-timestamp Typography-colorTextPrimary InReplyTo-inReplyTo"
>
In reply to
<span
className="Typography-root Typography-heading5 Typography-colorTextPrimary InReplyTo-username"
>
Markus
</span>
</span>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
@@ -463,6 +463,33 @@ exports[`post a reply: optimistic response 1`] = `
</div>
</div>
</div>
<div
className="Comment-subBar"
>
<div
className="Flex-root Flex-flex Flex-alignCenter"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm InReplyTo-icon"
>
reply
</span>
<span>
 
</span>
<span
className="Typography-root Typography-timestamp Typography-colorTextPrimary InReplyTo-inReplyTo"
>
In reply to
<span
className="Typography-root Typography-heading5 Typography-colorTextPrimary InReplyTo-username"
>
Markus
</span>
</span>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
-93
View File
@@ -1,93 +0,0 @@
---
name: Workarounds
---
# Workarounds
A place to write down temporary workarounds.
## Babel
Babel versions are currently locked to 7.0.0-beta.49 because of this bug:
https://github.com/babel/babel/issues/8167#issuecomment-397295483
## Relay Client Side Schema Extensions
We use Client Side Schema Extension in `Relay` to store client and UI related state. It works great, the only limitation currently is that locally created `Records` are garbage collected. We created a little helper in `coral-framework/lib/relay/createAndRetain.ts` that creates and retains these `Records` forever. Hopefully this gets resolved and we don't need to do this kind of manual lifecycle management.
Related: https://github.com/facebook/relay/issues/1656#issuecomment-374079965
```ts
import { commitLocalUpdate, Environment } from "relay-runtime";
import {
createAndRetain,
LOCAL_ID,
LOCAL_TYPE,
} from "coral-framework/lib/relay";
commitLocalUpdate(environment, s => {
const root = s.getRoot();
// Create the Local Record which is the Root for the client states.
const localRecord = createAndRetain(environment, s, LOCAL_ID, LOCAL_TYPE);
root.setLinkedRecord(localRecord, "local");
});
```
## Type inference for `compose(...fn)`
[recompose](https://github.com/acdlite/recompose) is a great library to work with Higher-Order-Components. `Typescript` is powerful enough to type a lot of HOC in a way that it works with type inference. However type inference currently does not work for `compose()` until this https://github.com/Microsoft/TypeScript/pull/24626 lands.
That's why in many cases instead of doing this
```ts
export type ContainerProps {
}
const enhance = compose<Props, ContainerProps>(
withLocalStateContainer(…),
withFragmentContainer(…)
);
export default enhance(Container);
```
We do this
```ts
const enhanced = withLocalStateContainer(
)(
withFragmentContainer(
)(Container)
);
export type ContainerProps = ReturnPropTypes<typeof enhanced>;
export default enhanced;
```
A working chaining example looks like this:
```
const enhanced = withFragmentContainer<{ data: Data }>({
data: graphql`
fragment PermalinkViewContainerQuery on Query
@argumentDefinitions(commentID: { type: "ID!" }) {
comment(id: $commentID) {
...CommentContainer_comment
}
}
`,
})(
withLocalStateContainer<Local>(
graphql`
fragment PermalinkViewContainerLocal on Local {
storyURL
}
`
)(PermalinkViewContainer)
);
```