Skip to content

Commit 685fdbd

Browse files
eanders-msshakaoriknollabchatraVivian Li
authored
Merge branch 'stable7.1' (microsoft#8486)
* [stable7.1] Cherry-pick fixes for skillmap, iframe (microsoft#8391) * Rest finishedActivityState when opening new activity (microsoft#8387) * Null check for header when saving, posting tutorial progress (microsoft#8389) * Don't unload/reload skillmap graph, reduces jittering (microsoft#8390) * Don't use telemetry item before it's declared (microsoft#8393) * 7.1.21 * Cherry pick fixes to stable7.1 (microsoft#8394) * Don't write the contents of nonexistent files (microsoft#8392) * Don't use telemetry item before it's declared (microsoft#8393) * 7.1.22 * Handle iframe reloading (microsoft#8398) * 7.1.23 * Only load project if skillmap editorview open (microsoft#8399) * 7.1.24 * Don't run sim in background when no project is loaded in the skillmap iframe (microsoft#8405) * Don't run sim in background when no project is loaded in the skillmap iframe * Don't reload header when exiting skillmp tutorial * Remove post progress * Retry request for skillmap markdown in safari (microsoft#8395) * 7.1.25 * Check for deleted flag when importing skillmap project (microsoft#8414) * Filter tiles out of image gallery in image editor (microsoft#8403) * Add user profile page (microsoft#8411) Co-authored-by: Eric Anderson <eanders@microsoft.com> * Fix hang in monaco by making regex more specific (microsoft#8427) * Fix hang in monaco by making regex more specific * cleanup * Only reset progress for currently open skillmap (microsoft#8415) * reenable auth in skillmap (microsoft#8428) * 7.1.26 * Check for placeholder type before guessing a new type (microsoft#8429) (microsoft#8435) * on signin, transfer local projects to cloud (microsoft#8431) (microsoft#8443) * on signin, transfer local projects to cloud * pr feedback and refinements * Add detection for chromium-based edge (microsoft#8432) * 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 * Fix User Menu header sizing (microsoft#8437) * Prompt users to sign in after completing the first skillmap activity (microsoft#8433) * 7.1.27 * Load entire toolbox instead of exiting tutorial on toolbox error (microsoft#8449) * Make copies of local projects when importing skillmap progress to cloud (microsoft#8456) * Make copies of local projects when importing skillmap progress to cloud * cleanup * PR feedback * always request all header cloud status * 7.1.28 * Tick event for grey block in tutorial filters, add 24hr expiration (microsoft#8463) * 7.1.29 * allow specifying pkgs to compile hex files for in staticpkg (microsoft#8460) (microsoft#8466) * Handle user initials better in skillmap (microsoft#8458) * 7.1.30 * Never use the icons.css from pxt-arcade-sim (microsoft#8451) * Never use the icons.css from pxt-arcade-sim (microsoft#8451) * [stable7.1] Cherry Pick (microsoft#8477) * cleanup cloudSyncCheckAsync trigger (microsoft#8476) * Cleanup console output (microsoft#8479) * [stable7.1] Expand inline blocks regex to allow arrays (microsoft#8471) (microsoft#8474) * Expand inline blocks regex to allow arrays (microsoft#8471) * Thow warning instead of error for loop in skillmap graph (microsoft#8424) * Allow skill map end action without link (microsoft#8478) * 7.1.31 * Align sign in button icon (microsoft#8481) * skillmap: export cloud projects to local on profile delete (microsoft#8482) * 7.1.32 * Merge branch 'stable7.1' * Copy skillmap headers over for cloud users (microsoft#8483) * Copy skillmap headers over nicely for new users * Remove trailing spaces * Put ready promise back * Don't block things forever * Move things from ready to appstate * Add comment * Use 'some' instead of filter * 7.1.33 * Use arcade-sim icons.css :) (microsoft#8485) * 7.1.34 * Copy over user tags. (microsoft#8488) * 7.1.35 Co-authored-by: shakao <34112083+shakao@users.noreply.github.com> Co-authored-by: Richard Knoll <riknoll@users.noreply.github.com> Co-authored-by: Abhijith Chatra <abchatra@microsoft.com> Co-authored-by: Vivian Li <vivl@microsoft.com> Co-authored-by: Joey Wunderlich <jwunderl@users.noreply.github.com> Co-authored-by: Joey Wunderlich <joey.wunderlich@gmail.com> Co-authored-by: livcheerful <li.v.cheerful@gmail.com>
1 parent 2a7c849 commit 685fdbd

File tree

17 files changed

+202
-95
lines changed

17 files changed

+202
-95
lines changed

pxteditor/editor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ namespace pxt.editor {
376376
createGitHubRepositoryAsync(): Promise<void>;
377377
saveLocalProjectsToCloudAsync(headerIds: string[]): Promise<pxt.Map<string> | undefined>;
378378
requestProjectCloudStatus(headerIds: string[]): Promise<void>;
379+
convertCloudProjectsToLocal(userId: string): Promise<void>;
379380
}
380381

381382
export interface IHexFileImporter {

pxteditor/editorcontroller.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ namespace pxt.editor {
5858
| "savelocalprojectstocloud"
5959
| "projectcloudstatus"
6060
| "requestprojectcloudstatus"
61+
| "convertcloudprojectstolocal"
6162

6263
| "toggletrace" // EditorMessageToggleTraceRequest
6364
| "togglehighcontrast"
@@ -248,6 +249,11 @@ namespace pxt.editor {
248249
headerIds: string[];
249250
}
250251

252+
export interface EditorMessageConvertCloudProjectsToLocal extends EditorMessageRequest {
253+
action: "convertcloudprojectstolocal";
254+
userId: string;
255+
}
256+
251257
export interface EditorMessageImportTutorialRequest extends EditorMessageRequest {
252258
action: "importtutorial";
253259
// markdown to load
@@ -575,6 +581,10 @@ namespace pxt.editor {
575581
const msg = data as EditorMessageRequestProjectCloudStatus;
576582
return projectView.requestProjectCloudStatus(msg.headerIds);
577583
}
584+
case "convertcloudprojectstolocal": {
585+
const msg = data as EditorMessageConvertCloudProjectsToLocal;
586+
return projectView.convertCloudProjectsToLocal(msg.userId);
587+
}
578588
}
579589
return Promise.resolve();
580590
});

pxtlib/auth.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ namespace pxt.auth {
6060
export const DEFAULT_USER_PREFERENCES: () => UserPreferences = () => ({
6161
highContrast: false,
6262
language: pxt.appTarget.appTheme.defaultLocale,
63-
reader: ""
63+
reader: "",
64+
skillmap: {mapProgress: {}, completedTags: {}}
6465
});
6566

6667
/**

skillmap/src/App.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,11 @@ code {
141141
height: 80%;
142142
margin: .5rem;
143143
margin-right: 1rem;
144-
padding-top: .5rem;
145-
padding-left: 1rem;
144+
padding: .6rem;
146145
font-family: var(--feature-text-font);
147146
font-weight: 500;
148147
flex-direction: row-reverse;
148+
align-items: center;
149149
}
150150

151151
.user-menu .header-dropdown {

skillmap/src/App.tsx

Lines changed: 60 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
55

66
import store from "./store/store";
77
import * as authClient from "./lib/authClient";
8-
import { getFlattenedHeaderIds } from "./lib/skillMapUtils";
8+
import { getFlattenedHeaderIds, hasUrlBeenStarted } from "./lib/skillMapUtils";
99

1010
import {
1111
dispatchAddSkillMap,
@@ -34,13 +34,15 @@ import { parseHash, getMarkdownAsync, MarkdownSource, parseQuery,
3434
import { MakeCodeFrame } from './components/makecodeFrame';
3535
import { getLocalUserStateAsync, getUserStateAsync, saveUserStateAsync } from './lib/workspaceProvider';
3636
import { Unsubscribe } from 'redux';
37+
import { UserProfile } from './components/UserProfile';
38+
import { ReadyResources, ReadyPromise } from './lib/readyResources';
3739

3840
/* eslint-disable import/no-unassigned-import */
3941
import './App.css';
4042

4143
// TODO: this file needs to read colors from the target
4244
import './arcade.css';
43-
import { UserProfile } from './components/UserProfile';
45+
4446
/* eslint-enable import/no-unassigned-import */
4547
interface AppProps {
4648
skillMaps: { [key: string]: SkillMap };
@@ -64,21 +66,28 @@ interface AppProps {
6466

6567
interface AppState {
6668
error?: string;
69+
cloudSyncCheckHasFinished: boolean;
6770
}
6871

6972
class AppImpl extends React.Component<AppProps, AppState> {
7073
protected queryFlags: {[index: string]: string} = {};
7174
protected unsubscribeChangeListener: Unsubscribe | undefined;
7275
protected loadedUser: UserState | undefined;
73-
protected sendMessageAsync: ((message: any) => Promise<any>) | undefined;
76+
protected readyPromise: ReadyPromise;
7477

7578
constructor(props: any) {
7679
super(props);
77-
this.state = {};
80+
this.state = {
81+
cloudSyncCheckHasFinished: false
82+
};
83+
this.readyPromise = new ReadyPromise();
7884

7985
window.addEventListener("hashchange", this.handleHashChange);
86+
this.cloudSyncCheckAsync();
8087
}
8188

89+
protected ready = (): Promise<ReadyResources> => this.readyPromise.promise();
90+
8291
protected handleHashChange = async (e: HashChangeEvent) => {
8392
await this.parseHashAsync();
8493
e.stopPropagation();
@@ -209,75 +218,66 @@ class AppImpl extends React.Component<AppProps, AppState> {
209218
}
210219

211220
protected async cloudSyncCheckAsync() {
212-
if (await authClient.loggedInAsync() && this.sendMessageAsync && this.loadedUser) {
221+
const res = await this.ready();
222+
if (await authClient.loggedInAsync()) {
213223
const state = store.getState();
214224
const localUser = await getLocalUserStateAsync();
215225

216226
let currentUser = await getUserStateAsync();
217227
let headerIds = getFlattenedHeaderIds(localUser, state.pageSourceUrl, currentUser);
218-
219228
// Tell the editor to transfer local skillmap projects to the cloud.
220-
const headerMap = (await this.sendMessageAsync({
229+
const headerMap = (await res.sendMessageAsync!({
221230
type: "pxteditor",
222231
action: "savelocalprojectstocloud",
223232
headerIds
224233
} as pxt.editor.EditorMessageSaveLocalProjectsToCloud)).resp.headerIdMap;
225-
226234
if (headerMap) {
227-
headerIds = headerIds.map(h => headerMap[h] || h);
228-
229-
// Patch all of the header ids in the user state and copy
230-
// over the local progress that doesn't exist in the signed in
231-
// user
232-
const urls = Object.keys(currentUser.mapProgress);
233235
const newUser: UserState = {
234236
...currentUser,
235237
mapProgress: {}
236238
}
237239

238-
for (const url of urls) {
240+
const localUrls = Object.keys(localUser.mapProgress);
241+
for (const url of localUrls) {
242+
// Copy over local user progress. If there is cloud progress, it will
243+
// be overwritten
239244
newUser.mapProgress[url] = {
240-
...currentUser.mapProgress[url],
241-
};
242-
243-
if (!localUser.mapProgress[url]) continue;
245+
...localUser.mapProgress[url]
246+
}
244247

245248
const maps = Object.keys(localUser.mapProgress[url]);
246249
for (const map of maps) {
247-
newUser.mapProgress[url][map] = {
248-
...currentUser.mapProgress[url][map]
249-
};
250-
251250
// Only copy over state if the user hasn't started this map yet
252-
if (Object.keys(newUser.mapProgress[url][map].activityState).length !== 0) {
253-
continue;
254-
}
255-
256-
const activityState: {[index: string]: ActivityState} = {};
257-
newUser.mapProgress[url][map].activityState = activityState;
258-
259-
const signedInProgress = currentUser.mapProgress[url][map].activityState;
260-
const localProgress = localUser.mapProgress[url][map].activityState
261-
262-
for (const activity of Object.keys(signedInProgress)) {
263-
const oldState = signedInProgress[activity];
264-
activityState[activity] = {
265-
...oldState,
266-
headerId: oldState.headerId ? (headerMap[oldState.headerId] || oldState.headerId) : oldState.headerId
251+
if (!hasUrlBeenStarted(currentUser, url)) {
252+
newUser.mapProgress[url][map] = {
253+
...localUser.mapProgress[url][map]
267254
};
255+
newUser.completedTags[url] = localUser.completedTags[url];
256+
const activityState: {[index: string]: ActivityState} = {};
257+
newUser.mapProgress[url][map].activityState = activityState;
258+
259+
const localProgress = localUser.mapProgress[url][map].activityState
260+
for (const activity of Object.keys(localProgress)) {
261+
const localActivity = localProgress[activity];
262+
if (localActivity.headerId) {
263+
activityState[activity] = {
264+
...localActivity,
265+
headerId: headerMap[localActivity.headerId] || localActivity.headerId
266+
};
267+
}
268+
}
268269
}
270+
}
271+
}
269272

270-
for (const activity of Object.keys(localProgress)) {
271-
const signedInActivity = signedInProgress[activity];
272-
const localActivity = localProgress[activity];
273-
if ((!signedInActivity || !signedInActivity.headerId) && localActivity.headerId) {
274-
const base = signedInActivity || localActivity;
275-
activityState[activity] = {
276-
...base,
277-
headerId: localActivity.headerId ? (headerMap[localActivity.headerId] || localActivity.headerId) : localActivity.headerId
278-
};
279-
}
273+
const visitedUrls = Object.keys(currentUser.mapProgress)
274+
// Copy progress from cloud user for all visited URLs.
275+
for (const url of visitedUrls) {
276+
if (hasUrlBeenStarted(currentUser, url)) {
277+
newUser.mapProgress[url] = {
278+
...currentUser.mapProgress[url]
280279
}
280+
newUser.completedTags[url] = currentUser.completedTags[url]
281281
}
282282
}
283283

@@ -287,17 +287,17 @@ class AppImpl extends React.Component<AppProps, AppState> {
287287
}
288288

289289
// Tell the editor to send us the cloud status of our projects.
290-
await this.sendMessageAsync({
290+
await res.sendMessageAsync!({
291291
type: "pxteditor",
292292
action: "requestprojectcloudstatus",
293293
headerIds: getFlattenedHeaderIds(currentUser, state.pageSourceUrl)
294294
} as pxt.editor.EditorMessageRequestProjectCloudStatus);
295295
}
296+
this.setState({cloudSyncCheckHasFinished: true})
296297
}
297298

298299
protected onMakeCodeFrameLoaded = async (sendMessageAsync: (message: any) => Promise<any>) => {
299-
this.sendMessageAsync = sendMessageAsync;
300-
await this.cloudSyncCheckAsync();
300+
this.readyPromise.setSendMessageAsync(sendMessageAsync);
301301
}
302302

303303
async componentDidMount() {
@@ -309,7 +309,7 @@ class AppImpl extends React.Component<AppProps, AppState> {
309309
await authClient.authCheckAsync();
310310
await this.initLocalizationAsync();
311311
await this.parseHashAsync();
312-
await this.cloudSyncCheckAsync();
312+
this.readyPromise.setAppMounted();
313313
}
314314

315315
componentWillUnmount() {
@@ -386,8 +386,14 @@ class AppImpl extends React.Component<AppProps, AppState> {
386386
const { user } = store.getState();
387387

388388
if (user !== this.loadedUser && (!this.loadedUser || user.id === this.loadedUser.id)) {
389-
await saveUserStateAsync(user);
390-
this.loadedUser = user;
389+
// To avoid a race condition where we save to local user's state to the cloud user
390+
// before we get a chance to run the cloud upgrade rules on projects, we need to wait
391+
// for cloudSyncCheck to finish if we're logged in.
392+
if (!this.props.signedIn ||
393+
(this.props.signedIn && this.state.cloudSyncCheckHasFinished)) {
394+
await saveUserStateAsync(user);
395+
this.loadedUser = user;
396+
}
391397
}
392398
}
393399
}

skillmap/src/actions/dispatch.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ReadyResources } from '../lib/readyResources';
12
import { PageSourceStatus } from '../store/reducer';
23
import * as actions from './types'
34

@@ -45,3 +46,4 @@ export const dispatchCloseUserProfile = () => ({ type: actions.HIDE_USER_PROFILE
4546

4647
export const dispatchSetShareStatus = (headerId?: string, url?: string) => ({ type: actions.SET_SHARE_STATUS, headerId, url });
4748
export const dispatchSetCloudStatus = (headerId: string, status: string) => ({ type: actions.SET_CLOUD_STATUS, headerId, status });
49+
export const dispatchSetReadyResources = (resources: ReadyResources) => ({ type: actions.SET_READY_RESOURCES, resources });

skillmap/src/actions/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,4 @@ export const HIDE_USER_PROFILE = "HIDE_USER_PROFILE";
4242

4343
export const SET_SHARE_STATUS = "SET_SHARE_STATUS";
4444
export const SET_CLOUD_STATUS = "SET_CLOUD_STATUS";
45+
export const SET_READY_RESOURCES = "SET_READY_RESOURCES";

skillmap/src/components/CloudActions.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { connect } from 'react-redux';
33
import { dispatchShowLoginModal } from "../actions/dispatch";
44
import { SkillMapState } from "../store/reducer";
55
import { lookupActivityProgress } from "../lib/skillMapUtils";
6-
import { head } from "request";
76

87
interface OwnProps {
98
signedIn: boolean;

skillmap/src/components/HeaderBar.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ interface HeaderBarProps {
1616
activityOpen: boolean;
1717
showReportAbuse?: boolean;
1818
signedIn: boolean;
19+
profile: pxt.auth.UserProfile;
1920
dispatchSaveAndCloseActivity: () => void;
2021
dispatchShowResetUserModal: () => void;
2122
dispatchShowLoginModal: () => void;
@@ -86,9 +87,8 @@ export class HeaderBarImpl extends React.Component<HeaderBarProps> {
8687
}
8788

8889
protected getUserMenu() {
89-
const { signedIn } = this.props;
90+
const { signedIn, profile } = this.props;
9091
const items = [];
91-
const user = pxt.auth.client()?.getState().profile;
9292

9393
if (signedIn) {
9494
items.push({
@@ -102,20 +102,18 @@ export class HeaderBarImpl extends React.Component<HeaderBarProps> {
102102
})
103103
}
104104

105-
const avatarElem = user?.idp?.picture?.dataUrl
106-
? <div className="avatar"><img src={user?.idp?.picture?.dataUrl} alt={lf("User Menu")}/></div>
105+
const avatarElem = profile?.idp?.picture?.dataUrl
106+
? <div className="avatar"><img src={profile?.idp?.picture?.dataUrl} alt={lf("User Menu")}/></div>
107107
: undefined;
108108

109-
const initialsElem = user?.idp?.displayName
110-
? <span className="circle">{pxt.auth.userInitials(user)}</span>
111-
: undefined;
109+
const initialsElem = <span className="circle">{pxt.auth.userInitials(profile)}</span>
112110

113111
return <div className="user-menu">
114112
{signedIn
115-
? <Dropdown icon="star" items={items} picture={avatarElem || initialsElem} className="header-dropdown"/>
116-
: <HeaderBarButton className="sign-in" icon="xicon icon cloud-user" title={lf("Sign In")} label={lf("Sign In")} onClick={ () => {
113+
? <Dropdown icon="user" items={items} picture={avatarElem || initialsElem} className="header-dropdown"/>
114+
: <HeaderBarButton className="sign-in" icon="xicon cloud-user" title={lf("Sign In")} label={lf("Sign In")} onClick={ () => {
117115
pxt.tickEvent(`skillmap.usermenu.signin`);
118-
this.props.dispatchShowLoginModal();
116+
this.props.dispatchShowLoginModal();
119117
}}/>}
120118
</div>;
121119
}
@@ -222,7 +220,8 @@ function mapStateToProps(state: SkillMapState, ownProps: any) {
222220
currentMapId: activityOpen && state.editorView?.currentMapId,
223221
currentActivityId: activityOpen && state.editorView?.currentActivityId,
224222
showReportAbuse: state.pageSourceStatus === "unknown",
225-
signedIn: state.auth.signedIn
223+
signedIn: state.auth.signedIn,
224+
profile: state.auth.profile
226225
}
227226
}
228227

skillmap/src/lib/authClient.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@ class AuthClient extends pxt.auth.AuthClient {
3131
protected onStateCleared(): Promise<void> {
3232
return Promise.resolve();
3333
}
34-
protected onProfileDeleted(userId: string): Promise<void> {
34+
protected async onProfileDeleted(userId: string): Promise<void> {
3535
// Show a notification?
36-
return Promise.resolve();
36+
const state = store.getState();
37+
await state.readyResources?.exportCloudProjectsToLocal(userId);
3738
}
3839
protected onApiError(err: any): Promise<void> {
3940
// Show a notification?

0 commit comments

Comments
 (0)