`;
+
+exports[`Storyshots Toast Click to close 1`] = `null`;
+
+exports[`Storyshots Toast Danger 1`] = `null`;
+
+exports[`Storyshots Toast Info 1`] = `null`;
+
+exports[`Storyshots Toast Open/Close 1`] = `
+
+
+
+`;
+
+exports[`Storyshots Toast Primary 1`] = `null`;
+
+exports[`Storyshots Toast Success 1`] = `null`;
+
+exports[`Storyshots Toast Warning 1`] = `null`;
diff --git a/scm-ui/ui-components/src/apiclient.ts b/scm-ui/ui-components/src/apiclient.ts
index 473bf3b925..c8990b000c 100644
--- a/scm-ui/ui-components/src/apiclient.ts
+++ b/scm-ui/ui-components/src/apiclient.ts
@@ -1,6 +1,43 @@
import { contextPath } from "./urls";
-import { createBackendError, ForbiddenError, isBackendError, UnauthorizedError } from "./errors";
-import { BackendErrorContent } from "./errors";
+// @ts-ignore we have not types for event-source-polyfill
+import { EventSourcePolyfill } from "event-source-polyfill";
+import { createBackendError, ForbiddenError, isBackendError, UnauthorizedError, BackendErrorContent } from "./errors";
+
+type SubscriptionEvent = {
+ type: string;
+};
+
+type OpenEvent = SubscriptionEvent;
+
+type ErrorEvent = SubscriptionEvent & {
+ error: Error;
+};
+
+type MessageEvent = SubscriptionEvent & {
+ data: string;
+ lastEventId?: string;
+};
+
+type MessageListeners = {
+ [eventType: string]: (event: MessageEvent) => void;
+};
+
+type SubscriptionContext = {
+ onOpen?: OpenEvent;
+ onMessage: MessageListeners;
+ onError?: ErrorEvent;
+};
+
+type SubscriptionArgument = MessageListeners | SubscriptionContext;
+
+type Cancel = () => void;
+
+const sessionId = (
+ Date.now().toString(36) +
+ Math.random()
+ .toString(36)
+ .substr(2, 5)
+).toUpperCase();
const extractXsrfTokenFromJwt = (jwt: string) => {
const parts = jwt.split(".");
@@ -26,26 +63,34 @@ const extractXsrfToken = () => {
return extractXsrfTokenFromCookie(document.cookie);
};
-const applyFetchOptions: (p: RequestInit) => RequestInit = o => {
- if (!o.headers) {
- o.headers = {};
- }
-
- // @ts-ignore We are sure that here we only get headers of type Record
- const headers: Record = o.headers;
- headers["Cache"] = "no-cache";
- // identify the request as ajax request
- headers["X-Requested-With"] = "XMLHttpRequest";
- // identify the web interface
- headers["X-SCM-Client"] = "WUI";
+const createRequestHeaders = () => {
+ const headers: { [key: string]: string } = {
+ // disable caching for now
+ Cache: "no-cache",
+ // identify the request as ajax request
+ "X-Requested-With": "XMLHttpRequest",
+ // identify the web interface
+ "X-SCM-Client": "WUI",
+ // identify the window session
+ "X-SCM-Session-ID": sessionId
+ };
const xsrf = extractXsrfToken();
if (xsrf) {
headers["X-XSRF-Token"] = xsrf;
}
+ return headers;
+};
+const applyFetchOptions: (p: RequestInit) => RequestInit = o => {
+ if (o.headers) {
+ o.headers = {
+ ...createRequestHeaders()
+ };
+ } else {
+ o.headers = createRequestHeaders();
+ }
o.credentials = "same-origin";
- o.headers = headers;
return o;
};
@@ -165,12 +210,39 @@ class ApiClient {
if (!options.headers) {
options.headers = {};
}
- // @ts-ignore We are sure that here we only get headers of type Record
+ // @ts-ignore We are sure that here we only get headers of type {[name:string]: string}
options.headers["Content-Type"] = contentType;
}
return fetch(createUrl(url), options).then(handleFailure);
}
+
+ subscribe(url: string, argument: SubscriptionArgument): Cancel {
+ const es = new EventSourcePolyfill(createUrl(url), {
+ withCredentials: true,
+ headers: createRequestHeaders()
+ });
+
+ let listeners: MessageListeners;
+ // type guard, to identify that argument is of type SubscriptionContext
+ if ("onMessage" in argument) {
+ listeners = (argument as SubscriptionContext).onMessage;
+ if (argument.onError) {
+ es.onerror = argument.onError;
+ }
+ if (argument.onOpen) {
+ es.onopen = argument.onOpen;
+ }
+ } else {
+ listeners = argument;
+ }
+
+ for (const type in listeners) {
+ es.addEventListener(type, listeners[type]);
+ }
+
+ return () => es.close();
+ }
}
export const apiClient = new ApiClient();
diff --git a/scm-ui/ui-components/src/forms/Textarea.stories.tsx b/scm-ui/ui-components/src/forms/Textarea.stories.tsx
new file mode 100644
index 0000000000..a27b2b738c
--- /dev/null
+++ b/scm-ui/ui-components/src/forms/Textarea.stories.tsx
@@ -0,0 +1,56 @@
+import React, {useState} from "react";
+import { storiesOf } from "@storybook/react";
+import styled from "styled-components";
+import Textarea from "./Textarea";
+
+const Spacing = styled.div`
+ padding: 2em;
+`;
+
+const OnChangeTextarea = () => {
+ const [value, setValue] = useState("Start typing");
+ return (
+
+
+ );
+};
+
+const OnSubmitTextare = () => {
+ const [value, setValue] = useState("Use the ctrl/command + Enter to submit the textarea");
+ const [submitted, setSubmitted] = useState("");
+
+ const submit = () => {
+ setSubmitted(value);
+ setValue("");
+ };
+
+ return (
+
+
+ );
+};
+
+const OnCancelTextare = () => {
+ const [value, setValue] = useState("Use the escape key to clear the textarea");
+
+ const cancel = () => {
+ setValue("");
+ };
+
+ return (
+
+
+ );
+};
+
+storiesOf("Forms|Textarea", module)
+ .add("OnChange", () => )
+ .add("OnSubmit", () => )
+ .add("OnCancel", () => );
diff --git a/scm-ui/ui-components/src/forms/Textarea.tsx b/scm-ui/ui-components/src/forms/Textarea.tsx
index b16c33b708..90ae07b576 100644
--- a/scm-ui/ui-components/src/forms/Textarea.tsx
+++ b/scm-ui/ui-components/src/forms/Textarea.tsx
@@ -1,4 +1,4 @@
-import React, { ChangeEvent } from "react";
+import React, { ChangeEvent, KeyboardEvent } from "react";
import LabelWithHelpIcon from "./LabelWithHelpIcon";
type Props = {
@@ -10,6 +10,8 @@ type Props = {
onChange: (value: string, name?: string) => void;
helpText?: string;
disabled?: boolean;
+ onSubmit?: () => void;
+ onCancel?: () => void;
};
class Textarea extends React.Component {
@@ -25,6 +27,19 @@ class Textarea extends React.Component {
this.props.onChange(event.target.value, this.props.name);
};
+ onKeyDown = (event: KeyboardEvent) => {
+ const { onCancel } = this.props;
+ if (onCancel && event.key === "Escape") {
+ onCancel();
+ return;
+ }
+
+ const { onSubmit } = this.props;
+ if (onSubmit && event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
+ onSubmit();
+ }
+ };
+
render() {
const { placeholder, value, label, helpText, disabled } = this.props;
@@ -41,6 +56,7 @@ class Textarea extends React.Component {
onChange={this.handleInput}
value={value}
disabled={!!disabled}
+ onKeyDown={this.onKeyDown}
/>