@acorex/platform 21.0.0-next.88 → 21.0.0-next.90
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.
- package/fesm2022/acorex-platform-auth.mjs +8 -2
- package/fesm2022/acorex-platform-auth.mjs.map +1 -1
- package/fesm2022/{acorex-platform-common-common-settings.provider-VfmZct9H.mjs → acorex-platform-common-common-settings.provider-DilV4dql.mjs} +15 -42
- package/fesm2022/acorex-platform-common-common-settings.provider-DilV4dql.mjs.map +1 -0
- package/fesm2022/acorex-platform-common.mjs +3299 -3200
- package/fesm2022/acorex-platform-common.mjs.map +1 -1
- package/fesm2022/acorex-platform-contracts.mjs +1 -0
- package/fesm2022/acorex-platform-contracts.mjs.map +1 -1
- package/fesm2022/acorex-platform-core.mjs +643 -30
- package/fesm2022/acorex-platform-core.mjs.map +1 -1
- package/fesm2022/acorex-platform-layout-builder.mjs +98 -189
- package/fesm2022/acorex-platform-layout-builder.mjs.map +1 -1
- package/fesm2022/{acorex-platform-layout-components-binding-expression-editor-popup.component-CXEdvDTf.mjs → acorex-platform-layout-components-binding-expression-editor-popup.component-RttFwaw9.mjs} +17 -4
- package/fesm2022/acorex-platform-layout-components-binding-expression-editor-popup.component-RttFwaw9.mjs.map +1 -0
- package/fesm2022/acorex-platform-layout-components.mjs +125 -26
- package/fesm2022/acorex-platform-layout-components.mjs.map +1 -1
- package/fesm2022/acorex-platform-layout-designer.mjs +4 -4
- package/fesm2022/acorex-platform-layout-designer.mjs.map +1 -1
- package/fesm2022/acorex-platform-layout-entity-contracts.mjs +106 -12
- package/fesm2022/acorex-platform-layout-entity-contracts.mjs.map +1 -1
- package/fesm2022/acorex-platform-layout-entity.mjs +512 -155
- package/fesm2022/acorex-platform-layout-entity.mjs.map +1 -1
- package/fesm2022/acorex-platform-layout-views.mjs +921 -173
- package/fesm2022/acorex-platform-layout-views.mjs.map +1 -1
- package/fesm2022/acorex-platform-layout-widget-core.mjs +380 -103
- package/fesm2022/acorex-platform-layout-widget-core.mjs.map +1 -1
- package/fesm2022/{acorex-platform-layout-widgets-tabular-data-edit-popup.component-D4MOHEpI.mjs → acorex-platform-layout-widgets-tabular-data-edit-popup.component-BvFykMTH.mjs} +2 -2
- package/fesm2022/{acorex-platform-layout-widgets-tabular-data-edit-popup.component-D4MOHEpI.mjs.map → acorex-platform-layout-widgets-tabular-data-edit-popup.component-BvFykMTH.mjs.map} +1 -1
- package/fesm2022/{acorex-platform-layout-widgets-tabular-data-view-popup.component-gX-3Kx9I.mjs → acorex-platform-layout-widgets-tabular-data-view-popup.component-CntopmDS.mjs} +2 -2
- package/fesm2022/{acorex-platform-layout-widgets-tabular-data-view-popup.component-gX-3Kx9I.mjs.map → acorex-platform-layout-widgets-tabular-data-view-popup.component-CntopmDS.mjs.map} +1 -1
- package/fesm2022/acorex-platform-layout-widgets.mjs +575 -239
- package/fesm2022/acorex-platform-layout-widgets.mjs.map +1 -1
- package/fesm2022/acorex-platform-themes-default.mjs +232 -25
- package/fesm2022/acorex-platform-themes-default.mjs.map +1 -1
- package/fesm2022/{acorex-platform-themes-shared-settings.provider-BUwPqwUS.mjs → acorex-platform-themes-shared-settings.provider-UyKdkeyk.mjs} +48 -1
- package/fesm2022/acorex-platform-themes-shared-settings.provider-UyKdkeyk.mjs.map +1 -0
- package/fesm2022/acorex-platform-themes-shared.mjs +103 -24
- package/fesm2022/acorex-platform-themes-shared.mjs.map +1 -1
- package/package.json +1 -1
- package/types/acorex-platform-auth.d.ts +1 -0
- package/types/acorex-platform-common.d.ts +202 -151
- package/types/acorex-platform-contracts.d.ts +1 -0
- package/types/acorex-platform-core.d.ts +162 -7
- package/types/acorex-platform-layout-builder.d.ts +10 -20
- package/types/acorex-platform-layout-components.d.ts +14 -1
- package/types/acorex-platform-layout-entity-contracts.d.ts +31 -2
- package/types/acorex-platform-layout-entity.d.ts +91 -16
- package/types/acorex-platform-layout-views.d.ts +107 -20
- package/types/acorex-platform-layout-widget-core.d.ts +71 -9
- package/types/acorex-platform-layout-widgets.d.ts +48 -26
- package/types/acorex-platform-themes-default.d.ts +60 -6
- package/types/acorex-platform-themes-shared.d.ts +4 -0
- package/fesm2022/acorex-platform-common-common-settings.provider-VfmZct9H.mjs.map +0 -1
- package/fesm2022/acorex-platform-layout-components-binding-expression-editor-popup.component-CXEdvDTf.mjs.map +0 -1
- package/fesm2022/acorex-platform-themes-shared-settings.provider-BUwPqwUS.mjs.map +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, inject, Injectable, Directive, computed, Injector, ChangeDetectionStrategy, Component, input, ElementRef, ViewContainerRef, signal, effect, runInInjectionContext, Optional, Inject, NgModule, EventEmitter, HostListener, Output, provideAppInitializer, Pipe } from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, Injectable, Directive, computed, Injector, ChangeDetectionStrategy, Component, input, ElementRef, ViewContainerRef, signal, effect, runInInjectionContext, Optional, Inject, NgModule, EventEmitter, HostListener, Output, DestroyRef, untracked, provideAppInitializer, Pipe } from '@angular/core';
|
|
3
3
|
import { AXTranslationService, resolveMultiLanguageString } from '@acorex/core/translation';
|
|
4
4
|
import { AXDataSource } from '@acorex/cdk/common';
|
|
5
5
|
import { AXPPlatformCoreDataSourceKeys, AXPSystemActionType } from '@acorex/platform/contracts';
|
|
6
|
-
import { isPlainObject, get, set, isNil, isEmpty, isArray, merge, isObjectLike, transform, isEqual, differenceWith, union, cloneDeep, has, sortBy, isUndefined, endsWith, startsWith, includes, lte, gte, lt, gt, orderBy } from 'lodash-es';
|
|
6
|
+
import { isPlainObject, get, set, isNil, isEmpty, isArray, merge, isObjectLike, transform, isEqual, differenceWith, union, cloneDeep, has, unset, sortBy, isUndefined, endsWith, startsWith, includes, lte, gte, lt, gt, orderBy } from 'lodash-es';
|
|
7
7
|
import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';
|
|
8
8
|
import { AXFormatService } from '@acorex/core/format';
|
|
9
9
|
import * as i1 from '@acorex/components/skeleton';
|
|
@@ -12,6 +12,7 @@ import { Subject, interval, fromEvent } from 'rxjs';
|
|
|
12
12
|
import { toSignal } from '@angular/core/rxjs-interop';
|
|
13
13
|
import { DOCUMENT } from '@angular/common';
|
|
14
14
|
import { AXLocaleService } from '@acorex/core/locale';
|
|
15
|
+
import { delay } from '@acorex/core/utils';
|
|
15
16
|
import { AXCalendarService } from '@acorex/core/date-time';
|
|
16
17
|
import { startWith, map, debounceTime } from 'rxjs/operators';
|
|
17
18
|
|
|
@@ -706,6 +707,104 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
706
707
|
type: Directive
|
|
707
708
|
}] });
|
|
708
709
|
|
|
710
|
+
//#region ---- Imports ----
|
|
711
|
+
//#endregion
|
|
712
|
+
//#region ---- Debug Service ----
|
|
713
|
+
/**
|
|
714
|
+
* Platform debug and trace logging.
|
|
715
|
+
* Call `trace` / `traceGroup` for diagnostics (gated by tenant Debug Mode).
|
|
716
|
+
* Call `warn` / `error` for always-on console output.
|
|
717
|
+
*
|
|
718
|
+
* Debug Mode is applied via `setDebugMode()` (wired from settings in platform common).
|
|
719
|
+
*/
|
|
720
|
+
class AXPDebugService {
|
|
721
|
+
//#endregion
|
|
722
|
+
//#region ---- Lifecycle ----
|
|
723
|
+
constructor() {
|
|
724
|
+
//#endregion
|
|
725
|
+
//#region ---- State ----
|
|
726
|
+
this.debugModeEnabled = false;
|
|
727
|
+
AXPDebugService.instance = this;
|
|
728
|
+
}
|
|
729
|
+
//#endregion
|
|
730
|
+
//#region ---- Debug Mode ----
|
|
731
|
+
isDebugMode() {
|
|
732
|
+
return this.debugModeEnabled;
|
|
733
|
+
}
|
|
734
|
+
setDebugMode(enabled) {
|
|
735
|
+
this.debugModeEnabled = enabled;
|
|
736
|
+
}
|
|
737
|
+
//#endregion
|
|
738
|
+
//#region ---- Trace API ----
|
|
739
|
+
/**
|
|
740
|
+
* @param namespace Module/feature namespace (kebab-case). Filter console with `[AXP:<namespace>]`.
|
|
741
|
+
* @param title Short area name (component, service, middleware).
|
|
742
|
+
* @param message What happened.
|
|
743
|
+
* @param data Optional structured payload, or a factory evaluated only when Debug Mode is on.
|
|
744
|
+
*/
|
|
745
|
+
trace(namespace, title, message, data) {
|
|
746
|
+
if (!this.debugModeEnabled) {
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
this.log('log', namespace, title, message, data);
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Same as {@link trace} but emits a collapsible console group (useful for multi-field payloads).
|
|
753
|
+
*/
|
|
754
|
+
traceGroup(namespace, title, message, data) {
|
|
755
|
+
if (!this.debugModeEnabled) {
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
const resolved = this.resolveData(data);
|
|
759
|
+
console.groupCollapsed(this.formatPrefix(namespace, title, message));
|
|
760
|
+
if (resolved != null) {
|
|
761
|
+
for (const [key, value] of Object.entries(resolved)) {
|
|
762
|
+
if (value !== undefined) {
|
|
763
|
+
console.log(key, value);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
console.groupEnd();
|
|
768
|
+
}
|
|
769
|
+
warn(namespace, title, message, data) {
|
|
770
|
+
this.log('warn', namespace, title, message, data);
|
|
771
|
+
}
|
|
772
|
+
error(namespace, title, message, data) {
|
|
773
|
+
this.log('error', namespace, title, message, data);
|
|
774
|
+
}
|
|
775
|
+
//#endregion
|
|
776
|
+
//#region ---- Utility Methods ----
|
|
777
|
+
formatPrefix(namespace, title, message) {
|
|
778
|
+
return `[AXP:${namespace}] ${title} — ${message}`;
|
|
779
|
+
}
|
|
780
|
+
resolveData(data) {
|
|
781
|
+
if (data == null) {
|
|
782
|
+
return undefined;
|
|
783
|
+
}
|
|
784
|
+
return typeof data === 'function' ? data() : data;
|
|
785
|
+
}
|
|
786
|
+
log(level, namespace, title, message, data) {
|
|
787
|
+
const prefix = this.formatPrefix(namespace, title, message);
|
|
788
|
+
const resolved = this.resolveData(data);
|
|
789
|
+
if (resolved != null) {
|
|
790
|
+
console[level](prefix, resolved);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
console[level](prefix);
|
|
794
|
+
}
|
|
795
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPDebugService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
796
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPDebugService, providedIn: 'root' }); }
|
|
797
|
+
}
|
|
798
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPDebugService, decorators: [{
|
|
799
|
+
type: Injectable,
|
|
800
|
+
args: [{
|
|
801
|
+
providedIn: 'root',
|
|
802
|
+
}]
|
|
803
|
+
}], ctorParameters: () => [] });
|
|
804
|
+
|
|
805
|
+
/** Trace namespace for form dirty-state diagnostics (lookup/select revert, discard, baseline). */
|
|
806
|
+
const FORM_DIRTY_TRACE_NS = 'FORM STATE';
|
|
807
|
+
|
|
709
808
|
//#region ---- Locale map shape ----
|
|
710
809
|
/** Per-locale string map (`{ 'en-US': '...', 'fa-IR': '...' }`). */
|
|
711
810
|
function isLocaleStringMap(value) {
|
|
@@ -1028,6 +1127,13 @@ function normalizeLocaleStringMap(value) {
|
|
|
1028
1127
|
}
|
|
1029
1128
|
return Object.keys(normalized).length === 0 ? null : normalized;
|
|
1030
1129
|
}
|
|
1130
|
+
function compareMultiLanguageLocaleStrings(a, b) {
|
|
1131
|
+
const htmlEqual = compareHtmlFormValues(a, b);
|
|
1132
|
+
if (htmlEqual !== null) {
|
|
1133
|
+
return htmlEqual;
|
|
1134
|
+
}
|
|
1135
|
+
return a === b;
|
|
1136
|
+
}
|
|
1031
1137
|
/**
|
|
1032
1138
|
* Compares multi-language field values (plain string vs locale map) for dirty/baseline checks.
|
|
1033
1139
|
* Returns `null` when either side is not a multi-language shape (caller uses generic normalization).
|
|
@@ -1058,15 +1164,13 @@ function compareMultiLanguageFormValues(a, b) {
|
|
|
1058
1164
|
return false;
|
|
1059
1165
|
}
|
|
1060
1166
|
if (typeof left === 'string' && typeof right === 'string') {
|
|
1061
|
-
return left
|
|
1167
|
+
return compareMultiLanguageLocaleStrings(left, right);
|
|
1062
1168
|
}
|
|
1063
1169
|
if (typeof left === 'string' && typeof right === 'object') {
|
|
1064
|
-
|
|
1065
|
-
return values.length > 0 && values.every((text) => text === left);
|
|
1170
|
+
return Object.values(right).some((text) => typeof text === 'string' && compareMultiLanguageLocaleStrings(left, text));
|
|
1066
1171
|
}
|
|
1067
1172
|
if (typeof right === 'string' && typeof left === 'object') {
|
|
1068
|
-
|
|
1069
|
-
return values.length > 0 && values.every((text) => text === right);
|
|
1173
|
+
return Object.values(left).some((text) => typeof text === 'string' && compareMultiLanguageLocaleStrings(right, text));
|
|
1070
1174
|
}
|
|
1071
1175
|
if (typeof left !== 'object' || typeof right !== 'object') {
|
|
1072
1176
|
return false;
|
|
@@ -1078,7 +1182,56 @@ function compareMultiLanguageFormValues(a, b) {
|
|
|
1078
1182
|
if (leftKeys.length !== rightKeys.length) {
|
|
1079
1183
|
return false;
|
|
1080
1184
|
}
|
|
1081
|
-
return leftKeys.every((key) => leftMap[key]
|
|
1185
|
+
return leftKeys.every((key) => compareMultiLanguageLocaleStrings(leftMap[key], rightMap[key]));
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Lookup/select expose may write a resolved MLS string while the baseline keeps the locale map.
|
|
1189
|
+
* Restore baseline shape when the string matches any locale entry.
|
|
1190
|
+
*/
|
|
1191
|
+
function shouldRestoreSavedMultiLanguageShape(value, savedAtPath) {
|
|
1192
|
+
if (typeof value !== 'string' || !isLocaleStringMap(savedAtPath)) {
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
1195
|
+
const trimmed = value.trim();
|
|
1196
|
+
if (trimmed === '') {
|
|
1197
|
+
return false;
|
|
1198
|
+
}
|
|
1199
|
+
return Object.values(savedAtPath).some((entry) => typeof entry === 'string' && entry.trim() === trimmed);
|
|
1200
|
+
}
|
|
1201
|
+
/** Decode common HTML entities for rich-text baseline compare. */
|
|
1202
|
+
function decodeHtmlEntities(value) {
|
|
1203
|
+
return value
|
|
1204
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
|
|
1205
|
+
.replace(/&#(\d+);/g, (_, dec) => String.fromCharCode(parseInt(dec, 10)))
|
|
1206
|
+
.replace(/&/g, '&')
|
|
1207
|
+
.replace(/</g, '<')
|
|
1208
|
+
.replace(/>/g, '>')
|
|
1209
|
+
.replace(/"/g, '"');
|
|
1210
|
+
}
|
|
1211
|
+
function looksLikeHtmlFormValue(value) {
|
|
1212
|
+
return /<[a-z][\s\S]*>/i.test(value.trim());
|
|
1213
|
+
}
|
|
1214
|
+
/** Normalizes rich-text HTML for semantic dirty/baseline compare (e.g. Quill `p`/`br` drift). */
|
|
1215
|
+
function normalizeRichTextHtmlForCompare(value) {
|
|
1216
|
+
const html = decodeHtmlEntities(value.trim());
|
|
1217
|
+
if (typeof document === 'undefined') {
|
|
1218
|
+
return html.replace(/\s+/g, ' ').trim();
|
|
1219
|
+
}
|
|
1220
|
+
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
1221
|
+
return (doc.body.textContent ?? '').replace(/\s+/g, ' ').trim();
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Compares rich-text HTML strings by visible text (ignores `p` vs `br` structure drift).
|
|
1225
|
+
* Returns `null` when either side is not HTML markup.
|
|
1226
|
+
*/
|
|
1227
|
+
function compareHtmlFormValues(a, b) {
|
|
1228
|
+
if (typeof a !== 'string' || typeof b !== 'string') {
|
|
1229
|
+
return null;
|
|
1230
|
+
}
|
|
1231
|
+
if (!looksLikeHtmlFormValue(a) || !looksLikeHtmlFormValue(b)) {
|
|
1232
|
+
return null;
|
|
1233
|
+
}
|
|
1234
|
+
return normalizeRichTextHtmlForCompare(a) === normalizeRichTextHtmlForCompare(b);
|
|
1082
1235
|
}
|
|
1083
1236
|
/** Lookup/select filter-mode payload: `{ value, displayText?, operation }`. */
|
|
1084
1237
|
function isFilterFormShape(value) {
|
|
@@ -1286,6 +1439,12 @@ function isFormValueEqual(a, b) {
|
|
|
1286
1439
|
}
|
|
1287
1440
|
return true;
|
|
1288
1441
|
}
|
|
1442
|
+
if (typeof a === 'string' && typeof b === 'string') {
|
|
1443
|
+
const htmlEqual = compareHtmlFormValues(a, b);
|
|
1444
|
+
if (htmlEqual !== null) {
|
|
1445
|
+
return htmlEqual;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1289
1448
|
return isEqual(normalizeFormValue(a), normalizeFormValue(b));
|
|
1290
1449
|
}
|
|
1291
1450
|
/**
|
|
@@ -1294,6 +1453,45 @@ function isFormValueEqual(a, b) {
|
|
|
1294
1453
|
function isFormContextDirty(current, baseline) {
|
|
1295
1454
|
return !isFormValueEqual(current, baseline);
|
|
1296
1455
|
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Lists paths where live context differs from baseline (for dirty-state trace logs).
|
|
1458
|
+
*/
|
|
1459
|
+
function summarizeFormContextDirtyDiff(current, baseline, maxEntries = 12) {
|
|
1460
|
+
const diffs = [];
|
|
1461
|
+
const walk = (path, currentValue, baselineValue) => {
|
|
1462
|
+
if (diffs.length >= maxEntries) {
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
if (isFormValueEqual(currentValue, baselineValue)) {
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
const bothPlainObjects = isPlainObject(currentValue) &&
|
|
1469
|
+
isPlainObject(baselineValue) &&
|
|
1470
|
+
!isArray(currentValue) &&
|
|
1471
|
+
!isArray(baselineValue);
|
|
1472
|
+
if (bothPlainObjects) {
|
|
1473
|
+
const left = currentValue;
|
|
1474
|
+
const right = baselineValue;
|
|
1475
|
+
const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
1476
|
+
for (const key of keys) {
|
|
1477
|
+
if (diffs.length >= maxEntries) {
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
const nextPath = path ? `${path}.${key}` : key;
|
|
1481
|
+
walk(nextPath, left[key], right[key]);
|
|
1482
|
+
}
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
diffs.push({
|
|
1486
|
+
path: path || '(root)',
|
|
1487
|
+
current: currentValue,
|
|
1488
|
+
baseline: baselineValue,
|
|
1489
|
+
selectionEqual: isSelectionValueEqual(currentValue, baselineValue),
|
|
1490
|
+
});
|
|
1491
|
+
};
|
|
1492
|
+
walk('', current, baseline);
|
|
1493
|
+
return diffs;
|
|
1494
|
+
}
|
|
1297
1495
|
/**
|
|
1298
1496
|
* Clones a context object for use as a dirty-tracking baseline.
|
|
1299
1497
|
*/
|
|
@@ -1312,20 +1510,26 @@ const AXPContextStore = signalStore(withState(() => ({
|
|
|
1312
1510
|
/** Last committed / saved baseline — discard reverts to this snapshot. */
|
|
1313
1511
|
savedSnapshot: {},
|
|
1314
1512
|
previousSnapshot: {},
|
|
1513
|
+
/** True after a user-origin update since the last {@link commitSaved}. */
|
|
1514
|
+
userEditedSinceCommit: false,
|
|
1315
1515
|
lastChange: {
|
|
1316
1516
|
state: 'initiated',
|
|
1317
1517
|
},
|
|
1318
|
-
})), withComputed(({ data, state, lastChange, savedSnapshot, previousSnapshot }) => ({
|
|
1518
|
+
})), withComputed(({ data, state, lastChange, savedSnapshot, previousSnapshot, userEditedSinceCommit }) => ({
|
|
1319
1519
|
isChanged: computed(() => state() === 'changed'),
|
|
1320
1520
|
isReset: computed(() => state() === 'restored'),
|
|
1321
1521
|
isInitiated: computed(() => state() === 'initiated'),
|
|
1322
1522
|
isEmpty: computed(() => Object.keys(data()).length === 0),
|
|
1323
1523
|
isSavedCommitted: computed(() => !isEmpty(savedSnapshot())),
|
|
1524
|
+
hasUserEditedSinceCommit: computed(() => userEditedSinceCommit()),
|
|
1324
1525
|
isDirty: computed(() => {
|
|
1325
1526
|
const saved = savedSnapshot();
|
|
1326
1527
|
if (isEmpty(saved)) {
|
|
1327
1528
|
return false;
|
|
1328
1529
|
}
|
|
1530
|
+
if (!userEditedSinceCommit()) {
|
|
1531
|
+
return false;
|
|
1532
|
+
}
|
|
1329
1533
|
return isFormContextDirty(data(), saved);
|
|
1330
1534
|
}),
|
|
1331
1535
|
snapshot: computed(() => cloneDeep(data())),
|
|
@@ -1335,24 +1539,80 @@ const AXPContextStore = signalStore(withState(() => ({
|
|
|
1335
1539
|
previous: computed(() => cloneDeep(previousSnapshot())),
|
|
1336
1540
|
changeEvent: computed(() => lastChange()),
|
|
1337
1541
|
})), withMethods((store) => {
|
|
1542
|
+
const traceDirty = (title, message, data) => {
|
|
1543
|
+
AXPDebugService.instance?.trace(FORM_DIRTY_TRACE_NS, title, message, data);
|
|
1544
|
+
};
|
|
1545
|
+
const traceDirtyState = (reason, data, path) => {
|
|
1546
|
+
const saved = store.savedSnapshot();
|
|
1547
|
+
const current = store.data();
|
|
1548
|
+
const hasSaved = !isEmpty(saved);
|
|
1549
|
+
const contextDirty = hasSaved && isFormContextDirty(current, saved);
|
|
1550
|
+
traceDirty('context-store', reason, () => ({
|
|
1551
|
+
path,
|
|
1552
|
+
userEditedSinceCommit: store.userEditedSinceCommit(),
|
|
1553
|
+
isSavedCommitted: hasSaved,
|
|
1554
|
+
isFormContextDirty: contextDirty,
|
|
1555
|
+
isDirty: store.isDirty(),
|
|
1556
|
+
...(hasSaved
|
|
1557
|
+
? {
|
|
1558
|
+
dirtyPaths: contextDirty ? summarizeFormContextDirtyDiff(current, saved) : [],
|
|
1559
|
+
}
|
|
1560
|
+
: {}),
|
|
1561
|
+
...data,
|
|
1562
|
+
}));
|
|
1563
|
+
};
|
|
1338
1564
|
const updateValue = (path, value, options) => {
|
|
1339
1565
|
const currentData = cloneDeep(store.data());
|
|
1340
1566
|
const oldValue = getSmart(currentData, path);
|
|
1341
1567
|
const hasSaved = !isEmpty(store.savedSnapshot());
|
|
1342
|
-
const
|
|
1568
|
+
const saved = store.savedSnapshot();
|
|
1569
|
+
const savedAtPath = hasSaved ? getSmart(saved, path) : undefined;
|
|
1570
|
+
const origin = options?.origin ?? 'system';
|
|
1343
1571
|
if (hasSaved && isSelectionValueEqual(value, savedAtPath) && !isEqual(value, savedAtPath)) {
|
|
1344
1572
|
value = cloneDeep(savedAtPath);
|
|
1345
1573
|
}
|
|
1574
|
+
if (hasSaved && !isEqual(value, savedAtPath) && isFormValueEqual(value, savedAtPath)) {
|
|
1575
|
+
value = cloneDeep(savedAtPath);
|
|
1576
|
+
}
|
|
1346
1577
|
const isArrayLengthChange = Array.isArray(value) && (!Array.isArray(oldValue) || value.length !== oldValue.length);
|
|
1347
|
-
|
|
1578
|
+
/** Same selection id as baseline but different object shape (lookup/select item vs scalar). */
|
|
1579
|
+
const needsSavedShapeRestore = hasSaved && isSelectionValueEqual(value, savedAtPath) && !isFormValueEqual(value, savedAtPath);
|
|
1580
|
+
if (!isArrayLengthChange && isEqual(oldValue, value) && !needsSavedShapeRestore) {
|
|
1581
|
+
traceDirty('context-store', 'updateValue skipped (lodash isEqual)', () => ({
|
|
1582
|
+
path,
|
|
1583
|
+
origin,
|
|
1584
|
+
oldValue,
|
|
1585
|
+
value,
|
|
1586
|
+
needsSavedShapeRestore,
|
|
1587
|
+
selectionEqualToSaved: hasSaved ? isSelectionValueEqual(value, savedAtPath) : null,
|
|
1588
|
+
savedAtPath,
|
|
1589
|
+
}));
|
|
1348
1590
|
return;
|
|
1349
1591
|
}
|
|
1350
1592
|
const shouldNormalizeSavedShape = hasSaved && isSelectionValueEqual(value, savedAtPath) && !isEqual(oldValue, value);
|
|
1351
|
-
if (!isArrayLengthChange &&
|
|
1593
|
+
if (!isArrayLengthChange &&
|
|
1594
|
+
isFormValueEqual(oldValue, value) &&
|
|
1595
|
+
!shouldNormalizeSavedShape &&
|
|
1596
|
+
!needsSavedShapeRestore) {
|
|
1597
|
+
traceDirty('context-store', 'updateValue skipped (isFormValueEqual)', () => ({
|
|
1598
|
+
path,
|
|
1599
|
+
origin,
|
|
1600
|
+
oldValue,
|
|
1601
|
+
value,
|
|
1602
|
+
needsSavedShapeRestore,
|
|
1603
|
+
shouldNormalizeSavedShape,
|
|
1604
|
+
selectionEqualToSaved: hasSaved ? isSelectionValueEqual(value, savedAtPath) : null,
|
|
1605
|
+
savedAtPath,
|
|
1606
|
+
}));
|
|
1352
1607
|
return;
|
|
1353
1608
|
}
|
|
1354
|
-
const origin = options?.origin ?? 'system';
|
|
1355
1609
|
const updatedData = setSmart(currentData, path, value);
|
|
1610
|
+
const isStillDirty = hasSaved && isFormContextDirty(updatedData, saved);
|
|
1611
|
+
const nextUserEdited = isStillDirty
|
|
1612
|
+
? origin === 'user'
|
|
1613
|
+
? true
|
|
1614
|
+
: store.userEditedSinceCommit()
|
|
1615
|
+
: false;
|
|
1356
1616
|
const changeEvent = {
|
|
1357
1617
|
oldValue,
|
|
1358
1618
|
newValue: value,
|
|
@@ -1366,7 +1626,22 @@ const AXPContextStore = signalStore(withState(() => ({
|
|
|
1366
1626
|
data: updatedData,
|
|
1367
1627
|
state: 'changed',
|
|
1368
1628
|
lastChange: changeEvent,
|
|
1629
|
+
userEditedSinceCommit: nextUserEdited,
|
|
1369
1630
|
});
|
|
1631
|
+
traceDirty('context-store', 'updateValue applied', () => ({
|
|
1632
|
+
path,
|
|
1633
|
+
origin,
|
|
1634
|
+
oldValue,
|
|
1635
|
+
newValue: value,
|
|
1636
|
+
savedAtPath,
|
|
1637
|
+
selectionEqualToSaved: hasSaved ? isSelectionValueEqual(value, savedAtPath) : null,
|
|
1638
|
+
formValueEqualToSaved: hasSaved ? isFormValueEqual(value, savedAtPath) : null,
|
|
1639
|
+
needsSavedShapeRestore,
|
|
1640
|
+
isStillDirty,
|
|
1641
|
+
nextUserEdited,
|
|
1642
|
+
isDirty: !isEmpty(saved) && nextUserEdited && isStillDirty,
|
|
1643
|
+
dirtyPaths: isStillDirty ? summarizeFormContextDirtyDiff(updatedData, saved) : [],
|
|
1644
|
+
}));
|
|
1370
1645
|
};
|
|
1371
1646
|
const applyObjectPaths = (obj, options, prefix = '') => {
|
|
1372
1647
|
for (const [key, value] of Object.entries(obj ?? {})) {
|
|
@@ -1379,6 +1654,57 @@ const AXPContextStore = signalStore(withState(() => ({
|
|
|
1379
1654
|
}
|
|
1380
1655
|
}
|
|
1381
1656
|
};
|
|
1657
|
+
const removePathFromContext = (path, origin = 'system') => {
|
|
1658
|
+
const currentData = cloneDeep(store.data());
|
|
1659
|
+
if (!has(currentData, path)) {
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
unset(currentData, path);
|
|
1663
|
+
const parts = path.split('.');
|
|
1664
|
+
for (let i = parts.length - 1; i > 0; i--) {
|
|
1665
|
+
const parentPath = parts.slice(0, i).join('.');
|
|
1666
|
+
const parentObj = getSmart(currentData, parentPath);
|
|
1667
|
+
if (isPlainObject(parentObj) && isEmpty(Object.keys(parentObj))) {
|
|
1668
|
+
unset(currentData, parentPath);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
const saved = store.savedSnapshot();
|
|
1672
|
+
const isStillDirty = !isEmpty(saved) && isFormContextDirty(currentData, saved);
|
|
1673
|
+
patchState(store, {
|
|
1674
|
+
previousSnapshot: store.snapshot(),
|
|
1675
|
+
data: currentData,
|
|
1676
|
+
state: 'changed',
|
|
1677
|
+
lastChange: {
|
|
1678
|
+
path,
|
|
1679
|
+
state: 'changed',
|
|
1680
|
+
data: currentData,
|
|
1681
|
+
origin,
|
|
1682
|
+
},
|
|
1683
|
+
userEditedSinceCommit: isStillDirty
|
|
1684
|
+
? origin === 'user'
|
|
1685
|
+
? true
|
|
1686
|
+
: store.userEditedSinceCommit()
|
|
1687
|
+
: false,
|
|
1688
|
+
});
|
|
1689
|
+
traceDirty('context-store', 'removePathFromContext', () => ({
|
|
1690
|
+
path,
|
|
1691
|
+
origin,
|
|
1692
|
+
isStillDirty,
|
|
1693
|
+
dirtyPaths: isStillDirty ? summarizeFormContextDirtyDiff(currentData, saved) : [],
|
|
1694
|
+
}));
|
|
1695
|
+
};
|
|
1696
|
+
const syncPathToSavedBaseline = (path, options) => {
|
|
1697
|
+
const saved = store.savedSnapshot();
|
|
1698
|
+
if (isEmpty(saved)) {
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
const origin = options?.origin ?? 'system';
|
|
1702
|
+
if (has(saved, path)) {
|
|
1703
|
+
updateValue(path, cloneDeep(getSmart(saved, path)), { origin });
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
removePathFromContext(path, origin);
|
|
1707
|
+
};
|
|
1382
1708
|
const revertToSaved = () => {
|
|
1383
1709
|
const savedData = store.saved();
|
|
1384
1710
|
const changeEvent = {
|
|
@@ -1393,13 +1719,17 @@ const AXPContextStore = signalStore(withState(() => ({
|
|
|
1393
1719
|
data: savedData,
|
|
1394
1720
|
state: 'restored',
|
|
1395
1721
|
lastChange: changeEvent,
|
|
1722
|
+
userEditedSinceCommit: false,
|
|
1396
1723
|
});
|
|
1724
|
+
traceDirtyState('revertToSaved');
|
|
1397
1725
|
};
|
|
1398
1726
|
return {
|
|
1399
1727
|
update(path, value, options) {
|
|
1400
1728
|
updateValue(path, value, options);
|
|
1401
1729
|
},
|
|
1402
1730
|
applyObjectPaths,
|
|
1731
|
+
/** Aligns a context path with the saved baseline (restore value or remove if absent). */
|
|
1732
|
+
syncPathToSavedBaseline,
|
|
1403
1733
|
patch(context, options) {
|
|
1404
1734
|
const currentData = cloneDeep(store.data());
|
|
1405
1735
|
const updatedData = { ...currentData, ...context };
|
|
@@ -1440,6 +1770,7 @@ const AXPContextStore = signalStore(withState(() => ({
|
|
|
1440
1770
|
data: initialData,
|
|
1441
1771
|
state: 'initiated',
|
|
1442
1772
|
lastChange: changeEvent,
|
|
1773
|
+
userEditedSinceCommit: false,
|
|
1443
1774
|
});
|
|
1444
1775
|
},
|
|
1445
1776
|
getValue(path) {
|
|
@@ -1458,11 +1789,13 @@ const AXPContextStore = signalStore(withState(() => ({
|
|
|
1458
1789
|
savedSnapshot: snapshot,
|
|
1459
1790
|
previousSnapshot: snapshot,
|
|
1460
1791
|
state: 'initiated',
|
|
1792
|
+
userEditedSinceCommit: false,
|
|
1461
1793
|
lastChange: {
|
|
1462
1794
|
state: 'initiated',
|
|
1463
1795
|
data: snapshot,
|
|
1464
1796
|
},
|
|
1465
1797
|
});
|
|
1798
|
+
traceDirtyState('commitSaved');
|
|
1466
1799
|
},
|
|
1467
1800
|
/** Merges parent-bound entity fields without resetting the saved baseline. */
|
|
1468
1801
|
applyParentBind(merged) {
|
|
@@ -2826,12 +3159,12 @@ const HORIZONTAL_DIRECTIONAL_KEY_MIRRORS = {
|
|
|
2826
3159
|
};
|
|
2827
3160
|
/** Maps a horizontal direction key to its RTL semantic counterpart. */
|
|
2828
3161
|
function mirrorHorizontalDirectionalKey(key) {
|
|
2829
|
-
const normalized = normalizeShortcutToken(key);
|
|
3162
|
+
const normalized = normalizeShortcutToken$1(key);
|
|
2830
3163
|
return HORIZONTAL_DIRECTIONAL_KEY_MIRRORS[normalized] ?? normalized;
|
|
2831
3164
|
}
|
|
2832
3165
|
/** True when the chord's primary key is a horizontal direction key (`←`/`→`, `[`/`]`). */
|
|
2833
3166
|
function isHorizontalDirectionalShortcutKey(key) {
|
|
2834
|
-
return normalizeShortcutToken(key) in HORIZONTAL_DIRECTIONAL_KEY_MIRRORS;
|
|
3167
|
+
return normalizeShortcutToken$1(key) in HORIZONTAL_DIRECTIONAL_KEY_MIRRORS;
|
|
2835
3168
|
}
|
|
2836
3169
|
/**
|
|
2837
3170
|
* Resolves direction behavior for a chord.
|
|
@@ -2847,7 +3180,7 @@ function resolveEffectiveDirectionBehavior(chord, explicit) {
|
|
|
2847
3180
|
}
|
|
2848
3181
|
/** Resolves the key the user presses for a semantically registered chord in RTL. */
|
|
2849
3182
|
function resolveSemanticDirectionalKey(storedKey, rtl) {
|
|
2850
|
-
const normalized = normalizeShortcutToken(storedKey);
|
|
3183
|
+
const normalized = normalizeShortcutToken$1(storedKey);
|
|
2851
3184
|
if (!rtl) {
|
|
2852
3185
|
return normalized;
|
|
2853
3186
|
}
|
|
@@ -2914,7 +3247,7 @@ function parseKeyboardShortcutChord(chord) {
|
|
|
2914
3247
|
.split('+')
|
|
2915
3248
|
.map((part) => part.trim())
|
|
2916
3249
|
.filter(Boolean);
|
|
2917
|
-
const key = normalizeShortcutToken(parts.pop() ?? '');
|
|
3250
|
+
const key = normalizeShortcutToken$1(parts.pop() ?? '');
|
|
2918
3251
|
const parsed = {
|
|
2919
3252
|
ctrl: false,
|
|
2920
3253
|
shift: false,
|
|
@@ -2942,7 +3275,7 @@ function matchesKeyboardShortcutChord(event, chord, options) {
|
|
|
2942
3275
|
return false;
|
|
2943
3276
|
}
|
|
2944
3277
|
const actualKey = normalizeShortcutEventKey(event);
|
|
2945
|
-
const expectedKey = normalizeShortcutToken(parsed.key);
|
|
3278
|
+
const expectedKey = normalizeShortcutToken$1(parsed.key);
|
|
2946
3279
|
const rtlSemantic = directionBehavior === 'semantic' &&
|
|
2947
3280
|
options?.isRtl === true &&
|
|
2948
3281
|
isHorizontalDirectionalShortcutKey(expectedKey);
|
|
@@ -3006,7 +3339,7 @@ function formatKeyboardShortcutChord(chord, options) {
|
|
|
3006
3339
|
if (parts.length === 0) {
|
|
3007
3340
|
return '';
|
|
3008
3341
|
}
|
|
3009
|
-
const keyPart = normalizeShortcutToken(parts.pop() ?? '');
|
|
3342
|
+
const keyPart = normalizeShortcutToken$1(parts.pop() ?? '');
|
|
3010
3343
|
const modifierParts = parts.map((part) => formatShortcutPart(part.toLowerCase(), mac));
|
|
3011
3344
|
const keyDisplay = formatShortcutPart(keyPart, mac);
|
|
3012
3345
|
if (mac) {
|
|
@@ -3100,7 +3433,7 @@ function isMacPlatform() {
|
|
|
3100
3433
|
}
|
|
3101
3434
|
return /Mac|iPhone|iPad|iPod/i.test(navigator.userAgent);
|
|
3102
3435
|
}
|
|
3103
|
-
function normalizeShortcutToken(key) {
|
|
3436
|
+
function normalizeShortcutToken$1(key) {
|
|
3104
3437
|
const normalized = key.trim().toLowerCase();
|
|
3105
3438
|
if (normalized === 'esc') {
|
|
3106
3439
|
return 'escape';
|
|
@@ -3162,7 +3495,7 @@ function normalizeShortcutEventKey(event) {
|
|
|
3162
3495
|
return event.key.toLowerCase();
|
|
3163
3496
|
}
|
|
3164
3497
|
function formatShortcutPart(part, mac) {
|
|
3165
|
-
const normalized = normalizeShortcutToken(part);
|
|
3498
|
+
const normalized = normalizeShortcutToken$1(part);
|
|
3166
3499
|
if (mac && KEY_DISPLAY_MAC[normalized]) {
|
|
3167
3500
|
return KEY_DISPLAY_MAC[normalized];
|
|
3168
3501
|
}
|
|
@@ -3439,6 +3772,271 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
3439
3772
|
}]
|
|
3440
3773
|
}] });
|
|
3441
3774
|
|
|
3775
|
+
//#region ---- Dialog Action Shortcut Utilities ----
|
|
3776
|
+
const DEFAULT_CANCEL_DIALOG_ACTION_SHORTCUTS = ['Esc'];
|
|
3777
|
+
const DEFAULT_SUBMIT_DIALOG_ACTION_SHORTCUTS = ['Enter', 'ctrl+s'];
|
|
3778
|
+
const PRIMARY_DIALOG_ACTION_COMMANDS = new Set([
|
|
3779
|
+
'submit',
|
|
3780
|
+
'create',
|
|
3781
|
+
'apply',
|
|
3782
|
+
'save',
|
|
3783
|
+
'select',
|
|
3784
|
+
'entity-form-done',
|
|
3785
|
+
'upload-image',
|
|
3786
|
+
]);
|
|
3787
|
+
/**
|
|
3788
|
+
* Parses a shortcut string such as `Enter`, `Escape`, or `ctrl+shift+s`.
|
|
3789
|
+
*/
|
|
3790
|
+
function parseDialogActionShortcut(shortcut) {
|
|
3791
|
+
const parts = shortcut
|
|
3792
|
+
.trim()
|
|
3793
|
+
.toLowerCase()
|
|
3794
|
+
.split('+')
|
|
3795
|
+
.map((part) => part.trim())
|
|
3796
|
+
.filter(Boolean);
|
|
3797
|
+
const key = normalizeShortcutToken(parts.pop() ?? '');
|
|
3798
|
+
return {
|
|
3799
|
+
ctrl: parts.includes('ctrl') || parts.includes('control'),
|
|
3800
|
+
shift: parts.includes('shift'),
|
|
3801
|
+
alt: parts.includes('alt') || parts.includes('option'),
|
|
3802
|
+
meta: parts.includes('meta') || parts.includes('cmd') || parts.includes('command'),
|
|
3803
|
+
key,
|
|
3804
|
+
};
|
|
3805
|
+
}
|
|
3806
|
+
/**
|
|
3807
|
+
* Returns true when the keyboard event matches the given shortcut definition.
|
|
3808
|
+
*/
|
|
3809
|
+
function matchesDialogActionShortcut(event, shortcut) {
|
|
3810
|
+
const parsed = parseDialogActionShortcut(shortcut);
|
|
3811
|
+
if (parsed.ctrl !== event.ctrlKey) {
|
|
3812
|
+
return false;
|
|
3813
|
+
}
|
|
3814
|
+
if (parsed.shift !== event.shiftKey) {
|
|
3815
|
+
return false;
|
|
3816
|
+
}
|
|
3817
|
+
if (parsed.alt !== event.altKey) {
|
|
3818
|
+
return false;
|
|
3819
|
+
}
|
|
3820
|
+
if (parsed.meta !== event.metaKey) {
|
|
3821
|
+
return false;
|
|
3822
|
+
}
|
|
3823
|
+
return normalizeShortcutKey(event) === normalizeShortcutToken(parsed.key);
|
|
3824
|
+
}
|
|
3825
|
+
/**
|
|
3826
|
+
* Resolves footer action shortcuts: defaults when omitted, merge when extras are provided, none when `[]`.
|
|
3827
|
+
*/
|
|
3828
|
+
function resolveDialogActionShortcuts(defaults, overrides) {
|
|
3829
|
+
if (overrides !== undefined) {
|
|
3830
|
+
if (overrides.length === 0) {
|
|
3831
|
+
return undefined;
|
|
3832
|
+
}
|
|
3833
|
+
return flattenDialogActionShortcutChords([...defaults, ...overrides]);
|
|
3834
|
+
}
|
|
3835
|
+
return flattenDialogActionShortcutChords(defaults);
|
|
3836
|
+
}
|
|
3837
|
+
/**
|
|
3838
|
+
* Applies built-in footer shortcuts when actions are declared without `shortcuts`
|
|
3839
|
+
* (e.g. workflow `show-layout-popup` raw action config).
|
|
3840
|
+
*/
|
|
3841
|
+
function resolveConfiguredFooterActionShortcuts(command, explicit) {
|
|
3842
|
+
if (explicit !== undefined) {
|
|
3843
|
+
return resolveDialogActionShortcuts([], explicit);
|
|
3844
|
+
}
|
|
3845
|
+
if (command === 'cancel') {
|
|
3846
|
+
return resolveDialogActionShortcuts(DEFAULT_CANCEL_DIALOG_ACTION_SHORTCUTS, undefined);
|
|
3847
|
+
}
|
|
3848
|
+
if (command && PRIMARY_DIALOG_ACTION_COMMANDS.has(command)) {
|
|
3849
|
+
return resolveDialogActionShortcuts(DEFAULT_SUBMIT_DIALOG_ACTION_SHORTCUTS, undefined);
|
|
3850
|
+
}
|
|
3851
|
+
return undefined;
|
|
3852
|
+
}
|
|
3853
|
+
function flattenDialogActionShortcutChords(shortcuts) {
|
|
3854
|
+
const chords = normalizeKeyboardShortcuts(shortcuts).flatMap((shortcut) => shortcut.keys);
|
|
3855
|
+
const deduped = dedupeDialogActionShortcuts(chords);
|
|
3856
|
+
return deduped.length ? deduped : undefined;
|
|
3857
|
+
}
|
|
3858
|
+
function dedupeDialogActionShortcuts(shortcuts) {
|
|
3859
|
+
const seen = new Set();
|
|
3860
|
+
const result = [];
|
|
3861
|
+
for (const shortcut of shortcuts) {
|
|
3862
|
+
const token = normalizeShortcutToken(shortcut);
|
|
3863
|
+
if (!token || seen.has(token)) {
|
|
3864
|
+
continue;
|
|
3865
|
+
}
|
|
3866
|
+
seen.add(token);
|
|
3867
|
+
result.push(shortcut.trim());
|
|
3868
|
+
}
|
|
3869
|
+
return result;
|
|
3870
|
+
}
|
|
3871
|
+
function normalizeShortcutToken(key) {
|
|
3872
|
+
const normalized = key.trim().toLowerCase();
|
|
3873
|
+
if (normalized === 'esc') {
|
|
3874
|
+
return 'escape';
|
|
3875
|
+
}
|
|
3876
|
+
return normalized;
|
|
3877
|
+
}
|
|
3878
|
+
/**
|
|
3879
|
+
* Whether a dialog footer shortcut may fire while focus is inside an editable field.
|
|
3880
|
+
*/
|
|
3881
|
+
function isDialogShortcutAllowedInEditableField(shortcut) {
|
|
3882
|
+
const parsed = parseDialogActionShortcut(shortcut);
|
|
3883
|
+
const isPlainEnter = parsed.key === 'enter' && !parsed.ctrl && !parsed.shift && !parsed.alt && !parsed.meta;
|
|
3884
|
+
return !isPlainEnter;
|
|
3885
|
+
}
|
|
3886
|
+
/**
|
|
3887
|
+
* Normalizes dialog footer chord strings for {@link AXPKeyboardShortcutRegistry} (`Esc` → `escape`).
|
|
3888
|
+
*/
|
|
3889
|
+
function normalizeDialogActionShortcutChord(shortcut) {
|
|
3890
|
+
const parts = shortcut
|
|
3891
|
+
.trim()
|
|
3892
|
+
.split('+')
|
|
3893
|
+
.map((part) => part.trim())
|
|
3894
|
+
.filter(Boolean);
|
|
3895
|
+
if (parts.length === 0) {
|
|
3896
|
+
return shortcut.trim();
|
|
3897
|
+
}
|
|
3898
|
+
const key = normalizeShortcutToken(parts.pop() ?? '');
|
|
3899
|
+
const modifiers = parts.map((part) => part.toLowerCase());
|
|
3900
|
+
return modifiers.length ? `${modifiers.join('+')}+${key}` : key;
|
|
3901
|
+
}
|
|
3902
|
+
/**
|
|
3903
|
+
* Whether a shortcut should fire given the current focus target.
|
|
3904
|
+
* Enter without modifiers is suppressed in multiline / rich-text fields.
|
|
3905
|
+
*/
|
|
3906
|
+
function shouldTriggerDialogActionShortcut(event, shortcut) {
|
|
3907
|
+
const parsed = parseDialogActionShortcut(shortcut);
|
|
3908
|
+
const isPlainEnter = parsed.key === 'enter' && !parsed.ctrl && !parsed.shift && !parsed.alt && !parsed.meta;
|
|
3909
|
+
if (!isPlainEnter) {
|
|
3910
|
+
return true;
|
|
3911
|
+
}
|
|
3912
|
+
return !isKeyboardTargetMultilineEditable(event.target);
|
|
3913
|
+
}
|
|
3914
|
+
function normalizeShortcutKey(event) {
|
|
3915
|
+
if (event.key === 'Enter') {
|
|
3916
|
+
return 'enter';
|
|
3917
|
+
}
|
|
3918
|
+
if (event.key === 'Escape') {
|
|
3919
|
+
return 'escape';
|
|
3920
|
+
}
|
|
3921
|
+
if (event.key === ' ') {
|
|
3922
|
+
return 'space';
|
|
3923
|
+
}
|
|
3924
|
+
if (event.key.length === 1) {
|
|
3925
|
+
return event.key.toLowerCase();
|
|
3926
|
+
}
|
|
3927
|
+
const codeMatch = event.code.match(/^Key([A-Z])$/);
|
|
3928
|
+
if (codeMatch) {
|
|
3929
|
+
return codeMatch[1].toLowerCase();
|
|
3930
|
+
}
|
|
3931
|
+
const digitMatch = event.code.match(/^Digit([0-9])$/);
|
|
3932
|
+
if (digitMatch) {
|
|
3933
|
+
return digitMatch[1];
|
|
3934
|
+
}
|
|
3935
|
+
return event.key.toLowerCase();
|
|
3936
|
+
}
|
|
3937
|
+
function isKeyboardTargetMultilineEditable(target) {
|
|
3938
|
+
if (!(target instanceof HTMLElement)) {
|
|
3939
|
+
return false;
|
|
3940
|
+
}
|
|
3941
|
+
if (target.isContentEditable) {
|
|
3942
|
+
return true;
|
|
3943
|
+
}
|
|
3944
|
+
const textarea = target.closest('textarea');
|
|
3945
|
+
if (textarea && !textarea.readOnly && !textarea.disabled) {
|
|
3946
|
+
return true;
|
|
3947
|
+
}
|
|
3948
|
+
const richText = target.closest('[contenteditable="true"]');
|
|
3949
|
+
return richText instanceof HTMLElement;
|
|
3950
|
+
}
|
|
3951
|
+
//#endregion
|
|
3952
|
+
|
|
3953
|
+
//#region ---- Popup Footer Keyboard Shortcuts ----
|
|
3954
|
+
/**
|
|
3955
|
+
* Registers popup footer shortcuts at modal priority so they win over page bindings and stop bubbling.
|
|
3956
|
+
*/
|
|
3957
|
+
function registerPopupFooterKeyboardShortcuts(options) {
|
|
3958
|
+
const group = '@general:keyboard-shortcuts.groups.dialog';
|
|
3959
|
+
const scope = options.scope ?? group;
|
|
3960
|
+
const shortcuts = [];
|
|
3961
|
+
for (const action of options.actions) {
|
|
3962
|
+
if (action.hidden?.()) {
|
|
3963
|
+
continue;
|
|
3964
|
+
}
|
|
3965
|
+
const chords = action.shortcuts !== undefined
|
|
3966
|
+
? resolveConfiguredFooterActionShortcuts(action.command, action.shortcuts)
|
|
3967
|
+
: resolveConfiguredFooterActionShortcuts(action.command, undefined);
|
|
3968
|
+
if (!chords?.length) {
|
|
3969
|
+
continue;
|
|
3970
|
+
}
|
|
3971
|
+
for (const shortcut of chords) {
|
|
3972
|
+
shortcuts.push({
|
|
3973
|
+
keys: [normalizeDialogActionShortcutChord(shortcut)],
|
|
3974
|
+
title: action.title,
|
|
3975
|
+
group,
|
|
3976
|
+
allowInEditableFields: isDialogShortcutAllowedInEditableField(shortcut),
|
|
3977
|
+
when: () => {
|
|
3978
|
+
if (options.when && !options.when()) {
|
|
3979
|
+
return false;
|
|
3980
|
+
}
|
|
3981
|
+
if (action.hidden?.()) {
|
|
3982
|
+
return false;
|
|
3983
|
+
}
|
|
3984
|
+
if (action.disabled?.()) {
|
|
3985
|
+
return false;
|
|
3986
|
+
}
|
|
3987
|
+
return true;
|
|
3988
|
+
},
|
|
3989
|
+
handler: () => {
|
|
3990
|
+
void action.handler();
|
|
3991
|
+
},
|
|
3992
|
+
});
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
options.registry.register({
|
|
3996
|
+
owner: options.owner,
|
|
3997
|
+
elementRef: options.elementRef,
|
|
3998
|
+
id: options.id,
|
|
3999
|
+
priority: AXPKeyboardShortcutPriority.Modal,
|
|
4000
|
+
scope,
|
|
4001
|
+
when: options.when,
|
|
4002
|
+
shortcuts,
|
|
4003
|
+
});
|
|
4004
|
+
}
|
|
4005
|
+
/**
|
|
4006
|
+
* Binds footer shortcuts for AXPopupService-hosted components. Call from the popup constructor.
|
|
4007
|
+
*/
|
|
4008
|
+
function setupPopupFooterKeyboardShortcuts(getActions, config) {
|
|
4009
|
+
const destroyRef = inject(DestroyRef);
|
|
4010
|
+
const elementRef = inject(ElementRef);
|
|
4011
|
+
const registry = inject(AXPKeyboardShortcutRegistry);
|
|
4012
|
+
const id = config?.id ?? 'popup-footer';
|
|
4013
|
+
effect(() => {
|
|
4014
|
+
getActions();
|
|
4015
|
+
config?.when?.();
|
|
4016
|
+
untracked(() => {
|
|
4017
|
+
registerPopupFooterKeyboardShortcuts({
|
|
4018
|
+
registry,
|
|
4019
|
+
owner: destroyRef,
|
|
4020
|
+
elementRef,
|
|
4021
|
+
id,
|
|
4022
|
+
scope: config?.scope,
|
|
4023
|
+
when: config?.when,
|
|
4024
|
+
actions: getActions(),
|
|
4025
|
+
});
|
|
4026
|
+
});
|
|
4027
|
+
});
|
|
4028
|
+
}
|
|
4029
|
+
//#endregion
|
|
4030
|
+
|
|
4031
|
+
/** Global overlay flag + task count ({@link AXPLayoutThemeService}). */
|
|
4032
|
+
const OVERLAY_LAYOUT_TRACE_NS = 'OVERLAY-LAYOUT';
|
|
4033
|
+
/** Root shell loading dialog sync ({@link AXPRootLayoutComponent}). */
|
|
4034
|
+
const OVERLAY_ROOT_LAYOUT_TRACE_NS = 'OVERLAY-ROOT-LAYOUT';
|
|
4035
|
+
/** Details view page load and overlay release orchestration. */
|
|
4036
|
+
const OVERLAY_DETAILS_VIEW_TRACE_NS = 'OVERLAY-DETAILS-VIEW';
|
|
4037
|
+
/** Widget tree mount / settle polling ({@link AXPWidgetContainerComponent}). */
|
|
4038
|
+
const OVERLAY_WIDGET_SETTLE_TRACE_NS = 'OVERLAY-WIDGET-SETTLE';
|
|
4039
|
+
|
|
3442
4040
|
//#region ---- Imports ----
|
|
3443
4041
|
//#endregion
|
|
3444
4042
|
|
|
@@ -3932,7 +4530,7 @@ class AXPIdleSchedulerService {
|
|
|
3932
4530
|
const timeoutMs = options?.timeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
3933
4531
|
const minIdleMs = options?.minIdleMs ?? DEFAULT_MIN_IDLE_MS;
|
|
3934
4532
|
if (this.pausedState() || typeof document !== 'undefined' && document.visibilityState === 'hidden') {
|
|
3935
|
-
return
|
|
4533
|
+
return delay(timeoutMs);
|
|
3936
4534
|
}
|
|
3937
4535
|
return new Promise((resolve) => {
|
|
3938
4536
|
let settled = false;
|
|
@@ -3992,9 +4590,6 @@ class AXPIdleSchedulerService {
|
|
|
3992
4590
|
}
|
|
3993
4591
|
}
|
|
3994
4592
|
}
|
|
3995
|
-
delay(ms) {
|
|
3996
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3997
|
-
}
|
|
3998
4593
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPIdleSchedulerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
3999
4594
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPIdleSchedulerService, providedIn: 'root' }); }
|
|
4000
4595
|
}
|
|
@@ -5109,6 +5704,24 @@ function toFilterSearchText(value) {
|
|
|
5109
5704
|
}
|
|
5110
5705
|
return undefined;
|
|
5111
5706
|
}
|
|
5707
|
+
function toSortComparableValue(value, locale) {
|
|
5708
|
+
if (value == null) {
|
|
5709
|
+
return '';
|
|
5710
|
+
}
|
|
5711
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
5712
|
+
return value;
|
|
5713
|
+
}
|
|
5714
|
+
if (typeof value === 'string') {
|
|
5715
|
+
return value.toLocaleLowerCase(locale);
|
|
5716
|
+
}
|
|
5717
|
+
if (value instanceof Date) {
|
|
5718
|
+
return value.getTime();
|
|
5719
|
+
}
|
|
5720
|
+
if (isLocaleStringMap(value)) {
|
|
5721
|
+
return resolveMultiLanguageString(value, locale).toLocaleLowerCase(locale);
|
|
5722
|
+
}
|
|
5723
|
+
return String(value).toLocaleLowerCase(locale);
|
|
5724
|
+
}
|
|
5112
5725
|
function filterValuesAreEqual(itemRaw, condRaw) {
|
|
5113
5726
|
if (typeof condRaw === 'string' && isLocaleStringMap(itemRaw)) {
|
|
5114
5727
|
return Object.values(itemRaw).some((v) => v.toLowerCase() === condRaw.toLowerCase());
|
|
@@ -5327,11 +5940,11 @@ function applyFilterArray(dataArray, filters, logic = 'and') {
|
|
|
5327
5940
|
return dataArray;
|
|
5328
5941
|
}
|
|
5329
5942
|
}
|
|
5330
|
-
function applySortArray(dataArray, sorts) {
|
|
5943
|
+
function applySortArray(dataArray, sorts, locale = 'en-US') {
|
|
5331
5944
|
if (sorts && sorts.length > 0) {
|
|
5332
|
-
const
|
|
5333
|
-
const sortOrders = sorts.map((s) => s.dir);
|
|
5334
|
-
return orderBy(dataArray,
|
|
5945
|
+
const iteratees = sorts.map((s) => (item) => toSortComparableValue(get(item, s.field), locale));
|
|
5946
|
+
const sortOrders = sorts.map((s) => (s.dir === 'desc' ? 'desc' : 'asc'));
|
|
5947
|
+
return orderBy(dataArray, iteratees, sortOrders);
|
|
5335
5948
|
}
|
|
5336
5949
|
else
|
|
5337
5950
|
return dataArray;
|
|
@@ -5821,5 +6434,5 @@ function generateKebabCase(title) {
|
|
|
5821
6434
|
* Generated bundle index. Do not edit.
|
|
5822
6435
|
*/
|
|
5823
6436
|
|
|
5824
|
-
export { AXHighlightService, AXPActivityLogProvider, AXPActivityLogService, AXPAppStartUpProvider, AXPAppStartUpService, AXPBroadcastEventService, AXPCatalogScopeDefinitionProviderService, AXPCatalogScopeDefinitionsDataSourceDefinition, AXPColorPaletteProvider, AXPColorPaletteService, AXPColumnWidthService, AXPComponentLogoConfig, AXPComponentSlot, AXPComponentSlotDirective, AXPComponentSlotModule, AXPComponentSlotRegistryService, AXPContentCheckerDirective, AXPContextChangeEvent, AXPContextDefinitionProviderService, AXPContextEvalFactory, AXPContextStore, AXPCountdownPipe, AXPDataGenerator, AXPDataSourceDefinitionProviderService, AXPDblClickDirective, AXPDefaultColorPalettesProvider, AXPDeviceService, AXPDeviceType, AXPDistributedEventListenerService, AXPElementDataDirective, AXPExpressionEvaluatorScopeProviderContext, AXPExpressionEvaluatorScopeProviderService, AXPExpressionEvaluatorService, AXPFeatureDefinitionProviderContext, AXPGridLayoutDirective, AXPHookService, AXPIconLogoConfig, AXPIdleSchedulerService, AXPImageUrlLogoConfig, AXPKeyboardShortcutPriority, AXPKeyboardShortcutRegistry, AXPModuleManifestModule, AXPModuleManifestRegistry, AXPModuleManifestsDataSourceDefinition, AXPScreenSize, AXPSystemActions, AXPTagProvider, AXPTagService, AXP_ACTIVITY_LOG_PROVIDER, AXP_CATALOG_SCOPE_DEFINITION_PROVIDER, AXP_COLOR_PALETTE_PROVIDER, AXP_COLUMN_WIDTH_PROVIDER, AXP_CONTEXT_DEFINITION_PROVIDER, AXP_DATASOURCE_DEFINITION_PROVIDER, AXP_DISTRIBUTED_EVENT_LISTENER_PROVIDER, AXP_EXPRESSION_EVALUATOR_SCOPE_PROVIDER, AXP_FEATURE_DEFINITION_PROVIDER, AXP_MODULE_MANIFEST_PROVIDER, AXP_SESSION_SERVICE, AXP_TAG_PROVIDER, AX_OVERLAY_CONTAINER_SELECTOR, AX_OVERLAY_PANE_SELECTOR, applyContextEvalPipes, applyFilterArray, applyPagination, applyQueryArray, applySortArray, applySystemActionDefault, buildLocaleTextMapValue, captureFormContextBaseline, chordToKbdItemKeys, cleanDeep, coerceUnknownToBoolean, coerceUnknownToDate, coerceUnknownToFiniteNumber, coerceUnknownToTrimmedString, compareMultiLanguageStrings, containsHtmlMarkup, contextEvalWithPipes, createProviderWithInjectionContext, defaultColumnWidthProvider, extractNestedFieldsWildcard, extractTextFromHtml, extractValue, findOverlayContainerAncestor, formatKeyboardShortcutChord, formatKeyboardShortcutChords, generateKebabCase, getActionButton, getChangedPaths, getDetailedChanges, getEnumValues, getNestedVisibleOverlayPanes, getPrimaryKeyboardShortcutChord, getSmart, getSystemActions, getTopVisibleOverlayContainer, getVisibleAnchoredOverlayPanes, getVisibleOverlayContainers, hasForegroundOverlayLayer, isFormContextDirty, isFormValueEqual, isHorizontalDirectionalShortcutKey, isKeyboardTargetInsideEditableField, isLocaleStringMap, isMacPlatform, isSelectionValueEqual, isVisibleOverlayElement, matchesKeyboardShortcutChord, mirrorHorizontalDirectionalKey, normalizeContextEvalPipes, normalizeExpressionTemplate, normalizeKeyboardShortcut, normalizeKeyboardShortcuts, objectKeyValueTransforms, parseKeyboardShortcutChord, provideLazyProvider, resolveActionLook, resolveDisplayShortcutChord, resolveEffectiveDirectionBehavior, resolveSemanticDirectionalKey, searchInMultiLanguageString, setSmart, shouldDeferEscapeToOpenOverlay, shouldUseLocaleMapShape, sortByMultiLanguageString, unwrapValueProperty };
|
|
6437
|
+
export { AXHighlightService, AXPActivityLogProvider, AXPActivityLogService, AXPAppStartUpProvider, AXPAppStartUpService, AXPBroadcastEventService, AXPCatalogScopeDefinitionProviderService, AXPCatalogScopeDefinitionsDataSourceDefinition, AXPColorPaletteProvider, AXPColorPaletteService, AXPColumnWidthService, AXPComponentLogoConfig, AXPComponentSlot, AXPComponentSlotDirective, AXPComponentSlotModule, AXPComponentSlotRegistryService, AXPContentCheckerDirective, AXPContextChangeEvent, AXPContextDefinitionProviderService, AXPContextEvalFactory, AXPContextStore, AXPCountdownPipe, AXPDataGenerator, AXPDataSourceDefinitionProviderService, AXPDblClickDirective, AXPDebugService, AXPDefaultColorPalettesProvider, AXPDeviceService, AXPDeviceType, AXPDistributedEventListenerService, AXPElementDataDirective, AXPExpressionEvaluatorScopeProviderContext, AXPExpressionEvaluatorScopeProviderService, AXPExpressionEvaluatorService, AXPFeatureDefinitionProviderContext, AXPGridLayoutDirective, AXPHookService, AXPIconLogoConfig, AXPIdleSchedulerService, AXPImageUrlLogoConfig, AXPKeyboardShortcutPriority, AXPKeyboardShortcutRegistry, AXPModuleManifestModule, AXPModuleManifestRegistry, AXPModuleManifestsDataSourceDefinition, AXPScreenSize, AXPSystemActions, AXPTagProvider, AXPTagService, AXP_ACTIVITY_LOG_PROVIDER, AXP_CATALOG_SCOPE_DEFINITION_PROVIDER, AXP_COLOR_PALETTE_PROVIDER, AXP_COLUMN_WIDTH_PROVIDER, AXP_CONTEXT_DEFINITION_PROVIDER, AXP_DATASOURCE_DEFINITION_PROVIDER, AXP_DISTRIBUTED_EVENT_LISTENER_PROVIDER, AXP_EXPRESSION_EVALUATOR_SCOPE_PROVIDER, AXP_FEATURE_DEFINITION_PROVIDER, AXP_MODULE_MANIFEST_PROVIDER, AXP_SESSION_SERVICE, AXP_TAG_PROVIDER, AX_OVERLAY_CONTAINER_SELECTOR, AX_OVERLAY_PANE_SELECTOR, DEFAULT_CANCEL_DIALOG_ACTION_SHORTCUTS, DEFAULT_SUBMIT_DIALOG_ACTION_SHORTCUTS, FORM_DIRTY_TRACE_NS, OVERLAY_DETAILS_VIEW_TRACE_NS, OVERLAY_LAYOUT_TRACE_NS, OVERLAY_ROOT_LAYOUT_TRACE_NS, OVERLAY_WIDGET_SETTLE_TRACE_NS, applyContextEvalPipes, applyFilterArray, applyPagination, applyQueryArray, applySortArray, applySystemActionDefault, buildLocaleTextMapValue, captureFormContextBaseline, chordToKbdItemKeys, cleanDeep, coerceUnknownToBoolean, coerceUnknownToDate, coerceUnknownToFiniteNumber, coerceUnknownToTrimmedString, compareMultiLanguageStrings, containsHtmlMarkup, contextEvalWithPipes, createProviderWithInjectionContext, defaultColumnWidthProvider, extractNestedFieldsWildcard, extractTextFromHtml, extractValue, findOverlayContainerAncestor, formatKeyboardShortcutChord, formatKeyboardShortcutChords, generateKebabCase, getActionButton, getChangedPaths, getDetailedChanges, getEnumValues, getNestedVisibleOverlayPanes, getPrimaryKeyboardShortcutChord, getSmart, getSystemActions, getTopVisibleOverlayContainer, getVisibleAnchoredOverlayPanes, getVisibleOverlayContainers, hasForegroundOverlayLayer, isDialogShortcutAllowedInEditableField, isFormContextDirty, isFormValueEqual, isHorizontalDirectionalShortcutKey, isKeyboardTargetInsideEditableField, isLocaleStringMap, isMacPlatform, isSelectionValueEqual, isVisibleOverlayElement, matchesDialogActionShortcut, matchesKeyboardShortcutChord, mirrorHorizontalDirectionalKey, normalizeContextEvalPipes, normalizeDialogActionShortcutChord, normalizeExpressionTemplate, normalizeKeyboardShortcut, normalizeKeyboardShortcuts, objectKeyValueTransforms, parseDialogActionShortcut, parseKeyboardShortcutChord, provideLazyProvider, registerPopupFooterKeyboardShortcuts, resolveActionLook, resolveConfiguredFooterActionShortcuts, resolveDialogActionShortcuts, resolveDisplayShortcutChord, resolveEffectiveDirectionBehavior, resolveSemanticDirectionalKey, searchInMultiLanguageString, setSmart, setupPopupFooterKeyboardShortcuts, shouldDeferEscapeToOpenOverlay, shouldRestoreSavedMultiLanguageShape, shouldTriggerDialogActionShortcut, shouldUseLocaleMapShape, sortByMultiLanguageString, summarizeFormContextDirtyDiff, unwrapValueProperty };
|
|
5825
6438
|
//# sourceMappingURL=acorex-platform-core.mjs.map
|