|
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