@yuuvis/client-shell-core 2.20.1 → 3.0.0-beta.20.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.
Files changed (72) hide show
  1. package/esm2022/index.js +15 -0
  2. package/esm2022/index.js.map +1 -0
  3. package/esm2022/lib/client-shell.assets.js +12 -0
  4. package/esm2022/lib/client-shell.assets.js.map +1 -0
  5. package/esm2022/lib/models/client-shell.interface.js +5 -0
  6. package/esm2022/lib/models/client-shell.interface.js.map +1 -0
  7. package/esm2022/lib/services/command-palette/command-palette/command-palette.component.js +94 -0
  8. package/esm2022/lib/services/command-palette/command-palette/command-palette.component.js.map +1 -0
  9. package/esm2022/lib/services/command-palette/command-palette.interface.js +2 -0
  10. package/esm2022/lib/services/command-palette/command-palette.interface.js.map +1 -0
  11. package/esm2022/lib/services/command-palette/command-palette.service.js +120 -0
  12. package/esm2022/lib/services/command-palette/command-palette.service.js.map +1 -0
  13. package/esm2022/lib/services/feature/feature.guard.js +9 -0
  14. package/esm2022/lib/services/feature/feature.guard.js.map +1 -0
  15. package/esm2022/lib/services/feature/feature.interface.js +2 -0
  16. package/esm2022/lib/services/feature/feature.interface.js.map +1 -0
  17. package/esm2022/lib/services/feature/feature.providers.js +14 -0
  18. package/esm2022/lib/services/feature/feature.providers.js.map +1 -0
  19. package/esm2022/lib/services/feature/feature.service.js +141 -0
  20. package/esm2022/lib/services/feature/feature.service.js.map +1 -0
  21. package/esm2022/lib/services/feature/index.js +5 -0
  22. package/esm2022/lib/services/feature/index.js.map +1 -0
  23. package/esm2022/lib/services/shell/noop.extension.js +13 -0
  24. package/esm2022/lib/services/shell/noop.extension.js.map +1 -0
  25. package/esm2022/lib/services/shell/requirements.parser.js +7 -0
  26. package/esm2022/lib/services/shell/requirements.parser.js.map +1 -0
  27. package/esm2022/lib/services/shell/shell.bootstrap.js +84 -0
  28. package/esm2022/lib/services/shell/shell.bootstrap.js.map +1 -0
  29. package/esm2022/lib/services/shell/shell.providers.js +20 -0
  30. package/esm2022/lib/services/shell/shell.providers.js.map +1 -0
  31. package/esm2022/lib/services/shell/shell.service.js +455 -0
  32. package/esm2022/lib/services/shell/shell.service.js.map +1 -0
  33. package/esm2022/lib/services/shell-config/shell-config.const.js +17 -0
  34. package/esm2022/lib/services/shell-config/shell-config.const.js.map +1 -0
  35. package/esm2022/lib/services/shell-config/shell-config.interface.js +2 -0
  36. package/esm2022/lib/services/shell-config/shell-config.interface.js.map +1 -0
  37. package/esm2022/lib/services/shell-config/shell-config.service.js +100 -0
  38. package/esm2022/lib/services/shell-config/shell-config.service.js.map +1 -0
  39. package/esm2022/lib/services/shell-notifications/shell-notifications.interface.js +2 -0
  40. package/esm2022/lib/services/shell-notifications/shell-notifications.interface.js.map +1 -0
  41. package/esm2022/lib/services/shell-notifications/shell-notifications.service.js +84 -0
  42. package/esm2022/lib/services/shell-notifications/shell-notifications.service.js.map +1 -0
  43. package/esm2022/lib/tile-extension.interface.js +2 -0
  44. package/esm2022/lib/tile-extension.interface.js.map +1 -0
  45. package/esm2022/yuuvis-client-shell-core.js +5 -0
  46. package/esm2022/yuuvis-client-shell-core.js.map +1 -0
  47. package/lib/services/command-palette/command-palette/command-palette.component.d.ts +2 -2
  48. package/package.json +10 -10
  49. package/yuuvis-client-shell-core.d.ts +5 -0
  50. package/esm2022/index.mjs +0 -15
  51. package/esm2022/lib/client-shell.assets.mjs +0 -12
  52. package/esm2022/lib/models/client-shell.interface.mjs +0 -5
  53. package/esm2022/lib/services/command-palette/command-palette/command-palette.component.mjs +0 -101
  54. package/esm2022/lib/services/command-palette/command-palette.interface.mjs +0 -2
  55. package/esm2022/lib/services/command-palette/command-palette.service.mjs +0 -120
  56. package/esm2022/lib/services/feature/feature.guard.mjs +0 -9
  57. package/esm2022/lib/services/feature/feature.interface.mjs +0 -2
  58. package/esm2022/lib/services/feature/feature.providers.mjs +0 -14
  59. package/esm2022/lib/services/feature/feature.service.mjs +0 -141
  60. package/esm2022/lib/services/feature/index.mjs +0 -5
  61. package/esm2022/lib/services/shell/noop.extension.mjs +0 -13
  62. package/esm2022/lib/services/shell/requirements.parser.mjs +0 -7
  63. package/esm2022/lib/services/shell/shell.bootstrap.mjs +0 -84
  64. package/esm2022/lib/services/shell/shell.providers.mjs +0 -20
  65. package/esm2022/lib/services/shell/shell.service.mjs +0 -456
  66. package/esm2022/lib/services/shell-config/shell-config.const.mjs +0 -17
  67. package/esm2022/lib/services/shell-config/shell-config.interface.mjs +0 -2
  68. package/esm2022/lib/services/shell-config/shell-config.service.mjs +0 -100
  69. package/esm2022/lib/services/shell-notifications/shell-notifications.interface.mjs +0 -2
  70. package/esm2022/lib/services/shell-notifications/shell-notifications.service.mjs +0 -84
  71. package/esm2022/lib/tile-extension.interface.mjs +0 -2
  72. package/esm2022/yuuvis-client-shell-core.mjs +0 -5
@@ -0,0 +1,84 @@
1
+ import { inject, provideAppInitializer } from '@angular/core';
2
+ import { bootstrapApplication } from '@angular/platform-browser';
3
+ import { BaseObjectTypeField, provideAvailabilityManagement, provideRequirements, provideUser } from '@yuuvis/client-core';
4
+ import { provideClientShellAlwaysShowNotifications, provideClientShellFeatures } from '../feature/feature.providers';
5
+ import { getShellConfigQueryString, SHELL_CONFIG_FEATURES } from '../shell-config/shell-config.const';
6
+ import { requirementsMapping } from './requirements.parser';
7
+ import { ShellService } from './shell.service';
8
+ /**
9
+ * Boostraps a yuuvis Momentum shell client.
10
+ *
11
+ * ```ts
12
+ * import { bootstrapShellApplication } from '@yuuvis/client-shell-core';
13
+ *
14
+ * bootstrapShellApplication(AppComponent, appConfig).catch((err) => console.error(err));
15
+ * ```
16
+ *
17
+ * @param rootComponent Root component of the application
18
+ * @param options Application configuration
19
+ * @returns
20
+ */
21
+ export async function bootstrapShellApplication(rootComponent, options, shellOptions, requirements) {
22
+ const o = options || {
23
+ providers: []
24
+ };
25
+ const defaultProviders = [];
26
+ const userResponse = await fetch('/api-web/api/idm/whoami', {
27
+ headers: {
28
+ 'Content-Type': 'application/json'
29
+ }
30
+ });
31
+ if (!userResponse.ok) {
32
+ if (userResponse.status === 401) {
33
+ window.location.href = `${location.origin}/logout`;
34
+ }
35
+ else {
36
+ throw new Error('Failed to fetch user information');
37
+ }
38
+ }
39
+ const userRes = await userResponse.json();
40
+ o.providers = [...o.providers, provideUser(userRes)];
41
+ if (shellOptions?.enableObjectTypeConfig) {
42
+ defaultProviders.push(provideAppInitializer(() => {
43
+ inject(ShellService)._initObjectTypeConfig();
44
+ }));
45
+ }
46
+ if (shellOptions?.alwaysShowNotifications) {
47
+ defaultProviders.push(provideClientShellAlwaysShowNotifications(true));
48
+ }
49
+ // only call the backend if there actually are requirements to check
50
+ if (requirements && Object.keys(requirements).length > 0) {
51
+ const requirementsResponse = await fetch(`/api/dms/apps`, { headers: { 'Content-Type': 'application/json' } });
52
+ if (requirementsResponse.ok) {
53
+ defaultProviders.push(provideAvailabilityManagement(requirementsMapping(await requirementsResponse.json())));
54
+ }
55
+ defaultProviders.push(provideRequirements(requirements));
56
+ }
57
+ if (userRes && shellOptions?.enableFeatureConfig) {
58
+ // find the config file
59
+ const response = await fetch('/api/dms/objects/search', {
60
+ method: 'POST',
61
+ headers: { 'Content-Type': 'application/json' },
62
+ body: JSON.stringify({
63
+ query: {
64
+ statement: getShellConfigQueryString(SHELL_CONFIG_FEATURES)
65
+ }
66
+ })
67
+ });
68
+ const res = await response.json();
69
+ const objects = res.objects;
70
+ if (objects?.length > 0) {
71
+ const id = objects[0]?.properties[BaseObjectTypeField.OBJECT_ID].value;
72
+ const file = await fetch(`/api/dms/objects/${id}/contents/file`, { headers: { 'Content-Type': 'application/json' } });
73
+ const res = await file.json();
74
+ const shellFeatureConfig = {
75
+ objectID: id,
76
+ features: res || []
77
+ };
78
+ defaultProviders.push(provideClientShellFeatures(shellFeatureConfig));
79
+ }
80
+ }
81
+ o.providers = [...defaultProviders, ...o.providers];
82
+ return bootstrapApplication(rootComponent, o);
83
+ }
84
+ //# sourceMappingURL=shell.bootstrap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell.bootstrap.js","sourceRoot":"","sources":["../../../../../../../../libs/yuuvis/client-shell-core/src/lib/services/shell/shell.bootstrap.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApplicationConfig, ApplicationRef, inject, provideAppInitializer, Type } from '@angular/core';\nimport { bootstrapApplication } from '@angular/platform-browser';\nimport { BaseObjectTypeField, Requirements, provideAvailabilityManagement, provideRequirements, provideUser } from '@yuuvis/client-core';\nimport { ShellBootstrapOptions } from '../../models/client-shell.interface';\nimport { ClientShellFeatureConfig } from '../feature/feature.interface';\nimport { provideClientShellAlwaysShowNotifications, provideClientShellFeatures } from '../feature/feature.providers';\nimport { getShellConfigQueryString, SHELL_CONFIG_FEATURES } from '../shell-config/shell-config.const';\nimport { requirementsMapping } from './requirements.parser';\nimport { ShellService } from './shell.service';\n\n/**\n * Boostraps a yuuvis Momentum shell client.\n *\n * ```ts\n * import { bootstrapShellApplication } from '@yuuvis/client-shell-core';\n *\n * bootstrapShellApplication(AppComponent, appConfig).catch((err) => console.error(err));\n * ```\n *\n * @param rootComponent Root component of the application\n * @param options Application configuration\n * @returns\n */\nexport async function bootstrapShellApplication(\n rootComponent: Type<unknown>,\n options?: ApplicationConfig,\n shellOptions?: ShellBootstrapOptions,\n requirements?: Requirements\n): Promise<ApplicationRef> {\n const o: ApplicationConfig = options || {\n providers: []\n };\n const defaultProviders = [];\n const userResponse: Response = await fetch('/api-web/api/idm/whoami', {\n headers: {\n 'Content-Type': 'application/json'\n }\n });\n\n if (!userResponse.ok) {\n if (userResponse.status === 401) {\n window.location.href = `${location.origin}/logout`;\n } else {\n throw new Error('Failed to fetch user information');\n }\n }\n\n const userRes = await userResponse.json();\n o.providers = [...o.providers, provideUser(userRes)];\n\n if (shellOptions?.enableObjectTypeConfig) {\n defaultProviders.push(\n provideAppInitializer(() => {\n inject(ShellService)._initObjectTypeConfig();\n })\n );\n }\n\n if (shellOptions?.alwaysShowNotifications) {\n defaultProviders.push(provideClientShellAlwaysShowNotifications(true));\n }\n\n // only call the backend if there actually are requirements to check\n if (requirements && Object.keys(requirements).length > 0) {\n const requirementsResponse: Response = await fetch(`/api/dms/apps`, { headers: { 'Content-Type': 'application/json' } });\n if (requirementsResponse.ok) {\n defaultProviders.push(provideAvailabilityManagement(requirementsMapping(await requirementsResponse.json())));\n }\n defaultProviders.push(provideRequirements(requirements));\n }\n\n if (userRes && shellOptions?.enableFeatureConfig) {\n // find the config file\n const response = await fetch('/api/dms/objects/search', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n query: {\n statement: getShellConfigQueryString(SHELL_CONFIG_FEATURES)\n }\n })\n });\n const res = await response.json();\n\n const objects = res.objects;\n if (objects?.length > 0) {\n const id = objects[0]?.properties[BaseObjectTypeField.OBJECT_ID].value;\n\n const file = await fetch(`/api/dms/objects/${id}/contents/file`, { headers: { 'Content-Type': 'application/json' } });\n const res = await file.json();\n const shellFeatureConfig: ClientShellFeatureConfig = {\n objectID: id,\n features: res || []\n };\n\n defaultProviders.push(provideClientShellFeatures(shellFeatureConfig));\n }\n }\n o.providers = [...defaultProviders, ...o.providers];\n return bootstrapApplication(rootComponent, o);\n}\n"]}
@@ -0,0 +1,20 @@
1
+ import { inject, provideAppInitializer } from '@angular/core';
2
+ import { FeatureService } from '../feature/feature.service';
3
+ import { NoopExtension } from './noop.extension';
4
+ export const importShellExtensions = (extensionImports) => {
5
+ const providers = [];
6
+ // TODO: find proper way of typing
7
+ const ne = NoopExtension;
8
+ providers.push(ne);
9
+ extensionImports.forEach((extensionImport) => {
10
+ providers.push(extensionImport.extension);
11
+ providers.push(provideAppInitializer(() => {
12
+ const e = inject(FeatureService).canUseFeatureById(extensionImport.id)
13
+ ? extensionImport.extension
14
+ : NoopExtension;
15
+ return inject(e).init();
16
+ }));
17
+ });
18
+ return providers;
19
+ };
20
+ //# sourceMappingURL=shell.providers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell.providers.js","sourceRoot":"","sources":["../../../../../../../../libs/yuuvis/client-shell-core/src/lib/services/shell/shell.providers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,MAAM,EAAE,qBAAqB,EAAiB,MAAM,eAAe,CAAC;AAEnG,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,gBAAwC,EAA0B,EAAE;IACxG,MAAM,SAAS,GAA2B,EAAE,CAAC;IAC7C,kCAAkC;IAClC,MAAM,EAAE,GAAG,aAAoB,CAAC;IAChC,SAAS,CAAC,IAAI,CAAC,EAA0B,CAAC,CAAC;IAC3C,gBAAgB,CAAC,OAAO,CAAC,CAAC,eAAe,EAAE,EAAE;QAC3C,SAAS,CAAC,IAAI,CAAC,eAAe,CAAC,SAA4C,CAAC,CAAC;QAC7E,SAAS,CAAC,IAAI,CACZ,qBAAqB,CAAC,GAAG,EAAE;YACzB,MAAM,CAAC,GAAwC,MAAM,CAAC,cAAc,CAAC,CAAC,iBAAiB,CAAC,eAAe,CAAC,EAAE,CAAC;gBACzG,CAAC,CAAE,eAAe,CAAC,SAAiD;gBACpE,CAAC,CAAC,aAAa,CAAC;YAClB,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC1B,CAAC,CAAC,CACH,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,OAAO,SAAS,CAAC;AACnB,CAAC,CAAC","sourcesContent":["import { EnvironmentProviders, inject, provideAppInitializer, ProviderToken } from '@angular/core';\nimport { ClientShellExtension, ShellExtensionImport } from '../../models/client-shell.interface';\nimport { FeatureService } from '../feature/feature.service';\nimport { NoopExtension } from './noop.extension';\n\nexport const importShellExtensions = (extensionImports: ShellExtensionImport[]): EnvironmentProviders[] => {\n const providers: EnvironmentProviders[] = [];\n // TODO: find proper way of typing\n const ne = NoopExtension as any;\n providers.push(ne as EnvironmentProviders);\n extensionImports.forEach((extensionImport) => {\n providers.push(extensionImport.extension as unknown as EnvironmentProviders);\n providers.push(\n provideAppInitializer(() => {\n const e: ProviderToken<ClientShellExtension> = inject(FeatureService).canUseFeatureById(extensionImport.id)\n ? (extensionImport.extension as ProviderToken<ClientShellExtension>)\n : NoopExtension;\n return inject(e).init();\n })\n );\n });\n return providers;\n};\n"]}
@@ -0,0 +1,455 @@
1
+ import { inject, Injectable, signal, DOCUMENT } from '@angular/core';
2
+ import { MatDialog } from '@angular/material/dialog';
3
+ import { MatIconRegistry } from '@angular/material/icon';
4
+ import { DomSanitizer } from '@angular/platform-browser';
5
+ import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
6
+ import { ApiBase, BaseObjectTypeField, ConfigService, DmsService, SystemService, SystemType, TranslateService, UserService, Utils } from '@yuuvis/client-core';
7
+ import { BehaviorSubject, debounceTime, filter, first, firstValueFrom, fromEvent, map, of, tap } from 'rxjs';
8
+ import { CLIENT_SHELL_ASSETS } from '../../client-shell.assets';
9
+ import { SHELL_CONFIG_TYPES } from '../shell-config/shell-config.const';
10
+ import { ShellConfigService } from '../shell-config/shell-config.service';
11
+ import * as i0 from "@angular/core";
12
+ export class ShellService {
13
+ #dmsService;
14
+ #dialog;
15
+ #system;
16
+ #userService;
17
+ // TODO: Need orignal MatIconRegistry to #registerObjectTypeIcons, otherwise icons could not be registered in the namespace
18
+ // #iconRegistry = inject(YuvMatIconRegistry);
19
+ #iconRegistry;
20
+ #shellConfig;
21
+ #configService;
22
+ #sanitizer;
23
+ #router;
24
+ #route;
25
+ #DOCUMENT;
26
+ // Apps can register their own setting components to be shown in shell settings
27
+ #appSettings;
28
+ #defaultShellConfig;
29
+ constructor() {
30
+ this.#dmsService = inject(DmsService);
31
+ this.#dialog = inject(MatDialog);
32
+ this.#system = inject(SystemService);
33
+ this.#userService = inject(UserService);
34
+ // TODO: Need orignal MatIconRegistry to #registerObjectTypeIcons, otherwise icons could not be registered in the namespace
35
+ // #iconRegistry = inject(YuvMatIconRegistry);
36
+ this.#iconRegistry = inject(MatIconRegistry);
37
+ this.#shellConfig = inject(ShellConfigService);
38
+ this.#configService = inject(ConfigService);
39
+ this.#sanitizer = inject(DomSanitizer);
40
+ this.#router = inject(Router);
41
+ this.#route = inject(ActivatedRoute);
42
+ this.#DOCUMENT = inject(DOCUMENT);
43
+ this.translate = inject(TranslateService);
44
+ this._busyCount = 0;
45
+ this.isBusySubject = new BehaviorSubject(false);
46
+ this.isBusy$ = this.isBusySubject.asObservable().pipe(debounceTime(500));
47
+ // Apps can register their own setting components to be shown in shell settings
48
+ this.#appSettings = signal([], ...(ngDevMode ? [{ debugName: "#appSettings" }] : []));
49
+ this.appSettings = this.#appSettings.asReadonly();
50
+ this.appBaseRoutes = {};
51
+ this.#defaultShellConfig = {
52
+ appIcon: CLIENT_SHELL_ASSETS.appIcon,
53
+ shellIconNamespace: CLIENT_SHELL_ASSETS.shellIconNamespace
54
+ };
55
+ this.shellConfig = signal(this.#defaultShellConfig, ...(ngDevMode ? [{ debugName: "shellConfig" }] : []));
56
+ this.apps = signal([], ...(ngDevMode ? [{ debugName: "apps" }] : []));
57
+ /** TODO: implement this feature ????
58
+ * Global shortcuts are captured by the shell to provide a consistent experience
59
+ * accross all apps. Search is one example. Apps should not define their own shourtcuts
60
+ * for search. Instead they should subscribe to the global shortcuts and initialize their
61
+ * search based on the global search shortcut.
62
+ */
63
+ this.globalShortcuts$ = fromEvent(document, 'keydown').pipe(map((e) => {
64
+ // global shortcut for search
65
+ // if (e.ctrlKey && e.code === 'KeyF') {
66
+ // e.preventDefault();
67
+ // return GlobalShortcut.search;
68
+ // }
69
+ return undefined;
70
+ }), filter((s) => s !== undefined), map((s) => s));
71
+ this._extensions = {};
72
+ this._objectFlavors = [];
73
+ this._objectCreateFlavors = [];
74
+ this.translate.onLangChange.subscribe(() => {
75
+ this.apps.set(this.#getMappedApps(this.apps()));
76
+ });
77
+ // track open dialogs in an auxillary route to support closing them on back navigation
78
+ // as this is the expected behaviour on mobile devices
79
+ this.#dialog.afterOpened.subscribe((ref) => {
80
+ this.#router.navigate([{ outlets: { dt: ['dialogs', this.#dialog.openDialogs.map((d, i) => i).join('-')] } }], {
81
+ // replaceUrl: true,
82
+ queryParamsHandling: 'preserve'
83
+ });
84
+ ref.afterClosed().subscribe((res) => {
85
+ if (res?.closedByRoute)
86
+ return;
87
+ const openDialogs = this.#dialog.openDialogs.filter((d) => d.id !== ref.id);
88
+ // having no open dialogs, clear the auxillary route
89
+ const param = openDialogs.length > 0 ? ['dialogs', openDialogs.map((d, i) => i).join('-')] : null;
90
+ this.#router.navigate([{ outlets: { dt: param } }], { replaceUrl: true, queryParamsHandling: 'preserve' });
91
+ });
92
+ });
93
+ this.#router.events.pipe(filter((e) => e instanceof NavigationEnd)).subscribe((e) => {
94
+ // when navigating and having no open dialogs, clear the auxillary route from the url
95
+ if (this.#dialog.openDialogs.length === 0 && e.urlAfterRedirects.match(/(dt:[.]*)/)) {
96
+ this.#router.navigate([{ outlets: { dt: null } }], { replaceUrl: true, queryParamsHandling: 'preserve' });
97
+ }
98
+ // if there are open dialogs and the primary route changed, close all open dialogs
99
+ if (this.#dialog.openDialogs.length > 0) {
100
+ if (this.primaryRouteChanged(this.#router.currentNavigation())) {
101
+ this.#dialog.openDialogs.forEach((d) => d.close());
102
+ }
103
+ }
104
+ });
105
+ }
106
+ #getPrimaryRoute(nav) {
107
+ if (!nav)
108
+ return undefined;
109
+ const url = nav.finalUrl || nav.extractedUrl;
110
+ const urlTreeChildren = url.root.children['primary'];
111
+ return urlTreeChildren ? urlTreeChildren.segments.map((s) => s.path).join('/') : undefined;
112
+ }
113
+ primaryRouteChanged(nav) {
114
+ const previousNav = nav?.previousNavigation || null;
115
+ return this.#getPrimaryRoute(nav) !== this.#getPrimaryRoute(previousNav);
116
+ }
117
+ pickFile() {
118
+ const input = this.#DOCUMENT.createElement('input');
119
+ input.type = 'file';
120
+ input.click();
121
+ return fromEvent(input, 'change').pipe(map((e) => e.target.files?.[0] || null), tap(() => {
122
+ input.remove();
123
+ }));
124
+ }
125
+ setShellConfig(csc) {
126
+ this.shellConfig.update((sc) => ({
127
+ appIcon: csc.appIcon ?? sc.appIcon,
128
+ shellIconNamespace: csc.shellIconNamespace ?? sc.shellIconNamespace
129
+ }));
130
+ }
131
+ setApps(apps) {
132
+ this.apps.set(this.#getMappedApps(apps));
133
+ const appBaseRoutes = {};
134
+ apps.forEach((a) => (appBaseRoutes[a.id] = a.path || ''));
135
+ this.appBaseRoutes = appBaseRoutes;
136
+ }
137
+ #getMappedApps(apps) {
138
+ return apps.map((a) => {
139
+ const ma = {
140
+ ...a,
141
+ title: this.#getAppTitle(a)
142
+ };
143
+ if (a.options?.appClaimKey) {
144
+ ma.options = { ...ma.options, appClaim: this.#getAppClaim(a.options.appClaimKey) || a.options.appClaim };
145
+ }
146
+ return ma;
147
+ });
148
+ }
149
+ #getAppTitle(a) {
150
+ const translatedTitle = this.translate.instant(a.id);
151
+ return translatedTitle && !translatedTitle.startsWith('!missing') ? translatedTitle : a.title || '';
152
+ }
153
+ #getAppClaim(claimKey) {
154
+ const translatedClaim = this.translate.instant(claimKey);
155
+ return translatedClaim && !translatedClaim.startsWith('!missing') ? translatedClaim : undefined;
156
+ }
157
+ getApp(id) {
158
+ return this.apps().find((a) => id === a.id);
159
+ }
160
+ getApps(ids = []) {
161
+ return ids.length ? this.apps().filter((a) => ids.includes(a.id)) : this.apps();
162
+ }
163
+ /**
164
+ * Register settings for apps to hook into shell settings page.
165
+ * @param appID ID of the app that exposes the settings
166
+ * @param cfg ShellAppSettings object containing the settings component
167
+ */
168
+ registerAppSettings(cfg) {
169
+ this.#appSettings.set([...this.#appSettings(), cfg]);
170
+ }
171
+ /**
172
+ * Load persisted settings for a specific app
173
+ * @param appID ID of the app to load the settings for
174
+ */
175
+ usersAppSettings$(appID) {
176
+ return this.#userService.user$.pipe(map((u) => {
177
+ return u && u.userSettings.clientAppSettings ? u.userSettings.clientAppSettings[appID] : undefined;
178
+ }));
179
+ }
180
+ saveUsersAppSettings(appID, settings) {
181
+ return this.#userService.saveUserSettings({ clientAppSettings: { [appID]: settings } });
182
+ }
183
+ addBusy() {
184
+ if (this._busyCount === 0) {
185
+ this.isBusySubject.next(true);
186
+ }
187
+ this._busyCount++;
188
+ }
189
+ removeBusy() {
190
+ this._busyCount--;
191
+ if (this._busyCount === 0) {
192
+ this.isBusySubject.next(false);
193
+ }
194
+ }
195
+ /**
196
+ * Exposes object create flavors to the shell.
197
+ * Thes flavors describe different ways of how objects can be created. So apps
198
+ * can register custom ways of creating objects that can then be used by other apps.
199
+ * @param flavors Create flavors to be exposed
200
+ */
201
+ exposeObjectCreateFlavors(flavors) {
202
+ flavors.forEach((flavor) => {
203
+ const idx = this._objectCreateFlavors.findIndex((f) => f.id === flavor.id);
204
+ if (idx === -1)
205
+ this._objectCreateFlavors.push(flavor);
206
+ });
207
+ }
208
+ /**
209
+ * Remove object create flavors from the shells registry
210
+ * @param flavors Create flavors to be removed
211
+ */
212
+ concealObjectCreateFlavors(flavors) {
213
+ const fid = flavors.map((f) => f.id);
214
+ this._objectCreateFlavors = this._objectCreateFlavors.filter((f) => !fid.includes(f.id));
215
+ }
216
+ /**
217
+ * Get all registerd object create flavors
218
+ */
219
+ getObjectCreateFlavors() {
220
+ return this._objectCreateFlavors;
221
+ }
222
+ /**
223
+ * Exposes a list of object flavors to the shell.
224
+ * Flavors are able to be applied to objects and add new metadata properties to them.
225
+ * @param flavors Array of flavors to be exposed
226
+ * @param app Optional ID of the app that exposes the flavors (defaults to 'global' if not provided)
227
+ * @param origin Optional origin of the flavors. Flavors could be added by apps or derived
228
+ * from a configuration (defaults to 'app' if not provided)
229
+ */
230
+ exposeObjectFlavors(flavors, app = 'global', origin = 'app') {
231
+ flavors.forEach((f) => this.exposeObjectFlavor(f, app, origin));
232
+ }
233
+ /**
234
+ * Removes exposed object flavors from the shell.
235
+ * @param flavors Array of flavors to be concealed
236
+ */
237
+ concealObjectFlavors(flavors, app = 'global') {
238
+ flavors.forEach((f) => this.concealObjectFlavor(f, app));
239
+ }
240
+ /**
241
+ * Exposes an object flavor to the shell. Flavors are able to be applied to objects
242
+ * in order to add a new aspect to them. An object containing an image for example could
243
+ * be added a flavor of 'EXIF Data' that will add the corresponding SOT to it. This way the
244
+ * object gets new metadata properties. If supported, these metadata could even be extracted
245
+ * from the file and filled out automatically.
246
+ * @param flavor The flavor object to be exposed
247
+ * @param app Optional ID of the app that exposes the flavor (defaults to 'global' if not provided)
248
+ * @param origin Optional origin of the flavors. Flavors could be added by apps or derived
249
+ * from a configuration (defaults to 'app' if not provided)
250
+ */
251
+ exposeObjectFlavor(flavor, app = 'global', origin = 'app') {
252
+ const idx = this._objectFlavors.findIndex((f) => f.id === flavor.id && f.app === app);
253
+ if (idx === -1)
254
+ this._objectFlavors.push({ ...flavor, app, origin });
255
+ }
256
+ /**
257
+ * Removes an exposed object flavor from the shell.
258
+ * @param flavor Flavor to be concealed
259
+ * @param app Optional ID of the app that exposes the flavor (defaults to 'global' if not provided)
260
+ */
261
+ concealObjectFlavor(flavor, app = 'global') {
262
+ this._objectFlavors = this._objectFlavors.filter((f) => {
263
+ const hasSameId = f.id === flavor.id;
264
+ const isFromSameApp = f.app === app;
265
+ return hasSameId && isFromSameApp ? false : true;
266
+ });
267
+ }
268
+ /**
269
+ * Get registered object flavors
270
+ * @param app Optional app ID that restricts the returned flavors to the ones
271
+ * provided by a particular app. If not provided all flavors are returned
272
+ * @returns Array of matching object flavors
273
+ */
274
+ getObjectFlavors(app) {
275
+ return app ? this._objectFlavors.filter((of) => of.app === app) : this._objectFlavors;
276
+ }
277
+ /**
278
+ * Triggers the application of an object flavor. If the flavor has an applyComponent
279
+ * defined, the overlay will be opened with the component. Otherwise the flavor will
280
+ * be applied directly to the object.
281
+ * @param dmsObject The object to apply the flavor to
282
+ * @param flavor The flavor to apply
283
+ * @param data Optional data to be passed to the flavor component
284
+ * @returns Observable that emits true if the flavor was applied successfully
285
+ */
286
+ triggerApplyObjectFlavor(dmsObject, flavor, data, applyComponent) {
287
+ if (flavor.preventApply)
288
+ return of(false);
289
+ if (dmsObject) {
290
+ const ac = applyComponent || flavor.applyComponent;
291
+ if (ac) {
292
+ this.#dialog.open(ac, {
293
+ maxWidth: '90vw',
294
+ maxHeight: '90vh',
295
+ minWidth: '50vw',
296
+ data: {
297
+ dmsObject,
298
+ flavor,
299
+ data
300
+ }
301
+ });
302
+ return of(true);
303
+ }
304
+ else {
305
+ const sots = dmsObject.data[BaseObjectTypeField.SECONDARY_OBJECT_TYPE_IDS] || [];
306
+ const data = {};
307
+ data[BaseObjectTypeField.SECONDARY_OBJECT_TYPE_IDS] = [...sots, flavor.sot];
308
+ return this.#dmsService.updateDmsObject(dmsObject?.id, data).pipe(map(() => true));
309
+ }
310
+ }
311
+ else
312
+ return of(false);
313
+ }
314
+ removeObjectFlavor(dmsObject, flavor) {
315
+ if (flavor.preventRemove)
316
+ return of(false);
317
+ const data = {
318
+ [BaseObjectTypeField.SECONDARY_OBJECT_TYPE_IDS]: dmsObject.data[BaseObjectTypeField.SECONDARY_OBJECT_TYPE_IDS].filter((sot) => sot !== flavor.sot)
319
+ };
320
+ return this.#dmsService.updateDmsObject(dmsObject.id, data).pipe(map(() => true));
321
+ }
322
+ /**
323
+ * Get object flavors applicable for a certain mimetype
324
+ * @param mimeType Mime type or mime type pattern like 'image/*'
325
+ * @param app Optional app ID that restricts the returned flavors to the ones
326
+ * provided by a particular app
327
+ * @param customFlavors Optional array of custom flavors to also take into account.
328
+ * This could be flavors managed by an app itself that are not supposed to be
329
+ * exposed to other apps
330
+ * @returns Array of matching object flavors
331
+ */
332
+ getApplicableDocumentFlavors(mimeType, app, customFlavors) {
333
+ return [...(customFlavors || []), ...this._objectFlavors].filter((of) =>
334
+ // !of.preventApply &&
335
+ (!of.applicableTo?.folders || (of.applicableTo?.folders && of.applicableTo?.documents)) &&
336
+ (!of.applicableTo ||
337
+ !of.applicableTo?.mimeTypes ||
338
+ (of.applicableTo?.mimeTypes?.some((mtp) => Utils.patternToRegExp(mtp).test(mimeType)) &&
339
+ (!app || of.app === app))));
340
+ }
341
+ getApplicableFolderFlavors(app, customFlavors) {
342
+ return [...(customFlavors || []), ...this._objectFlavors].filter((of) => !of.preventApply && of.applicableTo?.folders && (!app || of.app === app));
343
+ }
344
+ /**
345
+ * Get applied and applicable object flavors for a certain object
346
+ * @param dmsObject DmsObject to get flavors for
347
+ * @returns Object containing two arrays: applied and applicable flavors
348
+ */
349
+ getAppliedObjectFlavors(dmsObject) {
350
+ const applied = [];
351
+ const applicable = [];
352
+ if (dmsObject) {
353
+ const sots = dmsObject.data[BaseObjectTypeField.SECONDARY_OBJECT_TYPE_IDS] || [];
354
+ if (dmsObject.content) {
355
+ this.getApplicableDocumentFlavors(dmsObject.content.mimeType).forEach((flavor) => {
356
+ flavor = {
357
+ ...flavor,
358
+ name: this.#system.getLocalizedLabel(flavor.id) || this.translate.instant(flavor.id),
359
+ description: this.#system.getLocalizedDescription(flavor.id) ||
360
+ (flavor.descriptionKey ? this.translate.instant(flavor.descriptionKey) : undefined)
361
+ };
362
+ if (sots.includes(flavor.sot)) {
363
+ applied.push(flavor);
364
+ }
365
+ else if (!flavor.preventApply) {
366
+ applicable.push(flavor);
367
+ }
368
+ });
369
+ }
370
+ if (dmsObject.isFolder) {
371
+ this.getApplicableFolderFlavors().forEach((flavor) => {
372
+ flavor = {
373
+ ...flavor,
374
+ name: this.#system.getLocalizedLabel(flavor.id) || this.translate.instant(flavor.id),
375
+ description: this.#system.getLocalizedDescription(flavor.id) ||
376
+ (flavor.descriptionKey ? this.translate.instant(flavor.descriptionKey) : undefined)
377
+ };
378
+ if (sots.includes(flavor.sot)) {
379
+ applied.push(flavor);
380
+ }
381
+ else {
382
+ applicable.push(flavor);
383
+ }
384
+ });
385
+ }
386
+ }
387
+ return { applied, applicable };
388
+ }
389
+ getFlavorLabel(id) {
390
+ return this.#system.getLocalizedLabel(id) || this.translate.instant(id);
391
+ }
392
+ applyObjectFlavor(dmsObject, flavor, data = {}) {
393
+ const sots = dmsObject.data[BaseObjectTypeField.SECONDARY_OBJECT_TYPE_IDS] || [];
394
+ data[BaseObjectTypeField.SECONDARY_OBJECT_TYPE_IDS] = [...sots, flavor.sot];
395
+ return this.#dmsService.updateDmsObject(dmsObject.id, data);
396
+ }
397
+ registerTileExtension(ext) {
398
+ this._extensions[ext.typeId] = ext;
399
+ }
400
+ /**
401
+ * Get tile extensions for a certain type
402
+ * @param typeId ID of the type to fetch extesion for (objectTypeID or secondaryObjectTypeID)
403
+ * @returns
404
+ */
405
+ getRegisteredTileExtensions() {
406
+ return this._extensions;
407
+ }
408
+ _initObjectTypeConfig() {
409
+ // load flavors from config
410
+ console.debug('Loading object type config...');
411
+ return firstValueFrom(this.#shellConfig.get(SHELL_CONFIG_TYPES).pipe(tap((res) => {
412
+ if (res) {
413
+ this.#registerObjectTypeIcons(res.data.map((t) => t.id));
414
+ const flv = res.data.map((t) => ({
415
+ id: t.id,
416
+ sot: t.id,
417
+ icon: t.id,
418
+ svgIcon: true,
419
+ objectTypeID: t.baseType,
420
+ applicableTo: {
421
+ folders: t.baseType === SystemType.FOLDER || t.baseType === SystemType.OBJECT,
422
+ documents: t.baseType === SystemType.DOCUMENT || t.baseType === SystemType.OBJECT,
423
+ mimeTypes: t.applicableToMimeTypes || undefined
424
+ },
425
+ useDefaultApplyComponent: !t.instantApply
426
+ }));
427
+ this.exposeObjectFlavors(flv, 'global', 'config');
428
+ }
429
+ })));
430
+ }
431
+ #registerObjectTypeIcons(ids) {
432
+ this.#system.system$.pipe(first()).subscribe(() => {
433
+ ids.forEach((id) => {
434
+ // split technical name e.g. 'system:document' and use the first part as namespace
435
+ const idTokens = id.split(':');
436
+ if (idTokens.length === 2) {
437
+ const uri = `${this.#configService.getApiBase(ApiBase.none, true)}${this.#system.getObjectTypeIconUri(id)}`;
438
+ this.#iconRegistry.addSvgIconInNamespace(idTokens[0], idTokens[1], this.#sanitizer.bypassSecurityTrustResourceUrl(uri));
439
+ }
440
+ });
441
+ });
442
+ }
443
+ _init() {
444
+ // the place for initialization tasks if
445
+ }
446
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ShellService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
447
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ShellService, providedIn: 'root' }); }
448
+ }
449
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ShellService, decorators: [{
450
+ type: Injectable,
451
+ args: [{
452
+ providedIn: 'root'
453
+ }]
454
+ }], ctorParameters: () => [] });
455
+ //# sourceMappingURL=shell.service.js.map