@yuuvis/client-core 3.0.0 → 3.1.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/fesm2022/yuuvis-client-core.mjs +1814 -1535
- package/fesm2022/yuuvis-client-core.mjs.map +1 -1
- package/package.json +1 -1
- package/types/yuuvis-client-core.d.ts +211 -71
|
@@ -6,7 +6,7 @@ import { HttpErrorResponse, HttpClient, HttpHeaders, HttpRequest, HttpParams, Ht
|
|
|
6
6
|
import * as i0 from '@angular/core';
|
|
7
7
|
import { inject, Injectable, InjectionToken, NgZone, DOCUMENT, Inject, signal, Directive, Pipe, provideEnvironmentInitializer, makeEnvironmentProviders, provideAppInitializer } from '@angular/core';
|
|
8
8
|
import { tap, finalize, shareReplay, catchError, map, switchMap, first, filter, scan, delay } from 'rxjs/operators';
|
|
9
|
-
import { EMPTY, Subject, of, forkJoin, Observable,
|
|
9
|
+
import { EMPTY, Subject, of, forkJoin, Observable, tap as tap$1, catchError as catchError$1, ReplaySubject, BehaviorSubject, map as map$1, throwError, switchMap as switchMap$1, merge, fromEvent, filter as filter$1, debounceTime, isObservable } from 'rxjs';
|
|
10
10
|
import { StorageMap } from '@ngx-pwa/local-storage';
|
|
11
11
|
import { __decorate, __param, __metadata } from 'tslib';
|
|
12
12
|
import { coerceBooleanProperty } from '@angular/cdk/coercion';
|
|
@@ -80,6 +80,8 @@ const SystemType = {
|
|
|
80
80
|
FOLDER: 'system:folder',
|
|
81
81
|
AUDIT: 'system:audit',
|
|
82
82
|
ITEM: 'system:item',
|
|
83
|
+
CATALOG: 'system:catalog',
|
|
84
|
+
CATALOG_ENTRY: 'system:catalogEntry',
|
|
83
85
|
RELATIONSHIP: 'system:relationship',
|
|
84
86
|
SOT: 'system:secondary'
|
|
85
87
|
};
|
|
@@ -106,6 +108,13 @@ const RelationshipTypeField = {
|
|
|
106
108
|
SOURCE_ID: 'system:sourceId',
|
|
107
109
|
TARGET_ID: 'system:targetId'
|
|
108
110
|
};
|
|
111
|
+
const CatalogTypeField = {
|
|
112
|
+
NATIVE_ID: 'system:nativeId',
|
|
113
|
+
CATALOG_NATIVE_ID: 'system:catalogNativeId',
|
|
114
|
+
DATE_VALID_FROM: 'system:dateValidFrom',
|
|
115
|
+
DATE_VALID_UNTIL: 'system:dateValidUntil',
|
|
116
|
+
LOCALIZATION: 'system:localization'
|
|
117
|
+
};
|
|
109
118
|
const BaseObjectTypeField = {
|
|
110
119
|
OBJECT_TYPE_ID: 'system:objectTypeId',
|
|
111
120
|
VERSION_NUMBER: 'system:versionNumber',
|
|
@@ -167,8 +176,6 @@ var ContentStreamAllowed;
|
|
|
167
176
|
var Classification;
|
|
168
177
|
(function (Classification) {
|
|
169
178
|
Classification["STRING_CATALOG_I18N"] = "i18n:catalog";
|
|
170
|
-
Classification["STRING_CATALOG_CUSTOM"] = "custom:catalog";
|
|
171
|
-
Classification["STRING_CATALOG_DYNAMIC"] = "dynamic:catalog";
|
|
172
179
|
Classification["STRING_CATALOG"] = "catalog";
|
|
173
180
|
Classification["STRING_ORGANIZATION"] = "id:organization";
|
|
174
181
|
Classification["STRING_ORGANIZATION_SET"] = "id:organization:set";
|
|
@@ -1362,6 +1369,70 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1362
1369
|
}]
|
|
1363
1370
|
}] });
|
|
1364
1371
|
|
|
1372
|
+
/**
|
|
1373
|
+
* Service managing localizations. The localizations are fetched
|
|
1374
|
+
* during app initialization and stored in this service for
|
|
1375
|
+
* later use.
|
|
1376
|
+
*
|
|
1377
|
+
* Localizations could be extended by providing custom key-value pairs.
|
|
1378
|
+
*/
|
|
1379
|
+
class LocalizationService {
|
|
1380
|
+
#backend = inject(BackendService);
|
|
1381
|
+
#translate = inject(TranslateService);
|
|
1382
|
+
#i18n = {};
|
|
1383
|
+
/**
|
|
1384
|
+
* Extension for localizations. This allows to add custom localizations without
|
|
1385
|
+
* modifying the localizations provided by the backend. The extension is applied
|
|
1386
|
+
* on top of the localizations fetched from the backend, so it can be used to override
|
|
1387
|
+
* existing localizations as well.
|
|
1388
|
+
* Stored as record where the key is the language code (e.g. 'en', 'de', ...) and the
|
|
1389
|
+
* value is a record of key-value pairs for localizations.
|
|
1390
|
+
*/
|
|
1391
|
+
#extension = {};
|
|
1392
|
+
// called while app/core is initialized (APP_INITIALIZER)
|
|
1393
|
+
fetchLocalizations() {
|
|
1394
|
+
return this.#backend.get('/resources/text').pipe(tap$1((res) => (this.#i18n = res)), catchError$1((error) => {
|
|
1395
|
+
// in case of an error, return an empty object to avoid breaking the app
|
|
1396
|
+
console.error('Failed to fetch localizations', error);
|
|
1397
|
+
return of({});
|
|
1398
|
+
}));
|
|
1399
|
+
}
|
|
1400
|
+
getLocalizedResource(key) {
|
|
1401
|
+
const iso = this.#translate.getCurrentLang();
|
|
1402
|
+
return { ...this.#i18n, ...this.#extension[iso] }[key] || key;
|
|
1403
|
+
}
|
|
1404
|
+
getLocalizedLabel(id) {
|
|
1405
|
+
return this.getLocalizedResource(`${id}_label`);
|
|
1406
|
+
}
|
|
1407
|
+
getLocalizedDescription(id) {
|
|
1408
|
+
return this.getLocalizedResource(`${id}_description`);
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Extend localizations with custom key-value pairs. This allows to add custom localizations without
|
|
1412
|
+
* modifying the localizations provided by the backend. The extension is applied
|
|
1413
|
+
* on top of the localizations fetched from the backend, so it can be used to override
|
|
1414
|
+
* existing localizations as well.
|
|
1415
|
+
* @param localizations Record of key-value pairs for localizations, where the key is the language
|
|
1416
|
+
* code (e.g. 'en', 'de', ...) and the value is a record of key-value pairs for localizations.
|
|
1417
|
+
* Example:
|
|
1418
|
+
* {
|
|
1419
|
+
* en: { 'key1': 'value1', 'key2': 'value2' },
|
|
1420
|
+
* de: { 'key1': 'wert1', 'key2': 'wert2' }
|
|
1421
|
+
* }
|
|
1422
|
+
*/
|
|
1423
|
+
extendLocalizations(localizations) {
|
|
1424
|
+
this.#extension = { ...this.#extension, ...localizations };
|
|
1425
|
+
}
|
|
1426
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LocalizationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1427
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LocalizationService, providedIn: 'root' }); }
|
|
1428
|
+
}
|
|
1429
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LocalizationService, decorators: [{
|
|
1430
|
+
type: Injectable,
|
|
1431
|
+
args: [{
|
|
1432
|
+
providedIn: 'root'
|
|
1433
|
+
}]
|
|
1434
|
+
}] });
|
|
1435
|
+
|
|
1365
1436
|
/**
|
|
1366
1437
|
* Providing system definitions.
|
|
1367
1438
|
*/
|
|
@@ -1370,6 +1441,7 @@ class SystemService {
|
|
|
1370
1441
|
this.#backend = inject(BackendService);
|
|
1371
1442
|
this.#appCache = inject(AppCacheService);
|
|
1372
1443
|
this.#logger = inject(Logger);
|
|
1444
|
+
this.#localization = inject(LocalizationService);
|
|
1373
1445
|
this.#STORAGE_KEY = 'yuv.core.system.definition';
|
|
1374
1446
|
this.#STORAGE_KEY_AUTH_DATA = 'yuv.core.auth.data';
|
|
1375
1447
|
this.#STORAGE_KEY_FORMS = 'yuv.core.forms__';
|
|
@@ -1378,12 +1450,11 @@ class SystemService {
|
|
|
1378
1450
|
this.#resolvedClassificationsCache = {};
|
|
1379
1451
|
this.#systemSource = new ReplaySubject();
|
|
1380
1452
|
this.system$ = this.#systemSource.asObservable();
|
|
1381
|
-
// cache for resolved visible tags because they are used in lists and therefore should not be re-evaluated all the time
|
|
1382
|
-
this.#visibleTagsCache = {};
|
|
1383
1453
|
}
|
|
1384
1454
|
#backend;
|
|
1385
1455
|
#appCache;
|
|
1386
1456
|
#logger;
|
|
1457
|
+
#localization;
|
|
1387
1458
|
#STORAGE_KEY;
|
|
1388
1459
|
#STORAGE_KEY_AUTH_DATA;
|
|
1389
1460
|
#STORAGE_KEY_FORMS;
|
|
@@ -1391,8 +1462,6 @@ class SystemService {
|
|
|
1391
1462
|
#iconCache;
|
|
1392
1463
|
#resolvedClassificationsCache;
|
|
1393
1464
|
#systemSource;
|
|
1394
|
-
// cache for resolved visible tags because they are used in lists and therefore should not be re-evaluated all the time
|
|
1395
|
-
#visibleTagsCache;
|
|
1396
1465
|
#permissions;
|
|
1397
1466
|
/**
|
|
1398
1467
|
* Get all object types
|
|
@@ -1401,16 +1470,9 @@ class SystemService {
|
|
|
1401
1470
|
getObjectTypes(withLabels, situation) {
|
|
1402
1471
|
// Filter by user permissions based on situation
|
|
1403
1472
|
const objectTypes = this.#filterByPermissions([...this.system.objectTypes, ...this.system.secondaryObjectTypes.map((sot) => this.#sotToGenericType(sot))], situation);
|
|
1404
|
-
return withLabels
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
return {
|
|
1408
|
-
...sot,
|
|
1409
|
-
isFolder: false,
|
|
1410
|
-
creatable: true,
|
|
1411
|
-
secondaryObjectTypes: [],
|
|
1412
|
-
isSot: true
|
|
1413
|
-
};
|
|
1473
|
+
return withLabels
|
|
1474
|
+
? objectTypes.map((type) => ({ ...type, label: this.#localization.getLocalizedLabel(type.id) }))
|
|
1475
|
+
: objectTypes;
|
|
1414
1476
|
}
|
|
1415
1477
|
/**
|
|
1416
1478
|
* Get all secondary object types
|
|
@@ -1418,15 +1480,9 @@ class SystemService {
|
|
|
1418
1480
|
*/
|
|
1419
1481
|
getSecondaryObjectTypes(withLabels, situation) {
|
|
1420
1482
|
const sots = this.#filterByPermissions(this.system.secondaryObjectTypes.map((sot) => this.#sotToGenericType(sot)), situation);
|
|
1421
|
-
return (
|
|
1422
|
-
|
|
1423
|
-
.filter((
|
|
1424
|
-
}
|
|
1425
|
-
#filterByPermissions(types, situation) {
|
|
1426
|
-
if (!situation)
|
|
1427
|
-
return types;
|
|
1428
|
-
const allowedTypes = situation === 'search' ? this.#permissions.searchableObjectTypes : this.#permissions.createableObjectTypes;
|
|
1429
|
-
return types.filter((t) => allowedTypes.includes(t.id));
|
|
1483
|
+
return (withLabels
|
|
1484
|
+
? sots.map((objectType) => ({ ...objectType, label: this.#localization.getLocalizedLabel(objectType.id) }))
|
|
1485
|
+
: sots).filter((objectType) => objectType.id !== objectType.baseId && !objectType.id.startsWith('system:'));
|
|
1430
1486
|
}
|
|
1431
1487
|
/**
|
|
1432
1488
|
* Get a particular object type
|
|
@@ -1434,7 +1490,9 @@ class SystemService {
|
|
|
1434
1490
|
* @param withLabel Whether or not to also add the types label
|
|
1435
1491
|
*/
|
|
1436
1492
|
getObjectType(objectTypeId, withLabel) {
|
|
1437
|
-
let objectType = objectTypeId === SystemType.OBJECT
|
|
1493
|
+
let objectType = objectTypeId === SystemType.OBJECT
|
|
1494
|
+
? this.getBaseType()
|
|
1495
|
+
: this.system.objectTypes.find((objectType) => objectType.id === objectTypeId);
|
|
1438
1496
|
if (objectType && withLabel) {
|
|
1439
1497
|
objectType.label = this.getLocalizedResource(`${objectType.id}_label`) || objectTypeId;
|
|
1440
1498
|
}
|
|
@@ -1454,16 +1512,16 @@ class SystemService {
|
|
|
1454
1512
|
* @param withLabel Whether or not to also add the types label
|
|
1455
1513
|
*/
|
|
1456
1514
|
getSecondaryObjectType(objectTypeId, withLabel) {
|
|
1457
|
-
const objectType = this.system.secondaryObjectTypes.find((
|
|
1515
|
+
const objectType = this.system.secondaryObjectTypes.find((objectType) => objectType.id === objectTypeId);
|
|
1458
1516
|
if (objectType && withLabel) {
|
|
1459
|
-
objectType.label = this.
|
|
1517
|
+
objectType.label = this.#localization.getLocalizedLabel(objectType.id) || objectType.id;
|
|
1460
1518
|
}
|
|
1461
1519
|
return objectType;
|
|
1462
1520
|
}
|
|
1463
1521
|
getRelationship(id, withLabel) {
|
|
1464
|
-
const relationship = this.system.relationships.find((
|
|
1522
|
+
const relationship = this.system.relationships.find((relationship) => relationship.id === id);
|
|
1465
1523
|
if (relationship && withLabel) {
|
|
1466
|
-
relationship.label = this.getLocalizedLabel(relationship.id) || relationship.id;
|
|
1524
|
+
relationship.label = this.#localization.getLocalizedLabel(relationship.id) || relationship.id;
|
|
1467
1525
|
}
|
|
1468
1526
|
return relationship;
|
|
1469
1527
|
}
|
|
@@ -1506,15 +1564,14 @@ class SystemService {
|
|
|
1506
1564
|
const sysFolder = this.getBaseFolderType();
|
|
1507
1565
|
const sysDocument = this.getBaseDocumentType();
|
|
1508
1566
|
// base type contains only fields that are shared by base document and base folder ...
|
|
1509
|
-
const folderTypeFieldIDs = sysFolder.fields.map((
|
|
1510
|
-
const baseTypeFields = sysDocument.fields.filter((
|
|
1567
|
+
const folderTypeFieldIDs = sysFolder.fields.map((field) => field.id);
|
|
1568
|
+
const baseTypeFields = sysDocument.fields.filter((field) => folderTypeFieldIDs.includes(field.id));
|
|
1511
1569
|
return {
|
|
1512
1570
|
id: SystemType.OBJECT,
|
|
1513
1571
|
creatable: false,
|
|
1514
1572
|
isFolder: false,
|
|
1515
1573
|
secondaryObjectTypes: [],
|
|
1516
1574
|
fields: baseTypeFields
|
|
1517
|
-
// rawFields: baseTypeFields
|
|
1518
1575
|
};
|
|
1519
1576
|
}
|
|
1520
1577
|
/**
|
|
@@ -1526,8 +1583,8 @@ class SystemService {
|
|
|
1526
1583
|
const baseType = this.getBaseType();
|
|
1527
1584
|
return { id: baseType.id, fields: baseType.fields };
|
|
1528
1585
|
}
|
|
1529
|
-
const
|
|
1530
|
-
if (!
|
|
1586
|
+
const objectType = this.getObjectType(objectTypeId);
|
|
1587
|
+
if (!objectType) {
|
|
1531
1588
|
const sot = this.getSecondaryObjectType(objectTypeId) || { id: objectTypeId, fields: [] };
|
|
1532
1589
|
const baseType = this.getBaseType();
|
|
1533
1590
|
return {
|
|
@@ -1536,22 +1593,10 @@ class SystemService {
|
|
|
1536
1593
|
};
|
|
1537
1594
|
}
|
|
1538
1595
|
return {
|
|
1539
|
-
id:
|
|
1540
|
-
fields:
|
|
1596
|
+
id: objectType.id,
|
|
1597
|
+
fields: objectType.fields
|
|
1541
1598
|
};
|
|
1542
1599
|
}
|
|
1543
|
-
/**
|
|
1544
|
-
* Get the resolved object tags
|
|
1545
|
-
*/
|
|
1546
|
-
getResolvedTags(objectTypeId) {
|
|
1547
|
-
const vTags = this.getVisibleTags(objectTypeId);
|
|
1548
|
-
return Object.keys(vTags).map((k) => ({
|
|
1549
|
-
id: objectTypeId,
|
|
1550
|
-
tagName: k,
|
|
1551
|
-
tagValues: vTags[k],
|
|
1552
|
-
fields: this.getBaseType().fields.filter((f) => f.id === BaseObjectTypeField.TAGS)
|
|
1553
|
-
}));
|
|
1554
|
-
}
|
|
1555
1600
|
/**
|
|
1556
1601
|
* Get a list of classifications for a given object type including the
|
|
1557
1602
|
* classifications of its static secondary object types
|
|
@@ -1560,62 +1605,6 @@ class SystemService {
|
|
|
1560
1605
|
getResolvedClassifications(objectTypeId) {
|
|
1561
1606
|
return this.#resolvedClassificationsCache[objectTypeId] || this.#resolveClassifications(objectTypeId);
|
|
1562
1607
|
}
|
|
1563
|
-
#resolveClassifications(objectTypeId) {
|
|
1564
|
-
let classifications = [];
|
|
1565
|
-
const ot = this.getObjectType(objectTypeId);
|
|
1566
|
-
if (ot) {
|
|
1567
|
-
classifications = ot.classification || [];
|
|
1568
|
-
const staticSOTs = ot.secondaryObjectTypes ? ot.secondaryObjectTypes.filter((sot) => sot.static).map((sot) => sot.id) : [];
|
|
1569
|
-
staticSOTs.forEach((id) => {
|
|
1570
|
-
const sot = this.getSecondaryObjectType(id);
|
|
1571
|
-
classifications = sot?.classification
|
|
1572
|
-
? [
|
|
1573
|
-
...classifications,
|
|
1574
|
-
...sot.classification.filter((c) => {
|
|
1575
|
-
// also filter classifications that should not be inherited
|
|
1576
|
-
return c !== ObjectTypeClassification.CREATE_FALSE && c !== ObjectTypeClassification.SEARCH_FALSE;
|
|
1577
|
-
})
|
|
1578
|
-
]
|
|
1579
|
-
: classifications;
|
|
1580
|
-
});
|
|
1581
|
-
this.#resolvedClassificationsCache[objectTypeId] = classifications;
|
|
1582
|
-
}
|
|
1583
|
-
return classifications;
|
|
1584
|
-
}
|
|
1585
|
-
/**
|
|
1586
|
-
* Visible tags are defined by a classification on the object type (e.g. 'tag[tenkolibri:process,1,2,3]').
|
|
1587
|
-
*
|
|
1588
|
-
* The example will only return tags with the name 'tenkolibri:process'
|
|
1589
|
-
* and values of either 1, 2 or 3. All other tags will be ignored.
|
|
1590
|
-
*
|
|
1591
|
-
* @param objectTypeId ID of the object type to get the visible tags for
|
|
1592
|
-
* @returns object where the property name is the name of the tag and its value are the visible values
|
|
1593
|
-
* for that tag (if values is emoty all values are allowed)
|
|
1594
|
-
*/
|
|
1595
|
-
getVisibleTags(objectTypeId) {
|
|
1596
|
-
return this.#visibleTagsCache[objectTypeId] || this.fetchVisibleTags(objectTypeId);
|
|
1597
|
-
}
|
|
1598
|
-
fetchVisibleTags(objectTypeId) {
|
|
1599
|
-
const ot = this.getObjectType(objectTypeId) || this.getSecondaryObjectType(objectTypeId);
|
|
1600
|
-
const tagClassifications = this.getResolvedClassifications(objectTypeId).filter((t) => t.startsWith('tag['));
|
|
1601
|
-
const parentType = ot && ot.id;
|
|
1602
|
-
const to = {};
|
|
1603
|
-
(tagClassifications || []).forEach((tag) => {
|
|
1604
|
-
const m = tag.match(/\[(.*)\]/i)[1].split(',');
|
|
1605
|
-
const tagName = m.splice(0, 1)[0];
|
|
1606
|
-
const tagValues = m.map((v) => parseInt(v.trim()));
|
|
1607
|
-
to[tagName] = tagValues;
|
|
1608
|
-
});
|
|
1609
|
-
this.#visibleTagsCache[objectTypeId] = parentType ? { ...this.getVisibleTags(parentType), ...to } : to;
|
|
1610
|
-
return this.#visibleTagsCache[objectTypeId];
|
|
1611
|
-
}
|
|
1612
|
-
filterVisibleTags(objectTypeId, tagsValue) {
|
|
1613
|
-
if (!tagsValue)
|
|
1614
|
-
return [];
|
|
1615
|
-
const vTags = this.getVisibleTags(objectTypeId);
|
|
1616
|
-
// Tag value looks like this: [tagName: string, state: number, date: Date, traceId: string]
|
|
1617
|
-
return tagsValue.filter((v) => !!vTags[v[0]] && vTags[v[0]].includes(v[1]));
|
|
1618
|
-
}
|
|
1619
1608
|
/**
|
|
1620
1609
|
* Get the icon for an object type. This will return an SVG as a string.
|
|
1621
1610
|
* @param objectTypeId ID of the object type
|
|
@@ -1644,42 +1633,30 @@ class SystemService {
|
|
|
1644
1633
|
return this.#iconCache[objectTypeId].uri;
|
|
1645
1634
|
}
|
|
1646
1635
|
else {
|
|
1647
|
-
const
|
|
1648
|
-
const
|
|
1649
|
-
const uri = `/resources/icons/${encodeURIComponent(
|
|
1636
|
+
const classificationIcon = this.#getIconFromClassification(objectTypeId);
|
|
1637
|
+
const fallbackIcon = this.#getFallbackIcon(objectTypeId, fallback);
|
|
1638
|
+
const uri = `/resources/icons/${encodeURIComponent(classificationIcon || objectTypeId)}${fallbackIcon ? `?fallback=${encodeURIComponent(fallbackIcon)}` : ''}`;
|
|
1650
1639
|
this.#iconCache[objectTypeId] = { uri: `${this.#backend.getApiBase(ApiBase.apiWeb)}${uri}` };
|
|
1651
1640
|
return this.#iconCache[objectTypeId].uri;
|
|
1652
1641
|
}
|
|
1653
1642
|
}
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
// add default fallbacks for system:document and system:folder if now other fallback has been provided
|
|
1658
|
-
fallback = ot.isFolder ? 'system:folder' : 'system:document';
|
|
1659
|
-
// if (this.isFloatingObjectType(ot)) {
|
|
1660
|
-
// // types that do not have no object type assigned to them (primary FSOTs)
|
|
1661
|
-
// fallback = 'system:dlm';
|
|
1662
|
-
// }
|
|
1663
|
-
}
|
|
1664
|
-
return fallback;
|
|
1665
|
-
}
|
|
1666
|
-
#getIconFromClassification(objectTypeId) {
|
|
1667
|
-
const ce = this.getClassifications(this.getResolvedClassifications(objectTypeId));
|
|
1668
|
-
return ce.has(ObjectTypeClassification.OBJECT_TYPE_ICON) ? ce.get(ObjectTypeClassification.OBJECT_TYPE_ICON).options[0] : null;
|
|
1669
|
-
}
|
|
1643
|
+
/**
|
|
1644
|
+
* @deprecated use LocalizationService.getLocalizedResource instead
|
|
1645
|
+
*/
|
|
1670
1646
|
getLocalizedResource(key) {
|
|
1671
|
-
|
|
1672
|
-
return this.system.i18n[key];
|
|
1673
|
-
}
|
|
1674
|
-
catch (error) {
|
|
1675
|
-
return key;
|
|
1676
|
-
}
|
|
1647
|
+
return this.#localization.getLocalizedResource(key);
|
|
1677
1648
|
}
|
|
1649
|
+
/**
|
|
1650
|
+
* @deprecated use LocalizationService.getLocalizedResource instead
|
|
1651
|
+
*/
|
|
1678
1652
|
getLocalizedLabel(id) {
|
|
1679
|
-
return this.
|
|
1653
|
+
return this.#localization.getLocalizedLabel(id);
|
|
1680
1654
|
}
|
|
1655
|
+
/**
|
|
1656
|
+
* @deprecated use LocalizationService.getLocalizedResource instead
|
|
1657
|
+
*/
|
|
1681
1658
|
getLocalizedDescription(id) {
|
|
1682
|
-
return this.
|
|
1659
|
+
return this.#localization.getLocalizedDescription(id);
|
|
1683
1660
|
}
|
|
1684
1661
|
/**
|
|
1685
1662
|
* Determine whether or not the given object type field is a system field
|
|
@@ -1715,82 +1692,65 @@ class SystemService {
|
|
|
1715
1692
|
// })
|
|
1716
1693
|
// );
|
|
1717
1694
|
}
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
* @param user User to fetch definition for
|
|
1721
|
-
*/
|
|
1722
|
-
#fetchSystemDefinition(authData) {
|
|
1723
|
-
return (authData ? of(authData) : this.#appCache.getItem(this.#STORAGE_KEY_AUTH_DATA)).pipe(switchMap((data) => {
|
|
1724
|
-
this.updateAuthData(data).subscribe();
|
|
1725
|
-
const fetchTasks = [this.#backend.get('/dms/schema/native.json', ApiBase.core), this.#fetchLocalizations()];
|
|
1726
|
-
return forkJoin(fetchTasks);
|
|
1727
|
-
}), catchError((error) => {
|
|
1728
|
-
this.#logger.error('Error fetching recent version of system definition from server.', error);
|
|
1729
|
-
this.#systemSource.error('Error fetching recent version of system definition from server.');
|
|
1730
|
-
return of(null);
|
|
1731
|
-
}), map((data) => {
|
|
1732
|
-
if (data?.length) {
|
|
1733
|
-
this.setSchema(data[0], data[1]);
|
|
1734
|
-
}
|
|
1735
|
-
return !!data;
|
|
1736
|
-
}));
|
|
1737
|
-
}
|
|
1738
|
-
setPermissions(p) {
|
|
1739
|
-
this.#permissions = p;
|
|
1695
|
+
setPermissions(permissions) {
|
|
1696
|
+
this.#permissions = permissions;
|
|
1740
1697
|
}
|
|
1741
1698
|
/**
|
|
1742
1699
|
* Create the schema from the servers schema response
|
|
1743
1700
|
* @param schemaResponse Response from the backend
|
|
1744
1701
|
*/
|
|
1745
|
-
setSchema(schemaResponse
|
|
1702
|
+
setSchema(schemaResponse) {
|
|
1746
1703
|
// prepare a quick access object for the fields
|
|
1747
1704
|
const propertiesQA = {};
|
|
1748
1705
|
const orgTypeFields = [BaseObjectTypeField.MODIFIED_BY, BaseObjectTypeField.CREATED_BY];
|
|
1749
|
-
schemaResponse.propertyDefinition.forEach((
|
|
1750
|
-
p.classifications = p.classification;
|
|
1706
|
+
schemaResponse.propertyDefinition.forEach((propDef) => {
|
|
1707
|
+
// p.classifications = p.classification;
|
|
1751
1708
|
// TODO: Remove once schema supports organization classification for base params
|
|
1752
1709
|
// map certain fields to organization type (fake it until you make it ;-)
|
|
1753
|
-
if (orgTypeFields.includes(
|
|
1754
|
-
|
|
1710
|
+
if (orgTypeFields.includes(propDef.id)) {
|
|
1711
|
+
propDef.classifications = [Classification.STRING_ORGANIZATION];
|
|
1755
1712
|
}
|
|
1756
|
-
propertiesQA[
|
|
1713
|
+
propertiesQA[propDef.id] = propDef;
|
|
1757
1714
|
});
|
|
1758
1715
|
// prepare a quick access object for object types (including secondary objects)
|
|
1759
1716
|
const objectTypesQA = {};
|
|
1760
|
-
schemaResponse.typeFolderDefinition.forEach((
|
|
1761
|
-
objectTypesQA[
|
|
1717
|
+
schemaResponse.typeFolderDefinition.forEach((objType) => {
|
|
1718
|
+
objectTypesQA[objType.id] = objType;
|
|
1762
1719
|
});
|
|
1763
|
-
schemaResponse.typeDocumentDefinition.forEach((
|
|
1764
|
-
objectTypesQA[
|
|
1720
|
+
schemaResponse.typeDocumentDefinition.forEach((objType) => {
|
|
1721
|
+
objectTypesQA[objType.id] = objType;
|
|
1765
1722
|
});
|
|
1766
1723
|
schemaResponse.typeSecondaryDefinition.forEach((sot) => {
|
|
1767
1724
|
objectTypesQA[sot.id] = sot;
|
|
1768
1725
|
});
|
|
1769
1726
|
const objectTypes = [
|
|
1770
1727
|
// folder types
|
|
1771
|
-
...schemaResponse.typeFolderDefinition.map((
|
|
1772
|
-
id:
|
|
1773
|
-
description:
|
|
1774
|
-
classification:
|
|
1775
|
-
baseId:
|
|
1776
|
-
creatable: this.#isCreatable(
|
|
1728
|
+
...schemaResponse.typeFolderDefinition.map((folderDef) => ({
|
|
1729
|
+
id: folderDef.id,
|
|
1730
|
+
description: folderDef.description,
|
|
1731
|
+
classification: folderDef.classification,
|
|
1732
|
+
baseId: folderDef.baseId,
|
|
1733
|
+
creatable: this.#isCreatable(folderDef.id),
|
|
1777
1734
|
contentStreamAllowed: ContentStreamAllowed.NOT_ALLOWED,
|
|
1778
1735
|
isFolder: true,
|
|
1779
|
-
secondaryObjectTypes:
|
|
1780
|
-
|
|
1781
|
-
|
|
1736
|
+
secondaryObjectTypes: folderDef.secondaryObjectTypeId
|
|
1737
|
+
? folderDef.secondaryObjectTypeId.map((type) => ({ id: type.value, static: type.static }))
|
|
1738
|
+
: [],
|
|
1739
|
+
fields: this.#resolveObjectTypeFields(folderDef, propertiesQA, objectTypesQA)
|
|
1782
1740
|
})),
|
|
1783
1741
|
// document types
|
|
1784
|
-
...schemaResponse.typeDocumentDefinition.map((
|
|
1785
|
-
id:
|
|
1786
|
-
description:
|
|
1787
|
-
classification:
|
|
1788
|
-
baseId:
|
|
1789
|
-
creatable: this.#isCreatable(
|
|
1790
|
-
contentStreamAllowed:
|
|
1742
|
+
...schemaResponse.typeDocumentDefinition.map((documentDef) => ({
|
|
1743
|
+
id: documentDef.id,
|
|
1744
|
+
description: documentDef.description,
|
|
1745
|
+
classification: documentDef.classification,
|
|
1746
|
+
baseId: documentDef.baseId,
|
|
1747
|
+
creatable: this.#isCreatable(documentDef.id),
|
|
1748
|
+
contentStreamAllowed: documentDef.contentStreamAllowed,
|
|
1791
1749
|
isFolder: false,
|
|
1792
|
-
secondaryObjectTypes:
|
|
1793
|
-
|
|
1750
|
+
secondaryObjectTypes: documentDef.secondaryObjectTypeId
|
|
1751
|
+
? documentDef.secondaryObjectTypeId.map((type) => ({ id: type.value, static: type.static }))
|
|
1752
|
+
: [],
|
|
1753
|
+
fields: this.#resolveObjectTypeFields(documentDef, propertiesQA, objectTypesQA)
|
|
1794
1754
|
// rawFields: this.resolveObjectTypeFields(dd, propertiesQA, objectTypesQA, true),
|
|
1795
1755
|
}))
|
|
1796
1756
|
];
|
|
@@ -1810,8 +1770,8 @@ class SystemService {
|
|
|
1810
1770
|
baseId: std.baseId,
|
|
1811
1771
|
fields: this.#resolveObjectTypeFields(std, propertiesQA, objectTypesQA),
|
|
1812
1772
|
// allowedSourceType
|
|
1813
|
-
allowedSourceTypes: std.allowedSourceType.map((
|
|
1814
|
-
allowedTargetTypes: std.allowedTargetType.map((
|
|
1773
|
+
allowedSourceTypes: std.allowedSourceType.map((type) => type.objectTypeReference),
|
|
1774
|
+
allowedTargetTypes: std.allowedTargetType.map((type) => type.objectTypeReference)
|
|
1815
1775
|
}));
|
|
1816
1776
|
this.system = {
|
|
1817
1777
|
version: schemaResponse.version,
|
|
@@ -1819,41 +1779,11 @@ class SystemService {
|
|
|
1819
1779
|
objectTypes,
|
|
1820
1780
|
secondaryObjectTypes,
|
|
1821
1781
|
relationships,
|
|
1822
|
-
i18n: localizedResource,
|
|
1823
1782
|
allFields: propertiesQA
|
|
1824
1783
|
};
|
|
1825
1784
|
this.#appCache.setItem(this.#STORAGE_KEY, this.system).subscribe();
|
|
1826
1785
|
this.#systemSource.next(this.system);
|
|
1827
1786
|
}
|
|
1828
|
-
/**
|
|
1829
|
-
* Resolve all the fields for an object type. This also includes secondary object types and the fields inherited from
|
|
1830
|
-
* the base type (... and of course the base type (and its secondary object types) of the base type and so on)
|
|
1831
|
-
* @param schemaTypeDefinition object type definition from the native schema
|
|
1832
|
-
* @param propertiesQA Quick access object of all properties
|
|
1833
|
-
* @param objectTypesQA Quick access object of all object types
|
|
1834
|
-
* @param raw If set to 'true' only the properties of the object type itself will be returned (without SOTs)
|
|
1835
|
-
*/
|
|
1836
|
-
#resolveObjectTypeFields(schemaTypeDefinition, propertiesQA, objectTypesQA) {
|
|
1837
|
-
const objectTypeFieldIDs = schemaTypeDefinition.propertyReference.map((pr) => pr.value);
|
|
1838
|
-
if (schemaTypeDefinition.secondaryObjectTypeId) {
|
|
1839
|
-
schemaTypeDefinition.secondaryObjectTypeId
|
|
1840
|
-
.filter((sot) => sot.static)
|
|
1841
|
-
.map((sot) => sot.value)
|
|
1842
|
-
.forEach((sotID) => objectTypesQA[sotID].propertyReference.forEach((pr) => objectTypeFieldIDs.push(pr.value)));
|
|
1843
|
-
}
|
|
1844
|
-
let fields = objectTypeFieldIDs.map((id) => ({
|
|
1845
|
-
...propertiesQA[id],
|
|
1846
|
-
_internalType: this.getInternalFormElementType(propertiesQA[id].propertyType, propertiesQA[id].classifications)
|
|
1847
|
-
}));
|
|
1848
|
-
// also resolve properties of the base type
|
|
1849
|
-
if (schemaTypeDefinition.baseId !== schemaTypeDefinition.id && !!objectTypesQA[schemaTypeDefinition.baseId]) {
|
|
1850
|
-
fields = fields.concat(this.#resolveObjectTypeFields(objectTypesQA[schemaTypeDefinition.baseId], propertiesQA, objectTypesQA));
|
|
1851
|
-
}
|
|
1852
|
-
return fields;
|
|
1853
|
-
}
|
|
1854
|
-
#isCreatable(objectTypeId) {
|
|
1855
|
-
return ![SystemType.FOLDER, SystemType.DOCUMENT].includes(objectTypeId);
|
|
1856
|
-
}
|
|
1857
1787
|
/**
|
|
1858
1788
|
* Fetch a collection of form models.
|
|
1859
1789
|
* @param objectTypeIDs Object type IDs to fetch form model for
|
|
@@ -1861,35 +1791,18 @@ class SystemService {
|
|
|
1861
1791
|
* @returns Object where the object type id is key and the form model is the value
|
|
1862
1792
|
*/
|
|
1863
1793
|
getObjectTypeForms(objectTypeIDs, situation) {
|
|
1864
|
-
return forkJoin(objectTypeIDs.map((
|
|
1865
|
-
id:
|
|
1794
|
+
return forkJoin(objectTypeIDs.map((obj) => this.getObjectTypeForm(obj, situation).pipe(catchError(() => of(null)), map((res) => ({
|
|
1795
|
+
id: obj,
|
|
1866
1796
|
formModel: res
|
|
1867
|
-
}))))).pipe(map((
|
|
1797
|
+
}))))).pipe(map((modelRes) => {
|
|
1868
1798
|
const resMap = {};
|
|
1869
|
-
|
|
1870
|
-
.map((
|
|
1871
|
-
.filter((
|
|
1872
|
-
.forEach((
|
|
1799
|
+
modelRes
|
|
1800
|
+
.map((res) => (!res.formModel ? { ...res, formModel: this.#generateDefaultFormModel(res.id) } : res))
|
|
1801
|
+
.filter((res) => this.#formHasElements(res.formModel))
|
|
1802
|
+
.forEach((res) => (resMap[res.id] = res.formModel));
|
|
1873
1803
|
return resMap;
|
|
1874
1804
|
}));
|
|
1875
1805
|
}
|
|
1876
|
-
/**
|
|
1877
|
-
* Generate a default form model for an object type. This is used when the backend does not return a form model.
|
|
1878
|
-
* @param typeId Object type ID or SOT ID
|
|
1879
|
-
*/
|
|
1880
|
-
#generateDefaultFormModel(typeId) {
|
|
1881
|
-
const type = this.getObjectType(typeId) || this.getSecondaryObjectType(typeId);
|
|
1882
|
-
if (type) {
|
|
1883
|
-
return {
|
|
1884
|
-
id: type.id,
|
|
1885
|
-
label: this.getLocalizedLabel(type.id),
|
|
1886
|
-
elements: type.fields.map((f) => this.toFormElement(f)),
|
|
1887
|
-
title: this.getLocalizedLabel(type.id),
|
|
1888
|
-
description: this.getLocalizedDescription(type.id)
|
|
1889
|
-
};
|
|
1890
|
-
}
|
|
1891
|
-
return null;
|
|
1892
|
-
}
|
|
1893
1806
|
/**
|
|
1894
1807
|
* Get the form model of an object type.
|
|
1895
1808
|
*
|
|
@@ -1902,34 +1815,6 @@ class SystemService {
|
|
|
1902
1815
|
.getItem(`${this.#STORAGE_KEY_FORMS}${objectTypeId}_${situation}`)
|
|
1903
1816
|
.pipe(switchMap((res) => (res ? of(res) : this.#fetchObjectTypeForm(objectTypeId, situation))));
|
|
1904
1817
|
}
|
|
1905
|
-
#fetchObjectTypeForm(objectTypeId, situation) {
|
|
1906
|
-
return this.#backend.get(Utils.buildUri(`/dms/forms/${objectTypeId}`, { situation })).pipe(
|
|
1907
|
-
// add form to cache
|
|
1908
|
-
switchMap((res) => this.#appCache.setItem(`${this.#STORAGE_KEY_FORMS}${objectTypeId}_${situation}`, res).pipe(map(() => res))));
|
|
1909
|
-
}
|
|
1910
|
-
#clearObjectTypeFormCache() {
|
|
1911
|
-
// reset cache of object forms
|
|
1912
|
-
return this.#appCache
|
|
1913
|
-
.getStorageKeys()
|
|
1914
|
-
.pipe(switchMap((keys) => forkJoin(keys.filter((key) => key.startsWith(this.#STORAGE_KEY_FORMS)).map((key) => this.#appCache.removeItem(key).pipe(catchError(() => of(null)))))))
|
|
1915
|
-
.subscribe();
|
|
1916
|
-
}
|
|
1917
|
-
/**
|
|
1918
|
-
* Check whether or not the model has at least one form element. Recursive.
|
|
1919
|
-
* @param element Form element to check child elements for
|
|
1920
|
-
*/
|
|
1921
|
-
#formHasElements(element) {
|
|
1922
|
-
let hasElement = false;
|
|
1923
|
-
element.elements?.forEach((e) => {
|
|
1924
|
-
if (!['o2mGroup', 'o2mGroupStack'].includes(e.type)) {
|
|
1925
|
-
hasElement = true;
|
|
1926
|
-
}
|
|
1927
|
-
else if (!hasElement) {
|
|
1928
|
-
hasElement = this.#formHasElements(e);
|
|
1929
|
-
}
|
|
1930
|
-
});
|
|
1931
|
-
return hasElement;
|
|
1932
|
-
}
|
|
1933
1818
|
/**
|
|
1934
1819
|
* Generates an internal type for a given object type field.
|
|
1935
1820
|
* Adding this to a form element or object type field enables us to render forms
|
|
@@ -1937,10 +1822,14 @@ class SystemService {
|
|
|
1937
1822
|
* have to evaluate the conditions for every form element on every digest cycle.
|
|
1938
1823
|
* @param type propertyType of the ObjectTypeField
|
|
1939
1824
|
* @param classifications classifications of the ObjectTypeField
|
|
1825
|
+
* @param catalog catalog reference of the ObjectTypeField
|
|
1940
1826
|
*/
|
|
1941
|
-
getInternalFormElementType(type, classifications) {
|
|
1827
|
+
getInternalFormElementType(type, classifications, catalog) {
|
|
1942
1828
|
const _classifications = this.getClassifications(classifications || []);
|
|
1943
|
-
if (
|
|
1829
|
+
if (catalog) {
|
|
1830
|
+
return InternalFieldType.STRING_DYNAMIC_CATALOG;
|
|
1831
|
+
}
|
|
1832
|
+
else if (type === 'string' && _classifications.has(Classification.STRING_REFERENCE)) {
|
|
1944
1833
|
return InternalFieldType.STRING_REFERENCE;
|
|
1945
1834
|
}
|
|
1946
1835
|
else if ((type === 'string' && _classifications.has(Classification.STRING_ORGANIZATION)) ||
|
|
@@ -1956,18 +1845,16 @@ class SystemService {
|
|
|
1956
1845
|
else if (type === 'boolean' && _classifications.has(Classification.BOOLEAN_SWITCH)) {
|
|
1957
1846
|
return InternalFieldType.BOOLEAN_SWITCH;
|
|
1958
1847
|
}
|
|
1959
|
-
else if (type === 'string' &&
|
|
1960
|
-
(_classifications.has(Classification.STRING_CATALOG_DYNAMIC) || _classifications.has(Classification.STRING_CATALOG_CUSTOM))) {
|
|
1961
|
-
return InternalFieldType.STRING_DYNAMIC_CATALOG;
|
|
1962
|
-
}
|
|
1963
1848
|
else {
|
|
1964
1849
|
// if there are no matching conditions just return the original type
|
|
1965
1850
|
return type;
|
|
1966
1851
|
}
|
|
1967
1852
|
}
|
|
1968
1853
|
getObjectTypeField(id) {
|
|
1969
|
-
const
|
|
1970
|
-
return
|
|
1854
|
+
const field = this.system?.allFields[id];
|
|
1855
|
+
return field
|
|
1856
|
+
? { ...field, _internalType: this.getInternalFormElementType(field.propertyType, field.classifications) }
|
|
1857
|
+
: undefined;
|
|
1971
1858
|
}
|
|
1972
1859
|
/**
|
|
1973
1860
|
* Extract classifications from object type fields classification
|
|
@@ -1984,14 +1871,15 @@ class SystemService {
|
|
|
1984
1871
|
const res = new Map();
|
|
1985
1872
|
if (classifications) {
|
|
1986
1873
|
classifications
|
|
1987
|
-
.map((
|
|
1988
|
-
.filter((
|
|
1989
|
-
.forEach((
|
|
1990
|
-
|
|
1991
|
-
|
|
1874
|
+
.map((clf) => clf.replace(/\s+/g, ''))
|
|
1875
|
+
.filter((clf) => clf.length > 0)
|
|
1876
|
+
.forEach((clf) => {
|
|
1877
|
+
// eslint-disable-next-line no-useless-escape
|
|
1878
|
+
const matches = clf.match(/^([^\[]*)(\[(.*)\])?$/);
|
|
1879
|
+
if (matches?.length) {
|
|
1992
1880
|
res.set(matches[1], {
|
|
1993
1881
|
classification: matches[1],
|
|
1994
|
-
options: matches[3] ? matches[3].split(',').map((
|
|
1882
|
+
options: matches[3] ? matches[3].split(',').map((opt) => opt.trim()) : []
|
|
1995
1883
|
});
|
|
1996
1884
|
}
|
|
1997
1885
|
});
|
|
@@ -1999,23 +1887,18 @@ class SystemService {
|
|
|
1999
1887
|
return res;
|
|
2000
1888
|
}
|
|
2001
1889
|
toFormElement(field) {
|
|
2002
|
-
return {
|
|
1890
|
+
return {
|
|
1891
|
+
...field,
|
|
1892
|
+
label: this.#localization.getLocalizedLabel(field.id),
|
|
1893
|
+
name: field.id,
|
|
1894
|
+
type: field.propertyType
|
|
1895
|
+
};
|
|
2003
1896
|
}
|
|
2004
1897
|
updateAuthData(data) {
|
|
2005
1898
|
this.authData = { ...this.authData, ...data };
|
|
2006
1899
|
this.#backend.setHeader('Accept-Language', this.authData.language);
|
|
2007
1900
|
return this.#appCache.setItem(this.#STORAGE_KEY_AUTH_DATA, this.authData);
|
|
2008
1901
|
}
|
|
2009
|
-
updateLocalizations(iso) {
|
|
2010
|
-
return this.updateAuthData({ language: iso }).pipe(switchMap(() => this.#fetchLocalizations()), tap((res) => {
|
|
2011
|
-
this.system.i18n = res;
|
|
2012
|
-
this.#appCache.setItem(this.#STORAGE_KEY, this.system).subscribe();
|
|
2013
|
-
this.#systemSource.next(this.system);
|
|
2014
|
-
}));
|
|
2015
|
-
}
|
|
2016
|
-
#fetchLocalizations() {
|
|
2017
|
-
return this.#backend.get('/resources/text');
|
|
2018
|
-
}
|
|
2019
1902
|
fetchResources(id) {
|
|
2020
1903
|
return this.#backend
|
|
2021
1904
|
.batch([
|
|
@@ -2024,41 +1907,193 @@ class SystemService {
|
|
|
2024
1907
|
])
|
|
2025
1908
|
.pipe(map(([global, tenant]) => ({ global, tenant })));
|
|
2026
1909
|
}
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
1910
|
+
#filterByPermissions(types, situation) {
|
|
1911
|
+
if (!situation)
|
|
1912
|
+
return types;
|
|
1913
|
+
const allowedTypes = situation === 'search' ? this.#permissions.searchableObjectTypes : this.#permissions.createableObjectTypes;
|
|
1914
|
+
return types.filter((type) => allowedTypes.includes(type.id));
|
|
1915
|
+
}
|
|
1916
|
+
#sotToGenericType(sot) {
|
|
1917
|
+
return {
|
|
1918
|
+
...sot,
|
|
1919
|
+
isFolder: false,
|
|
1920
|
+
creatable: true,
|
|
1921
|
+
secondaryObjectTypes: [],
|
|
1922
|
+
isSot: true
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
#getFallbackIcon(objectTypeId, fallback) {
|
|
1926
|
+
const objectType = this.getObjectType(objectTypeId);
|
|
1927
|
+
if (objectType && !fallback) {
|
|
1928
|
+
// add default fallbacks for system:document and system:folder if now other fallback has been provided
|
|
1929
|
+
fallback = objectType.isFolder ? 'system:folder' : 'system:document';
|
|
1930
|
+
}
|
|
1931
|
+
return fallback;
|
|
1932
|
+
}
|
|
1933
|
+
#getIconFromClassification(objectTypeId) {
|
|
1934
|
+
const classificationEntry = this.getClassifications(this.getResolvedClassifications(objectTypeId));
|
|
1935
|
+
return classificationEntry.has(ObjectTypeClassification.OBJECT_TYPE_ICON)
|
|
1936
|
+
? classificationEntry.get(ObjectTypeClassification.OBJECT_TYPE_ICON).options[0]
|
|
1937
|
+
: null;
|
|
1938
|
+
}
|
|
1939
|
+
#fetchObjectTypeForm(objectTypeId, situation) {
|
|
1940
|
+
return this.#backend.get(Utils.buildUri(`/dms/forms/${objectTypeId}`, { situation })).pipe(
|
|
1941
|
+
// add form to cache
|
|
1942
|
+
switchMap((res) => this.#appCache.setItem(`${this.#STORAGE_KEY_FORMS}${objectTypeId}_${situation}`, res).pipe(map(() => res))));
|
|
1943
|
+
}
|
|
1944
|
+
#clearObjectTypeFormCache() {
|
|
1945
|
+
// reset cache of object forms
|
|
1946
|
+
this.#appCache
|
|
1947
|
+
.getStorageKeys()
|
|
1948
|
+
.pipe(switchMap((keys) => forkJoin(keys
|
|
1949
|
+
.filter((key) => key.startsWith(this.#STORAGE_KEY_FORMS))
|
|
1950
|
+
.map((key) => this.#appCache.removeItem(key).pipe(catchError(() => of(null)))))))
|
|
1951
|
+
.subscribe();
|
|
1952
|
+
}
|
|
2041
1953
|
/**
|
|
2042
|
-
*
|
|
2043
|
-
* @param
|
|
2044
|
-
* @returns Observable of a SearchResult
|
|
1954
|
+
* Check whether or not the model has at least one form element. Recursive.
|
|
1955
|
+
* @param element Form element to check child elements for
|
|
2045
1956
|
*/
|
|
2046
|
-
|
|
2047
|
-
|
|
1957
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1958
|
+
#formHasElements(element) {
|
|
1959
|
+
let hasElement = false;
|
|
1960
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1961
|
+
element.elements?.forEach((element) => {
|
|
1962
|
+
if (!['o2mGroup', 'o2mGroupStack'].includes(element.type)) {
|
|
1963
|
+
hasElement = true;
|
|
1964
|
+
}
|
|
1965
|
+
else if (!hasElement) {
|
|
1966
|
+
hasElement = this.#formHasElements(element);
|
|
1967
|
+
}
|
|
1968
|
+
});
|
|
1969
|
+
return hasElement;
|
|
2048
1970
|
}
|
|
2049
1971
|
/**
|
|
2050
|
-
*
|
|
2051
|
-
* @param
|
|
2052
|
-
* @returns Observable of the raw search result
|
|
1972
|
+
* Generate a default form model for an object type. This is used when the backend does not return a form model.
|
|
1973
|
+
* @param typeId Object type ID or SOT ID
|
|
2053
1974
|
*/
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
1975
|
+
#generateDefaultFormModel(typeId) {
|
|
1976
|
+
const type = this.getObjectType(typeId) || this.getSecondaryObjectType(typeId);
|
|
1977
|
+
if (type) {
|
|
1978
|
+
return {
|
|
1979
|
+
id: type.id,
|
|
1980
|
+
label: this.#localization.getLocalizedLabel(type.id),
|
|
1981
|
+
elements: type.fields.map((field) => this.toFormElement(field)),
|
|
1982
|
+
title: this.#localization.getLocalizedLabel(type.id),
|
|
1983
|
+
description: this.#localization.getLocalizedDescription(type.id)
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
return null;
|
|
2058
1987
|
}
|
|
2059
1988
|
/**
|
|
2060
|
-
*
|
|
2061
|
-
*
|
|
1989
|
+
* Resolve all the fields for an object type. This also includes secondary object types and the fields inherited from
|
|
1990
|
+
* the base type (... and of course the base type (and its secondary object types) of the base type and so on)
|
|
1991
|
+
* @param schemaTypeDefinition object type definition from the native schema
|
|
1992
|
+
* @param propertiesQA Quick access object of all properties
|
|
1993
|
+
* @param objectTypesQA Quick access object of all object types
|
|
1994
|
+
* @param raw If set to 'true' only the properties of the object type itself will be returned (without SOTs)
|
|
1995
|
+
*/
|
|
1996
|
+
#resolveObjectTypeFields(schemaTypeDefinition, propertiesQA, objectTypesQA) {
|
|
1997
|
+
const objectTypeFieldIDs = schemaTypeDefinition.propertyReference.map((propertyRef) => propertyRef.value);
|
|
1998
|
+
if (schemaTypeDefinition.secondaryObjectTypeId) {
|
|
1999
|
+
schemaTypeDefinition.secondaryObjectTypeId
|
|
2000
|
+
.filter((sot) => sot.static)
|
|
2001
|
+
.map((sot) => sot.value)
|
|
2002
|
+
.forEach((sotID) => objectTypesQA[sotID].propertyReference.forEach((propertyRef) => objectTypeFieldIDs.push(propertyRef.value)));
|
|
2003
|
+
}
|
|
2004
|
+
let fields = objectTypeFieldIDs.map((id) => ({
|
|
2005
|
+
...propertiesQA[id],
|
|
2006
|
+
_internalType: this.getInternalFormElementType(propertiesQA[id].propertyType, propertiesQA[id].classifications)
|
|
2007
|
+
}));
|
|
2008
|
+
// also resolve properties of the base type
|
|
2009
|
+
if (schemaTypeDefinition.baseId !== schemaTypeDefinition.id && !!objectTypesQA[schemaTypeDefinition.baseId]) {
|
|
2010
|
+
fields = fields.concat(this.#resolveObjectTypeFields(objectTypesQA[schemaTypeDefinition.baseId], propertiesQA, objectTypesQA));
|
|
2011
|
+
}
|
|
2012
|
+
return fields;
|
|
2013
|
+
}
|
|
2014
|
+
#isCreatable(objectTypeId) {
|
|
2015
|
+
return ![SystemType.FOLDER, SystemType.DOCUMENT].includes(objectTypeId);
|
|
2016
|
+
}
|
|
2017
|
+
/**
|
|
2018
|
+
* Actually fetch the system definition from the backend.
|
|
2019
|
+
* @param user User to fetch definition for
|
|
2020
|
+
*/
|
|
2021
|
+
#fetchSystemDefinition(authData) {
|
|
2022
|
+
return (authData ? of(authData) : this.#appCache.getItem(this.#STORAGE_KEY_AUTH_DATA)).pipe(switchMap((data) => {
|
|
2023
|
+
this.updateAuthData(data).subscribe();
|
|
2024
|
+
const fetchTasks = [this.#backend.get('/dms/schema/native.json', ApiBase.core)];
|
|
2025
|
+
return forkJoin(fetchTasks);
|
|
2026
|
+
}), catchError((error) => {
|
|
2027
|
+
this.#logger.error('Error fetching recent version of system definition from server.', error);
|
|
2028
|
+
this.#systemSource.error('Error fetching recent version of system definition from server.');
|
|
2029
|
+
return of(null);
|
|
2030
|
+
}), map((data) => {
|
|
2031
|
+
if (data?.length) {
|
|
2032
|
+
this.setSchema(data[0]);
|
|
2033
|
+
}
|
|
2034
|
+
return !!data;
|
|
2035
|
+
}));
|
|
2036
|
+
}
|
|
2037
|
+
#resolveClassifications(objectTypeId) {
|
|
2038
|
+
let classifications = [];
|
|
2039
|
+
const objectType = this.getObjectType(objectTypeId);
|
|
2040
|
+
if (objectType) {
|
|
2041
|
+
classifications = objectType.classification || [];
|
|
2042
|
+
const staticSOTs = objectType.secondaryObjectTypes
|
|
2043
|
+
? objectType.secondaryObjectTypes.filter((sot) => sot.static).map((sot) => sot.id)
|
|
2044
|
+
: [];
|
|
2045
|
+
staticSOTs.forEach((id) => {
|
|
2046
|
+
const sot = this.getSecondaryObjectType(id);
|
|
2047
|
+
classifications = sot?.classification
|
|
2048
|
+
? [
|
|
2049
|
+
...classifications,
|
|
2050
|
+
...sot.classification.filter((classification) => {
|
|
2051
|
+
// also filter classifications that should not be inherited
|
|
2052
|
+
return (classification !== ObjectTypeClassification.CREATE_FALSE &&
|
|
2053
|
+
classification !== ObjectTypeClassification.SEARCH_FALSE);
|
|
2054
|
+
})
|
|
2055
|
+
]
|
|
2056
|
+
: classifications;
|
|
2057
|
+
});
|
|
2058
|
+
this.#resolvedClassificationsCache[objectTypeId] = classifications;
|
|
2059
|
+
}
|
|
2060
|
+
return classifications;
|
|
2061
|
+
}
|
|
2062
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: SystemService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2063
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: SystemService, providedIn: 'root' }); }
|
|
2064
|
+
}
|
|
2065
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: SystemService, decorators: [{
|
|
2066
|
+
type: Injectable,
|
|
2067
|
+
args: [{
|
|
2068
|
+
providedIn: 'root'
|
|
2069
|
+
}]
|
|
2070
|
+
}] });
|
|
2071
|
+
|
|
2072
|
+
class SearchService {
|
|
2073
|
+
#backend = inject(BackendService);
|
|
2074
|
+
#system = inject(SystemService);
|
|
2075
|
+
static { this.DEFAULT_QUERY_SIZE = 50; }
|
|
2076
|
+
/**
|
|
2077
|
+
* Execute a search query ans transform the result to a SearchResult object
|
|
2078
|
+
* @param query The search query
|
|
2079
|
+
* @returns Observable of a SearchResult
|
|
2080
|
+
*/
|
|
2081
|
+
search(query) {
|
|
2082
|
+
return this.searchRaw(query).pipe(map((res) => this.toSearchResult(res, query.size || SearchService.DEFAULT_QUERY_SIZE, query.from || 0)));
|
|
2083
|
+
}
|
|
2084
|
+
/**
|
|
2085
|
+
* Execute a raw search query and return the result as is.
|
|
2086
|
+
* @param query The search query
|
|
2087
|
+
* @returns Observable of the raw search result
|
|
2088
|
+
*/
|
|
2089
|
+
searchRaw(query) {
|
|
2090
|
+
if (!query.size)
|
|
2091
|
+
query.size = SearchService.DEFAULT_QUERY_SIZE;
|
|
2092
|
+
return this.#backend.post(`/dms/objects/search`, query);
|
|
2093
|
+
}
|
|
2094
|
+
/**
|
|
2095
|
+
* Search for objects in the dms using CMIS like SQL syntax.
|
|
2096
|
+
* @param statement The query statement
|
|
2062
2097
|
* @param size The number of items to return
|
|
2063
2098
|
* @returns Observable of a SearchResult
|
|
2064
2099
|
*/
|
|
@@ -2433,36 +2468,36 @@ var YuvEventType;
|
|
|
2433
2468
|
* Service providing user account configurations.
|
|
2434
2469
|
*/
|
|
2435
2470
|
class UserService {
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
constructor(backend, translate, logger, system, eventService, config) {
|
|
2446
|
-
this.backend = backend;
|
|
2447
|
-
this.translate = translate;
|
|
2448
|
-
this.logger = logger;
|
|
2449
|
-
this.system = system;
|
|
2450
|
-
this.eventService = eventService;
|
|
2451
|
-
this.config = config;
|
|
2471
|
+
constructor() {
|
|
2472
|
+
this.#backend = inject(BackendService);
|
|
2473
|
+
this.#translate = inject(TranslateService);
|
|
2474
|
+
this.#logger = inject(Logger);
|
|
2475
|
+
this.#system = inject(SystemService);
|
|
2476
|
+
this.#localization = inject(LocalizationService);
|
|
2477
|
+
this.#eventService = inject(EventService);
|
|
2478
|
+
this.#config = inject(ConfigService);
|
|
2479
|
+
this.#document = inject(DOCUMENT);
|
|
2452
2480
|
this.#USERS_SETTINGS = '/users/settings/';
|
|
2453
2481
|
this.#DEFAULT_SETTINGS = '/users/settings';
|
|
2454
2482
|
this.#SETTINGS_SECTION_OBJECTCONFIG = 'object-config';
|
|
2455
2483
|
this.#userSource = new BehaviorSubject(this.#user);
|
|
2456
2484
|
this.user$ = this.#userSource.asObservable();
|
|
2457
2485
|
this.globalSettings = new Map();
|
|
2458
|
-
this.#document = inject(DOCUMENT);
|
|
2459
2486
|
this.canCreateObjects = false;
|
|
2460
2487
|
}
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2488
|
+
#backend;
|
|
2489
|
+
#translate;
|
|
2490
|
+
#logger;
|
|
2491
|
+
#system;
|
|
2492
|
+
#localization;
|
|
2493
|
+
#eventService;
|
|
2494
|
+
#config;
|
|
2495
|
+
#document;
|
|
2496
|
+
#USERS_SETTINGS;
|
|
2497
|
+
#DEFAULT_SETTINGS;
|
|
2498
|
+
#SETTINGS_SECTION_OBJECTCONFIG;
|
|
2499
|
+
#user;
|
|
2500
|
+
#userSource;
|
|
2466
2501
|
/**
|
|
2467
2502
|
* Set a new current user
|
|
2468
2503
|
* @param user The user to be set as current user
|
|
@@ -2485,17 +2520,17 @@ class UserService {
|
|
|
2485
2520
|
return this.hasAdminRole || this.hasSystemRole;
|
|
2486
2521
|
}
|
|
2487
2522
|
get hasManageSettingsRole() {
|
|
2488
|
-
const customRole = this
|
|
2523
|
+
const customRole = this.#config.get('core.permissions.manageSettingsRole');
|
|
2489
2524
|
const manageSettingsRole = customRole || AdministrationRoles.MANAGE_SETTINGS;
|
|
2490
2525
|
return this.#user?.authorities?.includes(manageSettingsRole) || false;
|
|
2491
2526
|
}
|
|
2492
2527
|
get isAdvancedUser() {
|
|
2493
|
-
const customRole = this
|
|
2528
|
+
const customRole = this.#config.get('core.permissions.advancedUserRole');
|
|
2494
2529
|
const advancedUserRole = customRole || AdministrationRoles.MANAGE_SETTINGS;
|
|
2495
2530
|
return this.#user?.authorities?.includes(advancedUserRole) || false;
|
|
2496
2531
|
}
|
|
2497
2532
|
get isRetentionManager() {
|
|
2498
|
-
const customRole = this
|
|
2533
|
+
const customRole = this.#config.get('core.permissions.retentionManagerRole');
|
|
2499
2534
|
const retenetionManagerRole = customRole || AdministrationRoles.MANAGE_SETTINGS;
|
|
2500
2535
|
return this.#user?.authorities?.includes(retenetionManagerRole) || false;
|
|
2501
2536
|
}
|
|
@@ -2505,29 +2540,30 @@ class UserService {
|
|
|
2505
2540
|
*/
|
|
2506
2541
|
changeClientLocale(iso, persist = true) {
|
|
2507
2542
|
if (this.#user) {
|
|
2508
|
-
const languages = this
|
|
2509
|
-
iso = iso || this.#user.getClientLocale(this
|
|
2543
|
+
const languages = this.#config.getClientLocales().map((lang) => lang.iso);
|
|
2544
|
+
iso = iso || this.#user.getClientLocale(this.#config.getDefaultClientLocale());
|
|
2510
2545
|
if (!languages.includes(iso)) {
|
|
2511
|
-
iso = this
|
|
2546
|
+
iso = this.#config.getDefaultClientLocale();
|
|
2512
2547
|
}
|
|
2513
|
-
this
|
|
2514
|
-
this
|
|
2515
|
-
this.#user.uiDirection = this
|
|
2548
|
+
this.#logger.debug("Changed client locale to '" + iso + "'");
|
|
2549
|
+
this.#backend.setHeader('Accept-Language', iso);
|
|
2550
|
+
this.#user.uiDirection = this.#getUiDirection(iso);
|
|
2516
2551
|
this.#document.body.dir = this.#user.uiDirection;
|
|
2517
2552
|
this.#document.documentElement.lang = iso;
|
|
2518
2553
|
this.#user.userSettings.locale = iso;
|
|
2519
|
-
if (this
|
|
2520
|
-
const
|
|
2554
|
+
if (this.#translate.getCurrentLang() !== iso || this.#system.authData?.language !== iso) {
|
|
2555
|
+
const obs = persist
|
|
2521
2556
|
? forkJoin([
|
|
2522
|
-
this
|
|
2523
|
-
this
|
|
2557
|
+
this.#translate.use(iso),
|
|
2558
|
+
this.#system
|
|
2559
|
+
.updateAuthData({ language: iso })
|
|
2560
|
+
.pipe(switchMap(() => this.#localization.fetchLocalizations())),
|
|
2524
2561
|
this.saveUserSettings(this.#user.userSettings).pipe(tap(() => {
|
|
2525
|
-
|
|
2526
|
-
this.logger.debug('Loading system definitions i18n resources for new locale.');
|
|
2562
|
+
this.#logger.debug('Loading system definitions i18n resources for new locale.');
|
|
2527
2563
|
}))
|
|
2528
2564
|
])
|
|
2529
|
-
: this
|
|
2530
|
-
|
|
2565
|
+
: this.#translate.use(iso);
|
|
2566
|
+
obs.subscribe(() => this.#eventService.trigger(YuvEventType.CLIENT_LOCALE_CHANGED, iso));
|
|
2531
2567
|
}
|
|
2532
2568
|
}
|
|
2533
2569
|
}
|
|
@@ -2535,47 +2571,19 @@ class UserService {
|
|
|
2535
2571
|
if (this.#user) {
|
|
2536
2572
|
//console.log(this.#user.userSettings);
|
|
2537
2573
|
this.#user.userSettings = { ...this.#user.userSettings, ...settings };
|
|
2538
|
-
return this
|
|
2574
|
+
return this.#backend
|
|
2575
|
+
.post(this.#DEFAULT_SETTINGS, this.#user.userSettings)
|
|
2576
|
+
.pipe(tap(() => this.#userSource.next(this.#user)));
|
|
2539
2577
|
}
|
|
2540
2578
|
else
|
|
2541
2579
|
return of(null);
|
|
2542
2580
|
}
|
|
2543
2581
|
fetchUserSettings() {
|
|
2544
|
-
return this
|
|
2545
|
-
this
|
|
2546
|
-
return this
|
|
2582
|
+
return this.#backend.get('/dms/permissions').pipe(catchError(() => of(undefined)), switchMap((res) => {
|
|
2583
|
+
this.#setUserPermissions(res);
|
|
2584
|
+
return this.#backend.get(this.#DEFAULT_SETTINGS);
|
|
2547
2585
|
}));
|
|
2548
2586
|
}
|
|
2549
|
-
setUserPermissions(res) {
|
|
2550
|
-
this.userPermissions = {
|
|
2551
|
-
create: this.mapPermissions('CREATE', res),
|
|
2552
|
-
write: this.mapPermissions('WRITE', res),
|
|
2553
|
-
read: this.mapPermissions('READ', res),
|
|
2554
|
-
delete: this.mapPermissions('DELETE', res)
|
|
2555
|
-
};
|
|
2556
|
-
const sp = {
|
|
2557
|
-
createableObjectTypes: [
|
|
2558
|
-
...this.userPermissions.create.folderTypes,
|
|
2559
|
-
...this.userPermissions.create.objectTypes,
|
|
2560
|
-
...this.userPermissions.create.secondaryObjectTypes
|
|
2561
|
-
],
|
|
2562
|
-
searchableObjectTypes: [
|
|
2563
|
-
...this.userPermissions.read.folderTypes,
|
|
2564
|
-
...this.userPermissions.read.objectTypes,
|
|
2565
|
-
...this.userPermissions.read.secondaryObjectTypes
|
|
2566
|
-
]
|
|
2567
|
-
};
|
|
2568
|
-
this.system.setPermissions(sp);
|
|
2569
|
-
this.canCreateObjects = sp.createableObjectTypes.length > 0;
|
|
2570
|
-
}
|
|
2571
|
-
mapPermissions(section, apiResponse) {
|
|
2572
|
-
const res = apiResponse[section] || {};
|
|
2573
|
-
return {
|
|
2574
|
-
folderTypes: res['folderTypeIds'] || [],
|
|
2575
|
-
objectTypes: res['objectTypeIds'] || [],
|
|
2576
|
-
secondaryObjectTypes: res['secondaryObjectTypeIds'] || []
|
|
2577
|
-
};
|
|
2578
|
-
}
|
|
2579
2587
|
/**
|
|
2580
2588
|
* Search for a user based on a search term
|
|
2581
2589
|
* @param term Search term
|
|
@@ -2585,25 +2593,62 @@ class UserService {
|
|
|
2585
2593
|
queryUser(term, excludeMe, roles) {
|
|
2586
2594
|
let params = new HttpParams().set('search', term).set('excludeMe', `${!!excludeMe}`);
|
|
2587
2595
|
roles?.length && roles.map((r) => (params = params.append(`roles`, r)));
|
|
2588
|
-
return this
|
|
2596
|
+
return this.#backend
|
|
2597
|
+
.get(`/idm/users?${params}`)
|
|
2598
|
+
.pipe(map((users) => (!users ? [] : users.map((u) => new YuvUser(u)))));
|
|
2589
2599
|
}
|
|
2590
2600
|
getUserById(id) {
|
|
2591
|
-
return this
|
|
2601
|
+
return this.#backend.get(`/idm/users/${id}`).pipe(map((user) => new YuvUser(user, this.#user.userSettings)));
|
|
2592
2602
|
}
|
|
2593
2603
|
logout(redirRoute) {
|
|
2594
2604
|
const redir = redirRoute ? `?redir=${redirRoute}` : '';
|
|
2595
|
-
window.location.href = `${this
|
|
2605
|
+
window.location.href = `${this.#backend.getApiBase('logout')}${redir}`;
|
|
2596
2606
|
}
|
|
2597
2607
|
getSettings(section) {
|
|
2598
|
-
return this.#user ? this
|
|
2608
|
+
return this.#user ? this.#backend.get(this.#USERS_SETTINGS + encodeURIComponent(section)) : of(null);
|
|
2599
2609
|
}
|
|
2600
2610
|
saveObjectConfig(objectConfigs) {
|
|
2601
|
-
return this
|
|
2611
|
+
return this.#backend.post(this.#USERS_SETTINGS + encodeURIComponent(this.#SETTINGS_SECTION_OBJECTCONFIG), objectConfigs);
|
|
2602
2612
|
}
|
|
2603
2613
|
loadObjectConfig() {
|
|
2604
2614
|
return this.getSettings(this.#SETTINGS_SECTION_OBJECTCONFIG).pipe(catchError(() => of(undefined)));
|
|
2605
2615
|
}
|
|
2606
|
-
|
|
2616
|
+
#getUiDirection(iso) {
|
|
2617
|
+
// languages that are read right to left
|
|
2618
|
+
const rtlLanguages = ['ar', 'arc', 'dv', 'fa', 'ha', 'he', 'khw', 'ks', 'ku', 'ps', 'ur', 'yi'];
|
|
2619
|
+
return !rtlLanguages.includes(iso) ? Direction.LTR : Direction.RTL;
|
|
2620
|
+
}
|
|
2621
|
+
#setUserPermissions(res) {
|
|
2622
|
+
this.userPermissions = {
|
|
2623
|
+
create: this.#mapPermissions('CREATE', res),
|
|
2624
|
+
write: this.#mapPermissions('WRITE', res),
|
|
2625
|
+
read: this.#mapPermissions('READ', res),
|
|
2626
|
+
delete: this.#mapPermissions('DELETE', res)
|
|
2627
|
+
};
|
|
2628
|
+
const permissions = {
|
|
2629
|
+
createableObjectTypes: [
|
|
2630
|
+
...this.userPermissions.create.folderTypes,
|
|
2631
|
+
...this.userPermissions.create.objectTypes,
|
|
2632
|
+
...this.userPermissions.create.secondaryObjectTypes
|
|
2633
|
+
],
|
|
2634
|
+
searchableObjectTypes: [
|
|
2635
|
+
...this.userPermissions.read.folderTypes,
|
|
2636
|
+
...this.userPermissions.read.objectTypes,
|
|
2637
|
+
...this.userPermissions.read.secondaryObjectTypes
|
|
2638
|
+
]
|
|
2639
|
+
};
|
|
2640
|
+
this.#system.setPermissions(permissions);
|
|
2641
|
+
this.canCreateObjects = permissions.createableObjectTypes.length > 0;
|
|
2642
|
+
}
|
|
2643
|
+
#mapPermissions(section, apiResponse) {
|
|
2644
|
+
const res = apiResponse[section] || {};
|
|
2645
|
+
return {
|
|
2646
|
+
folderTypes: res['folderTypeIds'] || [],
|
|
2647
|
+
objectTypes: res['objectTypeIds'] || [],
|
|
2648
|
+
secondaryObjectTypes: res['secondaryObjectTypeIds'] || []
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2651
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UserService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2607
2652
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UserService, providedIn: 'root' }); }
|
|
2608
2653
|
}
|
|
2609
2654
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UserService, decorators: [{
|
|
@@ -2611,7 +2656,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
2611
2656
|
args: [{
|
|
2612
2657
|
providedIn: 'root'
|
|
2613
2658
|
}]
|
|
2614
|
-
}]
|
|
2659
|
+
}] });
|
|
2615
2660
|
|
|
2616
2661
|
/**
|
|
2617
2662
|
* Service providing access to the systems audit entries. Audits can be seen as the history of
|
|
@@ -3057,6 +3102,7 @@ class AuthService {
|
|
|
3057
3102
|
#objectConfigService;
|
|
3058
3103
|
#appCache;
|
|
3059
3104
|
#systemService;
|
|
3105
|
+
#localizationService;
|
|
3060
3106
|
#backend;
|
|
3061
3107
|
#INITIAL_REQUEST_STORAGE_KEY;
|
|
3062
3108
|
#USER_FETCH_URI;
|
|
@@ -3072,6 +3118,7 @@ class AuthService {
|
|
|
3072
3118
|
this.#objectConfigService = inject(ObjectConfigService);
|
|
3073
3119
|
this.#appCache = inject(AppCacheService);
|
|
3074
3120
|
this.#systemService = inject(SystemService);
|
|
3121
|
+
this.#localizationService = inject(LocalizationService);
|
|
3075
3122
|
this.#backend = inject(BackendService);
|
|
3076
3123
|
this.#INITIAL_REQUEST_STORAGE_KEY = 'yuv.core.auth.initialrequest';
|
|
3077
3124
|
this.#USER_FETCH_URI = '/idm/whoami';
|
|
@@ -3157,7 +3204,10 @@ class AuthService {
|
|
|
3157
3204
|
tenant: currentUser.tenant,
|
|
3158
3205
|
language: currentUser.getClientLocale()
|
|
3159
3206
|
};
|
|
3160
|
-
return
|
|
3207
|
+
return forkJoin([
|
|
3208
|
+
this.#localizationService.fetchLocalizations(),
|
|
3209
|
+
this.#systemService.getSystemDefinition(this.#authData)
|
|
3210
|
+
]).pipe(switchMap(() => this.#objectConfigService.init()), map(() => currentUser));
|
|
3161
3211
|
}));
|
|
3162
3212
|
}
|
|
3163
3213
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AuthService, deps: [{ token: CORE_CONFIG }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
@@ -3224,1294 +3274,1518 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
3224
3274
|
}]
|
|
3225
3275
|
}] });
|
|
3226
3276
|
|
|
3227
|
-
|
|
3228
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: CatalogService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
3229
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: CatalogService, providedIn: 'root' }); }
|
|
3230
|
-
}
|
|
3231
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: CatalogService, decorators: [{
|
|
3232
|
-
type: Injectable,
|
|
3233
|
-
args: [{
|
|
3234
|
-
providedIn: 'root'
|
|
3235
|
-
}]
|
|
3236
|
-
}] });
|
|
3277
|
+
const LOCALIZATION_COLUMNS = ['locale', 'label', 'description'];
|
|
3237
3278
|
|
|
3279
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
3280
|
+
const transformResponse = () => map((res) => (res.body ? res.body.objects.map((val) => val) : null));
|
|
3238
3281
|
/**
|
|
3239
|
-
* Service for
|
|
3240
|
-
* Provides methods to get, update, and invalidate cache entries.
|
|
3282
|
+
* Service for providing upload of different object types into a client.
|
|
3241
3283
|
*/
|
|
3242
|
-
class
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
if (!expired || keepIfExpired) {
|
|
3256
|
-
return cachedItem.data;
|
|
3257
|
-
}
|
|
3258
|
-
else {
|
|
3259
|
-
this.invalidateCache(id);
|
|
3260
|
-
return null;
|
|
3261
|
-
}
|
|
3262
|
-
}
|
|
3263
|
-
return null;
|
|
3264
|
-
}));
|
|
3284
|
+
class UploadService {
|
|
3285
|
+
constructor() {
|
|
3286
|
+
// #region Dependencies
|
|
3287
|
+
this.#backend = inject(BackendService);
|
|
3288
|
+
this.#http = inject(HttpClient);
|
|
3289
|
+
this.#logger = inject(Logger);
|
|
3290
|
+
//#endregion
|
|
3291
|
+
//#region Properties
|
|
3292
|
+
this.#status = { err: 0, items: [] };
|
|
3293
|
+
this.#statusSource = new ReplaySubject();
|
|
3294
|
+
this.#uploadStatus = new BehaviorSubject(null);
|
|
3295
|
+
this.status$ = this.#statusSource.pipe(scan((acc, newVal) => ({ ...acc, ...newVal }), this.#status));
|
|
3296
|
+
this.uploadStatus$ = this.#uploadStatus.asObservable();
|
|
3265
3297
|
}
|
|
3298
|
+
// #region Dependencies
|
|
3299
|
+
#backend;
|
|
3300
|
+
#http;
|
|
3301
|
+
#logger;
|
|
3302
|
+
//#endregion
|
|
3303
|
+
//#region Properties
|
|
3304
|
+
#status;
|
|
3305
|
+
#statusSource;
|
|
3306
|
+
#uploadStatus;
|
|
3307
|
+
//#endregion
|
|
3308
|
+
//#region Public Methods
|
|
3266
3309
|
/**
|
|
3267
|
-
*
|
|
3268
|
-
* @param
|
|
3269
|
-
* @param
|
|
3270
|
-
* @param
|
|
3310
|
+
* Upload a file.
|
|
3311
|
+
* @param url The URL to upload the file to
|
|
3312
|
+
* @param file The file to be uploaded
|
|
3313
|
+
* @param label A label that will show up in the upload overlay dialog while uploading
|
|
3314
|
+
*
|
|
3315
|
+
* @deprecated use uploadFile instead. `label`, `silent` as well as `scope` can be provided through `options`.
|
|
3271
3316
|
*/
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
return this.#appCacheService.setItem(this.#CLIENT_CACHE_PREFIX + id, cacheEntry).pipe(map$1(() => cacheEntry));
|
|
3317
|
+
upload(url, file, label, silent) {
|
|
3318
|
+
return this.#executeUpload(url, file, { label: label || file.name, silent: silent });
|
|
3275
3319
|
}
|
|
3276
3320
|
/**
|
|
3277
|
-
*
|
|
3278
|
-
*
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3321
|
+
* Uploads a single file to the specified URL using the structured `FileUploadOptions` object.
|
|
3322
|
+
*
|
|
3323
|
+
* This is the preferred alternative to the legacy `upload()` method. It accepts a typed
|
|
3324
|
+
* options object instead of individual parameters, which makes it easier to pass optional
|
|
3325
|
+
* settings such as `silent` mode and `scope` without relying on positional arguments.
|
|
3326
|
+
*
|
|
3327
|
+
* **Behavior:**
|
|
3328
|
+
* - If `options.silent` is `false` (default), the upload appears in the upload overlay dialog
|
|
3329
|
+
* and progress is tracked via `status$`
|
|
3330
|
+
* - If `options.silent` is `true`, the upload runs quietly in the background with no UI feedback
|
|
3331
|
+
* - `options.label` is shown in the overlay dialog during upload; falls back to `file.name` if omitted
|
|
3332
|
+
* - `options.scope` tags the upload with a named scope, enabling an `UploadProgressComponent`
|
|
3333
|
+
* configured with the same scope to display only the uploads relevant to its section of the UI
|
|
3334
|
+
*
|
|
3335
|
+
* @param url - The backend URL to POST the file to
|
|
3336
|
+
* @param file - The `File` object to be uploaded
|
|
3337
|
+
* @param options - Upload configuration: label, silent mode, and optional scope
|
|
3338
|
+
* @returns An `Observable` that emits the server response on completion
|
|
3339
|
+
*
|
|
3340
|
+
* @example
|
|
3341
|
+
* ```typescript
|
|
3342
|
+
* // Standard upload with progress indicator
|
|
3343
|
+
* this.uploadService.uploadFile(url, file, { label: 'Uploading contract.pdf' }).subscribe();
|
|
3344
|
+
*
|
|
3345
|
+
* // Silent upload (no UI, runs in background)
|
|
3346
|
+
* this.uploadService.uploadFile(url, file, { label: 'document.pdf', silent: true }).subscribe();
|
|
3347
|
+
*
|
|
3348
|
+
* // Scoped upload — only the UploadProgressComponent with scope 'profile-editor' will show this
|
|
3349
|
+
* this.uploadService.uploadFile(url, file, { label: 'photo.jpg', scope: 'profile-editor' }).subscribe();
|
|
3350
|
+
* ```
|
|
3351
|
+
*/
|
|
3352
|
+
uploadFile(url, file, options) {
|
|
3353
|
+
return this.#executeUpload(url, file, options);
|
|
3282
3354
|
}
|
|
3283
3355
|
/**
|
|
3284
|
-
*
|
|
3356
|
+
* Upload files using multipart upload.
|
|
3357
|
+
* @param url The URL to upload the files to
|
|
3358
|
+
* @param files The files to be uploaded
|
|
3359
|
+
* @param data Data to be send along with the files
|
|
3360
|
+
* @param label A label that will show up in the upload overlay dialog while uploading
|
|
3361
|
+
*
|
|
3362
|
+
* @deprecated Use multipartUpload(url, files, options, data) instead. Provide scope through options.
|
|
3285
3363
|
*/
|
|
3286
|
-
|
|
3287
|
-
return this.#
|
|
3364
|
+
uploadMultipart(url, files, data, label, silent) {
|
|
3365
|
+
return this.#executeMultipartUpload(url, files, { label, silent }, data);
|
|
3288
3366
|
}
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3367
|
+
/**
|
|
3368
|
+
* Uploads multiple files to the specified URL using a multipart/form-data request.
|
|
3369
|
+
*
|
|
3370
|
+
* This is the preferred method for multi-file uploads. It accepts a typed `FileUploadOptions`
|
|
3371
|
+
* object for configuration, making it easier to pass `scope` and other options compared
|
|
3372
|
+
* to the legacy `uploadMultipart()` overload.
|
|
3373
|
+
*
|
|
3374
|
+
* **Behavior:**
|
|
3375
|
+
* - All files are bundled into a single `FormData` payload under the `files` field
|
|
3376
|
+
* - Optional `data` is appended as a JSON blob under the `data` field
|
|
3377
|
+
* - If `options.silent` is `false` (default), upload progress is shown in the overlay dialog
|
|
3378
|
+
* - If `options.silent` is `true`, the upload runs in the background with no UI feedback
|
|
3379
|
+
* - `options.scope` tags the upload with a named scope, enabling an `UploadProgressComponent`
|
|
3380
|
+
* configured with the same scope to display only the uploads relevant to its section of the UI
|
|
3381
|
+
*
|
|
3382
|
+
* @param url - The backend URL to POST the multipart request to
|
|
3383
|
+
* @param files - Array of `File` objects to upload
|
|
3384
|
+
* @param options - Upload configuration: label (required), silent mode, and optional scope
|
|
3385
|
+
* @param data - Optional metadata object to send alongside the files (serialized as JSON)
|
|
3386
|
+
* @returns An `Observable` that emits the server response on completion
|
|
3387
|
+
*
|
|
3388
|
+
* @example
|
|
3389
|
+
* ```typescript
|
|
3390
|
+
* // Upload files with a visible progress indicator
|
|
3391
|
+
* this.uploadService.multipartUpload(url, files, { label: 'Uploading 3 files' }).subscribe();
|
|
3392
|
+
*
|
|
3393
|
+
* // Upload files with additional metadata
|
|
3394
|
+
* this.uploadService
|
|
3395
|
+
* .multipartUpload(url, files, { label: 'Batch upload' }, { folderId: 'abc123' })
|
|
3396
|
+
* .subscribe();
|
|
3397
|
+
*
|
|
3398
|
+
* // Scoped upload — only the UploadProgressComponent with scope 'mail-editor' will show this
|
|
3399
|
+
* this.uploadService
|
|
3400
|
+
* .multipartUpload(url, files, { label: 'attachments', scope: 'mail-editor' })
|
|
3401
|
+
* .subscribe();
|
|
3402
|
+
* ```
|
|
3403
|
+
*/
|
|
3404
|
+
multipartUpload(url, files, options, data) {
|
|
3405
|
+
return this.#executeMultipartUpload(url, files, options, data);
|
|
3310
3406
|
}
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3407
|
+
/**
|
|
3408
|
+
* Creates a document on the server by sending structured metadata without a file attachment.
|
|
3409
|
+
*
|
|
3410
|
+
* Unlike `uploadFile()` and `multipartUpload()`, this method does not attach any file content.
|
|
3411
|
+
* It is intended for creating document objects from metadata alone — for example, creating
|
|
3412
|
+
* a folder, a placeholder record, or a document entry where the content stream will be
|
|
3413
|
+
* provided separately.
|
|
3414
|
+
*
|
|
3415
|
+
* **Behavior:**
|
|
3416
|
+
* - Sends the `data` object as a JSON blob inside a `FormData` payload
|
|
3417
|
+
* - Progress is NOT tracked — this method does not appear in `status$`
|
|
3418
|
+
* - Errors are re-thrown so the caller can handle them via the returned `Observable`
|
|
3419
|
+
*
|
|
3420
|
+
* @param url - The backend URL to POST the document creation request to
|
|
3421
|
+
* @param data - Metadata object describing the document to be created
|
|
3422
|
+
* @returns An `Observable` that emits the created object(s) on success
|
|
3423
|
+
*
|
|
3424
|
+
* @example
|
|
3425
|
+
* ```typescript
|
|
3426
|
+
* const metadata = {
|
|
3427
|
+
* objectTypeId: 'yuv:document',
|
|
3428
|
+
* properties: { 'yuv:title': 'My Document' }
|
|
3429
|
+
* };
|
|
3430
|
+
*
|
|
3431
|
+
* this.uploadService.createDocument(url, metadata).subscribe((result) => {
|
|
3432
|
+
* console.log('Created:', result);
|
|
3433
|
+
* });
|
|
3434
|
+
* ```
|
|
3435
|
+
*/
|
|
3436
|
+
createDocument(url, data) {
|
|
3437
|
+
const formData = this.#createFormData({ data });
|
|
3438
|
+
const request = this.#createHttpRequest(url, { formData }, false);
|
|
3439
|
+
return this.#http.request(request).pipe(filter((obj) => obj?.body), transformResponse(), catchError((err) => throwError(() => err)));
|
|
3319
3440
|
}
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3441
|
+
/**
|
|
3442
|
+
* Cancels one or all active upload requests and removes them from the tracked upload list.
|
|
3443
|
+
*
|
|
3444
|
+
* This method unsubscribes from the underlying HTTP request, effectively aborting the upload,
|
|
3445
|
+
* and removes the corresponding item(s) from the internal status list. The updated status
|
|
3446
|
+
* is then emitted via `status$` so that any subscribed UI components (e.g. the upload overlay)
|
|
3447
|
+
* can reflect the change immediately.
|
|
3448
|
+
*
|
|
3449
|
+
* **Behavior:**
|
|
3450
|
+
* - If `id` is provided, only the matching upload item is cancelled
|
|
3451
|
+
* - If `id` is omitted, **all** active uploads are cancelled at once
|
|
3452
|
+
* - If the provided `id` does not match any active item, the call is a no-op
|
|
3453
|
+
* - After cancellation, the error count on `status$` is recalculated
|
|
3454
|
+
*
|
|
3455
|
+
* @param id - Optional ID of the upload item to cancel. Omit to cancel all active uploads.
|
|
3456
|
+
*
|
|
3457
|
+
* @example
|
|
3458
|
+
* ```typescript
|
|
3459
|
+
* // Cancel a specific upload by ID
|
|
3460
|
+
* this.uploadService.cancelItem(uploadId);
|
|
3461
|
+
*
|
|
3462
|
+
* // Cancel all ongoing uploads (e.g. on component destroy or user confirmation)
|
|
3463
|
+
* this.uploadService.cancelItem();
|
|
3464
|
+
* ```
|
|
3465
|
+
*/
|
|
3466
|
+
cancelItem(id) {
|
|
3467
|
+
if (id) {
|
|
3468
|
+
const match = this.#status.items.find((i) => i.id === id);
|
|
3469
|
+
if (match) {
|
|
3470
|
+
match.subscription.unsubscribe();
|
|
3471
|
+
this.#status.items = this.#status.items.filter((i) => i.id !== id);
|
|
3337
3472
|
}
|
|
3338
|
-
return cd;
|
|
3339
|
-
}), filter$1((cd) => cd !== undefined && (cd.files || cd.objects)), map$1((cd) => cd));
|
|
3340
|
-
}
|
|
3341
|
-
getClipboardData(bucket) {
|
|
3342
|
-
if (bucket) {
|
|
3343
|
-
return this.#clipboard.buckets ? this.#clipboard.buckets[bucket] : undefined;
|
|
3344
3473
|
}
|
|
3345
|
-
else
|
|
3346
|
-
|
|
3474
|
+
else {
|
|
3475
|
+
this.#status.items.forEach((element) => element.subscription.unsubscribe());
|
|
3476
|
+
this.#status.items = [];
|
|
3477
|
+
}
|
|
3478
|
+
this.#status.err = this.#status.items.filter((i) => i.err).length;
|
|
3479
|
+
this.#statusSource.next(this.#status);
|
|
3347
3480
|
}
|
|
3481
|
+
//#endregion
|
|
3482
|
+
//#region Utilities
|
|
3348
3483
|
/**
|
|
3349
|
-
*
|
|
3350
|
-
* @param
|
|
3351
|
-
* If you have an app that would like to have a separate section in the clipboard
|
|
3352
|
-
* you'll use a unique bucket to store your stuff. When observing chages to the
|
|
3353
|
-
* clipboard you couls as well provide the bucket and you will only get
|
|
3484
|
+
* Prepares Formdata for multipart upload.
|
|
3485
|
+
* @param from contains form and or file
|
|
3354
3486
|
*/
|
|
3355
|
-
|
|
3356
|
-
const
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
}
|
|
3362
|
-
else
|
|
3363
|
-
this.#clipboard.main = cd;
|
|
3364
|
-
this.#clipboardSource.next(this.#clipboard);
|
|
3487
|
+
#createFormData({ file, data }) {
|
|
3488
|
+
const formData = new FormData();
|
|
3489
|
+
(file || []).forEach((f) => formData.append('files', f, f.name));
|
|
3490
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
3491
|
+
data ? formData.append('data', new Blob([JSON.stringify(data)], { type: 'application/json' })) : null;
|
|
3492
|
+
return formData;
|
|
3365
3493
|
}
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3494
|
+
/**
|
|
3495
|
+
* Prepares Http Request.
|
|
3496
|
+
* @param url The URL to upload the file to
|
|
3497
|
+
* @param content formdata or single file
|
|
3498
|
+
* @param reportProgress Request should report upload progress
|
|
3499
|
+
* @param method Request method
|
|
3500
|
+
*/
|
|
3501
|
+
#createHttpRequest(url, content, reportProgress, method = 'POST') {
|
|
3502
|
+
const { formData, file } = content;
|
|
3503
|
+
// add request param to bypass the service-worker
|
|
3504
|
+
url += `${!url.includes('?') ? '?' : '&'}ngsw-bypass=1`;
|
|
3505
|
+
let headers = this.#backend.getAuthHeaders();
|
|
3506
|
+
if (file) {
|
|
3507
|
+
headers = headers.set('Content-Disposition', `attachment; filename*=utf-8''${encodeURIComponent(file.name)}`);
|
|
3372
3508
|
}
|
|
3373
|
-
|
|
3509
|
+
return new HttpRequest(method, url, file || formData, { headers, reportProgress });
|
|
3374
3510
|
}
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3511
|
+
/**
|
|
3512
|
+
* Prepares single file POST upload.
|
|
3513
|
+
* @param url The URL to upload the file to
|
|
3514
|
+
* @param file The file to be uploaded
|
|
3515
|
+
* @param label A label that will show up in the upload overlay dialog while uploading
|
|
3516
|
+
*/
|
|
3517
|
+
#executeUpload(url, file, options) {
|
|
3518
|
+
const silent = options.silent === true;
|
|
3519
|
+
const label = options.label || file.name;
|
|
3520
|
+
const request = this.#createHttpRequest(url, { file }, !silent);
|
|
3521
|
+
return silent
|
|
3522
|
+
? this.#http.request(request).pipe(filter((res) => res instanceof HttpResponse))
|
|
3523
|
+
: this.#startUploadWithFile(request, label, options.scope).pipe(transformResponse());
|
|
3524
|
+
}
|
|
3525
|
+
/**
|
|
3526
|
+
* Prepare multipart upload.
|
|
3527
|
+
* @param url The URL to upload the file to
|
|
3528
|
+
* @param files Array of files to be uploaded
|
|
3529
|
+
* @param label A label that will show up in the upload overlay dialog while uploading
|
|
3530
|
+
* @param data Data to be send along with the files
|
|
3531
|
+
*/
|
|
3532
|
+
#executeMultipartUpload(url, files, options, data) {
|
|
3533
|
+
const label = options.label ?? 'Upload';
|
|
3534
|
+
const silent = options.silent === true;
|
|
3535
|
+
const formData = this.#createFormData({ file: files, data });
|
|
3536
|
+
const request = this.#createHttpRequest(url, { formData }, !silent);
|
|
3537
|
+
return silent
|
|
3538
|
+
? this.#http.request(request).pipe(filter((res) => res instanceof HttpResponse))
|
|
3539
|
+
: this.#startUploadWithFile(request, label, options.scope).pipe(transformResponse());
|
|
3540
|
+
}
|
|
3541
|
+
#generateResult(result) {
|
|
3542
|
+
const objects = result.body?.objects;
|
|
3543
|
+
if (objects && objects.length > 1) {
|
|
3544
|
+
const data = objects[0];
|
|
3545
|
+
// const bp = this.#system.getBaseProperties();
|
|
3546
|
+
// const label = data.properties[bp.title] ? data.properties[bp.title].value : '...';
|
|
3547
|
+
// TODO: Get the label from somewhere
|
|
3548
|
+
const label = '...';
|
|
3549
|
+
return [
|
|
3550
|
+
{
|
|
3551
|
+
objectId: objects.map((val) => val.properties[BaseObjectTypeField.OBJECT_ID].value),
|
|
3552
|
+
contentStreamId: data.contentStreams[0]?.contentStreamId,
|
|
3553
|
+
filename: data.contentStreams[0]?.fileName,
|
|
3554
|
+
label: `(${objects.length}) ${label}`
|
|
3555
|
+
}
|
|
3556
|
+
];
|
|
3379
3557
|
}
|
|
3380
|
-
|
|
3381
|
-
|
|
3558
|
+
else {
|
|
3559
|
+
return result.body.objects.map((o) => ({
|
|
3560
|
+
objectId: o.properties[BaseObjectTypeField.OBJECT_ID].value,
|
|
3561
|
+
contentStreamId: o.contentStreams[0]?.contentStreamId,
|
|
3562
|
+
filename: o.contentStreams[0]?.fileName,
|
|
3563
|
+
// label: o.properties[bp.title] ? o.properties[bp.title].value : o.contentStreams![0]?.fileName
|
|
3564
|
+
// TODO: Get the label from somewhere
|
|
3565
|
+
label: o.contentStreams[0]?.fileName
|
|
3566
|
+
}));
|
|
3382
3567
|
}
|
|
3383
3568
|
}
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3569
|
+
#createProgressStatus(event, progress, id) {
|
|
3570
|
+
if (event.type === HttpEventType.UploadProgress) {
|
|
3571
|
+
const fullPercentage = 100;
|
|
3572
|
+
const percentDone = Math.round((fullPercentage * event.loaded) / event.total);
|
|
3573
|
+
progress.next(percentDone);
|
|
3574
|
+
}
|
|
3575
|
+
else if (event instanceof HttpResponse) {
|
|
3576
|
+
progress.complete();
|
|
3577
|
+
// add upload response
|
|
3578
|
+
// this.status.items = this.status.items.filter(s => s.id !== id);
|
|
3579
|
+
const idx = this.#status.items.findIndex((item) => item.id === id);
|
|
3580
|
+
if (idx !== -1) {
|
|
3581
|
+
this.#status.items[idx].result = this.#generateResult(event);
|
|
3582
|
+
this.#statusSource.next(this.#status);
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
#createUploadError(err, progress, id) {
|
|
3587
|
+
const statusItem = this.#status.items.find((item) => item.id === id);
|
|
3588
|
+
statusItem.err = {
|
|
3589
|
+
code: err.status,
|
|
3590
|
+
message: err.error ? err.error.errorMessage : err.message
|
|
3591
|
+
};
|
|
3592
|
+
this.#logger.error('upload failed', statusItem);
|
|
3593
|
+
this.#status.err++;
|
|
3594
|
+
this.#statusSource.next(this.#status);
|
|
3595
|
+
progress.next(0);
|
|
3596
|
+
return throwError(() => new Error(err.message));
|
|
3597
|
+
}
|
|
3404
3598
|
/**
|
|
3405
|
-
*
|
|
3599
|
+
* Actually starts the upload process.
|
|
3600
|
+
* @param request Request to be executed
|
|
3601
|
+
* @param label A label that will show up in the upload overlay dialog while uploading
|
|
3406
3602
|
*/
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3603
|
+
#startUploadWithFile(request, label, scope) {
|
|
3604
|
+
return new Observable((uploadObserver) => {
|
|
3605
|
+
const id = Utils.uuid();
|
|
3606
|
+
const progress = new Subject();
|
|
3607
|
+
let result;
|
|
3608
|
+
// Create a subscription from the http request that will be applied to the upload
|
|
3609
|
+
// status item in order to be able to cancel the request later on.
|
|
3610
|
+
this.#uploadStatus.next(false);
|
|
3611
|
+
const subscription = this.#http
|
|
3612
|
+
.request(request)
|
|
3613
|
+
.pipe(catchError((err) => this.#createUploadError(err, progress, id)), tap((event) => this.#createProgressStatus(event, progress, id)))
|
|
3614
|
+
// actual return value of this function
|
|
3615
|
+
.subscribe({
|
|
3616
|
+
next: (res) => (res.status ? (result = res) : null),
|
|
3617
|
+
error: (err) => {
|
|
3618
|
+
uploadObserver.error(err);
|
|
3619
|
+
this.#uploadStatus.next(true);
|
|
3620
|
+
uploadObserver.complete();
|
|
3621
|
+
},
|
|
3622
|
+
complete: () => {
|
|
3623
|
+
uploadObserver.next(result);
|
|
3624
|
+
this.#uploadStatus.next(true);
|
|
3625
|
+
uploadObserver.complete();
|
|
3626
|
+
}
|
|
3627
|
+
});
|
|
3628
|
+
this.#status.items.push({
|
|
3629
|
+
id,
|
|
3630
|
+
filename: label,
|
|
3631
|
+
progress: progress.asObservable(),
|
|
3632
|
+
subscription,
|
|
3633
|
+
scope: scope,
|
|
3634
|
+
err: undefined
|
|
3635
|
+
});
|
|
3636
|
+
this.#statusSource.next(this.#status);
|
|
3421
3637
|
});
|
|
3422
3638
|
}
|
|
3423
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type:
|
|
3424
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type:
|
|
3639
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UploadService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
3640
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UploadService, providedIn: 'root' }); }
|
|
3425
3641
|
}
|
|
3426
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type:
|
|
3642
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UploadService, decorators: [{
|
|
3427
3643
|
type: Injectable,
|
|
3428
3644
|
args: [{
|
|
3429
3645
|
providedIn: 'root'
|
|
3430
3646
|
}]
|
|
3431
|
-
}]
|
|
3432
|
-
|
|
3433
|
-
/**
|
|
3434
|
-
* Providing functions,that are are injected at application startup and executed during app initialization.
|
|
3435
|
-
*/
|
|
3436
|
-
const init_moduleFnc = () => {
|
|
3437
|
-
const coreConfig = inject(CORE_CONFIG);
|
|
3438
|
-
const logger = inject(Logger);
|
|
3439
|
-
const http = inject(HttpClient);
|
|
3440
|
-
const configService = inject(ConfigService);
|
|
3441
|
-
const authService = inject(AuthService);
|
|
3442
|
-
return authService.setInitialRequestUri().pipe(switchMap(() => coreConfig.main
|
|
3443
|
-
? !Array.isArray(coreConfig.main)
|
|
3444
|
-
? of([coreConfig.main])
|
|
3445
|
-
: forkJoin([...coreConfig.main].map((uri) => http.get(`${Utils.getBaseHref()}${uri}`).pipe(catchError((e) => {
|
|
3446
|
-
logger.error('failed to catch config file', e);
|
|
3447
|
-
return of({});
|
|
3448
|
-
}), map((res) => res)))).pipe(
|
|
3449
|
-
// switchMap((configs: YuvConfig[]) => configService.extendConfig(configs)),
|
|
3450
|
-
tap((configs) => configService.extendConfig(configs)), switchMap(() => authService.initUser().pipe(catchError((e) => {
|
|
3451
|
-
authService.initError = {
|
|
3452
|
-
status: e.status,
|
|
3453
|
-
key: e.error.error
|
|
3454
|
-
};
|
|
3455
|
-
return of(true);
|
|
3456
|
-
}))))
|
|
3457
|
-
: of(false)));
|
|
3458
|
-
};
|
|
3459
|
-
|
|
3460
|
-
var DeviceScreenOrientation;
|
|
3461
|
-
(function (DeviceScreenOrientation) {
|
|
3462
|
-
DeviceScreenOrientation["PORTRAIT"] = "portrait";
|
|
3463
|
-
DeviceScreenOrientation["LANDSCAPE"] = "landscape";
|
|
3464
|
-
})(DeviceScreenOrientation || (DeviceScreenOrientation = {}));
|
|
3647
|
+
}] });
|
|
3465
3648
|
|
|
3466
3649
|
/**
|
|
3467
|
-
*
|
|
3468
|
-
*
|
|
3469
|
-
* This service is used to adapt styles and designs of the client to
|
|
3470
|
-
* different devices and screen sizes.
|
|
3471
|
-
*
|
|
3472
|
-
* Using `screenChange$` observable you are able to monitor changes to
|
|
3473
|
-
* the screen size and act upon it.
|
|
3474
|
-
*
|
|
3475
|
-
* This service will also adds attributes to the body tag that reflect the
|
|
3476
|
-
* current screen/device state. This way you can apply secific styles in your
|
|
3477
|
-
* css files for different screen resolutions and orientations.
|
|
3478
|
-
*
|
|
3479
|
-
* Attributes applied to the body tag are:
|
|
3480
|
-
*
|
|
3481
|
-
* - `data-screen` - [s, m, l, xl] - for different screen sizes
|
|
3482
|
-
* (s: for mobile phone like screen sizes, m: for tablet like screen
|
|
3483
|
-
* sizes, 'l': for desktop like screen sizes, 'xl': for screen sizes exceeding
|
|
3484
|
-
* the desktop screen size).
|
|
3485
|
-
*
|
|
3486
|
-
* - `data-orientation` - [portrait, landscape] - for the current screen orientation
|
|
3487
|
-
*
|
|
3488
|
-
* - `data-touch-enabled` - [true] - if the device has touch capabilities (won't be added if the device doesn't have touch capabilities)
|
|
3489
|
-
*
|
|
3490
|
-
* ```html
|
|
3491
|
-
* <body data-screen-size="s" data-screen-orientation="portrait" data-touch-enabled="true">
|
|
3492
|
-
* ...
|
|
3493
|
-
* </body>
|
|
3494
|
-
* ```
|
|
3650
|
+
* Service for working with dms objects: create them, delete, etc.
|
|
3495
3651
|
*/
|
|
3496
|
-
class
|
|
3497
|
-
#
|
|
3498
|
-
#
|
|
3499
|
-
#
|
|
3500
|
-
#
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
*/
|
|
3528
|
-
this.isTablet = this.#deviceDetectorService.isTablet();
|
|
3529
|
-
/**
|
|
3530
|
-
* if the app is running on a Desktop browser
|
|
3531
|
-
*/
|
|
3532
|
-
this.isDesktop = this.#deviceDetectorService.isDesktop();
|
|
3533
|
-
this.info = this.#deviceDetectorService.getDeviceInfo();
|
|
3534
|
-
this.isTouchEnabled = this.#isTouchEnabled();
|
|
3535
|
-
this.#resize$.subscribe((e) => {
|
|
3536
|
-
this.#setScreen();
|
|
3537
|
-
});
|
|
3538
|
-
}
|
|
3539
|
-
init(supportsSmallScreens = false) {
|
|
3540
|
-
this.#supportsSmallScreens.set(supportsSmallScreens);
|
|
3541
|
-
this.#setScreen();
|
|
3542
|
-
}
|
|
3543
|
-
#isTouchEnabled() {
|
|
3544
|
-
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
|
3545
|
-
}
|
|
3546
|
-
#setScreen() {
|
|
3547
|
-
const bounds = {
|
|
3548
|
-
width: window.innerWidth,
|
|
3549
|
-
height: window.innerHeight
|
|
3550
|
-
};
|
|
3551
|
-
let orientation = bounds.width >= bounds.height ? DeviceScreenOrientation.LANDSCAPE : DeviceScreenOrientation.PORTRAIT;
|
|
3552
|
-
if (this.isMobile && window.screen['orientation']) {
|
|
3553
|
-
const screenOrientation = window.screen['orientation'].type;
|
|
3554
|
-
if (screenOrientation === 'landscape-primary' || screenOrientation === 'landscape-secondary') {
|
|
3555
|
-
orientation = DeviceScreenOrientation.LANDSCAPE;
|
|
3556
|
-
}
|
|
3557
|
-
else if (screenOrientation === 'portrait-primary' || screenOrientation === 'portrait-secondary') {
|
|
3558
|
-
orientation = DeviceScreenOrientation.PORTRAIT;
|
|
3559
|
-
}
|
|
3560
|
-
}
|
|
3561
|
-
this.#screen = {
|
|
3562
|
-
size: this.#getScreenSize(bounds, orientation),
|
|
3563
|
-
orientation,
|
|
3564
|
-
width: bounds.width,
|
|
3565
|
-
height: bounds.height
|
|
3566
|
-
};
|
|
3567
|
-
this.#screenSource.next(this.#screen);
|
|
3568
|
-
this.smallScreenLayout.set(this.#supportsSmallScreens() && this.#screen.size === 's' && this.#screen.orientation === 'portrait');
|
|
3569
|
-
this.#setupDOM(this.#screen);
|
|
3570
|
-
// force change detection because resize will not be recognized by Angular in some cases
|
|
3571
|
-
// TODO: check: causes recursive ticks in some cases ...
|
|
3572
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
3573
|
-
setTimeout(() => { }, 0);
|
|
3574
|
-
}
|
|
3575
|
-
#setupDOM(screen) {
|
|
3576
|
-
const body = document.querySelector('body');
|
|
3577
|
-
body.setAttribute('data-screen-size', screen.size);
|
|
3578
|
-
body.setAttribute('data-screen-orientation', screen.orientation);
|
|
3579
|
-
if (this.isTouchEnabled)
|
|
3580
|
-
body.setAttribute('data-touch-enabled', 'true');
|
|
3581
|
-
else
|
|
3582
|
-
body.removeAttribute('data-touch-enabled');
|
|
3583
|
-
}
|
|
3584
|
-
#getScreenSize(bounds, orientation) {
|
|
3585
|
-
if (this.#isBelow(this.#upperScreenBoundary.small, bounds)) {
|
|
3586
|
-
return 's';
|
|
3587
|
-
}
|
|
3588
|
-
else if (this.#isBelow(orientation === 'landscape' ? this.#upperScreenBoundary.mediumLandscape : this.#upperScreenBoundary.mediumPortrait, bounds)) {
|
|
3589
|
-
return 'm';
|
|
3590
|
-
}
|
|
3591
|
-
else if (this.#isBelow(this.#upperScreenBoundary.large, bounds)) {
|
|
3592
|
-
return 'l';
|
|
3593
|
-
}
|
|
3594
|
-
else {
|
|
3595
|
-
return 'xl';
|
|
3596
|
-
}
|
|
3597
|
-
}
|
|
3598
|
-
#isBelow(size, bounds) {
|
|
3599
|
-
const landscape = bounds.width < this.#upperScreenBoundary.large ? bounds.width >= bounds.height : false;
|
|
3600
|
-
return (landscape && bounds.height < size) || (!landscape && bounds.width < size);
|
|
3652
|
+
class DmsService {
|
|
3653
|
+
#searchService = inject(SearchService);
|
|
3654
|
+
#backend = inject(BackendService);
|
|
3655
|
+
#eventService = inject(EventService);
|
|
3656
|
+
#uploadService = inject(UploadService);
|
|
3657
|
+
/**
|
|
3658
|
+
* Create new dms object(s). Providing an array of files here instead of one will create
|
|
3659
|
+
* a new dms object for every file. In this case indexdata will shared across all files.
|
|
3660
|
+
* @param objectTypeId The ID of the object type to be created
|
|
3661
|
+
* @param indexdata Indexdata for the new object(s)
|
|
3662
|
+
* @param files File(s) to create dms objects content(s) with
|
|
3663
|
+
* @param label A label that will show up in the upload overlay dialog while uploading
|
|
3664
|
+
*
|
|
3665
|
+
* @returns Array of IDs of the objects that have been created
|
|
3666
|
+
*/
|
|
3667
|
+
createDmsObject(objectTypeId, indexdata, files, label, silent = false, options = { waitForSearchConsistency: true }) {
|
|
3668
|
+
const url = `${this.#backend.getApiBase(ApiBase.apiWeb)}/dms/objects` +
|
|
3669
|
+
`${options.waitForSearchConsistency ? '?waitForSearchConsistency=true' : ''}`;
|
|
3670
|
+
const data = indexdata;
|
|
3671
|
+
data[BaseObjectTypeField.OBJECT_TYPE_ID] = objectTypeId;
|
|
3672
|
+
const upload = files.length
|
|
3673
|
+
? this.#uploadService.multipartUpload(url, files, { label: label ?? '', silent, scope: options.scope }, data)
|
|
3674
|
+
: this.#uploadService.createDocument(url, data);
|
|
3675
|
+
return upload
|
|
3676
|
+
.pipe(map((res) => res.map((response) => response.properties[BaseObjectTypeField.OBJECT_ID].value)),
|
|
3677
|
+
// TODO: Replace by proper solution
|
|
3678
|
+
// Right now there is a gap between when the object was
|
|
3679
|
+
// created and when it is indexed. So delaying here will
|
|
3680
|
+
// give backend time to get its stuff together.
|
|
3681
|
+
delay(1000))
|
|
3682
|
+
.pipe(this.triggerEvent(YuvEventType.DMS_OBJECT_CREATED, '', silent));
|
|
3601
3683
|
}
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3684
|
+
/**
|
|
3685
|
+
* Delete a dms object.
|
|
3686
|
+
* @param id ID of the object to be deleted
|
|
3687
|
+
*/
|
|
3688
|
+
deleteDmsObject(id, silent = false) {
|
|
3689
|
+
const url = `/dms/objects/${id}`;
|
|
3690
|
+
return this.#backend
|
|
3691
|
+
.delete(url, ApiBase.apiWeb)
|
|
3692
|
+
.pipe(map(() => ({ id })))
|
|
3693
|
+
.pipe(this.triggerEvent(YuvEventType.DMS_OBJECT_DELETED, '', silent));
|
|
3606
3694
|
}
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
const transformResponse = () => map((res) => (res.body ? res.body.objects.map((val) => val) : null));
|
|
3619
|
-
/**
|
|
3620
|
-
* Service for providing upload of different object types into a client.
|
|
3621
|
-
*/
|
|
3622
|
-
class UploadService {
|
|
3623
|
-
constructor() {
|
|
3624
|
-
// #region Dependencies
|
|
3625
|
-
this.#backend = inject(BackendService);
|
|
3626
|
-
this.#http = inject(HttpClient);
|
|
3627
|
-
this.#logger = inject(Logger);
|
|
3628
|
-
//#endregion
|
|
3629
|
-
//#region Properties
|
|
3630
|
-
this.#status = { err: 0, items: [] };
|
|
3631
|
-
this.#statusSource = new ReplaySubject();
|
|
3632
|
-
this.#uploadStatus = new BehaviorSubject(null);
|
|
3633
|
-
this.status$ = this.#statusSource.pipe(scan((acc, newVal) => ({ ...acc, ...newVal }), this.#status));
|
|
3634
|
-
this.uploadStatus$ = this.#uploadStatus.asObservable();
|
|
3695
|
+
/**
|
|
3696
|
+
* Restore older version of a dms object.
|
|
3697
|
+
* @param id ID of the object to be restored
|
|
3698
|
+
* @param version version of the object to be restored
|
|
3699
|
+
*/
|
|
3700
|
+
restoreDmsObject(id, version, silent = false) {
|
|
3701
|
+
// eslint-disable-next-line max-len
|
|
3702
|
+
const url = `/dms/objects/${id}/versions/${version}/actions/restore?waitForSearchConsistency=true&restoreParentId=false`;
|
|
3703
|
+
return this.#backend
|
|
3704
|
+
.post(url, {}, ApiBase.apiWeb)
|
|
3705
|
+
.pipe(this.triggerEvent(YuvEventType.DMS_OBJECT_UPDATED, id, silent));
|
|
3635
3706
|
}
|
|
3636
|
-
// #region Dependencies
|
|
3637
|
-
#backend;
|
|
3638
|
-
#http;
|
|
3639
|
-
#logger;
|
|
3640
|
-
//#endregion
|
|
3641
|
-
//#region Properties
|
|
3642
|
-
#status;
|
|
3643
|
-
#statusSource;
|
|
3644
|
-
#uploadStatus;
|
|
3645
|
-
//#endregion
|
|
3646
|
-
//#region Public Methods
|
|
3647
3707
|
/**
|
|
3648
|
-
* Upload a
|
|
3649
|
-
* @param
|
|
3708
|
+
* Upload (add/replace) content to a dms object.
|
|
3709
|
+
* @param objectId ID of the dms object to upload the file to
|
|
3650
3710
|
* @param file The file to be uploaded
|
|
3651
3711
|
* @param label A label that will show up in the upload overlay dialog while uploading
|
|
3652
3712
|
*
|
|
3653
|
-
* @deprecated use
|
|
3713
|
+
* @deprecated use uploadFileContent instead. Provide label and silent through `options`
|
|
3654
3714
|
*/
|
|
3655
|
-
|
|
3656
|
-
return this.#
|
|
3715
|
+
uploadContent(objectId, file, label, silent) {
|
|
3716
|
+
return this.#uploadService
|
|
3717
|
+
.upload(this.getContentPath(objectId), file, label, silent)
|
|
3718
|
+
.pipe(this.triggerEvent(YuvEventType.DMS_OBJECT_UPDATED, objectId));
|
|
3657
3719
|
}
|
|
3658
3720
|
/**
|
|
3659
|
-
* Uploads a
|
|
3721
|
+
* Uploads a file as the content of an existing DMS object, replacing or setting its content stream.
|
|
3660
3722
|
*
|
|
3661
|
-
* This is the preferred alternative to the legacy `
|
|
3662
|
-
*
|
|
3663
|
-
*
|
|
3723
|
+
* This is the preferred alternative to the legacy `uploadContent()` method. It accepts a typed
|
|
3724
|
+
* `FileUploadOptions` object instead of individual parameters, which enables full control over
|
|
3725
|
+
* the upload label, silent mode, and scope-based progress visibility.
|
|
3726
|
+
*
|
|
3727
|
+
* After a successful upload, a `DMS_OBJECT_UPDATED` event is triggered automatically so that
|
|
3728
|
+
* any subscribed parts of the application can react to the change (e.g. refreshing a preview).
|
|
3664
3729
|
*
|
|
3665
3730
|
* **Behavior:**
|
|
3666
|
-
* -
|
|
3667
|
-
*
|
|
3668
|
-
* - If `options.silent` is `true`, the upload runs
|
|
3669
|
-
*
|
|
3670
|
-
* - `options.scope` tags the upload
|
|
3671
|
-
* configured with the same scope
|
|
3731
|
+
* - Targets the content endpoint of the given DMS object: `/dms/objects/{objectId}/contents/file`
|
|
3732
|
+
* - `options.label` is shown in the upload progress indicator; falls back to `file.name` if omitted
|
|
3733
|
+
* - If `options.silent` is `true`, the upload runs in the background with no UI feedback
|
|
3734
|
+
* and no `DMS_OBJECT_UPDATED` event is emitted
|
|
3735
|
+
* - `options.scope` tags the upload so that only the `UploadProgressComponent` instance
|
|
3736
|
+
* configured with the same scope will display progress for this upload
|
|
3672
3737
|
*
|
|
3673
|
-
* @param
|
|
3674
|
-
* @param file - The `File` object to
|
|
3738
|
+
* @param objectId - ID of the DMS object whose content should be uploaded or replaced
|
|
3739
|
+
* @param file - The `File` object to upload as the new content stream
|
|
3675
3740
|
* @param options - Upload configuration: label, silent mode, and optional scope
|
|
3676
|
-
* @returns An `Observable` that emits the
|
|
3741
|
+
* @returns An `Observable` that emits the updated DMS object on completion
|
|
3677
3742
|
*
|
|
3678
3743
|
* @example
|
|
3679
3744
|
* ```typescript
|
|
3680
|
-
* //
|
|
3681
|
-
* this.
|
|
3745
|
+
* // Replace content with a visible progress indicator
|
|
3746
|
+
* this.dmsService.uploadFileContent(objectId, file, { label: 'Uploading contract.pdf' }).subscribe();
|
|
3682
3747
|
*
|
|
3683
|
-
* // Silent
|
|
3684
|
-
* this.
|
|
3748
|
+
* // Silent replacement (no UI feedback, no event emitted)
|
|
3749
|
+
* this.dmsService.uploadFileContent(objectId, file, { label: 'report.pdf', silent: true }).subscribe();
|
|
3685
3750
|
*
|
|
3686
|
-
* // Scoped upload — only
|
|
3687
|
-
* this.
|
|
3751
|
+
* // Scoped upload — only UploadProgressComponent with scope 'detail-panel' will show this
|
|
3752
|
+
* this.dmsService
|
|
3753
|
+
* .uploadFileContent(objectId, file, { label: 'photo.jpg', scope: 'detail-panel' })
|
|
3754
|
+
* .subscribe();
|
|
3688
3755
|
* ```
|
|
3689
3756
|
*/
|
|
3690
|
-
|
|
3691
|
-
return this.#
|
|
3757
|
+
uploadFileContent(objectId, file, options) {
|
|
3758
|
+
return this.#uploadService
|
|
3759
|
+
.uploadFile(this.getContentPath(objectId), file, { ...options, label: options.label ?? file.name })
|
|
3760
|
+
.pipe(this.triggerEvent(YuvEventType.DMS_OBJECT_UPDATED, objectId));
|
|
3692
3761
|
}
|
|
3693
3762
|
/**
|
|
3694
|
-
*
|
|
3695
|
-
* @param
|
|
3696
|
-
* @param
|
|
3697
|
-
* @param data Data to be send along with the files
|
|
3698
|
-
* @param label A label that will show up in the upload overlay dialog while uploading
|
|
3699
|
-
*
|
|
3700
|
-
* @deprecated Use multipartUpload(url, files, options, data) instead. Provide scope through options.
|
|
3763
|
+
* Path of dms object content file.
|
|
3764
|
+
* @param objectId ID of the dms object
|
|
3765
|
+
* @param version version number of the dms object
|
|
3701
3766
|
*/
|
|
3702
|
-
|
|
3703
|
-
return this.#
|
|
3767
|
+
getContentPath(objectId, version) {
|
|
3768
|
+
return (`${this.#backend.getApiBase(ApiBase.apiWeb)}/dms/objects/` +
|
|
3769
|
+
`${objectId}/contents/file${version ? '?version=' + version : ''}`);
|
|
3704
3770
|
}
|
|
3705
3771
|
/**
|
|
3706
|
-
*
|
|
3707
|
-
*
|
|
3708
|
-
*
|
|
3709
|
-
*
|
|
3710
|
-
* to the legacy `uploadMultipart()` overload.
|
|
3711
|
-
*
|
|
3712
|
-
* **Behavior:**
|
|
3713
|
-
* - All files are bundled into a single `FormData` payload under the `files` field
|
|
3714
|
-
* - Optional `data` is appended as a JSON blob under the `data` field
|
|
3715
|
-
* - If `options.silent` is `false` (default), upload progress is shown in the overlay dialog
|
|
3716
|
-
* - If `options.silent` is `true`, the upload runs in the background with no UI feedback
|
|
3717
|
-
* - `options.scope` tags the upload with a named scope, enabling an `UploadProgressComponent`
|
|
3718
|
-
* configured with the same scope to display only the uploads relevant to its section of the UI
|
|
3719
|
-
*
|
|
3720
|
-
* @param url - The backend URL to POST the multipart request to
|
|
3721
|
-
* @param files - Array of `File` objects to upload
|
|
3722
|
-
* @param options - Upload configuration: label (required), silent mode, and optional scope
|
|
3723
|
-
* @param data - Optional metadata object to send alongside the files (serialized as JSON)
|
|
3724
|
-
* @returns An `Observable` that emits the server response on completion
|
|
3725
|
-
*
|
|
3726
|
-
* @example
|
|
3727
|
-
* ```typescript
|
|
3728
|
-
* // Upload files with a visible progress indicator
|
|
3729
|
-
* this.uploadService.multipartUpload(url, files, { label: 'Uploading 3 files' }).subscribe();
|
|
3730
|
-
*
|
|
3731
|
-
* // Upload files with additional metadata
|
|
3732
|
-
* this.uploadService
|
|
3733
|
-
* .multipartUpload(url, files, { label: 'Batch upload' }, { folderId: 'abc123' })
|
|
3734
|
-
* .subscribe();
|
|
3735
|
-
*
|
|
3736
|
-
* // Scoped upload — only the UploadProgressComponent with scope 'mail-editor' will show this
|
|
3737
|
-
* this.uploadService
|
|
3738
|
-
* .multipartUpload(url, files, { label: 'attachments', scope: 'mail-editor' })
|
|
3739
|
-
* .subscribe();
|
|
3740
|
-
* ```
|
|
3772
|
+
* Original API Path of dms object content file.
|
|
3773
|
+
* @param objectId ID of the dms object
|
|
3774
|
+
* @param version version number of the dms object
|
|
3775
|
+
* @param rendition should return rendition path of the dms object
|
|
3741
3776
|
*/
|
|
3742
|
-
|
|
3743
|
-
return this.#
|
|
3777
|
+
getFullContentPath(objectId, version, rendition = false) {
|
|
3778
|
+
return (`${this.#backend.getApiBase(ApiBase.core, true)}/dms/objects/` +
|
|
3779
|
+
`${objectId}${version ? '/versions/' + version : ''}/contents/${rendition ? 'renditions/pdf' : 'file'}`);
|
|
3780
|
+
}
|
|
3781
|
+
getSlideURI(objectId, mimeType) {
|
|
3782
|
+
const supportedMimeTypes = ['*/*'];
|
|
3783
|
+
let supported = false;
|
|
3784
|
+
if (mimeType) {
|
|
3785
|
+
// check if mime type supports slides
|
|
3786
|
+
supportedMimeTypes.forEach((p) => (supported = supported || Utils.patternToRegExp(p).test(mimeType)));
|
|
3787
|
+
}
|
|
3788
|
+
return !mimeType || supported
|
|
3789
|
+
? `${this.#backend.getApiBase(ApiBase.core, true)}/dms/objects/${objectId}/contents/renditions/slide`
|
|
3790
|
+
: undefined;
|
|
3744
3791
|
}
|
|
3745
3792
|
/**
|
|
3746
|
-
*
|
|
3747
|
-
*
|
|
3748
|
-
* Unlike `uploadFile()` and `multipartUpload()`, this method does not attach any file content.
|
|
3749
|
-
* It is intended for creating document objects from metadata alone — for example, creating
|
|
3750
|
-
* a folder, a placeholder record, or a document entry where the content stream will be
|
|
3751
|
-
* provided separately.
|
|
3752
|
-
*
|
|
3753
|
-
* **Behavior:**
|
|
3754
|
-
* - Sends the `data` object as a JSON blob inside a `FormData` payload
|
|
3755
|
-
* - Progress is NOT tracked — this method does not appear in `status$`
|
|
3756
|
-
* - Errors are re-thrown so the caller can handle them via the returned `Observable`
|
|
3757
|
-
*
|
|
3758
|
-
* @param url - The backend URL to POST the document creation request to
|
|
3759
|
-
* @param data - Metadata object describing the document to be created
|
|
3760
|
-
* @returns An `Observable` that emits the created object(s) on success
|
|
3761
|
-
*
|
|
3762
|
-
* @example
|
|
3763
|
-
* ```typescript
|
|
3764
|
-
* const metadata = {
|
|
3765
|
-
* objectTypeId: 'yuv:document',
|
|
3766
|
-
* properties: { 'yuv:title': 'My Document' }
|
|
3767
|
-
* };
|
|
3793
|
+
* Downloads the content of dms objects.
|
|
3768
3794
|
*
|
|
3769
|
-
*
|
|
3770
|
-
*
|
|
3771
|
-
* });
|
|
3772
|
-
* ```
|
|
3795
|
+
* @param DmsObject[] dmsObjects Array of dms objects to be downloaded
|
|
3796
|
+
* @param withVersion should download specific version of the object
|
|
3773
3797
|
*/
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3798
|
+
downloadContent(objects, withVersion) {
|
|
3799
|
+
objects.forEach((object, i) => setTimeout(() => {
|
|
3800
|
+
const uri = `${this.getContentPath(object?.id, withVersion ? object?.version : undefined)}`;
|
|
3801
|
+
this.#backend.download(uri);
|
|
3802
|
+
}, Utils.isSafari() ? i * 1000 : 0 // Safari does not allow multi download
|
|
3803
|
+
));
|
|
3778
3804
|
}
|
|
3779
3805
|
/**
|
|
3780
|
-
*
|
|
3781
|
-
*
|
|
3782
|
-
*
|
|
3783
|
-
* and removes the corresponding item(s) from the internal status list. The updated status
|
|
3784
|
-
* is then emitted via `status$` so that any subscribed UI components (e.g. the upload overlay)
|
|
3785
|
-
* can reflect the change immediately.
|
|
3786
|
-
*
|
|
3787
|
-
* **Behavior:**
|
|
3788
|
-
* - If `id` is provided, only the matching upload item is cancelled
|
|
3789
|
-
* - If `id` is omitted, **all** active uploads are cancelled at once
|
|
3790
|
-
* - If the provided `id` does not match any active item, the call is a no-op
|
|
3791
|
-
* - After cancellation, the error count on `status$` is recalculated
|
|
3792
|
-
*
|
|
3793
|
-
* @param id - Optional ID of the upload item to cancel. Omit to cancel all active uploads.
|
|
3794
|
-
*
|
|
3795
|
-
* @example
|
|
3796
|
-
* ```typescript
|
|
3797
|
-
* // Cancel a specific upload by ID
|
|
3798
|
-
* this.uploadService.cancelItem(uploadId);
|
|
3799
|
-
*
|
|
3800
|
-
* // Cancel all ongoing uploads (e.g. on component destroy or user confirmation)
|
|
3801
|
-
* this.uploadService.cancelItem();
|
|
3802
|
-
* ```
|
|
3806
|
+
* Fetch a dms object.
|
|
3807
|
+
* @param id ID of the object to be retrieved
|
|
3808
|
+
* @param version Desired version of the object
|
|
3803
3809
|
*/
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
else {
|
|
3813
|
-
this.#status.items.forEach((element) => element.subscription.unsubscribe());
|
|
3814
|
-
this.#status.items = [];
|
|
3815
|
-
}
|
|
3816
|
-
this.#status.err = this.#status.items.filter((i) => i.err).length;
|
|
3817
|
-
this.#statusSource.next(this.#status);
|
|
3810
|
+
getDmsObject(id, version, silent = false, requestOptions) {
|
|
3811
|
+
return this.#backend
|
|
3812
|
+
.get(`/dms/objects/${id}${version ? '/versions/' + version : ''}`, undefined, requestOptions)
|
|
3813
|
+
.pipe(map((res) => {
|
|
3814
|
+
const item = this.#searchService.toSearchResult(res).items[0];
|
|
3815
|
+
return this.#searchResultToDmsObject(item);
|
|
3816
|
+
}))
|
|
3817
|
+
.pipe(this.triggerEvent(YuvEventType.DMS_OBJECT_LOADED, '', silent));
|
|
3818
3818
|
}
|
|
3819
|
-
//#endregion
|
|
3820
|
-
//#region Utilities
|
|
3821
3819
|
/**
|
|
3822
|
-
*
|
|
3823
|
-
* @param
|
|
3820
|
+
* Get tags of a dms object.
|
|
3821
|
+
* @param objectData Data field of the dms object
|
|
3824
3822
|
*/
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3823
|
+
getDmsObjectTags(objectData) {
|
|
3824
|
+
return (objectData[BaseObjectTypeField.TAGS] || []).map((row) => ({
|
|
3825
|
+
name: row[0],
|
|
3826
|
+
state: row[1],
|
|
3827
|
+
creationDate: new Date(row[2]),
|
|
3828
|
+
traceId: row[3]
|
|
3829
|
+
}));
|
|
3831
3830
|
}
|
|
3832
3831
|
/**
|
|
3833
|
-
*
|
|
3834
|
-
* @param
|
|
3835
|
-
* @param
|
|
3836
|
-
* @param
|
|
3837
|
-
* @param method Request method
|
|
3832
|
+
* Updates a tag on a dms object.
|
|
3833
|
+
* @param id The ID of the object
|
|
3834
|
+
* @param tag The tag to be updated
|
|
3835
|
+
* @param value The tags new value
|
|
3838
3836
|
*/
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
let headers = this.#backend.getAuthHeaders();
|
|
3844
|
-
if (file) {
|
|
3845
|
-
headers = headers.set('Content-Disposition', `attachment; filename*=utf-8''${encodeURIComponent(file.name)}`);
|
|
3846
|
-
}
|
|
3847
|
-
return new HttpRequest(method, url, file || formData, { headers, reportProgress });
|
|
3837
|
+
setDmsObjectTag(id, tag, value, silent = false) {
|
|
3838
|
+
return this.#backend
|
|
3839
|
+
.post(`/dms/objects/tags/${tag}/state/${value}?query=SELECT * FROM system:object WHERE system:objectId='${id}'`, {}, ApiBase.core)
|
|
3840
|
+
.pipe(this.triggerEvent(YuvEventType.DMS_OBJECT_UPDATED, id, silent));
|
|
3848
3841
|
}
|
|
3849
3842
|
/**
|
|
3850
|
-
*
|
|
3851
|
-
* @param
|
|
3852
|
-
* @param
|
|
3853
|
-
* @param label A label that will show up in the upload overlay dialog while uploading
|
|
3843
|
+
* Deletes a tag from a dms object.
|
|
3844
|
+
* @param id The ID of the object
|
|
3845
|
+
* @param tag The tag to be deleted
|
|
3854
3846
|
*/
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
return silent
|
|
3860
|
-
? this.#http.request(request).pipe(filter((res) => res instanceof HttpResponse))
|
|
3861
|
-
: this.#startUploadWithFile(request, label, options.scope).pipe(transformResponse());
|
|
3847
|
+
deleteDmsObjectTag(id, tag, silent = false) {
|
|
3848
|
+
return this.#backend
|
|
3849
|
+
.delete(`/dms/objects/${id}/tags/${tag}`, ApiBase.core)
|
|
3850
|
+
.pipe(this.triggerEvent(YuvEventType.DMS_OBJECT_UPDATED, id, silent));
|
|
3862
3851
|
}
|
|
3863
3852
|
/**
|
|
3864
|
-
*
|
|
3865
|
-
* @param
|
|
3866
|
-
* @param
|
|
3867
|
-
* @param
|
|
3868
|
-
* @param data Data to be send along with the files
|
|
3853
|
+
* Update indexdata of a dms object.
|
|
3854
|
+
* @param id ID of the object to apply the data to
|
|
3855
|
+
* @param data Indexdata to be applied
|
|
3856
|
+
* @param silent flag to trigger DMS_OBJECT_UPDATED event
|
|
3869
3857
|
*/
|
|
3870
|
-
|
|
3871
|
-
const
|
|
3872
|
-
|
|
3873
|
-
const formData = this.#createFormData({ file: files, data });
|
|
3874
|
-
const request = this.#createHttpRequest(url, { formData }, !silent);
|
|
3875
|
-
return silent
|
|
3876
|
-
? this.#http.request(request).pipe(filter((res) => res instanceof HttpResponse))
|
|
3877
|
-
: this.#startUploadWithFile(request, label, options.scope).pipe(transformResponse());
|
|
3878
|
-
}
|
|
3879
|
-
#generateResult(result) {
|
|
3880
|
-
const objects = result.body?.objects;
|
|
3881
|
-
if (objects && objects.length > 1) {
|
|
3882
|
-
const data = objects[0];
|
|
3883
|
-
// const bp = this.#system.getBaseProperties();
|
|
3884
|
-
// const label = data.properties[bp.title] ? data.properties[bp.title].value : '...';
|
|
3885
|
-
// TODO: Get the label from somewhere
|
|
3886
|
-
const label = '...';
|
|
3887
|
-
return [
|
|
3888
|
-
{
|
|
3889
|
-
objectId: objects.map((val) => val.properties[BaseObjectTypeField.OBJECT_ID].value),
|
|
3890
|
-
contentStreamId: data.contentStreams[0]?.contentStreamId,
|
|
3891
|
-
filename: data.contentStreams[0]?.fileName,
|
|
3892
|
-
label: `(${objects.length}) ${label}`
|
|
3893
|
-
}
|
|
3894
|
-
];
|
|
3895
|
-
}
|
|
3896
|
-
else {
|
|
3897
|
-
return result.body.objects.map((o) => ({
|
|
3898
|
-
objectId: o.properties[BaseObjectTypeField.OBJECT_ID].value,
|
|
3899
|
-
contentStreamId: o.contentStreams[0]?.contentStreamId,
|
|
3900
|
-
filename: o.contentStreams[0]?.fileName,
|
|
3901
|
-
// label: o.properties[bp.title] ? o.properties[bp.title].value : o.contentStreams![0]?.fileName
|
|
3902
|
-
// TODO: Get the label from somewhere
|
|
3903
|
-
label: o.contentStreams[0]?.fileName
|
|
3904
|
-
}));
|
|
3905
|
-
}
|
|
3858
|
+
updateDmsObject(id, data, silent = false, options = { waitForSearchConsistency: true }) {
|
|
3859
|
+
const url = `/dms/objects/${id}${options.waitForSearchConsistency ? '?waitForSearchConsistency=true' : ''}`;
|
|
3860
|
+
return this.#backend.patch(url, data).pipe(this.triggerEvent(YuvEventType.DMS_OBJECT_UPDATED, id, silent));
|
|
3906
3861
|
}
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
}
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
}
|
|
3922
|
-
}
|
|
3862
|
+
/**
|
|
3863
|
+
* Updates given objects.
|
|
3864
|
+
* @param objects the objects to updated
|
|
3865
|
+
*/
|
|
3866
|
+
updateDmsObjects(objects, silent = false, options = { waitForSearchConsistency: true }) {
|
|
3867
|
+
const url = `/dms/objects${options.waitForSearchConsistency ? '?waitForSearchConsistency=true' : ''}`;
|
|
3868
|
+
return this.#backend
|
|
3869
|
+
.patch(url, {
|
|
3870
|
+
patches: objects.map((o) => ({
|
|
3871
|
+
id: o.id,
|
|
3872
|
+
data: o.data
|
|
3873
|
+
}))
|
|
3874
|
+
})
|
|
3875
|
+
.pipe(this.#triggerEvents(YuvEventType.DMS_OBJECT_UPDATED, objects.map((o) => o.id), silent));
|
|
3923
3876
|
}
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
this.#
|
|
3932
|
-
this.#statusSource.next(this.#status);
|
|
3933
|
-
progress.next(0);
|
|
3934
|
-
return throwError(() => new Error(err.message));
|
|
3877
|
+
/**
|
|
3878
|
+
* Updates a tag on a dms object.
|
|
3879
|
+
* @param ids List of IDs of objects
|
|
3880
|
+
* @param tag The tag to be updated
|
|
3881
|
+
* @param value The tags new value
|
|
3882
|
+
*/
|
|
3883
|
+
updateDmsObjectsTag(ids, tag, value, silent = false) {
|
|
3884
|
+
return this.batchUpdateTag(ids, tag, value).pipe(this.#triggerEvents(YuvEventType.DMS_OBJECT_UPDATED, ids, silent));
|
|
3935
3885
|
}
|
|
3936
3886
|
/**
|
|
3937
|
-
*
|
|
3938
|
-
* @param
|
|
3939
|
-
* @param
|
|
3887
|
+
* Moves given objects to a different folder.
|
|
3888
|
+
* @param folderId the id of the new parent folder
|
|
3889
|
+
* @param objects objects to be moved
|
|
3890
|
+
* @param options options for the move operation
|
|
3940
3891
|
*/
|
|
3941
|
-
|
|
3942
|
-
return
|
|
3943
|
-
const id = Utils.uuid();
|
|
3944
|
-
const progress = new Subject();
|
|
3945
|
-
let result;
|
|
3946
|
-
// Create a subscription from the http request that will be applied to the upload
|
|
3947
|
-
// status item in order to be able to cancel the request later on.
|
|
3948
|
-
this.#uploadStatus.next(false);
|
|
3949
|
-
const subscription = this.#http
|
|
3950
|
-
.request(request)
|
|
3951
|
-
.pipe(catchError((err) => this.#createUploadError(err, progress, id)), tap((event) => this.#createProgressStatus(event, progress, id)))
|
|
3952
|
-
// actual return value of this function
|
|
3953
|
-
.subscribe({
|
|
3954
|
-
next: (res) => (res.status ? (result = res) : null),
|
|
3955
|
-
error: (err) => {
|
|
3956
|
-
uploadObserver.error(err);
|
|
3957
|
-
this.#uploadStatus.next(true);
|
|
3958
|
-
uploadObserver.complete();
|
|
3959
|
-
},
|
|
3960
|
-
complete: () => {
|
|
3961
|
-
uploadObserver.next(result);
|
|
3962
|
-
this.#uploadStatus.next(true);
|
|
3963
|
-
uploadObserver.complete();
|
|
3964
|
-
}
|
|
3965
|
-
});
|
|
3966
|
-
this.#status.items.push({
|
|
3967
|
-
id,
|
|
3968
|
-
filename: label,
|
|
3969
|
-
progress: progress.asObservable(),
|
|
3970
|
-
subscription,
|
|
3971
|
-
scope: scope,
|
|
3972
|
-
err: undefined
|
|
3973
|
-
});
|
|
3974
|
-
this.#statusSource.next(this.#status);
|
|
3975
|
-
});
|
|
3892
|
+
moveDmsObjects(targetFolderId, objects, options) {
|
|
3893
|
+
return this.updateDmsObjects(objects.map((o) => ({ id: o.id, data: { [BaseObjectTypeField.PARENT_ID]: targetFolderId } }), true)).pipe(tap((res) => !options?.silent && this.#eventService.trigger(YuvEventType.DMS_OBJECTS_MOVED, res)));
|
|
3976
3894
|
}
|
|
3977
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UploadService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
3978
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UploadService, providedIn: 'root' }); }
|
|
3979
|
-
}
|
|
3980
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UploadService, decorators: [{
|
|
3981
|
-
type: Injectable,
|
|
3982
|
-
args: [{
|
|
3983
|
-
providedIn: 'root'
|
|
3984
|
-
}]
|
|
3985
|
-
}] });
|
|
3986
|
-
|
|
3987
|
-
/**
|
|
3988
|
-
* Service for working with dms objects: create them, delete, etc.
|
|
3989
|
-
*/
|
|
3990
|
-
class DmsService {
|
|
3991
|
-
#searchService = inject(SearchService);
|
|
3992
|
-
#backend = inject(BackendService);
|
|
3993
|
-
#eventService = inject(EventService);
|
|
3994
|
-
#uploadService = inject(UploadService);
|
|
3995
3895
|
/**
|
|
3996
|
-
*
|
|
3997
|
-
*
|
|
3998
|
-
* @param
|
|
3999
|
-
* @param
|
|
4000
|
-
* @param
|
|
4001
|
-
* @param label A label that will show up in the upload overlay dialog while uploading
|
|
4002
|
-
*
|
|
4003
|
-
* @returns Array of IDs of the objects that have been created
|
|
3896
|
+
* Copy given objects to a different folder. The objects will be copied with their indexdata referencing
|
|
3897
|
+
* the existing content of the source object.
|
|
3898
|
+
* @param targetFolderId The ID of the target folder
|
|
3899
|
+
* @param objects The objects to be copied
|
|
3900
|
+
* @param options options for the copy operation
|
|
4004
3901
|
*/
|
|
4005
|
-
|
|
4006
|
-
const
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
3902
|
+
copyDmsObjects(targetFolderId, objects, options) {
|
|
3903
|
+
const excludeProperties = [
|
|
3904
|
+
BaseObjectTypeField.OBJECT_ID,
|
|
3905
|
+
BaseObjectTypeField.CREATED_BY,
|
|
3906
|
+
BaseObjectTypeField.CREATION_DATE,
|
|
3907
|
+
BaseObjectTypeField.MODIFIED_BY,
|
|
3908
|
+
BaseObjectTypeField.MODIFICATION_DATE,
|
|
3909
|
+
BaseObjectTypeField.VERSION_NUMBER,
|
|
3910
|
+
BaseObjectTypeField.TRACE_ID,
|
|
3911
|
+
BaseObjectTypeField.PARENT_OBJECT_TYPE_ID,
|
|
3912
|
+
BaseObjectTypeField.PARENT_VERSION_NUMBER
|
|
3913
|
+
];
|
|
3914
|
+
const mappedObjects = objects.map((o) => {
|
|
3915
|
+
o.data[BaseObjectTypeField.PARENT_ID] = targetFolderId;
|
|
3916
|
+
// Remove properties that are not allowed to be copied.
|
|
3917
|
+
// This also includes content stream fields bundled into the objects data field.
|
|
3918
|
+
// Properties ending with '_title' are also excluded as they are enrichment values
|
|
3919
|
+
// provided by API-Web.
|
|
3920
|
+
Object.keys(o.data)
|
|
3921
|
+
.filter((key) => key.endsWith('_title') || excludeProperties.includes(key) || Object.values(ContentStreamField).includes(key))
|
|
3922
|
+
.forEach((key) => Reflect.deleteProperty(o.data, key));
|
|
3923
|
+
// map filtered data field to data format required by the Core-API
|
|
3924
|
+
const properties = Object.keys(o.data).reduce((acc, key) => {
|
|
3925
|
+
acc[key] = { value: o.data[key] };
|
|
3926
|
+
return acc;
|
|
3927
|
+
}, {});
|
|
3928
|
+
return {
|
|
3929
|
+
properties: properties,
|
|
3930
|
+
objectTypeId: o.objectTypeId,
|
|
3931
|
+
...(o.content
|
|
3932
|
+
? {
|
|
3933
|
+
contentStreams: [o.content]
|
|
3934
|
+
}
|
|
3935
|
+
: {})
|
|
3936
|
+
};
|
|
3937
|
+
});
|
|
3938
|
+
const query = options
|
|
3939
|
+
? Object.keys(options)
|
|
3940
|
+
.map((key) => `${key}=${options[key]}`)
|
|
3941
|
+
.join('&')
|
|
3942
|
+
: '';
|
|
3943
|
+
return this.#backend.post(`/dms/objects?${query}`, { objects: mappedObjects }, ApiBase.core);
|
|
4021
3944
|
}
|
|
4022
3945
|
/**
|
|
4023
|
-
*
|
|
4024
|
-
* @param
|
|
3946
|
+
* Get a bunch of dms objects.
|
|
3947
|
+
* @param ids List of IDs of objects to be retrieved
|
|
4025
3948
|
*/
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
.
|
|
4031
|
-
.pipe(this
|
|
3949
|
+
getDmsObjects(ids, silent = false) {
|
|
3950
|
+
return this.batchGet(ids)
|
|
3951
|
+
.pipe(map((_res) => _res.map((res, i) => res?._error
|
|
3952
|
+
? { ...res, id: ids[i] }
|
|
3953
|
+
: this.#searchResultToDmsObject(this.#searchService.toSearchResult(res).items[0]))))
|
|
3954
|
+
.pipe(this.#triggerEvents(YuvEventType.DMS_OBJECT_LOADED, undefined, silent));
|
|
4032
3955
|
}
|
|
4033
3956
|
/**
|
|
4034
|
-
*
|
|
4035
|
-
* @param
|
|
4036
|
-
* @param
|
|
3957
|
+
* Delete a bunch of dms objects.
|
|
3958
|
+
* @param ids List of IDs of objects to be deleted
|
|
3959
|
+
* @param options Options for the delete operation
|
|
3960
|
+
* @returns Array of delete results.
|
|
4037
3961
|
*/
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
3962
|
+
deleteDmsObjects(objects, options) {
|
|
3963
|
+
const queryParams = options
|
|
3964
|
+
? `?${Object.keys(options)
|
|
3965
|
+
.filter((k) => ['waitForSearchConsistency', 'greedy'].includes(k))
|
|
3966
|
+
.map((k) => `${k}=${options[k]}`)
|
|
3967
|
+
.join('&')}`
|
|
3968
|
+
: '';
|
|
4041
3969
|
return this.#backend
|
|
4042
|
-
.
|
|
4043
|
-
|
|
3970
|
+
.delete(`/dms/objects${queryParams}`, ApiBase.core, {
|
|
3971
|
+
body: {
|
|
3972
|
+
objects: objects.map((o) => ({
|
|
3973
|
+
properties: typeof o === 'string'
|
|
3974
|
+
? {
|
|
3975
|
+
'system:objectId': {
|
|
3976
|
+
value: o
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
: {
|
|
3980
|
+
'system:objectId': { value: o.id },
|
|
3981
|
+
subject: { value: o.subject }
|
|
3982
|
+
}
|
|
3983
|
+
}))
|
|
3984
|
+
}
|
|
3985
|
+
})
|
|
3986
|
+
.pipe(map((res) => res?.objects?.map((r) => {
|
|
3987
|
+
const v = r.options?.[SystemResult.DELETE];
|
|
3988
|
+
return {
|
|
3989
|
+
id: r.properties?.[BaseObjectTypeField.OBJECT_ID]?.value,
|
|
3990
|
+
properties: r.properties,
|
|
3991
|
+
...(v?.httpStatusCode >= 400 ? { _error: { status: v.httpStatusCode, message: v.message } } : {})
|
|
3992
|
+
};
|
|
3993
|
+
})))
|
|
3994
|
+
.pipe(this.#triggerEvents(YuvEventType.DMS_OBJECT_DELETED, undefined, options?.silent));
|
|
4044
3995
|
}
|
|
4045
3996
|
/**
|
|
4046
|
-
*
|
|
4047
|
-
* @param
|
|
4048
|
-
* @param file The file to be uploaded
|
|
4049
|
-
* @param label A label that will show up in the upload overlay dialog while uploading
|
|
4050
|
-
*
|
|
4051
|
-
* @deprecated use uploadFileContent instead. Provide label and silent through `options`
|
|
3997
|
+
* Fetch a dms object versions.
|
|
3998
|
+
* @param id ID of the object to be retrieved
|
|
4052
3999
|
*/
|
|
4053
|
-
|
|
4054
|
-
return this.#
|
|
4055
|
-
|
|
4056
|
-
.
|
|
4000
|
+
getDmsObjectVersions(id) {
|
|
4001
|
+
return this.#backend.get('/dms/objects/' + id + '/versions').pipe(map((res) => {
|
|
4002
|
+
const items = this.#searchService.toSearchResult(res).items || [];
|
|
4003
|
+
return items.map((item) => this.#searchResultToDmsObject(item));
|
|
4004
|
+
}), map((res) => res.sort(Utils.sortValues('version', Sort.DESC))));
|
|
4057
4005
|
}
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
* `FileUploadOptions` object instead of individual parameters, which enables full control over
|
|
4063
|
-
* the upload label, silent mode, and scope-based progress visibility.
|
|
4064
|
-
*
|
|
4065
|
-
* After a successful upload, a `DMS_OBJECT_UPDATED` event is triggered automatically so that
|
|
4066
|
-
* any subscribed parts of the application can react to the change (e.g. refreshing a preview).
|
|
4067
|
-
*
|
|
4068
|
-
* **Behavior:**
|
|
4069
|
-
* - Targets the content endpoint of the given DMS object: `/dms/objects/{objectId}/contents/file`
|
|
4070
|
-
* - `options.label` is shown in the upload progress indicator; falls back to `file.name` if omitted
|
|
4071
|
-
* - If `options.silent` is `true`, the upload runs in the background with no UI feedback
|
|
4072
|
-
* and no `DMS_OBJECT_UPDATED` event is emitted
|
|
4073
|
-
* - `options.scope` tags the upload so that only the `UploadProgressComponent` instance
|
|
4074
|
-
* configured with the same scope will display progress for this upload
|
|
4075
|
-
*
|
|
4076
|
-
* @param objectId - ID of the DMS object whose content should be uploaded or replaced
|
|
4077
|
-
* @param file - The `File` object to upload as the new content stream
|
|
4078
|
-
* @param options - Upload configuration: label, silent mode, and optional scope
|
|
4079
|
-
* @returns An `Observable` that emits the updated DMS object on completion
|
|
4080
|
-
*
|
|
4081
|
-
* @example
|
|
4082
|
-
* ```typescript
|
|
4083
|
-
* // Replace content with a visible progress indicator
|
|
4084
|
-
* this.dmsService.uploadFileContent(objectId, file, { label: 'Uploading contract.pdf' }).subscribe();
|
|
4085
|
-
*
|
|
4086
|
-
* // Silent replacement (no UI feedback, no event emitted)
|
|
4087
|
-
* this.dmsService.uploadFileContent(objectId, file, { label: 'report.pdf', silent: true }).subscribe();
|
|
4088
|
-
*
|
|
4089
|
-
* // Scoped upload — only UploadProgressComponent with scope 'detail-panel' will show this
|
|
4090
|
-
* this.dmsService
|
|
4091
|
-
* .uploadFileContent(objectId, file, { label: 'photo.jpg', scope: 'detail-panel' })
|
|
4092
|
-
* .subscribe();
|
|
4093
|
-
* ```
|
|
4094
|
-
*/
|
|
4095
|
-
uploadFileContent(objectId, file, options) {
|
|
4096
|
-
return this.#uploadService
|
|
4097
|
-
.uploadFile(this.getContentPath(objectId), file, { ...options, label: options.label ?? file.name })
|
|
4098
|
-
.pipe(this.triggerEvent(YuvEventType.DMS_OBJECT_UPDATED, objectId));
|
|
4006
|
+
getDmsObjectVersion(id, version) {
|
|
4007
|
+
return this.#backend
|
|
4008
|
+
.get(`/dms/objects/${id}/versions/${version}`)
|
|
4009
|
+
.pipe(map((res) => this.#searchResultToDmsObject(this.#searchService.toSearchResult(res).items[0])));
|
|
4099
4010
|
}
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
return (`${this.#backend.getApiBase(ApiBase.apiWeb)}/dms/objects/` +
|
|
4107
|
-
`${objectId}/contents/file${version ? '?version=' + version : ''}`);
|
|
4011
|
+
coreApiResponseToDmsObject(res) {
|
|
4012
|
+
return this.#searchResultToDmsObject({
|
|
4013
|
+
objectTypeId: res.properties[BaseObjectTypeField.OBJECT_TYPE_ID].value,
|
|
4014
|
+
fields: new Map(Object.entries(res.properties)),
|
|
4015
|
+
content: res.contentStreams?.[0]
|
|
4016
|
+
});
|
|
4108
4017
|
}
|
|
4109
4018
|
/**
|
|
4110
|
-
*
|
|
4111
|
-
* @param
|
|
4112
|
-
* @
|
|
4113
|
-
* @param rendition should return rendition path of the dms object
|
|
4019
|
+
* Transforms a plain data object to a DmsObject.
|
|
4020
|
+
* @param data The plain data object
|
|
4021
|
+
* @returns DmsObject
|
|
4114
4022
|
*/
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4023
|
+
toDmsObject(data) {
|
|
4024
|
+
const fields = new Map();
|
|
4025
|
+
Object.keys(data).forEach((key) => fields.set(key, data[key]));
|
|
4026
|
+
const item = {
|
|
4027
|
+
content: {
|
|
4028
|
+
contentStreamId: data[ContentStreamField.ID],
|
|
4029
|
+
mimeType: data[ContentStreamField.MIME_TYPE],
|
|
4030
|
+
fileName: data[ContentStreamField.FILENAME],
|
|
4031
|
+
size: data[ContentStreamField.LENGTH],
|
|
4032
|
+
digest: data[ContentStreamField.DIGEST],
|
|
4033
|
+
repositoryId: data[ContentStreamField.REPOSITORY_ID],
|
|
4034
|
+
range: data[ContentStreamField.RANGE]
|
|
4035
|
+
},
|
|
4036
|
+
objectTypeId: data[BaseObjectTypeField.OBJECT_TYPE_ID],
|
|
4037
|
+
fields
|
|
4038
|
+
};
|
|
4039
|
+
return this.#searchResultToDmsObject(item);
|
|
4118
4040
|
}
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
}
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4041
|
+
batchUpdateTag(ids, tag, value) {
|
|
4042
|
+
return this.#backend.batch(ids.map((id) => ({
|
|
4043
|
+
method: 'POST',
|
|
4044
|
+
uri: `/dms/objects/tags/${tag}/state/${value}?query=SELECT * FROM system:object WHERE system:objectId='${id}'`,
|
|
4045
|
+
base: ApiBase.core,
|
|
4046
|
+
body: {}
|
|
4047
|
+
})));
|
|
4048
|
+
}
|
|
4049
|
+
batchDeleteTag(ids, tag) {
|
|
4050
|
+
return this.#backend.batch(ids.map((id) => ({
|
|
4051
|
+
method: 'DELETE',
|
|
4052
|
+
uri: `/dms/objects/${id}/tags/${tag}`,
|
|
4053
|
+
base: ApiBase.core
|
|
4054
|
+
})));
|
|
4055
|
+
}
|
|
4056
|
+
batchDelete(ids) {
|
|
4057
|
+
return this.#backend.batch(ids.map((id) => ({ method: 'DELETE', uri: `/dms/objects/${id}` })));
|
|
4058
|
+
}
|
|
4059
|
+
batchGet(ids) {
|
|
4060
|
+
return this.#backend.batch(ids.map((id) => ({ method: 'GET', uri: `/dms/objects/${id}` })));
|
|
4129
4061
|
}
|
|
4130
4062
|
/**
|
|
4131
|
-
*
|
|
4132
|
-
*
|
|
4133
|
-
* @param DmsObject[] dmsObjects Array of dms objects to be downloaded
|
|
4134
|
-
* @param withVersion should download specific version of the object
|
|
4063
|
+
* Map search result from the backend to applications SearchResult object
|
|
4064
|
+
* @param searchResponse The backend response
|
|
4135
4065
|
*/
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4066
|
+
toSearchResult(searchResponse) {
|
|
4067
|
+
const resultListItems = [];
|
|
4068
|
+
const objectTypes = [];
|
|
4069
|
+
searchResponse.objects.forEach((o) => {
|
|
4070
|
+
const fields = new Map();
|
|
4071
|
+
// process properties section of result
|
|
4072
|
+
Object.keys(o.properties).forEach((key) => {
|
|
4073
|
+
let value = o.properties[key].value;
|
|
4074
|
+
if (o.properties[key].clvalue) {
|
|
4075
|
+
// table fields will have a clientValue too ...
|
|
4076
|
+
value = o.properties[key].clvalue;
|
|
4077
|
+
// ... and also may contain values that need to be resolved
|
|
4078
|
+
if (o.properties[key].resolvedValues) {
|
|
4079
|
+
value.forEach((v) => {
|
|
4080
|
+
Object.keys(v).forEach((k) => {
|
|
4081
|
+
const resValue = Array.isArray(v[k])
|
|
4082
|
+
? v[k].map((i) => o.properties[key].resolvedValues[i])
|
|
4083
|
+
: o.properties[key].resolvedValues[v[k]];
|
|
4084
|
+
if (resValue) {
|
|
4085
|
+
v[`${k}_title`] = resValue;
|
|
4086
|
+
}
|
|
4087
|
+
});
|
|
4088
|
+
});
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
fields.set(key, value);
|
|
4092
|
+
if (o.properties[key].title) {
|
|
4093
|
+
fields.set(key + '_title', o.properties[key].title);
|
|
4094
|
+
}
|
|
4095
|
+
});
|
|
4096
|
+
// process contentStreams section of result if available.
|
|
4097
|
+
// Objects that don't have files attached won't have this section
|
|
4098
|
+
let content;
|
|
4099
|
+
if (o.contentStreams && o.contentStreams.length > 0) {
|
|
4100
|
+
// we assume that each result object only has ONE file attached, although
|
|
4101
|
+
// this is an array and there may be more
|
|
4102
|
+
const contentStream = o.contentStreams[0];
|
|
4103
|
+
// also add content-stream related fields to the result fields
|
|
4104
|
+
fields.set(ContentStreamField.LENGTH, contentStream.length);
|
|
4105
|
+
fields.set(ContentStreamField.MIME_TYPE, contentStream.mimeType);
|
|
4106
|
+
fields.set(ContentStreamField.FILENAME, contentStream.fileName);
|
|
4107
|
+
fields.set(ContentStreamField.ID, contentStream.contentStreamId);
|
|
4108
|
+
fields.set(ContentStreamField.RANGE, contentStream.contentStreamRange);
|
|
4109
|
+
fields.set(ContentStreamField.REPOSITORY_ID, contentStream.repositoryId);
|
|
4110
|
+
fields.set(ContentStreamField.DIGEST, contentStream.digest);
|
|
4111
|
+
fields.set(ContentStreamField.ARCHIVE_PATH, contentStream.archivePath);
|
|
4112
|
+
content = {
|
|
4113
|
+
contentStreamId: contentStream.contentStreamId,
|
|
4114
|
+
repositoryId: contentStream.repositoryId,
|
|
4115
|
+
range: contentStream.range,
|
|
4116
|
+
digest: contentStream.digest,
|
|
4117
|
+
archivePath: contentStream.archivePath,
|
|
4118
|
+
fileName: contentStream.fileName,
|
|
4119
|
+
mimeType: contentStream.mimeType,
|
|
4120
|
+
size: contentStream.length
|
|
4121
|
+
};
|
|
4122
|
+
}
|
|
4123
|
+
const objectTypeId = o.properties[BaseObjectTypeField.OBJECT_TYPE_ID]
|
|
4124
|
+
? o.properties[BaseObjectTypeField.OBJECT_TYPE_ID].value
|
|
4125
|
+
: null;
|
|
4126
|
+
if (!objectTypes.includes(objectTypeId)) {
|
|
4127
|
+
objectTypes.push(objectTypeId);
|
|
4128
|
+
}
|
|
4129
|
+
resultListItems.push({
|
|
4130
|
+
objectTypeId,
|
|
4131
|
+
content,
|
|
4132
|
+
fields,
|
|
4133
|
+
permissions: o.permissions
|
|
4134
|
+
});
|
|
4135
|
+
});
|
|
4136
|
+
const result = {
|
|
4137
|
+
hasMoreItems: searchResponse.hasMoreItems,
|
|
4138
|
+
totalNumItems: searchResponse.totalNumItems,
|
|
4139
|
+
items: resultListItems,
|
|
4140
|
+
objectTypes
|
|
4141
|
+
};
|
|
4142
|
+
return result;
|
|
4143
|
+
}
|
|
4144
|
+
// general trigger operator to handle all dms events
|
|
4145
|
+
triggerEvent(event, id, silent = false) {
|
|
4146
|
+
return (stream) => stream.pipe(
|
|
4147
|
+
// update does not return permissions, so we need to re-load the whole dms object
|
|
4148
|
+
// TODO: Remove once permissions are provided
|
|
4149
|
+
switchMap((res) => (!id ? of(res) : this.getDmsObject(id))),
|
|
4150
|
+
// TODO: enable once permissions are provided
|
|
4151
|
+
// map((res) => this.searchResultToDmsObject(this.searchService.toSearchResult(res).items[0])),
|
|
4152
|
+
tap((res) => !silent && this.#eventService.trigger(event, res)));
|
|
4153
|
+
}
|
|
4154
|
+
// general trigger operator to handle all dms events
|
|
4155
|
+
#triggerEvents(event, ids, silent = false) {
|
|
4156
|
+
return (stream) => stream.pipe(
|
|
4157
|
+
// update does not return permissions, so we need to re-load the whole dms object
|
|
4158
|
+
// TODO: Remove once permissions are provided
|
|
4159
|
+
switchMap((res) => (!ids ? of(res) : this.getDmsObjects(ids))),
|
|
4160
|
+
// TODO: enable once permissions are provided
|
|
4161
|
+
// map((_res: any[]) => _res.map((res, i) => res?._error ?
|
|
4162
|
+
// { ...res, id: ids?[i] } : this.searchResultToDmsObject(this.searchService.toSearchResult(res).items[0]))),
|
|
4163
|
+
map((_res) => _res.map((res, i) => (res?._error && ids ? { ...res, id: ids[i] } : res))), tap((res) => !silent && res.forEach((o) => o && this.#eventService.trigger(event, o))));
|
|
4164
|
+
}
|
|
4165
|
+
#searchResultToDmsObject(resItem) {
|
|
4166
|
+
return new DmsObject(resItem);
|
|
4167
|
+
}
|
|
4168
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DmsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
4169
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DmsService, providedIn: 'root' }); }
|
|
4170
|
+
}
|
|
4171
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DmsService, decorators: [{
|
|
4172
|
+
type: Injectable,
|
|
4173
|
+
args: [{
|
|
4174
|
+
providedIn: 'root'
|
|
4175
|
+
}]
|
|
4176
|
+
}] });
|
|
4177
|
+
|
|
4178
|
+
class CatalogService {
|
|
4179
|
+
constructor() {
|
|
4180
|
+
this.#backend = inject(BackendService);
|
|
4181
|
+
this.#dms = inject(DmsService);
|
|
4182
|
+
this.#search = inject(SearchService);
|
|
4183
|
+
this.catalogs = signal([], ...(ngDevMode ? [{ debugName: "catalogs" }] : /* istanbul ignore next */ []));
|
|
4184
|
+
}
|
|
4185
|
+
static { this.CATALOG_BASE = '/dms/catalogs/odata'; }
|
|
4186
|
+
#backend;
|
|
4187
|
+
#dms;
|
|
4188
|
+
#search;
|
|
4189
|
+
/** Fetch the list of catalogs and publish to the `catalogs` signal. */
|
|
4190
|
+
loadCatalogs() {
|
|
4191
|
+
return this.#fetchData();
|
|
4142
4192
|
}
|
|
4193
|
+
// ── Catalog CRUD ─────────────────────────────────────────────
|
|
4143
4194
|
/**
|
|
4144
|
-
*
|
|
4145
|
-
* @param id ID of the object to be retrieved
|
|
4146
|
-
* @param version Desired version of the object
|
|
4195
|
+
* Create a new catalog. Returns the new objectId.
|
|
4147
4196
|
*/
|
|
4148
|
-
|
|
4197
|
+
createCatalog(payload) {
|
|
4198
|
+
const data = {
|
|
4199
|
+
[BaseObjectTypeField.OBJECT_TYPE_ID]: { value: SystemType.ITEM },
|
|
4200
|
+
[BaseObjectTypeField.SECONDARY_OBJECT_TYPE_IDS]: { value: [SystemType.CATALOG] },
|
|
4201
|
+
[CatalogTypeField.NATIVE_ID]: { value: payload.name }
|
|
4202
|
+
};
|
|
4149
4203
|
return this.#backend
|
|
4150
|
-
.
|
|
4151
|
-
.pipe(map((res) =>
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4204
|
+
.post(`/dms/objects?waitForSearchConsistency=true`, { objects: [{ properties: data }] }, ApiBase.core)
|
|
4205
|
+
.pipe(map$1((res) => res.objects[0]?.properties[BaseObjectTypeField.OBJECT_ID]?.value), switchMap$1((id) => this.#fetchData().pipe(map$1(() => id))));
|
|
4206
|
+
}
|
|
4207
|
+
/** Delete a catalog by objectId. */
|
|
4208
|
+
deleteCatalog(objectId) {
|
|
4209
|
+
return this.#backend
|
|
4210
|
+
.delete(`/dms/objects/${objectId}?waitForSearchConsistency=true`, ApiBase.core)
|
|
4211
|
+
.pipe(switchMap$1(() => this.#fetchData()));
|
|
4212
|
+
}
|
|
4213
|
+
// ── Catalog entry CRUD ───────────────────────────────────────
|
|
4214
|
+
/** Create a new entry in the given catalog. Returns the new objectId. */
|
|
4215
|
+
createEntry(catalogName, payload) {
|
|
4216
|
+
const sots = [SystemType.CATALOG_ENTRY, ...(payload.secondaryObjectTypeIds ?? [])];
|
|
4217
|
+
const data = {
|
|
4218
|
+
[BaseObjectTypeField.SECONDARY_OBJECT_TYPE_IDS]: { value: sots },
|
|
4219
|
+
[BaseObjectTypeField.OBJECT_TYPE_ID]: { value: SystemType.ITEM },
|
|
4220
|
+
[CatalogTypeField.NATIVE_ID]: { value: payload.name },
|
|
4221
|
+
[CatalogTypeField.CATALOG_NATIVE_ID]: { value: catalogName },
|
|
4222
|
+
...(payload.validFrom !== undefined ? { [CatalogTypeField.DATE_VALID_FROM]: { value: payload.validFrom } } : {}),
|
|
4223
|
+
...(payload.validUntil !== undefined
|
|
4224
|
+
? { [CatalogTypeField.DATE_VALID_UNTIL]: { value: payload.validUntil } }
|
|
4225
|
+
: {}),
|
|
4226
|
+
...(payload.localizations
|
|
4227
|
+
? { [CatalogTypeField.LOCALIZATION]: this.#toLocalizationTable(payload.localizations) }
|
|
4228
|
+
: {}),
|
|
4229
|
+
...(payload.properties ?? {})
|
|
4230
|
+
};
|
|
4231
|
+
return this.#backend
|
|
4232
|
+
.post(`/dms/objects?waitForSearchConsistency=true`, { objects: [{ properties: data }] }, ApiBase.core)
|
|
4233
|
+
.pipe(map$1((res) => res.objects[0]?.properties[BaseObjectTypeField.OBJECT_ID]?.value));
|
|
4156
4234
|
}
|
|
4157
4235
|
/**
|
|
4158
|
-
*
|
|
4159
|
-
*
|
|
4236
|
+
* Update mutable fields of a catalog entry. Sparse: only fields present in `update`
|
|
4237
|
+
* are sent. Pass `null` for `validFrom`/`validUntil` to clear them.
|
|
4160
4238
|
*/
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4239
|
+
updateEntry(objectId, update) {
|
|
4240
|
+
const data = {
|
|
4241
|
+
// 'system:catalogNativeId': { value: update.catalogName }
|
|
4242
|
+
};
|
|
4243
|
+
if (update.validFrom !== undefined) {
|
|
4244
|
+
data[CatalogTypeField.DATE_VALID_FROM] = { value: update.validFrom };
|
|
4245
|
+
}
|
|
4246
|
+
if (update.validUntil !== undefined) {
|
|
4247
|
+
data[CatalogTypeField.DATE_VALID_UNTIL] = { value: update.validUntil };
|
|
4248
|
+
}
|
|
4249
|
+
if (update.localizations !== undefined) {
|
|
4250
|
+
data[CatalogTypeField.LOCALIZATION] = this.#toLocalizationTable(update.localizations);
|
|
4251
|
+
}
|
|
4252
|
+
return this.#backend.patch(`/dms/objects/${objectId}`, { objects: [{ properties: data }] }, ApiBase.core);
|
|
4253
|
+
}
|
|
4254
|
+
/** Delete a catalog entry by objectId. */
|
|
4255
|
+
deleteEntry(objectId) {
|
|
4256
|
+
return this.#backend.delete(`/dms/objects/${objectId}?waitForSearchConsistency=true`, ApiBase.core);
|
|
4257
|
+
}
|
|
4258
|
+
// ── Catalog entry queries ────────────────────────────────────
|
|
4259
|
+
/**
|
|
4260
|
+
* Get a specific catalog entry by name (use case 1).
|
|
4261
|
+
* Validity filtering does NOT apply — all existing entries are returned.
|
|
4262
|
+
* Returns 404 if the entry does not exist. Pass `locale` to receive
|
|
4263
|
+
* `system:localization` rows resolved against `Accept-Language`.
|
|
4264
|
+
*/
|
|
4265
|
+
getEntry(catalogName, entryName, locale) {
|
|
4266
|
+
const escapedEntry = entryName.replace(/'/g, "''");
|
|
4267
|
+
const url = `${CatalogService.CATALOG_BASE}/${catalogName}('${escapedEntry}')`;
|
|
4268
|
+
return this.#backend.get(url, ApiBase.core, this.#localeOptions(locale)).pipe(map$1((response) => {
|
|
4269
|
+
const obj = response.objects[0];
|
|
4270
|
+
if (!obj) {
|
|
4271
|
+
throw new Error(`Catalog entry '${entryName}' not found in '${catalogName}'`);
|
|
4272
|
+
}
|
|
4273
|
+
return this.#mapEntry(obj);
|
|
4167
4274
|
}));
|
|
4168
4275
|
}
|
|
4169
4276
|
/**
|
|
4170
|
-
*
|
|
4171
|
-
*
|
|
4172
|
-
*
|
|
4173
|
-
* @param value The tags new value
|
|
4277
|
+
* Get catalog entries with optional filtering and pagination (use cases 2 & 3).
|
|
4278
|
+
* By default only valid entries are returned. Pass `options.locale` to drive
|
|
4279
|
+
* `startswith(displayValue,'…')` matching and the localized response rows.
|
|
4174
4280
|
*/
|
|
4175
|
-
|
|
4281
|
+
getEntries(catalogName, options) {
|
|
4282
|
+
const url = `${CatalogService.CATALOG_BASE}/${catalogName}`;
|
|
4283
|
+
const params = this.#buildQueryParams(options);
|
|
4284
|
+
const requestOptions = { params, ...this.#localeOptions(options?.locale) };
|
|
4176
4285
|
return this.#backend
|
|
4177
|
-
.
|
|
4178
|
-
.pipe(
|
|
4286
|
+
.get(url, ApiBase.core, requestOptions)
|
|
4287
|
+
.pipe(map$1((response) => this.#mapResponse(response)));
|
|
4179
4288
|
}
|
|
4180
4289
|
/**
|
|
4181
|
-
*
|
|
4182
|
-
*
|
|
4183
|
-
* @param tag The tag to be deleted
|
|
4290
|
+
* Autocomplete / filtered search (convenience wrapper for use case 3).
|
|
4291
|
+
* Searches the localized display value using startswith().
|
|
4184
4292
|
*/
|
|
4185
|
-
|
|
4186
|
-
return this
|
|
4187
|
-
.delete(`/dms/objects/${id}/tags/${tag}`, ApiBase.core)
|
|
4188
|
-
.pipe(this.triggerEvent(YuvEventType.DMS_OBJECT_UPDATED, id, silent));
|
|
4293
|
+
autocomplete(catalogName, prefix, options) {
|
|
4294
|
+
return this.getEntries(catalogName, { ...options, autocompletePrefix: prefix });
|
|
4189
4295
|
}
|
|
4190
|
-
/**
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
updateDmsObject(id, data, silent = false, options = { waitForSearchConsistency: true }) {
|
|
4197
|
-
const url = `/dms/objects/${id}${options.waitForSearchConsistency ? '?waitForSearchConsistency=true' : ''}`;
|
|
4198
|
-
return this.#backend.patch(url, data).pipe(this.triggerEvent(YuvEventType.DMS_OBJECT_UPDATED, id, silent));
|
|
4296
|
+
/** List all catalogs (system:catalog) for the current tenant. */
|
|
4297
|
+
#getCatalogs() {
|
|
4298
|
+
return this.#search.searchCmis(`SELECT * FROM ${SystemType.CATALOG}`, 1000).pipe(map$1((res) => res.items.map((item) => ({
|
|
4299
|
+
objectId: item.fields.get(BaseObjectTypeField.OBJECT_ID),
|
|
4300
|
+
name: item.fields.get(CatalogTypeField.NATIVE_ID)
|
|
4301
|
+
}))));
|
|
4199
4302
|
}
|
|
4200
|
-
/**
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
return this.#backend
|
|
4207
|
-
.patch(url, {
|
|
4208
|
-
patches: objects.map((o) => ({
|
|
4209
|
-
id: o.id,
|
|
4210
|
-
data: o.data
|
|
4211
|
-
}))
|
|
4212
|
-
})
|
|
4213
|
-
.pipe(this.#triggerEvents(YuvEventType.DMS_OBJECT_UPDATED, objects.map((o) => o.id), silent));
|
|
4303
|
+
/** Loads the catalog list, sorts by name and publishes to the `catalogs` signal. */
|
|
4304
|
+
#fetchData() {
|
|
4305
|
+
return this.#getCatalogs().pipe(tap$1((catalogs) => {
|
|
4306
|
+
catalogs.sort((firstValue, secondValue) => firstValue.name.localeCompare(secondValue.name));
|
|
4307
|
+
this.catalogs.set(catalogs);
|
|
4308
|
+
}));
|
|
4214
4309
|
}
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4310
|
+
#localeOptions(locale) {
|
|
4311
|
+
return locale ? { headers: { 'Accept-Language': locale } } : {};
|
|
4312
|
+
}
|
|
4313
|
+
#buildQueryParams(options) {
|
|
4314
|
+
let params = new HttpParams();
|
|
4315
|
+
if (!options) {
|
|
4316
|
+
return params;
|
|
4317
|
+
}
|
|
4318
|
+
if (options.top != null) {
|
|
4319
|
+
params = params.set('$top', options.top.toString());
|
|
4320
|
+
}
|
|
4321
|
+
if (options.skip != null) {
|
|
4322
|
+
params = params.set('$skip', options.skip.toString());
|
|
4323
|
+
}
|
|
4324
|
+
if (options.includeInvalidEntries) {
|
|
4325
|
+
params = params.set('includeInvalidEntries', 'true');
|
|
4326
|
+
}
|
|
4327
|
+
const filter = this.#buildFilterExpression(options);
|
|
4328
|
+
if (filter) {
|
|
4329
|
+
params = params.set('$filter', filter);
|
|
4330
|
+
}
|
|
4331
|
+
return params;
|
|
4332
|
+
}
|
|
4333
|
+
#buildFilterExpression(options) {
|
|
4334
|
+
const parts = [];
|
|
4335
|
+
if (options.filters?.length) {
|
|
4336
|
+
for (const filter of options.filters) {
|
|
4337
|
+
const escapedValue = filter.value.replace(/'/g, "''");
|
|
4338
|
+
parts.push(`${filter.property} eq '${escapedValue}'`);
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
if (options.autocompletePrefix) {
|
|
4342
|
+
const escapedPrefix = options.autocompletePrefix.replace(/'/g, "''");
|
|
4343
|
+
parts.push(`startswith(displayValue,'${escapedPrefix}')`);
|
|
4344
|
+
}
|
|
4345
|
+
return parts.length ? parts.join(' and ') : null;
|
|
4346
|
+
}
|
|
4347
|
+
#mapResponse(response) {
|
|
4348
|
+
return {
|
|
4349
|
+
entries: response.objects.map((obj) => this.#mapEntry(obj)),
|
|
4350
|
+
hasMoreItems: response.hasMoreItems,
|
|
4351
|
+
totalNumItems: response.totalNumItems
|
|
4352
|
+
};
|
|
4353
|
+
}
|
|
4354
|
+
#mapEntry(apiObject) {
|
|
4355
|
+
const props = apiObject.properties;
|
|
4356
|
+
return {
|
|
4357
|
+
objectId: this.#prop(props, BaseObjectTypeField.OBJECT_ID),
|
|
4358
|
+
name: this.#prop(props, CatalogTypeField.NATIVE_ID),
|
|
4359
|
+
catalogName: this.#prop(props, CatalogTypeField.CATALOG_NATIVE_ID),
|
|
4360
|
+
localizations: this.#parseLocalization(props[CatalogTypeField.LOCALIZATION]),
|
|
4361
|
+
validFrom: this.#prop(props, CatalogTypeField.DATE_VALID_FROM),
|
|
4362
|
+
validUntil: this.#prop(props, CatalogTypeField.DATE_VALID_UNTIL),
|
|
4363
|
+
properties: this.#extractCustomProperties(props)
|
|
4364
|
+
};
|
|
4365
|
+
}
|
|
4366
|
+
#prop(properties, key) {
|
|
4367
|
+
return properties[key]?.value;
|
|
4368
|
+
}
|
|
4369
|
+
#toLocalizationTable(localizations) {
|
|
4370
|
+
return {
|
|
4371
|
+
columnNames: LOCALIZATION_COLUMNS,
|
|
4372
|
+
value: localizations.map((loc) => [loc.locale || '', loc.label || '', loc.description || ''])
|
|
4373
|
+
};
|
|
4374
|
+
}
|
|
4375
|
+
#parseLocalization(prop) {
|
|
4376
|
+
if (!prop) {
|
|
4377
|
+
return [];
|
|
4378
|
+
}
|
|
4379
|
+
const localization = prop;
|
|
4380
|
+
if (!localization.value.length) {
|
|
4381
|
+
return [];
|
|
4382
|
+
}
|
|
4383
|
+
return localization.value.map((row) => ({
|
|
4384
|
+
locale: row[0] ?? '',
|
|
4385
|
+
label: row[1] ?? '',
|
|
4386
|
+
description: row[2] ?? ''
|
|
4387
|
+
}));
|
|
4388
|
+
}
|
|
4389
|
+
#extractCustomProperties(properties) {
|
|
4390
|
+
const result = {};
|
|
4391
|
+
for (const [key, wrapper] of Object.entries(properties)) {
|
|
4392
|
+
if (key.startsWith('system:')) {
|
|
4393
|
+
continue;
|
|
4394
|
+
}
|
|
4395
|
+
const colonIndex = key.indexOf(':');
|
|
4396
|
+
const shortKey = colonIndex >= 0 ? key.substring(colonIndex + 1) : key;
|
|
4397
|
+
result[shortKey] = wrapper.value;
|
|
4398
|
+
}
|
|
4399
|
+
return result;
|
|
4223
4400
|
}
|
|
4401
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: CatalogService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
4402
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: CatalogService, providedIn: 'root' }); }
|
|
4403
|
+
}
|
|
4404
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: CatalogService, decorators: [{
|
|
4405
|
+
type: Injectable,
|
|
4406
|
+
args: [{
|
|
4407
|
+
providedIn: 'root'
|
|
4408
|
+
}]
|
|
4409
|
+
}] });
|
|
4410
|
+
|
|
4411
|
+
/**
|
|
4412
|
+
* Service for client-side caching of data with expiry handling.
|
|
4413
|
+
* Provides methods to get, update, and invalidate cache entries.
|
|
4414
|
+
*/
|
|
4415
|
+
class ClientCacheService {
|
|
4416
|
+
#appCacheService = inject(AppCacheService);
|
|
4417
|
+
#CLIENT_CACHE_PREFIX = 'yuv-client-cache:';
|
|
4224
4418
|
/**
|
|
4225
|
-
*
|
|
4226
|
-
* @param
|
|
4227
|
-
* @param
|
|
4228
|
-
* @
|
|
4419
|
+
* Get cached entry by ID.
|
|
4420
|
+
* @param id The ID of the cache entry.
|
|
4421
|
+
* @param keepIfExpired Whether to return/keep the entry even if it is expired.
|
|
4422
|
+
* @returns The cached data or null if not found/expired.
|
|
4229
4423
|
*/
|
|
4230
|
-
|
|
4231
|
-
return this.
|
|
4424
|
+
getFromCache(id, keepIfExpired = false) {
|
|
4425
|
+
return this.#appCacheService.getItem(this.#CLIENT_CACHE_PREFIX + id).pipe(map$1((cachedItem) => {
|
|
4426
|
+
if (cachedItem) {
|
|
4427
|
+
const expired = cachedItem.expires !== undefined && cachedItem.expires <= Date.now();
|
|
4428
|
+
if (!expired || keepIfExpired) {
|
|
4429
|
+
return cachedItem.data;
|
|
4430
|
+
}
|
|
4431
|
+
else {
|
|
4432
|
+
this.invalidateCache(id);
|
|
4433
|
+
return null;
|
|
4434
|
+
}
|
|
4435
|
+
}
|
|
4436
|
+
return null;
|
|
4437
|
+
}));
|
|
4232
4438
|
}
|
|
4233
4439
|
/**
|
|
4234
|
-
*
|
|
4235
|
-
*
|
|
4236
|
-
* @param
|
|
4237
|
-
* @param
|
|
4238
|
-
* @param options options for the copy operation
|
|
4440
|
+
* Update or add cache entry.
|
|
4441
|
+
* @param id The ID of the cache entry.
|
|
4442
|
+
* @param data The data to cache.
|
|
4443
|
+
* @param ttl TimeToLeave - The cache expiry time in milliseconds (how long to keep the cache entry valid).
|
|
4239
4444
|
*/
|
|
4240
|
-
|
|
4241
|
-
const
|
|
4242
|
-
|
|
4243
|
-
BaseObjectTypeField.CREATED_BY,
|
|
4244
|
-
BaseObjectTypeField.CREATION_DATE,
|
|
4245
|
-
BaseObjectTypeField.MODIFIED_BY,
|
|
4246
|
-
BaseObjectTypeField.MODIFICATION_DATE,
|
|
4247
|
-
BaseObjectTypeField.VERSION_NUMBER,
|
|
4248
|
-
BaseObjectTypeField.TRACE_ID,
|
|
4249
|
-
BaseObjectTypeField.PARENT_OBJECT_TYPE_ID,
|
|
4250
|
-
BaseObjectTypeField.PARENT_VERSION_NUMBER
|
|
4251
|
-
];
|
|
4252
|
-
const mappedObjects = objects.map((o) => {
|
|
4253
|
-
o.data[BaseObjectTypeField.PARENT_ID] = targetFolderId;
|
|
4254
|
-
// Remove properties that are not allowed to be copied.
|
|
4255
|
-
// This also includes content stream fields bundled into the objects data field.
|
|
4256
|
-
// Properties ending with '_title' are also excluded as they are enrichment values
|
|
4257
|
-
// provided by API-Web.
|
|
4258
|
-
Object.keys(o.data)
|
|
4259
|
-
.filter((key) => key.endsWith('_title') || excludeProperties.includes(key) || Object.values(ContentStreamField).includes(key))
|
|
4260
|
-
.forEach((key) => Reflect.deleteProperty(o.data, key));
|
|
4261
|
-
// map filtered data field to data format required by the Core-API
|
|
4262
|
-
const properties = Object.keys(o.data).reduce((acc, key) => {
|
|
4263
|
-
acc[key] = { value: o.data[key] };
|
|
4264
|
-
return acc;
|
|
4265
|
-
}, {});
|
|
4266
|
-
return {
|
|
4267
|
-
properties: properties,
|
|
4268
|
-
objectTypeId: o.objectTypeId,
|
|
4269
|
-
...(o.content
|
|
4270
|
-
? {
|
|
4271
|
-
contentStreams: [o.content]
|
|
4272
|
-
}
|
|
4273
|
-
: {})
|
|
4274
|
-
};
|
|
4275
|
-
});
|
|
4276
|
-
const query = options
|
|
4277
|
-
? Object.keys(options)
|
|
4278
|
-
.map((key) => `${key}=${options[key]}`)
|
|
4279
|
-
.join('&')
|
|
4280
|
-
: '';
|
|
4281
|
-
return this.#backend.post(`/dms/objects?${query}`, { objects: mappedObjects }, ApiBase.core);
|
|
4445
|
+
updateCache(id, data, ttl) {
|
|
4446
|
+
const cacheEntry = { id, expires: ttl ? ttl + Date.now() : undefined, data };
|
|
4447
|
+
return this.#appCacheService.setItem(this.#CLIENT_CACHE_PREFIX + id, cacheEntry).pipe(map$1(() => cacheEntry));
|
|
4282
4448
|
}
|
|
4283
4449
|
/**
|
|
4284
|
-
*
|
|
4285
|
-
* @param
|
|
4450
|
+
* Invalidate cache entry by ID. This will remove the entry from the cache.
|
|
4451
|
+
* @param id The ID of the cache entry to invalidate.
|
|
4286
4452
|
*/
|
|
4287
|
-
|
|
4288
|
-
return this.
|
|
4289
|
-
.pipe(map((_res) => _res.map((res, i) => res?._error
|
|
4290
|
-
? { ...res, id: ids[i] }
|
|
4291
|
-
: this.#searchResultToDmsObject(this.#searchService.toSearchResult(res).items[0]))))
|
|
4292
|
-
.pipe(this.#triggerEvents(YuvEventType.DMS_OBJECT_LOADED, undefined, silent));
|
|
4453
|
+
invalidateCache(id) {
|
|
4454
|
+
return this.#appCacheService.removeItem(this.#CLIENT_CACHE_PREFIX + id);
|
|
4293
4455
|
}
|
|
4294
4456
|
/**
|
|
4295
|
-
*
|
|
4296
|
-
* @param ids List of IDs of objects to be deleted
|
|
4297
|
-
* @param options Options for the delete operation
|
|
4298
|
-
* @returns Array of delete results.
|
|
4457
|
+
* Clear all client cache entries.
|
|
4299
4458
|
*/
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4459
|
+
clear() {
|
|
4460
|
+
return this.#appCacheService.clear((k) => k.startsWith(this.#CLIENT_CACHE_PREFIX));
|
|
4461
|
+
}
|
|
4462
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ClientCacheService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
4463
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ClientCacheService, providedIn: 'root' }); }
|
|
4464
|
+
}
|
|
4465
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ClientCacheService, decorators: [{
|
|
4466
|
+
type: Injectable,
|
|
4467
|
+
args: [{
|
|
4468
|
+
providedIn: 'root'
|
|
4469
|
+
}]
|
|
4470
|
+
}] });
|
|
4471
|
+
|
|
4472
|
+
class ClipboardService {
|
|
4473
|
+
#emptyClipboard = {
|
|
4474
|
+
buckets: undefined,
|
|
4475
|
+
main: undefined
|
|
4476
|
+
};
|
|
4477
|
+
#clipboard = structuredClone(this.#emptyClipboard);
|
|
4478
|
+
#clipboardSource = new ReplaySubject();
|
|
4479
|
+
#clipboard$ = this.#clipboardSource.asObservable();
|
|
4480
|
+
#pasteEvents = merge(fromEvent(window, 'keydown').pipe(filter$1((event) => event.ctrlKey && event.code === 'KeyV')), fromEvent(window, 'paste').pipe(filter$1((event) => !!event.clipboardData && event.clipboardData.files.length > 0)));
|
|
4481
|
+
clipboard(bucket) {
|
|
4482
|
+
return toSignal(this.clipboard$(bucket));
|
|
4483
|
+
}
|
|
4484
|
+
clipboard$(bucket) {
|
|
4485
|
+
return this.#clipboard$.pipe(map$1((cs) => {
|
|
4486
|
+
if (bucket) {
|
|
4487
|
+
return cs.buckets ? cs.buckets[bucket] : undefined;
|
|
4322
4488
|
}
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4489
|
+
else
|
|
4490
|
+
return cs.main;
|
|
4491
|
+
}));
|
|
4492
|
+
}
|
|
4493
|
+
paste$(bucket) {
|
|
4494
|
+
return this.#pasteEvents.pipe(map$1((e) => {
|
|
4495
|
+
let cd;
|
|
4496
|
+
if (e instanceof ClipboardEvent) {
|
|
4497
|
+
const fileList = e.clipboardData.files;
|
|
4498
|
+
const files = [];
|
|
4499
|
+
for (let i = 0; i < fileList.length; i++) {
|
|
4500
|
+
files.push(fileList.item(i));
|
|
4501
|
+
}
|
|
4502
|
+
cd = files.length
|
|
4503
|
+
? {
|
|
4504
|
+
files
|
|
4505
|
+
}
|
|
4506
|
+
: undefined;
|
|
4507
|
+
}
|
|
4508
|
+
else {
|
|
4509
|
+
cd = bucket ? (this.#clipboard.buckets ? this.#clipboard.buckets[bucket] : undefined) : this.#clipboard.main;
|
|
4510
|
+
}
|
|
4511
|
+
return cd;
|
|
4512
|
+
}), filter$1((cd) => cd !== undefined && (cd.files || cd.objects)), map$1((cd) => cd));
|
|
4513
|
+
}
|
|
4514
|
+
getClipboardData(bucket) {
|
|
4515
|
+
if (bucket) {
|
|
4516
|
+
return this.#clipboard.buckets ? this.#clipboard.buckets[bucket] : undefined;
|
|
4517
|
+
}
|
|
4518
|
+
else
|
|
4519
|
+
return this.#clipboard.main;
|
|
4333
4520
|
}
|
|
4334
4521
|
/**
|
|
4335
|
-
*
|
|
4336
|
-
* @param
|
|
4522
|
+
* Add objects to the clipboard
|
|
4523
|
+
* @param bucket Buckets are ways to separate data from the global scope.
|
|
4524
|
+
* If you have an app that would like to have a separate section in the clipboard
|
|
4525
|
+
* you'll use a unique bucket to store your stuff. When observing chages to the
|
|
4526
|
+
* clipboard you couls as well provide the bucket and you will only get
|
|
4337
4527
|
*/
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4528
|
+
addObjects(objects, mode, bucket) {
|
|
4529
|
+
const cd = { mode, objects };
|
|
4530
|
+
if (bucket) {
|
|
4531
|
+
if (!this.#clipboard.buckets)
|
|
4532
|
+
this.#clipboard.buckets = {};
|
|
4533
|
+
this.#clipboard.buckets[bucket] = cd;
|
|
4534
|
+
}
|
|
4535
|
+
else
|
|
4536
|
+
this.#clipboard.main = cd;
|
|
4537
|
+
this.#clipboardSource.next(this.#clipboard);
|
|
4343
4538
|
}
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
.
|
|
4347
|
-
|
|
4539
|
+
clear(bucket) {
|
|
4540
|
+
if (bucket && this.#clipboard.buckets) {
|
|
4541
|
+
delete this.#clipboard.buckets[bucket];
|
|
4542
|
+
}
|
|
4543
|
+
else {
|
|
4544
|
+
this.#clipboard = structuredClone(this.#emptyClipboard);
|
|
4545
|
+
}
|
|
4546
|
+
this.#clipboardSource.next(this.#clipboard);
|
|
4348
4547
|
}
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4548
|
+
async addToNavigatorClipBoard(data) {
|
|
4549
|
+
try {
|
|
4550
|
+
await navigator.clipboard.writeText(data);
|
|
4551
|
+
//console.log('Text copied to clipboard');
|
|
4552
|
+
}
|
|
4553
|
+
catch (error) {
|
|
4554
|
+
console.error('Failed to copy text: ', error);
|
|
4555
|
+
}
|
|
4355
4556
|
}
|
|
4557
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ClipboardService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
4558
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ClipboardService, providedIn: 'root' }); }
|
|
4559
|
+
}
|
|
4560
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ClipboardService, decorators: [{
|
|
4561
|
+
type: Injectable,
|
|
4562
|
+
args: [{
|
|
4563
|
+
providedIn: 'root'
|
|
4564
|
+
}]
|
|
4565
|
+
}] });
|
|
4566
|
+
|
|
4567
|
+
/**
|
|
4568
|
+
* Service to monitor the online/offline state of the application.
|
|
4569
|
+
* It listens to the browser's online and offline events and provides an observable
|
|
4570
|
+
* to track the current connection state.
|
|
4571
|
+
*
|
|
4572
|
+
* An observable `connection$` is provided which emits the current connection state
|
|
4573
|
+
* whenever the online or offline state changes. The initial state is determined by
|
|
4574
|
+
* the `window.navigator.onLine` property.
|
|
4575
|
+
*/
|
|
4576
|
+
class ConnectionService {
|
|
4356
4577
|
/**
|
|
4357
|
-
*
|
|
4358
|
-
* @param data The plain data object
|
|
4359
|
-
* @returns DmsObject
|
|
4578
|
+
* @ignore
|
|
4360
4579
|
*/
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4580
|
+
constructor() {
|
|
4581
|
+
this.currentState = {
|
|
4582
|
+
isOnline: window.navigator.onLine
|
|
4583
|
+
};
|
|
4584
|
+
this.connectionStateSource = new ReplaySubject();
|
|
4585
|
+
this.connection$ = this.connectionStateSource.asObservable();
|
|
4586
|
+
this.connectionStateSource.next(this.currentState);
|
|
4587
|
+
fromEvent(window, 'online').subscribe(() => {
|
|
4588
|
+
this.currentState.isOnline = true;
|
|
4589
|
+
this.connectionStateSource.next(this.currentState);
|
|
4590
|
+
});
|
|
4591
|
+
fromEvent(window, 'offline').subscribe(() => {
|
|
4592
|
+
this.currentState.isOnline = false;
|
|
4593
|
+
this.connectionStateSource.next(this.currentState);
|
|
4594
|
+
});
|
|
4595
|
+
}
|
|
4596
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ConnectionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
4597
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ConnectionService, providedIn: 'root' }); }
|
|
4598
|
+
}
|
|
4599
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ConnectionService, decorators: [{
|
|
4600
|
+
type: Injectable,
|
|
4601
|
+
args: [{
|
|
4602
|
+
providedIn: 'root'
|
|
4603
|
+
}]
|
|
4604
|
+
}], ctorParameters: () => [] });
|
|
4605
|
+
|
|
4606
|
+
/**
|
|
4607
|
+
* Providing functions,that are are injected at application startup and executed during app initialization.
|
|
4608
|
+
*/
|
|
4609
|
+
const init_moduleFnc = () => {
|
|
4610
|
+
const coreConfig = inject(CORE_CONFIG);
|
|
4611
|
+
const logger = inject(Logger);
|
|
4612
|
+
const http = inject(HttpClient);
|
|
4613
|
+
const configService = inject(ConfigService);
|
|
4614
|
+
const authService = inject(AuthService);
|
|
4615
|
+
return authService.setInitialRequestUri().pipe(switchMap(() => coreConfig.main
|
|
4616
|
+
? !Array.isArray(coreConfig.main)
|
|
4617
|
+
? of([coreConfig.main])
|
|
4618
|
+
: forkJoin([...coreConfig.main].map((uri) => http.get(`${Utils.getBaseHref()}${uri}`).pipe(catchError((e) => {
|
|
4619
|
+
logger.error('failed to catch config file', e);
|
|
4620
|
+
return of({});
|
|
4621
|
+
}), map((res) => res)))).pipe(
|
|
4622
|
+
// switchMap((configs: YuvConfig[]) => configService.extendConfig(configs)),
|
|
4623
|
+
tap((configs) => configService.extendConfig(configs)), switchMap(() => authService.initUser().pipe(catchError((e) => {
|
|
4624
|
+
authService.initError = {
|
|
4625
|
+
status: e.status,
|
|
4626
|
+
key: e.error.error
|
|
4627
|
+
};
|
|
4628
|
+
return of(true);
|
|
4629
|
+
}))))
|
|
4630
|
+
: of(false)));
|
|
4631
|
+
};
|
|
4632
|
+
|
|
4633
|
+
var DeviceScreenOrientation;
|
|
4634
|
+
(function (DeviceScreenOrientation) {
|
|
4635
|
+
DeviceScreenOrientation["PORTRAIT"] = "portrait";
|
|
4636
|
+
DeviceScreenOrientation["LANDSCAPE"] = "landscape";
|
|
4637
|
+
})(DeviceScreenOrientation || (DeviceScreenOrientation = {}));
|
|
4638
|
+
|
|
4639
|
+
/**
|
|
4640
|
+
* @deprecated This service is deprecated. Please use the DeviceService from the `@yuuvis/material` package instead.
|
|
4641
|
+
*
|
|
4642
|
+
* This service is used to adapt styles and designs of the client to
|
|
4643
|
+
* different devices and screen sizes.
|
|
4644
|
+
*
|
|
4645
|
+
* Using `screenChange$` observable you are able to monitor changes to
|
|
4646
|
+
* the screen size and act upon it.
|
|
4647
|
+
*
|
|
4648
|
+
* This service will also adds attributes to the body tag that reflect the
|
|
4649
|
+
* current screen/device state. This way you can apply secific styles in your
|
|
4650
|
+
* css files for different screen resolutions and orientations.
|
|
4651
|
+
*
|
|
4652
|
+
* Attributes applied to the body tag are:
|
|
4653
|
+
*
|
|
4654
|
+
* - `data-screen` - [s, m, l, xl] - for different screen sizes
|
|
4655
|
+
* (s: for mobile phone like screen sizes, m: for tablet like screen
|
|
4656
|
+
* sizes, 'l': for desktop like screen sizes, 'xl': for screen sizes exceeding
|
|
4657
|
+
* the desktop screen size).
|
|
4658
|
+
*
|
|
4659
|
+
* - `data-orientation` - [portrait, landscape] - for the current screen orientation
|
|
4660
|
+
*
|
|
4661
|
+
* - `data-touch-enabled` - [true] - if the device has touch capabilities (won't be added if the device doesn't have touch capabilities)
|
|
4662
|
+
*
|
|
4663
|
+
* ```html
|
|
4664
|
+
* <body data-screen-size="s" data-screen-orientation="portrait" data-touch-enabled="true">
|
|
4665
|
+
* ...
|
|
4666
|
+
* </body>
|
|
4667
|
+
* ```
|
|
4668
|
+
*/
|
|
4669
|
+
class DeviceService {
|
|
4670
|
+
#deviceDetectorService;
|
|
4671
|
+
#upperScreenBoundary;
|
|
4672
|
+
#resize$;
|
|
4673
|
+
#screen;
|
|
4674
|
+
#screenSource;
|
|
4675
|
+
#supportsSmallScreens;
|
|
4676
|
+
constructor() {
|
|
4677
|
+
this.#deviceDetectorService = inject(DeviceDetectorService);
|
|
4678
|
+
this.#upperScreenBoundary = {
|
|
4679
|
+
small: 600,
|
|
4680
|
+
mediumPortrait: 900,
|
|
4681
|
+
mediumLandscape: 1200,
|
|
4682
|
+
large: 1800
|
|
4376
4683
|
};
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
}))
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4684
|
+
this.#resize$ = fromEvent(window, 'resize').pipe(debounceTime(this.#getDebounceTime()));
|
|
4685
|
+
this.#screenSource = new ReplaySubject(1);
|
|
4686
|
+
this.screenChange$ = this.#screenSource.asObservable();
|
|
4687
|
+
/**
|
|
4688
|
+
* Signal to indicate if the screen size is small (e.g. mobile phone).
|
|
4689
|
+
* This will only be triggered if `supportsSmallScreens` is set to true.
|
|
4690
|
+
* Major components will use this metric to adapt to 'small screen behavior' and so can you
|
|
4691
|
+
*/
|
|
4692
|
+
this.smallScreenLayout = signal(false, ...(ngDevMode ? [{ debugName: "smallScreenLayout" }] : /* istanbul ignore next */ []));
|
|
4693
|
+
this.#supportsSmallScreens = signal(false, ...(ngDevMode ? [{ debugName: "#supportsSmallScreens" }] : /* istanbul ignore next */ []));
|
|
4694
|
+
/**
|
|
4695
|
+
* if the device is a mobile device (android / iPhone / windows-phone etc)
|
|
4696
|
+
*/
|
|
4697
|
+
this.isMobile = this.#deviceDetectorService.isMobile();
|
|
4698
|
+
/**
|
|
4699
|
+
* if the device us a tablet (iPad etc)
|
|
4700
|
+
*/
|
|
4701
|
+
this.isTablet = this.#deviceDetectorService.isTablet();
|
|
4702
|
+
/**
|
|
4703
|
+
* if the app is running on a Desktop browser
|
|
4704
|
+
*/
|
|
4705
|
+
this.isDesktop = this.#deviceDetectorService.isDesktop();
|
|
4706
|
+
this.info = this.#deviceDetectorService.getDeviceInfo();
|
|
4707
|
+
this.isTouchEnabled = this.#isTouchEnabled();
|
|
4708
|
+
this.#resize$.subscribe((e) => {
|
|
4709
|
+
this.#setScreen();
|
|
4710
|
+
});
|
|
4393
4711
|
}
|
|
4394
|
-
|
|
4395
|
-
|
|
4712
|
+
init(supportsSmallScreens = false) {
|
|
4713
|
+
this.#supportsSmallScreens.set(supportsSmallScreens);
|
|
4714
|
+
this.#setScreen();
|
|
4396
4715
|
}
|
|
4397
|
-
|
|
4398
|
-
return
|
|
4716
|
+
#isTouchEnabled() {
|
|
4717
|
+
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
|
4399
4718
|
}
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
Object.keys(o.properties).forEach((key) => {
|
|
4411
|
-
let value = o.properties[key].value;
|
|
4412
|
-
if (o.properties[key].clvalue) {
|
|
4413
|
-
// table fields will have a clientValue too ...
|
|
4414
|
-
value = o.properties[key].clvalue;
|
|
4415
|
-
// ... and also may contain values that need to be resolved
|
|
4416
|
-
if (o.properties[key].resolvedValues) {
|
|
4417
|
-
value.forEach((v) => {
|
|
4418
|
-
Object.keys(v).forEach((k) => {
|
|
4419
|
-
const resValue = Array.isArray(v[k])
|
|
4420
|
-
? v[k].map((i) => o.properties[key].resolvedValues[i])
|
|
4421
|
-
: o.properties[key].resolvedValues[v[k]];
|
|
4422
|
-
if (resValue) {
|
|
4423
|
-
v[`${k}_title`] = resValue;
|
|
4424
|
-
}
|
|
4425
|
-
});
|
|
4426
|
-
});
|
|
4427
|
-
}
|
|
4428
|
-
}
|
|
4429
|
-
fields.set(key, value);
|
|
4430
|
-
if (o.properties[key].title) {
|
|
4431
|
-
fields.set(key + '_title', o.properties[key].title);
|
|
4432
|
-
}
|
|
4433
|
-
});
|
|
4434
|
-
// process contentStreams section of result if available.
|
|
4435
|
-
// Objects that don't have files attached won't have this section
|
|
4436
|
-
let content;
|
|
4437
|
-
if (o.contentStreams && o.contentStreams.length > 0) {
|
|
4438
|
-
// we assume that each result object only has ONE file attached, although
|
|
4439
|
-
// this is an array and there may be more
|
|
4440
|
-
const contentStream = o.contentStreams[0];
|
|
4441
|
-
// also add content-stream related fields to the result fields
|
|
4442
|
-
fields.set(ContentStreamField.LENGTH, contentStream.length);
|
|
4443
|
-
fields.set(ContentStreamField.MIME_TYPE, contentStream.mimeType);
|
|
4444
|
-
fields.set(ContentStreamField.FILENAME, contentStream.fileName);
|
|
4445
|
-
fields.set(ContentStreamField.ID, contentStream.contentStreamId);
|
|
4446
|
-
fields.set(ContentStreamField.RANGE, contentStream.contentStreamRange);
|
|
4447
|
-
fields.set(ContentStreamField.REPOSITORY_ID, contentStream.repositoryId);
|
|
4448
|
-
fields.set(ContentStreamField.DIGEST, contentStream.digest);
|
|
4449
|
-
fields.set(ContentStreamField.ARCHIVE_PATH, contentStream.archivePath);
|
|
4450
|
-
content = {
|
|
4451
|
-
contentStreamId: contentStream.contentStreamId,
|
|
4452
|
-
repositoryId: contentStream.repositoryId,
|
|
4453
|
-
range: contentStream.range,
|
|
4454
|
-
digest: contentStream.digest,
|
|
4455
|
-
archivePath: contentStream.archivePath,
|
|
4456
|
-
fileName: contentStream.fileName,
|
|
4457
|
-
mimeType: contentStream.mimeType,
|
|
4458
|
-
size: contentStream.length
|
|
4459
|
-
};
|
|
4719
|
+
#setScreen() {
|
|
4720
|
+
const bounds = {
|
|
4721
|
+
width: window.innerWidth,
|
|
4722
|
+
height: window.innerHeight
|
|
4723
|
+
};
|
|
4724
|
+
let orientation = bounds.width >= bounds.height ? DeviceScreenOrientation.LANDSCAPE : DeviceScreenOrientation.PORTRAIT;
|
|
4725
|
+
if (this.isMobile && window.screen['orientation']) {
|
|
4726
|
+
const screenOrientation = window.screen['orientation'].type;
|
|
4727
|
+
if (screenOrientation === 'landscape-primary' || screenOrientation === 'landscape-secondary') {
|
|
4728
|
+
orientation = DeviceScreenOrientation.LANDSCAPE;
|
|
4460
4729
|
}
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
: null;
|
|
4464
|
-
if (!objectTypes.includes(objectTypeId)) {
|
|
4465
|
-
objectTypes.push(objectTypeId);
|
|
4730
|
+
else if (screenOrientation === 'portrait-primary' || screenOrientation === 'portrait-secondary') {
|
|
4731
|
+
orientation = DeviceScreenOrientation.PORTRAIT;
|
|
4466
4732
|
}
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
});
|
|
4474
|
-
const result = {
|
|
4475
|
-
hasMoreItems: searchResponse.hasMoreItems,
|
|
4476
|
-
totalNumItems: searchResponse.totalNumItems,
|
|
4477
|
-
items: resultListItems,
|
|
4478
|
-
objectTypes
|
|
4733
|
+
}
|
|
4734
|
+
this.#screen = {
|
|
4735
|
+
size: this.#getScreenSize(bounds, orientation),
|
|
4736
|
+
orientation,
|
|
4737
|
+
width: bounds.width,
|
|
4738
|
+
height: bounds.height
|
|
4479
4739
|
};
|
|
4480
|
-
|
|
4740
|
+
this.#screenSource.next(this.#screen);
|
|
4741
|
+
this.smallScreenLayout.set(this.#supportsSmallScreens() && this.#screen.size === 's' && this.#screen.orientation === 'portrait');
|
|
4742
|
+
this.#setupDOM(this.#screen);
|
|
4743
|
+
// force change detection because resize will not be recognized by Angular in some cases
|
|
4744
|
+
// TODO: check: causes recursive ticks in some cases ...
|
|
4745
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
4746
|
+
setTimeout(() => { }, 0);
|
|
4481
4747
|
}
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
tap((res) => !silent && this.#eventService.trigger(event, res)));
|
|
4748
|
+
#setupDOM(screen) {
|
|
4749
|
+
const body = document.querySelector('body');
|
|
4750
|
+
body.setAttribute('data-screen-size', screen.size);
|
|
4751
|
+
body.setAttribute('data-screen-orientation', screen.orientation);
|
|
4752
|
+
if (this.isTouchEnabled)
|
|
4753
|
+
body.setAttribute('data-touch-enabled', 'true');
|
|
4754
|
+
else
|
|
4755
|
+
body.removeAttribute('data-touch-enabled');
|
|
4491
4756
|
}
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4757
|
+
#getScreenSize(bounds, orientation) {
|
|
4758
|
+
if (this.#isBelow(this.#upperScreenBoundary.small, bounds)) {
|
|
4759
|
+
return 's';
|
|
4760
|
+
}
|
|
4761
|
+
else if (this.#isBelow(orientation === 'landscape' ? this.#upperScreenBoundary.mediumLandscape : this.#upperScreenBoundary.mediumPortrait, bounds)) {
|
|
4762
|
+
return 'm';
|
|
4763
|
+
}
|
|
4764
|
+
else if (this.#isBelow(this.#upperScreenBoundary.large, bounds)) {
|
|
4765
|
+
return 'l';
|
|
4766
|
+
}
|
|
4767
|
+
else {
|
|
4768
|
+
return 'xl';
|
|
4769
|
+
}
|
|
4502
4770
|
}
|
|
4503
|
-
#
|
|
4504
|
-
|
|
4771
|
+
#isBelow(size, bounds) {
|
|
4772
|
+
const landscape = bounds.width < this.#upperScreenBoundary.large ? bounds.width >= bounds.height : false;
|
|
4773
|
+
return (landscape && bounds.height < size) || (!landscape && bounds.width < size);
|
|
4505
4774
|
}
|
|
4506
|
-
|
|
4507
|
-
|
|
4775
|
+
#getDebounceTime() {
|
|
4776
|
+
// on mobile devices resize only happens when rotating the device or when
|
|
4777
|
+
// keyboard appears, so we dont't need to debounce
|
|
4778
|
+
return this.isMobile ? 0 : 500;
|
|
4779
|
+
}
|
|
4780
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DeviceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
4781
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DeviceService, providedIn: 'root' }); }
|
|
4508
4782
|
}
|
|
4509
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type:
|
|
4783
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DeviceService, decorators: [{
|
|
4510
4784
|
type: Injectable,
|
|
4511
4785
|
args: [{
|
|
4512
4786
|
providedIn: 'root'
|
|
4513
4787
|
}]
|
|
4514
|
-
}] });
|
|
4788
|
+
}], ctorParameters: () => [] });
|
|
4515
4789
|
|
|
4516
4790
|
/**
|
|
4517
4791
|
* Centralized service for Identity Management (IDM) operations.
|
|
@@ -4717,14 +4991,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
4717
4991
|
}], ctorParameters: () => [] });
|
|
4718
4992
|
|
|
4719
4993
|
const YuvToastStyles = `
|
|
4720
|
-
.yuv-toast-container {
|
|
4994
|
+
.yuv-toast-container {
|
|
4721
4995
|
--yuv-toast-container-padding: var(--ymt-spacing-m);
|
|
4722
|
-
--yuv-toast-success-background: var(--ymt-success
|
|
4723
|
-
--yuv-toast-success-tone: var(--ymt-on-success
|
|
4724
|
-
--yuv-toast-warning-background: var(--ymt-warning
|
|
4725
|
-
--yuv-toast-warning-tone: var(--ymt-on-warning
|
|
4726
|
-
--yuv-toast-error-background: var(--ymt-danger
|
|
4727
|
-
--yuv-toast-error-tone: var(--ymt-on-danger
|
|
4996
|
+
--yuv-toast-success-background: var(--ymt-success);
|
|
4997
|
+
--yuv-toast-success-tone: var(--ymt-on-success);
|
|
4998
|
+
--yuv-toast-warning-background: var(--ymt-warning);
|
|
4999
|
+
--yuv-toast-warning-tone: var(--ymt-on-warning);
|
|
5000
|
+
--yuv-toast-error-background: var(--ymt-danger);
|
|
5001
|
+
--yuv-toast-error-tone: var(--ymt-on-danger);
|
|
4728
5002
|
|
|
4729
5003
|
position: fixed;
|
|
4730
5004
|
z-index: 9999;
|
|
@@ -4736,27 +5010,33 @@ const YuvToastStyles = `
|
|
|
4736
5010
|
gap: 1vh;
|
|
4737
5011
|
pointer-events: none;
|
|
4738
5012
|
}
|
|
5013
|
+
|
|
4739
5014
|
.yuv-toast-container.top {
|
|
4740
5015
|
inset-block-start: 0;
|
|
4741
5016
|
padding-block-start: var(--yuv-toast-container-padding);
|
|
4742
5017
|
flex-direction: column-reverse;
|
|
4743
5018
|
}
|
|
5019
|
+
|
|
4744
5020
|
.yuv-toast-container.top.center {
|
|
4745
5021
|
inset-inline: 0;
|
|
4746
5022
|
}
|
|
5023
|
+
|
|
4747
5024
|
.yuv-toast-container.bottom {
|
|
4748
5025
|
inset-block-end: 0;
|
|
4749
5026
|
padding-block-end: var(--yuv-toast-container-padding);
|
|
4750
5027
|
}
|
|
5028
|
+
|
|
4751
5029
|
.yuv-toast-container.bottom.center {
|
|
4752
5030
|
inset-inline: 0;
|
|
4753
5031
|
}
|
|
5032
|
+
|
|
4754
5033
|
.yuv-toast-container.end {
|
|
4755
5034
|
padding-inline-end: var(--yuv-toast-container-padding);
|
|
4756
5035
|
align-items: end;
|
|
4757
5036
|
right:0;
|
|
4758
|
-
}
|
|
4759
|
-
|
|
5037
|
+
}
|
|
5038
|
+
|
|
5039
|
+
.yuv-toast-container.start {
|
|
4760
5040
|
padding-inline-start: var(--yuv-toast-container-padding);
|
|
4761
5041
|
align-items: start;
|
|
4762
5042
|
left:0;
|
|
@@ -4785,8 +5065,6 @@ const YuvToastStyles = `
|
|
|
4785
5065
|
slide-in .3s ease,
|
|
4786
5066
|
fade-out .3s ease var(--_duration);
|
|
4787
5067
|
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
5068
|
@media (--motionOK) {
|
|
4791
5069
|
--_travel-distance: 5vh;
|
|
4792
5070
|
}
|
|
@@ -4804,6 +5082,7 @@ const YuvToastStyles = `
|
|
|
4804
5082
|
background-color: var(--yuv-toast-success-background);
|
|
4805
5083
|
color: var(--yuv-toast-success-tone);
|
|
4806
5084
|
}
|
|
5085
|
+
|
|
4807
5086
|
.yuv-toast.warning {
|
|
4808
5087
|
background-color: var(--yuv-toast-warning-background);
|
|
4809
5088
|
color: var(--yuv-toast-warning-tone);
|
|
@@ -6208,5 +6487,5 @@ const provideYuvClientCore = (options = { translations: [] }, customEvents, cust
|
|
|
6208
6487
|
* Generated bundle index. Do not edit.
|
|
6209
6488
|
*/
|
|
6210
6489
|
|
|
6211
|
-
export { AFO_STATE, AVAILABLE_BACKEND_APPS, AdministrationRoles, ApiBase, AppCacheService, AuditField, AuditService, AuthService, BackendService, BaseObjectTypeField, BpmService, CLIENT_APP_REQUIREMENTS, CORE_CONFIG, CUSTOM_CONFIG, CUSTOM_YUV_EVENT_PREFIX, CatalogService, Classification, ClassificationPrefix, ClientCacheService, ClientDefaultsObjectTypeField, ClipboardService, ColumnConfigSkipFields, ConfigService, ConnectionService, ContentStreamAllowed, ContentStreamField, CoreConfig, DeviceScreenOrientation, DeviceService, DialogCloseGuard, Direction, DmsObject, DmsService, EventService, FileSizePipe, IdmService, InternalFieldType, KeysPipe, LocaleCurrencyPipe, LocaleDatePipe, LocaleDecimalPipe, LocaleNumberPipe, LocalePercentPipe, Logger, LoginStateName, NativeNotificationService, NotificationService, ObjectConfigService, ObjectFormControl, ObjectFormControlWrapper, ObjectFormGroup, ObjectTag, ObjectTypeClassification, ObjectTypePropertyClassification, Operator, OperatorLabel, ParentField, PendingChangesGuard, PendingChangesService, PredictionService, ProcessAction, RelationshipTypeField, RetentionField, RetentionService, SafeHtmlPipe, SafeUrlPipe, SearchService, SearchUtils, SecondaryObjectTypeClassification, SessionStorageService, Situation, Sort, SystemResult, SystemSOT, SystemService, SystemType, TENANT_HEADER, TabGuardDirective, ToastService, UploadService, UserRoles, UserService, UserStorageService, Utils, YUV_USER, YuvError, YuvEventType, YuvUser, init_moduleFnc, provideAvailabilityManagement, provideBeforeUnloadProtection, provideNavigationProtection, providePopstateDialogProtection, provideRequirements, provideUser, provideYuvClientCore };
|
|
6490
|
+
export { AFO_STATE, AVAILABLE_BACKEND_APPS, AdministrationRoles, ApiBase, AppCacheService, AuditField, AuditService, AuthService, BackendService, BaseObjectTypeField, BpmService, CLIENT_APP_REQUIREMENTS, CORE_CONFIG, CUSTOM_CONFIG, CUSTOM_YUV_EVENT_PREFIX, CatalogService, CatalogTypeField, Classification, ClassificationPrefix, ClientCacheService, ClientDefaultsObjectTypeField, ClipboardService, ColumnConfigSkipFields, ConfigService, ConnectionService, ContentStreamAllowed, ContentStreamField, CoreConfig, DeviceScreenOrientation, DeviceService, DialogCloseGuard, Direction, DmsObject, DmsService, EventService, FileSizePipe, IdmService, InternalFieldType, KeysPipe, LOCALIZATION_COLUMNS, LocaleCurrencyPipe, LocaleDatePipe, LocaleDecimalPipe, LocaleNumberPipe, LocalePercentPipe, LocalizationService, Logger, LoginStateName, NativeNotificationService, NotificationService, ObjectConfigService, ObjectFormControl, ObjectFormControlWrapper, ObjectFormGroup, ObjectTag, ObjectTypeClassification, ObjectTypePropertyClassification, Operator, OperatorLabel, ParentField, PendingChangesGuard, PendingChangesService, PredictionService, ProcessAction, RelationshipTypeField, RetentionField, RetentionService, SafeHtmlPipe, SafeUrlPipe, SearchService, SearchUtils, SecondaryObjectTypeClassification, SessionStorageService, Situation, Sort, SystemResult, SystemSOT, SystemService, SystemType, TENANT_HEADER, TabGuardDirective, ToastService, UploadService, UserRoles, UserService, UserStorageService, Utils, YUV_USER, YuvError, YuvEventType, YuvUser, init_moduleFnc, provideAvailabilityManagement, provideBeforeUnloadProtection, provideNavigationProtection, providePopstateDialogProtection, provideRequirements, provideUser, provideYuvClientCore };
|
|
6212
6491
|
//# sourceMappingURL=yuuvis-client-core.mjs.map
|