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 10 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
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
6 changes: 3 additions & 3 deletions public/externalLibs/sound/soundToneMatrix.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ var timeout_matrix;
// for coloring the matrix accordingly while it's being played
var timeout_color;

var timeout_objects = new Array();
var timeout_objects = [];

// vector_to_list returns a list that contains the elements of the argument vector
// in the given order.
Expand All @@ -54,7 +54,7 @@ function vector_to_list(vector) {
function x_y_to_row_column(x, y) {
var row = Math.floor((y - margin_length) / (square_side_length + distance_between_squares));
var column = Math.floor((x - margin_length) / (square_side_length + distance_between_squares));
return Array(row, column);
return [row, column];
}

// given the row number of a square, return the leftmost coordinate
Expand Down Expand Up @@ -365,5 +365,5 @@ function clear_all_timeout() {
clearTimeout(timeout_objects[i]);
}

timeout_objects = new Array();
timeout_objects = [];
}
66 changes: 57 additions & 9 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;
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> {
Expand All @@ -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 ? (
Expand All @@ -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}&nbsp;
Expand All @@ -66,12 +89,34 @@ 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
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)
Rachelcoll marked this conversation as resolved.
Show resolved Hide resolved
.then(res => {
return res.json();
})
.then(resp => {
this.setState({ keyword: resp.uuid });
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>
Expand All @@ -84,10 +129,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 Down Expand Up @@ -121,8 +169,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 Down
39 changes: 16 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';
import { action } from 'typesafe-actions';

import { PersistenceFile } from '../persistence/PersistenceTypes';
import {
Expand All @@ -12,31 +12,24 @@ import {
UPDATE_SHORT_URL
} from './PlaygroundTypes';

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);
155 changes: 155 additions & 0 deletions src/pages/playground/Decoder.tsx
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);
}
}
}
Loading
Loading