Skip to content

Commit a13754a

Browse files
authored
skillmap: reflect project cloud status on on info panel (microsoft#8440)
* cloud status wip * reflect cloud status on info panel * removed unneeded property * show cloud status on skillmap view * pr feedback and refinements
1 parent 6abb342 commit a13754a

File tree

15 files changed

+207
-82
lines changed

15 files changed

+207
-82
lines changed

pxteditor/editor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,7 @@ namespace pxt.editor {
375375
openNewTab(header: pxt.workspace.Header, dependent: boolean): void;
376376
createGitHubRepositoryAsync(): Promise<void>;
377377
saveLocalProjectsToCloudAsync(headerIds: string[]): Promise<void>;
378+
requestProjectCloudStatus(headerIds: string[]): Promise<void>;
378379
}
379380

380381
export interface IHexFileImporter {

pxteditor/editorcontroller.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ namespace pxt.editor {
5656
| "unloadproject"
5757
| "shareproject"
5858
| "savelocalprojectstocloud"
59+
| "projectcloudstatus"
60+
| "requestprojectcloudstatus"
5961

6062
| "toggletrace" // EditorMessageToggleTraceRequest
6163
| "togglehighcontrast"
@@ -230,6 +232,17 @@ namespace pxt.editor {
230232
headerIds: string[];
231233
}
232234

235+
export interface EditorMessageProjectCloudStatus extends EditorMessageRequest {
236+
action: "projectcloudstatus";
237+
headerId: string;
238+
status: pxt.cloud.CloudStatus;
239+
}
240+
241+
export interface EditorMessageRequestProjectCloudStatus extends EditorMessageRequest {
242+
action: "requestprojectcloudstatus";
243+
headerIds: string[];
244+
}
245+
233246
export interface EditorMessageImportTutorialRequest extends EditorMessageRequest {
234247
action: "importtutorial";
235248
// markdown to load
@@ -547,6 +560,11 @@ namespace pxt.editor {
547560
const msg = data as EditorMessageSaveLocalProjectsToCloud;
548561
return projectView.saveLocalProjectsToCloudAsync(msg.headerIds);
549562
}
563+
case "requestprojectcloudstatus": {
564+
// Responses are sent as separate "projectcloudstatus" messages.
565+
const msg = data as EditorMessageRequestProjectCloudStatus;
566+
return projectView.requestProjectCloudStatus(msg.headerIds);
567+
}
550568
}
551569
return Promise.resolve();
552570
});

pxtlib/cloud.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
namespace pxt.cloud {
2+
export type CloudStatus = "none" | "synced" | "justSynced" | "offline" | "syncing" | "conflict" | "localEdits";
3+
4+
export type CloudStatusInfo = {
5+
value: pxt.cloud.CloudStatus,
6+
icon?: string,
7+
tooltip?: string,
8+
shortStatus?: string,
9+
longStatus?: string,
10+
indicator?: string
11+
};
12+
13+
export const cloudStatus: { [index in pxt.cloud.CloudStatus]: CloudStatusInfo } = {
14+
"none": {
15+
value: "none",
16+
},
17+
"synced": {
18+
value: "synced",
19+
icon: "cloud-saved-b",
20+
tooltip: lf("Project saved to cloud"),
21+
shortStatus: lf("saved"),
22+
longStatus: lf("Saved to cloud"),
23+
indicator: "",
24+
},
25+
["justSynced"]: {
26+
value: "justSynced",
27+
icon: "cloud-saved-b",
28+
tooltip: lf("Project saved to cloud"),
29+
shortStatus: lf("saved!"),
30+
longStatus: lf("Saved to cloud!"),
31+
indicator: "",
32+
},
33+
["offline"]: {
34+
value: "offline",
35+
icon: "cloud-error-b",
36+
tooltip: lf("Unable to connect to cloud"),
37+
shortStatus: lf("offline"),
38+
longStatus: lf("Offline"),
39+
indicator: lf("offline"),
40+
},
41+
["syncing"]: {
42+
value: "syncing",
43+
icon: "cloud-saving-b",
44+
tooltip: lf("Saving project to cloud..."),
45+
shortStatus: lf("saving..."),
46+
longStatus: lf("Saving to cloud..."),
47+
indicator: lf("syncing..."),
48+
},
49+
["conflict"]: {
50+
value: "conflict",
51+
icon: "cloud-error-b",
52+
tooltip: lf("Project was edited in two places and the changes conflict"),
53+
shortStatus: lf("conflict!"),
54+
longStatus: lf("Project has a conflict!"),
55+
indicator: "!"
56+
},
57+
["localEdits"]: {
58+
value: "localEdits",
59+
icon: "cloud-saving-b",
60+
tooltip: lf("Saving project to the cloud..."),
61+
shortStatus: lf("saving..."),
62+
longStatus: lf("Saving to cloud..."),
63+
indicator: "*"
64+
},
65+
};
66+
}

pxtlib/localStorage.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,14 @@ namespace pxt.storage.shared {
128128
return routingEnabled && pxt.BrowserUtils.isLocalHostDev() && !pxt.BrowserUtils.noSharedLocalStorage();
129129
}
130130

131-
function storageNamespace(): string {
131+
function sharedStorageNamespace(): string {
132132
if (pxt.BrowserUtils.isChromiumEdge()) { return "chromium-edge"; }
133133
return pxt.BrowserUtils.browser();
134134
}
135135

136136
export async function getAsync<T>(container: string, key: string): Promise<T> {
137137
if (useSharedLocalStorage()) {
138-
container += '-' + storageNamespace();
138+
container += '-' + sharedStorageNamespace();
139139
const resp = await pxt.Util.requestAsync({
140140
url: `${localhostStoreUrl}${encodeURIComponent(container)}/${encodeURIComponent(key)}`,
141141
method: "GET",
@@ -166,7 +166,7 @@ namespace pxt.storage.shared {
166166
else
167167
sval = val.toString();
168168
if (useSharedLocalStorage()) {
169-
container += '-' + storageNamespace();
169+
container += '-' + sharedStorageNamespace();
170170
const data = {
171171
type: (typeof val === "object") ? "json" : "text",
172172
val: sval
@@ -184,7 +184,7 @@ namespace pxt.storage.shared {
184184

185185
export async function delAsync(container: string, key: string): Promise<void> {
186186
if (useSharedLocalStorage()) {
187-
container += '-' + storageNamespace();
187+
container += '-' + sharedStorageNamespace();
188188
await pxt.Util.requestAsync({
189189
url: `${localhostStoreUrl}${encodeURIComponent(container)}/${encodeURIComponent(key)}`,
190190
method: "DELETE",

skillmap/src/App.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,14 +210,20 @@ class AppImpl extends React.Component<AppProps, AppState> {
210210

211211
protected async cloudSyncCheckAsync() {
212212
if (await authClient.loggedInAsync() && this.sendMessageAsync && this.loadedUser) {
213-
// Tell the editor to transfer local skillmap projects to the cloud.
214213
const state = store.getState();
215214
const headerIds = getFlattenedHeaderIds(state.user, state.pageSourceUrl);
215+
// Tell the editor to transfer local skillmap projects to the cloud.
216216
await this.sendMessageAsync({
217217
type: "pxteditor",
218218
action: "savelocalprojectstocloud",
219219
headerIds
220220
} as pxt.editor.EditorMessageSaveLocalProjectsToCloud);
221+
// Tell the editor to send us the cloud status of our projects.
222+
await this.sendMessageAsync({
223+
type: "pxteditor",
224+
action: "requestprojectcloudstatus",
225+
headerIds
226+
} as pxt.editor.EditorMessageRequestProjectCloudStatus);
221227
}
222228
}
223229

skillmap/src/actions/dispatch.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,5 @@ export const dispatchLogout = () => ({ type: actions.USER_LOG_OUT });
4242
export const dispatchShowUserProfile = () => ({ type: actions.SHOW_USER_PROFILE });
4343
export const dispatchCloseUserProfile = () => ({ type: actions.HIDE_USER_PROFILE });
4444

45-
export const dispatchSetShareStatus = (headerId?: string, url?: string) => ({ type: actions.SET_SHARE_STATUS, headerId, url })
45+
export const dispatchSetShareStatus = (headerId?: string, url?: string) => ({ type: actions.SET_SHARE_STATUS, headerId, url });
46+
export const dispatchSetCloudStatus = (headerId: string, status: string) => ({ type: actions.SET_CLOUD_STATUS, headerId, status });

skillmap/src/actions/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@ export const USER_LOG_OUT = "USER_LOG_OUT";
3939
export const SHOW_USER_PROFILE = "SHOW_USER_PROFILE";
4040
export const HIDE_USER_PROFILE = "HIDE_USER_PROFILE";
4141

42-
export const SET_SHARE_STATUS = "SET_SHARE_STATUS";
42+
export const SET_SHARE_STATUS = "SET_SHARE_STATUS";
43+
export const SET_CLOUD_STATUS = "SET_CLOUD_STATUS";
Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import * as React from "react";
22
import { connect } from 'react-redux';
33
import { dispatchShowLoginModal } from "../actions/dispatch";
4-
54
import { SkillMapState } from "../store/reducer";
5+
import { lookupActivityProgress } from "../lib/skillMapUtils";
6+
import { head } from "request";
67

78
interface OwnProps {
89
signedIn: boolean;
10+
cloudStatus: pxt.cloud.CloudStatus;
911
}
1012

1113
interface DispatchProps {
@@ -16,14 +18,14 @@ type CloudActionsProps = OwnProps & DispatchProps;
1618

1719
export class CloudActionsImpl extends React.Component<CloudActionsProps> {
1820
render () {
21+
const cloudStatus = pxt.cloud.cloudStatus[this.props.cloudStatus];
1922
return <div className="cloud-action">
2023
{
2124
this.props.signedIn
2225
? <div className="cloud-indicator">
23-
<div className={"ui tiny cloudicon xicon cloud-saved-b"} title={lf("Project saved to cloud")} tabIndex={-1}></div>
24-
{lf("Saved To Cloud!")}
26+
<div className={`ui tiny cloudicon xicon ${cloudStatus.icon}`} title={cloudStatus.tooltip} tabIndex={-1}></div>
27+
{cloudStatus.longStatus}
2528
</div>
26-
2729
: <div className="sign-in-button" onClick={this.props.dispatchShowLoginModal}>
2830
{lf("Sign in to Save")}
2931
</div>
@@ -33,13 +35,33 @@ export class CloudActionsImpl extends React.Component<CloudActionsProps> {
3335
}
3436

3537
function mapStateToProps(state: SkillMapState, ownProps: any) {
38+
const { user, pageSourceUrl, selectedItem } = state;
39+
40+
let cloudStatus: pxt.cloud.CloudStatus = "none";
41+
42+
if (selectedItem?.activityId) {
43+
const headerId = lookupActivityProgress(
44+
user,
45+
pageSourceUrl,
46+
selectedItem.mapId,
47+
selectedItem.activityId,
48+
)?.headerId;
49+
if (headerId) {
50+
cloudStatus = state.cloudState && state.cloudState[headerId] || cloudStatus;
51+
}
52+
} else {
53+
// For skillmap view, show "Saved to cloud"
54+
cloudStatus = "synced";
55+
}
56+
3657
return {
37-
signedIn: state.auth.signedIn
58+
signedIn: state.auth.signedIn,
59+
cloudStatus
3860
}
3961
}
4062

4163
const mapDispatchToProps = {
4264
dispatchShowLoginModal
4365
}
4466

45-
export const CloudActions = connect(mapStateToProps, mapDispatchToProps)(CloudActionsImpl);
67+
export const CloudActions = connect(mapStateToProps, mapDispatchToProps)(CloudActionsImpl);

skillmap/src/components/InfoPanel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export class InfoPanelImpl extends React.Component<InfoPanelProps> {
8484
? <ActivityActions mapId={mapId} activityId={node!.activityId} status={status} completedHeaderId={completedHeaderId} />
8585
: <RewardActions mapId={mapId} activityId={node!.activityId} status={status} type={(node as MapReward).type} />)
8686
}
87-
{hasCloudSync && <CloudActions/>}
87+
{hasCloudSync && <CloudActions />}
8888
</div>
8989
</div>
9090
}

skillmap/src/components/makecodeFrame.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { isLocal, resolvePath, getEditorUrl, tickEvent } from "../lib/browserUti
55
import { lookupActivityProgress } from "../lib/skillMapUtils";
66

77
import { SkillMapState } from '../store/reducer';
8-
import { dispatchSetHeaderIdForActivity, dispatchCloseActivity, dispatchSaveAndCloseActivity, dispatchUpdateUserCompletedTags, dispatchSetShareStatus } from '../actions/dispatch';
8+
import { dispatchSetHeaderIdForActivity, dispatchCloseActivity, dispatchSaveAndCloseActivity, dispatchUpdateUserCompletedTags, dispatchSetShareStatus, dispatchSetCloudStatus } from '../actions/dispatch';
99

1010
/* eslint-disable import/no-unassigned-import, import/no-internal-modules */
1111
import '../styles/makecode-editor.css'
@@ -28,6 +28,7 @@ interface MakeCodeFrameProps {
2828
dispatchSaveAndCloseActivity: () => void;
2929
dispatchUpdateUserCompletedTags: () => void;
3030
dispatchSetShareStatus: (headerId?: string, url?: string) => void;
31+
dispatchSetCloudStatus: (headerId: string, status: string) => void;
3132
onFrameLoaded: (sendMessageAsync: (message: any) => Promise<any>) => void;
3233
}
3334

@@ -168,6 +169,11 @@ class MakeCodeFrameImpl extends React.Component<MakeCodeFrameProps, MakeCodeFram
168169
case "tutorialevent":
169170
this.handleTutorialEvent(data as pxt.editor.EditorMessageTutorialEventRequest);
170171
break;
172+
case "projectcloudstatus": {
173+
const msg = data as pxt.editor.EditorMessageProjectCloudStatus;
174+
this.props.dispatchSetCloudStatus(msg.headerId, msg.status);
175+
break;
176+
}
171177
default:
172178
// console.log(JSON.stringify(data, null, 4));
173179
}
@@ -343,7 +349,8 @@ const mapDispatchToProps = {
343349
dispatchCloseActivity,
344350
dispatchSaveAndCloseActivity,
345351
dispatchUpdateUserCompletedTags,
346-
dispatchSetShareStatus
352+
dispatchSetShareStatus,
353+
dispatchSetCloudStatus
347354
};
348355

349356
export const MakeCodeFrame = connect(mapStateToProps, mapDispatchToProps)(MakeCodeFrameImpl);

0 commit comments

Comments
 (0)