|
1 |
| -import { |
2 |
| - NonIdealState, |
3 |
| - Popover, |
4 |
| - Position, |
5 |
| - Spinner, |
6 |
| - SpinnerSize, |
7 |
| - Text, |
8 |
| - Tooltip |
9 |
| -} from '@blueprintjs/core'; |
| 1 | +import { NonIdealState, Popover, Position, Spinner, SpinnerSize, Tooltip } from '@blueprintjs/core'; |
10 | 2 | import { IconNames } from '@blueprintjs/icons';
|
11 |
| -import React from 'react'; |
| 3 | +import { useHotkeys } from '@mantine/hooks'; |
| 4 | +import React, { useRef, useState } from 'react'; |
12 | 5 | import * as CopyToClipboard from 'react-copy-to-clipboard';
|
13 |
| -import ShareLinkState from 'src/features/playground/shareLinks/ShareLinkState'; |
| 6 | +import JsonEncoderDelegate from 'src/features/playground/shareLinks/encoder/delegates/JsonEncoderDelegate'; |
| 7 | +import UrlParamsEncoderDelegate from 'src/features/playground/shareLinks/encoder/delegates/UrlParamsEncoderDelegate'; |
| 8 | +import { usePlaygroundConfigurationEncoder } from 'src/features/playground/shareLinks/encoder/EncoderHooks'; |
14 | 9 |
|
15 | 10 | import ControlButton from '../ControlButton';
|
16 |
| -import Constants from '../utils/Constants'; |
17 |
| -import { showWarningMessage } from '../utils/notifications/NotificationsHelper'; |
18 |
| -import { request } from '../utils/RequestHelper'; |
19 |
| -import { RemoveLast } from '../utils/TypeHelper'; |
| 11 | +import { externalUrlShortenerRequest } from '../sagas/PlaygroundSaga'; |
| 12 | +import { postSharedProgram } from '../sagas/RequestsSaga'; |
| 13 | +import Constants, { Links } from '../utils/Constants'; |
| 14 | +import { showSuccessMessage, showWarningMessage } from '../utils/notifications/NotificationsHelper'; |
20 | 15 |
|
21 |
| -type ControlBarShareButtonProps = DispatchProps & StateProps; |
22 |
| - |
23 |
| -type DispatchProps = { |
24 |
| - handleGenerateLz?: () => void; |
25 |
| - handleShortenURL: (s: string) => void; |
26 |
| - handleUpdateShortURL: (s: string) => void; |
27 |
| -}; |
28 |
| - |
29 |
| -type StateProps = { |
30 |
| - queryString?: string; |
31 |
| - shortURL?: string; |
32 |
| - key: string; |
| 16 | +type ControlBarShareButtonProps = { |
33 | 17 | isSicp?: boolean;
|
34 |
| - programConfig: ShareLinkState; |
35 |
| - token: Tokens; |
36 |
| -}; |
37 |
| - |
38 |
| -type State = { |
39 |
| - keyword: string; |
40 |
| - isLoading: boolean; |
41 |
| - isSuccess: boolean; |
42 | 18 | };
|
43 | 19 |
|
44 |
| -type ShareLinkRequestHelperParams = RemoveLast<Parameters<typeof request>>; |
45 |
| - |
46 |
| -export type Tokens = { |
47 |
| - accessToken: string | undefined; |
48 |
| - refreshToken: string | undefined; |
49 |
| -}; |
50 |
| - |
51 |
| -export const requestToShareProgram = async ( |
52 |
| - ...[path, method, opts]: ShareLinkRequestHelperParams |
53 |
| -) => { |
54 |
| - const resp = await request(path, method, opts); |
55 |
| - return resp; |
56 |
| -}; |
| 20 | +/** |
| 21 | + * Generates the share link for programs in the Playground. |
| 22 | + * |
| 23 | + * For playground-only (no backend) deployments: |
| 24 | + * - Generate a URL with playground configuration encoded as hash parameters |
| 25 | + * - URL sent to external URL shortener service |
| 26 | + * - Shortened URL displayed to user |
| 27 | + * - (note: SICP CodeSnippets use these hash parameters) |
| 28 | + * |
| 29 | + * For 'with backend' deployments: |
| 30 | + * - Send the playground configuration to the backend |
| 31 | + * - Backend stores configuration and assigns a UUID |
| 32 | + * - Backend pings the external URL shortener service with UUID link |
| 33 | + * - Shortened URL returned to Frontend and displayed to user |
| 34 | + */ |
| 35 | +export const ControlBarShareButton: React.FC<ControlBarShareButtonProps> = props => { |
| 36 | + const shareInputElem = useRef<HTMLInputElement>(null); |
| 37 | + const [isLoading, setIsLoading] = useState(false); |
| 38 | + const [shortenedUrl, setShortenedUrl] = useState(''); |
| 39 | + const [customStringKeyword, setCustomStringKeyword] = useState(''); |
| 40 | + const playgroundConfiguration = usePlaygroundConfigurationEncoder(); |
| 41 | + |
| 42 | + const generateLinkBackend = () => { |
| 43 | + setIsLoading(true); |
| 44 | + |
| 45 | + customStringKeyword; |
| 46 | + |
| 47 | + const configuration = playgroundConfiguration.encodeWith(new JsonEncoderDelegate()); |
| 48 | + |
| 49 | + return postSharedProgram(configuration) |
| 50 | + .then(({ shortenedUrl }) => setShortenedUrl(shortenedUrl)) |
| 51 | + .catch(err => showWarningMessage(err.toString())) |
| 52 | + .finally(() => setIsLoading(false)); |
| 53 | + }; |
57 | 54 |
|
58 |
| -export class ControlBarShareButton extends React.PureComponent<ControlBarShareButtonProps, State> { |
59 |
| - private shareInputElem: React.RefObject<HTMLInputElement>; |
60 |
| - |
61 |
| - constructor(props: ControlBarShareButtonProps) { |
62 |
| - super(props); |
63 |
| - this.selectShareInputText = this.selectShareInputText.bind(this); |
64 |
| - this.handleChange = this.handleChange.bind(this); |
65 |
| - this.toggleButton = this.toggleButton.bind(this); |
66 |
| - this.fetchUUID = this.fetchUUID.bind(this); |
67 |
| - this.shareInputElem = React.createRef(); |
68 |
| - this.state = { keyword: '', isLoading: false, isSuccess: false }; |
69 |
| - } |
70 |
| - |
71 |
| - componentDidMount() { |
72 |
| - document.addEventListener('keydown', this.handleKeyDown); |
73 |
| - } |
74 |
| - |
75 |
| - componentWillUnmount() { |
76 |
| - document.removeEventListener('keydown', this.handleKeyDown); |
77 |
| - } |
78 |
| - |
79 |
| - handleKeyDown = (event: any) => { |
80 |
| - if (event.key === 'Enter' && event.ctrlKey) { |
81 |
| - // press Ctrl+Enter to generate and copy new share link directly |
82 |
| - this.setState({ keyword: 'Test' }); |
83 |
| - this.props.handleShortenURL(this.state.keyword); |
84 |
| - this.setState({ isLoading: true }); |
85 |
| - if (this.props.shortURL || this.props.isSicp) { |
86 |
| - this.selectShareInputText(); |
87 |
| - console.log('link created.'); |
88 |
| - } |
89 |
| - } |
| 55 | + const generateLinkPlaygroundOnly = () => { |
| 56 | + const hash = playgroundConfiguration.encodeWith(new UrlParamsEncoderDelegate()); |
| 57 | + setIsLoading(true); |
| 58 | + |
| 59 | + return externalUrlShortenerRequest(hash, customStringKeyword) |
| 60 | + .then(({ shortenedUrl, message }) => { |
| 61 | + setShortenedUrl(shortenedUrl); |
| 62 | + if (message) showSuccessMessage(message); |
| 63 | + }) |
| 64 | + .catch(err => showWarningMessage(err.toString())) |
| 65 | + .finally(() => setIsLoading(false)); |
90 | 66 | };
|
91 | 67 |
|
92 |
| - public render() { |
93 |
| - const shareButtonPopoverContent = |
94 |
| - this.props.queryString === undefined ? ( |
95 |
| - <Text> |
96 |
| - Share your programs! Type something into the editor (left), then click on this button |
97 |
| - again. |
98 |
| - </Text> |
99 |
| - ) : this.props.isSicp ? ( |
100 |
| - <div> |
101 |
| - <input defaultValue={this.props.queryString!} readOnly={true} ref={this.shareInputElem} /> |
102 |
| - <Tooltip content="Copy link to clipboard"> |
103 |
| - <CopyToClipboard text={this.props.queryString!}> |
104 |
| - <ControlButton icon={IconNames.DUPLICATE} onClick={this.selectShareInputText} /> |
105 |
| - </CopyToClipboard> |
106 |
| - </Tooltip> |
107 |
| - </div> |
108 |
| - ) : ( |
109 |
| - <> |
110 |
| - {!this.state.isSuccess || this.props.shortURL === 'ERROR' ? ( |
111 |
| - !this.state.isLoading || this.props.shortURL === 'ERROR' ? ( |
112 |
| - <div> |
113 |
| - {Constants.urlShortenerBase} |
114 |
| - <input |
115 |
| - placeholder={'custom string (optional)'} |
116 |
| - onChange={this.handleChange} |
117 |
| - style={{ width: 175 }} |
118 |
| - /> |
119 |
| - <ControlButton |
120 |
| - label="Get Link" |
121 |
| - icon={IconNames.SHARE} |
122 |
| - // post request to backend, set keyword as return uuid |
123 |
| - onClick={() => this.fetchUUID(this.props.token)} |
124 |
| - /> |
125 |
| - </div> |
126 |
| - ) : ( |
127 |
| - <div> |
128 |
| - <NonIdealState |
129 |
| - description="Generating Shareable Link..." |
130 |
| - icon={<Spinner size={SpinnerSize.SMALL} />} |
131 |
| - /> |
132 |
| - </div> |
133 |
| - ) |
134 |
| - ) : ( |
135 |
| - <div key={this.state.keyword}> |
136 |
| - <input defaultValue={this.state.keyword} readOnly={true} ref={this.shareInputElem} /> |
137 |
| - <Tooltip content="Copy link to clipboard"> |
138 |
| - <CopyToClipboard text={this.state.keyword}> |
139 |
| - <ControlButton icon={IconNames.DUPLICATE} onClick={this.selectShareInputText} /> |
140 |
| - </CopyToClipboard> |
141 |
| - </Tooltip> |
142 |
| - </div> |
143 |
| - )} |
144 |
| - </> |
145 |
| - ); |
146 |
| - |
147 |
| - return ( |
148 |
| - <Popover |
149 |
| - popoverClassName="Popover-share" |
150 |
| - inheritDarkTheme={false} |
151 |
| - content={shareButtonPopoverContent} |
152 |
| - > |
153 |
| - <Tooltip content="Get shareable link" placement={Position.TOP}> |
154 |
| - <ControlButton label="Share" icon={IconNames.SHARE} onClick={() => this.toggleButton()} /> |
155 |
| - </Tooltip> |
156 |
| - </Popover> |
157 |
| - ); |
158 |
| - } |
159 |
| - |
160 |
| - public componentDidUpdate(prevProps: ControlBarShareButtonProps) { |
161 |
| - if (this.props.shortURL !== prevProps.shortURL) { |
162 |
| - this.setState({ keyword: '', isLoading: false }); |
163 |
| - } |
164 |
| - } |
| 68 | + const generateLinkSicp = () => { |
| 69 | + const hash = playgroundConfiguration.encodeWith(new UrlParamsEncoderDelegate()); |
| 70 | + const shortenedUrl = `${Links.playground}#${hash}`; |
| 71 | + setShortenedUrl(shortenedUrl); |
| 72 | + }; |
165 | 73 |
|
166 |
| - private toggleButton() { |
167 |
| - if (this.props.handleGenerateLz) { |
168 |
| - this.props.handleGenerateLz(); |
169 |
| - } |
| 74 | + const generateLink = props.isSicp |
| 75 | + ? generateLinkSicp |
| 76 | + : Constants.playgroundOnly |
| 77 | + ? generateLinkPlaygroundOnly |
| 78 | + : generateLinkBackend; |
170 | 79 |
|
171 |
| - // reset state |
172 |
| - this.setState({ keyword: '', isLoading: false, isSuccess: false }); |
173 |
| - } |
| 80 | + useHotkeys([['ctrl+e', generateLink]], []); |
174 | 81 |
|
175 |
| - private handleChange(event: React.FormEvent<HTMLInputElement>) { |
176 |
| - this.setState({ keyword: event.currentTarget.value }); |
177 |
| - } |
| 82 | + const handleCustomStringChange = (event: React.FormEvent<HTMLInputElement>) => { |
| 83 | + setCustomStringKeyword(event.currentTarget.value); |
| 84 | + }; |
178 | 85 |
|
179 |
| - private selectShareInputText() { |
180 |
| - if (this.shareInputElem.current !== null) { |
181 |
| - this.shareInputElem.current.focus(); |
182 |
| - this.shareInputElem.current.select(); |
| 86 | + // For visual effect of highlighting the text field on copy |
| 87 | + const selectShareInputText = () => { |
| 88 | + if (shareInputElem.current !== null) { |
| 89 | + shareInputElem.current.focus(); |
| 90 | + shareInputElem.current.select(); |
183 | 91 | }
|
184 |
| - } |
185 |
| - |
186 |
| - private fetchUUID(tokens: Tokens) { |
187 |
| - const requestBody = { |
188 |
| - shared_program: { |
189 |
| - data: this.props.programConfig |
190 |
| - } |
191 |
| - }; |
192 |
| - |
193 |
| - const getProgramUrl = async () => { |
194 |
| - const resp = await requestToShareProgram(`shared_programs`, 'POST', { |
195 |
| - body: requestBody, |
196 |
| - ...tokens |
197 |
| - }); |
198 |
| - if (!resp) { |
199 |
| - return showWarningMessage('Fail to generate url!'); |
200 |
| - } |
201 |
| - const respJson = await resp.json(); |
202 |
| - this.setState({ |
203 |
| - keyword: `${window.location.host}/playground/share/` + respJson.uuid |
204 |
| - }); |
205 |
| - this.setState({ isLoading: true, isSuccess: true }); |
206 |
| - return; |
207 |
| - }; |
208 |
| - |
209 |
| - getProgramUrl(); |
210 |
| - } |
211 |
| -} |
| 92 | + }; |
| 93 | + |
| 94 | + const generateLinkPopoverContent = ( |
| 95 | + <div> |
| 96 | + {Constants.urlShortenerBase} |
| 97 | + <input |
| 98 | + placeholder={'custom string (optional)'} |
| 99 | + onChange={handleCustomStringChange} |
| 100 | + style={{ width: 175 }} |
| 101 | + /> |
| 102 | + <ControlButton label="Get Link" icon={IconNames.SHARE} onClick={generateLink} /> |
| 103 | + </div> |
| 104 | + ); |
| 105 | + |
| 106 | + const generatingLinkPopoverContent = ( |
| 107 | + <div> |
| 108 | + <NonIdealState |
| 109 | + description="Generating Shareable Link..." |
| 110 | + icon={<Spinner size={SpinnerSize.SMALL} />} |
| 111 | + /> |
| 112 | + </div> |
| 113 | + ); |
| 114 | + |
| 115 | + const copyLinkPopoverContent = ( |
| 116 | + <div key={shortenedUrl}> |
| 117 | + <input defaultValue={shortenedUrl} readOnly={true} ref={shareInputElem} /> |
| 118 | + <Tooltip content="Copy link to clipboard"> |
| 119 | + <CopyToClipboard text={shortenedUrl}> |
| 120 | + <ControlButton icon={IconNames.DUPLICATE} onClick={selectShareInputText} /> |
| 121 | + </CopyToClipboard> |
| 122 | + </Tooltip> |
| 123 | + </div> |
| 124 | + ); |
| 125 | + |
| 126 | + const shareButtonPopoverContent = isLoading |
| 127 | + ? generatingLinkPopoverContent |
| 128 | + : shortenedUrl |
| 129 | + ? copyLinkPopoverContent |
| 130 | + : generateLinkPopoverContent; |
| 131 | + |
| 132 | + return ( |
| 133 | + <Popover |
| 134 | + popoverClassName="Popover-share" |
| 135 | + inheritDarkTheme={false} |
| 136 | + content={shareButtonPopoverContent} |
| 137 | + > |
| 138 | + <Tooltip content="Get shareable link" placement={Position.TOP}> |
| 139 | + <ControlButton label="Share" icon={IconNames.SHARE} /> |
| 140 | + </Tooltip> |
| 141 | + </Popover> |
| 142 | + ); |
| 143 | +}; |
0 commit comments