mirror of
https://github.com/wassname/talk.git
synced 2026-06-27 18:45:03 +08:00
[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:
Generated
+360
-627
File diff suppressed because it is too large
Load Diff
+11
-11
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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" />
|
||||
|
||||
+2
-2
@@ -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>
|
||||
);
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ exports[`renders correctly 1`] = `
|
||||
<div
|
||||
data-testid="moderate-container"
|
||||
>
|
||||
<withRouter(Relay(ModerateSearchBarContainer))
|
||||
<ForwardRef(render)
|
||||
allStories={true}
|
||||
story={Object {}}
|
||||
/>
|
||||
|
||||
+3
-3
@@ -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)>
|
||||
`;
|
||||
|
||||
+2
-2
@@ -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}
|
||||
|
||||
+3
-3
@@ -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");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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\\",
|
||||
|
||||
+2
-1
@@ -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}
|
||||
|
||||
+18
-5
@@ -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(),
|
||||
},
|
||||
|
||||
+4
-1
@@ -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: {
|
||||
|
||||
+4
-1
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
+30
-3
@@ -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(),
|
||||
|
||||
+29
-9
@@ -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(),
|
||||
|
||||
+27
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
```
|
||||
Reference in New Issue
Block a user