@magic-xpa/engine 4.1300.0-dev4130.183 → 4.1300.0-dev4130.185

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.
@@ -1,6 +1,6 @@
1
1
  import { StringBuilder, Int32, NString, ApplicationException, NNumber, List, Debug, Char, RefParam, NumberStyles, HashUtils, DateTime, Dictionary, Stack, NChar, isNullOrUndefined, WebException, Thread, Encoding, Exception, isUndefined, Hashtable, NotImplementedException, Array_Enumerator, ISO_8859_1_Encoding, Int64 } from '@magic-xpa/mscorelib';
2
2
  import { XMLConstants, StorageAttribute, ViewRefreshMode, InternalInterface, Logger, StorageAttributeCheck, StrUtil, SubformType, TableBehaviour, MgControlType, ScrollBarThumbType, ForceExit, XmlParser, Misc, Base64, Priority, SyncExecutionHelper, Queue, Constants, DateTimeUtils, Logger_LogLevels, Logger_MessageDirection, MsgInterface, RequestInfo, OSEnvironment, JSON_Utils, UtilDateJpn, UtilStrByteMode, PICInterface, WindowType, BrkScope, RaiseAt, CtrlButtonTypeGui } from '@magic-xpa/utils';
3
- import { RecordUtils, GuiFieldBase, ExpVal, BlobType, FieldDef, GuiTaskBase, MgControlBase, PropInterface, GuiDataCollection, CommandType, Commands, HtmlProperties, ControlTable, Modifiers, KeyboardItem, TaskDefinitionIdTableSaxHandler, DisplayConvertor, VectorType, PIC, MgTimer, GuiConstants, RuntimeContextBase, UsernamePasswordCredentials, Styles, Manager, NUM_TYPE, GuiExpressionEvaluator, ExpressionInterface, DataModificationTypes, GuiDataViewBase, ObjectReferencesCollection, EMPTY_DCREF, ObjectReferenceBase, PropTable, FieldsTable as FieldsTable$1, DcValuesBuilderBase, MgFormBase, GuiEnvironment, TaskDefinitionId, Events, Helps, FocusManager, EventsProcessor, UIBridge } from '@magic-xpa/gui';
3
+ import { RecordUtils, GuiFieldBase, ExpVal, BlobType, FieldDef, GuiTaskBase, MgControlBase, PropInterface, GuiDataCollection, CommandType, Commands, HtmlProperties, ControlTable, Modifiers, KeyboardItem, TaskDefinitionIdTableSaxHandler, DisplayConvertor, VectorType, PIC, MgTimer, GuiConstants, RuntimeContextBase, UsernamePasswordCredentials, Styles, Manager, NUM_TYPE, GuiExpressionEvaluator, ExpressionInterface, DateBreakParams, DataModificationTypes, GuiDataViewBase, ObjectReferencesCollection, EMPTY_DCREF, ObjectReferenceBase, PropTable, FieldsTable as FieldsTable$1, DcValuesBuilderBase, MgFormBase, GuiEnvironment, TaskDefinitionId, Events, Helps, FocusManager, EventsProcessor, UIBridge } from '@magic-xpa/gui';
4
4
  import { HttpHeaders, HttpErrorResponse } from '@angular/common/http';
5
5
  import { timer, Subject } from 'rxjs';
6
6
  import { io } from 'socket.io-client';
@@ -9784,6 +9784,10 @@ class ExpressionDict {
9784
9784
  null,
9785
9785
  new ExpDesc('U', 0, -1, -1, 'B', false), /* 719- Subscribe */
9786
9786
  new ExpDesc('U', 0, -1, -1, 'B', false), /* 720- Unsubscribe */
9787
+ null,
9788
+ null,
9789
+ new ExpDesc('B', 0, 4, 4, 'TDVV', false), /* 723- LocalToUTC */
9790
+ new ExpDesc('B', 0, 4, 4, 'TDVV', false), /* 724- UTCToLocal */
9787
9791
  ];
9788
9792
  }
9789
9793
 
@@ -15642,6 +15646,20 @@ class ExpressionEvaluator extends GuiExpressionEvaluator {
15642
15646
  resVal.Attr = StorageAttribute.BOOLEAN;
15643
15647
  resVal.BoolVal = this.eval_op_UnSubscribe(valStack);
15644
15648
  break;
15649
+ case ExpressionInterface.EXP_OP_LOCALTOUTC:
15650
+ val4 = valStack.pop(); // UTC date output variable
15651
+ val3 = valStack.pop(); // UTC time output variable
15652
+ val2 = valStack.pop(); // local date
15653
+ val1 = valStack.pop(); // local time
15654
+ await this.eval_op_localToUTC(resVal, val1, val2, val3, val4);
15655
+ break;
15656
+ case ExpressionInterface.EXP_OP_UTCTOLOCAL:
15657
+ val4 = valStack.pop(); // local date output variable
15658
+ val3 = valStack.pop(); // local time output variable
15659
+ val2 = valStack.pop(); // UTC date
15660
+ val1 = valStack.pop(); // UTC time
15661
+ await this.eval_op_utcToLocal(resVal, val1, val2, val3, val4);
15662
+ break;
15645
15663
  default:
15646
15664
  return;
15647
15665
  }
@@ -16556,6 +16574,217 @@ class ExpressionEvaluator extends GuiExpressionEvaluator {
16556
16574
  resVal.Attr = StorageAttribute.BOOLEAN;
16557
16575
  resVal.BoolVal = true;
16558
16576
  }
16577
+ /**
16578
+ * Safely extracts date/time parts from a JS Date using Intl.DateTimeFormat.
16579
+ *
16580
+ * WHY: Native JS Date has known pitfalls — century year bug (year < 100 mapped to 1900+year),
16581
+ * silent leap year rollovers, DST gaps, and 0-based months. Intl.DateTimeFormat avoids all
16582
+ * of these by using the browser's IANA timezone database internally.
16583
+ *
16584
+ * Used by both eval_op_localToUTC and eval_op_utcToLocal as a single safe extraction point.
16585
+ *
16586
+ * @param date - A valid JS Date object
16587
+ * @param timeZone - IANA timezone string (e.g. 'Asia/Kolkata', 'UTC').
16588
+ * Defaults to browser's local timezone — correct for any user, any country.
16589
+ * @returns Parsed parts (month is 1-based) or null if date is invalid
16590
+ */
16591
+ convertDateTimeParts(date, timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone) {
16592
+ if (!date || isNaN(date.getTime()))
16593
+ return null;
16594
+ // 'en-CA' locale formats date as YYYY-MM-DD — unambiguous and consistent across
16595
+ // all browsers. Other locales like en-US (MM/DD/YYYY) or en-GB (DD/MM/YYYY) can
16596
+ // behave differently across Chrome, Firefox and Safari. Nothing to do with Canada.
16597
+ const formatter = new Intl.DateTimeFormat('en-CA', {
16598
+ timeZone,
16599
+ year: 'numeric',
16600
+ month: '2-digit',
16601
+ day: '2-digit',
16602
+ hour: '2-digit',
16603
+ minute: '2-digit',
16604
+ second: '2-digit',
16605
+ hour12: false
16606
+ });
16607
+ const parts = formatter.formatToParts(date);
16608
+ const get = (type) => {
16609
+ const part = parts.find(p => p.type === type);
16610
+ if (!part)
16611
+ throw new Error(`Missing date part: ${type}`);
16612
+ return parseInt(part.value, 10);
16613
+ };
16614
+ try {
16615
+ const hour = get('hour');
16616
+ return {
16617
+ year: get('year'),
16618
+ month: get('month'), // 1-based because Intl returns display month (1–12), unlike JS Date.getMonth() which is 0-based
16619
+ day: get('day'),
16620
+ hour: hour === 24 ? 0 : hour, // normalize midnight — some browsers return 24 instead of 0
16621
+ minute: get('minute'),
16622
+ second: get('second')
16623
+ };
16624
+ }
16625
+ catch {
16626
+ return null;
16627
+ }
16628
+ }
16629
+ /**
16630
+ * Validates that a time value (in total seconds) is within a valid day range
16631
+ */
16632
+ isValidTimeSec(timeSec) {
16633
+ return Number.isInteger(timeSec) && timeSec >= 0 && timeSec <= 86399;
16634
+ }
16635
+ /**
16636
+ * Splits total seconds since midnight into hours, minutes, seconds.
16637
+ * e.g. 3661 → { hours: 1, minutes: 1, seconds: 1 }
16638
+ */
16639
+ splitTimeSec(timeSec) {
16640
+ return {
16641
+ hours: Math.floor(timeSec / 3600),
16642
+ minutes: Math.floor((timeSec % 3600) / 60),
16643
+ seconds: timeSec % 60
16644
+ };
16645
+ }
16646
+ /**
16647
+ * Writes a time (seconds) and date (Magic numeric) into their respective output fields.
16648
+ */
16649
+ async writeDateTimeToFields(timeFld, dateTimeSec, dateFld, dateVal) {
16650
+ const tmpVal = new ExpVal();
16651
+ tmpVal.MgNumVal = new NUM_TYPE();
16652
+ tmpVal.Attr = StorageAttribute.TIME;
16653
+ tmpVal.MgNumVal.NUM_4_LONG(dateTimeSec);
16654
+ await timeFld.setValueAndStartRecompute(tmpVal.ToMgVal(), false, true, false, false);
16655
+ await timeFld.updateDisplay();
16656
+ tmpVal.Attr = StorageAttribute.DATE;
16657
+ tmpVal.MgNumVal.NUM_4_LONG(dateVal);
16658
+ await dateFld.setValueAndStartRecompute(tmpVal.ToMgVal(), false, true, false, false);
16659
+ await dateFld.updateDisplay();
16660
+ }
16661
+ /**
16662
+ * Converts local date/time to UTC and writes the result into output fields.
16663
+ *
16664
+ * Edge cases handled:
16665
+ * - Century years — setFullYear() avoids JS year < 100 bug
16666
+ * - Leap years — round-trip validation catches silent date rollovers
16667
+ * - DST gaps — Intl.DateTimeFormat resolves correctly per IANA tz rules
16668
+ * - DST ambiguity — Intl uses first occurrence consistently
16669
+ * - Cross-midnight — JS Date handles automatically
16670
+ * - Out-of-range — time < 0 or > 86399 explicitly rejected
16671
+ *
16672
+ * @param resVal - Output: Boolean result (true = success)
16673
+ * @param localTimeVal - Input: Local time as total seconds since midnight (Magic numeric)
16674
+ * @param localDateVal - Input: Local date as Magic numeric
16675
+ * @param utcTimeVar - Input: Field index for the UTC time output field
16676
+ * @param utcDateVar - Input: Field index for the UTC date output field
16677
+ */
16678
+ async eval_op_localToUTC(resVal, localTimeVal, localDateVal, utcTimeVar, utcDateVar) {
16679
+ resVal.Attr = StorageAttribute.BOOLEAN;
16680
+ resVal.BoolVal = false;
16681
+ // Guard: all four numeric values must be present
16682
+ if (localTimeVal.MgNumVal === null || localDateVal.MgNumVal === null ||
16683
+ utcTimeVar.MgNumVal === null || utcDateVar.MgNumVal === null) {
16684
+ return;
16685
+ }
16686
+ // Fetch output fields
16687
+ const utcDateFld = this.GetFieldOfContextTask(utcDateVar.MgNumVal.NUM_2_LONG());
16688
+ const utcTimeFld = this.GetFieldOfContextTask(utcTimeVar.MgNumVal.NUM_2_LONG());
16689
+ if (utcTimeFld === null || utcDateFld === null)
16690
+ return;
16691
+ // Break the input date into parts
16692
+ const dateParts = new DateBreakParams();
16693
+ DisplayConvertor.Instance.date_break_datemode(dateParts, localDateVal.MgNumVal.NUM_2_LONG(), true, this.ExpTask.getCompIdx());
16694
+ // Reject zero/uninitialized Magic date parts
16695
+ if (dateParts.year === 0 || dateParts.month === 0 || dateParts.day === 0)
16696
+ return;
16697
+ // Guard: reject out-of-range time value
16698
+ const localTimeSec = localTimeVal.MgNumVal.NUM_2_LONG();
16699
+ if (!this.isValidTimeSec(localTimeSec))
16700
+ return;
16701
+ const { hours, minutes, seconds } = this.splitTimeSec(localTimeSec);
16702
+ // Use setFullYear to avoid JS bug where new Date(year < 100, ...) maps to 1900+year
16703
+ const localDateTime = new Date(0);
16704
+ localDateTime.setFullYear(dateParts.year, dateParts.month - 1, dateParts.day);
16705
+ localDateTime.setHours(hours, minutes, seconds, 0);
16706
+ // Guard: round-trip check catches silent rollovers (e.g. Feb 29 on non-leap year)
16707
+ if (localDateTime.getFullYear() !== dateParts.year ||
16708
+ localDateTime.getMonth() !== dateParts.month - 1 ||
16709
+ localDateTime.getDate() !== dateParts.day) {
16710
+ return;
16711
+ }
16712
+ if (isNaN(localDateTime.getTime()))
16713
+ return;
16714
+ // Extract UTC parts via Intl — DST, leap year, century year all handled automatically
16715
+ const utcParts = this.convertDateTimeParts(localDateTime, 'UTC');
16716
+ if (utcParts === null)
16717
+ return;
16718
+ const utcTime = utcParts.hour * 3600 + utcParts.minute * 60 + utcParts.second;
16719
+ const utcDate = DisplayConvertor.Instance.date_4_calender(utcParts.year, utcParts.month, utcParts.day, 1, false // month already 1-based
16720
+ );
16721
+ await this.writeDateTimeToFields(utcTimeFld, utcTime, utcDateFld, utcDate);
16722
+ resVal.BoolVal = true;
16723
+ }
16724
+ /**
16725
+ * Converts UTC date/time to local and writes the result into output fields.
16726
+ *
16727
+ * Edge cases handled:
16728
+ * - Century years — Date.UTC() does not have the year < 100 bug
16729
+ * - Leap years — round-trip validation catches silent date rollovers
16730
+ * - DST gaps — Intl.DateTimeFormat resolves to correct local time automatically
16731
+ * - DST ambiguity — Intl uses first occurrence consistently
16732
+ * - Cross-midnight — JS Date handles automatically
16733
+ * - Out-of-range — time < 0 or > 86399 explicitly rejected
16734
+ *
16735
+ * @param resVal - Output: Boolean result (true = success)
16736
+ * @param utcTimeVal - Input: UTC time as total seconds since midnight (Magic numeric)
16737
+ * @param utcDateVal - Input: UTC date as Magic numeric
16738
+ * @param localTimeVar - Input: Field index for the local time output field
16739
+ * @param localDateVar - Input: Field index for the local date output field
16740
+ */
16741
+ async eval_op_utcToLocal(resVal, utcTimeVal, utcDateVal, localTimeVar, localDateVar) {
16742
+ resVal.Attr = StorageAttribute.BOOLEAN;
16743
+ resVal.BoolVal = false;
16744
+ // Guard: all four numeric values must be present
16745
+ if (utcTimeVal.MgNumVal === null || utcDateVal.MgNumVal === null ||
16746
+ localTimeVar.MgNumVal === null || localDateVar.MgNumVal === null) {
16747
+ return;
16748
+ }
16749
+ // Fetch output fields
16750
+ const localDateFld = this.GetFieldOfContextTask(localDateVar.MgNumVal.NUM_2_LONG());
16751
+ const localTimeFld = this.GetFieldOfContextTask(localTimeVar.MgNumVal.NUM_2_LONG());
16752
+ if (localTimeFld === null || localDateFld === null)
16753
+ return;
16754
+ // Break the input date into parts
16755
+ const dateParts = new DateBreakParams();
16756
+ DisplayConvertor.Instance.date_break_datemode(dateParts, utcDateVal.MgNumVal.NUM_2_LONG(), true, this.ExpTask.getCompIdx());
16757
+ // Reject zero/uninitialized Magic date parts
16758
+ if (dateParts.year === 0 || dateParts.month === 0 || dateParts.day === 0)
16759
+ return;
16760
+ // Guard: reject out-of-range time value
16761
+ const utcTimeSec = utcTimeVal.MgNumVal.NUM_2_LONG();
16762
+ if (!this.isValidTimeSec(utcTimeSec))
16763
+ return;
16764
+ const { hours, minutes, seconds } = this.splitTimeSec(utcTimeSec);
16765
+ // Why Date.UTC() instead of new Date(year, month, day, ...)?
16766
+ // JS has a bug: new Date(99, 0, 1) does not give year 99 — it gives 1999.
16767
+ // Date.UTC() always uses the exact year provided — safe for century years like 1900, 2000, 2100
16768
+ const utcDateTime = new Date(Date.UTC(dateParts.year, dateParts.month - 1, dateParts.day, hours, minutes, seconds, 0));
16769
+ // Guard: round-trip check catches silent rollovers (e.g. Feb 29 on non-leap year)
16770
+ if (utcDateTime.getUTCFullYear() !== dateParts.year ||
16771
+ utcDateTime.getUTCMonth() !== dateParts.month - 1 ||
16772
+ utcDateTime.getUTCDate() !== dateParts.day) {
16773
+ return;
16774
+ }
16775
+ if (isNaN(utcDateTime.getTime()))
16776
+ return;
16777
+ // Extract local datetime parts via Intl — no timeZone arg = browser's local timezone
16778
+ // DST, leap year, century year all handled automatically
16779
+ const localParts = this.convertDateTimeParts(utcDateTime);
16780
+ if (localParts === null)
16781
+ return;
16782
+ const localTime = localParts.hour * 3600 + localParts.minute * 60 + localParts.second;
16783
+ const localDate = DisplayConvertor.Instance.date_4_calender(localParts.year, localParts.month, localParts.day, 1, false // month already 1-based
16784
+ );
16785
+ await this.writeDateTimeToFields(localTimeFld, localTime, localDateFld, localDate);
16786
+ resVal.BoolVal = true;
16787
+ }
16559
16788
  eval_op_ndow(resVal, val1, displayConvertor) {
16560
16789
  if (val1.MgNumVal === null) {
16561
16790
  super.SetNULL(resVal, StorageAttribute.NUMERIC);
@@ -39877,7 +40106,7 @@ class CommandsTable {
39877
40106
  }
39878
40107
  }
39879
40108
 
39880
- let CurrentClientVersion = '4.1300.0-dev4130.183';
40109
+ let CurrentClientVersion = '4.1300.0-dev4130.185';
39881
40110
 
39882
40111
  // @dynamic
39883
40112
  class ClientManager {