@ti-engine/web-framework 1.20.1 → 1.23.0
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/CHANGELOG.md +92 -0
- package/README.md +52 -0
- package/bin/build/hash-password.js +36 -0
- package/bin/config/local-users.example.json +8 -0
- package/bin/localization/web-server-labels.json +165 -1
- package/bin/static/fragments/components/component-sidebar.html +25 -0
- package/bin/static/fragments/frame-about.html +96 -0
- package/bin/static/fragments/frame-login.html +6 -1
- package/bin/static/fragments/frame-profile.html +103 -3
- package/bin/static/scripts/ti-framework.css +143 -0
- package/bin/static/scripts/ti-framework.js +339 -0
- package/bin/web-app-manager.js +235 -2
- package/bin/web-server.js +18 -1
- package/bin/web-server.json +3 -0
- package/components/application-info.js +190 -0
- package/components/auth-manager.js +159 -18
- package/components/definitions.types.js +65 -0
- package/components/local-user-directory.js +428 -0
- package/components/web-config-env.js +6 -1
- package/components/web-handlers.js +10 -2
- package/package.json +15 -2
- package/types/bin/web-app-manager.d.ts +95 -1
- package/types/bin/web-server.d.ts +19 -1
- package/types/components/application-info.d.ts +53 -0
- package/types/components/auth-manager.d.ts +7 -0
- package/types/components/definitions.types.d.ts +166 -0
- package/types/components/local-user-directory.d.ts +111 -0
- package/types/components/web-config-env.d.ts +1 -1
|
@@ -2042,6 +2042,145 @@ body {
|
|
|
2042
2042
|
color: var(--fg-primary);
|
|
2043
2043
|
}
|
|
2044
2044
|
|
|
2045
|
+
.ti-kv-value.mono {
|
|
2046
|
+
font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Consolas, monospace;
|
|
2047
|
+
font-size: var(--fs-xs);
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
/* Dimmed value — a hint, a placeholder, or a fact that is deliberately secondary. */
|
|
2051
|
+
.ti-kv-value.muted {
|
|
2052
|
+
color: var(--fg-tertiary);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
/* ============================================================
|
|
2056
|
+
Identity header + info sections — the read-only "facts"
|
|
2057
|
+
pattern behind the Profile and About screens. An application
|
|
2058
|
+
supplies the content as a descriptor; this supplies the
|
|
2059
|
+
layout, so every consumer's screens look the same.
|
|
2060
|
+
============================================================ */
|
|
2061
|
+
.ti-identity {
|
|
2062
|
+
display: flex;
|
|
2063
|
+
align-items: center;
|
|
2064
|
+
gap: var(--s-4);
|
|
2065
|
+
padding: var(--s-5);
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
/* Square brand/app mark, for an identity block that stands for an application rather than a person
|
|
2069
|
+
(the circular .ti-avatar is the person case). Mirrors the sidebar brand mark at a larger size. */
|
|
2070
|
+
.ti-identity-mark {
|
|
2071
|
+
width: 60px;
|
|
2072
|
+
height: 60px;
|
|
2073
|
+
display: grid;
|
|
2074
|
+
place-items: center;
|
|
2075
|
+
border-radius: var(--r-lg);
|
|
2076
|
+
background: linear-gradient(135deg, var(--accent), color-mix(in oklab, var(--accent) 70%, #000));
|
|
2077
|
+
color: #FFF;
|
|
2078
|
+
font-weight: 700;
|
|
2079
|
+
font-size: 26px;
|
|
2080
|
+
letter-spacing: -0.02em;
|
|
2081
|
+
text-transform: uppercase;
|
|
2082
|
+
box-shadow: 0 4px 12px color-mix(in oklab, var(--accent) 32%, transparent), inset 0 1px 0 rgba(255, 255, 255, 0.25);
|
|
2083
|
+
flex: 0 0 auto;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
.ti-identity-meta {
|
|
2087
|
+
flex: 1;
|
|
2088
|
+
min-width: 0;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
.ti-identity-name {
|
|
2092
|
+
display: flex;
|
|
2093
|
+
align-items: center;
|
|
2094
|
+
flex-wrap: wrap;
|
|
2095
|
+
gap: var(--s-2);
|
|
2096
|
+
font-size: var(--fs-2xl);
|
|
2097
|
+
font-weight: 600;
|
|
2098
|
+
letter-spacing: -0.02em;
|
|
2099
|
+
line-height: 1.1;
|
|
2100
|
+
color: var(--fg-primary);
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
.ti-identity-sub {
|
|
2104
|
+
display: flex;
|
|
2105
|
+
align-items: center;
|
|
2106
|
+
flex-wrap: wrap;
|
|
2107
|
+
gap: 6px;
|
|
2108
|
+
font-size: var(--fs-sm);
|
|
2109
|
+
color: var(--fg-secondary);
|
|
2110
|
+
margin-top: var(--s-1);
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
.ti-identity-sub-sep {
|
|
2114
|
+
color: var(--fg-tertiary);
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
.ti-identity-caption {
|
|
2118
|
+
font-size: var(--fs-sm);
|
|
2119
|
+
color: var(--fg-tertiary);
|
|
2120
|
+
margin-top: 2px;
|
|
2121
|
+
overflow-wrap: anywhere;
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
/* Right-hand stack of status pills / tags. */
|
|
2125
|
+
.ti-identity-aside {
|
|
2126
|
+
display: flex;
|
|
2127
|
+
flex-direction: column;
|
|
2128
|
+
align-items: flex-end;
|
|
2129
|
+
gap: var(--s-1);
|
|
2130
|
+
flex: 0 0 auto;
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
/* Two-up grid of section panels; a section may claim the full row with .wide. */
|
|
2134
|
+
.ti-info-sections {
|
|
2135
|
+
display: grid;
|
|
2136
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
2137
|
+
gap: var(--s-5);
|
|
2138
|
+
margin-top: var(--s-5);
|
|
2139
|
+
align-items: start;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
.ti-info-section.wide {
|
|
2143
|
+
grid-column: 1 / -1;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
@media (max-width: 900px) {
|
|
2147
|
+
.ti-info-sections {
|
|
2148
|
+
grid-template-columns: 1fr;
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
/* Label/value pairs inside a section, stacked (label above value) rather than the side-by-side
|
|
2153
|
+
rhythm of .ti-kv-grid — a read-only mirror of the .ti-form-grid a matching edit screen uses. */
|
|
2154
|
+
.ti-info-grid {
|
|
2155
|
+
display: grid;
|
|
2156
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
2157
|
+
gap: var(--s-4);
|
|
2158
|
+
padding: var(--s-5);
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
.ti-info-item {
|
|
2162
|
+
display: flex;
|
|
2163
|
+
flex-direction: column;
|
|
2164
|
+
gap: 3px;
|
|
2165
|
+
min-width: 0;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
.ti-info-item.wide {
|
|
2169
|
+
grid-column: 1 / -1;
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
/* An info value that is a destination rather than a fact. Only http(s)/mailto targets ever reach it — the
|
|
2173
|
+
normalizer in ti-framework.js drops anything else before it can become an href. */
|
|
2174
|
+
.ti-info-link {
|
|
2175
|
+
color: var(--accent);
|
|
2176
|
+
text-decoration: none;
|
|
2177
|
+
overflow-wrap: anywhere;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
.ti-info-link:hover {
|
|
2181
|
+
text-decoration: underline;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2045
2184
|
/* ============================================================
|
|
2046
2185
|
Tag / pill label
|
|
2047
2186
|
============================================================ */
|
|
@@ -2410,6 +2549,10 @@ body {
|
|
|
2410
2549
|
border-radius: var(--r-md);
|
|
2411
2550
|
}
|
|
2412
2551
|
|
|
2552
|
+
.ti-login-error.visible {
|
|
2553
|
+
display: block;
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2413
2556
|
.ti-login-form {
|
|
2414
2557
|
display: flex;
|
|
2415
2558
|
flex-direction: column;
|
|
@@ -1301,6 +1301,342 @@ document.addEventListener( "htmx:responseError", ( event ) => {
|
|
|
1301
1301
|
}
|
|
1302
1302
|
} );
|
|
1303
1303
|
|
|
1304
|
+
/* ============================================================================================================
|
|
1305
|
+
Read-only "facts" screens — Profile and About.
|
|
1306
|
+
|
|
1307
|
+
Both render the same descriptor shape: an identity header plus an ordered list of titled sections, each
|
|
1308
|
+
holding label/value items. The server decides the content (see TiWebAppManager#getProfileInfo /
|
|
1309
|
+
#getApplicationInfo); everything below only shapes it for the template, so the fragments stay free of
|
|
1310
|
+
formatting logic — which is also what keeps them within Alpine's CSP expression subset.
|
|
1311
|
+
============================================================================================================ */
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* Rendered in place of an item value the server left empty. Keeping it here rather than in each fragment means a
|
|
1315
|
+
* section never has to be conditionally assembled just to avoid a blank cell.
|
|
1316
|
+
*
|
|
1317
|
+
* @type {string}
|
|
1318
|
+
*/
|
|
1319
|
+
const INFO_VALUE_PLACEHOLDER = "—";
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* URL schemes an info item may link to. Descriptor content is server-authored, but an `href` becomes a live
|
|
1323
|
+
* navigation target, so the set is an allowlist rather than a `javascript:` denylist.
|
|
1324
|
+
*
|
|
1325
|
+
* @type {string[]}
|
|
1326
|
+
*/
|
|
1327
|
+
const INFO_LINK_SCHEMES = [ "http:", "https:", "mailto:" ];
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Returns the href for an info item, or an empty string when it has none or names a scheme that is not in
|
|
1331
|
+
* {@link INFO_LINK_SCHEMES}. A rejected href degrades the item to plain text rather than dropping it.
|
|
1332
|
+
*
|
|
1333
|
+
* @method
|
|
1334
|
+
* @param {Object} item
|
|
1335
|
+
* @returns {string}
|
|
1336
|
+
* @private
|
|
1337
|
+
*/
|
|
1338
|
+
const sanitizeInfoHref = ( item ) => {
|
|
1339
|
+
const href = String( ( item && item.href ) || "" ).trim();
|
|
1340
|
+
if ( !href ) {
|
|
1341
|
+
return "";
|
|
1342
|
+
}
|
|
1343
|
+
try {
|
|
1344
|
+
return INFO_LINK_SCHEMES.includes( new URL( href, window.location.origin ).protocol ) ? href : "";
|
|
1345
|
+
} catch {
|
|
1346
|
+
return "";
|
|
1347
|
+
}
|
|
1348
|
+
};
|
|
1349
|
+
|
|
1350
|
+
/**
|
|
1351
|
+
* Normalizes one label/value item: fills in the placeholder for an absent value, collapses the presentational
|
|
1352
|
+
* flags into a single class string, and vets any link target.
|
|
1353
|
+
*
|
|
1354
|
+
* @method
|
|
1355
|
+
* @param {Object} item
|
|
1356
|
+
* @returns {Object}
|
|
1357
|
+
* @private
|
|
1358
|
+
*/
|
|
1359
|
+
const normalizeInfoItem = ( item ) => {
|
|
1360
|
+
const source = ( item && typeof item === "object" ) ? item : {};
|
|
1361
|
+
const value = String( source.value === undefined || source.value === null ? "" : source.value ).trim();
|
|
1362
|
+
const classes = [];
|
|
1363
|
+
if ( source.mono ) classes.push( "mono" );
|
|
1364
|
+
if ( source.muted || !value ) classes.push( "muted" );
|
|
1365
|
+
|
|
1366
|
+
return {
|
|
1367
|
+
label: String( source.label || "" ),
|
|
1368
|
+
value: value || INFO_VALUE_PLACEHOLDER,
|
|
1369
|
+
valueClass: classes.join( " " ),
|
|
1370
|
+
wide: source.wide === true,
|
|
1371
|
+
href: value ? sanitizeInfoHref( source ) : ""
|
|
1372
|
+
};
|
|
1373
|
+
};
|
|
1374
|
+
|
|
1375
|
+
/**
|
|
1376
|
+
* Normalizes a descriptor's section list so the template can iterate it without guarding for missing keys.
|
|
1377
|
+
* A section carrying no items at all is dropped — an empty panel is noise, not information.
|
|
1378
|
+
*
|
|
1379
|
+
* @method
|
|
1380
|
+
* @param {Array} sections
|
|
1381
|
+
* @returns {Array}
|
|
1382
|
+
* @private
|
|
1383
|
+
*/
|
|
1384
|
+
const normalizeInfoSections = ( sections ) => {
|
|
1385
|
+
return ( Array.isArray( sections ) ? sections : [] )
|
|
1386
|
+
.filter( ( section ) => section && Array.isArray( section.items ) && section.items.length > 0 )
|
|
1387
|
+
.map( ( section ) => ( {
|
|
1388
|
+
title: String( section.title || "" ),
|
|
1389
|
+
description: String( section.description || "" ),
|
|
1390
|
+
icon: String( section.icon || "info-circle" ),
|
|
1391
|
+
wide: section.wide === true,
|
|
1392
|
+
items: section.items.map( normalizeInfoItem )
|
|
1393
|
+
} ) );
|
|
1394
|
+
};
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* Normalizes an identity tag into a ready-to-bind pill.
|
|
1398
|
+
*
|
|
1399
|
+
* @method
|
|
1400
|
+
* @param {Object} tag
|
|
1401
|
+
* @returns {Object}
|
|
1402
|
+
* @private
|
|
1403
|
+
*/
|
|
1404
|
+
const normalizeInfoTag = ( tag ) => {
|
|
1405
|
+
const source = ( tag && typeof tag === "object" ) ? tag : {};
|
|
1406
|
+
const classes = [ source.mono ? "ti-tag mono" : "ti-status-pill" ];
|
|
1407
|
+
if ( source.tone ) classes.push( String( source.tone ) );
|
|
1408
|
+
return {
|
|
1409
|
+
text: String( source.text || "" ),
|
|
1410
|
+
dot: source.dot === true && !source.mono,
|
|
1411
|
+
pillClass: classes.join( " " )
|
|
1412
|
+
};
|
|
1413
|
+
};
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
1416
|
+
* Runs `load` once the application store has finished initializing — the descriptor endpoints are session-scoped,
|
|
1417
|
+
* so requesting them before `/app/config` has resolved would race the session bootstrap.
|
|
1418
|
+
*
|
|
1419
|
+
* @method
|
|
1420
|
+
* @param {Object} component The Alpine component (for `$watch`).
|
|
1421
|
+
* @param {Function} load
|
|
1422
|
+
* @private
|
|
1423
|
+
*/
|
|
1424
|
+
const loadWhenInitialized = ( component, load ) => {
|
|
1425
|
+
const tiApplication = Alpine.store( "tiApplication" );
|
|
1426
|
+
if ( tiApplication.isInitialized ) {
|
|
1427
|
+
load();
|
|
1428
|
+
} else {
|
|
1429
|
+
component.$watch( () => tiApplication.isInitialized, ( isInitialized ) => {
|
|
1430
|
+
if ( isInitialized ) {
|
|
1431
|
+
load();
|
|
1432
|
+
}
|
|
1433
|
+
} );
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1437
|
+
/**
|
|
1438
|
+
* Returns a configuration object for the Profile screen "frame-profile.html".
|
|
1439
|
+
* <br/>
|
|
1440
|
+
* The descriptor comes from `GET /app/profile` (JSON), which the application's web app manager builds — this
|
|
1441
|
+
* component only shapes it for the template and never decides what a profile contains.
|
|
1442
|
+
*
|
|
1443
|
+
* @method
|
|
1444
|
+
* @returns {Object}
|
|
1445
|
+
* @public
|
|
1446
|
+
*/
|
|
1447
|
+
const configureScreenProfile = () => {
|
|
1448
|
+
const tiToolbox = Alpine.store( "tiToolbox" );
|
|
1449
|
+
const tiApplication = Alpine.store( "tiApplication" );
|
|
1450
|
+
|
|
1451
|
+
return {
|
|
1452
|
+
|
|
1453
|
+
profile: null,
|
|
1454
|
+
busy: true,
|
|
1455
|
+
|
|
1456
|
+
init() {
|
|
1457
|
+
loadWhenInitialized( this, () => this.load() );
|
|
1458
|
+
},
|
|
1459
|
+
|
|
1460
|
+
load() {
|
|
1461
|
+
this.busy = true;
|
|
1462
|
+
tiApplication.sendRequest( "/app/profile" ).then( ( result ) => {
|
|
1463
|
+
const data = ( result && result.data && typeof result.data === "object" ) ? result.data : {};
|
|
1464
|
+
const identity = ( data.identity && typeof data.identity === "object" ) ? data.identity : {};
|
|
1465
|
+
this.profile = {
|
|
1466
|
+
identity: {
|
|
1467
|
+
name: String( identity.name || "" ),
|
|
1468
|
+
subtitle: String( identity.subtitle || "" ),
|
|
1469
|
+
caption: String( identity.caption || "" ),
|
|
1470
|
+
avatarSeed: String( identity.avatarSeed || identity.name || "" ),
|
|
1471
|
+
badge: identity.badge ? { text: String( identity.badge.text || "" ), tone: String( identity.badge.tone || "" ) } : null,
|
|
1472
|
+
tags: ( Array.isArray( identity.tags ) ? identity.tags : [] ).map( normalizeInfoTag )
|
|
1473
|
+
},
|
|
1474
|
+
sections: normalizeInfoSections( data.sections )
|
|
1475
|
+
};
|
|
1476
|
+
this.busy = false;
|
|
1477
|
+
} ).catch( ( error ) => {
|
|
1478
|
+
this.busy = false;
|
|
1479
|
+
if ( error && ( error.name === "AbortError" || error.isAborted ) ) return;
|
|
1480
|
+
tiApplication.notify( tiApplication.formatException( error ) );
|
|
1481
|
+
} );
|
|
1482
|
+
},
|
|
1483
|
+
|
|
1484
|
+
avatarStyle() {
|
|
1485
|
+
const identity = this.profile ? this.profile.identity : {};
|
|
1486
|
+
return tiToolbox.generateAvatarStyle( identity.avatarSeed, identity.name );
|
|
1487
|
+
},
|
|
1488
|
+
|
|
1489
|
+
avatarInitial() {
|
|
1490
|
+
const identity = this.profile ? this.profile.identity : {};
|
|
1491
|
+
return String( identity.name || "?" ).charAt( 0 ).toUpperCase();
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
};
|
|
1495
|
+
};
|
|
1496
|
+
|
|
1497
|
+
/**
|
|
1498
|
+
* Returns a configuration object for the About screen "frame-about.html".
|
|
1499
|
+
* <br/>
|
|
1500
|
+
* The server descriptor carries the application's identity as flat fields; the release, component and runtime
|
|
1501
|
+
* sections are assembled here rather than server-side, because their labels are fixed framework chrome and the
|
|
1502
|
+
* client's `getLabel` takes a fallback — which is what keeps the screen readable inside a consuming application
|
|
1503
|
+
* that loads only its own label catalogue.
|
|
1504
|
+
*
|
|
1505
|
+
* @method
|
|
1506
|
+
* @returns {Object}
|
|
1507
|
+
* @public
|
|
1508
|
+
*/
|
|
1509
|
+
const configureScreenAbout = () => {
|
|
1510
|
+
const tiToolbox = Alpine.store( "tiToolbox" );
|
|
1511
|
+
const tiApplication = Alpine.store( "tiApplication" );
|
|
1512
|
+
|
|
1513
|
+
// Runtime keys the framework itself supplies; anything else an application adds falls back to its own key.
|
|
1514
|
+
const RUNTIME_LABELS = {
|
|
1515
|
+
node: [ "interface.about.runtime-node", "Node.js" ],
|
|
1516
|
+
platform: [ "interface.about.runtime-platform", "Platform" ],
|
|
1517
|
+
application: [ "interface.about.runtime-application", "Application ID" ]
|
|
1518
|
+
};
|
|
1519
|
+
|
|
1520
|
+
const label = ( key, fallback ) => tiApplication.getLabel( key, fallback );
|
|
1521
|
+
|
|
1522
|
+
return {
|
|
1523
|
+
|
|
1524
|
+
application: null,
|
|
1525
|
+
sections: [],
|
|
1526
|
+
busy: true,
|
|
1527
|
+
|
|
1528
|
+
init() {
|
|
1529
|
+
loadWhenInitialized( this, () => this.load() );
|
|
1530
|
+
},
|
|
1531
|
+
|
|
1532
|
+
load() {
|
|
1533
|
+
this.busy = true;
|
|
1534
|
+
tiApplication.sendRequest( "/app/about" ).then( ( result ) => {
|
|
1535
|
+
const data = ( result && result.data && typeof result.data === "object" ) ? result.data : {};
|
|
1536
|
+
this.application = {
|
|
1537
|
+
name: String( data.name || "" ),
|
|
1538
|
+
version: String( data.version || "" ),
|
|
1539
|
+
releaseDate: String( data.releaseDate || "" ),
|
|
1540
|
+
description: String( data.description || "" )
|
|
1541
|
+
};
|
|
1542
|
+
this.sections = normalizeInfoSections( [
|
|
1543
|
+
this._releaseSection( data ),
|
|
1544
|
+
this._componentsSection( data ),
|
|
1545
|
+
this._runtimeSection( data )
|
|
1546
|
+
].concat( Array.isArray( data.sections ) ? data.sections : [] ) );
|
|
1547
|
+
this.busy = false;
|
|
1548
|
+
} ).catch( ( error ) => {
|
|
1549
|
+
this.busy = false;
|
|
1550
|
+
if ( error && ( error.name === "AbortError" || error.isAborted ) ) return;
|
|
1551
|
+
tiApplication.notify( tiApplication.formatException( error ) );
|
|
1552
|
+
} );
|
|
1553
|
+
},
|
|
1554
|
+
|
|
1555
|
+
applicationInitial() {
|
|
1556
|
+
return String( ( this.application && this.application.name ) || "?" ).charAt( 0 ).toUpperCase();
|
|
1557
|
+
},
|
|
1558
|
+
|
|
1559
|
+
versionTag() {
|
|
1560
|
+
return "v" + ( ( this.application && this.application.version ) || "" );
|
|
1561
|
+
},
|
|
1562
|
+
|
|
1563
|
+
releaseDateText() {
|
|
1564
|
+
const released = label( "interface.about.released", "Released" );
|
|
1565
|
+
return released + " " + tiToolbox.formatDate( ( this.application && this.application.releaseDate ) || "" );
|
|
1566
|
+
},
|
|
1567
|
+
|
|
1568
|
+
_releaseSection( data ) {
|
|
1569
|
+
return {
|
|
1570
|
+
title: label( "interface.about.section-release", "Release" ),
|
|
1571
|
+
icon: "info-circle",
|
|
1572
|
+
items: [
|
|
1573
|
+
{ label: label( "interface.about.field-version", "Version" ), value: data.version, mono: true },
|
|
1574
|
+
{ label: label( "interface.about.field-release-date", "Release date" ), value: tiToolbox.formatDate( data.releaseDate ) },
|
|
1575
|
+
{ label: label( "interface.about.field-license", "License" ), value: data.license },
|
|
1576
|
+
{ label: label( "interface.about.field-author", "Author" ), value: data.author },
|
|
1577
|
+
{ label: label( "interface.about.field-package", "Package" ), value: data.packageName, mono: true, wide: true },
|
|
1578
|
+
{ label: label( "interface.about.field-homepage", "Homepage" ), value: data.homepage, href: data.homepage, wide: true }
|
|
1579
|
+
]
|
|
1580
|
+
};
|
|
1581
|
+
},
|
|
1582
|
+
|
|
1583
|
+
_componentsSection( data ) {
|
|
1584
|
+
const components = Array.isArray( data.components ) ? data.components : [];
|
|
1585
|
+
return {
|
|
1586
|
+
title: label( "interface.about.section-components", "Framework components" ),
|
|
1587
|
+
description: label( "interface.about.section-components-desc", "The ti-engine packages this application is built on." ),
|
|
1588
|
+
icon: "folder",
|
|
1589
|
+
items: components.map( ( component ) => ( {
|
|
1590
|
+
label: String( component.name || "" ),
|
|
1591
|
+
value: String( component.version || "" ),
|
|
1592
|
+
mono: true
|
|
1593
|
+
} ) )
|
|
1594
|
+
};
|
|
1595
|
+
},
|
|
1596
|
+
|
|
1597
|
+
_runtimeSection( data ) {
|
|
1598
|
+
// Present only for an admin session — the server withholds `runtime` from everyone else.
|
|
1599
|
+
const runtime = ( data.runtime && typeof data.runtime === "object" ) ? data.runtime : {};
|
|
1600
|
+
return {
|
|
1601
|
+
title: label( "interface.about.section-runtime", "Runtime" ),
|
|
1602
|
+
description: label( "interface.about.section-runtime-desc", "Visible to administrators only." ),
|
|
1603
|
+
icon: "settings",
|
|
1604
|
+
items: Object.keys( runtime ).map( ( key ) => {
|
|
1605
|
+
const known = RUNTIME_LABELS[ key ];
|
|
1606
|
+
return {
|
|
1607
|
+
label: known ? label( known[ 0 ], known[ 1 ] ) : key,
|
|
1608
|
+
value: String( runtime[ key ] || "" ),
|
|
1609
|
+
mono: true
|
|
1610
|
+
};
|
|
1611
|
+
} )
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
};
|
|
1616
|
+
};
|
|
1617
|
+
|
|
1618
|
+
/**
|
|
1619
|
+
* Returns a configuration object for the login screen error message.
|
|
1620
|
+
* <br/>
|
|
1621
|
+
* The login handlers answer a failed sign-in with a `303` to `/?error=<code>` (see `defaultErrorHandler`). This reads
|
|
1622
|
+
* that parameter and reveals the message element; the copy itself stays declarative via `x-text-label` so it localizes
|
|
1623
|
+
* through the normal path. Deliberately independent of the throwaway test-user panel so it survives that panel's removal.
|
|
1624
|
+
*
|
|
1625
|
+
* @method
|
|
1626
|
+
* @returns {Object}
|
|
1627
|
+
* @public
|
|
1628
|
+
*/
|
|
1629
|
+
const configureLoginError = () => {
|
|
1630
|
+
return {
|
|
1631
|
+
hasError: false,
|
|
1632
|
+
|
|
1633
|
+
init() {
|
|
1634
|
+
const tiToolbox = Alpine.store( "tiToolbox" );
|
|
1635
|
+
this.hasError = Boolean( tiToolbox.getUrlParam( "error" ) );
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
};
|
|
1639
|
+
|
|
1304
1640
|
/**
|
|
1305
1641
|
* Returns a configuration object for the login screen test user pill panel.
|
|
1306
1642
|
* <br/>
|
|
@@ -1424,4 +1760,7 @@ document.addEventListener( "alpine:init", () => {
|
|
|
1424
1760
|
Alpine.data( "tiComponentNotificationBar", configureComponentNotificationBar );
|
|
1425
1761
|
Alpine.data( "tiComponentTooltip", configureComponentTooltip );
|
|
1426
1762
|
Alpine.data( "tiLoginTestUserPanel", configureLoginTestUserPanel );
|
|
1763
|
+
Alpine.data( "tiLoginError", configureLoginError );
|
|
1764
|
+
Alpine.data( "tiScreenProfile", configureScreenProfile );
|
|
1765
|
+
Alpine.data( "tiScreenAbout", configureScreenAbout );
|
|
1427
1766
|
} );
|