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 4 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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"prepare": "husky"
},
"dependencies": {
"@blueprintjs/core": "^5.7.0",
"@blueprintjs/core": "^5.10.1",
Rachelcoll marked this conversation as resolved.
Show resolved Hide resolved
"@blueprintjs/datetime2": "^2.2.7",
"@blueprintjs/icons": "^5.5.0",
"@blueprintjs/popover2": "^2.0.0",
Expand Down Expand Up @@ -71,6 +71,7 @@
"react-draggable": "^4.4.5",
"react-dropzone": "^14.2.3",
"react-hotkeys": "^2.0.0",
"react-hotkeys-hook": "^4.4.4",
Rachelcoll marked this conversation as resolved.
Show resolved Hide resolved
"react-konva": "^18.2.10",
"react-latex-next": "^2.1.0",
"react-mde": "^11.5.0",
Expand Down
94 changes: 76 additions & 18 deletions src/commons/controlBar/ControlBarShareButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ type StateProps = {
shortURL?: string;
key: string;
isSicp?: boolean;
programConfig: object
};

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

export class ControlBarShareButton extends React.PureComponent<ControlBarShareButtonProps, State> {
Expand All @@ -36,7 +38,28 @@ 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() {
Expand All @@ -57,21 +80,50 @@ export class ControlBarShareButton extends React.PureComponent<ControlBarShareBu
</div>
) : (
<>
{!this.props.shortURL || this.props.shortURL === 'ERROR' ? (
{/* check this.props.postSuccess */}
{/* {!this.props.shortURL || this.props.shortURL === 'ERROR' ? (
!this.state.isLoading || this.props.shortURL === 'ERROR' ? ( */}
{!this.state.isSuccess || this.props.shortURL === 'ERROR' ? (
!this.state.isLoading || this.props.shortURL === 'ERROR' ? (

<div>
{Constants.urlShortenerBase}&nbsp;
<input
placeholder={'custom string (optional)'}
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
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The 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 = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use our request helper/wrapper instead of manual fetch.

Copy link
Author

Choose a reason for hiding this comment

The 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)
.then(res => {
return res.json()
})
.then(resp => {
this.setState({ keyword: resp.uuid })
console.log(resp)
})
.catch(err => console.log("Error: ", err));

// this.props.handleShortenURL(this.state.keyword);
// console.log("base", this.props.shortURL)
this.setState({ isLoading: true, isSuccess: true });
}}
/>
</div>
Expand All @@ -84,10 +136,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>
Expand All @@ -97,15 +152,15 @@ export class ControlBarShareButton extends React.PureComponent<ControlBarShareBu
);

return (
<Popover2
popoverClassName="Popover-share"
inheritDarkTheme={false}
content={shareButtonPopoverContent}
>
<Tooltip2 content="Get shareable link" placement={Position.TOP}>
<ControlButton label="Share" icon={IconNames.SHARE} onClick={() => this.toggleButton()} />
</Tooltip2>
</Popover2>
<Popover2
popoverClassName="Popover-share"
inheritDarkTheme={false}
content={shareButtonPopoverContent}
>
<Tooltip2 content="Get shareable link" placement={Position.TOP}>
<ControlButton label="Share" icon={IconNames.SHARE} onClick={() => this.toggleButton()} />
</Tooltip2>
</Popover2>
);
}

Expand All @@ -121,8 +176,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>) {
Expand All @@ -135,4 +190,7 @@ export class ControlBarShareButton extends React.PureComponent<ControlBarShareBu
this.shareInputElem.current.select();
}
}



}
40 changes: 17 additions & 23 deletions src/features/playground/PlaygroundActions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createAction } from '@reduxjs/toolkit';
import { SALanguage } from 'src/commons/application/ApplicationTypes';

Check failure on line 1 in src/features/playground/PlaygroundActions.ts

View workflow job for this annotation

GitHub Actions / lint (eslint)

Run autofix to sort these imports!
import { action } from 'typesafe-actions';

import { PersistenceFile } from '../persistence/PersistenceTypes';
import {
Expand All @@ -11,32 +11,26 @@
SHORTEN_URL,
UPDATE_SHORT_URL
} from './PlaygroundTypes';
import { programConfig } from 'src/pages/playground/Decoder';

Check warning on line 14 in src/features/playground/PlaygroundActions.ts

View workflow job for this annotation

GitHub Actions / lint (eslint)

'programConfig' is defined but never used

export const generateLzString = createAction(GENERATE_LZ_STRING, () => ({ payload: {} }));
export const generateLzString = () => action(GENERATE_LZ_STRING);

export const shortenURL = createAction(SHORTEN_URL, (keyword: string) => ({ payload: keyword }));
export const shortenURL = (keyword: string) => action(SHORTEN_URL, keyword);

export const updateShortURL = createAction(UPDATE_SHORT_URL, (shortURL: string) => ({
payload: shortURL
}));
export const updateShortURL = (shortURL: string) => action(UPDATE_SHORT_URL, shortURL);

export const changeQueryString = createAction(CHANGE_QUERY_STRING, (queryString: string) => ({
payload: queryString
}));
export const changeQueryString = (queryString: string) => action(CHANGE_QUERY_STRING, queryString);

export const playgroundUpdatePersistenceFile = createAction(
PLAYGROUND_UPDATE_PERSISTENCE_FILE,
(file?: PersistenceFile) => ({ payload: file })
);
// export const changeProgramConfig = (config: programConfig) => action(CHANGE_PROGRAM_CONFIG, config);

export const playgroundUpdateGitHubSaveInfo = createAction(
PLAYGROUND_UPDATE_GITHUB_SAVE_INFO,
(repoName: string, filePath: string, lastSaved: Date) => ({
payload: { repoName, filePath, lastSaved }
})
);
export const playgroundUpdatePersistenceFile = (file?: PersistenceFile) =>
action(PLAYGROUND_UPDATE_PERSISTENCE_FILE, file);

export const playgroundConfigLanguage = createAction(
PLAYGROUND_UPDATE_LANGUAGE_CONFIG,
(languageConfig: SALanguage) => ({ payload: languageConfig })
);
export const playgroundUpdateGitHubSaveInfo = (
repoName: string,
filePath: string,
lastSaved: Date
) => action(PLAYGROUND_UPDATE_GITHUB_SAVE_INFO, { repoName, filePath, lastSaved });

export const playgroundConfigLanguage = (languageConfig: SALanguage) =>
action(PLAYGROUND_UPDATE_LANGUAGE_CONFIG, languageConfig);
131 changes: 131 additions & 0 deletions src/pages/playground/Decoder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { FSModule } from 'browserfs/dist/node/core/FS';

Check failure on line 1 in src/pages/playground/Decoder.tsx

View workflow job for this annotation

GitHub Actions / lint (eslint)

Run autofix to sort these imports!
Rachelcoll marked this conversation as resolved.
Show resolved Hide resolved
import { Dispatch } from 'react';
import { AnyAction } from 'redux';
import { WorkspaceLocation } from 'src/commons/workspace/WorkspaceTypes';
import { IParsedQuery, parseQuery } from '../../commons/utils/QueryHelper';
import { convertParamToBoolean, convertParamToInt } from '../../commons/utils/ParamParseHelper';
import {
showFullJSWarningOnUrlLoad,
showFulTSWarningOnUrlLoad,
showHTMLDisclaimer
} from 'src/commons/utils/WarningDialogHelper';
import { decompressFromEncodedURIComponent } from 'lz-string';
import { getDefaultFilePath, getLanguageConfig } from 'src/commons/application/ApplicationTypes';
import { overwriteFilesInWorkspace } from 'src/commons/fileSystem/utils';
import { setFolderMode, removeEditorTabsForDirectory, addEditorTab, updateActiveEditorTabIndex } from 'src/commons/workspace/WorkspaceActions';
import { playgroundConfigLanguage } from 'src/features/playground/PlaygroundActions';
import { WORKSPACE_BASE_PATHS } from '../fileSystem/createInBrowserFileSystem';
import { Chapter, Variant } from 'js-slang/dist/types';


export type programConfig = {
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
}
/**
* #chap=4
* exec=1000
* ext=NONE
* files=KQJgYgDgNghgngcwE4HsCuA7AJqSrkwC2AdAFYDOAvEA
* isFolder=false
* tabIdx=0
* tabs=PQBwNghgng5gTgewK4DsAmpHwgWwHQBWAzkA
* variant=default
*/
export var Decoder = {

Check failure on line 42 in src/pages/playground/Decoder.tsx

View workflow job for this annotation

GitHub Actions / lint (eslint)

Unexpected var, use let or const instead
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) {
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);
}

dispatch(setFolderMode(workspaceLocation, false));
const isFolderModeEnabled = convertParamToBoolean(configObj.isFolder?.toString()) ?? false;
dispatch(setFolderMode(workspaceLocation, isFolderModeEnabled));

const editorTabFilePaths = configObj.tabs?.split(',').map(decompressFromEncodedURIComponent) ?? [
defaultFilePath
];

dispatch(
removeEditorTabsForDirectory(workspaceLocation, WORKSPACE_BASE_PATHS[workspaceLocation])
);

editorTabFilePaths.forEach(filePath =>
dispatch(addEditorTab(workspaceLocation, filePath, files[filePath] ?? ''))
);

const activeEditorTabIndex = convertParamToInt(configObj.tabIdx?.toString()) ?? 0;
dispatch(updateActiveEditorTabIndex(workspaceLocation, activeEditorTabIndex));
if (chapter) {
const languageConfig = getLanguageConfig(chapter, configObj.variant as Variant);
handlers.handleChapterSelect(chapter, languageConfig.variant);
dispatch(playgroundConfigLanguage(languageConfig));
}

const execTime = Math.max(convertParamToInt(configObj.exec?.toString() || '1000') || 1000, 1000);
if (execTime) {
handlers.handleChangeExecTime(execTime);
}
}
}
Loading
Loading