Skip to content

Commit d63f217

Browse files
committed
Migrate external URL shortener request out of sagas
1 parent 177f873 commit d63f217

File tree

8 files changed

+84
-222
lines changed

8 files changed

+84
-222
lines changed

src/commons/controlBar/ControlBarShareButton.tsx

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,18 @@ import { useHotkeys } from '@mantine/hooks';
44
import React, { useRef, useState } from 'react';
55
import * as CopyToClipboard from 'react-copy-to-clipboard';
66
import JsonEncoderDelegate from 'src/features/playground/shareLinks/encoder/delegates/JsonEncoderDelegate';
7+
import UrlParamsEncoderDelegate from 'src/features/playground/shareLinks/encoder/delegates/UrlParamsEncoderDelegate';
78
import { usePlaygroundConfigurationEncoder } from 'src/features/playground/shareLinks/encoder/EncoderHooks';
89

910
import ControlButton from '../ControlButton';
11+
import { externalUrlShortenerRequest } from '../sagas/PlaygroundSaga';
1012
import { postSharedProgram } from '../sagas/RequestsSaga';
11-
import Constants from '../utils/Constants';
12-
import { showWarningMessage } from '../utils/notifications/NotificationsHelper';
13+
import Constants, { Links } from '../utils/Constants';
14+
import { showSuccessMessage, showWarningMessage } from '../utils/notifications/NotificationsHelper';
1315
import { request } from '../utils/RequestHelper';
1416
import { RemoveLast } from '../utils/TypeHelper';
1517

16-
type ControlBarShareButtonProps = DispatchProps & StateProps;
17-
18-
type DispatchProps = {
19-
handleGenerateLz?: () => void;
20-
handleShortenURL: (s: string) => void;
21-
handleUpdateShortURL: (s: string) => void;
22-
};
23-
24-
type StateProps = {
25-
queryString?: string;
26-
shortURL?: string;
27-
key: string;
18+
type ControlBarShareButtonProps = {
2819
isSicp?: boolean;
2920
};
3021

@@ -37,14 +28,29 @@ export const requestToShareProgram = async (
3728
return resp;
3829
};
3930

31+
/**
32+
* Generates the share link for programs in the Playground.
33+
*
34+
* For playground-only (no backend) deployments:
35+
* - Generate a URL with playground configuration encoded as hash parameters
36+
* - URL sent to external URL shortener service
37+
* - Shortened URL displayed to user
38+
* - (note: SICP CodeSnippets use these hash parameters)
39+
*
40+
* For 'with backend' deployments:
41+
* - Send the playground configuration to the backend
42+
* - Backend stores configuration and assigns a UUID
43+
* - Backend pings the external URL shortener service with UUID link
44+
* - Shortened URL returned to Frontend and displayed to user
45+
*/
4046
export const ControlBarShareButton: React.FC<ControlBarShareButtonProps> = props => {
4147
const shareInputElem = useRef<HTMLInputElement>(null);
4248
const [isLoading, setIsLoading] = useState(false);
4349
const [shortenedUrl, setShortenedUrl] = useState('');
4450
const [customStringKeyword, setCustomStringKeyword] = useState('');
4551
const playgroundConfiguration = usePlaygroundConfigurationEncoder();
4652

47-
const generateLink = () => {
53+
const generateLinkBackend = () => {
4854
setIsLoading(true);
4955

5056
customStringKeyword;
@@ -56,6 +62,32 @@ export const ControlBarShareButton: React.FC<ControlBarShareButtonProps> = props
5662
.catch(err => showWarningMessage(err.toString()))
5763
.finally(() => setIsLoading(false));
5864
};
65+
66+
const generateLinkPlaygroundOnly = () => {
67+
const hash = playgroundConfiguration.encodeWith(new UrlParamsEncoderDelegate());
68+
setIsLoading(true);
69+
70+
return externalUrlShortenerRequest(hash, customStringKeyword)
71+
.then(({ shortenedUrl, message }) => {
72+
setShortenedUrl(shortenedUrl);
73+
if (message) showSuccessMessage(message);
74+
})
75+
.catch(err => showWarningMessage(err.toString()))
76+
.finally(() => setIsLoading(false));
77+
};
78+
79+
const generateLinkSicp = () => {
80+
const hash = playgroundConfiguration.encodeWith(new UrlParamsEncoderDelegate());
81+
const shortenedUrl = `${Links.playground}#${hash}`;
82+
setShortenedUrl(shortenedUrl);
83+
};
84+
85+
const generateLink = props.isSicp
86+
? generateLinkSicp
87+
: Constants.playgroundOnly
88+
? generateLinkPlaygroundOnly
89+
: generateLinkBackend;
90+
5991
useHotkeys([['ctrl+w', generateLink]], []);
6092

6193
const handleCustomStringChange = (event: React.FormEvent<HTMLInputElement>) => {
@@ -91,17 +123,6 @@ export const ControlBarShareButton: React.FC<ControlBarShareButtonProps> = props
91123
</div>
92124
);
93125

94-
const sicpCopyLinkPopoverContent = (
95-
<div>
96-
<input defaultValue={props.queryString!} readOnly={true} ref={shareInputElem} />
97-
<Tooltip content="Copy link to clipboard">
98-
<CopyToClipboard text={props.queryString!}>
99-
<ControlButton icon={IconNames.DUPLICATE} onClick={selectShareInputText} />
100-
</CopyToClipboard>
101-
</Tooltip>
102-
</div>
103-
);
104-
105126
const copyLinkPopoverContent = (
106127
<div key={shortenedUrl}>
107128
<input defaultValue={shortenedUrl} readOnly={true} ref={shareInputElem} />
@@ -115,8 +136,6 @@ export const ControlBarShareButton: React.FC<ControlBarShareButtonProps> = props
115136

116137
const shareButtonPopoverContent = isLoading
117138
? generatingLinkPopoverContent
118-
: props.isSicp
119-
? sicpCopyLinkPopoverContent
120139
: shortenedUrl
121140
? copyLinkPopoverContent
122141
: generateLinkPopoverContent;

src/commons/sagas/PlaygroundSaga.ts

Lines changed: 30 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,13 @@
1-
import { FSModule } from 'browserfs/dist/node/core/FS';
2-
import { Chapter, Variant } from 'js-slang/dist/types';
3-
import { compressToEncodedURIComponent } from 'lz-string';
4-
import qs from 'query-string';
1+
import { Chapter } from 'js-slang/dist/types';
52
import { SagaIterator } from 'redux-saga';
6-
import { call, delay, put, race, select } from 'redux-saga/effects';
3+
import { call, put, select } from 'redux-saga/effects';
74
import CseMachine from 'src/features/cseMachine/CseMachine';
85
import { CseMachine as JavaCseMachine } from 'src/features/cseMachine/java/CseMachine';
96

10-
import {
11-
changeQueryString,
12-
shortenURL,
13-
updateShortURL
14-
} from '../../features/playground/PlaygroundActions';
15-
import { GENERATE_LZ_STRING, SHORTEN_URL } from '../../features/playground/PlaygroundTypes';
167
import { isSourceLanguage, OverallState } from '../application/ApplicationTypes';
17-
import { ExternalLibraryName } from '../application/types/ExternalTypes';
18-
import { retrieveFilesInWorkspaceAsRecord } from '../fileSystem/utils';
198
import { visitSideContent } from '../sideContent/SideContentActions';
209
import { SideContentType, VISIT_SIDE_CONTENT } from '../sideContent/SideContentTypes';
2110
import Constants from '../utils/Constants';
22-
import { showSuccessMessage, showWarningMessage } from '../utils/notifications/NotificationsHelper';
2311
import {
2412
clearReplOutput,
2513
setEditorHighlightedLines,
@@ -29,46 +17,10 @@ import {
2917
updateCurrentStep,
3018
updateStepsTotal
3119
} from '../workspace/WorkspaceActions';
32-
import { EditorTabState, PlaygroundWorkspaceState } from '../workspace/WorkspaceTypes';
20+
import { PlaygroundWorkspaceState } from '../workspace/WorkspaceTypes';
3321
import { safeTakeEvery as takeEvery } from './SafeEffects';
3422

3523
export default function* PlaygroundSaga(): SagaIterator {
36-
yield takeEvery(GENERATE_LZ_STRING, updateQueryString);
37-
38-
yield takeEvery(SHORTEN_URL, function* (action: ReturnType<typeof shortenURL>): any {
39-
const queryString = yield select((state: OverallState) => state.playground.queryString);
40-
const keyword = action.payload;
41-
const errorMsg = 'ERROR';
42-
43-
let resp, timeout;
44-
45-
//we catch and move on if there are errors (plus have a timeout in case)
46-
try {
47-
const { result, hasTimedOut } = yield race({
48-
result: call(shortenURLRequest, queryString, keyword),
49-
hasTimedOut: delay(10000)
50-
});
51-
52-
resp = result;
53-
timeout = hasTimedOut;
54-
} catch (_) {}
55-
56-
if (!resp || timeout) {
57-
yield put(updateShortURL(errorMsg));
58-
return yield call(showWarningMessage, 'Something went wrong trying to create the link.');
59-
}
60-
61-
if (resp.status !== 'success' && !resp.shorturl) {
62-
yield put(updateShortURL(errorMsg));
63-
return yield call(showWarningMessage, resp.message);
64-
}
65-
66-
if (resp.status !== 'success') {
67-
yield call(showSuccessMessage, resp.message);
68-
}
69-
yield put(updateShortURL(Constants.urlShortenerBase + resp.url.keyword));
70-
});
71-
7224
yield takeEvery(
7325
VISIT_SIDE_CONTENT,
7426
function* ({
@@ -126,60 +78,30 @@ export default function* PlaygroundSaga(): SagaIterator {
12678
);
12779
}
12880

129-
function* updateQueryString() {
130-
const isFolderModeEnabled: boolean = yield select(
131-
(state: OverallState) => state.workspaces.playground.isFolderModeEnabled
132-
);
133-
const fileSystem: FSModule = yield select(
134-
(state: OverallState) => state.fileSystem.inBrowserFileSystem
135-
);
136-
const files: Record<string, string> = yield call(
137-
retrieveFilesInWorkspaceAsRecord,
138-
'playground',
139-
fileSystem
140-
);
141-
const editorTabs: EditorTabState[] = yield select(
142-
(state: OverallState) => state.workspaces.playground.editorTabs
143-
);
144-
const editorTabFilePaths = editorTabs
145-
.map((editorTab: EditorTabState) => editorTab.filePath)
146-
.filter((filePath): filePath is string => filePath !== undefined);
147-
const activeEditorTabIndex: number | null = yield select(
148-
(state: OverallState) => state.workspaces.playground.activeEditorTabIndex
149-
);
150-
const chapter: Chapter = yield select(
151-
(state: OverallState) => state.workspaces.playground.context.chapter
152-
);
153-
const variant: Variant = yield select(
154-
(state: OverallState) => state.workspaces.playground.context.variant
155-
);
156-
const external: ExternalLibraryName = yield select(
157-
(state: OverallState) => state.workspaces.playground.externalLibrary
158-
);
159-
const execTime: number = yield select(
160-
(state: OverallState) => state.workspaces.playground.execTime
161-
);
162-
const newQueryString = qs.stringify({
163-
isFolder: isFolderModeEnabled,
164-
files: compressToEncodedURIComponent(qs.stringify(files)),
165-
tabs: editorTabFilePaths.map(compressToEncodedURIComponent),
166-
tabIdx: activeEditorTabIndex,
167-
chap: chapter,
168-
variant,
169-
ext: external,
170-
exec: execTime
171-
});
172-
yield put(changeQueryString(newQueryString));
173-
}
174-
81+
type UrlShortenerResponse = {
82+
status: string;
83+
code: string;
84+
url: {
85+
keyword: string;
86+
url: string;
87+
title: string;
88+
date: string;
89+
ip: string;
90+
clicks: string;
91+
};
92+
message: string;
93+
title: string;
94+
shorturl: string;
95+
statusCode: number;
96+
};
17597
/**
17698
* Gets short url from microservice
17799
* @returns {(Response|null)} Response if successful, otherwise null.
178100
*/
179-
export async function shortenURLRequest(
101+
export async function externalUrlShortenerRequest(
180102
queryString: string,
181103
keyword: string
182-
): Promise<Response | null> {
104+
): Promise<{ shortenedUrl: string; message: string }> {
183105
const url = `${window.location.protocol}//${window.location.host}/playground#${queryString}`;
184106

185107
const params = {
@@ -199,9 +121,15 @@ export async function shortenURLRequest(
199121

200122
const resp = await fetch(`${Constants.urlShortenerBase}yourls-api.php`, fetchOpts);
201123
if (!resp || !resp.ok) {
202-
return null;
124+
throw new Error('Something went wrong trying to create the link.');
125+
}
126+
127+
const res: UrlShortenerResponse = await resp.json();
128+
if (res.status !== 'success' && !res.shorturl) {
129+
throw new Error(res.message);
203130
}
204131

205-
const res = await resp.json();
206-
return res;
132+
const message = res.status !== 'success' ? res.message : '';
133+
const shortenedUrl = Constants.urlShortenerBase + res.url.keyword;
134+
return { shortenedUrl, message };
207135
}

src/features/playground/PlaygroundActions.ts

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,11 @@ import { SALanguage } from 'src/commons/application/ApplicationTypes';
33

44
import { PersistenceFile } from '../persistence/PersistenceTypes';
55
import {
6-
CHANGE_QUERY_STRING,
7-
GENERATE_LZ_STRING,
86
PLAYGROUND_UPDATE_GITHUB_SAVE_INFO,
97
PLAYGROUND_UPDATE_LANGUAGE_CONFIG,
10-
PLAYGROUND_UPDATE_PERSISTENCE_FILE,
11-
SHORTEN_URL,
12-
UPDATE_SHORT_URL
8+
PLAYGROUND_UPDATE_PERSISTENCE_FILE
139
} from './PlaygroundTypes';
1410

15-
export const generateLzString = createAction(GENERATE_LZ_STRING, () => ({ payload: {} }));
16-
17-
export const shortenURL = createAction(SHORTEN_URL, (keyword: string) => ({ payload: keyword }));
18-
19-
export const updateShortURL = createAction(UPDATE_SHORT_URL, (shortURL: string) => ({
20-
payload: shortURL
21-
}));
22-
23-
export const changeQueryString = createAction(CHANGE_QUERY_STRING, (queryString: string) => ({
24-
payload: queryString
25-
}));
26-
2711
export const playgroundUpdatePersistenceFile = createAction(
2812
PLAYGROUND_UPDATE_PERSISTENCE_FILE,
2913
(file?: PersistenceFile) => ({ payload: file })

src/features/playground/PlaygroundReducer.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,17 @@ import { Reducer } from 'redux';
33
import { defaultPlayground } from '../../commons/application/ApplicationTypes';
44
import { SourceActionType } from '../../commons/utils/ActionsHelper';
55
import {
6-
CHANGE_QUERY_STRING,
76
PLAYGROUND_UPDATE_GITHUB_SAVE_INFO,
87
PLAYGROUND_UPDATE_LANGUAGE_CONFIG,
98
PLAYGROUND_UPDATE_PERSISTENCE_FILE,
10-
PlaygroundState,
11-
UPDATE_SHORT_URL
9+
PlaygroundState
1210
} from './PlaygroundTypes';
1311

1412
export const PlaygroundReducer: Reducer<PlaygroundState, SourceActionType> = (
1513
state = defaultPlayground,
1614
action
1715
) => {
1816
switch (action.type) {
19-
case CHANGE_QUERY_STRING:
20-
return {
21-
...state,
22-
queryString: action.payload
23-
};
24-
case UPDATE_SHORT_URL:
25-
return {
26-
...state,
27-
shortURL: action.payload
28-
};
2917
case PLAYGROUND_UPDATE_GITHUB_SAVE_INFO:
3018
return {
3119
...state,

src/features/playground/PlaygroundTypes.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,11 @@ import { SALanguage } from 'src/commons/application/ApplicationTypes';
33
import { GitHubSaveInfo } from '../github/GitHubTypes';
44
import { PersistenceFile } from '../persistence/PersistenceTypes';
55

6-
export const CHANGE_QUERY_STRING = 'CHANGE_QUERY_STRING';
7-
export const GENERATE_LZ_STRING = 'GENERATE_LZ_STRING';
8-
export const SHORTEN_URL = 'SHORTEN_URL';
9-
export const UPDATE_SHORT_URL = 'UPDATE_SHORT_URL';
106
export const PLAYGROUND_UPDATE_GITHUB_SAVE_INFO = 'PLAYGROUND_UPDATE_GITHUB_SAVE_INFO';
117
export const PLAYGROUND_UPDATE_PERSISTENCE_FILE = 'PLAYGROUND_UPDATE_PERSISTENCE_FILE';
128
export const PLAYGROUND_UPDATE_LANGUAGE_CONFIG = 'PLAYGROUND_UPDATE_LANGUAGE_CONFIG';
139

1410
export type PlaygroundState = {
15-
readonly queryString?: string;
16-
readonly shortURL?: string;
1711
readonly persistenceFile?: PersistenceFile;
1812
readonly githubSaveInfo: GitHubSaveInfo;
1913
readonly languageConfig: SALanguage;

src/features/playground/__tests__/PlaygroundActions.ts

Lines changed: 0 additions & 19 deletions
This file was deleted.

0 commit comments

Comments
 (0)