@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.
@@ -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, ReplaySubject, BehaviorSubject, tap as tap$1, map as map$1, merge, fromEvent, filter as filter$1, debounceTime, throwError, catchError as catchError$1, switchMap as switchMap$1, isObservable } from 'rxjs';
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 ? objectTypes.map((t) => ({ ...t, label: this.getLocalizedResource(`${t.id}_label`) })) : objectTypes;
1405
- }
1406
- #sotToGenericType(sot) {
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 ((withLabels ? sots.map((t) => ({ ...t, label: this.getLocalizedResource(`${t.id}_label`) })) : sots)
1422
- // ignore
1423
- .filter((t) => t.id !== t.baseId && !t.id.startsWith('system:') && t.id !== 'appClientsystem:leadingType'));
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 ? this.getBaseType() : this.system.objectTypes.find((ot) => ot.id === objectTypeId);
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((ot) => ot.id === objectTypeId);
1515
+ const objectType = this.system.secondaryObjectTypes.find((objectType) => objectType.id === objectTypeId);
1458
1516
  if (objectType && withLabel) {
1459
- objectType.label = this.getLocalizedResource(`${objectType.id}_label`) || objectType.id;
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((r) => r.id === id);
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((f) => f.id);
1510
- const baseTypeFields = sysDocument.fields.filter((f) => folderTypeFieldIDs.includes(f.id));
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 ot = this.getObjectType(objectTypeId);
1530
- if (!ot) {
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: ot.id,
1540
- fields: ot.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 ci = this.#getIconFromClassification(objectTypeId);
1648
- const fb = this.getFallbackIcon(objectTypeId, fallback);
1649
- const uri = `/resources/icons/${encodeURIComponent(ci || objectTypeId)}${fb ? `?fallback=${encodeURIComponent(fb)}` : ''}`;
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
- getFallbackIcon(objectTypeId, fallback) {
1655
- const ot = this.getObjectType(objectTypeId);
1656
- if (ot && !fallback) {
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
- try {
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.getLocalizedResource(`${id}_label`);
1653
+ return this.#localization.getLocalizedLabel(id);
1680
1654
  }
1655
+ /**
1656
+ * @deprecated use LocalizationService.getLocalizedResource instead
1657
+ */
1681
1658
  getLocalizedDescription(id) {
1682
- return this.getLocalizedResource(`${id}_description`);
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
- * Actually fetch the system definition from the backend.
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, localizedResource = {}) {
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((p) => {
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(p.id)) {
1754
- p.classifications = [Classification.STRING_ORGANIZATION];
1710
+ if (orgTypeFields.includes(propDef.id)) {
1711
+ propDef.classifications = [Classification.STRING_ORGANIZATION];
1755
1712
  }
1756
- propertiesQA[p.id] = p;
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((ot) => {
1761
- objectTypesQA[ot.id] = ot;
1717
+ schemaResponse.typeFolderDefinition.forEach((objType) => {
1718
+ objectTypesQA[objType.id] = objType;
1762
1719
  });
1763
- schemaResponse.typeDocumentDefinition.forEach((ot) => {
1764
- objectTypesQA[ot.id] = ot;
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((fd) => ({
1772
- id: fd.id,
1773
- description: fd.description,
1774
- classification: fd.classification,
1775
- baseId: fd.baseId,
1776
- creatable: this.#isCreatable(fd.id),
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: fd.secondaryObjectTypeId ? fd.secondaryObjectTypeId.map((t) => ({ id: t.value, static: t.static })) : [],
1780
- fields: this.#resolveObjectTypeFields(fd, propertiesQA, objectTypesQA)
1781
- // rawFields: this.resolveObjectTypeFields(fd, propertiesQA, objectTypesQA, true),
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((dd) => ({
1785
- id: dd.id,
1786
- description: dd.description,
1787
- classification: dd.classification,
1788
- baseId: dd.baseId,
1789
- creatable: this.#isCreatable(dd.id),
1790
- contentStreamAllowed: dd.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: dd.secondaryObjectTypeId ? dd.secondaryObjectTypeId.map((t) => ({ id: t.value, static: t.static })) : [],
1793
- fields: this.#resolveObjectTypeFields(dd, propertiesQA, objectTypesQA)
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((t) => t.objectTypeReference),
1814
- allowedTargetTypes: std.allowedTargetType.map((t) => t.objectTypeReference)
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((o) => this.getObjectTypeForm(o, situation).pipe(catchError((e) => of(null)), map((res) => ({
1865
- id: o,
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((res) => {
1797
+ }))))).pipe(map((modelRes) => {
1868
1798
  const resMap = {};
1869
- res
1870
- .map((r) => (!r.formModel ? { ...r, formModel: this.#generateDefaultFormModel(r.id) } : r))
1871
- .filter((r) => this.#formHasElements(r.formModel))
1872
- .forEach((r) => (resMap[r.id] = r.formModel));
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 (type === 'string' && _classifications.has(Classification.STRING_REFERENCE)) {
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 f = this.system?.allFields[id];
1970
- return f ? { ...f, _internalType: this.getInternalFormElementType(f.propertyType, f.classifications) } : undefined;
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((c) => c.replace(/\s+/g, ''))
1988
- .filter((c) => c.length > 0)
1989
- .forEach((c) => {
1990
- const matches = c.match(/^([^\[]*)(\[(.*)\])?$/);
1991
- if (matches && matches.length) {
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((o) => o.trim()) : []
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 { ...field, label: this.getLocalizedLabel(field.id), name: field.id, type: field.propertyType };
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
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: SystemService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2028
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: SystemService, providedIn: 'root' }); }
2029
- }
2030
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: SystemService, decorators: [{
2031
- type: Injectable,
2032
- args: [{
2033
- providedIn: 'root'
2034
- }]
2035
- }] });
2036
-
2037
- class SearchService {
2038
- #backend = inject(BackendService);
2039
- #system = inject(SystemService);
2040
- static { this.DEFAULT_QUERY_SIZE = 50; }
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
- * Execute a search query ans transform the result to a SearchResult object
2043
- * @param query The search query
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
- search(query) {
2047
- return this.searchRaw(query).pipe(map((res) => this.toSearchResult(res, query.size || SearchService.DEFAULT_QUERY_SIZE, query.from || 0)));
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
- * Execute a raw search query and return the result as is.
2051
- * @param query The search query
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
- searchRaw(query) {
2055
- if (!query.size)
2056
- query.size = SearchService.DEFAULT_QUERY_SIZE;
2057
- return this.#backend.post(`/dms/objects/search`, query);
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
- * Search for objects in the dms using CMIS like SQL syntax.
2061
- * @param statement The query statement
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
- #USERS_SETTINGS;
2437
- #DEFAULT_SETTINGS;
2438
- #SETTINGS_SECTION_OBJECTCONFIG;
2439
- #user;
2440
- #userSource;
2441
- #document;
2442
- /**
2443
- * @ignore
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
- getUiDirection(iso) {
2462
- // languages that are read right to left
2463
- const rtlLanguages = ['ar', 'arc', 'dv', 'fa', 'ha', 'he', 'khw', 'ks', 'ku', 'ps', 'ur', 'yi'];
2464
- return rtlLanguages.indexOf(iso) === -1 ? Direction.LTR : Direction.RTL;
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.config.get('core.permissions.manageSettingsRole');
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.config.get('core.permissions.advancedUserRole');
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.config.get('core.permissions.retentionManagerRole');
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.config.getClientLocales().map((lang) => lang.iso);
2509
- iso = iso || this.#user.getClientLocale(this.config.getDefaultClientLocale());
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.config.getDefaultClientLocale();
2546
+ iso = this.#config.getDefaultClientLocale();
2512
2547
  }
2513
- this.logger.debug("Changed client locale to '" + iso + "'");
2514
- this.backend.setHeader('Accept-Language', iso);
2515
- this.#user.uiDirection = this.getUiDirection(iso);
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.translate.getCurrentLang() !== iso || this.system.authData?.language !== iso) {
2520
- const ob = persist
2554
+ if (this.#translate.getCurrentLang() !== iso || this.#system.authData?.language !== iso) {
2555
+ const obs = persist
2521
2556
  ? forkJoin([
2522
- this.translate.use(iso),
2523
- this.system.updateLocalizations(iso),
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
- // this.#userSource.next(this.#user);
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.translate.use(iso);
2530
- ob.subscribe(() => this.eventService.trigger(YuvEventType.CLIENT_LOCALE_CHANGED, iso));
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.backend.post(this.#DEFAULT_SETTINGS, this.#user.userSettings).pipe(tap(() => this.#userSource.next(this.#user)));
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.backend.get('/dms/permissions').pipe(catchError((e) => of(undefined)), switchMap((res) => {
2545
- this.setUserPermissions(res);
2546
- return this.backend.get(this.#DEFAULT_SETTINGS);
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.backend.get(`/idm/users?${params}`).pipe(map((users) => (!users ? [] : users.map((u) => new YuvUser(u)))));
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.backend.get(`/idm/users/${id}`).pipe(map((user) => new YuvUser(user, this.#user.userSettings)));
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.backend.getApiBase('logout')}${redir}`;
2605
+ window.location.href = `${this.#backend.getApiBase('logout')}${redir}`;
2596
2606
  }
2597
2607
  getSettings(section) {
2598
- return this.#user ? this.backend.get(this.#USERS_SETTINGS + encodeURIComponent(section)) : of(null);
2608
+ return this.#user ? this.#backend.get(this.#USERS_SETTINGS + encodeURIComponent(section)) : of(null);
2599
2609
  }
2600
2610
  saveObjectConfig(objectConfigs) {
2601
- return this.backend.post(this.#USERS_SETTINGS + encodeURIComponent(this.#SETTINGS_SECTION_OBJECTCONFIG), objectConfigs);
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
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UserService, deps: [{ token: BackendService }, { token: i1.TranslateService }, { token: Logger }, { token: SystemService }, { token: EventService }, { token: ConfigService }], target: i0.ɵɵFactoryTarget.Injectable }); }
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
- }], ctorParameters: () => [{ type: BackendService }, { type: i1.TranslateService }, { type: Logger }, { type: SystemService }, { type: EventService }, { type: ConfigService }] });
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 this.#systemService.getSystemDefinition(this.#authData).pipe(switchMap(() => this.#objectConfigService.init()), map(() => currentUser));
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
- class CatalogService {
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 client-side caching of data with expiry handling.
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 ClientCacheService {
3243
- #appCacheService = inject(AppCacheService);
3244
- #CLIENT_CACHE_PREFIX = 'yuv-client-cache:';
3245
- /**
3246
- * Get cached entry by ID.
3247
- * @param id The ID of the cache entry.
3248
- * @param keepIfExpired Whether to return/keep the entry even if it is expired.
3249
- * @returns The cached data or null if not found/expired.
3250
- */
3251
- getFromCache(id, keepIfExpired = false) {
3252
- return this.#appCacheService.getItem(this.#CLIENT_CACHE_PREFIX + id).pipe(map$1((cachedItem) => {
3253
- if (cachedItem) {
3254
- const expired = cachedItem.expires !== undefined && cachedItem.expires <= Date.now();
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
- * Update or add cache entry.
3268
- * @param id The ID of the cache entry.
3269
- * @param data The data to cache.
3270
- * @param ttl TimeToLeave - The cache expiry time in milliseconds (how long to keep the cache entry valid).
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
- updateCache(id, data, ttl) {
3273
- const cacheEntry = { id, expires: ttl ? ttl + Date.now() : undefined, data };
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
- * Invalidate cache entry by ID. This will remove the entry from the cache.
3278
- * @param id The ID of the cache entry to invalidate.
3279
- */
3280
- invalidateCache(id) {
3281
- return this.#appCacheService.removeItem(this.#CLIENT_CACHE_PREFIX + id);
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
- * Clear all client cache entries.
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
- clear() {
3287
- return this.#appCacheService.clear((k) => k.startsWith(this.#CLIENT_CACHE_PREFIX));
3364
+ uploadMultipart(url, files, data, label, silent) {
3365
+ return this.#executeMultipartUpload(url, files, { label, silent }, data);
3288
3366
  }
3289
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ClientCacheService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3290
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ClientCacheService, providedIn: 'root' }); }
3291
- }
3292
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ClientCacheService, decorators: [{
3293
- type: Injectable,
3294
- args: [{
3295
- providedIn: 'root'
3296
- }]
3297
- }] });
3298
-
3299
- class ClipboardService {
3300
- #emptyClipboard = {
3301
- buckets: undefined,
3302
- main: undefined
3303
- };
3304
- #clipboard = structuredClone(this.#emptyClipboard);
3305
- #clipboardSource = new ReplaySubject();
3306
- #clipboard$ = this.#clipboardSource.asObservable();
3307
- #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)));
3308
- clipboard(bucket) {
3309
- return toSignal(this.clipboard$(bucket));
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
- clipboard$(bucket) {
3312
- return this.#clipboard$.pipe(map$1((cs) => {
3313
- if (bucket) {
3314
- return cs.buckets ? cs.buckets[bucket] : undefined;
3315
- }
3316
- else
3317
- return cs.main;
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
- paste$(bucket) {
3321
- return this.#pasteEvents.pipe(map$1((e) => {
3322
- let cd;
3323
- if (e instanceof ClipboardEvent) {
3324
- const fileList = e.clipboardData.files;
3325
- const files = [];
3326
- for (let i = 0; i < fileList.length; i++) {
3327
- files.push(fileList.item(i));
3328
- }
3329
- cd = files.length
3330
- ? {
3331
- files
3332
- }
3333
- : undefined;
3334
- }
3335
- else {
3336
- cd = bucket ? (this.#clipboard.buckets ? this.#clipboard.buckets[bucket] : undefined) : this.#clipboard.main;
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
- return this.#clipboard.main;
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
- * Add objects to the clipboard
3350
- * @param bucket Buckets are ways to separate data from the global scope.
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
- addObjects(objects, mode, bucket) {
3356
- const cd = { mode, objects };
3357
- if (bucket) {
3358
- if (!this.#clipboard.buckets)
3359
- this.#clipboard.buckets = {};
3360
- this.#clipboard.buckets[bucket] = cd;
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
- clear(bucket) {
3367
- if (bucket && this.#clipboard.buckets) {
3368
- delete this.#clipboard.buckets[bucket];
3369
- }
3370
- else {
3371
- this.#clipboard = structuredClone(this.#emptyClipboard);
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
- this.#clipboardSource.next(this.#clipboard);
3509
+ return new HttpRequest(method, url, file || formData, { headers, reportProgress });
3374
3510
  }
3375
- async addToNavigatorClipBoard(data) {
3376
- try {
3377
- await navigator.clipboard.writeText(data);
3378
- //console.log('Text copied to clipboard');
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
- catch (error) {
3381
- console.error('Failed to copy text: ', error);
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
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ClipboardService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3385
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ClipboardService, providedIn: 'root' }); }
3386
- }
3387
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ClipboardService, decorators: [{
3388
- type: Injectable,
3389
- args: [{
3390
- providedIn: 'root'
3391
- }]
3392
- }] });
3393
-
3394
- /**
3395
- * Service to monitor the online/offline state of the application.
3396
- * It listens to the browser's online and offline events and provides an observable
3397
- * to track the current connection state.
3398
- *
3399
- * An observable `connection$` is provided which emits the current connection state
3400
- * whenever the online or offline state changes. The initial state is determined by
3401
- * the `window.navigator.onLine` property.
3402
- */
3403
- class ConnectionService {
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
- * @ignore
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
- constructor() {
3408
- this.currentState = {
3409
- isOnline: window.navigator.onLine
3410
- };
3411
- this.connectionStateSource = new ReplaySubject();
3412
- this.connection$ = this.connectionStateSource.asObservable();
3413
- this.connectionStateSource.next(this.currentState);
3414
- fromEvent(window, 'online').subscribe(() => {
3415
- this.currentState.isOnline = true;
3416
- this.connectionStateSource.next(this.currentState);
3417
- });
3418
- fromEvent(window, 'offline').subscribe(() => {
3419
- this.currentState.isOnline = false;
3420
- this.connectionStateSource.next(this.currentState);
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: ConnectionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3424
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ConnectionService, providedIn: 'root' }); }
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: ConnectionService, decorators: [{
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
- }], ctorParameters: () => [] });
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
- * @deprecated This service is deprecated. Please use the DeviceService from the `@yuuvis/material` package instead.
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 DeviceService {
3497
- #deviceDetectorService;
3498
- #upperScreenBoundary;
3499
- #resize$;
3500
- #screen;
3501
- #screenSource;
3502
- #supportsSmallScreens;
3503
- constructor() {
3504
- this.#deviceDetectorService = inject(DeviceDetectorService);
3505
- this.#upperScreenBoundary = {
3506
- small: 600,
3507
- mediumPortrait: 900,
3508
- mediumLandscape: 1200,
3509
- large: 1800
3510
- };
3511
- this.#resize$ = fromEvent(window, 'resize').pipe(debounceTime(this.#getDebounceTime()));
3512
- this.#screenSource = new ReplaySubject(1);
3513
- this.screenChange$ = this.#screenSource.asObservable();
3514
- /**
3515
- * Signal to indicate if the screen size is small (e.g. mobile phone).
3516
- * This will only be triggered if `supportsSmallScreens` is set to true.
3517
- * Major components will use this metric to adapt to 'small screen behavior' and so can you
3518
- */
3519
- this.smallScreenLayout = signal(false, ...(ngDevMode ? [{ debugName: "smallScreenLayout" }] : /* istanbul ignore next */ []));
3520
- this.#supportsSmallScreens = signal(false, ...(ngDevMode ? [{ debugName: "#supportsSmallScreens" }] : /* istanbul ignore next */ []));
3521
- /**
3522
- * if the device is a mobile device (android / iPhone / windows-phone etc)
3523
- */
3524
- this.isMobile = this.#deviceDetectorService.isMobile();
3525
- /**
3526
- * if the device us a tablet (iPad etc)
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
- #getDebounceTime() {
3603
- // on mobile devices resize only happens when rotating the device or when
3604
- // keyboard appears, so we dont't need to debounce
3605
- return this.isMobile ? 0 : 500;
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
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DeviceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3608
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DeviceService, providedIn: 'root' }); }
3609
- }
3610
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DeviceService, decorators: [{
3611
- type: Injectable,
3612
- args: [{
3613
- providedIn: 'root'
3614
- }]
3615
- }], ctorParameters: () => [] });
3616
-
3617
- // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
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 file.
3649
- * @param url The URL to upload the file to
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 uploadFile instead. `label`, `silent` as well as `scope` can be provided through `options`.
3713
+ * @deprecated use uploadFileContent instead. Provide label and silent through `options`
3654
3714
  */
3655
- upload(url, file, label, silent) {
3656
- return this.#executeUpload(url, file, { label: label || file.name, silent: silent });
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 single file to the specified URL using the structured `FileUploadOptions` object.
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 `upload()` method. It accepts a typed
3662
- * options object instead of individual parameters, which makes it easier to pass optional
3663
- * settings such as `silent` mode and `scope` without relying on positional arguments.
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
- * - If `options.silent` is `false` (default), the upload appears in the upload overlay dialog
3667
- * and progress is tracked via `status$`
3668
- * - If `options.silent` is `true`, the upload runs quietly in the background with no UI feedback
3669
- * - `options.label` is shown in the overlay dialog during upload; falls back to `file.name` if omitted
3670
- * - `options.scope` tags the upload with a named scope, enabling an `UploadProgressComponent`
3671
- * configured with the same scope to display only the uploads relevant to its section of the UI
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 url - The backend URL to POST the file to
3674
- * @param file - The `File` object to be uploaded
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 server response on completion
3741
+ * @returns An `Observable` that emits the updated DMS object on completion
3677
3742
  *
3678
3743
  * @example
3679
3744
  * ```typescript
3680
- * // Standard upload with progress indicator
3681
- * this.uploadService.uploadFile(url, file, { label: 'Uploading contract.pdf' }).subscribe();
3745
+ * // Replace content with a visible progress indicator
3746
+ * this.dmsService.uploadFileContent(objectId, file, { label: 'Uploading contract.pdf' }).subscribe();
3682
3747
  *
3683
- * // Silent upload (no UI, runs in background)
3684
- * this.uploadService.uploadFile(url, file, { label: 'document.pdf', silent: true }).subscribe();
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 the UploadProgressComponent with scope 'profile-editor' will show this
3687
- * this.uploadService.uploadFile(url, file, { label: 'photo.jpg', scope: 'profile-editor' }).subscribe();
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
- uploadFile(url, file, options) {
3691
- return this.#executeUpload(url, file, options);
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
- * Upload files using multipart upload.
3695
- * @param url The URL to upload the files to
3696
- * @param files The files to be uploaded
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
- uploadMultipart(url, files, data, label, silent) {
3703
- return this.#executeMultipartUpload(url, files, { label, silent }, data);
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
- * Uploads multiple files to the specified URL using a multipart/form-data request.
3707
- *
3708
- * This is the preferred method for multi-file uploads. It accepts a typed `FileUploadOptions`
3709
- * object for configuration, making it easier to pass `scope` and other options compared
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
- multipartUpload(url, files, options, data) {
3743
- return this.#executeMultipartUpload(url, files, options, data);
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
- * Creates a document on the server by sending structured metadata without a file attachment.
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
- * this.uploadService.createDocument(url, metadata).subscribe((result) => {
3770
- * console.log('Created:', result);
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
- createDocument(url, data) {
3775
- const formData = this.#createFormData({ data });
3776
- const request = this.#createHttpRequest(url, { formData }, false);
3777
- return this.#http.request(request).pipe(filter((obj) => obj?.body), transformResponse(), catchError((err) => throwError(() => err)));
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
- * Cancels one or all active upload requests and removes them from the tracked upload list.
3781
- *
3782
- * This method unsubscribes from the underlying HTTP request, effectively aborting the upload,
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
- cancelItem(id) {
3805
- if (id) {
3806
- const match = this.#status.items.find((i) => i.id === id);
3807
- if (match) {
3808
- match.subscription.unsubscribe();
3809
- this.#status.items = this.#status.items.filter((i) => i.id !== id);
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
- * Prepares Formdata for multipart upload.
3823
- * @param from contains form and or file
3820
+ * Get tags of a dms object.
3821
+ * @param objectData Data field of the dms object
3824
3822
  */
3825
- #createFormData({ file, data }) {
3826
- const formData = new FormData();
3827
- (file || []).forEach((f) => formData.append('files', f, f.name));
3828
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
3829
- data ? formData.append('data', new Blob([JSON.stringify(data)], { type: 'application/json' })) : null;
3830
- return formData;
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
- * Prepares Http Request.
3834
- * @param url The URL to upload the file to
3835
- * @param content formdata or single file
3836
- * @param reportProgress Request should report upload progress
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
- #createHttpRequest(url, content, reportProgress, method = 'POST') {
3840
- const { formData, file } = content;
3841
- // add request param to bypass the service-worker
3842
- url += `${!url.includes('?') ? '?' : '&'}ngsw-bypass=1`;
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
- * Prepares single file POST upload.
3851
- * @param url The URL to upload the file to
3852
- * @param file The file to be uploaded
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
- #executeUpload(url, file, options) {
3856
- const silent = options.silent === true;
3857
- const label = options.label || file.name;
3858
- const request = this.#createHttpRequest(url, { file }, !silent);
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
- * Prepare multipart upload.
3865
- * @param url The URL to upload the file to
3866
- * @param files Array of files to be uploaded
3867
- * @param label A label that will show up in the upload overlay dialog while uploading
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
- #executeMultipartUpload(url, files, options, data) {
3871
- const label = options.label ?? 'Upload';
3872
- const silent = options.silent === true;
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
- #createProgressStatus(event, progress, id) {
3908
- if (event.type === HttpEventType.UploadProgress) {
3909
- const fullPercentage = 100;
3910
- const percentDone = Math.round((fullPercentage * event.loaded) / event.total);
3911
- progress.next(percentDone);
3912
- }
3913
- else if (event instanceof HttpResponse) {
3914
- progress.complete();
3915
- // add upload response
3916
- // this.status.items = this.status.items.filter(s => s.id !== id);
3917
- const idx = this.#status.items.findIndex((item) => item.id === id);
3918
- if (idx !== -1) {
3919
- this.#status.items[idx].result = this.#generateResult(event);
3920
- this.#statusSource.next(this.#status);
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
- #createUploadError(err, progress, id) {
3925
- const statusItem = this.#status.items.find((item) => item.id === id);
3926
- statusItem.err = {
3927
- code: err.status,
3928
- message: err.error ? err.error.errorMessage : err.message
3929
- };
3930
- this.#logger.error('upload failed', statusItem);
3931
- this.#status.err++;
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
- * Actually starts the upload process.
3938
- * @param request Request to be executed
3939
- * @param label A label that will show up in the upload overlay dialog while uploading
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
- #startUploadWithFile(request, label, scope) {
3942
- return new Observable((uploadObserver) => {
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
- * Create new dms object(s). Providing an array of files here instead of one will create
3997
- * a new dms object for every file. In this case indexdata will shared across all files.
3998
- * @param objectTypeId The ID of the object type to be created
3999
- * @param indexdata Indexdata for the new object(s)
4000
- * @param files File(s) to create dms objects content(s) with
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
- createDmsObject(objectTypeId, indexdata, files, label, silent = false, options = { waitForSearchConsistency: true }) {
4006
- const url = `${this.#backend.getApiBase(ApiBase.apiWeb)}/dms/objects` +
4007
- `${options.waitForSearchConsistency ? '?waitForSearchConsistency=true' : ''}`;
4008
- const data = indexdata;
4009
- data[BaseObjectTypeField.OBJECT_TYPE_ID] = objectTypeId;
4010
- const upload = files.length
4011
- ? this.#uploadService.multipartUpload(url, files, { label: label ?? '', silent, scope: options.scope }, data)
4012
- : this.#uploadService.createDocument(url, data);
4013
- return upload
4014
- .pipe(map((res) => res.map((response) => response.properties[BaseObjectTypeField.OBJECT_ID].value)),
4015
- // TODO: Replace by proper solution
4016
- // Right now there is a gap between when the object was
4017
- // created and when it is indexed. So delaying here will
4018
- // give backend time to get its stuff together.
4019
- delay(1000))
4020
- .pipe(this.triggerEvent(YuvEventType.DMS_OBJECT_CREATED, '', silent));
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
- * Delete a dms object.
4024
- * @param id ID of the object to be deleted
3946
+ * Get a bunch of dms objects.
3947
+ * @param ids List of IDs of objects to be retrieved
4025
3948
  */
4026
- deleteDmsObject(id, silent = false) {
4027
- const url = `/dms/objects/${id}`;
4028
- return this.#backend
4029
- .delete(url, ApiBase.apiWeb)
4030
- .pipe(map(() => ({ id })))
4031
- .pipe(this.triggerEvent(YuvEventType.DMS_OBJECT_DELETED, '', silent));
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
- * Restore older version of a dms object.
4035
- * @param id ID of the object to be restored
4036
- * @param version version of the object to be restored
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
- restoreDmsObject(id, version, silent = false) {
4039
- // eslint-disable-next-line max-len
4040
- const url = `/dms/objects/${id}/versions/${version}/actions/restore?waitForSearchConsistency=true&restoreParentId=false`;
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
- .post(url, {}, ApiBase.apiWeb)
4043
- .pipe(this.triggerEvent(YuvEventType.DMS_OBJECT_UPDATED, id, silent));
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
- * Upload (add/replace) content to a dms object.
4047
- * @param objectId ID of the dms object to upload the file to
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
- uploadContent(objectId, file, label, silent) {
4054
- return this.#uploadService
4055
- .upload(this.getContentPath(objectId), file, label, silent)
4056
- .pipe(this.triggerEvent(YuvEventType.DMS_OBJECT_UPDATED, objectId));
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
- * Uploads a file as the content of an existing DMS object, replacing or setting its content stream.
4060
- *
4061
- * This is the preferred alternative to the legacy `uploadContent()` method. It accepts a typed
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
- * Path of dms object content file.
4102
- * @param objectId ID of the dms object
4103
- * @param version version number of the dms object
4104
- */
4105
- getContentPath(objectId, version) {
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
- * Original API Path of dms object content file.
4111
- * @param objectId ID of the dms object
4112
- * @param version version number of the dms object
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
- getFullContentPath(objectId, version, rendition = false) {
4116
- return (`${this.#backend.getApiBase(ApiBase.core, true)}/dms/objects/` +
4117
- `${objectId}${version ? '/versions/' + version : ''}/contents/${rendition ? 'renditions/pdf' : 'file'}`);
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
- getSlideURI(objectId, mimeType) {
4120
- const supportedMimeTypes = ['*/*'];
4121
- let supported = false;
4122
- if (mimeType) {
4123
- // check if mime type supports slides
4124
- supportedMimeTypes.forEach((p) => (supported = supported || Utils.patternToRegExp(p).test(mimeType)));
4125
- }
4126
- return !mimeType || supported
4127
- ? `${this.#backend.getApiBase(ApiBase.core, true)}/dms/objects/${objectId}/contents/renditions/slide`
4128
- : undefined;
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
- * Downloads the content of dms objects.
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
- downloadContent(objects, withVersion) {
4137
- objects.forEach((object, i) => setTimeout(() => {
4138
- const uri = `${this.getContentPath(object?.id, withVersion ? object?.version : undefined)}`;
4139
- this.#backend.download(uri);
4140
- }, Utils.isSafari() ? i * 1000 : 0 // Safari does not allow multi download
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
- * Fetch a dms object.
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
- getDmsObject(id, version, silent = false, requestOptions) {
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
- .get(`/dms/objects/${id}${version ? '/versions/' + version : ''}`, undefined, requestOptions)
4151
- .pipe(map((res) => {
4152
- const item = this.#searchService.toSearchResult(res).items[0];
4153
- return this.#searchResultToDmsObject(item);
4154
- }))
4155
- .pipe(this.triggerEvent(YuvEventType.DMS_OBJECT_LOADED, '', silent));
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
- * Get tags of a dms object.
4159
- * @param objectData Data field of the dms object
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
- getDmsObjectTags(objectData) {
4162
- return (objectData[BaseObjectTypeField.TAGS] || []).map((row) => ({
4163
- name: row[0],
4164
- state: row[1],
4165
- creationDate: new Date(row[2]),
4166
- traceId: row[3]
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
- * Updates a tag on a dms object.
4171
- * @param id The ID of the object
4172
- * @param tag The tag to be updated
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
- setDmsObjectTag(id, tag, value, silent = false) {
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
- .post(`/dms/objects/tags/${tag}/state/${value}?query=SELECT * FROM system:object WHERE system:objectId='${id}'`, {}, ApiBase.core)
4178
- .pipe(this.triggerEvent(YuvEventType.DMS_OBJECT_UPDATED, id, silent));
4286
+ .get(url, ApiBase.core, requestOptions)
4287
+ .pipe(map$1((response) => this.#mapResponse(response)));
4179
4288
  }
4180
4289
  /**
4181
- * Deletes a tag from a dms object.
4182
- * @param id The ID of the object
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
- deleteDmsObjectTag(id, tag, silent = false) {
4186
- return this.#backend
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
- * Update indexdata of a dms object.
4192
- * @param id ID of the object to apply the data to
4193
- * @param data Indexdata to be applied
4194
- * @param silent flag to trigger DMS_OBJECT_UPDATED event
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
- * Updates given objects.
4202
- * @param objects the objects to updated
4203
- */
4204
- updateDmsObjects(objects, silent = false, options = { waitForSearchConsistency: true }) {
4205
- const url = `/dms/objects${options.waitForSearchConsistency ? '?waitForSearchConsistency=true' : ''}`;
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
- * Updates a tag on a dms object.
4217
- * @param ids List of IDs of objects
4218
- * @param tag The tag to be updated
4219
- * @param value The tags new value
4220
- */
4221
- updateDmsObjectsTag(ids, tag, value, silent = false) {
4222
- return this.batchUpdateTag(ids, tag, value).pipe(this.#triggerEvents(YuvEventType.DMS_OBJECT_UPDATED, ids, silent));
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
- * Moves given objects to a different folder.
4226
- * @param folderId the id of the new parent folder
4227
- * @param objects objects to be moved
4228
- * @param options options for the move operation
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
- moveDmsObjects(targetFolderId, objects, options) {
4231
- 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)));
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
- * Copy given objects to a different folder. The objects will be copied with their indexdata referencing
4235
- * the existing content of the source object.
4236
- * @param targetFolderId The ID of the target folder
4237
- * @param objects The objects to be copied
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
- copyDmsObjects(targetFolderId, objects, options) {
4241
- const excludeProperties = [
4242
- BaseObjectTypeField.OBJECT_ID,
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
- * Get a bunch of dms objects.
4285
- * @param ids List of IDs of objects to be retrieved
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
- getDmsObjects(ids, silent = false) {
4288
- return this.batchGet(ids)
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
- * Delete a bunch of dms objects.
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
- deleteDmsObjects(objects, options) {
4301
- const queryParams = options
4302
- ? `?${Object.keys(options)
4303
- .filter((k) => ['waitForSearchConsistency', 'greedy'].includes(k))
4304
- .map((k) => `${k}=${options[k]}`)
4305
- .join('&')}`
4306
- : '';
4307
- return this.#backend
4308
- .delete(`/dms/objects${queryParams}`, ApiBase.core, {
4309
- body: {
4310
- objects: objects.map((o) => ({
4311
- properties: typeof o === 'string'
4312
- ? {
4313
- 'system:objectId': {
4314
- value: o
4315
- }
4316
- }
4317
- : {
4318
- 'system:objectId': { value: o.id },
4319
- subject: { value: o.subject }
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
- .pipe(map((res) => res?.objects?.map((r) => {
4325
- const v = r.options?.[SystemResult.DELETE];
4326
- return {
4327
- id: r.properties?.[BaseObjectTypeField.OBJECT_ID]?.value,
4328
- properties: r.properties,
4329
- ...(v?.httpStatusCode >= 400 ? { _error: { status: v.httpStatusCode, message: v.message } } : {})
4330
- };
4331
- })))
4332
- .pipe(this.#triggerEvents(YuvEventType.DMS_OBJECT_DELETED, undefined, options?.silent));
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
- * Fetch a dms object versions.
4336
- * @param id ID of the object to be retrieved
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
- getDmsObjectVersions(id) {
4339
- return this.#backend.get('/dms/objects/' + id + '/versions').pipe(map((res) => {
4340
- const items = this.#searchService.toSearchResult(res).items || [];
4341
- return items.map((item) => this.#searchResultToDmsObject(item));
4342
- }), map((res) => res.sort(Utils.sortValues('version', Sort.DESC))));
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
- getDmsObjectVersion(id, version) {
4345
- return this.#backend
4346
- .get(`/dms/objects/${id}/versions/${version}`)
4347
- .pipe(map((res) => this.#searchResultToDmsObject(this.#searchService.toSearchResult(res).items[0])));
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
- coreApiResponseToDmsObject(res) {
4350
- return this.#searchResultToDmsObject({
4351
- objectTypeId: res.properties[BaseObjectTypeField.OBJECT_TYPE_ID].value,
4352
- fields: new Map(Object.entries(res.properties)),
4353
- content: res.contentStreams?.[0]
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
- * Transforms a plain data object to a DmsObject.
4358
- * @param data The plain data object
4359
- * @returns DmsObject
4578
+ * @ignore
4360
4579
  */
4361
- toDmsObject(data) {
4362
- const fields = new Map();
4363
- Object.keys(data).forEach((key) => fields.set(key, data[key]));
4364
- const item = {
4365
- content: {
4366
- contentStreamId: data[ContentStreamField.ID],
4367
- mimeType: data[ContentStreamField.MIME_TYPE],
4368
- fileName: data[ContentStreamField.FILENAME],
4369
- size: data[ContentStreamField.LENGTH],
4370
- digest: data[ContentStreamField.DIGEST],
4371
- repositoryId: data[ContentStreamField.REPOSITORY_ID],
4372
- range: data[ContentStreamField.RANGE]
4373
- },
4374
- objectTypeId: data[BaseObjectTypeField.OBJECT_TYPE_ID],
4375
- fields
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
- return this.#searchResultToDmsObject(item);
4378
- }
4379
- batchUpdateTag(ids, tag, value) {
4380
- return this.#backend.batch(ids.map((id) => ({
4381
- method: 'POST',
4382
- uri: `/dms/objects/tags/${tag}/state/${value}?query=SELECT * FROM system:object WHERE system:objectId='${id}'`,
4383
- base: ApiBase.core,
4384
- body: {}
4385
- })));
4386
- }
4387
- batchDeleteTag(ids, tag) {
4388
- return this.#backend.batch(ids.map((id) => ({
4389
- method: 'DELETE',
4390
- uri: `/dms/objects/${id}/tags/${tag}`,
4391
- base: ApiBase.core
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
- batchDelete(ids) {
4395
- return this.#backend.batch(ids.map((id) => ({ method: 'DELETE', uri: `/dms/objects/${id}` })));
4712
+ init(supportsSmallScreens = false) {
4713
+ this.#supportsSmallScreens.set(supportsSmallScreens);
4714
+ this.#setScreen();
4396
4715
  }
4397
- batchGet(ids) {
4398
- return this.#backend.batch(ids.map((id) => ({ method: 'GET', uri: `/dms/objects/${id}` })));
4716
+ #isTouchEnabled() {
4717
+ return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
4399
4718
  }
4400
- /**
4401
- * Map search result from the backend to applications SearchResult object
4402
- * @param searchResponse The backend response
4403
- */
4404
- toSearchResult(searchResponse) {
4405
- const resultListItems = [];
4406
- const objectTypes = [];
4407
- searchResponse.objects.forEach((o) => {
4408
- const fields = new Map();
4409
- // process properties section of result
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
- const objectTypeId = o.properties[BaseObjectTypeField.OBJECT_TYPE_ID]
4462
- ? o.properties[BaseObjectTypeField.OBJECT_TYPE_ID].value
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
- resultListItems.push({
4468
- objectTypeId,
4469
- content,
4470
- fields,
4471
- permissions: o.permissions
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
- return result;
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
- // general trigger operator to handle all dms events
4483
- triggerEvent(event, id, silent = false) {
4484
- return (stream) => stream.pipe(
4485
- // update does not return permissions, so we need to re-load the whole dms object
4486
- // TODO: Remove once permissions are provided
4487
- switchMap((res) => (!id ? of(res) : this.getDmsObject(id))),
4488
- // TODO: enable once permissions are provided
4489
- // map((res) => this.searchResultToDmsObject(this.searchService.toSearchResult(res).items[0])),
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
- // general trigger operator to handle all dms events
4493
- #triggerEvents(event, ids, silent = false) {
4494
- return (stream) => stream.pipe(
4495
- // update does not return permissions, so we need to re-load the whole dms object
4496
- // TODO: Remove once permissions are provided
4497
- switchMap((res) => (!ids ? of(res) : this.getDmsObjects(ids))),
4498
- // TODO: enable once permissions are provided
4499
- // map((_res: any[]) => _res.map((res, i) => res?._error ?
4500
- // { ...res, id: ids?[i] } : this.searchResultToDmsObject(this.searchService.toSearchResult(res).items[0]))),
4501
- 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))));
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
- #searchResultToDmsObject(resItem) {
4504
- return new DmsObject(resItem);
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
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DmsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4507
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DmsService, providedIn: 'root' }); }
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: DmsService, decorators: [{
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-container);
4723
- --yuv-toast-success-tone: var(--ymt-on-success-container);
4724
- --yuv-toast-warning-background: var(--ymt-warning-container);
4725
- --yuv-toast-warning-tone: var(--ymt-on-warning-container);
4726
- --yuv-toast-error-background: var(--ymt-danger-container);
4727
- --yuv-toast-error-tone: var(--ymt-on-danger-container);
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
- .yuv-toast-container.start {
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