pxt-core 7.4.7 → 7.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/built/pxt.js +90 -53
  2. package/built/pxtblockly.js +1361 -1450
  3. package/built/pxtblocks.d.ts +4 -4
  4. package/built/pxtblocks.js +35 -80
  5. package/built/pxtlib.d.ts +11 -2
  6. package/built/pxtlib.js +90 -53
  7. package/built/target.js +1 -1
  8. package/built/web/blockly.css +1 -1
  9. package/built/web/main.js +1 -1
  10. package/built/web/pxtapp.js +1 -1
  11. package/built/web/pxtasseteditor.js +1 -1
  12. package/built/web/pxtblockly.js +1 -1
  13. package/built/web/pxtblocks.js +1 -1
  14. package/built/web/pxtembed.js +1 -1
  15. package/built/web/pxtlib.js +1 -1
  16. package/built/web/pxtworker.js +1 -1
  17. package/built/web/react-common.css +10 -5
  18. package/built/web/rtlblockly.css +1 -1
  19. package/built/web/rtlsemantic.css +2 -2
  20. package/built/web/semantic.css +2 -2
  21. package/built/web/skillmap/css/main.96b1b3f1.chunk.css +1 -0
  22. package/built/web/skillmap/js/2.7dd06a3a.chunk.js +2 -0
  23. package/built/web/skillmap/js/main.55881627.chunk.js +1 -0
  24. package/localtypings/blockly.d.ts +12268 -7535
  25. package/localtypings/pxtarget.d.ts +1 -0
  26. package/package.json +2 -2
  27. package/theme/blockly-core.less +22 -13
  28. package/theme/melodyeditor.less +2 -2
  29. package/theme/print.less +1 -1
  30. package/theme/pxt.less +0 -1
  31. package/theme/toolbox.less +1 -0
  32. package/webapp/public/blockly/blockly_compressed.js +1271 -1288
  33. package/webapp/public/blockly/blocks_compressed.js +47 -65
  34. package/webapp/public/blockly/msg/js/en.js +8 -17
  35. package/webapp/public/blockly/msg/json/en.json +6 -15
  36. package/webapp/public/skillmap.html +2 -2
  37. package/built/web/skillmap/css/main.70954d9b.chunk.css +0 -1
  38. package/built/web/skillmap/js/2.c64f6be2.chunk.js +0 -2
  39. package/built/web/skillmap/js/main.86ef11e3.chunk.js +0 -1
@@ -471,7 +471,7 @@ declare namespace pxtblockly {
471
471
  protected animateRef: any;
472
472
  protected asset: pxt.Animation;
473
473
  protected initInterval: number;
474
- init(): void;
474
+ initView(): void;
475
475
  showEditor_(): void;
476
476
  render_(): void;
477
477
  protected getAssetType(): pxt.AssetType;
@@ -508,7 +508,7 @@ declare namespace pxtblockly {
508
508
  CURSOR: string;
509
509
  private type_;
510
510
  constructor(state: string, params: Blockly.FieldCustomOptions, opt_validator?: Function);
511
- init(): void;
511
+ initView(): void;
512
512
  updateSize_(): void;
513
513
  /**
514
514
  * Return 'TRUE' if the toggle is ON, 'FALSE' otherwise.
@@ -1243,7 +1243,7 @@ declare namespace pxtblockly {
1243
1243
  protected blocksInfo: pxtc.BlocksInfo;
1244
1244
  protected transparent: TilesetDropdownOption;
1245
1245
  constructor(text: string, options: FieldImageDropdownOptions, validator?: Function);
1246
- init(): void;
1246
+ initView(): void;
1247
1247
  getValue(): any;
1248
1248
  getText(): string;
1249
1249
  render_(): void;
@@ -1265,7 +1265,7 @@ declare namespace pxtblockly {
1265
1265
  CURSOR: string;
1266
1266
  private type_;
1267
1267
  constructor(state: string, params: Blockly.FieldCustomOptions, opt_validator?: Function);
1268
- init(): void;
1268
+ initView(): void;
1269
1269
  getDisplayText_(): string;
1270
1270
  getTrueText(): string;
1271
1271
  getFalseText(): string;
@@ -1497,7 +1497,7 @@ var pxt;
1497
1497
  function compileBlockAsync(b, blockInfo) {
1498
1498
  const w = b.workspace;
1499
1499
  const e = mkEnv(w, blockInfo);
1500
- infer(w && w.getAllBlocks(), e, w);
1500
+ infer(w && w.getAllBlocks(false), e, w);
1501
1501
  const compiled = compileStatementBlock(e, b);
1502
1502
  removeAllPlaceholders();
1503
1503
  return tdASTtoTS(e, compiled);
@@ -1518,7 +1518,7 @@ var pxt;
1518
1518
  function compileWorkspace(e, w, blockInfo) {
1519
1519
  try {
1520
1520
  // all compiled top level blocks are events
1521
- let allBlocks = w.getAllBlocks();
1521
+ let allBlocks = w.getAllBlocks(false);
1522
1522
  if (pxt.react.getTilemapProject) {
1523
1523
  pxt.react.getTilemapProject().removeInactiveBlockAssets(allBlocks.map(b => b.id));
1524
1524
  }
@@ -1788,13 +1788,11 @@ var pxt;
1788
1788
  });
1789
1789
  }
1790
1790
  function maybeAddComment(b, comments) {
1791
- if (b.comment) {
1792
- if ((typeof b.comment) === "string") {
1793
- comments.push(b.comment);
1794
- }
1795
- else {
1796
- comments.push(b.comment.getText());
1797
- }
1791
+ var _a;
1792
+ // Check if getCommentText exists, block may be placeholder
1793
+ const text = (_a = b.getCommentText) === null || _a === void 0 ? void 0 : _a.call(b);
1794
+ if (text) {
1795
+ comments.push(text);
1798
1796
  }
1799
1797
  }
1800
1798
  function addCommentNodes(comments, r) {
@@ -2457,9 +2455,9 @@ var pxt;
2457
2455
  }
2458
2456
  });
2459
2457
  // we'll ignore disabled blocks in the final output
2460
- const oldBlocks = oldWs.getAllBlocks().filter(b => b.isEnabled());
2458
+ const oldBlocks = oldWs.getAllBlocks(false).filter(b => b.isEnabled());
2461
2459
  const oldTopBlocks = oldWs.getTopBlocks(false).filter(b => b.isEnabled());
2462
- const newBlocks = newWs.getAllBlocks().filter(b => b.isEnabled());
2460
+ const newBlocks = newWs.getAllBlocks(false).filter(b => b.isEnabled());
2463
2461
  log(`blocks`, newBlocks.map(b => b.toDevString()));
2464
2462
  log(newBlocks);
2465
2463
  if (oldBlocks.length == 0 && newBlocks.length == 0) {
@@ -2481,11 +2479,11 @@ var pxt;
2481
2479
  const newXml = pxt.blocks.saveWorkspaceXml(newWs, true);
2482
2480
  pxt.blocks.domToWorkspaceNoEvents(Blockly.Xml.textToDom(newXml), ws);
2483
2481
  // delete disabled blocks from final workspace
2484
- ws.getAllBlocks().filter(b => !b.isEnabled()).forEach(b => {
2482
+ ws.getAllBlocks(false).filter(b => !b.isEnabled()).forEach(b => {
2485
2483
  log('disabled ', b.toDevString());
2486
2484
  b.dispose(false);
2487
2485
  });
2488
- const todoBlocks = pxt.Util.toDictionary(ws.getAllBlocks(), b => b.id);
2486
+ const todoBlocks = pxt.Util.toDictionary(ws.getAllBlocks(false), b => b.id);
2489
2487
  log(`todo blocks`, todoBlocks);
2490
2488
  logTodo('start');
2491
2489
  // 1. deleted top blocks
@@ -2495,7 +2493,7 @@ var pxt;
2495
2493
  done(b);
2496
2494
  const b2 = cloneIntoDiff(b);
2497
2495
  done(b2);
2498
- b2.setDisabled(true);
2496
+ b2.setEnabled(false);
2499
2497
  });
2500
2498
  logTodo('deleted top');
2501
2499
  }
@@ -2560,7 +2558,7 @@ var pxt;
2560
2558
  });
2561
2559
  logTodo('unmodified');
2562
2560
  // if nothing is left in the workspace, we "missed" change
2563
- if (!ws.getAllBlocks().length) {
2561
+ if (!ws.getAllBlocks(false).length) {
2564
2562
  pxt.tickEvent("blocks.diff", { missed: 1 });
2565
2563
  return {
2566
2564
  ws,
@@ -2593,7 +2591,7 @@ var pxt;
2593
2591
  function stitch(b) {
2594
2592
  log(`stitching ${b.toDevString()}->${dids[b.id]}`);
2595
2593
  const wb = ws.getBlockById(dids[b.id]);
2596
- wb.setDisabled(true);
2594
+ wb.setEnabled(false);
2597
2595
  markUsed(wb);
2598
2596
  done(wb);
2599
2597
  // connect previous connection to delted or existing block
@@ -2855,10 +2853,10 @@ var pxt;
2855
2853
  function applyMetaComments(workspace) {
2856
2854
  // process meta comments
2857
2855
  // @highlight -> highlight block
2858
- workspace.getAllBlocks()
2859
- .filter(b => !!b.comment && b.comment instanceof Blockly.Comment)
2856
+ workspace.getAllBlocks(false)
2857
+ .filter(b => !!b.getCommentText())
2860
2858
  .forEach(b => {
2861
- const c = b.comment.getText();
2859
+ const c = b.getCommentText();
2862
2860
  if (/@highlight/.test(c)) {
2863
2861
  const cc = c.replace(/@highlight/g, '').trim();
2864
2862
  b.setCommentText(cc || null);
@@ -4343,7 +4341,7 @@ var pxt;
4343
4341
  }
4344
4342
  else if (pr.type == "number" && pr.shadowBlockId && pr.shadowBlockId == "value") {
4345
4343
  inputName = undefined;
4346
- fields.push(namedField(new Blockly.FieldTextInput("0", Blockly.FieldTextInput.numberValidator), defName));
4344
+ fields.push(namedField(new Blockly.FieldNumber("0"), defName));
4347
4345
  }
4348
4346
  else if (pr.type == "string" && pr.shadowOptions && pr.shadowOptions.toString) {
4349
4347
  inputCheck = null;
@@ -5445,11 +5443,6 @@ var pxt;
5445
5443
  let blockText = '<xml>' +
5446
5444
  '<block type="variables_change" gap="' + gap + '">' +
5447
5445
  Blockly.Variables.generateVariableFieldXmlString(mostRecentVariable) +
5448
- '<value name="DELTA">' +
5449
- '<shadow type="math_number">' +
5450
- '<field name="NUM">1</field>' +
5451
- '</shadow>' +
5452
- '</value>' +
5453
5446
  '</block>' +
5454
5447
  '</xml>';
5455
5448
  let block = Blockly.Xml.textToDom(blockText).firstChild;
@@ -7622,19 +7615,8 @@ var pxtblockly;
7622
7615
  this.valueText = text;
7623
7616
  }
7624
7617
  init() {
7625
- if (this.isInitialized())
7626
- return;
7627
- // Build the DOM.
7628
- this.fieldGroup_ = Blockly.utils.dom.createSvgElement('g', {}, null);
7629
- if (!this.visible_) {
7630
- this.fieldGroup_.style.display = 'none';
7631
- }
7618
+ super.init();
7632
7619
  this.onInit();
7633
- this.updateEditable();
7634
- this.sourceBlock_.getSvgRoot().appendChild(this.fieldGroup_);
7635
- // Force a render.
7636
- this.render_();
7637
- this.mouseDownWrapper_ = Blockly.bindEventWithChecks_(this.getClickTarget_(), "mousedown", this, this.onMouseDown_);
7638
7620
  }
7639
7621
  dispose() {
7640
7622
  this.onDispose();
@@ -7787,9 +7769,11 @@ var pxtblockly;
7787
7769
  }
7788
7770
  getDisplayText_() {
7789
7771
  // This is only used when isGreyBlock is true
7790
- const text = pxt.Util.htmlUnescape(this.valueText);
7791
- return text.substr(0, text.indexOf("(")) + "(...)";
7792
- ;
7772
+ if (this.isGreyBlock) {
7773
+ const text = pxt.Util.htmlUnescape(this.valueText);
7774
+ return text.substr(0, text.indexOf("(")) + "(...)";
7775
+ }
7776
+ return "";
7793
7777
  }
7794
7778
  updateEditable() {
7795
7779
  if (this.isGreyBlock && this.fieldGroup_) {
@@ -8046,12 +8030,7 @@ var pxtblockly;
8046
8030
  }
8047
8031
  };
8048
8032
  }
8049
- init() {
8050
- if (this.fieldGroup_) {
8051
- // Field has already been initialized once.
8052
- return;
8053
- }
8054
- super.init();
8033
+ initView() {
8055
8034
  // Register mouseover events for animating preview
8056
8035
  this.sourceBlock_.getSvgRoot().addEventListener("mouseenter", this.onMouseEnter);
8057
8036
  this.sourceBlock_.getSvgRoot().addEventListener("mouseleave", this.onMouseLeave);
@@ -8257,16 +8236,10 @@ var pxtblockly;
8257
8236
  this.addArgType('toggle');
8258
8237
  this.type_ = params.type;
8259
8238
  }
8260
- init() {
8261
- if (this.fieldGroup_) {
8262
- // Field has already been initialized once.
8239
+ initView() {
8240
+ if (!this.fieldGroup_) {
8263
8241
  return;
8264
8242
  }
8265
- // Build the DOM.
8266
- this.fieldGroup_ = Blockly.utils.dom.createSvgElement('g', {}, null);
8267
- if (!this.visible_) {
8268
- this.fieldGroup_.style.display = 'none';
8269
- }
8270
8243
  // Add an attribute to cassify the type of field.
8271
8244
  if (this.getArgTypes() !== null) {
8272
8245
  if (this.sourceBlock_.isShadow()) {
@@ -8295,15 +8268,10 @@ var pxtblockly;
8295
8268
  'dy': '0.6ex',
8296
8269
  'y': size.height / 2
8297
8270
  }, this.fieldGroup_);
8298
- this.updateEditable();
8299
- this.sourceBlock_.getSvgRoot().appendChild(this.fieldGroup_);
8300
8271
  this.switchToggle(this.state_);
8301
8272
  this.setValue(this.getValue());
8302
8273
  // Force a render.
8303
- this.render_();
8304
- this.size_.width = 0;
8305
- this.mouseDownWrapper_ =
8306
- Blockly.bindEventWithChecks_(this.getClickTarget_(), 'mousedown', this, this.onMouseDown_);
8274
+ this.markDirty();
8307
8275
  }
8308
8276
  updateSize_() {
8309
8277
  this.size_.width = 30;
@@ -9773,7 +9741,7 @@ var pxtblockly;
9773
9741
  }
9774
9742
  render_() {
9775
9743
  if (!this.visible_) {
9776
- this.size_.width = 0;
9744
+ this.markDirty();
9777
9745
  return;
9778
9746
  }
9779
9747
  if (!this.elt) {
@@ -11277,7 +11245,7 @@ var pxtblockly;
11277
11245
  dropdownCreate() {
11278
11246
  let functionList = [];
11279
11247
  if (this.sourceBlock_ && this.sourceBlock_.workspace) {
11280
- let blocks = this.sourceBlock_.workspace.getAllBlocks();
11248
+ let blocks = this.sourceBlock_.workspace.getAllBlocks(false);
11281
11249
  // Iterate through every block and check the name.
11282
11250
  for (let i = 0; i < blocks.length; i++) {
11283
11251
  if (blocks[i].getProcedureDef) {
@@ -11306,10 +11274,6 @@ var pxtblockly;
11306
11274
  }
11307
11275
  onItemSelected(menu, menuItem) {
11308
11276
  let itemText = menuItem.getValue();
11309
- if (this.sourceBlock_) {
11310
- // Call any validation function, and allow it to override.
11311
- itemText = this.callValidator(itemText);
11312
- }
11313
11277
  if (itemText !== null) {
11314
11278
  this.setValue(itemText);
11315
11279
  }
@@ -11804,8 +11768,8 @@ var pxtblockly;
11804
11768
  }
11805
11769
  return FieldTileset.referencedTiles;
11806
11770
  }
11807
- init() {
11808
- super.init();
11771
+ initView() {
11772
+ super.initView();
11809
11773
  if (this.sourceBlock_ && this.sourceBlock_.isInFlyout) {
11810
11774
  this.setValue(this.getOptions()[0][1]);
11811
11775
  }
@@ -11954,16 +11918,10 @@ var pxtblockly;
11954
11918
  this.addArgType('toggle');
11955
11919
  this.type_ = params.type;
11956
11920
  }
11957
- init() {
11958
- if (this.fieldGroup_) {
11959
- // Field has already been initialized once.
11921
+ initView() {
11922
+ if (!this.fieldGroup_) {
11960
11923
  return;
11961
11924
  }
11962
- // Build the DOM.
11963
- this.fieldGroup_ = Blockly.utils.dom.createSvgElement('g', {}, null);
11964
- if (!this.visible_) {
11965
- this.fieldGroup_.style.display = 'none';
11966
- }
11967
11925
  // Add an attribute to cassify the type of field.
11968
11926
  if (this.getArgTypes() !== null) {
11969
11927
  if (this.sourceBlock_.isShadow()) {
@@ -12035,10 +11993,7 @@ var pxtblockly;
12035
11993
  this.switchToggle(this.state_);
12036
11994
  this.setValue(this.getValue());
12037
11995
  // Force a render.
12038
- this.render_();
12039
- this.size_.width = 0;
12040
- this.mouseDownWrapper_ =
12041
- Blockly.bindEventWithChecks_(this.getClickTarget_(), 'mousedown', this, this.onMouseDown_);
11996
+ this.markDirty();
12042
11997
  }
12043
11998
  getDisplayText_() {
12044
11999
  return this.state_ ? this.getTrueText() : this.getFalseText();
package/built/pxtlib.d.ts CHANGED
@@ -57,6 +57,10 @@ declare namespace pxt.auth {
57
57
  type UserBadgeState = {
58
58
  badges: Badge[];
59
59
  };
60
+ type SetPrefResult = {
61
+ success: boolean;
62
+ res: UserPreferences;
63
+ };
60
64
  /**
61
65
  * User preference state that should be synced with the cloud.
62
66
  */
@@ -66,6 +70,7 @@ declare namespace pxt.auth {
66
70
  reader?: string;
67
71
  skillmap?: UserSkillmapState;
68
72
  badges?: UserBadgeState;
73
+ email?: boolean;
69
74
  };
70
75
  const DEFAULT_USER_PREFERENCES: () => UserPreferences;
71
76
  /**
@@ -124,8 +129,11 @@ declare namespace pxt.auth {
124
129
  username?: string;
125
130
  avatarUrl?: string;
126
131
  }): Promise<boolean>;
127
- private prefPatchOps;
128
- patchUserPreferencesAsync(ops: ts.pxtc.jsonPatch.PatchOperation | ts.pxtc.jsonPatch.PatchOperation[], immediate?: boolean): Promise<void>;
132
+ private patchQueue;
133
+ patchUserPreferencesAsync(patchOps: ts.pxtc.jsonPatch.PatchOperation | ts.pxtc.jsonPatch.PatchOperation[], opts?: {
134
+ immediate?: boolean;
135
+ filter?: (op: ts.pxtc.jsonPatch.PatchOperation) => boolean;
136
+ }): Promise<SetPrefResult>;
129
137
  hasUserId(): boolean;
130
138
  private fetchUserAsync;
131
139
  private setUserProfileAsync;
@@ -297,6 +305,7 @@ declare namespace ts.pxtc.Util {
297
305
  export function groupBy<T>(arr: T[], f: (t: T) => string): pxt.Map<T[]>;
298
306
  export function toDictionary<T>(arr: T[], f: (t: T) => string): pxt.Map<T>;
299
307
  export function toSet<T>(arr: T[], f: (t: T) => string): pxt.Map<boolean>;
308
+ export function deepCopy(src: any): any;
300
309
  export interface ArrayLike<T> {
301
310
  [index: number]: T;
302
311
  length: number;
package/built/pxtlib.js CHANGED
@@ -185,7 +185,8 @@ var pxt;
185
185
  highContrast: false,
186
186
  language: pxt.appTarget.appTheme.defaultLocale,
187
187
  reader: "",
188
- skillmap: { mapProgress: {}, completedTags: {} }
188
+ skillmap: { mapProgress: {}, completedTags: {} },
189
+ email: false
189
190
  });
190
191
  let _client;
191
192
  function client() { return _client; }
@@ -198,7 +199,7 @@ var pxt;
198
199
  constructor() {
199
200
  this.initialUserPreferences_ = undefined;
200
201
  this.initialAuthCheck_ = undefined;
201
- this.prefPatchOps = [];
202
+ this.patchQueue = [];
202
203
  pxt.Util.assert(!_client);
203
204
  // Set global instance.
204
205
  _client = this;
@@ -388,62 +389,86 @@ var pxt;
388
389
  }
389
390
  return result.success;
390
391
  }
391
- async patchUserPreferencesAsync(ops, immediate = false) {
392
- ops = Array.isArray(ops) ? ops : [ops];
393
- ops = ops.filter(op => !!op);
394
- if (!ops.length) {
395
- return;
396
- }
392
+ async patchUserPreferencesAsync(patchOps, opts = {}) {
393
+ const defaultSuccessAsync = async () => ({ success: true, res: await this.userPreferencesAsync() });
394
+ patchOps = Array.isArray(patchOps) ? patchOps : [patchOps];
395
+ patchOps = patchOps.filter(op => !!op);
396
+ if (!patchOps.length) {
397
+ return await defaultSuccessAsync();
398
+ }
399
+ const patchDiff = (pSrc, ops, filter) => {
400
+ // Apply patches to pDst and return the diff as a set of new patch ops.
401
+ const pDst = pxt.U.deepCopy(pSrc);
402
+ ts.pxtc.jsonPatch.patchInPlace(pDst, ops);
403
+ let diff = ts.pxtc.jsonPatch.diff(pSrc, pDst);
404
+ // Run caller-provided filter
405
+ if (diff.length && filter) {
406
+ diff = diff.filter(filter);
407
+ }
408
+ return diff;
409
+ };
410
+ // Process incoming patch operations to produce a more fine-grained set of diffs. Incoming patches may be overly destructive
411
+ // Apply the patch in isolation and get the diff from original
397
412
  const curPref = await this.userPreferencesAsync();
398
- ts.pxtc.jsonPatch.patchInPlace(curPref, ops);
413
+ const diff = patchDiff(curPref, patchOps, opts.filter);
414
+ if (!diff.length) {
415
+ return await defaultSuccessAsync();
416
+ }
417
+ // Apply the new diff to the current state
418
+ ts.pxtc.jsonPatch.patchInPlace(curPref, diff);
399
419
  await this.setUserPreferencesAsync(curPref);
400
- // If we're not logged in, non-persistent local state is all we'll use
420
+ // If the user is not logged in, non-persistent local state is all we'll use (no sync to cloud)
401
421
  if (!await this.loggedInAsync()) {
402
- return;
422
+ return await defaultSuccessAsync();
403
423
  }
404
- // If the user is logged in, save to cloud, but debounce the api call as this can be called frequently from skillmaps
405
- // Replace matching patches in the queue
406
- ops.forEach((incoming, iIncoming) => {
407
- this.prefPatchOps.some((existing, iExisting) => {
408
- if (!ts.pxtc.jsonPatch.opsAreEqual(existing, incoming))
409
- return false;
410
- // Patches are equivalent, replace in queue
411
- this.prefPatchOps[iExisting] = incoming;
412
- // Clear from incoming so we don't add it below
413
- ops[iIncoming] = null;
414
- return true;
415
- });
416
- });
417
- // Add remaining ops to the queue
418
- ops.filter(op => !!op).forEach(op => this.prefPatchOps.push(op));
424
+ // If the user is logged in, sync to cloud, but debounce the api call as this can be called frequently from skillmaps
425
+ // Queue the patch for sync with backend
426
+ this.patchQueue.push({ ops: patchOps, filter: opts.filter });
419
427
  clearTimeout(debouncePreferencesChangedTimeout);
420
- const savePrefs = async () => {
428
+ const syncPrefs = async () => {
421
429
  debouncePreferencesChangedStarted = 0;
422
- // Clear queued patch ops before send.
423
- const prefPatchOps = this.prefPatchOps;
424
- this.prefPatchOps = [];
425
- const result = await this.apiAsync('/api/user/preferences', prefPatchOps, 'PATCH');
426
- if (result.success) {
427
- pxt.debug("Updating local user preferences w/ cloud data after result of POST");
428
- // Set user profile from returned value so we stay in-sync
429
- this.setUserPreferencesAsync(result.resp);
430
+ if (!this.patchQueue.length) {
431
+ return await defaultSuccessAsync();
432
+ }
433
+ // Fetch latest prefs from remote
434
+ const getResult = await this.apiAsync('/api/user/preferences');
435
+ if (!getResult.success) {
436
+ pxt.reportError("identity", "failed to fetch preferences for patch", getResult);
437
+ return { success: false, res: undefined };
438
+ }
439
+ // Apply queued patches to the remote state in isolation and develop a final diff to send to the backend
440
+ const remotePrefs = pxt.U.deepCopy(getResult.resp);
441
+ const patchQueue = this.patchQueue;
442
+ this.patchQueue = []; // Reset the queue
443
+ patchQueue.forEach(patch => {
444
+ const diff = patchDiff(remotePrefs, patch.ops, patch.filter);
445
+ ts.pxtc.jsonPatch.patchInPlace(remotePrefs, diff);
446
+ });
447
+ // Diff the original and patched remote states to get a final set of patch operations
448
+ const finalOps = pxtc.jsonPatch.diff(getResult.resp, remotePrefs);
449
+ const patchResult = await this.apiAsync('/api/user/preferences', finalOps, 'PATCH');
450
+ if (patchResult.success) {
451
+ // Set user profile from returned value so we stay in sync
452
+ this.setUserPreferencesAsync(patchResult.resp);
430
453
  }
431
454
  else {
432
- pxt.reportError("identity", "update preferences failed", result);
455
+ pxt.reportError("identity", "failed to patch preferences", patchResult);
433
456
  }
457
+ return { success: patchResult.success, res: patchResult.resp };
434
458
  };
435
- if (immediate) {
436
- await savePrefs();
459
+ if (opts.immediate) {
460
+ return await syncPrefs();
437
461
  }
438
462
  else {
439
463
  if (!debouncePreferencesChangedStarted) {
440
464
  debouncePreferencesChangedStarted = pxt.U.now();
441
465
  }
442
466
  if (PREFERENCES_DEBOUNCE_MAX_MS < pxt.U.now() - debouncePreferencesChangedStarted) {
443
- await savePrefs();
467
+ return await syncPrefs();
444
468
  }
445
469
  else {
446
- debouncePreferencesChangedTimeout = setTimeout(savePrefs, PREFERENCES_DEBOUNCE_MS);
470
+ debouncePreferencesChangedTimeout = setTimeout(syncPrefs, PREFERENCES_DEBOUNCE_MS);
471
+ return { success: false, res: undefined }; // This needs to be implemented correctly to return a promise with the debouncer
447
472
  }
448
473
  }
449
474
  }
@@ -1525,6 +1550,18 @@ var ts;
1525
1550
  return r;
1526
1551
  }
1527
1552
  Util.toSet = toSet;
1553
+ function deepCopy(src) {
1554
+ if (typeof src !== "object" || src === null) {
1555
+ return src;
1556
+ }
1557
+ const dst = Array.isArray(src) ? [] : {};
1558
+ for (const key in src) {
1559
+ const value = src[key];
1560
+ dst[key] = deepCopy(value);
1561
+ }
1562
+ return dst;
1563
+ }
1564
+ Util.deepCopy = deepCopy;
1528
1565
  function toArray(a) {
1529
1566
  if (Array.isArray(a)) {
1530
1567
  return a;
@@ -3944,7 +3981,7 @@ var pxt;
3944
3981
  'pxt_controls_for': {
3945
3982
  name: pxt.Util.lf("a loop that repeats the number of times you say"),
3946
3983
  tooltip: pxt.Util.lf("Have the variable '{0}' take on the values from 0 to the end number, counting by 1, and do the specified blocks."),
3947
- url: 'blocks/loops/for',
3984
+ url: '/blocks/loops/for',
3948
3985
  category: 'loops',
3949
3986
  block: {
3950
3987
  message0: pxt.Util.lf("for %1 from 0 to %2"),
@@ -3955,7 +3992,7 @@ var pxt;
3955
3992
  'controls_simple_for': {
3956
3993
  name: pxt.Util.lf("a loop that repeats the number of times you say"),
3957
3994
  tooltip: pxt.Util.lf("Have the variable '{0}' take on the values from 0 to the end number, counting by 1, and do the specified blocks."),
3958
- url: 'blocks/loops/for',
3995
+ url: '/blocks/loops/for',
3959
3996
  category: 'loops',
3960
3997
  block: {
3961
3998
  message0: pxt.Util.lf("for %1 from 0 to %2"),
@@ -3966,7 +4003,7 @@ var pxt;
3966
4003
  'pxt_controls_for_of': {
3967
4004
  name: pxt.Util.lf("a loop that repeats for each value in an array"),
3968
4005
  tooltip: pxt.Util.lf("Have the variable '{0}' take the value of each item in the array one by one, and do the specified blocks."),
3969
- url: 'blocks/loops/for-of',
4006
+ url: '/blocks/loops/for-of',
3970
4007
  category: 'loops',
3971
4008
  block: {
3972
4009
  message0: pxt.Util.lf("for element %1 of %2"),
@@ -3977,7 +4014,7 @@ var pxt;
3977
4014
  'controls_for_of': {
3978
4015
  name: pxt.Util.lf("a loop that repeats for each value in an array"),
3979
4016
  tooltip: pxt.Util.lf("Have the variable '{0}' take the value of each item in the array one by one, and do the specified blocks."),
3980
- url: 'blocks/loops/for-of',
4017
+ url: '/blocks/loops/for-of',
3981
4018
  category: 'loops',
3982
4019
  block: {
3983
4020
  message0: pxt.Util.lf("for element %1 of %2"),
@@ -4262,7 +4299,7 @@ var pxt;
4262
4299
  'text': {
4263
4300
  name: pxt.Util.lf("a piece of text"),
4264
4301
  tooltip: pxt.Util.lf("A letter, word, or line of text."),
4265
- url: 'types/string',
4302
+ url: '/types/string',
4266
4303
  category: 'text',
4267
4304
  block: {
4268
4305
  search: pxt.Util.lf("a piece of text") // Only used for search; this string is not surfaced in the block's text
@@ -4271,7 +4308,7 @@ var pxt;
4271
4308
  'text_length': {
4272
4309
  name: pxt.Util.lf("number of characters in the string"),
4273
4310
  tooltip: pxt.Util.lf("Returns the number of letters (including spaces) in the provided text."),
4274
- url: 'reference/text/length',
4311
+ url: '/reference/text/length',
4275
4312
  category: 'text',
4276
4313
  block: {
4277
4314
  TEXT_LENGTH_TITLE: pxt.Util.lf("length of %1")
@@ -4280,7 +4317,7 @@ var pxt;
4280
4317
  'text_join': {
4281
4318
  name: pxt.Util.lf("join items to create text"),
4282
4319
  tooltip: pxt.Util.lf("Create a piece of text by joining together any number of items."),
4283
- url: 'reference/text/join',
4320
+ url: '/reference/text/join',
4284
4321
  category: 'text',
4285
4322
  block: {
4286
4323
  TEXT_JOIN_TITLE_CREATEWITH: pxt.Util.lf("join")
@@ -4289,7 +4326,7 @@ var pxt;
4289
4326
  'procedures_defnoreturn': {
4290
4327
  name: pxt.Util.lf("define the function"),
4291
4328
  tooltip: pxt.Util.lf("Create a function."),
4292
- url: 'types/function/define',
4329
+ url: '/types/function/define',
4293
4330
  category: 'functions',
4294
4331
  block: {
4295
4332
  PROCEDURES_DEFNORETURN_TITLE: pxt.Util.lf("function"),
@@ -4299,7 +4336,7 @@ var pxt;
4299
4336
  'procedures_callnoreturn': {
4300
4337
  name: pxt.Util.lf("call the function"),
4301
4338
  tooltip: pxt.Util.lf("Call the user-defined function."),
4302
- url: 'types/function/call',
4339
+ url: '/types/function/call',
4303
4340
  category: 'functions',
4304
4341
  block: {
4305
4342
  PROCEDURES_CALLNORETURN_TITLE: pxt.Util.lf("call function")
@@ -4308,7 +4345,7 @@ var pxt;
4308
4345
  'function_return': {
4309
4346
  name: pxt.Util.lf("return a value from within a function"),
4310
4347
  tooltip: pxt.Util.lf("Return a value from within a user-defined function."),
4311
- url: 'types/function/return',
4348
+ url: '/types/function/return',
4312
4349
  category: 'functions',
4313
4350
  block: {
4314
4351
  message_with_value: pxt.Util.lf("return %1"),
@@ -4318,7 +4355,7 @@ var pxt;
4318
4355
  'function_definition': {
4319
4356
  name: pxt.Util.lf("define the function"),
4320
4357
  tooltip: pxt.Util.lf("Create a function."),
4321
- url: 'types/function/define',
4358
+ url: '/types/function/define',
4322
4359
  category: 'functions',
4323
4360
  block: {
4324
4361
  FUNCTIONS_EDIT_OPTION: pxt.Util.lf("Edit Function")
@@ -4327,7 +4364,7 @@ var pxt;
4327
4364
  'function_call': {
4328
4365
  name: pxt.Util.lf("call the function"),
4329
4366
  tooltip: pxt.Util.lf("Call the user-defined function."),
4330
- url: 'types/function/call',
4367
+ url: '/types/function/call',
4331
4368
  category: 'functions',
4332
4369
  block: {
4333
4370
  FUNCTIONS_CALL_TITLE: pxt.Util.lf("call"),
@@ -4337,7 +4374,7 @@ var pxt;
4337
4374
  'function_call_output': {
4338
4375
  name: pxt.Util.lf("call the function with a return value"),
4339
4376
  tooltip: pxt.Util.lf("Call the user-defined function with a return value."),
4340
- url: 'types/function/call',
4377
+ url: '/types/function/call',
4341
4378
  category: 'functions',
4342
4379
  block: {}
4343
4380
  }