mirror of
https://github.com/wassname/talk.git
synced 2026-07-03 20:06:36 +08:00
Generated
+391
-190
File diff suppressed because it is too large
Load Diff
+4
-1
@@ -103,6 +103,7 @@
|
||||
"@types/passport-oauth2": "^1.4.5",
|
||||
"@types/passport-strategy": "^0.2.33",
|
||||
"@types/query-string": "^6.1.0",
|
||||
"@types/react-copy-to-clipboard": "^4.2.5",
|
||||
"@types/react-dom": "^16.0.6",
|
||||
"@types/react-relay": "github:coralproject/patched#types/react-relay",
|
||||
"@types/react-responsive": "^3.0.1",
|
||||
@@ -168,9 +169,11 @@
|
||||
"query-string": "^6.1.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
"react": "^16.4.0",
|
||||
"react-copy-to-clipboard": "^5.0.1",
|
||||
"react-dev-utils": "6.0.0-next.3e165448",
|
||||
"react-dom": "^16.4.0",
|
||||
"react-final-form": "^3.6.4",
|
||||
"react-popper": "^1.0.0-beta.6",
|
||||
"react-relay": "github:coralproject/patched#react-relay",
|
||||
"react-responsive": "^4.1.0",
|
||||
"react-test-renderer": "^16.4.1",
|
||||
@@ -199,7 +202,7 @@
|
||||
"typed-css-modules": "^0.3.4",
|
||||
"typeface-manuale": "0.0.54",
|
||||
"typeface-source-sans-pro": "0.0.54",
|
||||
"typescript": "^2.9.2",
|
||||
"typescript": "^3.0.0",
|
||||
"uglifyjs-webpack-plugin": "^1.2.5",
|
||||
"webpack": "4.12.0",
|
||||
"webpack-cli": "^3.0.2",
|
||||
|
||||
@@ -5,9 +5,9 @@ import {
|
||||
Decorator,
|
||||
withAutoHeight,
|
||||
withClickEvent,
|
||||
withCommentID,
|
||||
withEventEmitter,
|
||||
withIOSSafariWidthWorkaround,
|
||||
withSetCommentID,
|
||||
} from "./decorators";
|
||||
import PymControl from "./PymControl";
|
||||
import { ensureEndSlash } from "./utils";
|
||||
@@ -15,6 +15,7 @@ import { ensureEndSlash } from "./utils";
|
||||
interface CreatePymControlConfig {
|
||||
assetID?: string;
|
||||
assetURL?: string;
|
||||
commentID?: string;
|
||||
title?: string;
|
||||
eventEmitter: EventEmitter2;
|
||||
id: string;
|
||||
@@ -26,13 +27,14 @@ export function createPymControl(config: CreatePymControlConfig) {
|
||||
withIOSSafariWidthWorkaround,
|
||||
withAutoHeight,
|
||||
withClickEvent,
|
||||
withCommentID,
|
||||
withSetCommentID,
|
||||
withEventEmitter(config.eventEmitter),
|
||||
];
|
||||
|
||||
const query = qs.stringify({
|
||||
assetID: config.assetID,
|
||||
assetURL: config.assetURL,
|
||||
commentID: config.commentID,
|
||||
});
|
||||
const url = `${ensureEndSlash(config.rootURL)}stream.html?${query}`;
|
||||
return new PymControl({
|
||||
@@ -73,6 +75,7 @@ export type StreamInterface = ReturnType<typeof createStreamInterface>;
|
||||
export interface CreateConfig {
|
||||
assetID?: string;
|
||||
assetURL?: string;
|
||||
commentID?: string;
|
||||
title?: string;
|
||||
eventEmitter: EventEmitter2;
|
||||
id: string;
|
||||
|
||||
@@ -4,7 +4,7 @@ export type CleanupCallback = () => void;
|
||||
export type Decorator = (pym: pym.Parent) => CleanupCallback | void;
|
||||
export { default as withAutoHeight } from "./withAutoHeight";
|
||||
export { default as withClickEvent } from "./withClickEvent";
|
||||
export { default as withCommentID } from "./withCommentID";
|
||||
export { default as withSetCommentID } from "./withSetCommentID";
|
||||
export { default as withEventEmitter } from "./withEventEmitter";
|
||||
export {
|
||||
default as withIOSSafariWidthWorkaround,
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import qs from "query-string";
|
||||
|
||||
import { buildURL } from "../utils";
|
||||
import { Decorator } from "./";
|
||||
|
||||
const withCommentID: Decorator = pym => {
|
||||
// Remove the comment id from the query.
|
||||
pym.onMessage("view-all-comments", () => {
|
||||
const search = qs.stringify({
|
||||
...qs.parse(location.search),
|
||||
commentId: undefined,
|
||||
});
|
||||
|
||||
// Remove the commentId url param.
|
||||
const url = buildURL({ search });
|
||||
|
||||
// Change the url.
|
||||
window.history.replaceState({}, document.title, url);
|
||||
});
|
||||
|
||||
// Add the permalink comment id to the query.
|
||||
pym.onMessage("view-comment", (id: string) => {
|
||||
const search = qs.stringify({
|
||||
...qs.parse(location.search),
|
||||
commentId: id,
|
||||
});
|
||||
|
||||
// Remove the commentId url param.
|
||||
const url = buildURL({ search });
|
||||
|
||||
// Change the url.
|
||||
window.history.replaceState({}, document.title, url);
|
||||
});
|
||||
};
|
||||
|
||||
export default withCommentID;
|
||||
+7
-7
@@ -1,17 +1,17 @@
|
||||
import withCommentID from "./withCommentID";
|
||||
import withSetCommentID from "./withSetCommentID";
|
||||
|
||||
it("should add commentID", () => {
|
||||
const previousLocation = location.toString();
|
||||
const previousState = window.history.state;
|
||||
const fakePym = {
|
||||
onMessage: (type: string, callback: (id: string) => void) => {
|
||||
if (type === "view-comment") {
|
||||
if (type === "setCommentID") {
|
||||
callback("comment-id");
|
||||
}
|
||||
},
|
||||
};
|
||||
withCommentID(fakePym as any);
|
||||
expect(location.toString()).toBe("http://localhost/?commentId=comment-id");
|
||||
withSetCommentID(fakePym as any);
|
||||
expect(location.toString()).toBe("http://localhost/?commentID=comment-id");
|
||||
window.history.replaceState(previousState, document.title, previousLocation);
|
||||
});
|
||||
|
||||
@@ -21,16 +21,16 @@ it("should remove commentID", () => {
|
||||
window.history.replaceState(
|
||||
previousState,
|
||||
document.title,
|
||||
"http://localhost/?commentId=comment-id"
|
||||
"http://localhost/?commentID=comment-id"
|
||||
);
|
||||
const fakePym = {
|
||||
onMessage: (type: string, callback: () => void) => {
|
||||
if (type === "view-all-comments") {
|
||||
if (type === "setCommentID") {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
};
|
||||
withCommentID(fakePym as any);
|
||||
withSetCommentID(fakePym as any);
|
||||
expect(location.toString()).toBe("http://localhost/");
|
||||
window.history.replaceState(previousState, document.title, previousLocation);
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import qs from "query-string";
|
||||
|
||||
import { buildURL } from "../utils";
|
||||
import { Decorator } from "./";
|
||||
|
||||
const withSetCommentID: Decorator = pym => {
|
||||
// Add the permalink comment id to the query.
|
||||
pym.onMessage("setCommentID", (id: string) => {
|
||||
const search = qs.stringify({
|
||||
...qs.parse(location.search),
|
||||
commentID: id || undefined,
|
||||
});
|
||||
|
||||
// Remove the commentId url param.
|
||||
const url = buildURL({ search });
|
||||
|
||||
// Change the url.
|
||||
window.history.replaceState({}, document.title, url);
|
||||
});
|
||||
};
|
||||
|
||||
export default withSetCommentID;
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<body>
|
||||
<h1 style="text-align: center" }>Talk 5.0 – Embed Stream</h1>
|
||||
<div id="coralStreamEmbed"></div>
|
||||
<div id="coralStreamEmbed" style="max-width: 600px; margin: 0 auto"></div>
|
||||
<script>
|
||||
window.TalkEmbed = Talk.render(document.getElementById('coralStreamEmbed'));
|
||||
</script>
|
||||
|
||||
@@ -6,6 +6,7 @@ import createStreamInterface from "./Stream";
|
||||
export interface Config {
|
||||
assetID?: string;
|
||||
assetURL?: string;
|
||||
commentID?: string;
|
||||
rootURL?: string;
|
||||
id?: string;
|
||||
events?: (eventEmitter: EventEmitter2) => void;
|
||||
@@ -23,6 +24,7 @@ export function render(config: Config = {}) {
|
||||
return createStreamInterface({
|
||||
assetID: config.assetID || query.assetID,
|
||||
assetURL: config.assetURL || query.assetURL,
|
||||
commentID: config.commentID || query.commentID,
|
||||
id: config.id || "talk-embed-stream",
|
||||
rootURL: config.rootURL || location.origin,
|
||||
eventEmitter,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "recompose";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { withContext } from "../bootstrap";
|
||||
import { TalkContext, withContext } from "../bootstrap";
|
||||
|
||||
/**
|
||||
* createMutationContainer creates a HOC that
|
||||
@@ -18,10 +18,14 @@ import { withContext } from "../bootstrap";
|
||||
*/
|
||||
function createMutationContainer<T extends string, I, R>(
|
||||
propName: T,
|
||||
commit: (environment: Environment, input: I) => Promise<R>
|
||||
commit: (
|
||||
environment: Environment,
|
||||
input: I,
|
||||
context: TalkContext
|
||||
) => Promise<R>
|
||||
): InferableComponentEnhancer<{ [P in T]: (input: I) => Promise<R> }> {
|
||||
return compose(
|
||||
withContext(({ relayEnvironment }) => ({ relayEnvironment })),
|
||||
withContext(context => ({ context })),
|
||||
hoistStatics((BaseComponent: React.ComponentType<any>) => {
|
||||
class CreateMutationContainer extends React.Component<any> {
|
||||
public static displayName = wrapDisplayName(
|
||||
@@ -30,7 +34,11 @@ function createMutationContainer<T extends string, I, R>(
|
||||
);
|
||||
|
||||
private commit = (input: I) => {
|
||||
return commit(this.props.relayEnvironment, input);
|
||||
return commit(
|
||||
this.props.context.relayEnvironment,
|
||||
input,
|
||||
this.props.context
|
||||
);
|
||||
};
|
||||
|
||||
public render() {
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import * as React from "react";
|
||||
import { compose, hoistStatics, InferableComponentEnhancer } from "recompose";
|
||||
import {
|
||||
compose,
|
||||
hoistStatics,
|
||||
InferableComponentEnhancer,
|
||||
wrapDisplayName,
|
||||
} from "recompose";
|
||||
import {
|
||||
CSelector,
|
||||
CSnapshot,
|
||||
Disposable,
|
||||
Environment,
|
||||
GraphQLTaggedNode,
|
||||
} from "relay-runtime";
|
||||
@@ -36,6 +42,12 @@ function withLocalStateContainer<T>(
|
||||
withContext(({ relayEnvironment }) => ({ relayEnvironment })),
|
||||
hoistStatics((BaseComponent: React.ComponentType<any>) => {
|
||||
class LocalStateContainer extends React.Component<Props, any> {
|
||||
public static displayName = wrapDisplayName(
|
||||
BaseComponent,
|
||||
"withLocalStateContainer"
|
||||
);
|
||||
private subscription: Disposable;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const fragment = (fragmentSpec as any).data().default;
|
||||
@@ -53,7 +65,10 @@ function withLocalStateContainer<T>(
|
||||
variables: {},
|
||||
};
|
||||
const snapshot = props.relayEnvironment.lookup(selector);
|
||||
props.relayEnvironment.subscribe(snapshot, this.updateSnapshot);
|
||||
this.subscription = props.relayEnvironment.subscribe(
|
||||
snapshot,
|
||||
this.updateSnapshot
|
||||
);
|
||||
this.state = {
|
||||
data: snapshot.data,
|
||||
};
|
||||
@@ -63,6 +78,10 @@ function withLocalStateContainer<T>(
|
||||
this.setState({ data: snapshot.data });
|
||||
};
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.subscription.dispose();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { relayEnvironment: _, ...rest } = this.props;
|
||||
return <BaseComponent {...rest} local={this.state.data} />;
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { IResolvers } from "graphql-tools";
|
||||
import { createFetch } from "relay-local-schema";
|
||||
import {
|
||||
commitLocalUpdate,
|
||||
Environment,
|
||||
Network,
|
||||
RecordProxy,
|
||||
RecordSource,
|
||||
RecordSourceProxy,
|
||||
Store,
|
||||
} from "relay-runtime";
|
||||
|
||||
import {
|
||||
createAndRetain,
|
||||
LOCAL_ID,
|
||||
LOCAL_TYPE,
|
||||
wrapFetchWithLogger,
|
||||
} from "talk-framework/lib/relay";
|
||||
|
||||
import { loadSchema } from "talk-common/graphql";
|
||||
|
||||
export interface CreateRelayEnvironmentNetworkParams {
|
||||
/** project name of graphql-config */
|
||||
projectName: string;
|
||||
/** graphql resolvers */
|
||||
resolvers: IResolvers<any, any>;
|
||||
/** If enabled, graphql responses will be logged to the console */
|
||||
logNetwork?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateRelayEnvironmentParams {
|
||||
/** If set, creates a network to a local graphql server with a local schema */
|
||||
network?: CreateRelayEnvironmentNetworkParams;
|
||||
/** Allows to set initial state for Local state */
|
||||
initLocalState?: (
|
||||
local: RecordProxy,
|
||||
source: RecordSourceProxy,
|
||||
environment: Environment
|
||||
) => void;
|
||||
/** Use this source for creating the environment */
|
||||
source?: RecordSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* create Relay environment for tests environments.
|
||||
*/
|
||||
export default function createRelayEnvironment(
|
||||
params: CreateRelayEnvironmentParams = {}
|
||||
) {
|
||||
let network: Network = null as any;
|
||||
if (params.network) {
|
||||
const schema = loadSchema(
|
||||
params.network.projectName,
|
||||
params.network.resolvers
|
||||
);
|
||||
network = Network.create(
|
||||
wrapFetchWithLogger(createFetch({ schema }), params.network.logNetwork)
|
||||
);
|
||||
}
|
||||
const environment = new Environment({
|
||||
network,
|
||||
store: new Store(params.source || new RecordSource()),
|
||||
});
|
||||
commitLocalUpdate(environment, sourceProxy => {
|
||||
const root = sourceProxy.getRoot();
|
||||
const localRecord = createAndRetain(
|
||||
environment,
|
||||
sourceProxy,
|
||||
LOCAL_ID,
|
||||
LOCAL_TYPE
|
||||
);
|
||||
root.setLinkedRecord(localRecord, "local");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState!(localRecord, sourceProxy, environment);
|
||||
}
|
||||
});
|
||||
return environment;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
default as createRelayEnvironment,
|
||||
CreateRelayEnvironmentParams,
|
||||
} from "./createRelayEnvironment";
|
||||
@@ -11,4 +11,5 @@
|
||||
}
|
||||
|
||||
.root {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -5,17 +5,17 @@ import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import App from "./App";
|
||||
|
||||
it("renders correctly", () => {
|
||||
it("renders stream", () => {
|
||||
const props: PropTypesOf<typeof App> = {
|
||||
asset: {},
|
||||
showPermalinkView: false,
|
||||
};
|
||||
const wrapper = shallow(<App {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders correctly when asset is null", () => {
|
||||
it("renders permalink view", () => {
|
||||
const props: PropTypesOf<typeof App> = {
|
||||
asset: null,
|
||||
showPermalinkView: true,
|
||||
};
|
||||
const wrapper = shallow(<App {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
@@ -3,23 +3,26 @@ import { StatelessComponent } from "react";
|
||||
|
||||
import { Flex } from "talk-ui/components";
|
||||
|
||||
import StreamContainer from "../containers/StreamContainer";
|
||||
import PermalinkViewQuery from "../queries/PermalinkViewQuery";
|
||||
import StreamQuery from "../queries/StreamQuery";
|
||||
|
||||
import * as styles from "./App.css";
|
||||
|
||||
export interface AppProps {
|
||||
asset: {} | null;
|
||||
showPermalinkView: boolean;
|
||||
}
|
||||
|
||||
const App: StatelessComponent<AppProps> = props => {
|
||||
if (props.asset) {
|
||||
return (
|
||||
<Flex justifyContent="center" className={styles.root}>
|
||||
<StreamContainer asset={props.asset} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
return <div>Asset not found </div>;
|
||||
const view = props.showPermalinkView ? (
|
||||
<PermalinkViewQuery />
|
||||
) : (
|
||||
<StreamQuery />
|
||||
);
|
||||
return (
|
||||
<Flex justifyContent="center" className={styles.root}>
|
||||
{view}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.footer {
|
||||
margin-top: var(--spacing-unit);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import Comment from "./Comment";
|
||||
|
||||
it("renders username and body", () => {
|
||||
const props: PropTypesOf<typeof Comment> = {
|
||||
id: "comment-id",
|
||||
author: {
|
||||
username: "Marvin",
|
||||
},
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
|
||||
import { Typography } from "talk-ui/components";
|
||||
import * as styles from "./Comment.css";
|
||||
|
||||
import PermalinkButtonContainer from "../../containers/PermalinkButtonContainer";
|
||||
import Timestamp from "./Timestamp";
|
||||
import TopBar from "./TopBar";
|
||||
import Username from "./Username";
|
||||
|
||||
export interface CommentProps {
|
||||
id: string;
|
||||
className?: string;
|
||||
author: {
|
||||
username: string | null;
|
||||
} | null;
|
||||
@@ -24,6 +27,9 @@ const Comment: StatelessComponent<CommentProps> = props => {
|
||||
<Timestamp>{props.createdAt}</Timestamp>
|
||||
</TopBar>
|
||||
<Typography>{props.body}</Typography>
|
||||
<div className={styles.footer}>
|
||||
<PermalinkButtonContainer commentID={props.id} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,5 +15,12 @@ exports[`renders username and body 1`] = `
|
||||
<withPropsOnChange(Typography)>
|
||||
Woof
|
||||
</withPropsOnChange(Typography)>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
>
|
||||
<withContext(withLocalStateContainer(PermalinkContainer))
|
||||
commentID="comment-id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import * as React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
|
||||
import { Typography } from "talk-ui/components";
|
||||
|
||||
export interface LogoProps {
|
||||
className?: string;
|
||||
gutterBottom?: boolean;
|
||||
}
|
||||
|
||||
const Logo: StatelessComponent<LogoProps> = props => {
|
||||
return (
|
||||
<Localized id="stream-logo">
|
||||
<Typography variant="heading1" gutterBottom={props.gutterBottom}>
|
||||
Talk NEO
|
||||
</Typography>
|
||||
</Localized>
|
||||
);
|
||||
};
|
||||
|
||||
export default Logo;
|
||||
@@ -0,0 +1,4 @@
|
||||
.popover {
|
||||
width: 350px;
|
||||
max-width: 80%;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React from "react";
|
||||
import { oncePerFrame } from "talk-common/utils";
|
||||
import {
|
||||
Button,
|
||||
ButtonIcon,
|
||||
ClickOutside,
|
||||
MatchMedia,
|
||||
Popover,
|
||||
} from "talk-ui/components";
|
||||
|
||||
import * as styles from "./PermalinkButton.css";
|
||||
import PermalinkPopover from "./PermalinkPopover";
|
||||
|
||||
interface PermalinkProps {
|
||||
commentID: string;
|
||||
assetURL: string | null;
|
||||
}
|
||||
|
||||
class Permalink extends React.Component<PermalinkProps> {
|
||||
// Helper that prevents calling toggleVisibility more then once per frame.
|
||||
// In essence this means we'll process an event only once.
|
||||
// This might happen, when clicking on the button which will
|
||||
// cause its onClick to happen as well as onClickOutside.
|
||||
private toggleVisibilityOncePerFrame = oncePerFrame(
|
||||
(toggleVisibility: () => void) => toggleVisibility()
|
||||
);
|
||||
|
||||
public render() {
|
||||
const { commentID, assetURL } = this.props;
|
||||
const popoverID = "permalink-popover";
|
||||
return (
|
||||
<Popover
|
||||
id={popoverID}
|
||||
placement="top-start"
|
||||
description="A dialog showing a permalink to the comment"
|
||||
className={styles.popover}
|
||||
body={({ toggleVisibility }) => (
|
||||
<ClickOutside
|
||||
onClickOutside={() =>
|
||||
this.toggleVisibilityOncePerFrame(toggleVisibility)
|
||||
}
|
||||
>
|
||||
<PermalinkPopover
|
||||
permalinkURL={`${assetURL}&commentID=${commentID}`}
|
||||
toggleVisibility={toggleVisibility}
|
||||
/>
|
||||
</ClickOutside>
|
||||
)}
|
||||
>
|
||||
{({ toggleVisibility, forwardRef, visible }) => (
|
||||
<Button
|
||||
onClick={() => this.toggleVisibilityOncePerFrame(toggleVisibility)}
|
||||
aria-controls={popoverID}
|
||||
forwardRef={forwardRef}
|
||||
variant="ghost"
|
||||
active={visible}
|
||||
size="small"
|
||||
>
|
||||
<MatchMedia minWidth="xs">
|
||||
<ButtonIcon>share</ButtonIcon>
|
||||
</MatchMedia>
|
||||
<Localized id="comments-permalink-share">
|
||||
<span>Share</span>
|
||||
</Localized>
|
||||
</Button>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Permalink;
|
||||
@@ -0,0 +1,7 @@
|
||||
.root {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.textField {
|
||||
flex-grow: 1;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React from "react";
|
||||
import CopyToClipboard from "react-copy-to-clipboard";
|
||||
|
||||
import { Button, Flex, TextField } from "talk-ui/components";
|
||||
|
||||
import * as styles from "./PermalinkPopover.css";
|
||||
|
||||
interface InnerProps {
|
||||
permalinkURL: string;
|
||||
toggleVisibility: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
copied: boolean;
|
||||
}
|
||||
|
||||
class PermalinkPopover extends React.Component<InnerProps> {
|
||||
public state: State = {
|
||||
copied: false,
|
||||
};
|
||||
|
||||
private onCopy = async () => {
|
||||
await this.toggleCopied();
|
||||
setTimeout(() => {
|
||||
this.toggleCopied();
|
||||
}, 800);
|
||||
};
|
||||
|
||||
private toggleCopied = () => {
|
||||
this.setState((state: State) => ({
|
||||
copied: !state.copied,
|
||||
}));
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { permalinkURL } = this.props;
|
||||
const { copied } = this.state;
|
||||
return (
|
||||
<Flex itemGutter="half" className={styles.root}>
|
||||
<TextField defaultValue={permalinkURL} className={styles.textField} />
|
||||
<CopyToClipboard text={permalinkURL} onCopy={this.onCopy}>
|
||||
<Button color="primary" variant="filled" size="small">
|
||||
{copied ? (
|
||||
<Localized id="comments-permalink-copied">
|
||||
<span>Copied!</span>
|
||||
</Localized>
|
||||
) : (
|
||||
<Localized id="comments-permalink-copy">
|
||||
<span>Copy</span>
|
||||
</Localized>
|
||||
)}
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PermalinkPopover;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./PermalinkButton";
|
||||
@@ -0,0 +1,7 @@
|
||||
.root {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-bottom: calc(2 * var(--spacing-unit));
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
|
||||
import { Button, Flex, Typography } from "talk-ui/components";
|
||||
|
||||
import CommentContainer from "../containers/CommentContainer";
|
||||
import * as styles from "./PermalinkView.css";
|
||||
|
||||
export interface PermalinkViewProps {
|
||||
comment: {} | null;
|
||||
assetURL: string | null;
|
||||
onShowAllComments: () => void;
|
||||
}
|
||||
|
||||
const PermalinkView: StatelessComponent<PermalinkViewProps> = ({
|
||||
assetURL,
|
||||
comment,
|
||||
onShowAllComments,
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
{assetURL && (
|
||||
<Button
|
||||
id="talk-comments-permalinkView-showAllComments"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={onShowAllComments}
|
||||
className={styles.button}
|
||||
fullWidth
|
||||
>
|
||||
Show all Comments
|
||||
</Button>
|
||||
)}
|
||||
{!comment && <Typography>Comment not found</Typography>}
|
||||
{comment && (
|
||||
<Flex direction="column">
|
||||
<CommentContainer data={comment} />
|
||||
</Flex>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PermalinkView;
|
||||
@@ -1,4 +1,3 @@
|
||||
.root {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
@@ -7,22 +7,20 @@ import { Button, Flex } from "talk-ui/components";
|
||||
import CommentContainer from "../containers/CommentContainer";
|
||||
import PostCommentFormContainer from "../containers/PostCommentFormContainer";
|
||||
import ReplyListContainer from "../containers/ReplyListContainer";
|
||||
import Logo from "./Logo";
|
||||
import * as styles from "./Stream.css";
|
||||
|
||||
export interface StreamProps {
|
||||
assetID: string;
|
||||
isClosed: boolean;
|
||||
isClosed?: boolean;
|
||||
comments: ReadonlyArray<{ id: string }>;
|
||||
onLoadMore: () => void;
|
||||
hasMore: boolean;
|
||||
disableLoadMore: boolean;
|
||||
onLoadMore?: () => void;
|
||||
hasMore?: boolean;
|
||||
disableLoadMore?: boolean;
|
||||
}
|
||||
|
||||
const Stream: StatelessComponent<StreamProps> = props => {
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<Logo gutterBottom />
|
||||
<PostCommentFormContainer assetID={props.assetID} />
|
||||
<Flex
|
||||
direction="column"
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
exports[`renders permalink view 1`] = `
|
||||
<withPropsOnChange(Flex)
|
||||
className="App-root"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Relay(StreamContainer)
|
||||
asset={Object {}}
|
||||
/>
|
||||
<withContext(withLocalStateContainer(PermalinkViewQuery)) />
|
||||
</withPropsOnChange(Flex)>
|
||||
`;
|
||||
|
||||
exports[`renders correctly when asset is null 1`] = `
|
||||
<div>
|
||||
Asset not found
|
||||
</div>
|
||||
exports[`renders stream 1`] = `
|
||||
<withPropsOnChange(Flex)
|
||||
className="App-root"
|
||||
justifyContent="center"
|
||||
>
|
||||
<withContext(withLocalStateContainer(StreamQuery)) />
|
||||
</withPropsOnChange(Flex)>
|
||||
`;
|
||||
|
||||
@@ -4,9 +4,6 @@ exports[`renders correctly 1`] = `
|
||||
<div
|
||||
className="Stream-root"
|
||||
>
|
||||
<Logo
|
||||
gutterBottom={true}
|
||||
/>
|
||||
<withContext(createMutationContainer(PostCommentFormContainer))
|
||||
assetID="asset-id"
|
||||
/>
|
||||
@@ -65,9 +62,6 @@ exports[`when there is more disables load more button 1`] = `
|
||||
<div
|
||||
className="Stream-root"
|
||||
>
|
||||
<Logo
|
||||
gutterBottom={true}
|
||||
/>
|
||||
<withContext(createMutationContainer(PostCommentFormContainer))
|
||||
assetID="asset-id"
|
||||
/>
|
||||
@@ -140,9 +134,6 @@ exports[`when there is more renders a load more button 1`] = `
|
||||
<div
|
||||
className="Stream-root"
|
||||
>
|
||||
<Logo
|
||||
gutterBottom={true}
|
||||
/>
|
||||
<withContext(createMutationContainer(PostCommentFormContainer))
|
||||
assetID="asset-id"
|
||||
/>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import { AppContainer } from "./AppContainer";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof AppContainer> = {
|
||||
data: {
|
||||
asset: {},
|
||||
},
|
||||
};
|
||||
const wrapper = shallow(<AppContainer {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
@@ -1,30 +1,27 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
import * as React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
|
||||
import { withFragmentContainer } from "talk-framework/lib/relay";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { AppContainer as Data } from "talk-stream/__generated__/AppContainer.graphql";
|
||||
import { graphql, withLocalStateContainer } from "talk-framework/lib/relay";
|
||||
import { AppContainerLocal as Local } from "talk-stream/__generated__/AppContainerLocal.graphql";
|
||||
|
||||
import App from "../components/App";
|
||||
|
||||
interface InnerProps {
|
||||
data: Data;
|
||||
local: Local;
|
||||
}
|
||||
|
||||
export const AppContainer: StatelessComponent<InnerProps> = props => {
|
||||
return <App {...props.data} />;
|
||||
const AppContainer: StatelessComponent<InnerProps> = ({
|
||||
local: { commentID },
|
||||
}) => {
|
||||
return <App showPermalinkView={!!commentID} />;
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<{ data: Data }>({
|
||||
data: graphql`
|
||||
fragment AppContainer on Query
|
||||
@argumentDefinitions(assetID: { type: "ID!" }) {
|
||||
asset(id: $assetID) {
|
||||
...StreamContainer_asset
|
||||
}
|
||||
const enhanced = withLocalStateContainer<Local>(
|
||||
graphql`
|
||||
fragment AppContainerLocal on Local {
|
||||
commentID
|
||||
}
|
||||
`,
|
||||
})(AppContainer);
|
||||
`
|
||||
)(AppContainer);
|
||||
|
||||
export type AppContainerProps = PropTypesOf<typeof enhanced>;
|
||||
export default enhanced;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { CommentContainer } from "./CommentContainer";
|
||||
it("renders username and body", () => {
|
||||
const props: PropTypesOf<typeof CommentContainer> = {
|
||||
data: {
|
||||
id: "comment-id",
|
||||
author: {
|
||||
username: "Marvin",
|
||||
},
|
||||
@@ -23,6 +24,7 @@ it("renders username and body", () => {
|
||||
it("renders body only", () => {
|
||||
const props: PropTypesOf<typeof CommentContainer> = {
|
||||
data: {
|
||||
id: "comment-id",
|
||||
author: {
|
||||
username: null,
|
||||
},
|
||||
|
||||
@@ -2,16 +2,19 @@ import React, { StatelessComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import withFragmentContainer from "talk-framework/lib/relay/withFragmentContainer";
|
||||
import { Omit, PropTypesOf } from "talk-framework/types";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { CommentContainer as Data } from "talk-stream/__generated__/CommentContainer.graphql";
|
||||
|
||||
import Comment, { CommentProps } from "../components/Comment";
|
||||
import Comment from "../components/Comment";
|
||||
|
||||
type InnerProps = { data: Data } & Omit<CommentProps, keyof Data>;
|
||||
interface InnerProps {
|
||||
data: Data;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
graphql`
|
||||
fragment CommentContainer_comment on Comment {
|
||||
id
|
||||
author {
|
||||
username
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
import { withLocalStateContainer } from "talk-framework/lib/relay";
|
||||
import { PermalinkButtonContainerLocal as Local } from "talk-stream/__generated__/PermalinkButtonContainerLocal.graphql";
|
||||
|
||||
import PermalinkButton from "../components/PermalinkButton";
|
||||
|
||||
interface InnerProps {
|
||||
local: Local;
|
||||
commentID: string;
|
||||
}
|
||||
|
||||
export const PermalinkContainer: StatelessComponent<InnerProps> = ({
|
||||
local,
|
||||
commentID,
|
||||
}) => {
|
||||
return local.assetURL ? (
|
||||
<PermalinkButton assetURL={local.assetURL} commentID={commentID} />
|
||||
) : null;
|
||||
};
|
||||
|
||||
const enhanced = withLocalStateContainer<Local>(
|
||||
graphql`
|
||||
fragment PermalinkButtonContainerLocal on Local {
|
||||
assetURL
|
||||
}
|
||||
`
|
||||
)(PermalinkContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from "react";
|
||||
import { graphql } from "react-relay";
|
||||
import { withFragmentContainer } from "talk-framework/lib/relay";
|
||||
import { PermalinkViewContainer_asset as AssetData } from "talk-stream/__generated__/PermalinkViewContainer_asset.graphql";
|
||||
import { PermalinkViewContainer_comment as CommentData } from "talk-stream/__generated__/PermalinkViewContainer_comment.graphql";
|
||||
import {
|
||||
SetCommentIDMutation,
|
||||
withSetCommentIDMutation,
|
||||
} from "talk-stream/mutations";
|
||||
import PermalinkView from "../components/PermalinkView";
|
||||
|
||||
interface PermalinkViewContainerProps {
|
||||
comment: CommentData | null;
|
||||
asset: AssetData | null;
|
||||
setCommentID: SetCommentIDMutation;
|
||||
}
|
||||
|
||||
class PermalinkViewContainer extends React.Component<
|
||||
PermalinkViewContainerProps
|
||||
> {
|
||||
private showAllComments = () => {
|
||||
this.props.setCommentID({ id: null });
|
||||
};
|
||||
public render() {
|
||||
const { comment, asset } = this.props;
|
||||
return (
|
||||
<PermalinkView
|
||||
comment={comment}
|
||||
assetURL={(asset && asset.url) || null}
|
||||
onShowAllComments={this.showAllComments}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withSetCommentIDMutation(
|
||||
withFragmentContainer<{
|
||||
comment: CommentData | null;
|
||||
asset: AssetData | null;
|
||||
}>({
|
||||
comment: graphql`
|
||||
fragment PermalinkViewContainer_comment on Comment {
|
||||
...CommentContainer
|
||||
}
|
||||
`,
|
||||
asset: graphql`
|
||||
fragment PermalinkViewContainer_asset on Asset {
|
||||
url
|
||||
}
|
||||
`,
|
||||
})(PermalinkViewContainer)
|
||||
);
|
||||
|
||||
export default enhanced;
|
||||
@@ -1,7 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<App
|
||||
asset={Object {}}
|
||||
/>
|
||||
`;
|
||||
@@ -9,6 +9,7 @@ exports[`renders body only 1`] = `
|
||||
}
|
||||
body="Woof"
|
||||
createdAt="1995-12-17T03:24:00.000Z"
|
||||
id="comment-id"
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -21,5 +22,6 @@ exports[`renders username and body 1`] = `
|
||||
}
|
||||
body="Woof"
|
||||
createdAt="1995-12-17T03:24:00.000Z"
|
||||
id="comment-id"
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
TalkContextProvider,
|
||||
} from "talk-framework/lib/bootstrap";
|
||||
|
||||
import AppContainer from "./containers/AppContainer";
|
||||
import { initLocalState } from "./local";
|
||||
import localesData from "./locales";
|
||||
import AppQuery from "./queries/AppQuery";
|
||||
|
||||
// This is called when the context is first initialized.
|
||||
async function init({ relayEnvironment }: TalkContext) {
|
||||
@@ -29,7 +29,7 @@ async function main() {
|
||||
|
||||
const Index: StatelessComponent = () => (
|
||||
<TalkContextProvider value={context}>
|
||||
<AppQuery />
|
||||
<AppContainer />
|
||||
</TalkContextProvider>
|
||||
);
|
||||
|
||||
|
||||
@@ -21,10 +21,23 @@ export default async function initLocalState(environment: Environment) {
|
||||
|
||||
// Parse query params
|
||||
const query = qs.parse(location.search);
|
||||
|
||||
if (query.assetID) {
|
||||
localRecord.setValue(query.assetID, "assetID");
|
||||
}
|
||||
|
||||
// Saving location host for permalink until we get the asset url - the url now points to the tenant
|
||||
if (location && query.assetID) {
|
||||
localRecord.setValue(
|
||||
`${location.origin}/?assetID=${query.assetID}`,
|
||||
"assetURL"
|
||||
);
|
||||
}
|
||||
|
||||
if (query.commentID) {
|
||||
localRecord.setValue(query.commentID, "commentID");
|
||||
}
|
||||
|
||||
// Create network Record
|
||||
const networkRecord = createAndRetain(
|
||||
environment,
|
||||
|
||||
@@ -6,6 +6,8 @@ type Network {
|
||||
type Local {
|
||||
network: Network!
|
||||
assetID: String
|
||||
commentID: String
|
||||
assetURL: String
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Environment, RecordSource } from "relay-runtime";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
import { createRelayEnvironment } from "talk-framework/testHelpers";
|
||||
|
||||
import { commit } from "./SetCommentIDMutation";
|
||||
|
||||
let environment: Environment;
|
||||
const source: RecordSource = new RecordSource();
|
||||
|
||||
beforeAll(() => {
|
||||
environment = createRelayEnvironment({
|
||||
source,
|
||||
});
|
||||
});
|
||||
|
||||
it("Sets comment id", () => {
|
||||
const id = "comment1-id";
|
||||
commit(environment, { id }, {} as any);
|
||||
expect(source.get(LOCAL_ID)!.commentID).toEqual(id);
|
||||
});
|
||||
|
||||
it("Should call setCommentID in pym", async () => {
|
||||
const id = "comment2-id";
|
||||
const context = {
|
||||
pym: {
|
||||
sendMessage: sinon
|
||||
.mock()
|
||||
.once()
|
||||
.withArgs("setCommentID", id),
|
||||
},
|
||||
};
|
||||
commit(environment, { id }, context as any);
|
||||
await timeout();
|
||||
expect(source.get(LOCAL_ID)!.commentID).toEqual(id);
|
||||
context.pym.sendMessage.verify();
|
||||
});
|
||||
|
||||
it("Should call setCommentID in pym with empty id", async () => {
|
||||
const context = {
|
||||
pym: {
|
||||
sendMessage: sinon
|
||||
.mock()
|
||||
.once()
|
||||
.withArgs("setCommentID", ""),
|
||||
},
|
||||
};
|
||||
commit(environment, { id: null }, context as any);
|
||||
await timeout();
|
||||
expect(source.get(LOCAL_ID)!.commentID).toEqual(null);
|
||||
context.pym.sendMessage.verify();
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { commitLocalUpdate, Environment } from "relay-runtime";
|
||||
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { createMutationContainer } from "talk-framework/lib/relay";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay/withLocalStateContainer";
|
||||
|
||||
export interface SetCommentIDInput {
|
||||
id: string | null;
|
||||
}
|
||||
|
||||
export type SetCommentIDMutation = (input: SetCommentIDInput) => Promise<void>;
|
||||
|
||||
export async function commit(
|
||||
environment: Environment,
|
||||
input: SetCommentIDInput,
|
||||
{ pym }: TalkContext
|
||||
) {
|
||||
return commitLocalUpdate(environment, store => {
|
||||
const record = store.get(LOCAL_ID)!;
|
||||
record.setValue(input.id, "commentID");
|
||||
if (pym) {
|
||||
// This sets the comment id on the parent url.
|
||||
pym.sendMessage("setCommentID", input.id || "");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const withSetCommentIDMutation = createMutationContainer(
|
||||
"setCommentID",
|
||||
commit
|
||||
);
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Environment, RecordSource } from "relay-runtime";
|
||||
|
||||
import { createRelayEnvironment } from "talk-framework/testHelpers";
|
||||
|
||||
import { NETWORK_ID, NETWORK_TYPE } from "../local";
|
||||
|
||||
import { commit } from "./SetNetworkStatusMutation";
|
||||
|
||||
let environment: Environment;
|
||||
const source: RecordSource = new RecordSource();
|
||||
|
||||
beforeAll(() => {
|
||||
environment = createRelayEnvironment({
|
||||
source,
|
||||
initLocalState: (localRecord, sourceProxy) => {
|
||||
const networkRecord = sourceProxy.create(NETWORK_ID, NETWORK_TYPE);
|
||||
networkRecord.setValue(false, "isOffline");
|
||||
localRecord.setLinkedRecord(networkRecord, "network");
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("Sets comment id", () => {
|
||||
commit(environment, { isOffline: true });
|
||||
expect(source.get(NETWORK_ID)!.isOffline).toEqual(true);
|
||||
});
|
||||
@@ -8,9 +8,14 @@ export interface SetNetworkStatusInput {
|
||||
isOffline: boolean;
|
||||
}
|
||||
|
||||
export type SetNetworkStatusMutation = (input: SetNetworkStatusInput) => void;
|
||||
export type SetNetworkStatusMutation = (
|
||||
input: SetNetworkStatusInput
|
||||
) => Promise<void>;
|
||||
|
||||
async function commit(environment: Environment, input: SetNetworkStatusInput) {
|
||||
export async function commit(
|
||||
environment: Environment,
|
||||
input: SetNetworkStatusInput
|
||||
) {
|
||||
return commitLocalUpdate(environment, store => {
|
||||
const record = store.get(NETWORK_ID)!;
|
||||
record.setValue(input.isOffline, "isOffline");
|
||||
|
||||
@@ -1,2 +1,15 @@
|
||||
export * from "./CreateCommentMutation";
|
||||
export * from "./SetNetworkStatusMutation";
|
||||
export {
|
||||
withCreateCommentMutation,
|
||||
CreateCommentMutation,
|
||||
CreateCommentInput,
|
||||
} from "./CreateCommentMutation";
|
||||
export {
|
||||
withSetNetworkStatusMutation,
|
||||
SetNetworkStatusMutation,
|
||||
SetNetworkStatusInput,
|
||||
} from "./SetNetworkStatusMutation";
|
||||
export {
|
||||
withSetCommentIDMutation,
|
||||
SetCommentIDMutation,
|
||||
SetCommentIDInput,
|
||||
} from "./SetCommentIDMutation";
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
import { ReadyState } from "react-relay";
|
||||
|
||||
import {
|
||||
graphql,
|
||||
QueryRenderer,
|
||||
withLocalStateContainer,
|
||||
} from "talk-framework/lib/relay";
|
||||
import {
|
||||
AppQueryResponse,
|
||||
AppQueryVariables,
|
||||
} from "talk-stream/__generated__/AppQuery.graphql";
|
||||
import { AppQueryLocal as Local } from "talk-stream/__generated__/AppQueryLocal.graphql";
|
||||
|
||||
import AppContainer from "../containers/AppContainer";
|
||||
|
||||
export const render = ({ error, props }: ReadyState<AppQueryResponse>) => {
|
||||
if (error) {
|
||||
return <div>{error.message}</div>;
|
||||
}
|
||||
if (props) {
|
||||
return <AppContainer data={props} />;
|
||||
}
|
||||
return <div>Loading</div>;
|
||||
};
|
||||
|
||||
interface InnerProps {
|
||||
local: Local;
|
||||
}
|
||||
|
||||
const AppQuery: StatelessComponent<InnerProps> = props => {
|
||||
return (
|
||||
<QueryRenderer<AppQueryVariables, AppQueryResponse>
|
||||
query={graphql`
|
||||
query AppQuery($assetID: ID!) {
|
||||
...AppContainer @arguments(assetID: $assetID)
|
||||
}
|
||||
`}
|
||||
variables={{
|
||||
assetID: props.local.assetID,
|
||||
}}
|
||||
render={render}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withLocalStateContainer<Local>(
|
||||
graphql`
|
||||
fragment AppQueryLocal on Local {
|
||||
assetID
|
||||
}
|
||||
`
|
||||
)(AppQuery);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
|
||||
import { render } from "./PermalinkViewQuery";
|
||||
|
||||
it("renders permalink view container", () => {
|
||||
const data = {
|
||||
props: {
|
||||
asset: {},
|
||||
comment: {},
|
||||
} as any,
|
||||
error: null,
|
||||
};
|
||||
const wrapper = shallow(React.createElement(() => render(data)));
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders loading", () => {
|
||||
const data = {
|
||||
props: null,
|
||||
error: null,
|
||||
};
|
||||
const wrapper = shallow(React.createElement(() => render(data)));
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders error", () => {
|
||||
const data = {
|
||||
props: null,
|
||||
error: new Error("error"),
|
||||
};
|
||||
const wrapper = shallow(React.createElement(() => render(data)));
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import * as React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
import { ReadyState } from "react-relay";
|
||||
|
||||
import {
|
||||
graphql,
|
||||
QueryRenderer,
|
||||
withLocalStateContainer,
|
||||
} from "talk-framework/lib/relay";
|
||||
import {
|
||||
PermalinkViewQueryResponse,
|
||||
PermalinkViewQueryVariables,
|
||||
} from "talk-stream/__generated__/PermalinkViewQuery.graphql";
|
||||
import { PermalinkViewQueryLocal as Local } from "talk-stream/__generated__/PermalinkViewQueryLocal.graphql";
|
||||
|
||||
import PermalinkViewContainer from "../containers/PermalinkViewContainer";
|
||||
|
||||
interface InnerProps {
|
||||
local: Local;
|
||||
}
|
||||
|
||||
export const render = ({
|
||||
error,
|
||||
props,
|
||||
}: ReadyState<PermalinkViewQueryResponse>) => {
|
||||
if (error) {
|
||||
return <div>{error.message}</div>;
|
||||
}
|
||||
if (props) {
|
||||
return (
|
||||
<PermalinkViewContainer asset={props.asset} comment={props.comment} />
|
||||
);
|
||||
}
|
||||
return <div>Loading</div>;
|
||||
};
|
||||
|
||||
const PermalinkViewQuery: StatelessComponent<InnerProps> = ({
|
||||
local: { commentID, assetID },
|
||||
}) => (
|
||||
<QueryRenderer<PermalinkViewQueryVariables, PermalinkViewQueryResponse>
|
||||
query={graphql`
|
||||
query PermalinkViewQuery($assetID: ID!, $commentID: ID!) {
|
||||
asset(id: $assetID) {
|
||||
...PermalinkViewContainer_asset
|
||||
}
|
||||
comment(id: $commentID) {
|
||||
...PermalinkViewContainer_comment
|
||||
}
|
||||
}
|
||||
`}
|
||||
variables={{
|
||||
assetID,
|
||||
commentID,
|
||||
}}
|
||||
render={render}
|
||||
/>
|
||||
);
|
||||
|
||||
const enhanced = withLocalStateContainer<Local>(
|
||||
graphql`
|
||||
fragment PermalinkViewQueryLocal on Local {
|
||||
assetID
|
||||
commentID
|
||||
}
|
||||
`
|
||||
)(PermalinkViewQuery);
|
||||
|
||||
export default enhanced;
|
||||
+5
-3
@@ -1,11 +1,13 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
|
||||
import { render } from "./AppQuery";
|
||||
import { render } from "./StreamQuery";
|
||||
|
||||
it("renders app", () => {
|
||||
it("renders stream container", () => {
|
||||
const data = {
|
||||
props: {} as any,
|
||||
props: {
|
||||
asset: {},
|
||||
} as any,
|
||||
error: null,
|
||||
};
|
||||
const wrapper = shallow(React.createElement(() => render(data)));
|
||||
@@ -0,0 +1,58 @@
|
||||
import * as React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
import { ReadyState } from "react-relay";
|
||||
|
||||
import {
|
||||
graphql,
|
||||
QueryRenderer,
|
||||
withLocalStateContainer,
|
||||
} from "talk-framework/lib/relay";
|
||||
import {
|
||||
StreamQueryResponse,
|
||||
StreamQueryVariables,
|
||||
} from "talk-stream/__generated__/StreamQuery.graphql";
|
||||
import { StreamQueryLocal as Local } from "talk-stream/__generated__/StreamQueryLocal.graphql";
|
||||
|
||||
import StreamContainer from "../containers/StreamContainer";
|
||||
|
||||
interface InnerProps {
|
||||
local: Local;
|
||||
}
|
||||
|
||||
export const render = ({ error, props }: ReadyState<StreamQueryResponse>) => {
|
||||
if (error) {
|
||||
return <div>{error.message}</div>;
|
||||
}
|
||||
if (props) {
|
||||
return <StreamContainer asset={props.asset} />;
|
||||
}
|
||||
return <div>Loading</div>;
|
||||
};
|
||||
|
||||
const StreamQuery: StatelessComponent<InnerProps> = ({
|
||||
local: { assetID },
|
||||
}) => (
|
||||
<QueryRenderer<StreamQueryVariables, StreamQueryResponse>
|
||||
query={graphql`
|
||||
query StreamQuery($assetID: ID!) {
|
||||
asset(id: $assetID) {
|
||||
...StreamContainer_asset
|
||||
}
|
||||
}
|
||||
`}
|
||||
variables={{
|
||||
assetID,
|
||||
}}
|
||||
render={render}
|
||||
/>
|
||||
);
|
||||
|
||||
const enhanced = withLocalStateContainer<Local>(
|
||||
graphql`
|
||||
fragment StreamQueryLocal on Local {
|
||||
assetID
|
||||
}
|
||||
`
|
||||
)(StreamQuery);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,20 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders error 1`] = `
|
||||
<div>
|
||||
error
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders loading 1`] = `
|
||||
<div>
|
||||
Loading
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders permalink view container 1`] = `
|
||||
<withContext(createMutationContainer(Relay(PermalinkViewContainer)))
|
||||
asset={Object {}}
|
||||
comment={Object {}}
|
||||
/>
|
||||
`;
|
||||
+6
-6
@@ -1,11 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders app 1`] = `
|
||||
<Relay(AppContainer)
|
||||
data={Object {}}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`renders error 1`] = `
|
||||
<div>
|
||||
error
|
||||
@@ -17,3 +11,9 @@ exports[`renders loading 1`] = `
|
||||
Loading
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders stream container 1`] = `
|
||||
<Relay(StreamContainer)
|
||||
asset={Object {}}
|
||||
/>
|
||||
`;
|
||||
@@ -7,11 +7,6 @@ exports[`loads more comments 1`] = `
|
||||
<div
|
||||
className="Stream-root"
|
||||
>
|
||||
<h1
|
||||
className="Typography-root Typography-heading1 Typography-colorTextPrimary Typography-gutterBottom"
|
||||
>
|
||||
Talk NEO
|
||||
</h1>
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
@@ -74,6 +69,9 @@ exports[`loads more comments 1`] = `
|
||||
>
|
||||
Joining Too
|
||||
</p>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -103,6 +101,9 @@ exports[`loads more comments 1`] = `
|
||||
>
|
||||
What's up?
|
||||
</p>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -132,6 +133,9 @@ exports[`loads more comments 1`] = `
|
||||
>
|
||||
Hey!
|
||||
</p>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -146,11 +150,6 @@ exports[`renders comment stream 1`] = `
|
||||
<div
|
||||
className="Stream-root"
|
||||
>
|
||||
<h1
|
||||
className="Typography-root Typography-heading1 Typography-colorTextPrimary Typography-gutterBottom"
|
||||
>
|
||||
Talk NEO
|
||||
</h1>
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
@@ -213,6 +212,9 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
Joining Too
|
||||
</p>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -242,6 +244,9 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
What's up?
|
||||
</p>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders permalink view 1`] = `
|
||||
<div
|
||||
className="Flex-root App-root Flex-justifyCenter Flex-alignCenter"
|
||||
>
|
||||
<div
|
||||
className="PermalinkView-root"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root PermalinkView-button Button-sizeRegular Button-colorPrimary Button-variantOutlined Button-fullWidth"
|
||||
id="talk-comments-permalinkView-showAllComments"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
>
|
||||
Show all Comments
|
||||
</button>
|
||||
<div
|
||||
className="Flex-root Flex-alignFlexStart Flex-directionColumn"
|
||||
>
|
||||
<div
|
||||
role="article"
|
||||
>
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
>
|
||||
<span
|
||||
className="Typography-root Typography-heading3 Typography-colorTextPrimary Username-root"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
Joining Too
|
||||
</p>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`show all comments 1`] = `
|
||||
<div
|
||||
className="Flex-root App-root Flex-justifyCenter Flex-alignCenter"
|
||||
>
|
||||
<div
|
||||
className="Stream-root"
|
||||
>
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div>
|
||||
<textarea
|
||||
className="PostCommentForm-textarea"
|
||||
name="body"
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="PostCommentForm-postButtonContainer"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
>
|
||||
Post
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="Flex-root Flex-itemGutter Flex-alignFlexStart Flex-directionColumn"
|
||||
id="talk-comments-stream-log"
|
||||
role="log"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-itemGutter Flex-alignFlexStart Flex-directionColumn"
|
||||
>
|
||||
<div
|
||||
role="article"
|
||||
>
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
>
|
||||
<span
|
||||
className="Typography-root Typography-heading3 Typography-colorTextPrimary Username-root"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
Joining Too
|
||||
</p>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,17 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders permalink view with unknown asset 1`] = `
|
||||
<div
|
||||
className="Flex-root App-root Flex-justifyCenter Flex-alignCenter"
|
||||
>
|
||||
<div
|
||||
className="PermalinkView-root"
|
||||
>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
Comment not found
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,109 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders permalink view with unknown comment 1`] = `
|
||||
<div
|
||||
className="Flex-root App-root Flex-justifyCenter Flex-alignCenter"
|
||||
>
|
||||
<div
|
||||
className="PermalinkView-root"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root PermalinkView-button Button-sizeRegular Button-colorPrimary Button-variantOutlined Button-fullWidth"
|
||||
id="talk-comments-permalinkView-showAllComments"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
>
|
||||
Show all Comments
|
||||
</button>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
Comment not found
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`show all comments 1`] = `
|
||||
<div
|
||||
className="Flex-root App-root Flex-justifyCenter Flex-alignCenter"
|
||||
>
|
||||
<div
|
||||
className="Stream-root"
|
||||
>
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div>
|
||||
<textarea
|
||||
className="PostCommentForm-textarea"
|
||||
name="body"
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="PostCommentForm-postButtonContainer"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
>
|
||||
Post
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="Flex-root Flex-itemGutter Flex-alignFlexStart Flex-directionColumn"
|
||||
id="talk-comments-stream-log"
|
||||
role="log"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-itemGutter Flex-alignFlexStart Flex-directionColumn"
|
||||
>
|
||||
<div
|
||||
role="article"
|
||||
>
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
>
|
||||
<span
|
||||
className="Typography-root Typography-heading3 Typography-colorTextPrimary Username-root"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
Joining Too
|
||||
</p>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -7,11 +7,6 @@ exports[`renders comment stream 1`] = `
|
||||
<div
|
||||
className="Stream-root"
|
||||
>
|
||||
<h1
|
||||
className="Typography-root Typography-heading1 Typography-colorTextPrimary Typography-gutterBottom"
|
||||
>
|
||||
Talk NEO
|
||||
</h1>
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
@@ -74,6 +69,9 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
Joining Too
|
||||
</p>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -103,6 +101,9 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
I like yoghurt
|
||||
</p>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="Indent-root Indent-level0"
|
||||
@@ -136,6 +137,9 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
Joining Too
|
||||
</p>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
role="article"
|
||||
@@ -161,6 +165,9 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
What's up?
|
||||
</p>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,6 @@ exports[`renders comment stream 1`] = `
|
||||
<div
|
||||
className="Stream-root"
|
||||
>
|
||||
<h1
|
||||
className="Typography-root Typography-heading1 Typography-colorTextPrimary Typography-gutterBottom"
|
||||
>
|
||||
Talk NEO
|
||||
</h1>
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
@@ -74,6 +69,9 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
Joining Too
|
||||
</p>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -103,6 +101,9 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
What's up?
|
||||
</p>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,6 @@ exports[`renders comment stream 1`] = `
|
||||
<div
|
||||
className="Stream-root"
|
||||
>
|
||||
<h1
|
||||
className="Typography-root Typography-heading1 Typography-colorTextPrimary Typography-gutterBottom"
|
||||
>
|
||||
Talk NEO
|
||||
</h1>
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
@@ -74,6 +69,9 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
Joining Too
|
||||
</p>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="Indent-root Indent-level0"
|
||||
@@ -107,6 +105,9 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
What's up?
|
||||
</p>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
aria-controls="talk-comments-replyList-log--comment-0"
|
||||
@@ -138,11 +139,6 @@ exports[`show all replies 1`] = `
|
||||
<div
|
||||
className="Stream-root"
|
||||
>
|
||||
<h1
|
||||
className="Typography-root Typography-heading1 Typography-colorTextPrimary Typography-gutterBottom"
|
||||
>
|
||||
Talk NEO
|
||||
</h1>
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
@@ -205,6 +201,9 @@ exports[`show all replies 1`] = `
|
||||
>
|
||||
Joining Too
|
||||
</p>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="Indent-root Indent-level0"
|
||||
@@ -238,6 +237,9 @@ exports[`show all replies 1`] = `
|
||||
>
|
||||
What's up?
|
||||
</p>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
role="article"
|
||||
@@ -263,6 +265,9 @@ exports[`show all replies 1`] = `
|
||||
>
|
||||
Hey!
|
||||
</p>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { IResolvers } from "graphql-tools";
|
||||
import { createFetch } from "relay-local-schema";
|
||||
import {
|
||||
commitLocalUpdate,
|
||||
Environment,
|
||||
Network,
|
||||
RecordProxy,
|
||||
RecordSource,
|
||||
Store,
|
||||
} from "relay-runtime";
|
||||
|
||||
import { loadSchema } from "talk-common/graphql";
|
||||
import {
|
||||
createAndRetain,
|
||||
LOCAL_ID,
|
||||
LOCAL_TYPE,
|
||||
wrapFetchWithLogger,
|
||||
} from "talk-framework/lib/relay";
|
||||
|
||||
export interface CreateEnvironmentParams {
|
||||
/** graphql resolvers */
|
||||
resolvers: IResolvers<any, any>;
|
||||
/** Allows to set initial state for Local state */
|
||||
initLocalState?: (local: RecordProxy) => void;
|
||||
/** If enabled, graphql responses will be logged to the console */
|
||||
logNetwork?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* create Relay environment for integration tests.
|
||||
*/
|
||||
export default function createEnvironment(params: CreateEnvironmentParams) {
|
||||
const schema = loadSchema("tenant", params.resolvers);
|
||||
const environment = new Environment({
|
||||
network: Network.create(
|
||||
wrapFetchWithLogger(createFetch({ schema }), params.logNetwork)
|
||||
),
|
||||
store: new Store(new RecordSource()),
|
||||
});
|
||||
if (params.initLocalState) {
|
||||
commitLocalUpdate(environment, s => {
|
||||
const root = s.getRoot();
|
||||
const localRecord = createAndRetain(environment, s, LOCAL_ID, LOCAL_TYPE);
|
||||
root.setLinkedRecord(localRecord, "local");
|
||||
params.initLocalState!(localRecord);
|
||||
});
|
||||
}
|
||||
return environment;
|
||||
}
|
||||
@@ -37,6 +37,7 @@ export const comments = [
|
||||
export const assets = [
|
||||
{
|
||||
id: "asset-1",
|
||||
url: "http://localhost/assets/asset-1",
|
||||
isClosed: false,
|
||||
comments: {
|
||||
edges: [
|
||||
@@ -68,6 +69,7 @@ export const commentWithReplies = {
|
||||
|
||||
export const assetWithReplies = {
|
||||
id: "asset-with-replies",
|
||||
url: "http://localhost/assets/asset-with-replies",
|
||||
isClosed: false,
|
||||
comments: {
|
||||
edges: [
|
||||
|
||||
@@ -5,9 +5,9 @@ import sinon from "sinon";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
|
||||
import AppQuery from "talk-stream/queries/AppQuery";
|
||||
import { createRelayEnvironment } from "talk-framework/testHelpers";
|
||||
import AppContainer from "talk-stream/containers/AppContainer";
|
||||
|
||||
import createEnvironment from "./createEnvironment";
|
||||
import { assets, comments } from "./fixtures";
|
||||
|
||||
const connectionStub = sinon.stub().throws();
|
||||
@@ -61,10 +61,13 @@ const resolvers = {
|
||||
},
|
||||
};
|
||||
|
||||
const environment = createEnvironment({
|
||||
// Set this to true, to see graphql responses.
|
||||
logNetwork: false,
|
||||
resolvers,
|
||||
const environment = createRelayEnvironment({
|
||||
network: {
|
||||
// Set this to true, to see graphql responses.
|
||||
logNetwork: false,
|
||||
resolvers,
|
||||
projectName: "tenant",
|
||||
},
|
||||
initLocalState: (localRecord: RecordProxy) => {
|
||||
localRecord.setValue(assetStub.id, "assetID");
|
||||
},
|
||||
@@ -77,7 +80,7 @@ const context: TalkContext = {
|
||||
|
||||
const testRenderer = TestRenderer.create(
|
||||
<TalkContextProvider value={context}>
|
||||
<AppQuery />
|
||||
<AppContainer />
|
||||
</TalkContextProvider>
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import React from "react";
|
||||
import TestRenderer from "react-test-renderer";
|
||||
import { RecordProxy } from "relay-runtime";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
|
||||
import { createRelayEnvironment } from "talk-framework/testHelpers";
|
||||
import AppContainer from "talk-stream/containers/AppContainer";
|
||||
|
||||
import { assets, comments } from "./fixtures";
|
||||
|
||||
const commentStub = {
|
||||
...comments[0],
|
||||
};
|
||||
|
||||
const assetStub = {
|
||||
...assets[0],
|
||||
comments: {
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
},
|
||||
edges: [
|
||||
{
|
||||
node: commentStub,
|
||||
cursor: commentStub.createdAt,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
comment: sinon
|
||||
.stub()
|
||||
.throws()
|
||||
.withArgs(undefined, { id: commentStub.id })
|
||||
.returns(commentStub),
|
||||
asset: sinon
|
||||
.stub()
|
||||
.throws()
|
||||
.withArgs(undefined, { id: assetStub.id })
|
||||
.returns(assetStub),
|
||||
},
|
||||
};
|
||||
|
||||
const environment = createRelayEnvironment({
|
||||
network: {
|
||||
// Set this to true, to see graphql responses.
|
||||
logNetwork: false,
|
||||
resolvers,
|
||||
projectName: "tenant",
|
||||
},
|
||||
initLocalState: (localRecord: RecordProxy) => {
|
||||
localRecord.setValue(assetStub.id, "assetID");
|
||||
localRecord.setValue(commentStub.id, "commentID");
|
||||
},
|
||||
});
|
||||
|
||||
const context: TalkContext = {
|
||||
relayEnvironment: environment,
|
||||
localeMessages: [],
|
||||
};
|
||||
|
||||
const testRenderer = TestRenderer.create(
|
||||
<TalkContextProvider value={context}>
|
||||
<AppContainer />
|
||||
</TalkContextProvider>
|
||||
);
|
||||
|
||||
it("renders permalink view", async () => {
|
||||
// Wait for loading.
|
||||
await timeout();
|
||||
expect(testRenderer.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("show all comments", async () => {
|
||||
testRenderer.root
|
||||
.findByProps({
|
||||
id: "talk-comments-permalinkView-showAllComments",
|
||||
})
|
||||
.props.onClick();
|
||||
await timeout();
|
||||
expect(testRenderer.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from "react";
|
||||
import TestRenderer from "react-test-renderer";
|
||||
import { RecordProxy } from "relay-runtime";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
|
||||
import { createRelayEnvironment } from "talk-framework/testHelpers";
|
||||
import AppContainer from "talk-stream/containers/AppContainer";
|
||||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
comment: () => null,
|
||||
asset: () => null,
|
||||
},
|
||||
};
|
||||
|
||||
const environment = createRelayEnvironment({
|
||||
network: {
|
||||
// Set this to true, to see graphql responses.
|
||||
logNetwork: false,
|
||||
resolvers,
|
||||
projectName: "tenant",
|
||||
},
|
||||
initLocalState: (localRecord: RecordProxy) => {
|
||||
localRecord.setValue("unknown-asset-id", "assetID");
|
||||
localRecord.setValue("unknown-comment-id", "commentID");
|
||||
},
|
||||
});
|
||||
|
||||
const context: TalkContext = {
|
||||
relayEnvironment: environment,
|
||||
localeMessages: [],
|
||||
};
|
||||
|
||||
const testRenderer = TestRenderer.create(
|
||||
<TalkContextProvider value={context}>
|
||||
<AppContainer />
|
||||
</TalkContextProvider>
|
||||
);
|
||||
|
||||
it("renders permalink view with unknown asset", async () => {
|
||||
// Wait for loading.
|
||||
await timeout();
|
||||
expect(testRenderer.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import React from "react";
|
||||
import TestRenderer from "react-test-renderer";
|
||||
import { RecordProxy } from "relay-runtime";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
|
||||
import { createRelayEnvironment } from "talk-framework/testHelpers";
|
||||
import AppContainer from "talk-stream/containers/AppContainer";
|
||||
|
||||
import { assets, comments } from "./fixtures";
|
||||
|
||||
const commentStub = {
|
||||
...comments[0],
|
||||
};
|
||||
|
||||
const assetStub = {
|
||||
...assets[0],
|
||||
comments: {
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
},
|
||||
edges: [
|
||||
{
|
||||
node: commentStub,
|
||||
cursor: commentStub.createdAt,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
comment: () => null,
|
||||
asset: sinon
|
||||
.stub()
|
||||
.throws()
|
||||
.withArgs(undefined, { id: assetStub.id })
|
||||
.returns(assetStub),
|
||||
},
|
||||
};
|
||||
|
||||
const environment = createRelayEnvironment({
|
||||
network: {
|
||||
// Set this to true, to see graphql responses.
|
||||
logNetwork: false,
|
||||
resolvers,
|
||||
projectName: "tenant",
|
||||
},
|
||||
initLocalState: (localRecord: RecordProxy) => {
|
||||
localRecord.setValue(assetStub.id, "assetID");
|
||||
localRecord.setValue("unknown-comment-id", "commentID");
|
||||
},
|
||||
});
|
||||
|
||||
const context: TalkContext = {
|
||||
relayEnvironment: environment,
|
||||
localeMessages: [],
|
||||
};
|
||||
|
||||
const testRenderer = TestRenderer.create(
|
||||
<TalkContextProvider value={context}>
|
||||
<AppContainer />
|
||||
</TalkContextProvider>
|
||||
);
|
||||
|
||||
it("renders permalink view with unknown comment", async () => {
|
||||
// Wait for loading.
|
||||
await timeout();
|
||||
expect(testRenderer.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("show all comments", async () => {
|
||||
testRenderer.root
|
||||
.findByProps({
|
||||
id: "talk-comments-permalinkView-showAllComments",
|
||||
})
|
||||
.props.onClick();
|
||||
await timeout();
|
||||
expect(testRenderer.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
@@ -5,9 +5,9 @@ import sinon from "sinon";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
|
||||
import AppQuery from "talk-stream/queries/AppQuery";
|
||||
import { createRelayEnvironment } from "talk-framework/testHelpers";
|
||||
import AppContainer from "talk-stream/containers/AppContainer";
|
||||
|
||||
import createEnvironment from "./createEnvironment";
|
||||
import { assetWithReplies } from "./fixtures";
|
||||
|
||||
const resolvers = {
|
||||
@@ -20,10 +20,13 @@ const resolvers = {
|
||||
},
|
||||
};
|
||||
|
||||
const environment = createEnvironment({
|
||||
// Set this to true, to see graphql responses.
|
||||
logNetwork: false,
|
||||
resolvers,
|
||||
const environment = createRelayEnvironment({
|
||||
network: {
|
||||
// Set this to true, to see graphql responses.
|
||||
logNetwork: false,
|
||||
resolvers,
|
||||
projectName: "tenant",
|
||||
},
|
||||
initLocalState: (localRecord: RecordProxy) => {
|
||||
localRecord.setValue(assetWithReplies.id, "assetID");
|
||||
},
|
||||
@@ -36,7 +39,7 @@ const context: TalkContext = {
|
||||
|
||||
const testRenderer = TestRenderer.create(
|
||||
<TalkContextProvider value={context}>
|
||||
<AppQuery />
|
||||
<AppContainer />
|
||||
</TalkContextProvider>
|
||||
);
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ import sinon from "sinon";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
|
||||
import AppQuery from "talk-stream/queries/AppQuery";
|
||||
import { createRelayEnvironment } from "talk-framework/testHelpers";
|
||||
import AppContainer from "talk-stream/containers/AppContainer";
|
||||
|
||||
import createEnvironment from "./createEnvironment";
|
||||
import { assets } from "./fixtures";
|
||||
|
||||
const resolvers = {
|
||||
@@ -20,10 +20,13 @@ const resolvers = {
|
||||
},
|
||||
};
|
||||
|
||||
const environment = createEnvironment({
|
||||
// Set this to true, to see graphql responses.
|
||||
logNetwork: false,
|
||||
resolvers,
|
||||
const environment = createRelayEnvironment({
|
||||
network: {
|
||||
// Set this to true, to see graphql responses.
|
||||
logNetwork: false,
|
||||
resolvers,
|
||||
projectName: "tenant",
|
||||
},
|
||||
initLocalState: (localRecord: RecordProxy) => {
|
||||
localRecord.setValue(assets[0].id, "assetID");
|
||||
},
|
||||
@@ -36,7 +39,7 @@ const context: TalkContext = {
|
||||
|
||||
const testRenderer = TestRenderer.create(
|
||||
<TalkContextProvider value={context}>
|
||||
<AppQuery />
|
||||
<AppContainer />
|
||||
</TalkContextProvider>
|
||||
);
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ import sinon from "sinon";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
|
||||
import AppQuery from "talk-stream/queries/AppQuery";
|
||||
import { createRelayEnvironment } from "talk-framework/testHelpers";
|
||||
import AppContainer from "talk-stream/containers/AppContainer";
|
||||
|
||||
import createEnvironment from "./createEnvironment";
|
||||
import { assets, comments } from "./fixtures";
|
||||
|
||||
const connectionStub = sinon.stub().throws();
|
||||
@@ -77,10 +77,13 @@ const resolvers = {
|
||||
},
|
||||
};
|
||||
|
||||
const environment = createEnvironment({
|
||||
// Set this to true, to see graphql responses.
|
||||
logNetwork: false,
|
||||
resolvers,
|
||||
const environment = createRelayEnvironment({
|
||||
network: {
|
||||
// Set this to true, to see graphql responses.
|
||||
logNetwork: false,
|
||||
resolvers,
|
||||
projectName: "tenant",
|
||||
},
|
||||
initLocalState: (localRecord: RecordProxy) => {
|
||||
localRecord.setValue(assetStub.id, "assetID");
|
||||
},
|
||||
@@ -93,7 +96,7 @@ const context: TalkContext = {
|
||||
|
||||
const testRenderer = TestRenderer.create(
|
||||
<TalkContextProvider value={context}>
|
||||
<AppQuery />
|
||||
<AppContainer />
|
||||
</TalkContextProvider>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../tslint.json",
|
||||
"tslint-react"
|
||||
],
|
||||
"extends": ["../../../tslint.json", "tslint-react"],
|
||||
"rules": {
|
||||
"jsx-curly-spacing": false,
|
||||
"jsx-no-multiline-js": false,
|
||||
"jsx-boolean-value": [
|
||||
true,
|
||||
"never"
|
||||
]
|
||||
"jsx-boolean-value": [true, "never"],
|
||||
"jsx-no-lambda": false
|
||||
},
|
||||
"jsRules": {
|
||||
"jsx-curly-spacing": false,
|
||||
"jsx-no-multiline-js": false,
|
||||
"jsx-boolean-value": [
|
||||
true,
|
||||
"never"
|
||||
]
|
||||
"jsx-boolean-value": [true, "never"],
|
||||
"jsx-no-lambda": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,11 @@ clicks outside the component.
|
||||
Click the blue background. It should trigger an alert. Nothing should happen if you click the button.
|
||||
|
||||
<Playground>
|
||||
<div style={{background: 'blue', padding: '10px'}}>
|
||||
<ClickOutside onClickOutside={() => alert('You clicked outside!')}>
|
||||
<div id="outside" style={{background: 'blue', padding: '10px'}}>
|
||||
<ClickOutside onClickOutside={e => {
|
||||
if (e.srcElement.id === "outside") {
|
||||
alert('You clicked outside!');
|
||||
}}}>
|
||||
<Button variant="filled">Push Me</Button>
|
||||
</ClickOutside>
|
||||
</div>
|
||||
|
||||
@@ -10,8 +10,8 @@ export type ClickFarAwayRegister = (
|
||||
callback: ClickFarAwayCallback
|
||||
) => ClickFarAwayUnlistenCallback;
|
||||
|
||||
interface Props {
|
||||
onClickOutside: () => void;
|
||||
export interface ClickOutsideProps {
|
||||
onClickOutside: (e?: MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* A way to listen for clicks that are e.g. outside of the
|
||||
@@ -22,7 +22,7 @@ interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export class ClickOutside extends React.Component<Props> {
|
||||
export class ClickOutside extends React.Component<ClickOutsideProps> {
|
||||
public domNode: Element | null = null;
|
||||
private unlisten?: ClickFarAwayUnlistenCallback;
|
||||
|
||||
@@ -30,7 +30,7 @@ export class ClickOutside extends React.Component<Props> {
|
||||
const { onClickOutside } = this.props;
|
||||
if (!e || !this.domNode!.contains(e.target as HTMLInputElement)) {
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
onClickOutside && onClickOutside();
|
||||
onClickOutside && onClickOutside(e);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -65,7 +65,9 @@ export class ClickOutside extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
const ClickOutsideWithContext: StatelessComponent<Props> = props => (
|
||||
const ClickOutsideWithContext: StatelessComponent<
|
||||
ClickOutsideProps
|
||||
> = props => (
|
||||
<UIContext.Consumer>
|
||||
{({ registerClickFarAway }) => (
|
||||
<ClickOutside {...props} registerClickFarAway={registerClickFarAway} />
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default as ClickOutside, ClickFarAwayRegister } from "./ClickOutside";
|
||||
export { default, ClickFarAwayRegister } from "./ClickOutside";
|
||||
|
||||
@@ -37,18 +37,18 @@
|
||||
}
|
||||
|
||||
.sm {
|
||||
font-size: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
.md {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
.md {
|
||||
.lg {
|
||||
font-size: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
.lg {
|
||||
.xl {
|
||||
font-size: 36px;
|
||||
width: 36px;
|
||||
}
|
||||
.xl {
|
||||
font-size: 48px;
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
.root {
|
||||
background: var(--palette-common-white);
|
||||
border: 1px solid var(--palette-grey-lighter);
|
||||
box-sizing: border-box;
|
||||
box-shadow: var(--elevation-main);
|
||||
border-radius: var(--round-corners);
|
||||
padding: calc(0.5 * var(--spacing-unit));
|
||||
}
|
||||
|
||||
.top {
|
||||
margin: calc(0.5 * var(--spacing-unit)) 0;
|
||||
}
|
||||
.left {
|
||||
margin: 0 calc(0.5 * var(--spacing-unit));
|
||||
}
|
||||
.right {
|
||||
margin: 0 calc(0.5 * var(--spacing-unit));
|
||||
}
|
||||
.bottom {
|
||||
margin: calc(0.5 * var(--spacing-unit)) 0;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: Popover
|
||||
menu: UI Kit
|
||||
---
|
||||
|
||||
import { Playground } from 'docz'
|
||||
import Popover from './Popover'
|
||||
import Button from '../Button'
|
||||
import Flex from '../Flex'
|
||||
import Typography from '../Typography'
|
||||
import ButtonIcon from '../Button/ButtonIcon'
|
||||
|
||||
# Popover
|
||||
|
||||
`Popover` renders a popover dialog attached to another `Element`.
|
||||
|
||||
## Basic usage
|
||||
<Playground>
|
||||
<Popover
|
||||
body={<Typography>This is the body</Typography>}
|
||||
>
|
||||
{({ toggleVisibility, forwardRef }) => (
|
||||
<Button onClick={toggleVisibility} forwardRef={forwardRef} variant="filled" color="primary">
|
||||
Click me!
|
||||
</Button>
|
||||
)}
|
||||
</Popover>
|
||||
</Playground>
|
||||
|
||||
#### Example with `placement=top`
|
||||
<Playground>
|
||||
<Popover
|
||||
placement="top"
|
||||
body={({ toggleVisibility }) => (
|
||||
<Flex itemGutter="half">
|
||||
<Typography>This is the body</Typography>
|
||||
<Button onClick={toggleVisibility} size="small">
|
||||
<ButtonIcon>close</ButtonIcon>
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
>
|
||||
{({ toggleVisibility, forwardRef }) => (
|
||||
<Button onClick={toggleVisibility} forwardRef={forwardRef} variant="filled" color="primary">
|
||||
Click me!
|
||||
</Button>
|
||||
)}
|
||||
</Popover>
|
||||
</Playground>
|
||||
@@ -0,0 +1,147 @@
|
||||
import cn from "classnames";
|
||||
import React from "react";
|
||||
import {
|
||||
Manager,
|
||||
Popper,
|
||||
PopperArrowProps,
|
||||
Reference,
|
||||
RefHandler,
|
||||
} from "react-popper";
|
||||
import AriaInfo from "../AriaInfo";
|
||||
import * as styles from "./Popover.css";
|
||||
|
||||
type Placement =
|
||||
| "top-start"
|
||||
| "top"
|
||||
| "top-end"
|
||||
| "right-start"
|
||||
| "right"
|
||||
| "right-end"
|
||||
| "bottom-end"
|
||||
| "bottom"
|
||||
| "bottom-start"
|
||||
| "left-end"
|
||||
| "left"
|
||||
| "left-start";
|
||||
|
||||
interface BodyRenderProps {
|
||||
toggleVisibility: () => void;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
interface ChildrenRenderProps {
|
||||
toggleVisibility: () => void;
|
||||
forwardRef?: RefHandler;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
interface PopoverProps {
|
||||
body: (props: BodyRenderProps) => React.ReactNode | React.ReactElement<any>;
|
||||
children: (props: ChildrenRenderProps) => React.ReactNode;
|
||||
description: string;
|
||||
id: string;
|
||||
onClose?: () => void;
|
||||
className?: string;
|
||||
placement?: Placement;
|
||||
}
|
||||
|
||||
interface State {
|
||||
visible: false;
|
||||
}
|
||||
|
||||
class Popover extends React.Component<PopoverProps> {
|
||||
public static defaultProps = {
|
||||
placement: "top",
|
||||
};
|
||||
public state: State = {
|
||||
visible: false,
|
||||
};
|
||||
|
||||
public toggleVisibility = () => {
|
||||
this.setState((state: State) => ({
|
||||
visible: !state.visible,
|
||||
}));
|
||||
};
|
||||
|
||||
public close = () => {
|
||||
this.setState((state: State) => ({
|
||||
visible: false,
|
||||
}));
|
||||
};
|
||||
|
||||
public handleEsc = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
document.addEventListener("keydown", this.handleEsc, true);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
document.removeEventListener("keydown", this.handleEsc, true);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
id,
|
||||
body,
|
||||
children,
|
||||
description,
|
||||
className,
|
||||
placement,
|
||||
} = this.props;
|
||||
|
||||
const { visible } = this.state;
|
||||
const popoverClassName = cn(styles.root, className, {
|
||||
[styles.top]: placement!.startsWith("top"),
|
||||
[styles.left]: placement!.startsWith("left"),
|
||||
[styles.right]: placement!.startsWith("right"),
|
||||
[styles.bottom]: placement!.startsWith("bottom"),
|
||||
});
|
||||
|
||||
return (
|
||||
<Manager>
|
||||
<Reference>
|
||||
{(props: PopperArrowProps) =>
|
||||
children({
|
||||
forwardRef: props.ref,
|
||||
toggleVisibility: this.toggleVisibility,
|
||||
visible: this.state.visible,
|
||||
})
|
||||
}
|
||||
</Reference>
|
||||
<Popper placement={placement} eventsEnabled positionFixed={false}>
|
||||
{(props: PopperArrowProps) => (
|
||||
<div
|
||||
id={id}
|
||||
role="popup"
|
||||
aria-labelledby={`${id}-ariainfo`}
|
||||
aria-hidden={!visible}
|
||||
>
|
||||
<AriaInfo id={`${id}-ariainfo`}>{description}</AriaInfo>
|
||||
{visible && (
|
||||
<div
|
||||
style={props.style}
|
||||
className={popoverClassName}
|
||||
ref={props.ref}
|
||||
>
|
||||
{typeof body === "function"
|
||||
? body({
|
||||
toggleVisibility: this.toggleVisibility,
|
||||
visible: this.state.visible,
|
||||
})
|
||||
: body}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Popper>
|
||||
</Manager>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Popover;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./Popover";
|
||||
@@ -0,0 +1,8 @@
|
||||
.root {
|
||||
composes: textField from "talk-ui/shared/typography.css";
|
||||
background: var(--palette-common-white);
|
||||
border: 1px solid var(--palette-grey-lighter);
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--round-corners);
|
||||
padding: calc(0.5 * var(--spacing-unit));
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: TextField
|
||||
menu: UI Kit
|
||||
---
|
||||
|
||||
import { Playground, PropsTable } from 'docz'
|
||||
import TextField from './TextField'
|
||||
|
||||
# TextField
|
||||
|
||||
`TextField`
|
||||
|
||||
## Basic usage
|
||||
<Playground>
|
||||
<TextField value="Hallo Talk" />
|
||||
</Playground>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import cn from "classnames";
|
||||
import React, { InputHTMLAttributes, StatelessComponent } from "react";
|
||||
|
||||
import * as styles from "./TextField.css";
|
||||
|
||||
interface TextFieldProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
classes?: typeof styles;
|
||||
}
|
||||
|
||||
const TextField: StatelessComponent<TextFieldProps> = ({
|
||||
className,
|
||||
...rest
|
||||
}) => {
|
||||
return <input {...rest} className={cn(styles.root, className)} />;
|
||||
};
|
||||
|
||||
export default TextField;
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./TextField";
|
||||
export { default } from "./TextField";
|
||||
@@ -1,9 +1,13 @@
|
||||
export { default as BaseButton } from "./BaseButton";
|
||||
export { default as Button } from "./Button";
|
||||
export { default as ButtonIcon } from "./Button/ButtonIcon";
|
||||
export { default as Typography } from "./Typography";
|
||||
export { default as Popover } from "./Popover";
|
||||
export { default as TextField } from "./TextField";
|
||||
export { default as RelativeTime } from "./RelativeTime";
|
||||
export { default as UIContext, UIContextProps } from "./UIContext";
|
||||
export { default as Flex } from "./Flex";
|
||||
export { default as MatchMedia } from "./MatchMedia";
|
||||
export { default as TrapFocus } from "./TrapFocus";
|
||||
export { default as ClickOutside } from "./ClickOutside";
|
||||
export { default as Popup } from "./Popup";
|
||||
|
||||
@@ -126,3 +126,12 @@
|
||||
line-height: calc(18em / 16);
|
||||
letter-spacing: calc(0.2em / 16);
|
||||
}
|
||||
|
||||
.textField {
|
||||
color: var(--palette-common-black);
|
||||
font-family: "Source Sans Pro";
|
||||
font-weight: var(--font-weight-regular);
|
||||
font-size: 16px;
|
||||
line-height: calc(18em / 16);
|
||||
letter-spacing: calc(0.57em / 16);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
*/
|
||||
|
||||
const variables = {
|
||||
elevation: {
|
||||
main: "1px 0px 4px rgba(0, 0, 0, 0.25)",
|
||||
},
|
||||
palette: {
|
||||
/* Primary colors */
|
||||
primary: {
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as timeout } from "./timeout";
|
||||
export { default as pascalCase } from "./pascalCase";
|
||||
export { default as oncePerFrame } from "./oncePerFrame";
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Function decorator that prevents calling `fn` more then once per frame.
|
||||
* If called more than once, the last return value gets returned.
|
||||
*/
|
||||
const oncePerFrame = <T extends (...args: any[]) => any>(fn: T) => {
|
||||
let toggledThisFrame = false;
|
||||
let lastResult: any = null;
|
||||
return ((...args: any[]) => {
|
||||
if (toggledThisFrame) {
|
||||
return lastResult;
|
||||
}
|
||||
toggledThisFrame = true;
|
||||
lastResult = fn(...args);
|
||||
setTimeout(() => (toggledThisFrame = false), 0);
|
||||
}) as T;
|
||||
};
|
||||
|
||||
export default oncePerFrame;
|
||||
@@ -2,6 +2,8 @@ import { GQLQueryTypeResolver } from "talk-server/graph/tenant/schema/__generate
|
||||
|
||||
const Query: GQLQueryTypeResolver<void> = {
|
||||
asset: (source, args, ctx) => ctx.loaders.Assets.findOrCreate(args),
|
||||
comment: (source, { id }, ctx) =>
|
||||
id ? ctx.loaders.Comments.comment.load(id) : null,
|
||||
settings: (source, args, ctx) => ctx.tenant,
|
||||
me: (source, args, ctx) => ctx.user,
|
||||
};
|
||||
|
||||
@@ -5,5 +5,11 @@ route: '/'
|
||||
|
||||
# Introduction
|
||||
|
||||
Hello, I'm a mdx file!
|
||||
## Running the Talk V5
|
||||
|
||||
Run this command to start Talk V5 in watch mode:
|
||||
|
||||
```sh
|
||||
npm run watch
|
||||
```
|
||||
|
||||
|
||||
@@ -78,3 +78,26 @@ const enhanced = withLocalStateContainer(
|
||||
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
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(
|
||||
withLocalStateContainer<Local>(
|
||||
graphql`
|
||||
fragment PermalinkViewContainerLocal on Local {
|
||||
assetURL
|
||||
}
|
||||
`
|
||||
)(PermalinkViewContainer)
|
||||
);
|
||||
```
|
||||
|
||||
@@ -5,3 +5,7 @@
|
||||
comments-postCommentForm-post = Post
|
||||
comments-stream-loadMore = Load more
|
||||
comments-replyList-showAll = Show all
|
||||
|
||||
comments-permalink-share = Share
|
||||
comments-permalink-copy = Copy
|
||||
comments-permalink-copied = Copied
|
||||
|
||||
Reference in New Issue
Block a user