Skip to content

Commit 24b31fa

Browse files
committed
Refactor ControlBarShareButton to functional react
1 parent 840e5d5 commit 24b31fa

File tree

4 files changed

+120
-159
lines changed

4 files changed

+120
-159
lines changed

src/commons/controlBar/ControlBarShareButton.tsx

Lines changed: 90 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@ import {
44
Position,
55
Spinner,
66
SpinnerSize,
7-
Text,
87
Tooltip
98
} from '@blueprintjs/core';
109
import { IconNames } from '@blueprintjs/icons';
11-
import React from 'react';
10+
import React, { useRef, useState } from 'react';
1211
import * as CopyToClipboard from 'react-copy-to-clipboard';
12+
import { usePlaygroundConfigurationEncoder } from 'src/features/playground/shareLinks/encoder/Encoder';
1313
import ShareLinkState from 'src/features/playground/shareLinks/ShareLinkState';
1414

1515
import ControlButton from '../ControlButton';
16+
import { postSharedProgram } from '../sagas/RequestsSaga';
1617
import Constants from '../utils/Constants';
1718
import { showWarningMessage } from '../utils/notifications/NotificationsHelper';
1819
import { request } from '../utils/RequestHelper';
@@ -35,12 +36,6 @@ type StateProps = {
3536
token: Tokens;
3637
};
3738

38-
type State = {
39-
keyword: string;
40-
isLoading: boolean;
41-
isSuccess: boolean;
42-
};
43-
4439
type ShareLinkRequestHelperParams = RemoveLast<Parameters<typeof request>>;
4540

4641
export type Tokens = {
@@ -55,157 +50,96 @@ export const requestToShareProgram = async (
5550
return resp;
5651
};
5752

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-
}
90-
};
53+
export const ControlBarShareButton: React.FC<ControlBarShareButtonProps> = props => {
54+
const shareInputElem = useRef<HTMLInputElement>(null);
55+
const [isLoading, setIsLoading] = useState(false);
56+
const [shortenedUrl, setShortenedUrl] = useState('');
57+
const [customStringKeyword, setCustomStringKeyword] = useState('');
58+
const playgroundConfiguration = usePlaygroundConfigurationEncoder();
9159

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}&nbsp;
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-
}
60+
const generateLink = () => {
61+
setIsLoading(true);
16562

166-
private toggleButton() {
167-
if (this.props.handleGenerateLz) {
168-
this.props.handleGenerateLz();
169-
}
63+
customStringKeyword;
17064

171-
// reset state
172-
this.setState({ keyword: '', isLoading: false, isSuccess: false });
173-
}
65+
return postSharedProgram(playgroundConfiguration)
66+
.then(({ shortenedUrl }) => setShortenedUrl(shortenedUrl))
67+
.catch(err => showWarningMessage(err.toString()))
68+
.finally(() => setIsLoading(false));
69+
};
17470

175-
private handleChange(event: React.FormEvent<HTMLInputElement>) {
176-
this.setState({ keyword: event.currentTarget.value });
177-
}
71+
const handleCustomStringChange = (event: React.FormEvent<HTMLInputElement>) => {
72+
setCustomStringKeyword(event.currentTarget.value);
73+
};
17874

179-
private selectShareInputText() {
180-
if (this.shareInputElem.current !== null) {
181-
this.shareInputElem.current.focus();
182-
this.shareInputElem.current.select();
75+
// For visual effect of highlighting the text field on copy
76+
const selectShareInputText = () => {
77+
if (shareInputElem.current !== null) {
78+
shareInputElem.current.focus();
79+
shareInputElem.current.select();
18380
}
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-
}
81+
};
82+
83+
const generateLinkPopoverContent = (
84+
<div>
85+
{Constants.urlShortenerBase}&nbsp;
86+
<input
87+
placeholder={'custom string (optional)'}
88+
onChange={handleCustomStringChange}
89+
style={{ width: 175 }}
90+
/>
91+
<ControlButton label="Get Link" icon={IconNames.SHARE} onClick={generateLink} />
92+
</div>
93+
);
94+
95+
const generatingLinkPopoverContent = (
96+
<div>
97+
<NonIdealState
98+
description="Generating Shareable Link..."
99+
icon={<Spinner size={SpinnerSize.SMALL} />}
100+
/>
101+
</div>
102+
);
103+
104+
const sicpCopyLinkPopoverContent = (
105+
<div>
106+
<input defaultValue={props.queryString!} readOnly={true} ref={shareInputElem} />
107+
<Tooltip content="Copy link to clipboard">
108+
<CopyToClipboard text={props.queryString!}>
109+
<ControlButton icon={IconNames.DUPLICATE} onClick={selectShareInputText} />
110+
</CopyToClipboard>
111+
</Tooltip>
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+
: props.isSicp
129+
? sicpCopyLinkPopoverContent
130+
: shortenedUrl
131+
? copyLinkPopoverContent
132+
: generateLinkPopoverContent;
133+
134+
return (
135+
<Popover
136+
popoverClassName="Popover-share"
137+
inheritDarkTheme={false}
138+
content={shareButtonPopoverContent}
139+
>
140+
<Tooltip content="Get shareable link" placement={Position.TOP}>
141+
<ControlButton label="Share" icon={IconNames.SHARE} />
142+
</Tooltip>
143+
</Popover>
144+
);
145+
};

src/commons/sagas/RequestsSaga.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { call } from 'redux-saga/effects';
22
import { backendParamsToProgressStatus } from 'src/features/grading/GradingUtils';
3+
import ShareLinkState from 'src/features/playground/shareLinks/ShareLinkState';
34
import { OptionType } from 'src/pages/academy/teamFormation/subcomponents/TeamFormationForm';
45

56
import {
@@ -44,6 +45,7 @@ import {
4445
UpdateCourseConfiguration,
4546
User
4647
} from '../application/types/SessionTypes';
48+
import { ShareLinkShortenedUrlResponse } from '../application/types/ShareLinkTypes';
4749
import {
4850
Assessment,
4951
AssessmentConfiguration,
@@ -1660,6 +1662,31 @@ export async function deleteDevice(device: Pick<Device, 'id'>, tokens?: Tokens):
16601662
return true;
16611663
}
16621664

1665+
/**
1666+
* POST /shared_programs
1667+
*/
1668+
export async function postSharedProgram(
1669+
programConfig: ShareLinkState,
1670+
tokens?: Tokens
1671+
): Promise<ShareLinkShortenedUrlResponse> {
1672+
tokens = fillTokens(tokens);
1673+
const resp = await request(`shared_programs`, 'POST', {
1674+
body: programConfig,
1675+
...tokens
1676+
});
1677+
1678+
if (!resp) {
1679+
throw new Error('Failed to generate shortened URL!');
1680+
}
1681+
1682+
if (!resp.ok) {
1683+
const message = await resp.text();
1684+
throw new Error(`Failed to generate shortened URL: ${message}`);
1685+
}
1686+
1687+
return resp.json();
1688+
}
1689+
16631690
function fillTokens(tokens?: Tokens): Tokens {
16641691
tokens = tokens || getTokensFromStore();
16651692
if (!tokens) {

src/features/playground/shareLinks/encoder/Encoder.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { EditorTabState } from 'src/commons/workspace/WorkspaceTypes';
88

99
import ShareLinkState from '../ShareLinkState';
1010

11-
export const useUrlEncoder = () => {
11+
export const usePlaygroundConfigurationEncoder = () => {
1212
const isFolderModeEnabled = useTypedSelector(
1313
state => state.workspaces.playground.isFolderModeEnabled
1414
);

src/pages/playground/Playground.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ import {
9191
import ShareLinkStateDecoder from 'src/features/playground/shareLinks/decoder/Decoder';
9292
import JsonDecoderDelegate from 'src/features/playground/shareLinks/decoder/delegates/JsonDecoderDelegate';
9393
import UrlParamsDecoderDelegate from 'src/features/playground/shareLinks/decoder/delegates/UrlParamsDecoderDelegate';
94-
import { useUrlEncoder } from 'src/features/playground/shareLinks/encoder/Encoder';
94+
import { usePlaygroundConfigurationEncoder } from 'src/features/playground/shareLinks/encoder/Encoder';
9595
import ShareLinkState from 'src/features/playground/shareLinks/ShareLinkState';
9696

9797
import {
@@ -445,7 +445,7 @@ const Playground: React.FC<PlaygroundProps> = props => {
445445

446446
const hash = isSicpEditor ? props.initialEditorValueHash : location.hash;
447447
const { uuid } = useParams<{ uuid: string }>();
448-
const config = useUrlEncoder();
448+
const config = usePlaygroundConfigurationEncoder();
449449
const tokens = useTypedSelector((state: OverallState) => ({
450450
accessToken: state.session.accessToken,
451451
refreshToken: state.session.refreshToken

0 commit comments

Comments
 (0)