Skip to content

Commit c7fe3b4

Browse files
izruffRichDom2185
andauthored
Implement read-only option to playground sessions (#2834)
* Implemented session editing and viewing IDs * Added readonly info to the UI * Slight improvements to UI and added popup message upon joining session * Added popup message upon joining * Updated redux action * Updated tests * Fixed formatting * Removed unused function * Disable editor in read-only sessions This uses the current version of @sourceacademy/sharedb-ace at NPM, not the one proposed in this PR. Refer to the PR in sharedb-ace for more info. --------- Co-authored-by: Richard Dominick <34370238+RichDom2185@users.noreply.github.com>
1 parent da0f0c9 commit c7fe3b4

File tree

15 files changed

+164
-40
lines changed

15 files changed

+164
-40
lines changed

src/commons/application/ApplicationTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ export const createDefaultWorkspace = (workspaceLocation: WorkspaceLocation): Wo
358358
programPrependValue: '',
359359
programPostpendValue: '',
360360
editorSessionId: '',
361+
sessionDetails: null,
361362
isEditorReadonly: false,
362363
editorTestcases: [],
363364
externalLibrary: ExternalLibraryName.NONE,

src/commons/assessmentWorkspace/AssessmentWorkspace.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -829,6 +829,7 @@ const AssessmentWorkspace: React.FC<AssessmentWorkspaceProps> = props => {
829829
removeEditorTabByIndex: editorContainerHandlers.removeEditorTabByIndex,
830830
editorTabs: editorTabs.map(convertEditorTabStateToProps),
831831
editorSessionId: '',
832+
sessionDetails: null,
832833
sourceChapter: question.library.chapter || Chapter.SOURCE_4,
833834
sourceVariant: question.library.variant ?? Variant.DEFAULT,
834835
externalLibraryName: question.library.external.name || 'NONE',

src/commons/collabEditing/CollabEditingActions.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { createAction } from '@reduxjs/toolkit';
22

33
import { WorkspaceLocation } from '../workspace/WorkspaceTypes';
4-
import { SET_EDITOR_SESSION_ID, SET_SHAREDB_CONNECTED } from './CollabEditingTypes';
4+
import {
5+
SET_EDITOR_SESSION_ID,
6+
SET_SESSION_DETAILS,
7+
SET_SHAREDB_CONNECTED
8+
} from './CollabEditingTypes';
59

610
export const setEditorSessionId = createAction(
711
SET_EDITOR_SESSION_ID,
@@ -10,6 +14,16 @@ export const setEditorSessionId = createAction(
1014
})
1115
);
1216

17+
export const setSessionDetails = createAction(
18+
SET_SESSION_DETAILS,
19+
(
20+
workspaceLocation: WorkspaceLocation,
21+
sessionDetails: { docId: string; readOnly: boolean } | null
22+
) => ({
23+
payload: { workspaceLocation, sessionDetails }
24+
})
25+
);
26+
1327
/**
1428
* Sets ShareDB connection status.
1529
*

src/commons/collabEditing/CollabEditingHelper.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,25 @@ export function getSessionUrl(sessionId: string, ws?: boolean): string {
1313
return url.toString();
1414
}
1515

16-
export async function checkSessionIdExists(sessionId: string): Promise<boolean> {
16+
export async function getDocInfoFromSessionId(
17+
sessionId: string
18+
): Promise<{ docId: string; readOnly: boolean } | null> {
1719
const resp = await fetch(getSessionUrl(sessionId));
1820

19-
return resp && resp.ok;
21+
if (resp && resp.ok) {
22+
return resp.json();
23+
} else {
24+
return null;
25+
}
2026
}
2127

22-
export async function createNewSession(initial: string): Promise<string> {
28+
export async function createNewSession(
29+
contents: string
30+
): Promise<{ docId: string; sessionEditingId: string; sessionViewingId: string }> {
2331
const resp = await fetch(Constants.sharedbBackendUrl, {
2432
method: 'POST',
25-
body: initial,
26-
headers: { 'Content-Type': 'text/plain' }
33+
body: JSON.stringify({ contents }),
34+
headers: { 'Content-Type': 'application/json' }
2735
});
2836

2937
if (!resp || !resp.ok) {
@@ -32,5 +40,5 @@ export async function createNewSession(initial: string): Promise<string> {
3240
);
3341
}
3442

35-
return resp.text();
43+
return resp.json();
3644
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export const SET_EDITOR_SESSION_ID = 'SET_EDITOR_SESSION_ID';
2+
export const SET_SESSION_DETAILS = 'SET_SESSION_DETAILS';
23
export const SET_SHAREDB_CONNECTED = 'SET_SHAREDB_CONNECTED';

src/commons/controlBar/ControlBarSessionButton.tsx

Lines changed: 89 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
import { Classes, Colors, Menu } from '@blueprintjs/core';
1+
import { Classes, Colors, Divider, FormGroup, Menu, Text } from '@blueprintjs/core';
22
import { IconNames } from '@blueprintjs/icons';
33
import { Popover2, Tooltip2 } from '@blueprintjs/popover2';
44
import React from 'react';
55
import * as CopyToClipboard from 'react-copy-to-clipboard';
66

7-
import { checkSessionIdExists, createNewSession } from '../collabEditing/CollabEditingHelper';
7+
import { createNewSession, getDocInfoFromSessionId } from '../collabEditing/CollabEditingHelper';
88
import ControlButton from '../ControlButton';
99
import { showWarningMessage } from '../utils/notifications/NotificationsHelper';
1010

1111
type ControlBarSessionButtonsProps = DispatchProps & StateProps;
1212

1313
type DispatchProps = {
1414
handleSetEditorSessionId?: (editorSessionId: string) => void;
15+
handleSetSessionDetails?: (sessionDetails: { docId: string; readOnly: boolean } | null) => void;
1516
};
1617

1718
type StateProps = {
@@ -24,6 +25,8 @@ type StateProps = {
2425

2526
type State = {
2627
joinElemValue: string;
28+
sessionEditingId: string;
29+
sessionViewingId: string;
2730
};
2831

2932
function handleError(error: any) {
@@ -34,34 +37,77 @@ export class ControlBarSessionButtons extends React.PureComponent<
3437
ControlBarSessionButtonsProps,
3538
State
3639
> {
37-
private inviteInputElem: React.RefObject<HTMLInputElement>;
40+
private sessionEditingIdInputElem: React.RefObject<HTMLInputElement>;
41+
private sessionViewingIdInputElem: React.RefObject<HTMLInputElement>;
3842

3943
constructor(props: ControlBarSessionButtonsProps) {
4044
super(props);
41-
this.state = { joinElemValue: '' };
45+
this.state = { joinElemValue: '', sessionEditingId: '', sessionViewingId: '' };
4246

4347
this.handleChange = this.handleChange.bind(this);
44-
this.inviteInputElem = React.createRef();
45-
this.selectInviteInputText = this.selectInviteInputText.bind(this);
48+
this.sessionEditingIdInputElem = React.createRef();
49+
this.sessionViewingIdInputElem = React.createRef();
50+
this.selectSessionEditingId = this.selectSessionEditingId.bind(this);
51+
this.selectSessionViewingId = this.selectSessionViewingId.bind(this);
4652
}
4753

4854
public render() {
4955
const handleStartInvite = () => {
5056
// FIXME this handler should be a Saga action or at least in a controller
5157
if (this.props.editorSessionId === '') {
52-
createNewSession(this.props.getEditorValue()).then(sessionId => {
53-
this.props.handleSetEditorSessionId!(sessionId);
58+
createNewSession(this.props.getEditorValue()).then(resp => {
59+
this.setState({
60+
sessionEditingId: resp.sessionEditingId,
61+
sessionViewingId: resp.sessionViewingId
62+
});
63+
this.props.handleSetEditorSessionId!(resp.sessionEditingId);
64+
this.props.handleSetSessionDetails!({ docId: resp.docId, readOnly: false });
5465
}, handleError);
5566
}
5667
};
5768

5869
const inviteButtonPopoverContent = (
59-
<>
60-
<input value={this.props.editorSessionId} readOnly={true} ref={this.inviteInputElem} />
61-
<CopyToClipboard text={'' + this.props.editorSessionId}>
62-
<ControlButton icon={IconNames.DUPLICATE} onClick={this.selectInviteInputText} />
63-
</CopyToClipboard>
64-
</>
70+
<div style={{ display: 'flex', flexDirection: 'column' }}>
71+
{!this.props.editorSessionId ? (
72+
<>
73+
<Text>You are not currently in any session.</Text>
74+
<Divider />
75+
<ControlButton label={'Create'} icon={IconNames.ADD} onClick={handleStartInvite} />
76+
</>
77+
) : (
78+
<>
79+
<Text>
80+
You have joined the session as{' '}
81+
{this.state.sessionEditingId ? 'an editor' : 'a viewer'}.
82+
</Text>
83+
<Divider />
84+
{this.state.sessionEditingId && (
85+
<FormGroup subLabel="Invite as editor">
86+
<input
87+
value={this.state.sessionEditingId}
88+
readOnly={true}
89+
ref={this.sessionEditingIdInputElem}
90+
/>
91+
<CopyToClipboard text={'' + this.state.sessionEditingId}>
92+
<ControlButton icon={IconNames.DUPLICATE} onClick={this.selectSessionEditingId} />
93+
</CopyToClipboard>
94+
</FormGroup>
95+
)}
96+
{this.state.sessionViewingId && (
97+
<FormGroup subLabel="Invite as viewer">
98+
<input
99+
value={this.state.sessionViewingId}
100+
readOnly={true}
101+
ref={this.sessionViewingIdInputElem}
102+
/>
103+
<CopyToClipboard text={'' + this.state.sessionViewingId}>
104+
<ControlButton icon={IconNames.DUPLICATE} onClick={this.selectSessionViewingId} />
105+
</CopyToClipboard>
106+
</FormGroup>
107+
)}
108+
</>
109+
)}
110+
</div>
65111
);
66112

67113
const inviteButton = (
@@ -70,19 +116,32 @@ export class ControlBarSessionButtons extends React.PureComponent<
70116
inheritDarkTheme={false}
71117
content={inviteButtonPopoverContent}
72118
>
73-
<ControlButton label="Invite" icon={IconNames.GRAPH} onClick={handleStartInvite} />
119+
<ControlButton label="Invite" icon={IconNames.GRAPH} />
74120
</Popover2>
75121
);
76122

77123
const handleStartJoining = (event: React.FormEvent<HTMLFormElement>) => {
78124
event.preventDefault();
79125
// FIXME this handler should be a Saga action or at least in a controller
80-
checkSessionIdExists(this.state.joinElemValue).then(
81-
exists => {
82-
if (exists) {
126+
getDocInfoFromSessionId(this.state.joinElemValue).then(
127+
docInfo => {
128+
if (docInfo !== null) {
83129
this.props.handleSetEditorSessionId!(this.state!.joinElemValue);
130+
this.props.handleSetSessionDetails!(docInfo);
131+
if (docInfo.readOnly) {
132+
this.setState({
133+
sessionEditingId: '',
134+
sessionViewingId: this.state.joinElemValue
135+
});
136+
} else {
137+
this.setState({
138+
sessionEditingId: this.state.joinElemValue,
139+
sessionViewingId: ''
140+
});
141+
}
84142
} else {
85143
this.props.handleSetEditorSessionId!('');
144+
this.props.handleSetSessionDetails!(null);
86145
showWarningMessage('Could not find a session with that ID.');
87146
}
88147
},
@@ -120,7 +179,7 @@ export class ControlBarSessionButtons extends React.PureComponent<
120179
onClick={() => {
121180
// FIXME: this handler should be a Saga action or at least in a controller
122181
this.props.handleSetEditorSessionId!('');
123-
this.setState({ joinElemValue: '' });
182+
this.setState({ joinElemValue: '', sessionEditingId: '', sessionViewingId: '' });
124183
}}
125184
/>
126185
);
@@ -158,10 +217,17 @@ export class ControlBarSessionButtons extends React.PureComponent<
158217
);
159218
}
160219

161-
private selectInviteInputText() {
162-
if (this.inviteInputElem.current !== null) {
163-
this.inviteInputElem.current.focus();
164-
this.inviteInputElem.current.select();
220+
private selectSessionEditingId() {
221+
if (this.sessionEditingIdInputElem.current !== null) {
222+
this.sessionEditingIdInputElem.current.focus();
223+
this.sessionEditingIdInputElem.current.select();
224+
}
225+
}
226+
227+
private selectSessionViewingId() {
228+
if (this.sessionViewingIdInputElem.current !== null) {
229+
this.sessionViewingIdInputElem.current.focus();
230+
this.sessionViewingIdInputElem.current.select();
165231
}
166232
}
167233

src/commons/editingWorkspace/EditingWorkspace.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,7 @@ const EditingWorkspace: React.FC<EditingWorkspaceProps> = props => {
680680
};
681681
}),
682682
editorSessionId: '',
683+
sessionDetails: null,
683684
handleDeclarationNavigate: handleDeclarationNavigate,
684685
handleEditorEval: handleEditorEval,
685686
handleEditorValueChange: handleEditorValueChange,

src/commons/editor/Editor.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ type DispatchProps = {
4848

4949
type EditorStateProps = {
5050
editorSessionId: string;
51+
sessionDetails: { docId: string; readOnly: boolean } | null;
5152
isEditorAutorun: boolean;
5253
sourceChapter?: Chapter;
5354
externalLibraryName?: string;

src/commons/editor/UseShareAce.tsx

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import * as Sentry from '@sentry/browser';
22
import sharedbAce from '@sourceacademy/sharedb-ace';
33
import React from 'react';
44

5-
import { checkSessionIdExists, getSessionUrl } from '../collabEditing/CollabEditingHelper';
5+
import { getDocInfoFromSessionId, getSessionUrl } from '../collabEditing/CollabEditingHelper';
6+
import { showSuccessMessage } from '../utils/notifications/NotificationsHelper';
67
import { EditorHook } from './Editor';
78

89
// EditorHook structure:
@@ -18,22 +19,30 @@ const useShareAce: EditorHook = (inProps, outProps, keyBindings, reactAceRef) =>
1819
const propsRef = React.useRef(inProps);
1920
propsRef.current = inProps;
2021

21-
const { editorSessionId } = inProps;
22+
const { editorSessionId, sessionDetails } = inProps;
2223

2324
React.useEffect(() => {
24-
if (!editorSessionId) {
25+
if (!editorSessionId || !sessionDetails) {
2526
return;
2627
}
2728

2829
const editor = reactAceRef.current!.editor;
29-
const ShareAce = new sharedbAce(editorSessionId, {
30+
const ShareAce = new sharedbAce(sessionDetails.docId, {
3031
WsUrl: getSessionUrl(editorSessionId, true),
3132
pluginWsUrl: null,
3233
namespace: 'sa'
3334
});
35+
3436
ShareAce.on('ready', () => {
35-
ShareAce.add(editor, [], []);
37+
ShareAce.add(editor, ['contents'], []);
3638
propsRef.current.handleSetSharedbConnected!(true);
39+
40+
// Disables editor in a read-only session
41+
editor.setReadOnly(sessionDetails.readOnly);
42+
43+
showSuccessMessage(
44+
'You have joined a session as ' + (sessionDetails.readOnly ? 'a viewer.' : 'an editor.')
45+
);
3746
});
3847
ShareAce.on('error', (path: string, error: any) => {
3948
console.error('ShareAce error', error);
@@ -50,8 +59,8 @@ const useShareAce: EditorHook = (inProps, outProps, keyBindings, reactAceRef) =>
5059
return;
5160
}
5261
try {
53-
const exists = await checkSessionIdExists(editorSessionId);
54-
if (!exists) {
62+
const docInfo = await getDocInfoFromSessionId(editorSessionId);
63+
if (docInfo === null) {
5564
clearInterval(interval);
5665
WS.close();
5766
}
@@ -75,8 +84,11 @@ const useShareAce: EditorHook = (inProps, outProps, keyBindings, reactAceRef) =>
7584
connection.unlisten();
7685
}
7786
ShareAce.WS.close();
87+
88+
// Resets editor to normal after leaving the session
89+
editor.setReadOnly(false);
7890
};
79-
}, [editorSessionId, reactAceRef]);
91+
}, [editorSessionId, sessionDetails, reactAceRef]);
8092
};
8193

8294
export default useShareAce;

src/commons/editor/__tests__/Editor.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ test('Editor renders correctly', () => {
88
editorTabIndex: 0,
99
breakpoints: [],
1010
editorSessionId: '',
11+
sessionDetails: null,
1112
editorValue: '',
1213
highlightedLines: [],
1314
isEditorAutorun: false,

0 commit comments

Comments
 (0)