Skip to content
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

Draft
wants to merge 37 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
635cde8
connect shared links creation with backend database
Rachelcoll Mar 28, 2024
c4c0747
shared links procedure change
Rachelcoll Mar 28, 2024
5b655e0
shared links url change
Rachelcoll Mar 30, 2024
2b99c40
Merge branch 'master' into shared-links-frontend
RichDom2185 Mar 30, 2024
91b84e1
delete unnecessary lines
Rachelcoll Mar 31, 2024
5218f9d
Merge branch 'shared-links-frontend' of https://github.com/source-aca…
Rachelcoll Mar 31, 2024
a67a6b7
Merge branch 'master' into shared-links-frontend
RichDom2185 Apr 1, 2024
b6f036b
Fix format errors
RichDom2185 Apr 1, 2024
2315ede
Revert lockfile change
RichDom2185 Apr 1, 2024
d133925
Revert TS config change
RichDom2185 Apr 1, 2024
0f0fb61
test check
Rachelcoll Apr 3, 2024
76c2178
Merge branch 'shared-links-frontend' of https://github.com/source-aca…
Rachelcoll Apr 3, 2024
0a356ca
format
Rachelcoll Apr 4, 2024
55c81a8
format
Rachelcoll Apr 4, 2024
557d23a
format
Rachelcoll Apr 4, 2024
ccb8023
Merge branch 'master' into shared-links-frontend
RichDom2185 Apr 6, 2024
91a0a1e
Fix incorrect merge resolution
RichDom2185 Apr 6, 2024
8518e65
Add OOP-oriented implementation of encoding and decoding of share lin…
chownces Apr 7, 2024
433c81e
debug and add decoder oop
Rachelcoll Apr 12, 2024
bfe97cc
debug and add decoder oop
Rachelcoll Apr 12, 2024
b1842b4
Merge branch 'master' into shared-links-frontend
martin-henz Apr 13, 2024
6e7dcc4
remove decoder oop and fix bugs
Rachelcoll Apr 13, 2024
7bd35c8
Merge branch 'master' into shared-links-frontend
martin-henz Apr 13, 2024
cf4c625
Merge branch 'master' into shared-links-frontend
martin-henz Apr 13, 2024
cc412f0
change request method and fix bugs
Rachelcoll Apr 13, 2024
49537fc
Merge branch 'shared-links-frontend' of https://github.com/source-aca…
Rachelcoll Apr 13, 2024
5618a03
Revert lockfile changes
RichDom2185 Apr 13, 2024
fe9eea2
Merge branch 'master' of https://github.com/source-academy/frontend i…
RichDom2185 Apr 13, 2024
d7333da
Merge branch 'master' into shared-links-frontend
RichDom2185 Apr 13, 2024
840e5d5
Merge branch 'master' into shared-links-frontend
RichDom2185 Apr 14, 2024
cd4760e
Shared links frontend refactor (#2937)
chownces Apr 15, 2024
9b701f0
Merge branch 'master' of https://github.com/source-academy/frontend i…
RichDom2185 Apr 15, 2024
b48948a
Merge branch 'master' of https://github.com/source-academy/frontend i…
RichDom2185 May 5, 2024
ecb2063
Merge branch 'master' of https://github.com/source-academy/frontend i…
RichDom2185 May 6, 2024
bde45d1
Merge branch 'master' of https://github.com/source-academy/frontend i…
RichDom2185 May 12, 2024
2cf4672
Merge branch 'master' into shared-links-updated
chownces May 16, 2024
59e8106
Remove redundant playground saga test
chownces May 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 61 additions & 11 deletions src/commons/controlBar/ControlBarShareButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { IconNames } from '@blueprintjs/icons';
import { Popover2, Tooltip2 } from '@blueprintjs/popover2';
import React from 'react';
import * as CopyToClipboard from 'react-copy-to-clipboard';
import ShareLinkState from 'src/features/playground/shareLinks/ShareLinkState';

import ControlButton from '../ControlButton';
import Constants from '../utils/Constants';
import { showWarningMessage } from '../utils/notifications/NotificationsHelper';

type ControlBarShareButtonProps = DispatchProps & StateProps;

Expand All @@ -20,11 +22,13 @@ type StateProps = {
shortURL?: string;
key: string;
isSicp?: boolean;
programConfig: ShareLinkState;
};

type State = {
keyword: string;
isLoading: boolean;
isSuccess: boolean;
};

export class ControlBarShareButton extends React.PureComponent<ControlBarShareButtonProps, State> {
Expand All @@ -35,10 +39,32 @@ export class ControlBarShareButton extends React.PureComponent<ControlBarShareBu
this.selectShareInputText = this.selectShareInputText.bind(this);
this.handleChange = this.handleChange.bind(this);
this.toggleButton = this.toggleButton.bind(this);
this.fetchUUID = this.fetchUUID.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) {
// press Ctrl+Enter to generate and copy new share link directly
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 ? (
Expand All @@ -57,7 +83,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}&nbsp;
Expand All @@ -69,10 +95,8 @@ export class ControlBarShareButton extends React.PureComponent<ControlBarShareBu
<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
onClick={this.fetchUUID}
/>
</div>
) : (
Expand All @@ -84,10 +108,10 @@ 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.state.keyword}>
<input defaultValue={this.state.keyword} readOnly={true} ref={this.shareInputElem} />
<Tooltip2 content="Copy link to clipboard">
<CopyToClipboard text={this.props.shortURL}>
<CopyToClipboard text={this.state.keyword}>
<ControlButton icon={IconNames.DUPLICATE} onClick={this.selectShareInputText} />
</CopyToClipboard>
</Tooltip2>
Expand Down Expand Up @@ -121,8 +145,7 @@ export class ControlBarShareButton extends React.PureComponent<ControlBarShareBu
}

// reset state
this.props.handleUpdateShortURL('');
this.setState({ keyword: '', isLoading: false });
this.setState({ keyword: '', isLoading: false, isSuccess: false });
}

private handleChange(event: React.FormEvent<HTMLInputElement>) {
Expand All @@ -135,4 +158,31 @@ export class ControlBarShareButton extends React.PureComponent<ControlBarShareBu
this.shareInputElem.current.select();
}
}

private fetchUUID() {
const requestBody = {
shared_program: {
data: this.props.programConfig
}
};
const fetchOpts: RequestInit = {
method: 'POST',
body: JSON.stringify(requestBody),
headers: {
'Content-Type': 'application/json'
}
};
fetch(`${Constants.backendUrl}/api/shared_programs`, fetchOpts)
.then(res => {
return res.json();
})
.then(resp => {
this.setState({
// seems like there's no frontend url env variable, should be replaced by frontend server accordingly
keyword: `http://localhost:8000/playground/share/` + resp.uuid
Rachelcoll marked this conversation as resolved.
Show resolved Hide resolved
});
this.setState({ isLoading: true, isSuccess: true });
})
.catch(err => showWarningMessage('Fail to generate url! Error: ' + err));
}
}
13 changes: 13 additions & 0 deletions src/features/playground/shareLinks/ShareLinkState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
type ShareLinkState = Partial<{
chownces marked this conversation as resolved.
Show resolved Hide resolved
isFolder: string;
tabs: string;
tabIdx: string;
chap: string;
variant: string;
ext: string;
exec: string;
files: string;
prgrm: string;
}>;

export default ShareLinkState;
19 changes: 19 additions & 0 deletions src/features/playground/shareLinks/decoder/Decoder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import ShareLinkState from '../ShareLinkState';
import DecoderDelegate from './delegates/DecoderDelegate';

/**
* Decodes the given encodedString with the specified decoder in `decodeWith`.
*/
class ShareLinkStateDecoder {
encodedString: string;

constructor(encodedString: string) {
this.encodedString = encodedString;
}

decodeWith(decoderDelegate: DecoderDelegate): ShareLinkState {
return decoderDelegate.decode(this.encodedString);
}
}

export default ShareLinkStateDecoder;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import ShareLinkState from '../../ShareLinkState';

interface DecoderDelegate {
decode(str: string): ShareLinkState;
}

export default DecoderDelegate;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import ShareLinkState from '../../ShareLinkState';
import DecoderDelegate from './DecoderDelegate';

class JsonDecoderDelegate implements DecoderDelegate {
decode(str: string): ShareLinkState {
const jsonObject = JSON.parse(str);
return jsonObject.data;
}
}

export default JsonDecoderDelegate;
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { IParsedQuery, parseQuery } from 'src/commons/utils/QueryHelper';

import ShareLinkState from '../../ShareLinkState';
import DecoderDelegate from './DecoderDelegate';

class UrlParamsDecoderDelegate implements DecoderDelegate {
decode(str: string): ShareLinkState {
const qs: Partial<IParsedQuery> = parseQuery(str);
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
};
}
}

export default UrlParamsDecoderDelegate;
49 changes: 49 additions & 0 deletions src/features/playground/shareLinks/encoder/Encoder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { FSModule } from 'browserfs/dist/node/core/FS';
import { compressToEncodedURIComponent } from 'lz-string';
import qs from 'query-string';
import { useState } from 'react';
import { retrieveFilesInWorkspaceAsRecord } from 'src/commons/fileSystem/utils';
import { useTypedSelector } from 'src/commons/utils/Hooks';
import { EditorTabState } from 'src/commons/workspace/WorkspaceTypes';

import ShareLinkState from '../ShareLinkState';

export const useUrlEncoder = () => {
const isFolderModeEnabled = useTypedSelector(
state => state.workspaces.playground.isFolderModeEnabled
);

const editorTabs = useTypedSelector(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 = useTypedSelector(state => state.workspaces.playground.context.chapter);
const variant = useTypedSelector(state => state.workspaces.playground.context.variant);
const execTime = useTypedSelector(state => state.workspaces.playground.execTime);
const files = useGetFile();

const result: ShareLinkState = {
chownces marked this conversation as resolved.
Show resolved Hide resolved
isFolder: isFolderModeEnabled.toString(),
files: files.toString(),
tabs: editorTabFilePaths.map(compressToEncodedURIComponent)[0],
tabIdx: activeEditorTabIndex?.toString(),
chap: chapter.toString(),
variant,
ext: 'NONE',
exec: execTime.toString()
};

return result;
};

const useGetFile = () => {
const fileSystem = useTypedSelector(state => state.fileSystem.inBrowserFileSystem);
const [files, setFiles] = useState<Record<string, string>>({});
retrieveFilesInWorkspaceAsRecord('playground', fileSystem as FSModule).then(result => {
setFiles(result);
});
return compressToEncodedURIComponent(qs.stringify(files));
};
Loading
Loading