-
Notifications
You must be signed in to change notification settings - Fork 172
Shared links frontend #2890
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Shared links frontend #2890
Changes from 17 commits
635cde8
c4c0747
5b655e0
2b99c40
91b84e1
5218f9d
a67a6b7
b6f036b
2315ede
d133925
0f0fb61
76c2178
0a356ca
55c81a8
557d23a
ccb8023
91a0a1e
8518e65
433c81e
bfe97cc
b1842b4
6e7dcc4
7bd35c8
cf4c625
cc412f0
49537fc
5618a03
fe9eea2
d7333da
840e5d5
cd4760e
9b701f0
b48948a
ecb2063
bde45d1
2cf4672
59e8106
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,11 +20,13 @@ type StateProps = { | |
shortURL?: string; | ||
key: string; | ||
isSicp?: boolean; | ||
programConfig: object; | ||
Rachelcoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}; | ||
|
||
type State = { | ||
keyword: string; | ||
isLoading: boolean; | ||
isSuccess: boolean; | ||
}; | ||
|
||
export class ControlBarShareButton extends React.PureComponent<ControlBarShareButtonProps, State> { | ||
|
@@ -36,9 +38,30 @@ export class ControlBarShareButton extends React.PureComponent<ControlBarShareBu | |
this.handleChange = this.handleChange.bind(this); | ||
this.toggleButton = this.toggleButton.bind(this); | ||
this.shareInputElem = React.createRef(); | ||
this.state = { keyword: '', isLoading: false }; | ||
this.state = { keyword: '', isLoading: false, isSuccess: false }; | ||
} | ||
|
||
componentDidMount() { | ||
document.addEventListener('keydown', this.handleKeyDown); | ||
chownces marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
componentWillUnmount() { | ||
document.removeEventListener('keydown', this.handleKeyDown); | ||
} | ||
|
||
handleKeyDown = (event: any) => { | ||
if (event.key === 'Enter' && event.ctrlKey) { | ||
// console.log('Ctrl+Enter pressed!'); | ||
this.setState({ keyword: 'Test' }); | ||
this.props.handleShortenURL(this.state.keyword); | ||
this.setState({ isLoading: true }); | ||
if (this.props.shortURL || this.props.isSicp) { | ||
this.selectShareInputText(); | ||
console.log('link created.'); | ||
} | ||
} | ||
}; | ||
|
||
public render() { | ||
const shareButtonPopoverContent = | ||
this.props.queryString === undefined ? ( | ||
|
@@ -57,7 +80,7 @@ export class ControlBarShareButton extends React.PureComponent<ControlBarShareBu | |
</div> | ||
) : ( | ||
<> | ||
{!this.props.shortURL || this.props.shortURL === 'ERROR' ? ( | ||
{!this.state.isSuccess || this.props.shortURL === 'ERROR' ? ( | ||
Rachelcoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
!this.state.isLoading || this.props.shortURL === 'ERROR' ? ( | ||
<div> | ||
{Constants.urlShortenerBase} | ||
|
@@ -66,12 +89,36 @@ export class ControlBarShareButton extends React.PureComponent<ControlBarShareBu | |
onChange={this.handleChange} | ||
style={{ width: 175 }} | ||
/> | ||
<>{console.log(this.props.programConfig)}</> | ||
Rachelcoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<ControlButton | ||
label="Get Link" | ||
icon={IconNames.SHARE} | ||
onClick={() => { | ||
this.props.handleShortenURL(this.state.keyword); | ||
this.setState({ isLoading: true }); | ||
// post request to backend, set keyword as return uuid | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please abstract this into the appropriate file together with the rest of the API callers. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I abstract it out as a class function. Since it iteract closedly with class field variables I still keep it inside the class. |
||
const requestBody = { | ||
shared_program: { | ||
data: this.props.programConfig | ||
} | ||
}; | ||
const fetchOpts: RequestInit = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use our request helper/wrapper instead of manual There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry I just figured out how to use this, will be updated at next push. |
||
method: 'POST', | ||
body: JSON.stringify(requestBody), | ||
headers: { | ||
'Content-Type': 'application/json' | ||
} | ||
}; | ||
fetch('http://localhost:4000/api/shared_programs', fetchOpts) | ||
Rachelcoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
.then(res => { | ||
return res.json(); | ||
}) | ||
.then(resp => { | ||
this.setState({ | ||
keyword: 'http://localhost:8000/playground/share/' + resp.uuid | ||
RichDom2185 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); | ||
console.log(resp); | ||
Rachelcoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}) | ||
.catch(err => console.log('Error: ', err)); | ||
RichDom2185 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
this.setState({ isLoading: true, isSuccess: true }); | ||
}} | ||
/> | ||
</div> | ||
|
@@ -84,10 +131,13 @@ export class ControlBarShareButton extends React.PureComponent<ControlBarShareBu | |
</div> | ||
) | ||
) : ( | ||
<div key={this.props.shortURL}> | ||
<input defaultValue={this.props.shortURL} readOnly={true} ref={this.shareInputElem} /> | ||
// <div key={this.props.shortURL}> | ||
Rachelcoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<div key={this.state.keyword}> | ||
{/* <input defaultValue={this.props.shortURL} readOnly={true} ref={this.shareInputElem} /> */} | ||
<input defaultValue={this.state.keyword} readOnly={true} ref={this.shareInputElem} /> | ||
<Tooltip2 content="Copy link to clipboard"> | ||
<CopyToClipboard text={this.props.shortURL}> | ||
{/* <CopyToClipboard text={this.props.shortURL}> */} | ||
<CopyToClipboard text={this.state.keyword}> | ||
<ControlButton icon={IconNames.DUPLICATE} onClick={this.selectShareInputText} /> | ||
</CopyToClipboard> | ||
</Tooltip2> | ||
|
@@ -121,8 +171,8 @@ export class ControlBarShareButton extends React.PureComponent<ControlBarShareBu | |
} | ||
|
||
// reset state | ||
this.props.handleUpdateShortURL(''); | ||
this.setState({ keyword: '', isLoading: false }); | ||
// this.props.handleUpdateShortURL(''); | ||
this.setState({ keyword: '', isLoading: false, isSuccess: false }); | ||
} | ||
|
||
private handleChange(event: React.FormEvent<HTMLInputElement>) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
import { FSModule } from 'browserfs/dist/node/core/FS'; | ||
import { Chapter, Variant } from 'js-slang/dist/types'; | ||
import { decompressFromEncodedURIComponent } from 'lz-string'; | ||
import { Dispatch } from 'react'; | ||
import { AnyAction } from 'redux'; | ||
import { getDefaultFilePath, getLanguageConfig } from 'src/commons/application/ApplicationTypes'; | ||
import { overwriteFilesInWorkspace } from 'src/commons/fileSystem/utils'; | ||
import { | ||
showFullJSWarningOnUrlLoad, | ||
showFulTSWarningOnUrlLoad, | ||
showHTMLDisclaimer | ||
} from 'src/commons/utils/WarningDialogHelper'; | ||
import { | ||
addEditorTab, | ||
removeEditorTabsForDirectory, | ||
setFolderMode, | ||
updateActiveEditorTabIndex | ||
} from 'src/commons/workspace/WorkspaceActions'; | ||
import { WorkspaceLocation } from 'src/commons/workspace/WorkspaceTypes'; | ||
import { playgroundConfigLanguage } from 'src/features/playground/PlaygroundActions'; | ||
|
||
import { convertParamToBoolean, convertParamToInt } from '../../commons/utils/ParamParseHelper'; | ||
import { IParsedQuery, parseQuery } from '../../commons/utils/QueryHelper'; | ||
import { WORKSPACE_BASE_PATHS } from '../fileSystem/createInBrowserFileSystem'; | ||
|
||
export type programConfig = { | ||
Rachelcoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
isFolder: string | undefined; | ||
tabs: string | undefined; | ||
tabIdx: string | undefined; | ||
chap: string | undefined; | ||
variant: string | undefined; | ||
ext: string | undefined; | ||
exec: string | undefined; | ||
files: string | undefined; | ||
prgrm: string | undefined; | ||
Rachelcoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}; | ||
/** | ||
* #chap=4 | ||
* exec=1000 | ||
* ext=NONE | ||
* files=KQJgYgDgNghgngcwE4HsCuA7AJqSrkwC2AdAFYDOAvEA | ||
* isFolder=false | ||
* tabIdx=0 | ||
* tabs=PQBwNghgng5gTgewK4DsAmpHwgWwHQBWAzkA | ||
* variant=default | ||
*/ | ||
export const Decoder = { | ||
Rachelcoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
decodeString: function (inputString: string) { | ||
const qs: Partial<IParsedQuery> = parseQuery(inputString); | ||
return { | ||
chap: qs.chap, | ||
exec: qs.exec, | ||
files: qs.files, | ||
isFolder: qs.isFolder, | ||
tabIdx: qs.tabIdx, | ||
tabs: qs.tabs, | ||
variant: qs.variant, | ||
prgrm: qs.prgrm, | ||
ext: qs.ext | ||
}; | ||
}, | ||
|
||
decodeJSON: function (inputJSON: string) { | ||
Rachelcoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const jsonObject = JSON.parse(inputJSON); | ||
return jsonObject.data; | ||
} | ||
}; | ||
|
||
export async function resetConfig( | ||
configObj: programConfig, | ||
handlers: { | ||
handleChapterSelect: (chapter: Chapter, variant: Variant) => void; | ||
handleChangeExecTime: (execTime: number) => void; | ||
}, | ||
workspaceLocation: WorkspaceLocation, | ||
dispatch: Dispatch<AnyAction>, | ||
fileSystem: FSModule | null | ||
) { | ||
const chapter = convertParamToInt(configObj.chap?.toString()) ?? undefined; | ||
if (chapter === Chapter.FULL_JS) { | ||
showFullJSWarningOnUrlLoad(); | ||
} else if (chapter === Chapter.FULL_TS) { | ||
showFulTSWarningOnUrlLoad(); | ||
} else { | ||
if (chapter === Chapter.HTML) { | ||
const continueToHtml = await showHTMLDisclaimer(); | ||
if (!continueToHtml) { | ||
return; | ||
} | ||
} | ||
|
||
// For backward compatibility with old share links - 'prgrm' is no longer used. | ||
const program = | ||
configObj.prgrm === undefined ? '' : decompressFromEncodedURIComponent(configObj.prgrm); | ||
|
||
// By default, create just the default file. | ||
const defaultFilePath = getDefaultFilePath(workspaceLocation); | ||
const files: Record<string, string> = | ||
configObj.files === undefined | ||
? { | ||
[defaultFilePath]: program | ||
} | ||
: parseQuery(decompressFromEncodedURIComponent(configObj.files)); | ||
if (fileSystem !== null) { | ||
await overwriteFilesInWorkspace(workspaceLocation, fileSystem, files); | ||
} | ||
|
||
// BrowserFS does not provide a way of listening to changes in the file system, which makes | ||
// updating the file system view troublesome. To force the file system view to re-render | ||
// (and thus display the updated file system), we first disable Folder mode. | ||
dispatch(setFolderMode(workspaceLocation, false)); | ||
const isFolderModeEnabled = convertParamToBoolean(configObj.isFolder?.toString()) ?? false; | ||
|
||
// If Folder mode should be enabled, enabling it after disabling it earlier will cause the | ||
// newly-added files to be shown. Note that this has to take place after the files are | ||
// already added to the file system. | ||
dispatch(setFolderMode(workspaceLocation, isFolderModeEnabled)); | ||
|
||
// By default, open a single editor tab containing the default playground file. | ||
const editorTabFilePaths = configObj.tabs | ||
?.split(',') | ||
.map(decompressFromEncodedURIComponent) ?? [defaultFilePath]; | ||
|
||
// Remove all editor tabs before populating with the ones from the query string. | ||
dispatch( | ||
removeEditorTabsForDirectory(workspaceLocation, WORKSPACE_BASE_PATHS[workspaceLocation]) | ||
); | ||
// Add editor tabs from the query string. | ||
editorTabFilePaths.forEach(filePath => | ||
// Fall back on the empty string if the file contents do not exist. | ||
dispatch(addEditorTab(workspaceLocation, filePath, files[filePath] ?? '')) | ||
); | ||
|
||
// By default, use the first editor tab. | ||
const activeEditorTabIndex = convertParamToInt(configObj.tabIdx?.toString()) ?? 0; | ||
dispatch(updateActiveEditorTabIndex(workspaceLocation, activeEditorTabIndex)); | ||
if (chapter) { | ||
// TODO: To migrate the state logic away from playgroundSourceChapter | ||
// and playgroundSourceVariant into the language config instead | ||
const languageConfig = getLanguageConfig(chapter, configObj.variant as Variant); | ||
handlers.handleChapterSelect(chapter, languageConfig.variant); | ||
// Hardcoded for Playground only for now, while we await workspace refactoring | ||
// to decouple the SicpWorkspace from the Playground. | ||
dispatch(playgroundConfigLanguage(languageConfig)); | ||
} | ||
|
||
const execTime = Math.max( | ||
convertParamToInt(configObj.exec?.toString() || '1000') || 1000, | ||
1000 | ||
); | ||
if (execTime) { | ||
handlers.handleChangeExecTime(execTime); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { FSModule } from 'browserfs/dist/node/core/FS'; | ||
import { Chapter, Variant } from 'js-slang/dist/types'; | ||
import { compressToEncodedURIComponent } from 'lz-string'; | ||
import qs from 'query-string'; | ||
import { useState } from 'react'; | ||
// import { OverallState } from 'src/commons/application/ApplicationTypes'; | ||
import { retrieveFilesInWorkspaceAsRecord } from 'src/commons/fileSystem/utils'; | ||
import { useTypedSelector } from 'src/commons/utils/Hooks'; | ||
import { EditorTabState } from 'src/commons/workspace/WorkspaceTypes'; | ||
|
||
export const EncodeURL = () => { | ||
Rachelcoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const isFolderModeEnabled: boolean = useTypedSelector( | ||
Rachelcoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
state => state.workspaces.playground.isFolderModeEnabled | ||
); | ||
|
||
const editorTabs: EditorTabState[] = useTypedSelector( | ||
Rachelcoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
state => state.workspaces.playground.editorTabs | ||
); | ||
const editorTabFilePaths = editorTabs | ||
.map((editorTab: EditorTabState) => editorTab.filePath) | ||
.filter((filePath): filePath is string => filePath !== undefined); | ||
const activeEditorTabIndex: number | null = useTypedSelector( | ||
state => state.workspaces.playground.activeEditorTabIndex | ||
); | ||
const chapter: Chapter = useTypedSelector(state => state.workspaces.playground.context.chapter); | ||
const variant: Variant = useTypedSelector(state => state.workspaces.playground.context.variant); | ||
const execTime: number = useTypedSelector(state => state.workspaces.playground.execTime); | ||
Rachelcoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const fileSystem: FSModule = GetFileSystem(); | ||
|
||
const result: object = { | ||
Rachelcoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
isFolder: isFolderModeEnabled, | ||
files: GetFile(fileSystem), | ||
tabs: editorTabFilePaths.map(compressToEncodedURIComponent)[0], | ||
tabIdx: activeEditorTabIndex, | ||
chap: chapter, | ||
variant, | ||
ext: 'NONE', | ||
exec: execTime | ||
}; | ||
|
||
return result; | ||
}; | ||
|
||
const GetFileSystem = () => { | ||
Rachelcoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const fileSystem: FSModule | null = useTypedSelector( | ||
state => state.fileSystem.inBrowserFileSystem | ||
); | ||
return fileSystem as FSModule; | ||
Rachelcoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}; | ||
|
||
const GetFile = (fileSystem: FSModule) => { | ||
const [files, setFiles] = useState<Record<string, string>>(); | ||
retrieveFilesInWorkspaceAsRecord('playground', fileSystem).then( | ||
(result: Record<string, string>) => { | ||
Rachelcoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
setFiles(result); | ||
} | ||
); | ||
return compressToEncodedURIComponent(qs.stringify(files as Record<string, string>)); | ||
Rachelcoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}; |
Uh oh!
There was an error while loading. Please reload this page.