@sumaris-net/ngx-components 18.16.8 → 18.17.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 (53) hide show
  1. package/doc/changelog.md +4 -0
  2. package/esm2022/public_api.mjs +4 -4
  3. package/esm2022/src/app/admin/users/person.filter.mjs +76 -0
  4. package/esm2022/src/app/admin/users/person.service.mjs +197 -0
  5. package/esm2022/src/app/admin/users/person.validator.mjs +72 -0
  6. package/esm2022/src/app/admin/users/users.mjs +4 -4
  7. package/esm2022/src/app/core/about/about.modal.mjs +5 -5
  8. package/esm2022/src/app/core/home/home.mjs +3 -3
  9. package/esm2022/src/app/core/services/account.service.mjs +8 -2
  10. package/esm2022/src/app/core/services/network.service.mjs +5 -2
  11. package/esm2022/src/app/core/services/validator/account.validator.mjs +2 -2
  12. package/esm2022/src/app/shared/validator/validators.mjs +3 -3
  13. package/esm2022/src/app/social/feed/feed.component.mjs +117 -30
  14. package/esm2022/src/app/social/feed/feed.model.mjs +1 -1
  15. package/esm2022/src/app/social/feed/feed.page.mjs +3 -3
  16. package/esm2022/src/app/social/feed/feed.service.mjs +63 -12
  17. package/esm2022/src/app/social/feed/testing/feed.testing.mjs +3 -3
  18. package/esm2022/src/app/social/message/message.form.mjs +8 -5
  19. package/esm2022/src/app/social/message/message.modal.mjs +10 -4
  20. package/esm2022/src/app/social/message/message.model.mjs +2 -2
  21. package/esm2022/src/app/social/message/message.module.mjs +5 -4
  22. package/esm2022/src/app/social/message/message.service.mjs +81 -1
  23. package/esm2022/src/app/social/social.errors.mjs +2 -1
  24. package/esm2022/src/environments/environment.mjs +7 -4
  25. package/fesm2022/sumaris-net.ngx-components.mjs +1301 -1060
  26. package/fesm2022/sumaris-net.ngx-components.mjs.map +1 -1
  27. package/package.json +1 -1
  28. package/public_api.d.ts +3 -3
  29. package/src/app/admin/{services/filter → users}/person.filter.d.ts +4 -4
  30. package/src/app/admin/{services → users}/person.service.d.ts +2 -1
  31. package/src/app/admin/{services/validator → users}/person.validator.d.ts +4 -4
  32. package/src/app/admin/users/users.d.ts +2 -2
  33. package/src/app/core/services/account.service.d.ts +2 -0
  34. package/src/app/core/services/network.service.d.ts +3 -2
  35. package/src/app/core/services/validator/account.validator.d.ts +1 -1
  36. package/src/app/social/feed/feed.component.d.ts +36 -14
  37. package/src/app/social/feed/feed.model.d.ts +3 -2
  38. package/src/app/social/feed/feed.service.d.ts +35 -24
  39. package/src/app/social/message/message.form.d.ts +3 -3
  40. package/src/app/social/message/message.modal.d.ts +6 -2
  41. package/src/app/social/message/message.model.d.ts +1 -1
  42. package/src/app/social/message/message.module.d.ts +2 -1
  43. package/src/app/social/message/message.service.d.ts +12 -0
  44. package/src/app/social/social.errors.d.ts +1 -0
  45. package/src/assets/i18n/en-US.json +7 -3
  46. package/src/assets/i18n/en.json +5 -2
  47. package/src/assets/i18n/fr.json +5 -2
  48. package/src/assets/manifest.json +1 -1
  49. package/src/theme/_ngx-components.forms.scss +1 -0
  50. package/src/theme/_ngx-components.table.scss +1 -0
  51. package/esm2022/src/app/admin/services/filter/person.filter.mjs +0 -76
  52. package/esm2022/src/app/admin/services/person.service.mjs +0 -184
  53. package/esm2022/src/app/admin/services/validator/person.validator.mjs +0 -72
@@ -1,7 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { InjectionToken, Directive, Pipe, Injectable, EventEmitter, Output, Optional, Inject, inject, NgModule, forwardRef, booleanAttribute, Component, ChangeDetectionStrategy, Input, ViewChildren, HostBinding, HostListener, ElementRef, numberAttribute, ViewChild, ANIMATION_MODULE_TYPE, RendererStyleFlags2, CUSTOM_ELEMENTS_SCHEMA, ChangeDetectorRef, Self, ViewEncapsulation, APP_INITIALIZER, SecurityContext, viewChild, signal, input, model, computed, effect, untracked } from '@angular/core';
3
3
  import { firstValueFrom, shareReplay, tap, of, timer, Subject, merge, delay, isObservable, from, Subscription, BehaviorSubject, fromEvent, noop as noop$9, Observable, forkJoin, timeout, defer, combineLatest, debounceTime as debounceTime$1, distinctUntilChanged as distinctUntilChanged$1, interval, mergeMap as mergeMap$1, switchMap as switchMap$1, EMPTY } from 'rxjs';
4
- import { catchError, map, filter, takeUntil, first, switchMap, tap as tap$1, throttleTime, debounceTime, startWith, distinctUntilChanged, mergeMap, skip, finalize, bufferWhen, distinctUntilKeyChanged, take } from 'rxjs/operators';
4
+ import { catchError, map, filter, takeUntil, first, switchMap, tap as tap$1, throttleTime, debounceTime, startWith, distinctUntilChanged, mergeMap, skip, finalize, distinctUntilKeyChanged, bufferWhen, take } from 'rxjs/operators';
5
5
  import * as cloneImported from 'clone';
6
6
  import * as i3$1 from '@angular/common';
7
7
  import { CommonModule, DOCUMENT, LocationStrategy, Location, AsyncPipe, JsonPipe } from '@angular/common';
@@ -2871,14 +2871,14 @@ class SharedValidators {
2871
2871
  }
2872
2872
  static entity(control) {
2873
2873
  const value = control.value;
2874
- if (value && (typeof value !== 'object' || value.id === undefined || value.id === null)) {
2874
+ if (value && (typeof value !== 'object' || isNil(value.id))) {
2875
2875
  return { entity: true };
2876
2876
  }
2877
2877
  return null;
2878
2878
  }
2879
2879
  static entities(control) {
2880
2880
  const value = control.value;
2881
- if (value && (!Array.isArray(value) || value.every((e) => e?.id === undefined || e?.id === null))) {
2881
+ if (value && (!Array.isArray(value) || value.every((e) => isNil(e?.id)))) {
2882
2882
  return { entity: true };
2883
2883
  }
2884
2884
  return null;
@@ -13138,11 +13138,14 @@ const environment = Object.freeze({
13138
13138
  '/api/feed.json',
13139
13139
  'https://gitlab.ifremer.fr/sih-public/sumaris/ngx-sumaris-components/-/raw/master/doc/feed/feed-fr.json',
13140
13140
  // Example with JsonFeed version 1 (and not 1.1)
13141
- 'https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-fr.json',
13141
+ //'https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-fr.json',
13142
13142
  // Example with discourse API :
13143
- 'https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/1.1/feed-fr-FR.json',
13143
+ //'https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/1.1/feed-fr-FR.json',
13144
+ ],
13145
+ en: [
13146
+ '/api/feed.json',
13147
+ 'https://gitlab.ifremer.fr/sih-public/sumaris/ngx-sumaris-components/-/raw/master/doc/feed/feed-en.json'
13144
13148
  ],
13145
- en: ['https://gitlab.ifremer.fr/sih-public/sumaris/ngx-sumaris-components/-/raw/master/doc/feed/feed-en.json'],
13146
13149
  },
13147
13150
  maxContentLength: 1000,
13148
13151
  maxAgeInMonths: -1,
@@ -19713,6 +19716,9 @@ class NetworkService extends StartableObservableService {
19713
19716
  get connectionParams() {
19714
19717
  return this._connectionParams;
19715
19718
  }
19719
+ get peer$() {
19720
+ return this.dataSubject.pipe(distinctUntilKeyChanged('url'));
19721
+ }
19716
19722
  constructor(_document, platform, modalCtrl, storage, settings, cache, http, environment, loggingService, translate, toastController) {
19717
19723
  super(platform);
19718
19724
  this._document = _document;
@@ -23014,7 +23020,7 @@ const Fragments = {
23014
23020
  `,
23015
23021
  };
23016
23022
  // Account queries
23017
- const Queries = {
23023
+ const Queries$1 = {
23018
23024
  load: gql `
23019
23025
  query Account {
23020
23026
  data: account {
@@ -23176,12 +23182,18 @@ class AccountService extends BaseGraphqlService {
23176
23182
  get account() {
23177
23183
  return this._cache.loaded ? this._data : undefined;
23178
23184
  }
23185
+ get account$() {
23186
+ return this.onChange.asObservable().pipe(startWith(this.account));
23187
+ }
23179
23188
  get person() {
23180
23189
  if (this._cache.loaded && !this._cache.person) {
23181
23190
  this._cache.person = this._cache.loaded ? this._data.asPerson() : undefined;
23182
23191
  }
23183
23192
  return this._cache.person;
23184
23193
  }
23194
+ get person$() {
23195
+ return this.onChange.asObservable().pipe(map(() => this.person), startWith(this.person));
23196
+ }
23185
23197
  get department() {
23186
23198
  if (this._cache.loaded && !this._cache.department) {
23187
23199
  this._cache.department = this._cache.loaded ? this._data.asPerson().department : undefined;
@@ -23610,7 +23622,7 @@ class AccountService extends BaseGraphqlService {
23610
23622
  }
23611
23623
  // Load remotely
23612
23624
  else {
23613
- const query = opts?.query || (this.apiTokenEnabled ? Queries.loadWithTokens : Queries.load);
23625
+ const query = opts?.query || (this.apiTokenEnabled ? Queries$1.loadWithTokens : Queries$1.load);
23614
23626
  const { data } = await this.graphql.query({
23615
23627
  query,
23616
23628
  error: { code: ErrorCodes.LOAD_ACCOUNT_ERROR, message: 'ERROR.LOAD_ACCOUNT_ERROR' },
@@ -30020,10 +30032,10 @@ class AboutModal {
30020
30032
  }
30021
30033
  /* -- protected functions -- */
30022
30034
  setConfig(config) {
30023
- this.forumUrl = config?.getProperty(CORE_CONFIG_OPTIONS.FORUM_URL) || this.environment.forumUrl;
30024
- this.helpUrl = config?.getProperty(CORE_CONFIG_OPTIONS.HELP_URL) || this.environment.helpUrl;
30025
- this.reportIssueUrl = config?.getProperty(CORE_CONFIG_OPTIONS.REPORT_ISSUE_URL) || this.environment.reportIssueUrl;
30026
- this.buildDate = this.environment.buildDate || new Date().toISOString();
30035
+ this.forumUrl = config?.getProperty(CORE_CONFIG_OPTIONS.FORUM_URL) || this.environment?.forumUrl;
30036
+ this.helpUrl = config?.getProperty(CORE_CONFIG_OPTIONS.HELP_URL) || this.environment?.helpUrl;
30037
+ this.reportIssueUrl = config?.getProperty(CORE_CONFIG_OPTIONS.REPORT_ISSUE_URL) || this.environment?.reportIssueUrl;
30038
+ this.buildDate = this.environment?.buildDate;
30027
30039
  }
30028
30040
  async loadNodeInfo() {
30029
30041
  this.nodeInfo = await this.getNodeInfo();
@@ -32720,6 +32732,7 @@ const SocialErrorCodes = {
32720
32732
  SAVE_USER_EVENT_ERROR: 50001,
32721
32733
  COUNT_USER_EVENT_ERROR: 50002,
32722
32734
  SEND_MESSAGE_ERROR: 50003,
32735
+ DELETE_MESSAGE_ERROR: 50004,
32723
32736
  SUBSCRIBE_USER_EVENTS_ERROR: 50005,
32724
32737
  SUBSCRIBE_JOB_PROGRESSION_ERROR: 50010,
32725
32738
  LOAD_JOB_PROGRESSIONS_ERROR: 50011,
@@ -33781,297 +33794,1063 @@ class JsonFeedUtils {
33781
33794
  }
33782
33795
  }
33783
33796
 
33784
- const APP_FEED_SERVICE = new InjectionToken('FeeService');
33785
- class FeedService extends StartableService {
33786
- settings;
33787
- environment;
33788
- _logPrefix = '[feed-service] ';
33789
- _state = new RxState();
33790
- network = inject(NetworkService);
33791
- locale$ = this._state.select('locale');
33792
- feedUrls$ = this._state.select('feedUrls');
33793
- get feedUrls() {
33794
- return this._state.get('feedUrls');
33797
+ var PersonFilter_1;
33798
+ // @dynamic
33799
+ let PersonFilter = class PersonFilter extends EntityFilter {
33800
+ static { PersonFilter_1 = this; }
33801
+ static fromObject;
33802
+ static searchFilter(source) {
33803
+ return source && PersonFilter_1.fromObject(source).asFilterFn();
33795
33804
  }
33796
- set feedUrls(urls) {
33797
- this._state.set('feedUrls', () => urls);
33805
+ email;
33806
+ pubkey;
33807
+ searchText;
33808
+ statusIds;
33809
+ userProfiles;
33810
+ includedIds;
33811
+ excludedIds;
33812
+ searchAttribute;
33813
+ searchAttributes;
33814
+ constructor() {
33815
+ super(PersonFilter_1.TYPENAME);
33798
33816
  }
33799
- constructor(settings, environment) {
33800
- super(settings);
33801
- this.settings = settings;
33802
- this.environment = environment;
33803
- this._state.connect('locale', this.settings.locale$);
33804
- this._state.connect('feedUrls', this.locale$.pipe(filter(isNotNilOrBlank), map((locale) => (locale && environment.feed?.jsonFeed?.[locale]) ?? [])));
33805
- // DEBUG
33806
- this._debug = !environment.production;
33807
- if (this._debug)
33808
- console.debug(`${this._logPrefix}created`);
33817
+ fromObject(source, opts) {
33818
+ super.fromObject(source, opts);
33819
+ this.email = source.email;
33820
+ this.pubkey = source.pubkey;
33821
+ this.searchText = source.searchText;
33822
+ this.statusIds = source.statusIds || (isNotNil(source.statusId) ? [source.statusId] : undefined);
33823
+ this.userProfiles = source.userProfiles;
33824
+ this.excludedIds = source.excludedIds;
33825
+ this.searchAttribute = source.searchAttribute;
33826
+ this.searchAttributes = source.searchAttributes;
33809
33827
  }
33810
- async ngOnStart() {
33811
- await Promise.all([this.settings.ready(), this.network.ready()]);
33812
- return {
33813
- locale: this.settings.locale,
33814
- };
33828
+ asObject(opts) {
33829
+ const target = super.asObject(opts);
33830
+ target.email = this.email;
33831
+ target.pubkey = this.pubkey;
33832
+ target.searchText = this.searchText;
33833
+ target.statusIds = this.statusIds;
33834
+ target.userProfiles = this.userProfiles;
33835
+ target.excludedIds = this.excludedIds;
33836
+ target.searchAttribute = this.searchAttribute;
33837
+ target.searchAttributes = this.searchAttributes;
33838
+ return target;
33815
33839
  }
33816
- watchAll(opts) {
33817
- if (!this.started) {
33818
- return from(this.start()).pipe(switchMap$1(() => this.watchAll(opts)));
33840
+ buildFilter() {
33841
+ const filterFns = super.buildFilter();
33842
+ // Filter by status
33843
+ if (isNotEmptyArray(this.statusIds)) {
33844
+ filterFns.push((e) => this.statusIds.includes(e.statusId));
33819
33845
  }
33820
- if (isNotEmptyArray(opts?.urls)) {
33821
- return this.locale$.pipe(mergeMap((locale) => this.loadAll(opts.urls, { locale, ...opts })));
33846
+ // Filter included ids
33847
+ const includedIds = this.includedIds;
33848
+ if (isNotEmptyArray(includedIds)) {
33849
+ filterFns.push((e) => isNotNil(e.id) && includedIds.includes(e.id));
33822
33850
  }
33823
- return this.feedUrls$.pipe(filter(isNotEmptyArray), mergeMap((urls) => this.loadAll(urls, opts)));
33824
- }
33825
- getHomeUrl(feed) {
33826
- const feedUrl = feed?.feed_url ?? firstArrayValue(this.feedUrls);
33827
- return feedUrl ? UrlUtils.getRootUrl(feedUrl) : undefined;
33828
- }
33829
- getTagUrl(feed, tag) {
33830
- if (!feed || !tag)
33831
- throw new Error("Missing 'feed' or 'tag' argument");
33832
- const baseUrl = this.getHomeUrl(feed);
33833
- return feed.tag_template?.replace('{tag}', tag) ?? baseUrl + '/tag/' + tag;
33851
+ // Filter excluded ids
33852
+ const excludedIds = this.excludedIds;
33853
+ if (isNotEmptyArray(excludedIds)) {
33854
+ filterFns.push((e) => isNil(e.id) || !excludedIds.includes(e.id));
33855
+ }
33856
+ // Search text
33857
+ const searchTextFilter = EntityUtils.searchTextFilter(this.searchAttribute || this.searchAttributes || ['lastName', 'firstName', 'department.name'], this.searchText);
33858
+ if (searchTextFilter)
33859
+ filterFns.push(searchTextFilter);
33860
+ return filterFns;
33834
33861
  }
33835
- async load(url, opts) {
33836
- const { data } = await this.loadAll([url], opts);
33837
- return data?.[0];
33862
+ };
33863
+ PersonFilter = PersonFilter_1 = __decorate([
33864
+ EntityClass({ typename: 'PersonFilterVO' })
33865
+ ], PersonFilter);
33866
+
33867
+ const MessageTypes = {
33868
+ INBOX_MESSAGE: 'INBOX_MESSAGE',
33869
+ EMAIL: 'EMAIL',
33870
+ FEED: 'FEED',
33871
+ };
33872
+ const MessageTypeList = Object.freeze([
33873
+ {
33874
+ id: MessageTypes.INBOX_MESSAGE,
33875
+ icon: 'notifications',
33876
+ label: 'SOCIAL.MESSAGE.TYPE_ENUM.INBOX_MESSAGE',
33877
+ },
33878
+ {
33879
+ id: MessageTypes.EMAIL,
33880
+ icon: 'mail',
33881
+ label: 'SOCIAL.MESSAGE.TYPE_ENUM.EMAIL',
33882
+ },
33883
+ {
33884
+ id: MessageTypes.FEED,
33885
+ icon: 'megaphone',
33886
+ label: 'SOCIAL.MESSAGE.TYPE_ENUM.FEED',
33887
+ },
33888
+ ]);
33889
+ // @dynamic
33890
+ let Message = class Message extends Entity {
33891
+ static fromObject;
33892
+ type;
33893
+ issuer;
33894
+ recipients;
33895
+ recipientFilter;
33896
+ subject;
33897
+ body;
33898
+ constructor() {
33899
+ super();
33838
33900
  }
33839
- async loadAll(urls, opts) {
33840
- await this.ready();
33841
- urls = urls ?? this.feedUrls;
33842
- opts = {
33843
- maxAgeInMonths: opts?.maxAgeInMonths ?? this.environment.feed?.maxAgeInMonths,
33844
- maxContentLength: opts?.maxContentLength ?? this.environment.feed?.maxContentLength,
33845
- locale: this.settings.locale,
33846
- depth: 0,
33847
- ...opts,
33848
- };
33849
- const feeds = await Promise.all((urls || []).map(async (url) => {
33850
- try {
33851
- // Get JSON
33852
- const json = await this.network.get(url);
33853
- console.debug(`${this._logPrefix}Loaded JSON from ${url}`, json);
33854
- // Resolve feed
33855
- return await this.resolveFeed(json, url, opts);
33856
- }
33857
- catch (err) {
33858
- if (err?.status === 404) {
33859
- console.error(`${this._logPrefix} Error while fetching feeds at ${url}. 404 (Not found)`);
33860
- }
33861
- else {
33862
- console.error(`${this._logPrefix} Error while fetching feeds at ${url}`, err);
33863
- }
33864
- return Promise.resolve([]);
33865
- }
33866
- }));
33867
- const data = feeds.flat();
33868
- // Close opened resources
33869
- if (opts.depth === 0) {
33870
- this.onAfterLoadAll();
33871
- }
33872
- return { data, total: data.length };
33901
+ fromObject(source, opts) {
33902
+ super.fromObject(source, opts);
33903
+ this.type = source.type;
33904
+ this.issuer = source.issuer && Person.fromObject(source.issuer);
33905
+ this.recipients = source.recipients && source.recipients.map(Person.fromObject);
33906
+ this.recipientFilter = (source.recipientFilter && PersonFilter.fromObject(source.recipientFilter)) || undefined;
33907
+ this.subject = source.subject;
33908
+ this.body = source.body;
33873
33909
  }
33874
- async resolveFeed(json, url, opts) {
33875
- // Check json is JsonFeed compatible
33876
- if (JsonFeedUtils.isJsonFeed(json)) {
33877
- // Migrate from old versions
33878
- let feed = JsonFeedUtils.migrateFeed(json);
33879
- // Resolve items (e.g. using feed.next_url)
33880
- feed = await this.resolveFeedItems(feed, { ...opts, depth: opts?.depth ?? 0 });
33881
- // Truncate html
33882
- JsonFeedUtils.truncateFeedItemsHtml(feed, opts);
33883
- return isNotEmptyArray(feed?.items) ? [feed] : [];
33884
- }
33885
- // not resolved (unknown format)
33886
- return undefined;
33910
+ asObject(opts) {
33911
+ const target = super.asObject(opts);
33912
+ target.recipients = this.recipients && this.recipients.map((p) => p.asObject(opts));
33913
+ target.recipientFilter = (this.recipientFilter && this.recipientFilter.asObject(opts)) || undefined;
33914
+ return target;
33887
33915
  }
33888
- async resolveFeedItems(json, opts) {
33889
- if (!json)
33890
- return json;
33891
- // Load items
33892
- if (isEmptyArray(json.items) && isNotNilOrBlank(json.next_url)) {
33893
- const depth = opts?.depth ?? 0;
33894
- if (depth >= 3) {
33895
- console.warn(`${this._logPrefix}Max depth reached (${depth}) for ${json.feed_url}`);
33896
- return json;
33897
- }
33898
- // Load items
33899
- const feed = await this.load(json.next_url, { ...opts, depth: depth + 1 });
33900
- if (isEmptyArray(feed?.items)) {
33901
- return json;
33902
- }
33903
- // Merge with the input json
33904
- return {
33905
- ...json,
33906
- title: json.title ?? feed.title,
33907
- home_page_url: json.home_page_url ?? feed.home_page_url,
33908
- authors: isNotEmptyArray(json.authors) ? json.authors : feed.authors,
33909
- items: feed.items,
33910
- tag_template: json.tag_template ?? feed.tag_template,
33911
- };
33912
- }
33913
- return json;
33916
+ };
33917
+ Message = __decorate([
33918
+ EntityClass({ typename: 'MessageVO' })
33919
+ ], Message);
33920
+ // @dynamic
33921
+ let MessageFilter = class MessageFilter extends EntityFilter {
33922
+ static fromObject;
33923
+ fromObject(source, opts) {
33924
+ super.fromObject(source, opts);
33914
33925
  }
33915
- onAfterLoadAll() {
33916
- console.debug(`${this._logPrefix}Feeds loaded`);
33926
+ buildFilter() {
33927
+ return super.buildFilter();
33917
33928
  }
33918
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FeedService, deps: [{ token: LocalSettingsService }, { token: ENVIRONMENT, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
33919
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FeedService });
33920
- }
33921
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FeedService, decorators: [{
33922
- type: Injectable
33923
- }], ctorParameters: () => [{ type: LocalSettingsService }, { type: Environment, decorators: [{
33924
- type: Optional
33925
- }, {
33926
- type: Inject,
33927
- args: [ENVIRONMENT]
33928
- }] }] });
33929
+ };
33930
+ MessageFilter = __decorate([
33931
+ EntityClass({ typename: 'MessageFilterVO' })
33932
+ ], MessageFilter);
33929
33933
 
33930
- class FeedDirective {
33931
- element;
33932
- _subscription = new Subscription();
33933
- platform = inject(PlatformService);
33934
- locationStrategy = inject(LocationStrategy);
33935
- navController = inject(NavController);
33936
- router = inject(Router);
33937
- route = inject(ActivatedRoute);
33938
- _feedUrl;
33939
- _baseUrl;
33940
- set feedUrl(value) {
33941
- this._feedUrl = value;
33942
- this._baseUrl = value && JsonFeedUtils.removeJsonExtension(value);
33934
+ const PersonFragments = {
33935
+ person: gql$1 `
33936
+ fragment PersonFragment on PersonVO {
33937
+ id
33938
+ firstName
33939
+ lastName
33940
+ email
33941
+ pubkey
33942
+ avatar
33943
+ statusId
33944
+ updateDate
33945
+ creationDate
33946
+ profiles
33947
+ username
33948
+ usernameExtranet
33949
+ department {
33950
+ id
33951
+ label
33952
+ name
33953
+ logo
33954
+ __typename
33955
+ }
33956
+ __typename
33943
33957
  }
33944
- get feedUrl() {
33945
- return this._feedUrl;
33958
+ `,
33959
+ personFilter: gql$1 `
33960
+ fragment PersonFilterFragment on PersonFilterVO {
33961
+ email
33962
+ pubkey
33963
+ searchText
33964
+ statusIds
33965
+ userProfiles
33966
+ includedIds
33967
+ excludedIds
33968
+ searchAttribute
33969
+ searchAttributes
33946
33970
  }
33947
- constructor(element) {
33948
- this.element = element;
33949
- // DEBUG
33950
- //console.debug('[feed-directive] Creating feed directive');
33971
+ `,
33972
+ };
33973
+ // Load persons query
33974
+ const PersonQueries = {
33975
+ loadAll: gql$1 `
33976
+ query Persons($offset: Int, $size: Int, $sortBy: String, $sortDirection: String, $filter: PersonFilterVOInput) {
33977
+ data: persons(filter: $filter, offset: $offset, size: $size, sortBy: $sortBy, sortDirection: $sortDirection) {
33978
+ ...PersonFragment
33979
+ }
33951
33980
  }
33952
- ngAfterViewInit() {
33953
- console.debug('[feed-directive] Processing anchors');
33954
- // Listening click events
33955
- this._subscription.add(this.listenClickEvents(this.element.nativeElement));
33981
+ ${PersonFragments.person}
33982
+ `,
33983
+ loadAllWithTotal: gql$1 `
33984
+ query PersonsWithTotal($offset: Int, $size: Int, $sortBy: String, $sortDirection: String, $filter: PersonFilterVOInput) {
33985
+ data: persons(filter: $filter, offset: $offset, size: $size, sortBy: $sortBy, sortDirection: $sortDirection) {
33986
+ ...PersonFragment
33987
+ }
33988
+ total: personsCount(filter: $filter)
33956
33989
  }
33957
- ngOnDestroy() {
33958
- this._subscription.unsubscribe();
33990
+ ${PersonFragments.person}
33991
+ `,
33992
+ };
33993
+ const PersonMutations = {
33994
+ saveAll: gql$1 `
33995
+ mutation savePersons($data: [PersonVOInput]) {
33996
+ data: savePersons(persons: $data) {
33997
+ ...PersonFragment
33998
+ }
33959
33999
  }
33960
- listenClickEvents(nativeElement) {
33961
- console.debug('[feed-directive] Start listening click events...');
33962
- const subscription = new Subscription();
33963
- const listener = (event) => this.click(event, nativeElement);
33964
- const links = nativeElement.querySelectorAll('a');
33965
- links.forEach((link) => {
33966
- // DEBUG
33967
- //console.debug('[feed-directive] Adding click listener to', link);
33968
- link.addEventListener('click', listener);
33969
- subscription.add(() => link.removeEventListener('click', listener));
34000
+ ${PersonFragments.person}
34001
+ `,
34002
+ deleteAll: gql$1 `
34003
+ mutation deletePersons($ids: [Int]) {
34004
+ deletePersons(ids: $ids)
34005
+ }
34006
+ `,
34007
+ };
34008
+ class PersonService extends BaseEntityService {
34009
+ graphql;
34010
+ platform;
34011
+ network;
34012
+ entities;
34013
+ constructor(graphql, platform, network, entities) {
34014
+ super(graphql, platform, Person, PersonFilter, {
34015
+ queries: PersonQueries,
34016
+ mutations: PersonMutations,
34017
+ defaultSortBy: 'lastName',
34018
+ defaultSortDirection: 'asc',
33970
34019
  });
33971
- // DEBUG
33972
- subscription.add(() => console.debug('[feed-directive] Stop listening click events'));
33973
- return subscription;
34020
+ this.graphql = graphql;
34021
+ this.platform = platform;
34022
+ this.network = network;
34023
+ this.entities = entities;
34024
+ // for DEV only -----
34025
+ this._debug = !environment.production;
33974
34026
  }
33975
- async click(event, containerElement) {
33976
- console.debug('[feed-directive] Processing click event...', event);
33977
- if (!(event.target instanceof HTMLAnchorElement) && !(event.target?.['tagName'] === 'A')) {
33978
- console.warn('[feed-directive] Invalid click event target. Should an anchor <a>', event.target);
33979
- return;
33980
- }
33981
- const element = event.target;
33982
- let href = element.getAttribute('href');
33983
- const fragment = UrlUtils.getFragment(href);
33984
- // Fragment
33985
- if (href?.startsWith('#') && isNotNilOrBlank(fragment)) {
33986
- event.preventDefault();
33987
- this.scrollToAnchor(containerElement, fragment);
33988
- return true;
33989
- }
33990
- const routerLink = element.getAttribute('routerLink');
33991
- if (isNotNilOrBlank(routerLink)) {
33992
- event.preventDefault();
33993
- return this.navController.navigateForward(routerLink);
33994
- }
33995
- // Resolve relative URL, if baseUrl has been set
33996
- if (this._baseUrl && UrlUtils.isRelativeUrl(href)) {
33997
- // Resolve URL, then open using the platform
33998
- href = UrlUtils.resolveRelativeUrl(this._baseUrl, href);
33999
- }
34000
- // Resolve internal URL as an app route
34001
- if (UrlUtils.isInternalUrl(href)) {
34002
- const routePath = this.normalizeUrl(href.startsWith('/') ? href : `/${href}`);
34003
- const urlTree = this.getUrlTree(routePath);
34004
- event.preventDefault();
34005
- this.router.navigated = false;
34006
- // Opening route
34007
- return this.navController.navigateForward(urlTree);
34027
+ async loadAll(offset, size, sortBy, sortDirection, filter, opts) {
34028
+ const offline = this.network.offline && (!opts || opts.fetchPolicy !== 'network-only');
34029
+ if (offline) {
34030
+ return this.loadAllLocally(offset, size, sortBy, sortDirection, filter, opts);
34008
34031
  }
34009
- // External URL: open using the platform
34010
- event.preventDefault();
34011
- await this.platform.open(href);
34012
- return true;
34032
+ return super.loadAll(offset, size, sortBy, sortDirection, filter, opts);
34013
34033
  }
34014
- scrollToAnchor(containerElement, anchorId) {
34015
- if (!containerElement || !anchorId) {
34016
- console.warn("Missing 'containerElement' or 'fragment' argument.");
34017
- return;
34018
- }
34019
- console.debug('[feed-directive-anchor] Scrolling to anchor #' + anchorId);
34020
- // Find the target element with the specified fragment ID
34021
- const targetElement = containerElement.querySelector(`#${anchorId}`);
34022
- if (targetElement) {
34023
- // Scroll smoothly to the target element
34024
- // eslint-disable-next-line @rx-angular/prefer-no-layout-sensitive-apis
34025
- targetElement.scrollIntoView({
34026
- behavior: 'smooth', // Smooth scrolling behavior
34027
- block: 'start', // Align the target element to the top of the container
34028
- });
34029
- }
34030
- else {
34031
- console.warn(`No element found with the ID "${anchorId}" inside the container.`);
34034
+ async loadAllLocally(offset, size, sortBy, sortDirection, filter, opts) {
34035
+ filter = this.asFilter(filter);
34036
+ const variables = {
34037
+ offset: offset || 0,
34038
+ size: size || 100,
34039
+ sortBy: sortBy || this.defaultSortBy,
34040
+ sortDirection: sortDirection || this.defaultSortDirection,
34041
+ filter: filter && filter.asFilterFn(),
34042
+ };
34043
+ const { data, total } = await this.entities.loadAll('PersonVO', variables);
34044
+ const entities = this.fromObjects(data, opts);
34045
+ const res = { data: entities, total };
34046
+ // Add fetch more function
34047
+ const nextOffset = (offset || 0) + entities.length;
34048
+ if (nextOffset < total) {
34049
+ res.fetchMore = () => this.loadAllLocally(nextOffset, size, sortBy, sortDirection, filter, opts);
34032
34050
  }
34051
+ return res;
34033
34052
  }
34034
- /**
34035
- * Transform a relative URL to its absolute representation according to current router state.
34036
- * @param url Relative URL path.
34037
- * @return Absolute URL based on the current route.
34038
- */
34039
- normalizeUrl(url) {
34040
- if (UrlUtils.isExternalUrl(url)) {
34041
- return url;
34042
- }
34043
- if (this._baseUrl && UrlUtils.isRelativeUrl(url)) {
34044
- return UrlUtils.resolveRelativeUrl(this._baseUrl, url);
34045
- }
34046
- const urlTree = this.getUrlTree(url);
34047
- const serializedUrl = this.router.serializeUrl(urlTree);
34048
- return this.locationStrategy.prepareExternalUrl(serializedUrl);
34053
+ async suggest(value, filter, sortBy, sortDirection, opts) {
34054
+ if (EntityUtils.isNotEmpty(value, 'id'))
34055
+ return { data: [value] };
34056
+ value = (typeof value === 'string' && value !== '*' && value) || undefined;
34057
+ sortBy = sortBy || filter.searchAttribute || (filter.searchAttributes && filter.searchAttributes[0]);
34058
+ return this.loadAll(0, !value ? 30 : 10, sortBy, sortDirection, {
34059
+ ...filter,
34060
+ searchText: value,
34061
+ statusIds: (filter && filter.statusIds) || [StatusIds.ENABLE, StatusIds.TEMPORARY],
34062
+ userProfiles: filter && filter.userProfiles,
34063
+ }, {
34064
+ withTotal: true /* need by autocomplete */,
34065
+ ...opts,
34066
+ });
34049
34067
  }
34050
- getUrlTree(url) {
34051
- url = UrlUtils.normalizeUrl(url);
34052
- const urlPath = UrlUtils.stripFragmentAndQuery(url) || UrlUtils.stripFragmentAndQuery(this.router.url);
34053
- const parsedUrl = this.router.parseUrl(url);
34054
- const fragment = parsedUrl.fragment;
34055
- const queryParams = parsedUrl.queryParams;
34056
- return this.router.createUrlTree([urlPath], { relativeTo: this.route, fragment, queryParams });
34068
+ async executeImport(filter, opts) {
34069
+ const maxProgression = (opts && opts.maxProgression) || 100;
34070
+ filter = {
34071
+ ...filter,
34072
+ statusIds: [StatusIds.ENABLE, StatusIds.TEMPORARY],
34073
+ userProfiles: ['SUPERVISOR', 'USER', 'GUEST'],
34074
+ };
34075
+ console.info('[person-service] Importing persons...');
34076
+ const res = await JobUtils.fetchAllPages((offset, size) => this.loadAll(offset, size, 'id', null, filter, {
34077
+ debug: false,
34078
+ fetchPolicy: 'network-only',
34079
+ withTotal: offset === 0, // Compute total only once
34080
+ toEntity: false,
34081
+ }), { progression: opts?.progression, maxProgression: maxProgression * 0.9 });
34082
+ // Save result locally
34083
+ await this.entities.saveAll(res.data, { entityName: 'PersonVO', reset: true });
34057
34084
  }
34058
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FeedDirective, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
34059
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.13", type: FeedDirective, selector: "feed,[feed]", inputs: { feedUrl: ["feed", "feedUrl"] }, ngImport: i0 });
34060
- }
34061
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FeedDirective, decorators: [{
34062
- type: Directive,
34063
- args: [{
34064
- selector: 'feed,[feed]',
34065
- }]
34066
- }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { feedUrl: [{
34067
- type: Input,
34068
- args: [{ alias: 'feed', required: true }]
34069
- }] } });
34070
-
34071
- const NOOP_ITEM_FILTER = (item) => true;
34085
+ async loadById(id, opts) {
34086
+ const { data } = await this.loadAll(0, 1, null, null, { includedIds: [id] }, { withTotal: false, ...opts });
34087
+ const source = isNotEmptyArray(data) ? data[0] : { id };
34088
+ return this.fromObject(source, opts);
34089
+ }
34090
+ async loadByPubkey(pubkey, opts) {
34091
+ const { data } = await this.loadAll(0, 1, null, null, { pubkey }, { withTotal: false, ...opts });
34092
+ const source = isNotEmptyArray(data) ? data[0] : { pubkey };
34093
+ return this.fromObject(source, opts);
34094
+ }
34095
+ /* -- protected methods -- */
34096
+ asObject(source) {
34097
+ if (!source)
34098
+ return undefined;
34099
+ if (!(source instanceof Person)) {
34100
+ source = Person.fromObject(source);
34101
+ }
34102
+ const target = source.asObject();
34103
+ // Not known in server GraphQL schema
34104
+ delete target.mainProfile;
34105
+ target.department = source.department?.asObject();
34106
+ return target;
34107
+ }
34108
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: PersonService, deps: [{ token: GraphqlService }, { token: PlatformService }, { token: NetworkService }, { token: EntitiesStorage }], target: i0.ɵɵFactoryTarget.Injectable });
34109
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: PersonService, providedIn: 'root' });
34110
+ }
34111
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: PersonService, decorators: [{
34112
+ type: Injectable,
34113
+ args: [{ providedIn: 'root' }]
34114
+ }], ctorParameters: () => [{ type: GraphqlService }, { type: PlatformService }, { type: NetworkService }, { type: EntitiesStorage }] });
34115
+
34116
+ class MessageForm extends AppForm {
34117
+ formBuilder;
34118
+ cd;
34119
+ mobile;
34120
+ suggestFn;
34121
+ subjectMinLength = 5;
34122
+ subjectMaxLength = 255;
34123
+ bodyMaxLength = 2000;
34124
+ bodyAutoHeight = true;
34125
+ canSelectType = false;
34126
+ canRecipientFilter = false;
34127
+ recipientFilterCount = 0;
34128
+ types = MessageTypeList;
34129
+ constructor(injector, formBuilder, cd) {
34130
+ super(injector);
34131
+ this.formBuilder = formBuilder;
34132
+ this.cd = cd;
34133
+ this.mobile = this.settings.mobile;
34134
+ }
34135
+ ngOnInit() {
34136
+ this.setForm(this.formBuilder.group({
34137
+ type: [MessageTypes.INBOX_MESSAGE, Validators.required],
34138
+ recipients: [null, Validators.required],
34139
+ recipientFilter: [null],
34140
+ subject: [
34141
+ null,
34142
+ this.subjectMaxLength
34143
+ ? Validators.compose([Validators.required, Validators.minLength(this.subjectMinLength), Validators.maxLength(this.subjectMaxLength)])
34144
+ : Validators.required,
34145
+ ],
34146
+ body: [null, this.bodyMaxLength ? Validators.compose([Validators.maxLength(this.bodyMaxLength)]) : Validators.required],
34147
+ }));
34148
+ this.registerSubscription(this._form
34149
+ .get('type')
34150
+ .valueChanges.pipe(filter(isNotNil))
34151
+ .subscribe((type) => this.updateFormGroup(this._form, { type })));
34152
+ // Person combo
34153
+ const personAttributes = this.settings.getFieldDisplayAttributes('person', ['lastName', 'firstName', 'department.name']);
34154
+ this.registerAutocompleteField('recipients', {
34155
+ showAllOnFocus: false,
34156
+ suggestFn: this.suggestFn,
34157
+ filter: {
34158
+ statusIds: [StatusIds.TEMPORARY, StatusIds.ENABLE],
34159
+ },
34160
+ attributes: personAttributes,
34161
+ columnNames: personAttributes.map((attr) => `USER.${changeCaseToUnderscore(attr).toUpperCase()}`),
34162
+ displayWith: PersonUtils.personToString,
34163
+ multiple: true,
34164
+ mobile: this.mobile,
34165
+ });
34166
+ }
34167
+ isSamePerson(o1, o2) {
34168
+ return EntityUtils.equals(o1, o2, 'id');
34169
+ }
34170
+ updateFormGroup(formGroup, opts) {
34171
+ console.debug('[message-form] Updating form group...', opts);
34172
+ // Recipient validator
34173
+ const recipientsRequired = toBoolean(opts?.recipientRequired, opts?.type !== MessageTypes.FEED);
34174
+ {
34175
+ const control = formGroup.get('recipients');
34176
+ if (recipientsRequired) {
34177
+ if (!control.hasValidator(Validators.required)) {
34178
+ control.addValidators(Validators.required);
34179
+ }
34180
+ control.enable();
34181
+ }
34182
+ else {
34183
+ if (control.hasValidator(Validators.required)) {
34184
+ control.removeValidators(Validators.required);
34185
+ }
34186
+ control.disable();
34187
+ }
34188
+ }
34189
+ // Recipient filter validator
34190
+ const recipientFilterRequired = this.canRecipientFilter && !recipientsRequired;
34191
+ {
34192
+ const control = formGroup.get('recipientFilter');
34193
+ if (recipientFilterRequired) {
34194
+ if (!control.hasValidator(Validators.required)) {
34195
+ control.addValidators(Validators.required);
34196
+ }
34197
+ control.enable();
34198
+ }
34199
+ else {
34200
+ if (control.hasValidator(Validators.required)) {
34201
+ control.removeValidators(Validators.required);
34202
+ }
34203
+ control.disable();
34204
+ }
34205
+ }
34206
+ formGroup.updateValueAndValidity();
34207
+ }
34208
+ markForCheck() {
34209
+ this.cd.markForCheck();
34210
+ }
34211
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: MessageForm, deps: [{ token: i0.Injector }, { token: i1$3.UntypedFormBuilder }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
34212
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: MessageForm, selector: "app-message-form", inputs: { mobile: "mobile", suggestFn: "suggestFn", subjectMinLength: "subjectMinLength", subjectMaxLength: "subjectMaxLength", bodyMaxLength: "bodyMaxLength", bodyAutoHeight: "bodyAutoHeight", canSelectType: "canSelectType", canRecipientFilter: "canRecipientFilter", recipientFilterCount: "recipientFilterCount" }, usesInheritance: true, ngImport: i0, template: "<!-- debug -->\n@if (debug) {\n <ng-container *ngTemplateOutlet=\"debugPanel\"></ng-container>\n}\n\n<form [formGroup]=\"form\" class=\"form-container\">\n <!-- type -->\n <mat-form-field *ngIf=\"canSelectType\">\n <mat-label>{{ 'SOCIAL.MESSAGE.TYPE' | translate }}</mat-label>\n <mat-select formControlName=\"type\">\n @for (item of types; track item.id) {\n @if (canRecipientFilter || item.id !== 'FEED') {\n <mat-option [value]=\"item.id\">\n <ion-icon [name]=\"item.icon\"></ion-icon>\n {{ item.label | translate }}\n </mat-option>\n }\n }\n </mat-select>\n </mat-form-field>\n\n <!-- Recipients -->\n @if ((form | formGetValue: 'type') !== 'FEED') {\n <mat-chips-field\n formControlName=\"recipients\"\n chipColor=\"accent\"\n [placeholder]=\"'SOCIAL.MESSAGE.RECIPIENTS' | translate\"\n [config]=\"autocompleteFields.recipients\"\n [equals]=\"isSamePerson\"\n ></mat-chips-field>\n } @else {\n <mat-form-field>\n <input matInput hidden formControlName=\"recipients\" type=\"text\" />\n <mat-chip-grid>\n <mat-chip-row>\n {{ 'SOCIAL.MESSAGE.RECIPIENT_FILTER_COUNT' | translate: { count: recipientFilterCount } }}\n </mat-chip-row>\n </mat-chip-grid>\n </mat-form-field>\n }\n\n <!-- Subject -->\n <mat-form-field>\n <mat-label>{{ 'SOCIAL.MESSAGE.SUBJECT' | translate }}</mat-label>\n <input matInput type=\"text\" formControlName=\"subject\" autocomplete=\"off\" required />\n <mat-error *ngIf=\"form.controls.subject.hasError('required')\" translate>ERROR.FIELD_REQUIRED</mat-error>\n <mat-error *ngIf=\"form.controls.subject.hasError('minlength')\">\n {{ 'ERROR.FIELD_MIN_LENGTH_COMPACT' | translate }}\n </mat-error>\n <mat-error *ngIf=\"form.controls.subject.hasError('maxlength')\">\n {{ 'ERROR.FIELD_MAX_LENGTH_COMPACT' | translate }}\n </mat-error>\n <mat-hint *ngIf=\"subjectMaxLength\" align=\"end\">\n {{\n 'INFO.TEXT_PROGRESS' | translate: { current: form.controls.subject.value?.length || 0, max: subjectMaxLength }\n }}\n </mat-hint>\n </mat-form-field>\n\n <!-- Body -->\n <mat-form-field>\n <mat-label>{{ 'SOCIAL.MESSAGE.BODY_HELP' | translate }}</mat-label>\n <textarea\n matInput\n type=\"text\"\n formControlName=\"body\"\n [class.fixed-height]=\"!bodyAutoHeight\"\n [cdkTextareaAutosize]=\"bodyAutoHeight\"\n (keydown.control.enter)=\"doSubmit($event)\"\n ></textarea>\n <mat-error *ngIf=\"form.controls.body.hasError('maxlength')\">\n {{ 'ERROR.FIELD_MAX_LENGTH' | translate: form.controls.body.errors.maxlength }}\n </mat-error>\n <mat-hint *ngIf=\"bodyMaxLength\" align=\"end\">\n {{ 'INFO.TEXT_PROGRESS' | translate: { current: form.controls.body.value?.length || 0, max: bodyMaxLength } }}\n </mat-hint>\n </mat-form-field>\n</form>\n\n\n<ng-template #debugPanel>\n <app-debug title=\"Message form\">\n <ion-grid>\n <ion-row>\n <ion-col>\n ready: {{ readySubject | async }}\n <br />\n loading: {{ loadingSubject | async }}\n <br />\n enabled: {{ enabled }}\n <br />\n dirty: {{ dirty }}\n <br />\n valid: {{ valid }}\n <br />\n <br />\n error: {{ _form | formError | json}}\n </ion-col>\n </ion-row>\n </ion-grid>\n </app-debug>\n</ng-template>\n", styles: ["textarea.fixed-height{height:11.5em}textarea{min-height:11.5em}\n"], dependencies: [{ kind: "directive", type: i3$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: i2$1.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i2$1.IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: i2$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$1.IonRow, selector: "ion-row" }, { kind: "directive", type: i1$1.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { kind: "directive", type: i1$3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$3.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$3.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$3.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i3.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "directive", type: i1$4.CdkTextareaAutosize, selector: "textarea[cdkTextareaAutosize]", inputs: ["cdkAutosizeMinRows", "cdkAutosizeMaxRows", "cdkTextareaAutosize", "placeholder"], exportAs: ["cdkTextareaAutosize"] }, { kind: "component", type: i6$3.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i2.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: MatChipsField, selector: "mat-chips-field", inputs: ["equals", "logPrefix", "formControl", "formControlName", "floatLabel", "placeholder", "suggestFn", "required", "mobile", "readonly", "clearable", "debounceTime", "displaySeparator", "displayWith", "displayAttributes", "displayColumnSizes", "displayColumnNames", "highlightAccent", "showAllOnFocus", "showPanelOnFocus", "autofocus", "config", "i18nPrefix", "noResultMessage", "panelClass", "panelWidth", "disableRipple", "matAutocompletePosition", "itemSize", "fetchMoreThreshold", "suggestLengthThreshold", "showLoadingSpinner", "chipColor", "debug", "applyImplicitValue", "dropButtonTitle", "clearButtonTitle", "trimSearchText", "hideRequiredMarker", "colSizes", "separatorKeysCodes", "appearance", "subscriptSizing", "class", "filter", "tabindex", "items"], outputs: ["click", "blur", "focus", "dropButtonClick", "keydown.escape", "keyup.enter"] }, { kind: "directive", type: AutoResizeDirective, selector: "textarea[cdkTextareaAutosize]", inputs: ["cdkAutosizeMaxRows", "cdkAutosizeMinRows"] }, { kind: "component", type: i11$1.MatChipGrid, selector: "mat-chip-grid", inputs: ["disabled", "placeholder", "required", "value", "errorStateMatcher"], outputs: ["change", "valueChange"] }, { kind: "component", type: i11$1.MatChipRow, selector: "mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]", inputs: ["editable"], outputs: ["edited"] }, { kind: "component", type: DebugComponent, selector: "app-debug", inputs: ["titlePrefix", "title", "enable", "expanded"] }, { kind: "pipe", type: i3$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i3$1.JsonPipe, name: "json" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }, { kind: "pipe", type: FormErrorPipe, name: "formError" }, { kind: "pipe", type: FormGetValuePipe, name: "formGetValue" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
34213
+ }
34214
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: MessageForm, decorators: [{
34215
+ type: Component,
34216
+ args: [{ selector: 'app-message-form', changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- debug -->\n@if (debug) {\n <ng-container *ngTemplateOutlet=\"debugPanel\"></ng-container>\n}\n\n<form [formGroup]=\"form\" class=\"form-container\">\n <!-- type -->\n <mat-form-field *ngIf=\"canSelectType\">\n <mat-label>{{ 'SOCIAL.MESSAGE.TYPE' | translate }}</mat-label>\n <mat-select formControlName=\"type\">\n @for (item of types; track item.id) {\n @if (canRecipientFilter || item.id !== 'FEED') {\n <mat-option [value]=\"item.id\">\n <ion-icon [name]=\"item.icon\"></ion-icon>\n {{ item.label | translate }}\n </mat-option>\n }\n }\n </mat-select>\n </mat-form-field>\n\n <!-- Recipients -->\n @if ((form | formGetValue: 'type') !== 'FEED') {\n <mat-chips-field\n formControlName=\"recipients\"\n chipColor=\"accent\"\n [placeholder]=\"'SOCIAL.MESSAGE.RECIPIENTS' | translate\"\n [config]=\"autocompleteFields.recipients\"\n [equals]=\"isSamePerson\"\n ></mat-chips-field>\n } @else {\n <mat-form-field>\n <input matInput hidden formControlName=\"recipients\" type=\"text\" />\n <mat-chip-grid>\n <mat-chip-row>\n {{ 'SOCIAL.MESSAGE.RECIPIENT_FILTER_COUNT' | translate: { count: recipientFilterCount } }}\n </mat-chip-row>\n </mat-chip-grid>\n </mat-form-field>\n }\n\n <!-- Subject -->\n <mat-form-field>\n <mat-label>{{ 'SOCIAL.MESSAGE.SUBJECT' | translate }}</mat-label>\n <input matInput type=\"text\" formControlName=\"subject\" autocomplete=\"off\" required />\n <mat-error *ngIf=\"form.controls.subject.hasError('required')\" translate>ERROR.FIELD_REQUIRED</mat-error>\n <mat-error *ngIf=\"form.controls.subject.hasError('minlength')\">\n {{ 'ERROR.FIELD_MIN_LENGTH_COMPACT' | translate }}\n </mat-error>\n <mat-error *ngIf=\"form.controls.subject.hasError('maxlength')\">\n {{ 'ERROR.FIELD_MAX_LENGTH_COMPACT' | translate }}\n </mat-error>\n <mat-hint *ngIf=\"subjectMaxLength\" align=\"end\">\n {{\n 'INFO.TEXT_PROGRESS' | translate: { current: form.controls.subject.value?.length || 0, max: subjectMaxLength }\n }}\n </mat-hint>\n </mat-form-field>\n\n <!-- Body -->\n <mat-form-field>\n <mat-label>{{ 'SOCIAL.MESSAGE.BODY_HELP' | translate }}</mat-label>\n <textarea\n matInput\n type=\"text\"\n formControlName=\"body\"\n [class.fixed-height]=\"!bodyAutoHeight\"\n [cdkTextareaAutosize]=\"bodyAutoHeight\"\n (keydown.control.enter)=\"doSubmit($event)\"\n ></textarea>\n <mat-error *ngIf=\"form.controls.body.hasError('maxlength')\">\n {{ 'ERROR.FIELD_MAX_LENGTH' | translate: form.controls.body.errors.maxlength }}\n </mat-error>\n <mat-hint *ngIf=\"bodyMaxLength\" align=\"end\">\n {{ 'INFO.TEXT_PROGRESS' | translate: { current: form.controls.body.value?.length || 0, max: bodyMaxLength } }}\n </mat-hint>\n </mat-form-field>\n</form>\n\n\n<ng-template #debugPanel>\n <app-debug title=\"Message form\">\n <ion-grid>\n <ion-row>\n <ion-col>\n ready: {{ readySubject | async }}\n <br />\n loading: {{ loadingSubject | async }}\n <br />\n enabled: {{ enabled }}\n <br />\n dirty: {{ dirty }}\n <br />\n valid: {{ valid }}\n <br />\n <br />\n error: {{ _form | formError | json}}\n </ion-col>\n </ion-row>\n </ion-grid>\n </app-debug>\n</ng-template>\n", styles: ["textarea.fixed-height{height:11.5em}textarea{min-height:11.5em}\n"] }]
34217
+ }], ctorParameters: () => [{ type: i0.Injector }, { type: i1$3.UntypedFormBuilder }, { type: i0.ChangeDetectorRef }], propDecorators: { mobile: [{
34218
+ type: Input
34219
+ }], suggestFn: [{
34220
+ type: Input
34221
+ }], subjectMinLength: [{
34222
+ type: Input
34223
+ }], subjectMaxLength: [{
34224
+ type: Input
34225
+ }], bodyMaxLength: [{
34226
+ type: Input
34227
+ }], bodyAutoHeight: [{
34228
+ type: Input
34229
+ }], canSelectType: [{
34230
+ type: Input
34231
+ }], canRecipientFilter: [{
34232
+ type: Input
34233
+ }], recipientFilterCount: [{
34234
+ type: Input
34235
+ }] } });
34236
+
34237
+ class MessageModal {
34238
+ settings;
34239
+ viewCtrl;
34240
+ accountService;
34241
+ cd;
34242
+ mobile;
34243
+ debug = false;
34244
+ title = 'SOCIAL.MESSAGE.NEW';
34245
+ suggestFn;
34246
+ data;
34247
+ canSelectType;
34248
+ canRecipientFilter;
34249
+ recipientFilterCount;
34250
+ form;
34251
+ constructor(settings, viewCtrl, accountService, cd) {
34252
+ this.settings = settings;
34253
+ this.viewCtrl = viewCtrl;
34254
+ this.accountService = accountService;
34255
+ this.cd = cd;
34256
+ this.mobile = this.settings.mobile;
34257
+ }
34258
+ ngOnInit() {
34259
+ this.canSelectType = toBoolean(this.canSelectType, this.accountService.isAdmin());
34260
+ }
34261
+ ngAfterViewInit() {
34262
+ setTimeout(() => {
34263
+ this.form.markAsReady({ emitEvent: false });
34264
+ if (this.data) {
34265
+ this.data.type = this.data.type || MessageTypes.INBOX_MESSAGE;
34266
+ this.form.setValue(this.data);
34267
+ }
34268
+ this.form.markAsLoaded();
34269
+ this.form.enable();
34270
+ });
34271
+ }
34272
+ cancel() {
34273
+ this.viewCtrl.dismiss();
34274
+ }
34275
+ async doSubmit() {
34276
+ if (this.form.disabled)
34277
+ return;
34278
+ if (!this.form.valid) {
34279
+ await AppFormUtils.waitWhilePending(this.form);
34280
+ if (this.form.invalid) {
34281
+ AppFormUtils.logFormErrors(this.form.form, '[message-modal] ');
34282
+ this.form.markAllAsTouched();
34283
+ return;
34284
+ }
34285
+ }
34286
+ this.markAsLoading();
34287
+ try {
34288
+ const data = this.form.value;
34289
+ // Disable the form
34290
+ this.form.disable();
34291
+ const entity = Message.fromObject(data);
34292
+ return this.viewCtrl.dismiss(entity);
34293
+ }
34294
+ catch (err) {
34295
+ this.form.error = (err && err.message) || err;
34296
+ this.markAsLoaded();
34297
+ // Enable the form
34298
+ this.form.enable();
34299
+ // Reset form error on next changes
34300
+ firstNotNilPromise(this.form.form.valueChanges).then(() => {
34301
+ this.form.error = null;
34302
+ this.markForCheck();
34303
+ });
34304
+ return;
34305
+ }
34306
+ }
34307
+ markForCheck() {
34308
+ this.cd.markForCheck();
34309
+ }
34310
+ markAsLoading(opts) {
34311
+ this.form.markAsLoading(opts);
34312
+ }
34313
+ markAsLoaded(opts) {
34314
+ this.form.markAsLoaded(opts);
34315
+ }
34316
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: MessageModal, deps: [{ token: LocalSettingsService }, { token: i2$1.ModalController }, { token: AccountService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
34317
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: MessageModal, selector: "app-message-modal", inputs: { debug: "debug", title: "title", suggestFn: "suggestFn", data: "data", canSelectType: "canSelectType", canRecipientFilter: "canRecipientFilter", recipientFilterCount: "recipientFilterCount" }, viewQueries: [{ propertyName: "form", first: true, predicate: ["form"], descendants: true, static: true }], ngImport: i0, template: "<ion-header>\n <ion-toolbar color=\"secondary\">\n <ion-buttons slot=\"start\">\n <ion-button class=\"back-button\" (click)=\"cancel()\" visible-xs visible-sm visible-mobile>\n <ion-icon slot=\"icon-only\" name=\"arrow-back\"></ion-icon>\n </ion-button>\n </ion-buttons>\n\n <ion-title>\n {{ title | translate }}\n </ion-title>\n\n <ion-buttons slot=\"end\">\n <ion-button\n class=\"back-button\"\n (click)=\"doSubmit()\"\n [disabled]=\"!form.valid\"\n visible-xs\n visible-sm\n visible-mobile\n >\n <ion-icon slot=\"icon-only\" name=\"checkmark\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n\n<ion-content class=\"ion-padding\">\n <ion-item *ngIf=\"form.error\" visible-xs visible-sm visible-mobile lines=\"none\">\n <ion-icon color=\"danger\" slot=\"start\" name=\"alert-circle\"></ion-icon>\n <ion-label color=\"danger\" class=\"error\" [innerHTML]=\"form.error | translate\"></ion-label>\n </ion-item>\n\n <app-message-form\n #form\n (onSubmit)=\"doSubmit()\"\n (onCancel)=\"cancel()\"\n [canSelectType]=\"canSelectType\"\n [canRecipientFilter]=\"canRecipientFilter\"\n [recipientFilterCount]=\"recipientFilterCount\"\n [suggestFn]=\"suggestFn\"\n [debug]=\"debug\">\n ></app-message-form>\n</ion-content>\n\n<ion-footer hidden-xs hidden-sm hidden-mobile>\n <ion-toolbar>\n <ion-row class=\"ion-no-padding\" nowrap>\n <ion-col></ion-col>\n\n <!-- buttons -->\n <ion-col size=\"auto\">\n <ion-button fill=\"clear\" color=\"dark\" (click)=\"cancel()\">\n <ion-label translate>COMMON.BTN_CANCEL</ion-label>\n </ion-button>\n\n <ion-button\n [fill]=\"form.invalid ? 'clear' : 'solid'\"\n [disabled]=\"form.loading || form.invalid\"\n (keyup.enter)=\"doSubmit()\"\n (click)=\"doSubmit()\"\n color=\"tertiary\"\n >\n <ion-label translate>COMMON.BTN_SEND</ion-label>\n </ion-button>\n </ion-col>\n </ion-row>\n </ion-toolbar>\n</ion-footer>\n", dependencies: [{ kind: "directive", type: i3$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i2$1.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i2$1.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$1.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i2$1.IonContent, selector: "ion-content", inputs: ["color", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$1.IonFooter, selector: "ion-footer", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$1.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$1.IonItem, selector: "ion-item", inputs: ["button", "color", "counter", "counterFormatter", "detail", "detailIcon", "disabled", "download", "fill", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "shape", "target", "type"] }, { kind: "component", type: i2$1.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$1.IonRow, selector: "ion-row" }, { kind: "component", type: i2$1.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2$1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "directive", type: i1$1.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { kind: "component", type: MessageForm, selector: "app-message-form", inputs: ["mobile", "suggestFn", "subjectMinLength", "subjectMaxLength", "bodyMaxLength", "bodyAutoHeight", "canSelectType", "canRecipientFilter", "recipientFilterCount"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
34318
+ }
34319
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: MessageModal, decorators: [{
34320
+ type: Component,
34321
+ args: [{ selector: 'app-message-modal', changeDetection: ChangeDetectionStrategy.OnPush, template: "<ion-header>\n <ion-toolbar color=\"secondary\">\n <ion-buttons slot=\"start\">\n <ion-button class=\"back-button\" (click)=\"cancel()\" visible-xs visible-sm visible-mobile>\n <ion-icon slot=\"icon-only\" name=\"arrow-back\"></ion-icon>\n </ion-button>\n </ion-buttons>\n\n <ion-title>\n {{ title | translate }}\n </ion-title>\n\n <ion-buttons slot=\"end\">\n <ion-button\n class=\"back-button\"\n (click)=\"doSubmit()\"\n [disabled]=\"!form.valid\"\n visible-xs\n visible-sm\n visible-mobile\n >\n <ion-icon slot=\"icon-only\" name=\"checkmark\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n\n<ion-content class=\"ion-padding\">\n <ion-item *ngIf=\"form.error\" visible-xs visible-sm visible-mobile lines=\"none\">\n <ion-icon color=\"danger\" slot=\"start\" name=\"alert-circle\"></ion-icon>\n <ion-label color=\"danger\" class=\"error\" [innerHTML]=\"form.error | translate\"></ion-label>\n </ion-item>\n\n <app-message-form\n #form\n (onSubmit)=\"doSubmit()\"\n (onCancel)=\"cancel()\"\n [canSelectType]=\"canSelectType\"\n [canRecipientFilter]=\"canRecipientFilter\"\n [recipientFilterCount]=\"recipientFilterCount\"\n [suggestFn]=\"suggestFn\"\n [debug]=\"debug\">\n ></app-message-form>\n</ion-content>\n\n<ion-footer hidden-xs hidden-sm hidden-mobile>\n <ion-toolbar>\n <ion-row class=\"ion-no-padding\" nowrap>\n <ion-col></ion-col>\n\n <!-- buttons -->\n <ion-col size=\"auto\">\n <ion-button fill=\"clear\" color=\"dark\" (click)=\"cancel()\">\n <ion-label translate>COMMON.BTN_CANCEL</ion-label>\n </ion-button>\n\n <ion-button\n [fill]=\"form.invalid ? 'clear' : 'solid'\"\n [disabled]=\"form.loading || form.invalid\"\n (keyup.enter)=\"doSubmit()\"\n (click)=\"doSubmit()\"\n color=\"tertiary\"\n >\n <ion-label translate>COMMON.BTN_SEND</ion-label>\n </ion-button>\n </ion-col>\n </ion-row>\n </ion-toolbar>\n</ion-footer>\n" }]
34322
+ }], ctorParameters: () => [{ type: LocalSettingsService }, { type: i2$1.ModalController }, { type: AccountService }, { type: i0.ChangeDetectorRef }], propDecorators: { debug: [{
34323
+ type: Input
34324
+ }], title: [{
34325
+ type: Input
34326
+ }], suggestFn: [{
34327
+ type: Input
34328
+ }], data: [{
34329
+ type: Input
34330
+ }], canSelectType: [{
34331
+ type: Input
34332
+ }], canRecipientFilter: [{
34333
+ type: Input
34334
+ }], recipientFilterCount: [{
34335
+ type: Input
34336
+ }], form: [{
34337
+ type: ViewChild,
34338
+ args: ['form', { static: true }]
34339
+ }] } });
34340
+
34341
+ const MessageFragments = {
34342
+ message: gql$1 `
34343
+ fragment MessageFragment on MessageVO {
34344
+ id
34345
+ subject
34346
+ body
34347
+ type
34348
+ recipientFilter {
34349
+ ...PersonFilterFragment
34350
+ }
34351
+ issuer {
34352
+ ...PersonFragment
34353
+ }
34354
+ }
34355
+ `,
34356
+ };
34357
+ const Queries = {
34358
+ load: gql$1 `
34359
+ mutation LoadMessage($id: Int!) {
34360
+ data: message(id: $id) {
34361
+ ...MessageFragment
34362
+ }
34363
+ }
34364
+ ${MessageFragments.message}
34365
+ ${PersonFragments.person}
34366
+ ${PersonFragments.personFilter}
34367
+ `,
34368
+ };
34369
+ const Mutations = {
34370
+ send: gql$1 `
34371
+ mutation SendMessage($data: MessageVOInput) {
34372
+ done: sendMessage(message: $data)
34373
+ }
34374
+ `,
34375
+ delete: gql$1 `
34376
+ mutation DeleteMessage($id: Int!) {
34377
+ done: deleteMessage(id: $id)
34378
+ }
34379
+ `,
34380
+ };
34381
+ class MessageService extends BaseGraphqlService {
34382
+ graphql;
34383
+ translate;
34384
+ modalCtrl;
34385
+ toastController;
34386
+ environment;
34387
+ constructor(graphql, translate, modalCtrl, toastController, environment) {
34388
+ super(graphql, environment);
34389
+ this.graphql = graphql;
34390
+ this.translate = translate;
34391
+ this.modalCtrl = modalCtrl;
34392
+ this.toastController = toastController;
34393
+ this.environment = environment;
34394
+ // For DEV only
34395
+ this._debug = !environment.production;
34396
+ }
34397
+ /**
34398
+ * Send a message to recipient(s)
34399
+ *
34400
+ * @param entity
34401
+ * @param opts
34402
+ */
34403
+ async send(entity, opts) {
34404
+ // Transform into json
34405
+ const data = entity.asObject(MINIFY_ENTITY_FOR_POD);
34406
+ const now = Date.now();
34407
+ if (this._debug)
34408
+ console.debug(`[message-service] Sending message...`);
34409
+ try {
34410
+ const { done } = await this.graphql.mutate({
34411
+ mutation: Mutations.send,
34412
+ variables: { data },
34413
+ error: { code: SocialErrorCodes.SEND_MESSAGE_ERROR, message: 'SOCIAL.ERROR.SEND_MESSAGE_ERROR' },
34414
+ });
34415
+ if (this._debug)
34416
+ console.debug(`[message-service] Send message [OK] in ${Date.now() - now}ms`);
34417
+ if (done && (!opts || opts.showToast !== false)) {
34418
+ await this.showToast({ type: 'info', message: 'SOCIAL.INFO.MESSAGE_SENT' });
34419
+ }
34420
+ return done;
34421
+ }
34422
+ catch (err) {
34423
+ const error = (err && err.message) || err;
34424
+ console.error(error);
34425
+ // Show error
34426
+ if (!opts || opts.showToast !== false) {
34427
+ const message = error || 'SOCIAL.ERROR.SEND_MESSAGE_ERROR';
34428
+ await this.showToast({ type: 'error', message });
34429
+ }
34430
+ return false;
34431
+ }
34432
+ }
34433
+ /**
34434
+ * Delete a message by ID
34435
+ *
34436
+ * @param id
34437
+ * @param opts
34438
+ */
34439
+ async delete(id, opts) {
34440
+ const now = Date.now();
34441
+ if (this._debug)
34442
+ console.debug(`[message-service] Deleting message #${id}...`);
34443
+ try {
34444
+ const { done } = await this.graphql.mutate({
34445
+ mutation: Mutations.delete,
34446
+ variables: { id },
34447
+ error: { code: SocialErrorCodes.DELETE_MESSAGE_ERROR, message: 'SOCIAL.ERROR.DELETE_MESSAGE_ERROR' },
34448
+ });
34449
+ if (this._debug)
34450
+ console.debug(`[message-service] Delete message [OK] in ${Date.now() - now}ms`);
34451
+ if (done && (!opts || opts.showToast !== false)) {
34452
+ await this.showToast({ type: 'info', message: 'SOCIAL.INFO.MESSAGE_DELETED' });
34453
+ }
34454
+ return done;
34455
+ }
34456
+ catch (err) {
34457
+ const error = (err && err.message) || err;
34458
+ console.error(error);
34459
+ // Show error
34460
+ if (!opts || opts.showToast !== false) {
34461
+ const message = error || 'SOCIAL.ERROR.DELETE_MESSAGE_ERROR';
34462
+ await this.showToast({ type: 'error', message });
34463
+ }
34464
+ return false;
34465
+ }
34466
+ }
34467
+ async openComposeModal(options) {
34468
+ const hasTopModal = !!(await this.modalCtrl.getTop());
34469
+ const modal = await this.modalCtrl.create({
34470
+ component: MessageModal,
34471
+ componentProps: options,
34472
+ cssClass: hasTopModal && 'stack-modal',
34473
+ });
34474
+ // Open the modal
34475
+ await modal.present();
34476
+ // On dismiss
34477
+ const { data } = await modal.onDidDismiss();
34478
+ if (!data || !(data instanceof Message))
34479
+ return true; // CANCELLED
34480
+ // Send message
34481
+ return await this.send(data, { showToast: options?.showToast });
34482
+ }
34483
+ async load(id, opts) {
34484
+ const { data } = await this.graphql.query({
34485
+ query: Queries.load,
34486
+ variables: {
34487
+ id,
34488
+ },
34489
+ fetchPolicy: 'no-cache',
34490
+ });
34491
+ const entity = opts?.toEntity !== false ? Message.fromObject(data) : data;
34492
+ console.debug(`[message-service] Loaded message #${id}:`, entity);
34493
+ return entity;
34494
+ }
34495
+ /* -- protected methods -- */
34496
+ async showToast(opts) {
34497
+ return Toasts.show(this.toastController, this.translate, {
34498
+ type: 'info',
34499
+ message: 'SOCIAL.INFO.MESSAGE_SENT',
34500
+ ...opts,
34501
+ });
34502
+ }
34503
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: MessageService, deps: [{ token: GraphqlService }, { token: i1$1.TranslateService }, { token: i2$1.ModalController }, { token: i2$1.ToastController }, { token: ENVIRONMENT, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
34504
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: MessageService, providedIn: 'root' });
34505
+ }
34506
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: MessageService, decorators: [{
34507
+ type: Injectable,
34508
+ args: [{ providedIn: 'root' }]
34509
+ }], ctorParameters: () => [{ type: GraphqlService }, { type: i1$1.TranslateService }, { type: i2$1.ModalController }, { type: i2$1.ToastController }, { type: Environment, decorators: [{
34510
+ type: Optional
34511
+ }, {
34512
+ type: Inject,
34513
+ args: [ENVIRONMENT]
34514
+ }] }] });
34515
+
34516
+ const APP_FEED_SERVICE = new InjectionToken('FeeService');
34517
+ class FeedService extends StartableService {
34518
+ settings;
34519
+ environment;
34520
+ _logPrefix = '[feed-service] ';
34521
+ _state = new RxState();
34522
+ network = inject(NetworkService);
34523
+ modalCtrl = inject(ModalController);
34524
+ messageService = inject(MessageService);
34525
+ personService = inject(PersonService);
34526
+ locale$ = this._state.select('locale');
34527
+ feedUrls$ = this._state.select('feedUrls');
34528
+ get feedUrls() {
34529
+ return this._state.get('feedUrls');
34530
+ }
34531
+ set feedUrls(urls) {
34532
+ this._state.set('feedUrls', () => urls);
34533
+ }
34534
+ constructor(settings, environment) {
34535
+ super(settings);
34536
+ this.settings = settings;
34537
+ this.environment = environment;
34538
+ this._state.connect('locale', this.settings.locale$);
34539
+ this._state.connect('feedUrls', this.locale$.pipe(filter(isNotNilOrBlank), map((locale) => (locale && environment.feed?.jsonFeed?.[locale]) ?? [])));
34540
+ // DEBUG
34541
+ this._debug = !environment.production;
34542
+ if (this._debug)
34543
+ console.debug(`${this._logPrefix}created`);
34544
+ }
34545
+ async ngOnStart() {
34546
+ await Promise.all([this.settings.ready(), this.network.ready()]);
34547
+ return {
34548
+ locale: this.settings.locale,
34549
+ };
34550
+ }
34551
+ watchAll(opts) {
34552
+ if (!this.started) {
34553
+ return from(this.start()).pipe(switchMap$1(() => this.watchAll(opts)));
34554
+ }
34555
+ if (isNotEmptyArray(opts?.urls)) {
34556
+ const locale$ = opts.locale ? of(opts.locale) : this.locale$;
34557
+ return locale$.pipe(mergeMap((locale) => this.loadAll({ locale, ...opts })));
34558
+ }
34559
+ return this.feedUrls$.pipe(filter(isNotEmptyArray), mergeMap((urls) => this.loadAll({ ...opts, urls })));
34560
+ }
34561
+ getHomeUrl(feed) {
34562
+ const feedUrl = feed?.feed_url ?? firstArrayValue(this.feedUrls);
34563
+ return feedUrl ? UrlUtils.getRootUrl(feedUrl) : undefined;
34564
+ }
34565
+ getTagUrl(feed, tag) {
34566
+ if (!feed || !tag)
34567
+ throw new Error("Missing 'feed' or 'tag' argument");
34568
+ const baseUrl = this.getHomeUrl(feed);
34569
+ return feed.tag_template?.replace('{tag}', tag) ?? baseUrl + '/tag/' + tag;
34570
+ }
34571
+ async load(url, opts) {
34572
+ if (!url)
34573
+ throw new Error('Missing url argument');
34574
+ const { data } = await this.loadAll({ ...opts, urls: [url] });
34575
+ return firstArrayValue(data);
34576
+ }
34577
+ async loadAll(opts) {
34578
+ await this.ready();
34579
+ const urls = opts?.urls ?? this.feedUrls;
34580
+ opts = {
34581
+ maxAgeInMonths: opts?.maxAgeInMonths ?? this.environment.feed?.maxAgeInMonths,
34582
+ maxContentLength: opts?.maxContentLength ?? this.environment.feed?.maxContentLength,
34583
+ locale: this.settings.locale,
34584
+ depth: 0,
34585
+ ...opts,
34586
+ };
34587
+ const feeds = await Promise.all((urls || []).map(async (url) => {
34588
+ try {
34589
+ // Get JSON
34590
+ const json = await this.network.get(url, { nocache: opts?.cache });
34591
+ console.debug(`${this._logPrefix}Loaded JSON from ${url}`, json);
34592
+ // Resolve feed
34593
+ return await this.resolveFeed(json, url, opts);
34594
+ }
34595
+ catch (err) {
34596
+ if (err?.status === 404) {
34597
+ console.error(`${this._logPrefix} Error while fetching feeds at ${url}. 404 (Not found)`);
34598
+ }
34599
+ else {
34600
+ console.error(`${this._logPrefix} Error while fetching feeds at ${url}`, err);
34601
+ }
34602
+ return Promise.resolve([]);
34603
+ }
34604
+ }));
34605
+ const data = feeds.filter(isNotEmptyArray).flat();
34606
+ // Close opened resources
34607
+ if (opts.depth === 0) {
34608
+ this.onAfterLoadAll();
34609
+ }
34610
+ return { data, total: data.length };
34611
+ }
34612
+ async openEditModal(event, feedItem, opts) {
34613
+ const messageId = +feedItem.id;
34614
+ if (isNilOrNaN(messageId))
34615
+ throw new Error(`Cannot load message - invalid id: ${feedItem.id}`);
34616
+ // Fetch the original message
34617
+ const message = await this.messageService.load(messageId);
34618
+ // Count recipients
34619
+ let recipientFilterCount;
34620
+ if (message?.recipientFilter) {
34621
+ recipientFilterCount = await this.personService.countAll(message?.recipientFilter);
34622
+ }
34623
+ const hasTopModal = !!(await this.modalCtrl.getTop());
34624
+ const modal = await this.modalCtrl.create({
34625
+ component: MessageModal,
34626
+ componentProps: {
34627
+ title: 'SOCIAL.FEED.EDIT_TITLE',
34628
+ suggestFn: (value, filter, sortBy, sortDirection, opts) => this.personService.suggest(value, filter, sortBy, sortDirection, opts),
34629
+ canSelectType: false,
34630
+ canRecipientFilter: false,
34631
+ recipientFilterCount,
34632
+ data: message,
34633
+ debug: true,
34634
+ },
34635
+ cssClass: hasTopModal && 'stack-modal',
34636
+ });
34637
+ // Open the modal
34638
+ await modal.present();
34639
+ // On dismiss
34640
+ const { data } = await modal.onDidDismiss();
34641
+ if (!data || !(data instanceof Message))
34642
+ return true; // CANCELLED
34643
+ return await this.messageService.send(data, opts);
34644
+ }
34645
+ async deleteItem(feedItem) {
34646
+ const messageId = +feedItem.id;
34647
+ if (isNilOrNaN(messageId))
34648
+ throw new Error(`Cannot delete message - invalid id: ${feedItem.id}`);
34649
+ return await this.messageService.delete(messageId);
34650
+ }
34651
+ /* -- protected functions -- */
34652
+ async resolveFeed(json, url, opts) {
34653
+ // Check json is JsonFeed compatible
34654
+ if (JsonFeedUtils.isJsonFeed(json)) {
34655
+ // Migrate from old versions
34656
+ let feed = JsonFeedUtils.migrateFeed(json);
34657
+ // Resolve items (e.g. using feed.next_url)
34658
+ feed = await this.resolveFeedItems(feed, { ...opts, depth: opts?.depth ?? 0 });
34659
+ // Truncate html
34660
+ JsonFeedUtils.truncateFeedItemsHtml(feed, opts);
34661
+ return isNotEmptyArray(feed?.items) ? [feed] : [];
34662
+ }
34663
+ // not resolved (unknown format)
34664
+ return [];
34665
+ }
34666
+ async resolveFeedItems(json, opts) {
34667
+ if (!json)
34668
+ return json;
34669
+ // Load items
34670
+ if (isEmptyArray(json.items) && isNotNilOrBlank(json.next_url)) {
34671
+ const depth = opts?.depth ?? 0;
34672
+ if (depth >= 3) {
34673
+ console.warn(`${this._logPrefix}Max depth reached (${depth}) for ${json.feed_url}`);
34674
+ return json;
34675
+ }
34676
+ // Load items
34677
+ const feed = await this.load(json.next_url, { ...opts, depth: depth + 1 });
34678
+ if (isEmptyArray(feed?.items)) {
34679
+ return json;
34680
+ }
34681
+ // Merge with the input json
34682
+ return {
34683
+ ...json,
34684
+ title: json.title ?? feed.title,
34685
+ home_page_url: json.home_page_url ?? feed.home_page_url,
34686
+ authors: isNotEmptyArray(json.authors) ? json.authors : feed.authors,
34687
+ items: feed.items,
34688
+ tag_template: json.tag_template ?? feed.tag_template,
34689
+ };
34690
+ }
34691
+ return json;
34692
+ }
34693
+ onAfterLoadAll() {
34694
+ console.debug(`${this._logPrefix}Feeds loaded`);
34695
+ }
34696
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FeedService, deps: [{ token: LocalSettingsService }, { token: ENVIRONMENT, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
34697
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FeedService });
34698
+ }
34699
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FeedService, decorators: [{
34700
+ type: Injectable
34701
+ }], ctorParameters: () => [{ type: LocalSettingsService }, { type: Environment, decorators: [{
34702
+ type: Optional
34703
+ }, {
34704
+ type: Inject,
34705
+ args: [ENVIRONMENT]
34706
+ }] }] });
34707
+
34708
+ class FeedDirective {
34709
+ element;
34710
+ _subscription = new Subscription();
34711
+ platform = inject(PlatformService);
34712
+ locationStrategy = inject(LocationStrategy);
34713
+ navController = inject(NavController);
34714
+ router = inject(Router);
34715
+ route = inject(ActivatedRoute);
34716
+ _feedUrl;
34717
+ _baseUrl;
34718
+ set feedUrl(value) {
34719
+ this._feedUrl = value;
34720
+ this._baseUrl = value && JsonFeedUtils.removeJsonExtension(value);
34721
+ }
34722
+ get feedUrl() {
34723
+ return this._feedUrl;
34724
+ }
34725
+ constructor(element) {
34726
+ this.element = element;
34727
+ // DEBUG
34728
+ //console.debug('[feed-directive] Creating feed directive');
34729
+ }
34730
+ ngAfterViewInit() {
34731
+ console.debug('[feed-directive] Processing anchors');
34732
+ // Listening click events
34733
+ this._subscription.add(this.listenClickEvents(this.element.nativeElement));
34734
+ }
34735
+ ngOnDestroy() {
34736
+ this._subscription.unsubscribe();
34737
+ }
34738
+ listenClickEvents(nativeElement) {
34739
+ console.debug('[feed-directive] Start listening click events...');
34740
+ const subscription = new Subscription();
34741
+ const listener = (event) => this.click(event, nativeElement);
34742
+ const links = nativeElement.querySelectorAll('a');
34743
+ links.forEach((link) => {
34744
+ // DEBUG
34745
+ //console.debug('[feed-directive] Adding click listener to', link);
34746
+ link.addEventListener('click', listener);
34747
+ subscription.add(() => link.removeEventListener('click', listener));
34748
+ });
34749
+ // DEBUG
34750
+ subscription.add(() => console.debug('[feed-directive] Stop listening click events'));
34751
+ return subscription;
34752
+ }
34753
+ async click(event, containerElement) {
34754
+ console.debug('[feed-directive] Processing click event...', event);
34755
+ if (!(event.target instanceof HTMLAnchorElement) && !(event.target?.['tagName'] === 'A')) {
34756
+ console.warn('[feed-directive] Invalid click event target. Should an anchor <a>', event.target);
34757
+ return;
34758
+ }
34759
+ const element = event.target;
34760
+ let href = element.getAttribute('href');
34761
+ const fragment = UrlUtils.getFragment(href);
34762
+ // Fragment
34763
+ if (href?.startsWith('#') && isNotNilOrBlank(fragment)) {
34764
+ event.preventDefault();
34765
+ this.scrollToAnchor(containerElement, fragment);
34766
+ return true;
34767
+ }
34768
+ const routerLink = element.getAttribute('routerLink');
34769
+ if (isNotNilOrBlank(routerLink)) {
34770
+ event.preventDefault();
34771
+ return this.navController.navigateForward(routerLink);
34772
+ }
34773
+ // Resolve relative URL, if baseUrl has been set
34774
+ if (this._baseUrl && UrlUtils.isRelativeUrl(href)) {
34775
+ // Resolve URL, then open using the platform
34776
+ href = UrlUtils.resolveRelativeUrl(this._baseUrl, href);
34777
+ }
34778
+ // Resolve internal URL as an app route
34779
+ if (UrlUtils.isInternalUrl(href)) {
34780
+ const routePath = this.normalizeUrl(href.startsWith('/') ? href : `/${href}`);
34781
+ const urlTree = this.getUrlTree(routePath);
34782
+ event.preventDefault();
34783
+ this.router.navigated = false;
34784
+ // Opening route
34785
+ return this.navController.navigateForward(urlTree);
34786
+ }
34787
+ // External URL: open using the platform
34788
+ event.preventDefault();
34789
+ await this.platform.open(href);
34790
+ return true;
34791
+ }
34792
+ scrollToAnchor(containerElement, anchorId) {
34793
+ if (!containerElement || !anchorId) {
34794
+ console.warn("Missing 'containerElement' or 'fragment' argument.");
34795
+ return;
34796
+ }
34797
+ console.debug('[feed-directive-anchor] Scrolling to anchor #' + anchorId);
34798
+ // Find the target element with the specified fragment ID
34799
+ const targetElement = containerElement.querySelector(`#${anchorId}`);
34800
+ if (targetElement) {
34801
+ // Scroll smoothly to the target element
34802
+ // eslint-disable-next-line @rx-angular/prefer-no-layout-sensitive-apis
34803
+ targetElement.scrollIntoView({
34804
+ behavior: 'smooth', // Smooth scrolling behavior
34805
+ block: 'start', // Align the target element to the top of the container
34806
+ });
34807
+ }
34808
+ else {
34809
+ console.warn(`No element found with the ID "${anchorId}" inside the container.`);
34810
+ }
34811
+ }
34812
+ /**
34813
+ * Transform a relative URL to its absolute representation according to current router state.
34814
+ * @param url Relative URL path.
34815
+ * @return Absolute URL based on the current route.
34816
+ */
34817
+ normalizeUrl(url) {
34818
+ if (UrlUtils.isExternalUrl(url)) {
34819
+ return url;
34820
+ }
34821
+ if (this._baseUrl && UrlUtils.isRelativeUrl(url)) {
34822
+ return UrlUtils.resolveRelativeUrl(this._baseUrl, url);
34823
+ }
34824
+ const urlTree = this.getUrlTree(url);
34825
+ const serializedUrl = this.router.serializeUrl(urlTree);
34826
+ return this.locationStrategy.prepareExternalUrl(serializedUrl);
34827
+ }
34828
+ getUrlTree(url) {
34829
+ url = UrlUtils.normalizeUrl(url);
34830
+ const urlPath = UrlUtils.stripFragmentAndQuery(url) || UrlUtils.stripFragmentAndQuery(this.router.url);
34831
+ const parsedUrl = this.router.parseUrl(url);
34832
+ const fragment = parsedUrl.fragment;
34833
+ const queryParams = parsedUrl.queryParams;
34834
+ return this.router.createUrlTree([urlPath], { relativeTo: this.route, fragment, queryParams });
34835
+ }
34836
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FeedDirective, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
34837
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.13", type: FeedDirective, selector: "feed,[feed]", inputs: { feedUrl: ["feed", "feedUrl"] }, ngImport: i0 });
34838
+ }
34839
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FeedDirective, decorators: [{
34840
+ type: Directive,
34841
+ args: [{
34842
+ selector: 'feed,[feed]',
34843
+ }]
34844
+ }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { feedUrl: [{
34845
+ type: Input,
34846
+ args: [{ alias: 'feed', required: true }]
34847
+ }] } });
34848
+
34072
34849
  class FeedsComponent {
34073
34850
  environment;
34074
34851
  feedService;
34852
+ accountService;
34853
+ alertCtrl;
34075
34854
  _state = new RxState();
34076
34855
  translate = inject(TranslateService);
34077
34856
  platform = inject(PlatformService);
@@ -34079,9 +34858,11 @@ class FeedsComponent {
34079
34858
  router = inject(Router);
34080
34859
  version;
34081
34860
  modalItemId;
34861
+ onRefresh = new EventEmitter();
34082
34862
  feeds$ = this._state.select('feeds');
34083
- feedUrls$ = this._state.select('feedUrls');
34863
+ urls$ = this._state.select('urls');
34084
34864
  hasFeeds$ = this._state.select('hasFeeds');
34865
+ userId$ = this._state.select('userId');
34085
34866
  debug = false;
34086
34867
  mobile;
34087
34868
  showHeader = true;
@@ -34092,17 +34873,19 @@ class FeedsComponent {
34092
34873
  class = '';
34093
34874
  itemId;
34094
34875
  filterItem;
34876
+ editItem = new EventEmitter();
34877
+ deleteItem = new EventEmitter();
34095
34878
  set feeds(value) {
34096
34879
  this._state.set('feeds', () => value);
34097
34880
  }
34098
34881
  get feeds() {
34099
34882
  return this._state.get('feeds');
34100
34883
  }
34101
- set feedUrls(value) {
34102
- this._state.set('feedUrls', () => value);
34884
+ set urls(value) {
34885
+ this._state.set('urls', () => value);
34103
34886
  }
34104
- get feedUrls() {
34105
- return this._state.get('feedUrls');
34887
+ get urls() {
34888
+ return this._state.get('urls');
34106
34889
  }
34107
34890
  get hasFeeds() {
34108
34891
  return this._state.get('hasFeeds');
@@ -34119,6 +34902,9 @@ class FeedsComponent {
34119
34902
  get maxContentLength() {
34120
34903
  return this._state.get('maxContentLength');
34121
34904
  }
34905
+ get peerUrl() {
34906
+ return this._state.get('peerUrl');
34907
+ }
34122
34908
  get hostClass() {
34123
34909
  const classes = [this.class];
34124
34910
  if (this.shape) {
@@ -34127,16 +34913,25 @@ class FeedsComponent {
34127
34913
  return classes.filter((cls) => cls).join(' ');
34128
34914
  }
34129
34915
  modal;
34130
- constructor(settings, environment, feedService) {
34916
+ constructor(settings, environment, feedService, accountService, alertCtrl) {
34131
34917
  this.environment = environment;
34132
34918
  this.feedService = feedService;
34919
+ this.accountService = accountService;
34920
+ this.alertCtrl = alertCtrl;
34133
34921
  this.mobile = settings.mobile;
34922
+ this._state.connect('locale', settings.locale$);
34134
34923
  this._state.connect('hasFeeds', this.feeds$.pipe(map(isNotEmptyArray)));
34924
+ this._state.connect('userId', this.accountService.person$.pipe(map((person) => person?.id)));
34925
+ this._state.connect('peerUrl', this.networkService.peer$.pipe(map((peer) => peer?.url)));
34135
34926
  if (this.feedService) {
34136
- this._state.connect('feeds', this._state.select(['feedUrls', 'maxAgeInMonths', 'maxContentLength']).pipe(switchMap(({ feedUrls, maxAgeInMonths, maxContentLength }) => this.feedService.watchAll({
34137
- urls: feedUrls,
34927
+ this._state.connect('feeds', merge(this.onRefresh.pipe(map(() => ({ ...this._state.get(), cache: false }))), this._state
34928
+ .select(['urls', 'maxAgeInMonths', 'maxContentLength', 'locale'])
34929
+ .pipe(map(({ urls, maxAgeInMonths, maxContentLength, locale }) => ({ urls, maxAgeInMonths, maxContentLength, locale, cache: true })))).pipe(switchMap(({ urls, maxAgeInMonths, maxContentLength, locale, cache }) => this.feedService.watchAll({
34930
+ urls: urls,
34138
34931
  maxAgeInMonths,
34139
34932
  maxContentLength,
34933
+ locale,
34934
+ cache,
34140
34935
  })), map(({ data }) => data)
34141
34936
  // DEBUG
34142
34937
  //tap((feeds) => console.debug('[feed-component] feeds', feeds))
@@ -34144,7 +34939,7 @@ class FeedsComponent {
34144
34939
  }
34145
34940
  }
34146
34941
  ngOnInit() {
34147
- this.feedUrls = this.feedUrls ?? null; // Should set the feedUrls, in order to trigger the select in constructor
34942
+ this.urls = this.urls ?? null; // Should set the urls, in order to trigger the select in constructor
34148
34943
  this.maxAgeInMonths = this.maxAgeInMonths ?? this.environment.feed?.maxAgeInMonths ?? -1;
34149
34944
  this.maxContentLength = this.maxContentLength ?? this.environment.feed?.maxContentLength ?? -1;
34150
34945
  const self = this;
@@ -34220,12 +35015,73 @@ class FeedsComponent {
34220
35015
  getFeedHomeUrl(feed) {
34221
35016
  return feed.home_page_url ?? JsonFeedUtils.removeJsonExtension(feed.feed_url) ?? this.feedService.getHomeUrl();
34222
35017
  }
34223
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FeedsComponent, deps: [{ token: LocalSettingsService }, { token: ENVIRONMENT }, { token: APP_FEED_SERVICE, optional: true }], target: i0.ɵɵFactoryTarget.Component });
34224
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: FeedsComponent, selector: "app-feed", inputs: { debug: "debug", mobile: "mobile", showHeader: "showHeader", showReadMoreButton: "showReadMoreButton", headerColor: "headerColor", cardColor: "cardColor", shape: "shape", class: "class", itemId: "itemId", filterItem: "filterItem", feeds: "feeds", feedUrls: "feedUrls", maxAgeInMonths: "maxAgeInMonths", maxContentLength: "maxContentLength" }, host: { properties: { "class": "this.hostClass" } }, providers: [RxState], viewQueries: [{ propertyName: "modal", first: true, predicate: ["modal"], descendants: true }], ngImport: i0, template: "<!-- debug -->\n@if (debug) {\n <app-debug [title]=\"'Feed'\">\n <p>\n hasFeeds?: {{ hasFeeds$ | async }}\n <br />\n feedUrls: {{ feedUrls$ | async | json }}\n <br />\n shape: {{ shape }}\n </p>\n </app-debug>\n}\n\n@let feeds = feeds$ | async;\n@if (feeds | isNotEmptyArray) {\n <!-- top header -->\n @if (showHeader && (feeds | arrayFirst); as feed) {\n <ion-item lines=\"none\" [color]=\"headerColor\" class=\"feed-header shape-{{ shape }}\">\n <ion-icon slot=\"start\" name=\"megaphone\"></ion-icon>\n <ion-label>\n <b>{{ feed.title || ('SOCIAL.FEED.NEWS' | translate) }}</b>\n </ion-label>\n <ion-button slot=\"end\" fill=\"clear\" (click)=\"openFeedHome()\" shape=\"\">\n <ion-label translate>SOCIAL.FEED.SHOW_ALL_FEED</ion-label>\n <ion-icon slot=\"end\" name=\"chevron-forward-outline\"></ion-icon>\n </ion-button>\n </ion-item>\n }\n\n <div class=\"feed-content shape-{{ shape }} ion-no-padding\" [class.has-header]=\"showHeader\">\n <!-- feeds -->\n @for (feed of feeds; track feed.feed_url; let firstFeed = $first; let lastFeed = $last) {\n <!-- items -->\n @for (item of feed.items | arrayFilter: filterItem; track item.id; let firstItem = $first; let lastItem = $last) {\n <ion-card\n [class.first]=\"firstFeed && firstItem\"\n [class.last]=\"lastFeed && lastItem\"\n [color]=\"cardColor !== 'light-transparent' ? cardColor : undefined\"\n >\n <ion-card-header>\n <ion-card-subtitle style=\"vertical-align: middle\">\n <!-- Authors -->\n @for (author of item.authors || feed.authors; track author) {\n @if (author.name || author.avatar) {\n <ion-chip (click)=\"openUrl(author.url)\" tappable>\n @if (author.avatar) {\n <ion-avatar>\n <ion-img [src]=\"author.avatar\" [alt]=\"author.name\"></ion-img>\n </ion-avatar>\n }\n @if (author.name) {\n <ion-label class=\"author\">{{ author.name }}</ion-label>\n }\n </ion-chip>\n }\n }\n <ion-note class=\"ion-float-end\">\n <small>{{ item.date_published | dateFromNow }}</small>\n </ion-note>\n </ion-card-subtitle>\n\n <!-- title -->\n <ion-card-title (click)=\"openFeedItem(item, feed)\" tappable>{{ item?.title }}</ion-card-title>\n\n <!-- tags -->\n @let tags = item | map: getTags;\n @if (tags | isNotEmptyArray) {\n <ion-text class=\"tags\">\n @for (tag of tags; track tag; let last = $last) {\n <a (click)=\"openTag(feed, tag)\" tappable>\n <ion-text>#{{ tag }}</ion-text>\n </a>\n @if (!last) {\n &nbsp;\n }\n }\n </ion-text>\n }\n </ion-card-header>\n\n <!-- Feed content -->\n <ion-card-content>\n <ion-text [feed]=\"item.url || feed.feed_url\">\n @if (item.content_html) {\n <p [innerHTML]=\"item.content_html\"></p>\n } @else if (item.content_text) {\n <p>\n <markdown [data]=\"item.content_text\" emoji></markdown>\n </p>\n }\n </ion-text>\n </ion-card-content>\n\n @if (showReadMoreButton) {\n <ion-button (click)=\"openFeedItem(item, feed)\" class=\"ion-float-end\" fill=\"clear\">\n <ion-label>{{ (item.truncated ? 'SOCIAL.FEED.READ_MORE' : 'COMMON.BTN_SHOW') | translate }}</ion-label>\n <ion-icon slot=\"end\" name=\"chevron-forward-outline\"></ion-icon>\n </ion-button>\n }\n </ion-card>\n }\n }\n </div>\n}\n\n<ion-modal #modal [showBackdrop]=\"false\"\n class=\"stack-modal\" [class.modal-large]=\"!mobile\">\n <ng-template>\n <ion-header>\n <ion-toolbar color=\"secondary\">\n <ion-title>{{ 'SOCIAL.FEED.NEWS' | translate }}</ion-title>\n <ion-buttons slot=\"end\">\n <ion-button (click)=\"modal.dismiss()\">\n <ion-icon slot=\"icon-only\" name=\"close\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content>\n <div class=\"ion-padding\">\n <app-feed\n [feedUrls]=\"feedUrls\"\n [showHeader]=\"false\"\n [showReadMoreButton]=\"false\"\n [maxContentLength]=\"-1\"\n [maxAgeInMonths]=\"-1\"\n [itemId]=\"modalItemId\"\n cardColor=\"light\"\n [debug]=\"debug\"\n ></app-feed>\n </div>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [":host{display:block;height:calc(100% - 10px);max-height:fit-content;overflow:hidden;--feed-header-height: 48px;-webkit-box-shadow:0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12);box-shadow:0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px rgba(0,0,0,.14),0 1px 5px rgba(0,0,0,.12);-webkit-margin-start:10px;margin-inline-start:10px;-webkit-margin-end:10px;margin-inline-end:10px;margin-top:0;margin-bottom:10px;--feed-border-radius: 4px;border-radius:var(--feed-border-radius);--ion-card-background: rgba(var(--ion-background-color-rgb), .6)}:host.shape-round{--feed-border-radius: 12px}ion-button{text-transform:unset;--color: rgba(var(--ion-color-contrast-rgb), .7)}ion-item.feed-header{height:var(--feed-header-height);margin:0;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);border-radius:var(--feed-border-radius) var(--feed-border-radius) 0 0}ion-item.feed-header ion-icon[slot=start]{-webkit-margin-end:8px;margin-inline-end:8px}.feed-content{height:auto;overflow-y:auto;--margin-bottom: 8px}.feed-content.has-header{height:calc(100% - var(--feed-header-height, 0))}.feed-content ion-card{--top-radius: 4px;--bottom-radius: 4px;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);margin:0 0 var(--margin-bottom) 0;border-radius:var(--top-radius) var(--top-radius) var(--bottom-radius) var(--bottom-radius);--ion-card-color-contrast-rgb: var(--ion-color-contrast-rgb, var(--ion-color-dark-rgb, 0, 0, 0));--color: rgba(var(--ion-card-color-contrast-rgb), .87)}.feed-content ion-card.first{--top-radius: 0}.feed-content ion-card.last{--margin-bottom: 0;--bottom-radius: 0}.feed-content ion-card ion-card-header ion-card-title{--color: rgba(var(--ion-card-color-contrast-rgb), .87)}.feed-content ion-card ion-card-header ion-card-subtitle ion-chip{--background: transparent;--border-color: transparent;--border-width: 0}.feed-content ion-card ion-card-header ion-card-subtitle ion-chip ion-avatar{--color: rgba(var(--ion-card-color-contrast-rgb), .6);border:1px solid var(--color)}.feed-content ion-card ion-card-header ion-card-subtitle ion-chip ion-label{--color: rgba(var(--ion-card-color-contrast-rgb), .8);color:var(--color)}.feed-content ion-card ion-card-header ion-card-subtitle ion-note{--color: rgba(var(--ion-card-color-contrast-rgb), .6);color:var(--color)}.feed-content ion-card ion-card-header .tags{display:inline}.feed-content ion-card ion-card-header .tags a{color:rgba(var(--ion-card-color-contrast-rgb),.6)!important}.feed-content ion-card ion-card-content ion-text ::ng-deep img{max-width:100%;height:auto!important}\n"], dependencies: [{ kind: "component", type: i2$1.IonAvatar, selector: "ion-avatar" }, { kind: "component", type: i2$1.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i2$1.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$1.IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$1.IonCardContent, selector: "ion-card-content", inputs: ["mode"] }, { kind: "component", type: i2$1.IonCardHeader, selector: "ion-card-header", inputs: ["color", "mode", "translucent"] }, { kind: "component", type: i2$1.IonCardSubtitle, selector: "ion-card-subtitle", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonCardTitle, selector: "ion-card-title", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonChip, selector: "ion-chip", inputs: ["color", "disabled", "mode", "outline"] }, { kind: "component", type: i2$1.IonContent, selector: "ion-content", inputs: ["color", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$1.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$1.IonImg, selector: "ion-img", inputs: ["alt", "src"] }, { kind: "component", type: i2$1.IonItem, selector: "ion-item", inputs: ["button", "color", "counter", "counterFormatter", "detail", "detailIcon", "disabled", "download", "fill", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "shape", "target", "type"] }, { kind: "component", type: i2$1.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$1.IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2$1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonModal, selector: "ion-modal" }, { kind: "directive", type: i1$1.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { kind: "component", type: i4$2.MarkdownComponent, selector: "markdown, [markdown]", inputs: ["data", "src", "disableSanitizer", "inline", "clipboard", "clipboardButtonComponent", "clipboardButtonTemplate", "emoji", "katex", "katexOptions", "mermaid", "mermaidOptions", "lineHighlight", "line", "lineOffset", "lineNumbers", "start", "commandLine", "filterOutput", "host", "prompt", "output", "user"], outputs: ["error", "load", "ready"] }, { kind: "component", type: DebugComponent, selector: "app-debug", inputs: ["titlePrefix", "title", "enable", "expanded"] }, { kind: "directive", type: MarkdownDirective, selector: "markdown,[markdown]" }, { kind: "component", type: FeedsComponent, selector: "app-feed", inputs: ["debug", "mobile", "showHeader", "showReadMoreButton", "headerColor", "cardColor", "shape", "class", "itemId", "filterItem", "feeds", "feedUrls", "maxAgeInMonths", "maxContentLength"] }, { kind: "directive", type: FeedDirective, selector: "feed,[feed]", inputs: ["feed"] }, { kind: "pipe", type: i3$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i3$1.JsonPipe, name: "json" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }, { kind: "pipe", type: DateFromNowPipe, name: "dateFromNow" }, { kind: "pipe", type: NotEmptyArrayPipe, name: "isNotEmptyArray" }, { kind: "pipe", type: ArrayFirstPipe, name: "arrayFirst" }, { kind: "pipe", type: ArrayFilterPipe, name: "arrayFilter" }, { kind: "pipe", type: MapPipe, name: "map" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
35018
+ /**
35019
+ * Check if the current user is the author of the feed item
35020
+ */
35021
+ canEditItem = (item, userId, feed) => {
35022
+ if (!item || isNil(userId))
35023
+ return false; // No item, or user not connected
35024
+ // DEBUG
35025
+ //console.debug('[feed-component] canEditItem...');
35026
+ // Check feed URL point on the current peer (/api/feed/ ...)
35027
+ if (!item.url?.startsWith(this.peerUrl))
35028
+ return false;
35029
+ // Check item authors first
35030
+ let isAuthor = false;
35031
+ if (isNotEmptyArray(item.authors)) {
35032
+ isAuthor = item.authors.some((author) => +author.id === userId);
35033
+ }
35034
+ // Check feed authors
35035
+ if (!isAuthor && isNotEmptyArray(feed?.authors)) {
35036
+ isAuthor = feed.authors.some((author) => +author.id === userId);
35037
+ }
35038
+ return isAuthor || this.accountService.isAdmin();
35039
+ };
35040
+ /**
35041
+ * Handle edit item action
35042
+ */
35043
+ async onEditItem(event, item) {
35044
+ if (event) {
35045
+ event.preventDefault();
35046
+ event.stopPropagation();
35047
+ }
35048
+ if (this.editItem.observed) {
35049
+ this.editItem.emit(item);
35050
+ }
35051
+ const done = await this.feedService?.openEditModal(event, item);
35052
+ if (done) {
35053
+ // force a refresh of the feeds
35054
+ this.onRefresh.emit();
35055
+ }
35056
+ }
35057
+ /**
35058
+ * Handle delete item action
35059
+ */
35060
+ async onDeleteItem(event, item) {
35061
+ if (event) {
35062
+ event.preventDefault();
35063
+ event.stopPropagation();
35064
+ }
35065
+ // Ask for confirmation before deletion
35066
+ const confirmed = await Alerts.askActionConfirmation(this.alertCtrl, this.translate, true, event);
35067
+ if (confirmed !== true) {
35068
+ return; // User cancelled or dismissed
35069
+ }
35070
+ if (this.deleteItem.observed) {
35071
+ this.deleteItem.emit(item);
35072
+ }
35073
+ const done = await this.feedService?.deleteItem(item);
35074
+ if (done) {
35075
+ // force a refresh of the feeds
35076
+ this.onRefresh.emit();
35077
+ }
35078
+ }
35079
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FeedsComponent, deps: [{ token: LocalSettingsService }, { token: ENVIRONMENT }, { token: APP_FEED_SERVICE, optional: true }, { token: AccountService }, { token: i2$1.AlertController }], target: i0.ɵɵFactoryTarget.Component });
35080
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: FeedsComponent, selector: "app-feed", inputs: { debug: "debug", mobile: "mobile", showHeader: "showHeader", showReadMoreButton: "showReadMoreButton", headerColor: "headerColor", cardColor: "cardColor", shape: "shape", class: "class", itemId: "itemId", filterItem: "filterItem", feeds: "feeds", urls: "urls", maxAgeInMonths: "maxAgeInMonths", maxContentLength: "maxContentLength" }, outputs: { editItem: "editItem", deleteItem: "deleteItem" }, host: { properties: { "class": "this.hostClass" } }, providers: [RxState], viewQueries: [{ propertyName: "modal", first: true, predicate: ["modal"], descendants: true }], ngImport: i0, template: "<!-- debug -->\n@if (debug) {\n <app-debug [title]=\"'Feed'\">\n <p>\n hasFeeds?: {{ hasFeeds$ | async }}\n <br />\n urls: {{ urls$ | async | json }}\n <br />\n shape: {{ shape }}\n </p>\n </app-debug>\n}\n\n@let feeds = feeds$ | async;\n@let userId = userId$ | async;\n\n@if (feeds | isNotEmptyArray) {\n <!-- top header -->\n @if (showHeader && (feeds | arrayFirst); as feed) {\n <ion-item lines=\"none\" [color]=\"headerColor\" class=\"feed-header shape-{{ shape }}\">\n <ion-icon slot=\"start\" name=\"megaphone\"></ion-icon>\n <ion-label>\n <b>{{ feed.title || ('SOCIAL.FEED.NEWS' | translate) }}</b>\n </ion-label>\n <ion-button slot=\"end\" fill=\"clear\" (click)=\"openFeedHome()\" shape=\"\">\n <ion-label translate>SOCIAL.FEED.SHOW_ALL_FEED</ion-label>\n <ion-icon slot=\"end\" name=\"chevron-forward-outline\"></ion-icon>\n </ion-button>\n </ion-item>\n }\n\n <div class=\"feed-content shape-{{ shape }} ion-no-padding\" [class.has-header]=\"showHeader\">\n <!-- feeds -->\n @for (feed of feeds; track feed.feed_url; let firstFeed = $first; let lastFeed = $last) {\n <!-- items -->\n @for (item of feed.items | arrayFilter: filterItem; track item.id; let firstItem = $first; let lastItem = $last) {\n <ion-card\n [class.first]=\"firstFeed && firstItem\"\n [class.last]=\"lastFeed && lastItem\"\n [color]=\"cardColor !== 'light-transparent' ? cardColor : undefined\"\n class=\"feed-item-card\"\n >\n <ion-card-header>\n <ion-card-subtitle style=\"vertical-align: middle\">\n <!-- Authors -->\n @for (author of item.authors || feed.authors; track author) {\n @if (author.name || author.avatar) {\n <ion-chip (click)=\"openUrl(author.url)\" tappable>\n @if (author.avatar) {\n <ion-avatar>\n <ion-img [src]=\"author.avatar\" [alt]=\"author.name\"></ion-img>\n </ion-avatar>\n }\n @if (author.name) {\n <ion-label class=\"author\">{{ author.name }}</ion-label>\n }\n </ion-chip>\n }\n }\n <ion-note class=\"ion-float-end\">\n <small>{{ item.date_published | dateFromNow }}</small>\n </ion-note>\n </ion-card-subtitle>\n\n <!-- title -->\n <ion-card-title (click)=\"openFeedItem(item, feed)\" tappable>{{ item?.title }}</ion-card-title>\n\n <!-- tags -->\n @let tags = item | map: getTags;\n @if (tags | isNotEmptyArray) {\n <ion-text class=\"tags\">\n @for (tag of tags; track tag; let last = $last) {\n <a (click)=\"openTag(feed, tag)\" tappable>\n <ion-text>#{{ tag }}</ion-text>\n </a>\n @if (!last) {\n &nbsp;\n }\n }\n </ion-text>\n }\n </ion-card-header>\n\n <!-- Feed content -->\n <ion-card-content>\n <ion-text [feed]=\"item.url || feed.feed_url\">\n @if (item.content_html) {\n <p [innerHTML]=\"item.content_html\"></p>\n } @else if (item.content_text) {\n <p>\n <markdown [data]=\"item.content_text\" emoji></markdown>\n </p>\n }\n </ion-text>\n </ion-card-content>\n\n @let editable = canEditItem(item, userId, feed);\n @if (editable || showReadMoreButton) {\n @if (editable) {\n\n <!-- Delete button (visible hover)-->\n <button\n mat-icon-button\n (click)=\"onDeleteItem($event, item)\"\n class=\"visible-hover ion-float-start\"\n [title]=\"'COMMON.BTN_DELETE' | translate\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n <!-- Edit button (visible hover) -->\n <ion-button\n (click)=\"onEditItem($event, item)\"\n class=\"visible-hover ion-float-start\" fill=\"clear\"\n >\n <mat-icon slot=\"start\">edit</mat-icon>\n<!-- <ion-icon name=\"pencil\" slot=\"start\"></ion-icon>-->\n <ion-label translate>COMMON.BTN_EDIT</ion-label>\n </ion-button>\n }\n @if (showReadMoreButton) {\n <ion-button (click)=\"openFeedItem(item, feed)\" class=\"ion-float-end\" fill=\"clear\">\n <ion-label>{{ (item.truncated ? 'SOCIAL.FEED.READ_MORE' : 'COMMON.BTN_SHOW') | translate }}</ion-label>\n <ion-icon slot=\"end\" name=\"chevron-forward-outline\"></ion-icon>\n </ion-button>\n }\n }\n </ion-card>\n }\n }\n </div>\n}\n\n<!-- Details modal -->\n<ion-modal #modal [showBackdrop]=\"false\"\n class=\"stack-modal\" [class.modal-large]=\"!mobile\">\n <ng-template>\n <ion-header>\n <ion-toolbar color=\"secondary\">\n <ion-title>{{ 'SOCIAL.FEED.NEWS' | translate }}</ion-title>\n <ion-buttons slot=\"end\">\n <ion-button (click)=\"modal.dismiss()\">\n <ion-icon slot=\"icon-only\" name=\"close\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content>\n <div class=\"ion-padding\">\n <app-feed\n [urls]=\"urls\"\n [showHeader]=\"false\"\n [showReadMoreButton]=\"false\"\n [maxContentLength]=\"-1\"\n [maxAgeInMonths]=\"-1\"\n [itemId]=\"modalItemId\"\n cardColor=\"light\"\n [debug]=\"debug\"\n ></app-feed>\n </div>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [":host{display:block;height:calc(100% - 10px);max-height:fit-content;overflow:hidden;--feed-header-height: 48px;-webkit-box-shadow:0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12);box-shadow:0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px rgba(0,0,0,.14),0 1px 5px rgba(0,0,0,.12);-webkit-margin-start:10px;margin-inline-start:10px;-webkit-margin-end:10px;margin-inline-end:10px;margin-top:0;margin-bottom:10px;--feed-border-radius: 4px;border-radius:var(--feed-border-radius);--ion-card-background: rgba(var(--ion-background-color-rgb), .6)}:host.shape-round{--feed-border-radius: 12px}ion-button{text-transform:unset;--color: rgba(var(--ion-color-contrast-rgb), .7)}ion-item.feed-header{height:var(--feed-header-height);margin:0;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);border-radius:var(--feed-border-radius) var(--feed-border-radius) 0 0}ion-item.feed-header ion-icon[slot=start]{-webkit-margin-end:8px;margin-inline-end:8px}.feed-content{height:auto;overflow-y:auto;--margin-bottom: 8px}.feed-content.has-header{height:calc(100% - var(--feed-header-height, 0))}.feed-content ion-card.feed-item-card{--top-radius: 4px;--bottom-radius: 4px;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);margin:0 0 var(--margin-bottom) 0;border-radius:var(--top-radius) var(--top-radius) var(--bottom-radius) var(--bottom-radius);--ion-card-color-contrast-rgb: var(--ion-color-contrast-rgb, var(--ion-color-dark-rgb, 0, 0, 0));--color: rgba(var(--ion-card-color-contrast-rgb), .87)}.feed-content ion-card.feed-item-card.first{--top-radius: 0}.feed-content ion-card.feed-item-card.last{--margin-bottom: 0;--bottom-radius: 0}.feed-content ion-card.feed-item-card ion-card-header ion-card-title{--color: rgba(var(--ion-card-color-contrast-rgb), .87)}.feed-content ion-card.feed-item-card ion-card-header ion-card-subtitle ion-chip{--background: transparent;--border-color: transparent;--border-width: 0}.feed-content ion-card.feed-item-card ion-card-header ion-card-subtitle ion-chip ion-avatar{--color: rgba(var(--ion-card-color-contrast-rgb), .6);border:1px solid var(--color)}.feed-content ion-card.feed-item-card ion-card-header ion-card-subtitle ion-chip ion-label{--color: rgba(var(--ion-card-color-contrast-rgb), .8);color:var(--color)}.feed-content ion-card.feed-item-card ion-card-header ion-card-subtitle ion-note{--color: rgba(var(--ion-card-color-contrast-rgb), .6);color:var(--color)}.feed-content ion-card.feed-item-card ion-card-header .tags{display:inline}.feed-content ion-card.feed-item-card ion-card-header .tags a{color:rgba(var(--ion-card-color-contrast-rgb),.6)!important}.feed-content ion-card.feed-item-card ion-card-content ion-text ::ng-deep img{max-width:100%;height:auto!important}.feed-content ion-card.feed-item-card .visible-hover{opacity:0;transition:opacity .2s ease-in-out}.feed-content ion-card.feed-item-card:hover .visible-hover{opacity:1}\n"], dependencies: [{ kind: "component", type: i2$1.IonAvatar, selector: "ion-avatar" }, { kind: "component", type: i2$1.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i2$1.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$1.IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$1.IonCardContent, selector: "ion-card-content", inputs: ["mode"] }, { kind: "component", type: i2$1.IonCardHeader, selector: "ion-card-header", inputs: ["color", "mode", "translucent"] }, { kind: "component", type: i2$1.IonCardSubtitle, selector: "ion-card-subtitle", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonCardTitle, selector: "ion-card-title", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonChip, selector: "ion-chip", inputs: ["color", "disabled", "mode", "outline"] }, { kind: "component", type: i2$1.IonContent, selector: "ion-content", inputs: ["color", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$1.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$1.IonImg, selector: "ion-img", inputs: ["alt", "src"] }, { kind: "component", type: i2$1.IonItem, selector: "ion-item", inputs: ["button", "color", "counter", "counterFormatter", "detail", "detailIcon", "disabled", "download", "fill", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "shape", "target", "type"] }, { kind: "component", type: i2$1.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$1.IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2$1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonModal, selector: "ion-modal" }, { kind: "directive", type: i1$1.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { kind: "component", type: i4$2.MarkdownComponent, selector: "markdown, [markdown]", inputs: ["data", "src", "disableSanitizer", "inline", "clipboard", "clipboardButtonComponent", "clipboardButtonTemplate", "emoji", "katex", "katexOptions", "mermaid", "mermaidOptions", "lineHighlight", "line", "lineOffset", "lineNumbers", "start", "commandLine", "filterOutput", "host", "prompt", "output", "user"], outputs: ["error", "load", "ready"] }, { kind: "component", type: i6$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i9.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "component", type: DebugComponent, selector: "app-debug", inputs: ["titlePrefix", "title", "enable", "expanded"] }, { kind: "directive", type: MarkdownDirective, selector: "markdown,[markdown]" }, { kind: "component", type: FeedsComponent, selector: "app-feed", inputs: ["debug", "mobile", "showHeader", "showReadMoreButton", "headerColor", "cardColor", "shape", "class", "itemId", "filterItem", "feeds", "urls", "maxAgeInMonths", "maxContentLength"], outputs: ["editItem", "deleteItem"] }, { kind: "directive", type: FeedDirective, selector: "feed,[feed]", inputs: ["feed"] }, { kind: "pipe", type: i3$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i3$1.JsonPipe, name: "json" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }, { kind: "pipe", type: DateFromNowPipe, name: "dateFromNow" }, { kind: "pipe", type: NotEmptyArrayPipe, name: "isNotEmptyArray" }, { kind: "pipe", type: ArrayFirstPipe, name: "arrayFirst" }, { kind: "pipe", type: ArrayFilterPipe, name: "arrayFilter" }, { kind: "pipe", type: MapPipe, name: "map" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
34225
35081
  }
34226
35082
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FeedsComponent, decorators: [{
34227
35083
  type: Component,
34228
- args: [{ selector: 'app-feed', providers: [RxState], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- debug -->\n@if (debug) {\n <app-debug [title]=\"'Feed'\">\n <p>\n hasFeeds?: {{ hasFeeds$ | async }}\n <br />\n feedUrls: {{ feedUrls$ | async | json }}\n <br />\n shape: {{ shape }}\n </p>\n </app-debug>\n}\n\n@let feeds = feeds$ | async;\n@if (feeds | isNotEmptyArray) {\n <!-- top header -->\n @if (showHeader && (feeds | arrayFirst); as feed) {\n <ion-item lines=\"none\" [color]=\"headerColor\" class=\"feed-header shape-{{ shape }}\">\n <ion-icon slot=\"start\" name=\"megaphone\"></ion-icon>\n <ion-label>\n <b>{{ feed.title || ('SOCIAL.FEED.NEWS' | translate) }}</b>\n </ion-label>\n <ion-button slot=\"end\" fill=\"clear\" (click)=\"openFeedHome()\" shape=\"\">\n <ion-label translate>SOCIAL.FEED.SHOW_ALL_FEED</ion-label>\n <ion-icon slot=\"end\" name=\"chevron-forward-outline\"></ion-icon>\n </ion-button>\n </ion-item>\n }\n\n <div class=\"feed-content shape-{{ shape }} ion-no-padding\" [class.has-header]=\"showHeader\">\n <!-- feeds -->\n @for (feed of feeds; track feed.feed_url; let firstFeed = $first; let lastFeed = $last) {\n <!-- items -->\n @for (item of feed.items | arrayFilter: filterItem; track item.id; let firstItem = $first; let lastItem = $last) {\n <ion-card\n [class.first]=\"firstFeed && firstItem\"\n [class.last]=\"lastFeed && lastItem\"\n [color]=\"cardColor !== 'light-transparent' ? cardColor : undefined\"\n >\n <ion-card-header>\n <ion-card-subtitle style=\"vertical-align: middle\">\n <!-- Authors -->\n @for (author of item.authors || feed.authors; track author) {\n @if (author.name || author.avatar) {\n <ion-chip (click)=\"openUrl(author.url)\" tappable>\n @if (author.avatar) {\n <ion-avatar>\n <ion-img [src]=\"author.avatar\" [alt]=\"author.name\"></ion-img>\n </ion-avatar>\n }\n @if (author.name) {\n <ion-label class=\"author\">{{ author.name }}</ion-label>\n }\n </ion-chip>\n }\n }\n <ion-note class=\"ion-float-end\">\n <small>{{ item.date_published | dateFromNow }}</small>\n </ion-note>\n </ion-card-subtitle>\n\n <!-- title -->\n <ion-card-title (click)=\"openFeedItem(item, feed)\" tappable>{{ item?.title }}</ion-card-title>\n\n <!-- tags -->\n @let tags = item | map: getTags;\n @if (tags | isNotEmptyArray) {\n <ion-text class=\"tags\">\n @for (tag of tags; track tag; let last = $last) {\n <a (click)=\"openTag(feed, tag)\" tappable>\n <ion-text>#{{ tag }}</ion-text>\n </a>\n @if (!last) {\n &nbsp;\n }\n }\n </ion-text>\n }\n </ion-card-header>\n\n <!-- Feed content -->\n <ion-card-content>\n <ion-text [feed]=\"item.url || feed.feed_url\">\n @if (item.content_html) {\n <p [innerHTML]=\"item.content_html\"></p>\n } @else if (item.content_text) {\n <p>\n <markdown [data]=\"item.content_text\" emoji></markdown>\n </p>\n }\n </ion-text>\n </ion-card-content>\n\n @if (showReadMoreButton) {\n <ion-button (click)=\"openFeedItem(item, feed)\" class=\"ion-float-end\" fill=\"clear\">\n <ion-label>{{ (item.truncated ? 'SOCIAL.FEED.READ_MORE' : 'COMMON.BTN_SHOW') | translate }}</ion-label>\n <ion-icon slot=\"end\" name=\"chevron-forward-outline\"></ion-icon>\n </ion-button>\n }\n </ion-card>\n }\n }\n </div>\n}\n\n<ion-modal #modal [showBackdrop]=\"false\"\n class=\"stack-modal\" [class.modal-large]=\"!mobile\">\n <ng-template>\n <ion-header>\n <ion-toolbar color=\"secondary\">\n <ion-title>{{ 'SOCIAL.FEED.NEWS' | translate }}</ion-title>\n <ion-buttons slot=\"end\">\n <ion-button (click)=\"modal.dismiss()\">\n <ion-icon slot=\"icon-only\" name=\"close\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content>\n <div class=\"ion-padding\">\n <app-feed\n [feedUrls]=\"feedUrls\"\n [showHeader]=\"false\"\n [showReadMoreButton]=\"false\"\n [maxContentLength]=\"-1\"\n [maxAgeInMonths]=\"-1\"\n [itemId]=\"modalItemId\"\n cardColor=\"light\"\n [debug]=\"debug\"\n ></app-feed>\n </div>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [":host{display:block;height:calc(100% - 10px);max-height:fit-content;overflow:hidden;--feed-header-height: 48px;-webkit-box-shadow:0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12);box-shadow:0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px rgba(0,0,0,.14),0 1px 5px rgba(0,0,0,.12);-webkit-margin-start:10px;margin-inline-start:10px;-webkit-margin-end:10px;margin-inline-end:10px;margin-top:0;margin-bottom:10px;--feed-border-radius: 4px;border-radius:var(--feed-border-radius);--ion-card-background: rgba(var(--ion-background-color-rgb), .6)}:host.shape-round{--feed-border-radius: 12px}ion-button{text-transform:unset;--color: rgba(var(--ion-color-contrast-rgb), .7)}ion-item.feed-header{height:var(--feed-header-height);margin:0;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);border-radius:var(--feed-border-radius) var(--feed-border-radius) 0 0}ion-item.feed-header ion-icon[slot=start]{-webkit-margin-end:8px;margin-inline-end:8px}.feed-content{height:auto;overflow-y:auto;--margin-bottom: 8px}.feed-content.has-header{height:calc(100% - var(--feed-header-height, 0))}.feed-content ion-card{--top-radius: 4px;--bottom-radius: 4px;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);margin:0 0 var(--margin-bottom) 0;border-radius:var(--top-radius) var(--top-radius) var(--bottom-radius) var(--bottom-radius);--ion-card-color-contrast-rgb: var(--ion-color-contrast-rgb, var(--ion-color-dark-rgb, 0, 0, 0));--color: rgba(var(--ion-card-color-contrast-rgb), .87)}.feed-content ion-card.first{--top-radius: 0}.feed-content ion-card.last{--margin-bottom: 0;--bottom-radius: 0}.feed-content ion-card ion-card-header ion-card-title{--color: rgba(var(--ion-card-color-contrast-rgb), .87)}.feed-content ion-card ion-card-header ion-card-subtitle ion-chip{--background: transparent;--border-color: transparent;--border-width: 0}.feed-content ion-card ion-card-header ion-card-subtitle ion-chip ion-avatar{--color: rgba(var(--ion-card-color-contrast-rgb), .6);border:1px solid var(--color)}.feed-content ion-card ion-card-header ion-card-subtitle ion-chip ion-label{--color: rgba(var(--ion-card-color-contrast-rgb), .8);color:var(--color)}.feed-content ion-card ion-card-header ion-card-subtitle ion-note{--color: rgba(var(--ion-card-color-contrast-rgb), .6);color:var(--color)}.feed-content ion-card ion-card-header .tags{display:inline}.feed-content ion-card ion-card-header .tags a{color:rgba(var(--ion-card-color-contrast-rgb),.6)!important}.feed-content ion-card ion-card-content ion-text ::ng-deep img{max-width:100%;height:auto!important}\n"] }]
35084
+ args: [{ selector: 'app-feed', providers: [RxState], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- debug -->\n@if (debug) {\n <app-debug [title]=\"'Feed'\">\n <p>\n hasFeeds?: {{ hasFeeds$ | async }}\n <br />\n urls: {{ urls$ | async | json }}\n <br />\n shape: {{ shape }}\n </p>\n </app-debug>\n}\n\n@let feeds = feeds$ | async;\n@let userId = userId$ | async;\n\n@if (feeds | isNotEmptyArray) {\n <!-- top header -->\n @if (showHeader && (feeds | arrayFirst); as feed) {\n <ion-item lines=\"none\" [color]=\"headerColor\" class=\"feed-header shape-{{ shape }}\">\n <ion-icon slot=\"start\" name=\"megaphone\"></ion-icon>\n <ion-label>\n <b>{{ feed.title || ('SOCIAL.FEED.NEWS' | translate) }}</b>\n </ion-label>\n <ion-button slot=\"end\" fill=\"clear\" (click)=\"openFeedHome()\" shape=\"\">\n <ion-label translate>SOCIAL.FEED.SHOW_ALL_FEED</ion-label>\n <ion-icon slot=\"end\" name=\"chevron-forward-outline\"></ion-icon>\n </ion-button>\n </ion-item>\n }\n\n <div class=\"feed-content shape-{{ shape }} ion-no-padding\" [class.has-header]=\"showHeader\">\n <!-- feeds -->\n @for (feed of feeds; track feed.feed_url; let firstFeed = $first; let lastFeed = $last) {\n <!-- items -->\n @for (item of feed.items | arrayFilter: filterItem; track item.id; let firstItem = $first; let lastItem = $last) {\n <ion-card\n [class.first]=\"firstFeed && firstItem\"\n [class.last]=\"lastFeed && lastItem\"\n [color]=\"cardColor !== 'light-transparent' ? cardColor : undefined\"\n class=\"feed-item-card\"\n >\n <ion-card-header>\n <ion-card-subtitle style=\"vertical-align: middle\">\n <!-- Authors -->\n @for (author of item.authors || feed.authors; track author) {\n @if (author.name || author.avatar) {\n <ion-chip (click)=\"openUrl(author.url)\" tappable>\n @if (author.avatar) {\n <ion-avatar>\n <ion-img [src]=\"author.avatar\" [alt]=\"author.name\"></ion-img>\n </ion-avatar>\n }\n @if (author.name) {\n <ion-label class=\"author\">{{ author.name }}</ion-label>\n }\n </ion-chip>\n }\n }\n <ion-note class=\"ion-float-end\">\n <small>{{ item.date_published | dateFromNow }}</small>\n </ion-note>\n </ion-card-subtitle>\n\n <!-- title -->\n <ion-card-title (click)=\"openFeedItem(item, feed)\" tappable>{{ item?.title }}</ion-card-title>\n\n <!-- tags -->\n @let tags = item | map: getTags;\n @if (tags | isNotEmptyArray) {\n <ion-text class=\"tags\">\n @for (tag of tags; track tag; let last = $last) {\n <a (click)=\"openTag(feed, tag)\" tappable>\n <ion-text>#{{ tag }}</ion-text>\n </a>\n @if (!last) {\n &nbsp;\n }\n }\n </ion-text>\n }\n </ion-card-header>\n\n <!-- Feed content -->\n <ion-card-content>\n <ion-text [feed]=\"item.url || feed.feed_url\">\n @if (item.content_html) {\n <p [innerHTML]=\"item.content_html\"></p>\n } @else if (item.content_text) {\n <p>\n <markdown [data]=\"item.content_text\" emoji></markdown>\n </p>\n }\n </ion-text>\n </ion-card-content>\n\n @let editable = canEditItem(item, userId, feed);\n @if (editable || showReadMoreButton) {\n @if (editable) {\n\n <!-- Delete button (visible hover)-->\n <button\n mat-icon-button\n (click)=\"onDeleteItem($event, item)\"\n class=\"visible-hover ion-float-start\"\n [title]=\"'COMMON.BTN_DELETE' | translate\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n <!-- Edit button (visible hover) -->\n <ion-button\n (click)=\"onEditItem($event, item)\"\n class=\"visible-hover ion-float-start\" fill=\"clear\"\n >\n <mat-icon slot=\"start\">edit</mat-icon>\n<!-- <ion-icon name=\"pencil\" slot=\"start\"></ion-icon>-->\n <ion-label translate>COMMON.BTN_EDIT</ion-label>\n </ion-button>\n }\n @if (showReadMoreButton) {\n <ion-button (click)=\"openFeedItem(item, feed)\" class=\"ion-float-end\" fill=\"clear\">\n <ion-label>{{ (item.truncated ? 'SOCIAL.FEED.READ_MORE' : 'COMMON.BTN_SHOW') | translate }}</ion-label>\n <ion-icon slot=\"end\" name=\"chevron-forward-outline\"></ion-icon>\n </ion-button>\n }\n }\n </ion-card>\n }\n }\n </div>\n}\n\n<!-- Details modal -->\n<ion-modal #modal [showBackdrop]=\"false\"\n class=\"stack-modal\" [class.modal-large]=\"!mobile\">\n <ng-template>\n <ion-header>\n <ion-toolbar color=\"secondary\">\n <ion-title>{{ 'SOCIAL.FEED.NEWS' | translate }}</ion-title>\n <ion-buttons slot=\"end\">\n <ion-button (click)=\"modal.dismiss()\">\n <ion-icon slot=\"icon-only\" name=\"close\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content>\n <div class=\"ion-padding\">\n <app-feed\n [urls]=\"urls\"\n [showHeader]=\"false\"\n [showReadMoreButton]=\"false\"\n [maxContentLength]=\"-1\"\n [maxAgeInMonths]=\"-1\"\n [itemId]=\"modalItemId\"\n cardColor=\"light\"\n [debug]=\"debug\"\n ></app-feed>\n </div>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [":host{display:block;height:calc(100% - 10px);max-height:fit-content;overflow:hidden;--feed-header-height: 48px;-webkit-box-shadow:0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12);box-shadow:0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px rgba(0,0,0,.14),0 1px 5px rgba(0,0,0,.12);-webkit-margin-start:10px;margin-inline-start:10px;-webkit-margin-end:10px;margin-inline-end:10px;margin-top:0;margin-bottom:10px;--feed-border-radius: 4px;border-radius:var(--feed-border-radius);--ion-card-background: rgba(var(--ion-background-color-rgb), .6)}:host.shape-round{--feed-border-radius: 12px}ion-button{text-transform:unset;--color: rgba(var(--ion-color-contrast-rgb), .7)}ion-item.feed-header{height:var(--feed-header-height);margin:0;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);border-radius:var(--feed-border-radius) var(--feed-border-radius) 0 0}ion-item.feed-header ion-icon[slot=start]{-webkit-margin-end:8px;margin-inline-end:8px}.feed-content{height:auto;overflow-y:auto;--margin-bottom: 8px}.feed-content.has-header{height:calc(100% - var(--feed-header-height, 0))}.feed-content ion-card.feed-item-card{--top-radius: 4px;--bottom-radius: 4px;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);margin:0 0 var(--margin-bottom) 0;border-radius:var(--top-radius) var(--top-radius) var(--bottom-radius) var(--bottom-radius);--ion-card-color-contrast-rgb: var(--ion-color-contrast-rgb, var(--ion-color-dark-rgb, 0, 0, 0));--color: rgba(var(--ion-card-color-contrast-rgb), .87)}.feed-content ion-card.feed-item-card.first{--top-radius: 0}.feed-content ion-card.feed-item-card.last{--margin-bottom: 0;--bottom-radius: 0}.feed-content ion-card.feed-item-card ion-card-header ion-card-title{--color: rgba(var(--ion-card-color-contrast-rgb), .87)}.feed-content ion-card.feed-item-card ion-card-header ion-card-subtitle ion-chip{--background: transparent;--border-color: transparent;--border-width: 0}.feed-content ion-card.feed-item-card ion-card-header ion-card-subtitle ion-chip ion-avatar{--color: rgba(var(--ion-card-color-contrast-rgb), .6);border:1px solid var(--color)}.feed-content ion-card.feed-item-card ion-card-header ion-card-subtitle ion-chip ion-label{--color: rgba(var(--ion-card-color-contrast-rgb), .8);color:var(--color)}.feed-content ion-card.feed-item-card ion-card-header ion-card-subtitle ion-note{--color: rgba(var(--ion-card-color-contrast-rgb), .6);color:var(--color)}.feed-content ion-card.feed-item-card ion-card-header .tags{display:inline}.feed-content ion-card.feed-item-card ion-card-header .tags a{color:rgba(var(--ion-card-color-contrast-rgb),.6)!important}.feed-content ion-card.feed-item-card ion-card-content ion-text ::ng-deep img{max-width:100%;height:auto!important}.feed-content ion-card.feed-item-card .visible-hover{opacity:0;transition:opacity .2s ease-in-out}.feed-content ion-card.feed-item-card:hover .visible-hover{opacity:1}\n"] }]
34229
35085
  }], ctorParameters: () => [{ type: LocalSettingsService }, { type: Environment, decorators: [{
34230
35086
  type: Inject,
34231
35087
  args: [ENVIRONMENT]
@@ -34234,7 +35090,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
34234
35090
  }, {
34235
35091
  type: Inject,
34236
35092
  args: [APP_FEED_SERVICE]
34237
- }] }], propDecorators: { debug: [{
35093
+ }] }, { type: AccountService }, { type: i2$1.AlertController }], propDecorators: { debug: [{
34238
35094
  type: Input
34239
35095
  }], mobile: [{
34240
35096
  type: Input
@@ -34254,9 +35110,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
34254
35110
  type: Input
34255
35111
  }], filterItem: [{
34256
35112
  type: Input
35113
+ }], editItem: [{
35114
+ type: Output
35115
+ }], deleteItem: [{
35116
+ type: Output
34257
35117
  }], feeds: [{
34258
35118
  type: Input
34259
- }], feedUrls: [{
35119
+ }], urls: [{
34260
35120
  type: Input
34261
35121
  }], maxAgeInMonths: [{
34262
35122
  type: Input
@@ -34643,11 +35503,11 @@ class HomePage extends RxState {
34643
35503
  this.cd.markForCheck();
34644
35504
  }
34645
35505
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: HomePage, deps: [{ token: AccountService }, { token: i2$1.ModalController }, { token: i1$1.TranslateService }, { token: i2$1.ToastController }, { token: ConfigService }, { token: PlatformService }, { token: i0.ChangeDetectorRef }, { token: NetworkService }, { token: LocalSettingsService }, { token: ENVIRONMENT }, { token: APP_LOCALES }, { token: APP_HOME_BUTTONS, optional: true }, { token: APP_HOME_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Component });
34646
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: HomePage, selector: "app-page-home", viewQueries: [{ propertyName: "content", first: true, predicate: IonContent, descendants: true, static: true }], usesInheritance: true, ngImport: i0, template: "<app-toolbar [canGoBack]=\"false\" visible-xs visible-sm visible-mobile>\n <ion-buttons slot=\"end\">\n @if (showNotificationIcon) {\n <app-user-event-notification-icon [autoHide]=\"autoHideNotificationIcon\"></app-user-event-notification-icon>\n }\n\n <!-- dark mode (if allow by the environment token) -->\n @if (settings.allowDarkMode) {\n <ion-button (click)=\"toggleDarkMode()\">\n <ion-icon slot=\"icon-only\" [name]=\"(darkMode$ | push) ? 'sunny' : 'moon'\"></ion-icon>\n </ion-button>\n }\n\n <!-- Change locale button -->\n <ion-button [matMenuTriggerFor]=\"localeMenu\">\n <ion-icon slot=\"start\" name=\"language\"></ion-icon>\n </ion-button>\n </ion-buttons>\n</app-toolbar>\n\n<!-- Change locale menu -->\n<mat-menu #localeMenu=\"matMenu\">\n <ng-template matMenuContent>\n @for (item of locales; track item.key) {\n <button mat-menu-item (click)=\"changeLanguage(item.key)\">\n <!--<mat-icon>{{ currentLocale === item.key ? 'checkmark' : undefined }}</mat-icon>-->\n <ion-label>{{ item.value }}</ion-label>\n </button>\n }\n </ng-template>\n</mat-menu>\n\n<ion-content [ngStyle]=\"contentStyle\" [class.has-scrollbar]=\"hasScrollbar\" no-padding-xs>\n <!-- loading spinner -->\n <div class=\"loading-page\" [class.cdk-visually-hidden]=\"!(loading$ | push)\">\n @if (showSpinner) {\n <div class=\"spinner\">\n <p [innerHTML]=\"'COMMON.LOADING_DOTS' | translate\"></p>\n <div class=\"sk-cube1 sk-cube\"></div>\n <div class=\"sk-cube2 sk-cube\"></div>\n <div class=\"sk-cube4 sk-cube\"></div>\n <div class=\"sk-cube3 sk-cube\"></div>\n </div>\n }\n </div>\n\n <!-- Desktop: translucent top toolbar -->\n @if (!mobile) {\n <div class=\"hidden-xs hidden-sm\">\n <ion-toolbar color=\"traansparent\" translucent>\n <ion-buttons slot=\"start\">\n <ion-menu-toggle>\n <ion-button color=\"light\" fill=\"clear\">\n <ion-icon slot=\"icon-only\" name=\"menu\"></ion-icon>\n </ion-button>\n </ion-menu-toggle>\n </ion-buttons>\n\n <ion-buttons slot=\"end\">\n @if (showNotificationIcon) {\n <app-user-event-notification-icon\n [style]=\"'ion-button'\"\n color=\"secondary\"\n unreadColor=\"tertiary\"\n fill=\"solid\"\n [autoHide]=\"autoHideNotificationIcon\"\n ></app-user-event-notification-icon>\n }\n\n <!-- dark mode (if allow by the environment token) -->\n @if (settings.allowDarkMode) {\n <ion-button\n color=\"secondary\"\n fill=\"solid\"\n (click)=\"toggleDarkMode()\"\n [title]=\"'HOME.BTN_DARK_MODE' | translate\"\n >\n <ion-icon slot=\"icon-only\" [name]=\"(darkMode$ | push) ? 'moon' : 'sunny'\"></ion-icon>\n </ion-button>\n }\n <!-- change locale button -->\n <ion-button\n color=\"secondary\"\n fill=\"solid\"\n [matMenuTriggerFor]=\"localeMenu\"\n [title]=\"'SETTINGS.LOCALE' | translate\"\n >\n <ion-icon slot=\"icon-only\" name=\"language\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </div>\n }\n\n <!-- Install (and upgrade) card -->\n <app-install-upgrade-card [isLogin]=\"isLogin\" [showInstallButton]=\"true\"></app-install-upgrade-card>\n\n @if (!(loading$ | push)) {\n @let hasFeed = hasFeed$ | async;\n <ion-grid class=\"ion-no-padding\">\n <ion-row>\n <ion-col size=\"0\" [sizeXl]=\"hasFeed ? 3 : 0\"></ion-col>\n <ion-col>\n <!-- Welcome card -->\n <ion-card class=\"main welcome ion-padding ion-text-center ion-align-self-center\" @fadeInAnimation>\n <ion-card-header>\n <ion-card-title class=\"ion-text-center\">\n <span *ngIf=\"isWeb\" [innerHTML]=\"'HOME.WELCOME_WEB' | translate: { appName: appName }\"></span>\n <span *ngIf=\"!isWeb\" [innerHTML]=\"'HOME.WELCOME_APP' | translate: { appName: appName }\"></span>\n </ion-card-title>\n <ion-card-subtitle [innerHTML]=\"description\"></ion-card-subtitle>\n </ion-card-header>\n <ion-card-content class=\"ion-no-padding\">\n <ion-text color=\"primary\">\n <img *ngIf=\"logo\" [attr.src]=\"logo\" alt=\"Logo\" />\n </ion-text>\n <!-- register help text -->\n <ion-text *ngIf=\"!isLogin && canRegister\">\n <br />\n <span translate>HOME.REGISTER_HELP</span>\n </ion-text>\n <div class=\"ion-padding-top\">\n <!-- If NOT login -->\n <ng-container *ngIf=\"!isLogin; else loginButtons\">\n <ion-button *ngIf=\"canRegister\" expand=\"block\" color=\"tertiary\" (click)=\"register()\">\n <span translate>HOME.BTN_REGISTER</span>\n </ion-button>\n <ion-button expand=\"block\" color=\"light\" [routerLink]=\"['/']\" (click)=\"login()\">\n <span translate>AUTH.BTN_LOGIN</span>\n </ion-button>\n </ng-container>\n\n <!-- If user login -->\n <ng-template #loginButtons>\n <!-- Feature buttons -->\n <ng-container *rxIf=\"$filteredButtons; let buttons\">\n <ng-container *ngFor=\"let item of buttons\">\n <ion-button\n *ngIf=\"item.path\"\n expand=\"block\"\n color=\"tertiary\"\n [class]=\"item.cssClass\"\n [routerLink]=\"item.path\"\n routerDirection=\"root\"\n >\n <ion-icon slot=\"start\" class=\"ion-float-start\" *ngIf=\"item.icon\" [name]=\"item.icon\"></ion-icon>\n <mat-icon slot=\"start\" class=\"ion-float-start\" *ngIf=\"item.matIcon\">\n {{ item.matIcon }}\n </mat-icon>\n <ion-text>{{ 'HOME.BTN_DATA_ENTRY' | translate: { name: (item.title | translate) } }}</ion-text>\n </ion-button>\n\n <!-- divider -->\n <div *ngIf=\"!item.path && !item.action\" [class]=\"item.cssClass\">\n <ion-label translate>{{ item.title }}&nbsp;</ion-label>\n </div>\n </ng-container>\n\n <!--<p *ngIf=\"buttons.length\" class=\"visible-mobile\">&nbsp;</p>-->\n </ng-container>\n\n <ion-button *ngIf=\"showAccountButton\" expand=\"block\" color=\"secondary\" [routerLink]=\"['/account']\">\n <ion-icon slot=\"start\" class=\"ion-float-start\" name=\"person-circle\"></ion-icon>\n <ion-text translate>HOME.BTN_MY_ACCOUNT</ion-text>\n </ion-button>\n\n <p hidden-xs hidden-sm hidden-mobile>\n <ion-text\n [innerHTML]=\"'HOME.NOT_THIS_ACCOUNT_QUESTION' | translate: { displayName: accountName }\"\n ></ion-text>\n <br />\n <ion-text>\n <a href=\"#\" (click)=\"logout($event)\">\n <span translate>HOME.BTN_DISCONNECT</span>\n </a>\n </ion-text>\n </p>\n </ng-template>\n </div>\n </ion-card-content>\n </ion-card>\n </ion-col>\n <ion-col size=\"0\" [sizeXl]=\"hasFeed ? 1 : 0\"></ion-col>\n <ion-col [class.feed]=\"hasFeed\" [size]=\"hasFeed ? 12 : 0\" [sizeXl]=\"hasFeed ? 4 : 0\">\n @if (showFeed) {\n @if (mobile) {\n <app-feed\n #feed\n shape=\"round\"\n [feedUrls]=\"feedUrls\"\n [maxAgeInMonths]=\"feedMaxAgeInMonths\"\n [maxContentLength]=\"feedMaxContentLength\"\n (ngInit)=\"initFeed(feed)\"\n @fadeInAnimation\n ></app-feed>\n } @else {\n <app-feed\n #feed\n shape=\"round\"\n [debug]=\"false\"\n [feedUrls]=\"feedUrls\"\n [maxAgeInMonths]=\"feedMaxAgeInMonths\"\n [maxContentLength]=\"feedMaxContentLength\"\n (ngInit)=\"initFeed(feed)\"\n @slideInAnimation\n @fadeInAnimation\n ></app-feed>\n }\n }\n </ion-col>\n </ion-row>\n </ion-grid>\n\n <!-- Page history -->\n @let pageHistory = isLogin && (pageHistory$ | push);\n @if (pageHistory | isNotEmptyArray) {\n <ion-grid class=\"history-container ion-align-self-center\">\n <ion-row>\n @for (page of pageHistory; track page.path) {\n <ion-col size=\"12\" size-xl=\"\" class=\"ion-text-center\">\n <ion-card class=\"ion-align-self-start ion-text-start\" @fadeInAnimation>\n <ion-card-header class=\"ion-no-padding\">\n <!-- top bar -->\n <ion-card-subtitle>\n <button\n type=\"button\"\n tabindex=\"-1\"\n (click)=\"removePageHistory(page.path)\"\n mat-icon-button\n class=\"ion-float-start ion-no-margin close-button\"\n >\n <mat-icon>close</mat-icon>\n </button>\n <ion-label [innerHTML]=\"page.subtitle | translate\"></ion-label>\n <ion-text class=\"ion-float-end\" [title]=\"page.time | dateFormat: { time: true }\">\n <small>\n <ion-icon name=\"time-outline\"></ion-icon>\n &nbsp;{{ page.time | dateFromNow }}\n </small>\n &nbsp;\n </ion-text>\n </ion-card-subtitle>\n\n <!-- main page -->\n <ion-card-title class=\"ion-no-margin ion-no-padding\">\n <ion-item\n detail=\"true\"\n tappable\n class=\"text-1x\"\n [routerLink]=\"page.path\"\n routerDirection=\"root\"\n lines=\"none\"\n >\n <!-- page icon-->\n <ion-icon *ngIf=\"page.icon\" slot=\"start\" [name]=\"page.icon\"></ion-icon>\n <mat-icon *ngIf=\"page.matIcon\" slot=\"start\">{{ page.matIcon }}</mat-icon>\n\n <ion-label color=\"primary\" [innerHTML]=\"page.title\"></ion-label>\n </ion-item>\n </ion-card-title>\n </ion-card-header>\n\n <!-- children pages -->\n @if (page.children | isNotEmptyArray) {\n <ion-card-content class=\"ion-no-padding ion-padding-start\">\n <ion-list class=\"ion-no-padding\">\n @for (childPage of page.children; track childPage.path) {\n <ion-item\n detail=\"true\"\n tappable\n class=\"text-1x\"\n [routerLink]=\"childPage.path\"\n routerDirection=\"root\"\n >\n <!-- page icon-->\n <ion-icon *ngIf=\"childPage.icon\" slot=\"start\" color=\"dark\" [name]=\"childPage.icon\"></ion-icon>\n <mat-icon *ngIf=\"childPage.matIcon\" slot=\"start\" color=\"dark\">\n {{ childPage.matIcon }}\n </mat-icon>\n\n <ion-label color=\"dark\" [innerHTML]=\"childPage.title\"></ion-label>\n </ion-item>\n }\n </ion-list>\n </ion-card-content>\n }\n </ion-card>\n </ion-col>\n }\n </ion-row>\n </ion-grid>\n }\n\n <!-- Bottom banner -->\n @if (showPartnerBanner || showLegalInformation) {\n <ion-grid\n class=\"bottom-banner ion-text-center\"\n [class.floating]=\"!mobile && (pageHistory | isEmptyArray)\"\n @fadeInAnimation\n >\n <ion-row>\n <!-- partners logos -->\n @if (showPartnerBanner) {\n <ion-col size=\"12\" [sizeLg]=\"partnerBannerSize\" class=\"ion-text-center\">\n @if (showLegalInformation) {\n <ion-list class=\"legal-info\">\n <ion-list-header>{{ 'HOME.PARTNERS' | translate }}</ion-list-header>\n </ion-list>\n }\n <!-- partners -->\n @for (item of $partners | async; track $index) {\n <a href=\"{{ item.siteUrl }}\">\n <img class=\"logo\" src=\"{{ item.logo }}\" alt=\"{{ item.label }}\" [title]=\"item.label\" />\n </a>\n }\n </ion-col>\n }\n\n <!-- legal information -->\n @if (showLegalInformation) {\n <ion-col size=\"12\" size-sm=\"6\" size-lg=\"4\" [class.left-border]=\"showPartnerBanner\">\n <ion-list class=\"legal-info\">\n <ion-list-header class=\"ion-text-left\">{{ 'HOME.LEGAL_INFORMATION' | translate }}</ion-list-header>\n @for (item of legalInformation; track item.path) {\n <ion-item (click)=\"openMarkdownModal($event, item)\" lines=\"none\" [button]=\"true\">\n <ion-label>\n <p>\n {{ item.title | translate: item.titleArgs }}\n </p>\n </ion-label>\n </ion-item>\n }\n </ion-list>\n </ion-col>\n @if (showInstallLinks) {\n <ion-col size=\"12\" size-lg=\"3\" offset-lg=\"9\" [class.left-border]=\"showPartnerBanner\">\n <ion-list class=\"legal-info\">\n <ion-list-header>{{ 'HOME.INSTALL_APP' | translate }}</ion-list-header>\n <!-- TODO -->\n </ion-list>\n </ion-col>\n }\n }\n </ion-row>\n </ion-grid>\n }\n }\n\n <!-- image credits -->\n @if (contentCredits | isNotNilOrBlank) {\n <div class=\"content-credits\" [class.floating]=\"!mobile\">\n <ion-text>{{ contentCredits }}</ion-text>\n </div>\n }\n</ion-content>\n", styles: [":host{--ion-card-background-color: rgba(var(--ion-background-color-rgb), .7);--scrollbar-width: 0;--bottom-banner-height: 122px}h1,h2,h3,h4,h5{white-space:normal}.center,ion-content ion-card.welcome img{text-align:center;display:inline-block}ion-content{--background: transparent;background-size:cover;-webkit-background-size:cover;-moz-background-size:cover;-o-background-size:cover;background-color:var(--ion-background-color);display:inline-block;padding:15px}ion-content.has-scrollbar{--scrollbar-width: 8px}ion-content .loading-page{display:block;height:100%;width:100%;position:absolute;background-color:transparent;z-index:5;transition:background .5s linear;-webkit-transition:background 1.5s linear}ion-content .loading-page.hidden{background-color:rgba(var(--ion-color-primary),0)}ion-content ion-card{z-index:9;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}ion-content ion-card ion-card-header{display:block}ion-content ion-card ion-card-header ion-card-subtitle,ion-content ion-card ion-card-header ion-card-title{display:block;width:100%}ion-content ion-card.main{min-width:240px;max-width:400px;margin-left:auto;margin-right:auto}ion-content ion-card.welcome{background-color:var(--ion-card-background-color)}ion-content ion-card.welcome button{margin-top:16px}ion-content ion-card.welcome img{max-width:250px}ion-content .history-container{margin-left:auto;margin-right:auto}ion-content .history-container ion-card{display:inline-block;box-sizing:border-box;z-index:8;min-width:240px;max-width:400px;width:100%;margin-left:auto;margin-right:auto;background-color:var(--ion-card-background-color);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}ion-content .history-container ion-card ion-card-header ion-card-subtitle{height:40px}ion-content .history-container ion-card ion-card-header ion-card-subtitle button[float-start]{--mdc-icon-button-state-layer-size: 40px;padding:8px;z-index:99;margin:0}ion-content .history-container ion-card ion-card-header ion-card-subtitle ion-label{line-height:40px}ion-content .history-container ion-card ion-card-header ion-card-subtitle ion-label[float-end]{padding-right:16px;font-weight:400}ion-content .history-container ion-card ion-card-header ion-card-title ion-item{width:100%;--ion-item-background: $transparent;--ion-item-icon-color: var(--ion-color-primary);--ion-item-text-color: var(--ion-color-primary)}ion-content .history-container ion-card ion-card-header ion-card-title ion-item ion-icon[slot=start],ion-content .history-container ion-card ion-card-header ion-card-title ion-item mat-icon[slot=start]{margin-right:unset;margin-inline-end:16px!important;color:var(--ion-item-icon-color)!important;fill:currentColor;stroke:currentColor}ion-content .history-container ion-card ion-card-header ion-card-title ion-item ion-text,ion-content .history-container ion-card ion-card-header ion-card-title ion-item ion-label{color:var(--ion-item-text-color)!important;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;-ms-text-size-adjust:100%;text-size-adjust:100%}ion-content .history-container ion-card ion-card-content ion-list{--ion-item-background: var(--ion-color-transparent)}ion-content .history-container ion-card ion-card-content ion-list ion-item{--ion-item-icon-color: var(--ion-color-dark);--ion-item-text-color: var(--ion-color-dark)}ion-content .history-container ion-card ion-card-content ion-list ion-item ion-icon[slot=start],ion-content .history-container ion-card ion-card-content ion-list ion-item mat-icon[slot=start]{color:var(--ion-item-icon-color)!important;fill:currentColor;stroke:currentColor;margin-right:unset;margin-inline-end:16px!important}ion-content .history-container ion-card ion-card-content ion-list ion-item ion-text,ion-content .history-container ion-card ion-card-content ion-list ion-item ion-label{color:var(--ion-item-text-color)!important}ion-content .bottom-banner{--ion-card-background-color: rgba(var(--ion-background-color-rgb), .5);background-color:var(--ion-card-background-color);min-width:240px;z-index:0;border-radius:4px;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);margin:16px}ion-content .bottom-banner img.logo{max-height:50px;margin-left:8px}ion-content .bottom-banner ion-list-header{min-height:30px}ion-content .bottom-banner ion-item{--min-height: 28px}ion-content .bottom-banner ion-item ion-label{margin-top:3px!important;margin-bottom:3px!important}ion-content ion-text{text-align:center}@media screen and (max-width: 575px){ion-content ion-card{min-width:160px;width:calc(100% - 20px);max-width:100%;margin-left:10px;margin-right:10px}ion-content ion-card h1{margin-top:0;font-size:20px}ion-content ion-card ion-card-content ion-button{margin-top:8px}ion-content .bottom-banner.floating{display:block;position:unset}ion-content .bottom-banner img.logo{max-height:30px}}@media screen and (max-width: 767px) and (min-width: 576px){ion-content{min-height:555px}ion-content .bottom-banner img.logo{max-height:40px}}@media screen and (min-width: 768px){ion-content{min-height:calc(100% - var(--bottom-banner-height))}}@media screen and (min-width: 1200px){ion-content ion-col.feed{height:fit-content;max-height:calc(100vh - var(--ion-toolbar-height) - var(--bottom-banner-height) - var(--ion-padding) * 2);padding-inline-end:calc(var(--ion-padding) - 10px);overflow:hidden}ion-content .bottom-banner.floating{position:fixed;left:0;right:0;bottom:0;overflow-y:auto;max-height:var(--bottom-banner-height);margin-top:0}}.content-credits{float:right}.content-credits.floating{float:unset;position:fixed;bottom:0;right:var(--scrollbar-width, 0)}.content-credits ion-text{padding-inline-start:4px;padding-inline-end:4px;font-size:.7rem;background-color:rgba(var(--ion-background-color-rgb),.5);color:rgba(var(--ion-text-color-rgb),.7)}@media screen and (min-width: 992px){.left-border{border-left:1px solid var(--ion-color-medium)}}\n"], dependencies: [{ kind: "directive", type: i3$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3$1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i2$1.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i2$1.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$1.IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$1.IonCardContent, selector: "ion-card-content", inputs: ["mode"] }, { kind: "component", type: i2$1.IonCardHeader, selector: "ion-card-header", inputs: ["color", "mode", "translucent"] }, { kind: "component", type: i2$1.IonCardSubtitle, selector: "ion-card-subtitle", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonCardTitle, selector: "ion-card-title", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i2$1.IonContent, selector: "ion-content", inputs: ["color", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$1.IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: i2$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$1.IonItem, selector: "ion-item", inputs: ["button", "color", "counter", "counterFormatter", "detail", "detailIcon", "disabled", "download", "fill", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "shape", "target", "type"] }, { kind: "component", type: i2$1.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$1.IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: i2$1.IonListHeader, selector: "ion-list-header", inputs: ["color", "lines", "mode"] }, { kind: "component", type: i2$1.IonMenuToggle, selector: "ion-menu-toggle", inputs: ["autoHide", "menu"] }, { kind: "component", type: i2$1.IonRow, selector: "ion-row" }, { kind: "component", type: i2$1.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "directive", type: i2$1.RouterLinkDelegate, selector: ":not(a):not(area)[routerLink]" }, { kind: "directive", type: i1$1.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { kind: "directive", type: i8$2.RxIf, selector: "[rxIf]", inputs: ["rxIf", "rxIfStrategy", "rxIfElse", "rxIfThen", "rxIfSuspense", "rxIfComplete", "rxIfError", "rxIfContextTrigger", "rxIfNextTrigger", "rxIfSuspenseTrigger", "rxIfErrorTrigger", "rxIfCompleteTrigger", "rxIfParent", "rxIfPatchZone", "rxIfRenderCallback"] }, { kind: "component", type: i6$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i9.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "component", type: i8$1.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i8$1.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i8$1.MatMenuContent, selector: "ng-template[matMenuContent]" }, { kind: "directive", type: i8$1.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "directive", type: NgInitDirective, selector: "[ngInit]", outputs: ["ngInit"] }, { kind: "component", type: ToolbarComponent, selector: "app-toolbar", inputs: ["progressBarMode", "title", "color", "class", "id", "backHref", "defaultBackHref", "hasValidate", "hasClose", "hasSearch", "canGoBack", "canShowMenu"], outputs: ["onValidate", "onClose", "onValidateAndClose", "onBackClick", "onSearch"] }, { kind: "directive", type: i1$6.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: AppInstallUpgradeCard, selector: "app-install-upgrade-card", inputs: ["isLogin", "showUpgradeWarning", "showOfflineWarning", "showInstallButton", "debug"] }, { kind: "component", type: UserEventNotificationIcon, selector: "app-user-event-notification-icon", inputs: ["titleI18n", "disabled", "filter", "autoHide", "headerActions", "footerActions", "style", "color", "unreadColor", "fill", "mobile", "listStyle", "debug"], outputs: ["showList"] }, { kind: "component", type: FeedsComponent, selector: "app-feed", inputs: ["debug", "mobile", "showHeader", "showReadMoreButton", "headerColor", "cardColor", "shape", "class", "itemId", "filterItem", "feeds", "feedUrls", "maxAgeInMonths", "maxContentLength"] }, { kind: "pipe", type: i3$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }, { kind: "pipe", type: i19.RxPush, name: "push" }, { kind: "pipe", type: DateFormatPipe, name: "dateFormat" }, { kind: "pipe", type: DateFromNowPipe, name: "dateFromNow" }, { kind: "pipe", type: NotEmptyArrayPipe, name: "isNotEmptyArray" }, { kind: "pipe", type: EmptyArrayPipe, name: "isEmptyArray" }, { kind: "pipe", type: IsNotNilOrBlankPipe, name: "isNotNilOrBlank" }], animations: [fadeInAnimation, slideUpDownAnimation, slideInAnimation], changeDetection: i0.ChangeDetectionStrategy.OnPush });
35506
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: HomePage, selector: "app-page-home", viewQueries: [{ propertyName: "content", first: true, predicate: IonContent, descendants: true, static: true }], usesInheritance: true, ngImport: i0, template: "<app-toolbar [canGoBack]=\"false\" visible-xs visible-sm visible-mobile>\n <ion-buttons slot=\"end\">\n @if (showNotificationIcon) {\n <app-user-event-notification-icon [autoHide]=\"autoHideNotificationIcon\"></app-user-event-notification-icon>\n }\n\n <!-- dark mode (if allow by the environment token) -->\n @if (settings.allowDarkMode) {\n <ion-button (click)=\"toggleDarkMode()\">\n <ion-icon slot=\"icon-only\" [name]=\"(darkMode$ | push) ? 'sunny' : 'moon'\"></ion-icon>\n </ion-button>\n }\n\n <!-- Change locale button -->\n <ion-button [matMenuTriggerFor]=\"localeMenu\">\n <ion-icon slot=\"start\" name=\"language\"></ion-icon>\n </ion-button>\n </ion-buttons>\n</app-toolbar>\n\n<!-- Change locale menu -->\n<mat-menu #localeMenu=\"matMenu\">\n <ng-template matMenuContent>\n @for (item of locales; track item.key) {\n <button mat-menu-item (click)=\"changeLanguage(item.key)\">\n <!--<mat-icon>{{ currentLocale === item.key ? 'checkmark' : undefined }}</mat-icon>-->\n <ion-label>{{ item.value }}</ion-label>\n </button>\n }\n </ng-template>\n</mat-menu>\n\n<ion-content [ngStyle]=\"contentStyle\" [class.has-scrollbar]=\"hasScrollbar\" no-padding-xs>\n <!-- loading spinner -->\n <div class=\"loading-page\" [class.cdk-visually-hidden]=\"!(loading$ | push)\">\n @if (showSpinner) {\n <div class=\"spinner\">\n <p [innerHTML]=\"'COMMON.LOADING_DOTS' | translate\"></p>\n <div class=\"sk-cube1 sk-cube\"></div>\n <div class=\"sk-cube2 sk-cube\"></div>\n <div class=\"sk-cube4 sk-cube\"></div>\n <div class=\"sk-cube3 sk-cube\"></div>\n </div>\n }\n </div>\n\n <!-- Desktop: translucent top toolbar -->\n @if (!mobile) {\n <div class=\"hidden-xs hidden-sm\">\n <ion-toolbar color=\"traansparent\" translucent>\n <ion-buttons slot=\"start\">\n <ion-menu-toggle>\n <ion-button color=\"light\" fill=\"clear\">\n <ion-icon slot=\"icon-only\" name=\"menu\"></ion-icon>\n </ion-button>\n </ion-menu-toggle>\n </ion-buttons>\n\n <ion-buttons slot=\"end\">\n @if (showNotificationIcon) {\n <app-user-event-notification-icon\n [style]=\"'ion-button'\"\n color=\"secondary\"\n unreadColor=\"tertiary\"\n fill=\"solid\"\n [autoHide]=\"autoHideNotificationIcon\"\n ></app-user-event-notification-icon>\n }\n\n <!-- dark mode (if allow by the environment token) -->\n @if (settings.allowDarkMode) {\n <ion-button\n color=\"secondary\"\n fill=\"solid\"\n (click)=\"toggleDarkMode()\"\n [title]=\"'HOME.BTN_DARK_MODE' | translate\"\n >\n <ion-icon slot=\"icon-only\" [name]=\"(darkMode$ | push) ? 'moon' : 'sunny'\"></ion-icon>\n </ion-button>\n }\n <!-- change locale button -->\n <ion-button\n color=\"secondary\"\n fill=\"solid\"\n [matMenuTriggerFor]=\"localeMenu\"\n [title]=\"'SETTINGS.LOCALE' | translate\"\n >\n <ion-icon slot=\"icon-only\" name=\"language\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </div>\n }\n\n <!-- Install (and upgrade) card -->\n <app-install-upgrade-card [isLogin]=\"isLogin\" [showInstallButton]=\"true\"></app-install-upgrade-card>\n\n @if (!(loading$ | push)) {\n @let hasFeed = hasFeed$ | async;\n <ion-grid class=\"ion-no-padding\">\n <ion-row>\n <ion-col size=\"0\" [sizeXl]=\"hasFeed ? 3 : 0\"></ion-col>\n <ion-col>\n <!-- Welcome card -->\n <ion-card class=\"main welcome ion-padding ion-text-center ion-align-self-center\" @fadeInAnimation>\n <ion-card-header>\n <ion-card-title class=\"ion-text-center\">\n <span *ngIf=\"isWeb\" [innerHTML]=\"'HOME.WELCOME_WEB' | translate: { appName: appName }\"></span>\n <span *ngIf=\"!isWeb\" [innerHTML]=\"'HOME.WELCOME_APP' | translate: { appName: appName }\"></span>\n </ion-card-title>\n <ion-card-subtitle [innerHTML]=\"description\"></ion-card-subtitle>\n </ion-card-header>\n <ion-card-content class=\"ion-no-padding\">\n <ion-text color=\"primary\">\n <img *ngIf=\"logo\" [attr.src]=\"logo\" alt=\"Logo\" />\n </ion-text>\n <!-- register help text -->\n <ion-text *ngIf=\"!isLogin && canRegister\">\n <br />\n <span translate>HOME.REGISTER_HELP</span>\n </ion-text>\n <div class=\"ion-padding-top\">\n <!-- If NOT login -->\n <ng-container *ngIf=\"!isLogin; else loginButtons\">\n <ion-button *ngIf=\"canRegister\" expand=\"block\" color=\"tertiary\" (click)=\"register()\">\n <span translate>HOME.BTN_REGISTER</span>\n </ion-button>\n <ion-button expand=\"block\" color=\"light\" [routerLink]=\"['/']\" (click)=\"login()\">\n <span translate>AUTH.BTN_LOGIN</span>\n </ion-button>\n </ng-container>\n\n <!-- If user login -->\n <ng-template #loginButtons>\n <!-- Feature buttons -->\n <ng-container *rxIf=\"$filteredButtons; let buttons\">\n <ng-container *ngFor=\"let item of buttons\">\n <ion-button\n *ngIf=\"item.path\"\n expand=\"block\"\n color=\"tertiary\"\n [class]=\"item.cssClass\"\n [routerLink]=\"item.path\"\n routerDirection=\"root\"\n >\n <ion-icon slot=\"start\" class=\"ion-float-start\" *ngIf=\"item.icon\" [name]=\"item.icon\"></ion-icon>\n <mat-icon slot=\"start\" class=\"ion-float-start\" *ngIf=\"item.matIcon\">\n {{ item.matIcon }}\n </mat-icon>\n <ion-text>{{ 'HOME.BTN_DATA_ENTRY' | translate: { name: (item.title | translate) } }}</ion-text>\n </ion-button>\n\n <!-- divider -->\n <div *ngIf=\"!item.path && !item.action\" [class]=\"item.cssClass\">\n <ion-label translate>{{ item.title }}&nbsp;</ion-label>\n </div>\n </ng-container>\n\n <!--<p *ngIf=\"buttons.length\" class=\"visible-mobile\">&nbsp;</p>-->\n </ng-container>\n\n <ion-button *ngIf=\"showAccountButton\" expand=\"block\" color=\"secondary\" [routerLink]=\"['/account']\">\n <ion-icon slot=\"start\" class=\"ion-float-start\" name=\"person-circle\"></ion-icon>\n <ion-text translate>HOME.BTN_MY_ACCOUNT</ion-text>\n </ion-button>\n\n <p hidden-xs hidden-sm hidden-mobile>\n <ion-text\n [innerHTML]=\"'HOME.NOT_THIS_ACCOUNT_QUESTION' | translate: { displayName: accountName }\"\n ></ion-text>\n <br />\n <ion-text>\n <a href=\"#\" (click)=\"logout($event)\">\n <span translate>HOME.BTN_DISCONNECT</span>\n </a>\n </ion-text>\n </p>\n </ng-template>\n </div>\n </ion-card-content>\n </ion-card>\n </ion-col>\n <ion-col size=\"0\" [sizeXl]=\"hasFeed ? 1 : 0\"></ion-col>\n <ion-col [class.feed]=\"hasFeed\" [size]=\"hasFeed ? 12 : 0\" [sizeXl]=\"hasFeed ? 4 : 0\">\n @if (showFeed) {\n @if (mobile) {\n <app-feed\n #feed\n shape=\"round\"\n [urls]=\"feedUrls\"\n [maxAgeInMonths]=\"feedMaxAgeInMonths\"\n [maxContentLength]=\"feedMaxContentLength\"\n (ngInit)=\"initFeed(feed)\"\n @fadeInAnimation\n ></app-feed>\n } @else {\n <app-feed\n #feed\n shape=\"round\"\n [debug]=\"false\"\n [urls]=\"feedUrls\"\n [maxAgeInMonths]=\"feedMaxAgeInMonths\"\n [maxContentLength]=\"feedMaxContentLength\"\n (ngInit)=\"initFeed(feed)\"\n @slideInAnimation\n @fadeInAnimation\n ></app-feed>\n }\n }\n </ion-col>\n </ion-row>\n </ion-grid>\n\n <!-- Page history -->\n @let pageHistory = isLogin && (pageHistory$ | push);\n @if (pageHistory | isNotEmptyArray) {\n <ion-grid class=\"history-container ion-align-self-center\">\n <ion-row>\n @for (page of pageHistory; track page.path) {\n <ion-col size=\"12\" size-xl=\"\" class=\"ion-text-center\">\n <ion-card class=\"ion-align-self-start ion-text-start\" @fadeInAnimation>\n <ion-card-header class=\"ion-no-padding\">\n <!-- top bar -->\n <ion-card-subtitle>\n <button\n type=\"button\"\n tabindex=\"-1\"\n (click)=\"removePageHistory(page.path)\"\n mat-icon-button\n class=\"ion-float-start ion-no-margin close-button\"\n >\n <mat-icon>close</mat-icon>\n </button>\n <ion-label [innerHTML]=\"page.subtitle | translate\"></ion-label>\n <ion-text class=\"ion-float-end\" [title]=\"page.time | dateFormat: { time: true }\">\n <small>\n <ion-icon name=\"time-outline\"></ion-icon>\n &nbsp;{{ page.time | dateFromNow }}\n </small>\n &nbsp;\n </ion-text>\n </ion-card-subtitle>\n\n <!-- main page -->\n <ion-card-title class=\"ion-no-margin ion-no-padding\">\n <ion-item\n detail=\"true\"\n tappable\n class=\"text-1x\"\n [routerLink]=\"page.path\"\n routerDirection=\"root\"\n lines=\"none\"\n >\n <!-- page icon-->\n <ion-icon *ngIf=\"page.icon\" slot=\"start\" [name]=\"page.icon\"></ion-icon>\n <mat-icon *ngIf=\"page.matIcon\" slot=\"start\">{{ page.matIcon }}</mat-icon>\n\n <ion-label color=\"primary\" [innerHTML]=\"page.title\"></ion-label>\n </ion-item>\n </ion-card-title>\n </ion-card-header>\n\n <!-- children pages -->\n @if (page.children | isNotEmptyArray) {\n <ion-card-content class=\"ion-no-padding ion-padding-start\">\n <ion-list class=\"ion-no-padding\">\n @for (childPage of page.children; track childPage.path) {\n <ion-item\n detail=\"true\"\n tappable\n class=\"text-1x\"\n [routerLink]=\"childPage.path\"\n routerDirection=\"root\"\n >\n <!-- page icon-->\n <ion-icon *ngIf=\"childPage.icon\" slot=\"start\" color=\"dark\" [name]=\"childPage.icon\"></ion-icon>\n <mat-icon *ngIf=\"childPage.matIcon\" slot=\"start\" color=\"dark\">\n {{ childPage.matIcon }}\n </mat-icon>\n\n <ion-label color=\"dark\" [innerHTML]=\"childPage.title\"></ion-label>\n </ion-item>\n }\n </ion-list>\n </ion-card-content>\n }\n </ion-card>\n </ion-col>\n }\n </ion-row>\n </ion-grid>\n }\n\n <!-- Bottom banner -->\n @if (showPartnerBanner || showLegalInformation) {\n <ion-grid\n class=\"bottom-banner ion-text-center\"\n [class.floating]=\"!mobile && (pageHistory | isEmptyArray)\"\n @fadeInAnimation\n >\n <ion-row>\n <!-- partners logos -->\n @if (showPartnerBanner) {\n <ion-col size=\"12\" [sizeLg]=\"partnerBannerSize\" class=\"ion-text-center\">\n @if (showLegalInformation) {\n <ion-list class=\"legal-info\">\n <ion-list-header>{{ 'HOME.PARTNERS' | translate }}</ion-list-header>\n </ion-list>\n }\n <!-- partners -->\n @for (item of $partners | async; track $index) {\n <a href=\"{{ item.siteUrl }}\">\n <img class=\"logo\" src=\"{{ item.logo }}\" alt=\"{{ item.label }}\" [title]=\"item.label\" />\n </a>\n }\n </ion-col>\n }\n\n <!-- legal information -->\n @if (showLegalInformation) {\n <ion-col size=\"12\" size-sm=\"6\" size-lg=\"4\" [class.left-border]=\"showPartnerBanner\">\n <ion-list class=\"legal-info\">\n <ion-list-header class=\"ion-text-left\">{{ 'HOME.LEGAL_INFORMATION' | translate }}</ion-list-header>\n @for (item of legalInformation; track item.path) {\n <ion-item (click)=\"openMarkdownModal($event, item)\" lines=\"none\" [button]=\"true\">\n <ion-label>\n <p>\n {{ item.title | translate: item.titleArgs }}\n </p>\n </ion-label>\n </ion-item>\n }\n </ion-list>\n </ion-col>\n @if (showInstallLinks) {\n <ion-col size=\"12\" size-lg=\"3\" offset-lg=\"9\" [class.left-border]=\"showPartnerBanner\">\n <ion-list class=\"legal-info\">\n <ion-list-header>{{ 'HOME.INSTALL_APP' | translate }}</ion-list-header>\n <!-- TODO -->\n </ion-list>\n </ion-col>\n }\n }\n </ion-row>\n </ion-grid>\n }\n }\n\n <!-- image credits -->\n @if (contentCredits | isNotNilOrBlank) {\n <div class=\"content-credits\" [class.floating]=\"!mobile\">\n <ion-text>{{ contentCredits }}</ion-text>\n </div>\n }\n</ion-content>\n", styles: [":host{--ion-card-background-color: rgba(var(--ion-background-color-rgb), .7);--scrollbar-width: 0;--bottom-banner-height: 122px}h1,h2,h3,h4,h5{white-space:normal}.center,ion-content ion-card.welcome img{text-align:center;display:inline-block}ion-content{--background: transparent;background-size:cover;-webkit-background-size:cover;-moz-background-size:cover;-o-background-size:cover;background-color:var(--ion-background-color);display:inline-block;padding:15px}ion-content.has-scrollbar{--scrollbar-width: 8px}ion-content .loading-page{display:block;height:100%;width:100%;position:absolute;background-color:transparent;z-index:5;transition:background .5s linear;-webkit-transition:background 1.5s linear}ion-content .loading-page.hidden{background-color:rgba(var(--ion-color-primary),0)}ion-content ion-card{z-index:9;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}ion-content ion-card ion-card-header{display:block}ion-content ion-card ion-card-header ion-card-subtitle,ion-content ion-card ion-card-header ion-card-title{display:block;width:100%}ion-content ion-card.main{min-width:240px;max-width:400px;margin-left:auto;margin-right:auto}ion-content ion-card.welcome{background-color:var(--ion-card-background-color)}ion-content ion-card.welcome button{margin-top:16px}ion-content ion-card.welcome img{max-width:250px}ion-content .history-container{margin-left:auto;margin-right:auto}ion-content .history-container ion-card{display:inline-block;box-sizing:border-box;z-index:8;min-width:240px;max-width:400px;width:100%;margin-left:auto;margin-right:auto;background-color:var(--ion-card-background-color);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}ion-content .history-container ion-card ion-card-header ion-card-subtitle{height:40px}ion-content .history-container ion-card ion-card-header ion-card-subtitle button[float-start]{--mdc-icon-button-state-layer-size: 40px;padding:8px;z-index:99;margin:0}ion-content .history-container ion-card ion-card-header ion-card-subtitle ion-label{line-height:40px}ion-content .history-container ion-card ion-card-header ion-card-subtitle ion-label[float-end]{padding-right:16px;font-weight:400}ion-content .history-container ion-card ion-card-header ion-card-title ion-item{width:100%;--ion-item-background: $transparent;--ion-item-icon-color: var(--ion-color-primary);--ion-item-text-color: var(--ion-color-primary)}ion-content .history-container ion-card ion-card-header ion-card-title ion-item ion-icon[slot=start],ion-content .history-container ion-card ion-card-header ion-card-title ion-item mat-icon[slot=start]{margin-right:unset;margin-inline-end:16px!important;color:var(--ion-item-icon-color)!important;fill:currentColor;stroke:currentColor}ion-content .history-container ion-card ion-card-header ion-card-title ion-item ion-text,ion-content .history-container ion-card ion-card-header ion-card-title ion-item ion-label{color:var(--ion-item-text-color)!important;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;-ms-text-size-adjust:100%;text-size-adjust:100%}ion-content .history-container ion-card ion-card-content ion-list{--ion-item-background: var(--ion-color-transparent)}ion-content .history-container ion-card ion-card-content ion-list ion-item{--ion-item-icon-color: var(--ion-color-dark);--ion-item-text-color: var(--ion-color-dark)}ion-content .history-container ion-card ion-card-content ion-list ion-item ion-icon[slot=start],ion-content .history-container ion-card ion-card-content ion-list ion-item mat-icon[slot=start]{color:var(--ion-item-icon-color)!important;fill:currentColor;stroke:currentColor;margin-right:unset;margin-inline-end:16px!important}ion-content .history-container ion-card ion-card-content ion-list ion-item ion-text,ion-content .history-container ion-card ion-card-content ion-list ion-item ion-label{color:var(--ion-item-text-color)!important}ion-content .bottom-banner{--ion-card-background-color: rgba(var(--ion-background-color-rgb), .5);background-color:var(--ion-card-background-color);min-width:240px;z-index:0;border-radius:4px;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);margin:16px}ion-content .bottom-banner img.logo{max-height:50px;margin-left:8px}ion-content .bottom-banner ion-list-header{min-height:30px}ion-content .bottom-banner ion-item{--min-height: 28px}ion-content .bottom-banner ion-item ion-label{margin-top:3px!important;margin-bottom:3px!important}ion-content ion-text{text-align:center}@media screen and (max-width: 575px){ion-content ion-card{min-width:160px;width:calc(100% - 20px);max-width:100%;margin-left:10px;margin-right:10px}ion-content ion-card h1{margin-top:0;font-size:20px}ion-content ion-card ion-card-content ion-button{margin-top:8px}ion-content .bottom-banner.floating{display:block;position:unset}ion-content .bottom-banner img.logo{max-height:30px}}@media screen and (max-width: 767px) and (min-width: 576px){ion-content{min-height:555px}ion-content .bottom-banner img.logo{max-height:40px}}@media screen and (min-width: 768px){ion-content{min-height:calc(100% - var(--bottom-banner-height))}}@media screen and (min-width: 1200px){ion-content ion-col.feed{height:fit-content;max-height:calc(100vh - var(--ion-toolbar-height) - var(--bottom-banner-height) - var(--ion-padding) * 2);padding-inline-end:calc(var(--ion-padding) - 10px);overflow:hidden}ion-content .bottom-banner.floating{position:fixed;left:0;right:0;bottom:0;overflow-y:auto;max-height:var(--bottom-banner-height);margin-top:0}}.content-credits{float:right}.content-credits.floating{float:unset;position:fixed;bottom:0;right:var(--scrollbar-width, 0)}.content-credits ion-text{padding-inline-start:4px;padding-inline-end:4px;font-size:.7rem;background-color:rgba(var(--ion-background-color-rgb),.5);color:rgba(var(--ion-text-color-rgb),.7)}@media screen and (min-width: 992px){.left-border{border-left:1px solid var(--ion-color-medium)}}\n"], dependencies: [{ kind: "directive", type: i3$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3$1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i2$1.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i2$1.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$1.IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$1.IonCardContent, selector: "ion-card-content", inputs: ["mode"] }, { kind: "component", type: i2$1.IonCardHeader, selector: "ion-card-header", inputs: ["color", "mode", "translucent"] }, { kind: "component", type: i2$1.IonCardSubtitle, selector: "ion-card-subtitle", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonCardTitle, selector: "ion-card-title", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i2$1.IonContent, selector: "ion-content", inputs: ["color", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$1.IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: i2$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$1.IonItem, selector: "ion-item", inputs: ["button", "color", "counter", "counterFormatter", "detail", "detailIcon", "disabled", "download", "fill", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "shape", "target", "type"] }, { kind: "component", type: i2$1.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$1.IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: i2$1.IonListHeader, selector: "ion-list-header", inputs: ["color", "lines", "mode"] }, { kind: "component", type: i2$1.IonMenuToggle, selector: "ion-menu-toggle", inputs: ["autoHide", "menu"] }, { kind: "component", type: i2$1.IonRow, selector: "ion-row" }, { kind: "component", type: i2$1.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "directive", type: i2$1.RouterLinkDelegate, selector: ":not(a):not(area)[routerLink]" }, { kind: "directive", type: i1$1.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { kind: "directive", type: i8$2.RxIf, selector: "[rxIf]", inputs: ["rxIf", "rxIfStrategy", "rxIfElse", "rxIfThen", "rxIfSuspense", "rxIfComplete", "rxIfError", "rxIfContextTrigger", "rxIfNextTrigger", "rxIfSuspenseTrigger", "rxIfErrorTrigger", "rxIfCompleteTrigger", "rxIfParent", "rxIfPatchZone", "rxIfRenderCallback"] }, { kind: "component", type: i6$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i9.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "component", type: i8$1.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i8$1.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i8$1.MatMenuContent, selector: "ng-template[matMenuContent]" }, { kind: "directive", type: i8$1.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "directive", type: NgInitDirective, selector: "[ngInit]", outputs: ["ngInit"] }, { kind: "component", type: ToolbarComponent, selector: "app-toolbar", inputs: ["progressBarMode", "title", "color", "class", "id", "backHref", "defaultBackHref", "hasValidate", "hasClose", "hasSearch", "canGoBack", "canShowMenu"], outputs: ["onValidate", "onClose", "onValidateAndClose", "onBackClick", "onSearch"] }, { kind: "directive", type: i1$6.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: AppInstallUpgradeCard, selector: "app-install-upgrade-card", inputs: ["isLogin", "showUpgradeWarning", "showOfflineWarning", "showInstallButton", "debug"] }, { kind: "component", type: UserEventNotificationIcon, selector: "app-user-event-notification-icon", inputs: ["titleI18n", "disabled", "filter", "autoHide", "headerActions", "footerActions", "style", "color", "unreadColor", "fill", "mobile", "listStyle", "debug"], outputs: ["showList"] }, { kind: "component", type: FeedsComponent, selector: "app-feed", inputs: ["debug", "mobile", "showHeader", "showReadMoreButton", "headerColor", "cardColor", "shape", "class", "itemId", "filterItem", "feeds", "urls", "maxAgeInMonths", "maxContentLength"], outputs: ["editItem", "deleteItem"] }, { kind: "pipe", type: i3$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }, { kind: "pipe", type: i19.RxPush, name: "push" }, { kind: "pipe", type: DateFormatPipe, name: "dateFormat" }, { kind: "pipe", type: DateFromNowPipe, name: "dateFromNow" }, { kind: "pipe", type: NotEmptyArrayPipe, name: "isNotEmptyArray" }, { kind: "pipe", type: EmptyArrayPipe, name: "isEmptyArray" }, { kind: "pipe", type: IsNotNilOrBlankPipe, name: "isNotNilOrBlank" }], animations: [fadeInAnimation, slideUpDownAnimation, slideInAnimation], changeDetection: i0.ChangeDetectionStrategy.OnPush });
34647
35507
  }
34648
35508
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: HomePage, decorators: [{
34649
35509
  type: Component,
34650
- args: [{ selector: 'app-page-home', changeDetection: ChangeDetectionStrategy.OnPush, animations: [fadeInAnimation, slideUpDownAnimation, slideInAnimation], template: "<app-toolbar [canGoBack]=\"false\" visible-xs visible-sm visible-mobile>\n <ion-buttons slot=\"end\">\n @if (showNotificationIcon) {\n <app-user-event-notification-icon [autoHide]=\"autoHideNotificationIcon\"></app-user-event-notification-icon>\n }\n\n <!-- dark mode (if allow by the environment token) -->\n @if (settings.allowDarkMode) {\n <ion-button (click)=\"toggleDarkMode()\">\n <ion-icon slot=\"icon-only\" [name]=\"(darkMode$ | push) ? 'sunny' : 'moon'\"></ion-icon>\n </ion-button>\n }\n\n <!-- Change locale button -->\n <ion-button [matMenuTriggerFor]=\"localeMenu\">\n <ion-icon slot=\"start\" name=\"language\"></ion-icon>\n </ion-button>\n </ion-buttons>\n</app-toolbar>\n\n<!-- Change locale menu -->\n<mat-menu #localeMenu=\"matMenu\">\n <ng-template matMenuContent>\n @for (item of locales; track item.key) {\n <button mat-menu-item (click)=\"changeLanguage(item.key)\">\n <!--<mat-icon>{{ currentLocale === item.key ? 'checkmark' : undefined }}</mat-icon>-->\n <ion-label>{{ item.value }}</ion-label>\n </button>\n }\n </ng-template>\n</mat-menu>\n\n<ion-content [ngStyle]=\"contentStyle\" [class.has-scrollbar]=\"hasScrollbar\" no-padding-xs>\n <!-- loading spinner -->\n <div class=\"loading-page\" [class.cdk-visually-hidden]=\"!(loading$ | push)\">\n @if (showSpinner) {\n <div class=\"spinner\">\n <p [innerHTML]=\"'COMMON.LOADING_DOTS' | translate\"></p>\n <div class=\"sk-cube1 sk-cube\"></div>\n <div class=\"sk-cube2 sk-cube\"></div>\n <div class=\"sk-cube4 sk-cube\"></div>\n <div class=\"sk-cube3 sk-cube\"></div>\n </div>\n }\n </div>\n\n <!-- Desktop: translucent top toolbar -->\n @if (!mobile) {\n <div class=\"hidden-xs hidden-sm\">\n <ion-toolbar color=\"traansparent\" translucent>\n <ion-buttons slot=\"start\">\n <ion-menu-toggle>\n <ion-button color=\"light\" fill=\"clear\">\n <ion-icon slot=\"icon-only\" name=\"menu\"></ion-icon>\n </ion-button>\n </ion-menu-toggle>\n </ion-buttons>\n\n <ion-buttons slot=\"end\">\n @if (showNotificationIcon) {\n <app-user-event-notification-icon\n [style]=\"'ion-button'\"\n color=\"secondary\"\n unreadColor=\"tertiary\"\n fill=\"solid\"\n [autoHide]=\"autoHideNotificationIcon\"\n ></app-user-event-notification-icon>\n }\n\n <!-- dark mode (if allow by the environment token) -->\n @if (settings.allowDarkMode) {\n <ion-button\n color=\"secondary\"\n fill=\"solid\"\n (click)=\"toggleDarkMode()\"\n [title]=\"'HOME.BTN_DARK_MODE' | translate\"\n >\n <ion-icon slot=\"icon-only\" [name]=\"(darkMode$ | push) ? 'moon' : 'sunny'\"></ion-icon>\n </ion-button>\n }\n <!-- change locale button -->\n <ion-button\n color=\"secondary\"\n fill=\"solid\"\n [matMenuTriggerFor]=\"localeMenu\"\n [title]=\"'SETTINGS.LOCALE' | translate\"\n >\n <ion-icon slot=\"icon-only\" name=\"language\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </div>\n }\n\n <!-- Install (and upgrade) card -->\n <app-install-upgrade-card [isLogin]=\"isLogin\" [showInstallButton]=\"true\"></app-install-upgrade-card>\n\n @if (!(loading$ | push)) {\n @let hasFeed = hasFeed$ | async;\n <ion-grid class=\"ion-no-padding\">\n <ion-row>\n <ion-col size=\"0\" [sizeXl]=\"hasFeed ? 3 : 0\"></ion-col>\n <ion-col>\n <!-- Welcome card -->\n <ion-card class=\"main welcome ion-padding ion-text-center ion-align-self-center\" @fadeInAnimation>\n <ion-card-header>\n <ion-card-title class=\"ion-text-center\">\n <span *ngIf=\"isWeb\" [innerHTML]=\"'HOME.WELCOME_WEB' | translate: { appName: appName }\"></span>\n <span *ngIf=\"!isWeb\" [innerHTML]=\"'HOME.WELCOME_APP' | translate: { appName: appName }\"></span>\n </ion-card-title>\n <ion-card-subtitle [innerHTML]=\"description\"></ion-card-subtitle>\n </ion-card-header>\n <ion-card-content class=\"ion-no-padding\">\n <ion-text color=\"primary\">\n <img *ngIf=\"logo\" [attr.src]=\"logo\" alt=\"Logo\" />\n </ion-text>\n <!-- register help text -->\n <ion-text *ngIf=\"!isLogin && canRegister\">\n <br />\n <span translate>HOME.REGISTER_HELP</span>\n </ion-text>\n <div class=\"ion-padding-top\">\n <!-- If NOT login -->\n <ng-container *ngIf=\"!isLogin; else loginButtons\">\n <ion-button *ngIf=\"canRegister\" expand=\"block\" color=\"tertiary\" (click)=\"register()\">\n <span translate>HOME.BTN_REGISTER</span>\n </ion-button>\n <ion-button expand=\"block\" color=\"light\" [routerLink]=\"['/']\" (click)=\"login()\">\n <span translate>AUTH.BTN_LOGIN</span>\n </ion-button>\n </ng-container>\n\n <!-- If user login -->\n <ng-template #loginButtons>\n <!-- Feature buttons -->\n <ng-container *rxIf=\"$filteredButtons; let buttons\">\n <ng-container *ngFor=\"let item of buttons\">\n <ion-button\n *ngIf=\"item.path\"\n expand=\"block\"\n color=\"tertiary\"\n [class]=\"item.cssClass\"\n [routerLink]=\"item.path\"\n routerDirection=\"root\"\n >\n <ion-icon slot=\"start\" class=\"ion-float-start\" *ngIf=\"item.icon\" [name]=\"item.icon\"></ion-icon>\n <mat-icon slot=\"start\" class=\"ion-float-start\" *ngIf=\"item.matIcon\">\n {{ item.matIcon }}\n </mat-icon>\n <ion-text>{{ 'HOME.BTN_DATA_ENTRY' | translate: { name: (item.title | translate) } }}</ion-text>\n </ion-button>\n\n <!-- divider -->\n <div *ngIf=\"!item.path && !item.action\" [class]=\"item.cssClass\">\n <ion-label translate>{{ item.title }}&nbsp;</ion-label>\n </div>\n </ng-container>\n\n <!--<p *ngIf=\"buttons.length\" class=\"visible-mobile\">&nbsp;</p>-->\n </ng-container>\n\n <ion-button *ngIf=\"showAccountButton\" expand=\"block\" color=\"secondary\" [routerLink]=\"['/account']\">\n <ion-icon slot=\"start\" class=\"ion-float-start\" name=\"person-circle\"></ion-icon>\n <ion-text translate>HOME.BTN_MY_ACCOUNT</ion-text>\n </ion-button>\n\n <p hidden-xs hidden-sm hidden-mobile>\n <ion-text\n [innerHTML]=\"'HOME.NOT_THIS_ACCOUNT_QUESTION' | translate: { displayName: accountName }\"\n ></ion-text>\n <br />\n <ion-text>\n <a href=\"#\" (click)=\"logout($event)\">\n <span translate>HOME.BTN_DISCONNECT</span>\n </a>\n </ion-text>\n </p>\n </ng-template>\n </div>\n </ion-card-content>\n </ion-card>\n </ion-col>\n <ion-col size=\"0\" [sizeXl]=\"hasFeed ? 1 : 0\"></ion-col>\n <ion-col [class.feed]=\"hasFeed\" [size]=\"hasFeed ? 12 : 0\" [sizeXl]=\"hasFeed ? 4 : 0\">\n @if (showFeed) {\n @if (mobile) {\n <app-feed\n #feed\n shape=\"round\"\n [feedUrls]=\"feedUrls\"\n [maxAgeInMonths]=\"feedMaxAgeInMonths\"\n [maxContentLength]=\"feedMaxContentLength\"\n (ngInit)=\"initFeed(feed)\"\n @fadeInAnimation\n ></app-feed>\n } @else {\n <app-feed\n #feed\n shape=\"round\"\n [debug]=\"false\"\n [feedUrls]=\"feedUrls\"\n [maxAgeInMonths]=\"feedMaxAgeInMonths\"\n [maxContentLength]=\"feedMaxContentLength\"\n (ngInit)=\"initFeed(feed)\"\n @slideInAnimation\n @fadeInAnimation\n ></app-feed>\n }\n }\n </ion-col>\n </ion-row>\n </ion-grid>\n\n <!-- Page history -->\n @let pageHistory = isLogin && (pageHistory$ | push);\n @if (pageHistory | isNotEmptyArray) {\n <ion-grid class=\"history-container ion-align-self-center\">\n <ion-row>\n @for (page of pageHistory; track page.path) {\n <ion-col size=\"12\" size-xl=\"\" class=\"ion-text-center\">\n <ion-card class=\"ion-align-self-start ion-text-start\" @fadeInAnimation>\n <ion-card-header class=\"ion-no-padding\">\n <!-- top bar -->\n <ion-card-subtitle>\n <button\n type=\"button\"\n tabindex=\"-1\"\n (click)=\"removePageHistory(page.path)\"\n mat-icon-button\n class=\"ion-float-start ion-no-margin close-button\"\n >\n <mat-icon>close</mat-icon>\n </button>\n <ion-label [innerHTML]=\"page.subtitle | translate\"></ion-label>\n <ion-text class=\"ion-float-end\" [title]=\"page.time | dateFormat: { time: true }\">\n <small>\n <ion-icon name=\"time-outline\"></ion-icon>\n &nbsp;{{ page.time | dateFromNow }}\n </small>\n &nbsp;\n </ion-text>\n </ion-card-subtitle>\n\n <!-- main page -->\n <ion-card-title class=\"ion-no-margin ion-no-padding\">\n <ion-item\n detail=\"true\"\n tappable\n class=\"text-1x\"\n [routerLink]=\"page.path\"\n routerDirection=\"root\"\n lines=\"none\"\n >\n <!-- page icon-->\n <ion-icon *ngIf=\"page.icon\" slot=\"start\" [name]=\"page.icon\"></ion-icon>\n <mat-icon *ngIf=\"page.matIcon\" slot=\"start\">{{ page.matIcon }}</mat-icon>\n\n <ion-label color=\"primary\" [innerHTML]=\"page.title\"></ion-label>\n </ion-item>\n </ion-card-title>\n </ion-card-header>\n\n <!-- children pages -->\n @if (page.children | isNotEmptyArray) {\n <ion-card-content class=\"ion-no-padding ion-padding-start\">\n <ion-list class=\"ion-no-padding\">\n @for (childPage of page.children; track childPage.path) {\n <ion-item\n detail=\"true\"\n tappable\n class=\"text-1x\"\n [routerLink]=\"childPage.path\"\n routerDirection=\"root\"\n >\n <!-- page icon-->\n <ion-icon *ngIf=\"childPage.icon\" slot=\"start\" color=\"dark\" [name]=\"childPage.icon\"></ion-icon>\n <mat-icon *ngIf=\"childPage.matIcon\" slot=\"start\" color=\"dark\">\n {{ childPage.matIcon }}\n </mat-icon>\n\n <ion-label color=\"dark\" [innerHTML]=\"childPage.title\"></ion-label>\n </ion-item>\n }\n </ion-list>\n </ion-card-content>\n }\n </ion-card>\n </ion-col>\n }\n </ion-row>\n </ion-grid>\n }\n\n <!-- Bottom banner -->\n @if (showPartnerBanner || showLegalInformation) {\n <ion-grid\n class=\"bottom-banner ion-text-center\"\n [class.floating]=\"!mobile && (pageHistory | isEmptyArray)\"\n @fadeInAnimation\n >\n <ion-row>\n <!-- partners logos -->\n @if (showPartnerBanner) {\n <ion-col size=\"12\" [sizeLg]=\"partnerBannerSize\" class=\"ion-text-center\">\n @if (showLegalInformation) {\n <ion-list class=\"legal-info\">\n <ion-list-header>{{ 'HOME.PARTNERS' | translate }}</ion-list-header>\n </ion-list>\n }\n <!-- partners -->\n @for (item of $partners | async; track $index) {\n <a href=\"{{ item.siteUrl }}\">\n <img class=\"logo\" src=\"{{ item.logo }}\" alt=\"{{ item.label }}\" [title]=\"item.label\" />\n </a>\n }\n </ion-col>\n }\n\n <!-- legal information -->\n @if (showLegalInformation) {\n <ion-col size=\"12\" size-sm=\"6\" size-lg=\"4\" [class.left-border]=\"showPartnerBanner\">\n <ion-list class=\"legal-info\">\n <ion-list-header class=\"ion-text-left\">{{ 'HOME.LEGAL_INFORMATION' | translate }}</ion-list-header>\n @for (item of legalInformation; track item.path) {\n <ion-item (click)=\"openMarkdownModal($event, item)\" lines=\"none\" [button]=\"true\">\n <ion-label>\n <p>\n {{ item.title | translate: item.titleArgs }}\n </p>\n </ion-label>\n </ion-item>\n }\n </ion-list>\n </ion-col>\n @if (showInstallLinks) {\n <ion-col size=\"12\" size-lg=\"3\" offset-lg=\"9\" [class.left-border]=\"showPartnerBanner\">\n <ion-list class=\"legal-info\">\n <ion-list-header>{{ 'HOME.INSTALL_APP' | translate }}</ion-list-header>\n <!-- TODO -->\n </ion-list>\n </ion-col>\n }\n }\n </ion-row>\n </ion-grid>\n }\n }\n\n <!-- image credits -->\n @if (contentCredits | isNotNilOrBlank) {\n <div class=\"content-credits\" [class.floating]=\"!mobile\">\n <ion-text>{{ contentCredits }}</ion-text>\n </div>\n }\n</ion-content>\n", styles: [":host{--ion-card-background-color: rgba(var(--ion-background-color-rgb), .7);--scrollbar-width: 0;--bottom-banner-height: 122px}h1,h2,h3,h4,h5{white-space:normal}.center,ion-content ion-card.welcome img{text-align:center;display:inline-block}ion-content{--background: transparent;background-size:cover;-webkit-background-size:cover;-moz-background-size:cover;-o-background-size:cover;background-color:var(--ion-background-color);display:inline-block;padding:15px}ion-content.has-scrollbar{--scrollbar-width: 8px}ion-content .loading-page{display:block;height:100%;width:100%;position:absolute;background-color:transparent;z-index:5;transition:background .5s linear;-webkit-transition:background 1.5s linear}ion-content .loading-page.hidden{background-color:rgba(var(--ion-color-primary),0)}ion-content ion-card{z-index:9;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}ion-content ion-card ion-card-header{display:block}ion-content ion-card ion-card-header ion-card-subtitle,ion-content ion-card ion-card-header ion-card-title{display:block;width:100%}ion-content ion-card.main{min-width:240px;max-width:400px;margin-left:auto;margin-right:auto}ion-content ion-card.welcome{background-color:var(--ion-card-background-color)}ion-content ion-card.welcome button{margin-top:16px}ion-content ion-card.welcome img{max-width:250px}ion-content .history-container{margin-left:auto;margin-right:auto}ion-content .history-container ion-card{display:inline-block;box-sizing:border-box;z-index:8;min-width:240px;max-width:400px;width:100%;margin-left:auto;margin-right:auto;background-color:var(--ion-card-background-color);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}ion-content .history-container ion-card ion-card-header ion-card-subtitle{height:40px}ion-content .history-container ion-card ion-card-header ion-card-subtitle button[float-start]{--mdc-icon-button-state-layer-size: 40px;padding:8px;z-index:99;margin:0}ion-content .history-container ion-card ion-card-header ion-card-subtitle ion-label{line-height:40px}ion-content .history-container ion-card ion-card-header ion-card-subtitle ion-label[float-end]{padding-right:16px;font-weight:400}ion-content .history-container ion-card ion-card-header ion-card-title ion-item{width:100%;--ion-item-background: $transparent;--ion-item-icon-color: var(--ion-color-primary);--ion-item-text-color: var(--ion-color-primary)}ion-content .history-container ion-card ion-card-header ion-card-title ion-item ion-icon[slot=start],ion-content .history-container ion-card ion-card-header ion-card-title ion-item mat-icon[slot=start]{margin-right:unset;margin-inline-end:16px!important;color:var(--ion-item-icon-color)!important;fill:currentColor;stroke:currentColor}ion-content .history-container ion-card ion-card-header ion-card-title ion-item ion-text,ion-content .history-container ion-card ion-card-header ion-card-title ion-item ion-label{color:var(--ion-item-text-color)!important;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;-ms-text-size-adjust:100%;text-size-adjust:100%}ion-content .history-container ion-card ion-card-content ion-list{--ion-item-background: var(--ion-color-transparent)}ion-content .history-container ion-card ion-card-content ion-list ion-item{--ion-item-icon-color: var(--ion-color-dark);--ion-item-text-color: var(--ion-color-dark)}ion-content .history-container ion-card ion-card-content ion-list ion-item ion-icon[slot=start],ion-content .history-container ion-card ion-card-content ion-list ion-item mat-icon[slot=start]{color:var(--ion-item-icon-color)!important;fill:currentColor;stroke:currentColor;margin-right:unset;margin-inline-end:16px!important}ion-content .history-container ion-card ion-card-content ion-list ion-item ion-text,ion-content .history-container ion-card ion-card-content ion-list ion-item ion-label{color:var(--ion-item-text-color)!important}ion-content .bottom-banner{--ion-card-background-color: rgba(var(--ion-background-color-rgb), .5);background-color:var(--ion-card-background-color);min-width:240px;z-index:0;border-radius:4px;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);margin:16px}ion-content .bottom-banner img.logo{max-height:50px;margin-left:8px}ion-content .bottom-banner ion-list-header{min-height:30px}ion-content .bottom-banner ion-item{--min-height: 28px}ion-content .bottom-banner ion-item ion-label{margin-top:3px!important;margin-bottom:3px!important}ion-content ion-text{text-align:center}@media screen and (max-width: 575px){ion-content ion-card{min-width:160px;width:calc(100% - 20px);max-width:100%;margin-left:10px;margin-right:10px}ion-content ion-card h1{margin-top:0;font-size:20px}ion-content ion-card ion-card-content ion-button{margin-top:8px}ion-content .bottom-banner.floating{display:block;position:unset}ion-content .bottom-banner img.logo{max-height:30px}}@media screen and (max-width: 767px) and (min-width: 576px){ion-content{min-height:555px}ion-content .bottom-banner img.logo{max-height:40px}}@media screen and (min-width: 768px){ion-content{min-height:calc(100% - var(--bottom-banner-height))}}@media screen and (min-width: 1200px){ion-content ion-col.feed{height:fit-content;max-height:calc(100vh - var(--ion-toolbar-height) - var(--bottom-banner-height) - var(--ion-padding) * 2);padding-inline-end:calc(var(--ion-padding) - 10px);overflow:hidden}ion-content .bottom-banner.floating{position:fixed;left:0;right:0;bottom:0;overflow-y:auto;max-height:var(--bottom-banner-height);margin-top:0}}.content-credits{float:right}.content-credits.floating{float:unset;position:fixed;bottom:0;right:var(--scrollbar-width, 0)}.content-credits ion-text{padding-inline-start:4px;padding-inline-end:4px;font-size:.7rem;background-color:rgba(var(--ion-background-color-rgb),.5);color:rgba(var(--ion-text-color-rgb),.7)}@media screen and (min-width: 992px){.left-border{border-left:1px solid var(--ion-color-medium)}}\n"] }]
35510
+ args: [{ selector: 'app-page-home', changeDetection: ChangeDetectionStrategy.OnPush, animations: [fadeInAnimation, slideUpDownAnimation, slideInAnimation], template: "<app-toolbar [canGoBack]=\"false\" visible-xs visible-sm visible-mobile>\n <ion-buttons slot=\"end\">\n @if (showNotificationIcon) {\n <app-user-event-notification-icon [autoHide]=\"autoHideNotificationIcon\"></app-user-event-notification-icon>\n }\n\n <!-- dark mode (if allow by the environment token) -->\n @if (settings.allowDarkMode) {\n <ion-button (click)=\"toggleDarkMode()\">\n <ion-icon slot=\"icon-only\" [name]=\"(darkMode$ | push) ? 'sunny' : 'moon'\"></ion-icon>\n </ion-button>\n }\n\n <!-- Change locale button -->\n <ion-button [matMenuTriggerFor]=\"localeMenu\">\n <ion-icon slot=\"start\" name=\"language\"></ion-icon>\n </ion-button>\n </ion-buttons>\n</app-toolbar>\n\n<!-- Change locale menu -->\n<mat-menu #localeMenu=\"matMenu\">\n <ng-template matMenuContent>\n @for (item of locales; track item.key) {\n <button mat-menu-item (click)=\"changeLanguage(item.key)\">\n <!--<mat-icon>{{ currentLocale === item.key ? 'checkmark' : undefined }}</mat-icon>-->\n <ion-label>{{ item.value }}</ion-label>\n </button>\n }\n </ng-template>\n</mat-menu>\n\n<ion-content [ngStyle]=\"contentStyle\" [class.has-scrollbar]=\"hasScrollbar\" no-padding-xs>\n <!-- loading spinner -->\n <div class=\"loading-page\" [class.cdk-visually-hidden]=\"!(loading$ | push)\">\n @if (showSpinner) {\n <div class=\"spinner\">\n <p [innerHTML]=\"'COMMON.LOADING_DOTS' | translate\"></p>\n <div class=\"sk-cube1 sk-cube\"></div>\n <div class=\"sk-cube2 sk-cube\"></div>\n <div class=\"sk-cube4 sk-cube\"></div>\n <div class=\"sk-cube3 sk-cube\"></div>\n </div>\n }\n </div>\n\n <!-- Desktop: translucent top toolbar -->\n @if (!mobile) {\n <div class=\"hidden-xs hidden-sm\">\n <ion-toolbar color=\"traansparent\" translucent>\n <ion-buttons slot=\"start\">\n <ion-menu-toggle>\n <ion-button color=\"light\" fill=\"clear\">\n <ion-icon slot=\"icon-only\" name=\"menu\"></ion-icon>\n </ion-button>\n </ion-menu-toggle>\n </ion-buttons>\n\n <ion-buttons slot=\"end\">\n @if (showNotificationIcon) {\n <app-user-event-notification-icon\n [style]=\"'ion-button'\"\n color=\"secondary\"\n unreadColor=\"tertiary\"\n fill=\"solid\"\n [autoHide]=\"autoHideNotificationIcon\"\n ></app-user-event-notification-icon>\n }\n\n <!-- dark mode (if allow by the environment token) -->\n @if (settings.allowDarkMode) {\n <ion-button\n color=\"secondary\"\n fill=\"solid\"\n (click)=\"toggleDarkMode()\"\n [title]=\"'HOME.BTN_DARK_MODE' | translate\"\n >\n <ion-icon slot=\"icon-only\" [name]=\"(darkMode$ | push) ? 'moon' : 'sunny'\"></ion-icon>\n </ion-button>\n }\n <!-- change locale button -->\n <ion-button\n color=\"secondary\"\n fill=\"solid\"\n [matMenuTriggerFor]=\"localeMenu\"\n [title]=\"'SETTINGS.LOCALE' | translate\"\n >\n <ion-icon slot=\"icon-only\" name=\"language\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </div>\n }\n\n <!-- Install (and upgrade) card -->\n <app-install-upgrade-card [isLogin]=\"isLogin\" [showInstallButton]=\"true\"></app-install-upgrade-card>\n\n @if (!(loading$ | push)) {\n @let hasFeed = hasFeed$ | async;\n <ion-grid class=\"ion-no-padding\">\n <ion-row>\n <ion-col size=\"0\" [sizeXl]=\"hasFeed ? 3 : 0\"></ion-col>\n <ion-col>\n <!-- Welcome card -->\n <ion-card class=\"main welcome ion-padding ion-text-center ion-align-self-center\" @fadeInAnimation>\n <ion-card-header>\n <ion-card-title class=\"ion-text-center\">\n <span *ngIf=\"isWeb\" [innerHTML]=\"'HOME.WELCOME_WEB' | translate: { appName: appName }\"></span>\n <span *ngIf=\"!isWeb\" [innerHTML]=\"'HOME.WELCOME_APP' | translate: { appName: appName }\"></span>\n </ion-card-title>\n <ion-card-subtitle [innerHTML]=\"description\"></ion-card-subtitle>\n </ion-card-header>\n <ion-card-content class=\"ion-no-padding\">\n <ion-text color=\"primary\">\n <img *ngIf=\"logo\" [attr.src]=\"logo\" alt=\"Logo\" />\n </ion-text>\n <!-- register help text -->\n <ion-text *ngIf=\"!isLogin && canRegister\">\n <br />\n <span translate>HOME.REGISTER_HELP</span>\n </ion-text>\n <div class=\"ion-padding-top\">\n <!-- If NOT login -->\n <ng-container *ngIf=\"!isLogin; else loginButtons\">\n <ion-button *ngIf=\"canRegister\" expand=\"block\" color=\"tertiary\" (click)=\"register()\">\n <span translate>HOME.BTN_REGISTER</span>\n </ion-button>\n <ion-button expand=\"block\" color=\"light\" [routerLink]=\"['/']\" (click)=\"login()\">\n <span translate>AUTH.BTN_LOGIN</span>\n </ion-button>\n </ng-container>\n\n <!-- If user login -->\n <ng-template #loginButtons>\n <!-- Feature buttons -->\n <ng-container *rxIf=\"$filteredButtons; let buttons\">\n <ng-container *ngFor=\"let item of buttons\">\n <ion-button\n *ngIf=\"item.path\"\n expand=\"block\"\n color=\"tertiary\"\n [class]=\"item.cssClass\"\n [routerLink]=\"item.path\"\n routerDirection=\"root\"\n >\n <ion-icon slot=\"start\" class=\"ion-float-start\" *ngIf=\"item.icon\" [name]=\"item.icon\"></ion-icon>\n <mat-icon slot=\"start\" class=\"ion-float-start\" *ngIf=\"item.matIcon\">\n {{ item.matIcon }}\n </mat-icon>\n <ion-text>{{ 'HOME.BTN_DATA_ENTRY' | translate: { name: (item.title | translate) } }}</ion-text>\n </ion-button>\n\n <!-- divider -->\n <div *ngIf=\"!item.path && !item.action\" [class]=\"item.cssClass\">\n <ion-label translate>{{ item.title }}&nbsp;</ion-label>\n </div>\n </ng-container>\n\n <!--<p *ngIf=\"buttons.length\" class=\"visible-mobile\">&nbsp;</p>-->\n </ng-container>\n\n <ion-button *ngIf=\"showAccountButton\" expand=\"block\" color=\"secondary\" [routerLink]=\"['/account']\">\n <ion-icon slot=\"start\" class=\"ion-float-start\" name=\"person-circle\"></ion-icon>\n <ion-text translate>HOME.BTN_MY_ACCOUNT</ion-text>\n </ion-button>\n\n <p hidden-xs hidden-sm hidden-mobile>\n <ion-text\n [innerHTML]=\"'HOME.NOT_THIS_ACCOUNT_QUESTION' | translate: { displayName: accountName }\"\n ></ion-text>\n <br />\n <ion-text>\n <a href=\"#\" (click)=\"logout($event)\">\n <span translate>HOME.BTN_DISCONNECT</span>\n </a>\n </ion-text>\n </p>\n </ng-template>\n </div>\n </ion-card-content>\n </ion-card>\n </ion-col>\n <ion-col size=\"0\" [sizeXl]=\"hasFeed ? 1 : 0\"></ion-col>\n <ion-col [class.feed]=\"hasFeed\" [size]=\"hasFeed ? 12 : 0\" [sizeXl]=\"hasFeed ? 4 : 0\">\n @if (showFeed) {\n @if (mobile) {\n <app-feed\n #feed\n shape=\"round\"\n [urls]=\"feedUrls\"\n [maxAgeInMonths]=\"feedMaxAgeInMonths\"\n [maxContentLength]=\"feedMaxContentLength\"\n (ngInit)=\"initFeed(feed)\"\n @fadeInAnimation\n ></app-feed>\n } @else {\n <app-feed\n #feed\n shape=\"round\"\n [debug]=\"false\"\n [urls]=\"feedUrls\"\n [maxAgeInMonths]=\"feedMaxAgeInMonths\"\n [maxContentLength]=\"feedMaxContentLength\"\n (ngInit)=\"initFeed(feed)\"\n @slideInAnimation\n @fadeInAnimation\n ></app-feed>\n }\n }\n </ion-col>\n </ion-row>\n </ion-grid>\n\n <!-- Page history -->\n @let pageHistory = isLogin && (pageHistory$ | push);\n @if (pageHistory | isNotEmptyArray) {\n <ion-grid class=\"history-container ion-align-self-center\">\n <ion-row>\n @for (page of pageHistory; track page.path) {\n <ion-col size=\"12\" size-xl=\"\" class=\"ion-text-center\">\n <ion-card class=\"ion-align-self-start ion-text-start\" @fadeInAnimation>\n <ion-card-header class=\"ion-no-padding\">\n <!-- top bar -->\n <ion-card-subtitle>\n <button\n type=\"button\"\n tabindex=\"-1\"\n (click)=\"removePageHistory(page.path)\"\n mat-icon-button\n class=\"ion-float-start ion-no-margin close-button\"\n >\n <mat-icon>close</mat-icon>\n </button>\n <ion-label [innerHTML]=\"page.subtitle | translate\"></ion-label>\n <ion-text class=\"ion-float-end\" [title]=\"page.time | dateFormat: { time: true }\">\n <small>\n <ion-icon name=\"time-outline\"></ion-icon>\n &nbsp;{{ page.time | dateFromNow }}\n </small>\n &nbsp;\n </ion-text>\n </ion-card-subtitle>\n\n <!-- main page -->\n <ion-card-title class=\"ion-no-margin ion-no-padding\">\n <ion-item\n detail=\"true\"\n tappable\n class=\"text-1x\"\n [routerLink]=\"page.path\"\n routerDirection=\"root\"\n lines=\"none\"\n >\n <!-- page icon-->\n <ion-icon *ngIf=\"page.icon\" slot=\"start\" [name]=\"page.icon\"></ion-icon>\n <mat-icon *ngIf=\"page.matIcon\" slot=\"start\">{{ page.matIcon }}</mat-icon>\n\n <ion-label color=\"primary\" [innerHTML]=\"page.title\"></ion-label>\n </ion-item>\n </ion-card-title>\n </ion-card-header>\n\n <!-- children pages -->\n @if (page.children | isNotEmptyArray) {\n <ion-card-content class=\"ion-no-padding ion-padding-start\">\n <ion-list class=\"ion-no-padding\">\n @for (childPage of page.children; track childPage.path) {\n <ion-item\n detail=\"true\"\n tappable\n class=\"text-1x\"\n [routerLink]=\"childPage.path\"\n routerDirection=\"root\"\n >\n <!-- page icon-->\n <ion-icon *ngIf=\"childPage.icon\" slot=\"start\" color=\"dark\" [name]=\"childPage.icon\"></ion-icon>\n <mat-icon *ngIf=\"childPage.matIcon\" slot=\"start\" color=\"dark\">\n {{ childPage.matIcon }}\n </mat-icon>\n\n <ion-label color=\"dark\" [innerHTML]=\"childPage.title\"></ion-label>\n </ion-item>\n }\n </ion-list>\n </ion-card-content>\n }\n </ion-card>\n </ion-col>\n }\n </ion-row>\n </ion-grid>\n }\n\n <!-- Bottom banner -->\n @if (showPartnerBanner || showLegalInformation) {\n <ion-grid\n class=\"bottom-banner ion-text-center\"\n [class.floating]=\"!mobile && (pageHistory | isEmptyArray)\"\n @fadeInAnimation\n >\n <ion-row>\n <!-- partners logos -->\n @if (showPartnerBanner) {\n <ion-col size=\"12\" [sizeLg]=\"partnerBannerSize\" class=\"ion-text-center\">\n @if (showLegalInformation) {\n <ion-list class=\"legal-info\">\n <ion-list-header>{{ 'HOME.PARTNERS' | translate }}</ion-list-header>\n </ion-list>\n }\n <!-- partners -->\n @for (item of $partners | async; track $index) {\n <a href=\"{{ item.siteUrl }}\">\n <img class=\"logo\" src=\"{{ item.logo }}\" alt=\"{{ item.label }}\" [title]=\"item.label\" />\n </a>\n }\n </ion-col>\n }\n\n <!-- legal information -->\n @if (showLegalInformation) {\n <ion-col size=\"12\" size-sm=\"6\" size-lg=\"4\" [class.left-border]=\"showPartnerBanner\">\n <ion-list class=\"legal-info\">\n <ion-list-header class=\"ion-text-left\">{{ 'HOME.LEGAL_INFORMATION' | translate }}</ion-list-header>\n @for (item of legalInformation; track item.path) {\n <ion-item (click)=\"openMarkdownModal($event, item)\" lines=\"none\" [button]=\"true\">\n <ion-label>\n <p>\n {{ item.title | translate: item.titleArgs }}\n </p>\n </ion-label>\n </ion-item>\n }\n </ion-list>\n </ion-col>\n @if (showInstallLinks) {\n <ion-col size=\"12\" size-lg=\"3\" offset-lg=\"9\" [class.left-border]=\"showPartnerBanner\">\n <ion-list class=\"legal-info\">\n <ion-list-header>{{ 'HOME.INSTALL_APP' | translate }}</ion-list-header>\n <!-- TODO -->\n </ion-list>\n </ion-col>\n }\n }\n </ion-row>\n </ion-grid>\n }\n }\n\n <!-- image credits -->\n @if (contentCredits | isNotNilOrBlank) {\n <div class=\"content-credits\" [class.floating]=\"!mobile\">\n <ion-text>{{ contentCredits }}</ion-text>\n </div>\n }\n</ion-content>\n", styles: [":host{--ion-card-background-color: rgba(var(--ion-background-color-rgb), .7);--scrollbar-width: 0;--bottom-banner-height: 122px}h1,h2,h3,h4,h5{white-space:normal}.center,ion-content ion-card.welcome img{text-align:center;display:inline-block}ion-content{--background: transparent;background-size:cover;-webkit-background-size:cover;-moz-background-size:cover;-o-background-size:cover;background-color:var(--ion-background-color);display:inline-block;padding:15px}ion-content.has-scrollbar{--scrollbar-width: 8px}ion-content .loading-page{display:block;height:100%;width:100%;position:absolute;background-color:transparent;z-index:5;transition:background .5s linear;-webkit-transition:background 1.5s linear}ion-content .loading-page.hidden{background-color:rgba(var(--ion-color-primary),0)}ion-content ion-card{z-index:9;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}ion-content ion-card ion-card-header{display:block}ion-content ion-card ion-card-header ion-card-subtitle,ion-content ion-card ion-card-header ion-card-title{display:block;width:100%}ion-content ion-card.main{min-width:240px;max-width:400px;margin-left:auto;margin-right:auto}ion-content ion-card.welcome{background-color:var(--ion-card-background-color)}ion-content ion-card.welcome button{margin-top:16px}ion-content ion-card.welcome img{max-width:250px}ion-content .history-container{margin-left:auto;margin-right:auto}ion-content .history-container ion-card{display:inline-block;box-sizing:border-box;z-index:8;min-width:240px;max-width:400px;width:100%;margin-left:auto;margin-right:auto;background-color:var(--ion-card-background-color);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}ion-content .history-container ion-card ion-card-header ion-card-subtitle{height:40px}ion-content .history-container ion-card ion-card-header ion-card-subtitle button[float-start]{--mdc-icon-button-state-layer-size: 40px;padding:8px;z-index:99;margin:0}ion-content .history-container ion-card ion-card-header ion-card-subtitle ion-label{line-height:40px}ion-content .history-container ion-card ion-card-header ion-card-subtitle ion-label[float-end]{padding-right:16px;font-weight:400}ion-content .history-container ion-card ion-card-header ion-card-title ion-item{width:100%;--ion-item-background: $transparent;--ion-item-icon-color: var(--ion-color-primary);--ion-item-text-color: var(--ion-color-primary)}ion-content .history-container ion-card ion-card-header ion-card-title ion-item ion-icon[slot=start],ion-content .history-container ion-card ion-card-header ion-card-title ion-item mat-icon[slot=start]{margin-right:unset;margin-inline-end:16px!important;color:var(--ion-item-icon-color)!important;fill:currentColor;stroke:currentColor}ion-content .history-container ion-card ion-card-header ion-card-title ion-item ion-text,ion-content .history-container ion-card ion-card-header ion-card-title ion-item ion-label{color:var(--ion-item-text-color)!important;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;-ms-text-size-adjust:100%;text-size-adjust:100%}ion-content .history-container ion-card ion-card-content ion-list{--ion-item-background: var(--ion-color-transparent)}ion-content .history-container ion-card ion-card-content ion-list ion-item{--ion-item-icon-color: var(--ion-color-dark);--ion-item-text-color: var(--ion-color-dark)}ion-content .history-container ion-card ion-card-content ion-list ion-item ion-icon[slot=start],ion-content .history-container ion-card ion-card-content ion-list ion-item mat-icon[slot=start]{color:var(--ion-item-icon-color)!important;fill:currentColor;stroke:currentColor;margin-right:unset;margin-inline-end:16px!important}ion-content .history-container ion-card ion-card-content ion-list ion-item ion-text,ion-content .history-container ion-card ion-card-content ion-list ion-item ion-label{color:var(--ion-item-text-color)!important}ion-content .bottom-banner{--ion-card-background-color: rgba(var(--ion-background-color-rgb), .5);background-color:var(--ion-card-background-color);min-width:240px;z-index:0;border-radius:4px;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);margin:16px}ion-content .bottom-banner img.logo{max-height:50px;margin-left:8px}ion-content .bottom-banner ion-list-header{min-height:30px}ion-content .bottom-banner ion-item{--min-height: 28px}ion-content .bottom-banner ion-item ion-label{margin-top:3px!important;margin-bottom:3px!important}ion-content ion-text{text-align:center}@media screen and (max-width: 575px){ion-content ion-card{min-width:160px;width:calc(100% - 20px);max-width:100%;margin-left:10px;margin-right:10px}ion-content ion-card h1{margin-top:0;font-size:20px}ion-content ion-card ion-card-content ion-button{margin-top:8px}ion-content .bottom-banner.floating{display:block;position:unset}ion-content .bottom-banner img.logo{max-height:30px}}@media screen and (max-width: 767px) and (min-width: 576px){ion-content{min-height:555px}ion-content .bottom-banner img.logo{max-height:40px}}@media screen and (min-width: 768px){ion-content{min-height:calc(100% - var(--bottom-banner-height))}}@media screen and (min-width: 1200px){ion-content ion-col.feed{height:fit-content;max-height:calc(100vh - var(--ion-toolbar-height) - var(--bottom-banner-height) - var(--ion-padding) * 2);padding-inline-end:calc(var(--ion-padding) - 10px);overflow:hidden}ion-content .bottom-banner.floating{position:fixed;left:0;right:0;bottom:0;overflow-y:auto;max-height:var(--bottom-banner-height);margin-top:0}}.content-credits{float:right}.content-credits.floating{float:unset;position:fixed;bottom:0;right:var(--scrollbar-width, 0)}.content-credits ion-text{padding-inline-start:4px;padding-inline-end:4px;font-size:.7rem;background-color:rgba(var(--ion-background-color-rgb),.5);color:rgba(var(--ion-text-color-rgb),.7)}@media screen and (min-width: 992px){.left-border{border-left:1px solid var(--ion-color-medium)}}\n"] }]
34651
35511
  }], ctorParameters: () => [{ type: AccountService }, { type: i2$1.ModalController }, { type: i1$1.TranslateService }, { type: i2$1.ToastController }, { type: ConfigService }, { type: PlatformService }, { type: i0.ChangeDetectorRef }, { type: NetworkService }, { type: LocalSettingsService }, { type: Environment, decorators: [{
34652
35512
  type: Inject,
34653
35513
  args: [ENVIRONMENT]
@@ -34864,11 +35724,11 @@ class FeedPage {
34864
35724
  this.destroySubject.complete();
34865
35725
  }
34866
35726
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FeedPage, deps: [{ token: LocalSettingsService }, { token: i0.ChangeDetectorRef }, { token: APP_FEED_SERVICE, optional: true }, { token: ENVIRONMENT, optional: true }], target: i0.ɵɵFactoryTarget.Component });
34867
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: FeedPage, selector: "app-feed-page", inputs: { debug: "debug", title: "title", feedUrls: "feedUrls", feedItemId: "feedItemId" }, ngImport: i0, template: "<ion-header>\n <ion-toolbar color=\"primary\">\n <ion-buttons slot=\"start\">\n <ion-back-button></ion-back-button>\n </ion-buttons>\n\n <ion-title>{{ title | translate }}</ion-title>\n </ion-toolbar>\n</ion-header>\n\n<ion-content>\n <div class=\"ion-padding\">\n @if (loadingSubject | async) {\n <ion-spinner></ion-spinner>\n } @else {\n <app-feed [showHeader]=\"false\"\n [showReadMoreButton]=\"false\"\n [feedUrls]=\"feedUrls\"\n cardColor=\"light\"\n [debug]=\"debug\"\n ></app-feed>\n }\n </div>\n</ion-content>\n", dependencies: [{ kind: "component", type: i2$1.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$1.IonContent, selector: "ion-content", inputs: ["color", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$1.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$1.IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: i2$1.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2$1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonBackButton, selector: "ion-back-button" }, { kind: "component", type: FeedsComponent, selector: "app-feed", inputs: ["debug", "mobile", "showHeader", "showReadMoreButton", "headerColor", "cardColor", "shape", "class", "itemId", "filterItem", "feeds", "feedUrls", "maxAgeInMonths", "maxContentLength"] }, { kind: "pipe", type: i3$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
35727
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: FeedPage, selector: "app-feed-page", inputs: { debug: "debug", title: "title", feedUrls: "feedUrls", feedItemId: "feedItemId" }, ngImport: i0, template: "<ion-header>\n <ion-toolbar color=\"primary\">\n <ion-buttons slot=\"start\">\n <ion-back-button></ion-back-button>\n </ion-buttons>\n\n <ion-title>{{ title | translate }}</ion-title>\n </ion-toolbar>\n</ion-header>\n\n<ion-content>\n <div class=\"ion-padding\">\n @if (loadingSubject | async) {\n <ion-spinner></ion-spinner>\n } @else {\n <app-feed [showHeader]=\"false\"\n [showReadMoreButton]=\"false\"\n [urls]=\"feedUrls\"\n cardColor=\"light\"\n [debug]=\"debug\"\n ></app-feed>\n }\n </div>\n</ion-content>\n", dependencies: [{ kind: "component", type: i2$1.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$1.IonContent, selector: "ion-content", inputs: ["color", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$1.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$1.IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: i2$1.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2$1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonBackButton, selector: "ion-back-button" }, { kind: "component", type: FeedsComponent, selector: "app-feed", inputs: ["debug", "mobile", "showHeader", "showReadMoreButton", "headerColor", "cardColor", "shape", "class", "itemId", "filterItem", "feeds", "urls", "maxAgeInMonths", "maxContentLength"], outputs: ["editItem", "deleteItem"] }, { kind: "pipe", type: i3$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
34868
35728
  }
34869
35729
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FeedPage, decorators: [{
34870
35730
  type: Component,
34871
- args: [{ selector: 'app-feed-page', changeDetection: ChangeDetectionStrategy.OnPush, template: "<ion-header>\n <ion-toolbar color=\"primary\">\n <ion-buttons slot=\"start\">\n <ion-back-button></ion-back-button>\n </ion-buttons>\n\n <ion-title>{{ title | translate }}</ion-title>\n </ion-toolbar>\n</ion-header>\n\n<ion-content>\n <div class=\"ion-padding\">\n @if (loadingSubject | async) {\n <ion-spinner></ion-spinner>\n } @else {\n <app-feed [showHeader]=\"false\"\n [showReadMoreButton]=\"false\"\n [feedUrls]=\"feedUrls\"\n cardColor=\"light\"\n [debug]=\"debug\"\n ></app-feed>\n }\n </div>\n</ion-content>\n" }]
35731
+ args: [{ selector: 'app-feed-page', changeDetection: ChangeDetectionStrategy.OnPush, template: "<ion-header>\n <ion-toolbar color=\"primary\">\n <ion-buttons slot=\"start\">\n <ion-back-button></ion-back-button>\n </ion-buttons>\n\n <ion-title>{{ title | translate }}</ion-title>\n </ion-toolbar>\n</ion-header>\n\n<ion-content>\n <div class=\"ion-padding\">\n @if (loadingSubject | async) {\n <ion-spinner></ion-spinner>\n } @else {\n <app-feed [showHeader]=\"false\"\n [showReadMoreButton]=\"false\"\n [urls]=\"feedUrls\"\n cardColor=\"light\"\n [debug]=\"debug\"\n ></app-feed>\n }\n </div>\n</ion-content>\n" }]
34872
35732
  }], ctorParameters: () => [{ type: LocalSettingsService }, { type: i0.ChangeDetectorRef }, { type: undefined, decorators: [{
34873
35733
  type: Optional
34874
35734
  }, {
@@ -44334,528 +45194,174 @@ class AppEntityEditor extends AppTabEditor {
44334
45194
  // can be overwritten by subclasses
44335
45195
  }
44336
45196
  computeUsageMode(data) {
44337
- // can be overwritten by subclasses
44338
- // Using local settings default value
44339
- return this.settings.usageMode;
44340
- }
44341
- waitWhilePending(opts) {
44342
- return AppFormUtils.waitWhilePending(this, opts);
44343
- }
44344
- async getValue() {
44345
- const json = await this.getJsonValueToSave();
44346
- const res = new this.dataType();
44347
- res.fromObject(json);
44348
- return res;
44349
- }
44350
- getJsonValueToSave() {
44351
- return Promise.resolve(this.form.value);
44352
- }
44353
- /**
44354
- * Compute the title
44355
- *
44356
- * @param data
44357
- */
44358
- async updateTitle(data) {
44359
- data = data || this.data;
44360
- const title = await this.computeTitle(data);
44361
- this.titleSubject.next(title);
44362
- // If NOT data, then add to page history
44363
- if (!this.isNewData) {
44364
- const page = await this.computePageHistory(title);
44365
- if (page)
44366
- return this.addToPageHistory(page);
44367
- }
44368
- }
44369
- async addToPageHistory(page, opts) {
44370
- if (!page)
44371
- return; // Skip
44372
- return this.settings.addToPageHistory(page, {
44373
- removePathQueryParams: true,
44374
- removeTitleSmallTag: true,
44375
- emitEvent: false,
44376
- ...opts,
44377
- });
44378
- }
44379
- async computePageHistory(title) {
44380
- if (this.debug)
44381
- console.debug('[entity-editor] Computing page history, using url: ' + this.router.url);
44382
- return {
44383
- title,
44384
- path: this.router.url,
44385
- };
44386
- }
44387
- async removePageHistory(opts) {
44388
- return this.settings.removePageHistory(this.router.url, opts);
44389
- }
44390
- computePageUrl(id) {
44391
- const parentUrl = this.getParentPageUrl();
44392
- return parentUrl && `${parentUrl}/${id}`;
44393
- }
44394
- getParentPageUrl(withQueryParams) {
44395
- let parentUrl = this.defaultBackHref;
44396
- // Remove query params
44397
- if (withQueryParams !== true && parentUrl && parentUrl.indexOf('?') !== -1) {
44398
- parentUrl = parentUrl.substring(0, parentUrl.indexOf('?'));
44399
- }
44400
- return parentUrl;
44401
- }
44402
- listenChanges(id, opts) {
44403
- return this.dataService.listenChanges(this.data.id, {
44404
- // Make sure to skip cache, because NOT the full graph will be fetched by subscription
44405
- // When a changes occur, we call reload() to force refetching the full graph
44406
- fetchPolicy: 'no-cache',
44407
- ...opts,
44408
- });
44409
- }
44410
- markForCheck() {
44411
- this.cd.markForCheck();
44412
- }
44413
- /**
44414
- * Update the route, without reloading the component
44415
- *
44416
- * @param data
44417
- * @param queryParams
44418
- * @protected
44419
- */
44420
- async updateRoute(data, queryParams) {
44421
- data = data || this.data;
44422
- if (!data || !this.route)
44423
- return false;
44424
- const currId = this.route.snapshot.paramMap.get(this.pathIdAttribute);
44425
- const futureId = isNotNil(data.id) ? data.id.toString() : 'new';
44426
- if (queryParams) {
44427
- this.queryParams = {
44428
- ...this.queryParams,
44429
- ...queryParams,
44430
- };
44431
- }
44432
- if (currId === futureId) {
44433
- // Update queryParam only (not the path)
44434
- // /!\ We should get query Params from the updated route, and not from snapshot that can be outdated
44435
- const actualQueryParams = await firstNotNilPromise(this.route.queryParams);
44436
- if (!equals(actualQueryParams, this.queryParams)) {
44437
- if (this.debug)
44438
- console.debug(`${this._logPrefix}Updating route using queryParams: `, this.queryParams);
44439
- return await this.router.navigate(['.'], {
44440
- relativeTo: this.route,
44441
- replaceUrl: false,
44442
- queryParams: this.queryParams,
44443
- state: { animated: false },
44444
- });
44445
- }
44446
- }
44447
- else {
44448
- const path = this.computePageUrl(isNotNil(data.id) ? data.id : 'new');
44449
- const commands = path && typeof path === 'string' ? path.split('/').slice(1) : path;
44450
- if (isNotEmptyArray(commands)) {
44451
- // Current route was '/new' => change to '/new?id=:id' (to allow CustomReuseStrategy to detect that route can be reused)
44452
- if (currId === 'new' && this.queryParams[this.pathIdAttribute] !== futureId) {
44453
- if (this.debug)
44454
- console.debug(`${this._logPrefix}Updating route /new into /:id, using queryParams: `, queryParams);
44455
- this.queryParams[this.pathIdAttribute] = futureId;
44456
- const res = await this.router.navigate(['.'], {
44457
- relativeTo: this.route,
44458
- replaceUrl: true,
44459
- queryParams: this.queryParams,
44460
- state: { animated: false },
44461
- });
44462
- if (!res)
44463
- return false;
44464
- }
44465
- if (this.debug)
44466
- console.debug(`${this._logPrefix}Updating route to: ` + path);
44467
- return await this.router.navigate(commands, {
44468
- replaceUrl: true,
44469
- queryParams: this.queryParams,
44470
- state: { animated: false },
44471
- });
44472
- }
44473
- return Promise.reject('Missing page URL');
44474
- }
44475
- }
44476
- /* -- private functions -- */
44477
- /**
44478
- * Open the first tab that is invalid
44479
- */
44480
- openFirstInvalidTab() {
44481
- const invalidTabIndex = this.getFirstInvalidTabIndex();
44482
- if (invalidTabIndex !== -1 && this.selectedTabIndex !== invalidTabIndex) {
44483
- this.selectedTabIndex = invalidTabIndex;
44484
- this.markForCheck();
44485
- }
44486
- }
44487
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AppEntityEditor, deps: "invalid", target: i0.ɵɵFactoryTarget.Directive });
44488
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.13", type: AppEntityEditor, usesInheritance: true, ngImport: i0 });
44489
- }
44490
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AppEntityEditor, decorators: [{
44491
- type: Directive
44492
- }], ctorParameters: () => [{ type: i0.Injector }, { type: undefined }, { type: undefined }, { type: AppEditorOptions, decorators: [{
44493
- type: Optional
44494
- }] }] });
44495
-
44496
- var PersonFilter_1;
44497
- // @dynamic
44498
- let PersonFilter = class PersonFilter extends EntityFilter {
44499
- static { PersonFilter_1 = this; }
44500
- static fromObject;
44501
- static searchFilter(source) {
44502
- return source && PersonFilter_1.fromObject(source).asFilterFn();
44503
- }
44504
- email;
44505
- pubkey;
44506
- searchText;
44507
- statusIds;
44508
- userProfiles;
44509
- includedIds;
44510
- excludedIds;
44511
- searchAttribute;
44512
- searchAttributes;
44513
- constructor() {
44514
- super(PersonFilter_1.TYPENAME);
44515
- }
44516
- fromObject(source, opts) {
44517
- super.fromObject(source, opts);
44518
- this.email = source.email;
44519
- this.pubkey = source.pubkey;
44520
- this.searchText = source.searchText;
44521
- this.statusIds = source.statusIds || (isNotNil(source.statusId) ? [source.statusId] : undefined);
44522
- this.userProfiles = source.userProfiles;
44523
- this.excludedIds = source.excludedIds;
44524
- this.searchAttribute = source.searchAttribute;
44525
- this.searchAttributes = source.searchAttributes;
44526
- }
44527
- asObject(opts) {
44528
- const target = super.asObject(opts);
44529
- target.email = this.email;
44530
- target.pubkey = this.pubkey;
44531
- target.searchText = this.searchText;
44532
- target.statusIds = this.statusIds;
44533
- target.userProfiles = this.userProfiles;
44534
- target.excludedIds = this.excludedIds;
44535
- target.searchAttribute = this.searchAttribute;
44536
- target.searchAttributes = this.searchAttributes;
44537
- return target;
44538
- }
44539
- buildFilter() {
44540
- const filterFns = super.buildFilter();
44541
- // Filter by status
44542
- if (isNotEmptyArray(this.statusIds)) {
44543
- filterFns.push((e) => this.statusIds.includes(e.statusId));
44544
- }
44545
- // Filter included ids
44546
- const includedIds = this.includedIds;
44547
- if (isNotEmptyArray(includedIds)) {
44548
- filterFns.push((e) => isNotNil(e.id) && includedIds.includes(e.id));
44549
- }
44550
- // Filter excluded ids
44551
- const excludedIds = this.excludedIds;
44552
- if (isNotEmptyArray(excludedIds)) {
44553
- filterFns.push((e) => isNil(e.id) || !excludedIds.includes(e.id));
44554
- }
44555
- // Search text
44556
- const searchTextFilter = EntityUtils.searchTextFilter(this.searchAttribute || this.searchAttributes || ['lastName', 'firstName', 'department.name'], this.searchText);
44557
- if (searchTextFilter)
44558
- filterFns.push(searchTextFilter);
44559
- return filterFns;
44560
- }
44561
- };
44562
- PersonFilter = PersonFilter_1 = __decorate([
44563
- EntityClass({ typename: 'PersonFilterVO' })
44564
- ], PersonFilter);
44565
-
44566
- const MessageTypes = {
44567
- INBOX_MESSAGE: 'INBOX_MESSAGE',
44568
- EMAIL: 'EMAIL',
44569
- FEED: 'FEED',
44570
- };
44571
- const MessageTypeList = Object.freeze([
44572
- {
44573
- id: MessageTypes.INBOX_MESSAGE,
44574
- icon: 'notifications',
44575
- label: 'SOCIAL.MESSAGE.TYPE_ENUM.INBOX_MESSAGE',
44576
- },
44577
- {
44578
- id: MessageTypes.EMAIL,
44579
- icon: 'mail',
44580
- label: 'SOCIAL.MESSAGE.TYPE_ENUM.EMAIL',
44581
- },
44582
- {
44583
- id: MessageTypes.FEED,
44584
- icon: 'megaphone',
44585
- label: 'SOCIAL.MESSAGE.TYPE_ENUM.FEED',
44586
- },
44587
- ]);
44588
- // @dynamic
44589
- let Message = class Message extends Entity {
44590
- static fromObject;
44591
- type;
44592
- issuer;
44593
- recipients;
44594
- recipientFilter;
44595
- subject;
44596
- body;
44597
- constructor() {
44598
- super();
44599
- }
44600
- fromObject(source, opts) {
44601
- super.fromObject(source, opts);
44602
- this.type = source.type;
44603
- this.issuer = source.issuer && Person.fromObject(source.issuer);
44604
- this.recipients = source.recipients && source.recipients.map(Person.fromObject);
44605
- this.recipientFilter = (source.recipientFilter && PersonFilter.fromObject(source.recipientFilter)) || undefined;
44606
- this.subject = source.subject;
44607
- this.body = source.body;
44608
- }
44609
- asObject(opts) {
44610
- const target = super.asObject(opts);
44611
- target.recipients = this.recipients && this.recipients.map((p) => p.asObject(opts));
44612
- target.recipientFilter = (this.recipientFilter && this.recipientFilter.asObject(opts)) || undefined;
44613
- return target;
44614
- }
44615
- };
44616
- Message = __decorate([
44617
- EntityClass({ typename: 'MessageVO' })
44618
- ], Message);
44619
- // @dynamic
44620
- let MessageFilter = class MessageFilter extends EntityFilter {
44621
- static fromObject;
44622
- fromObject(source, opts) {
44623
- super.fromObject(source, opts);
44624
- }
44625
- buildFilter() {
44626
- return super.buildFilter();
44627
- }
44628
- };
44629
- MessageFilter = __decorate([
44630
- EntityClass({ typename: 'MessageFilterVO' })
44631
- ], MessageFilter);
44632
-
44633
- class MessageForm extends AppForm {
44634
- formBuilder;
44635
- cd;
44636
- mobile;
44637
- suggestFn;
44638
- subjectMinLength = 5;
44639
- subjectMaxLength = 255;
44640
- bodyMaxLength = 2000;
44641
- bodyAutoHeight = true;
44642
- canSelectType = false;
44643
- canRecipientFilter = false;
44644
- recipientFilterCount = 0;
44645
- types = MessageTypeList;
44646
- constructor(injector, formBuilder, cd) {
44647
- super(injector);
44648
- this.formBuilder = formBuilder;
44649
- this.cd = cd;
44650
- this.mobile = this.settings.mobile;
44651
- }
44652
- ngOnInit() {
44653
- this.setForm(this.formBuilder.group({
44654
- type: [MessageTypes.INBOX_MESSAGE, Validators.required],
44655
- recipients: [null, Validators.required],
44656
- recipientFilter: [null],
44657
- subject: [
44658
- null,
44659
- this.subjectMaxLength
44660
- ? Validators.compose([Validators.required, Validators.minLength(this.subjectMinLength), Validators.maxLength(this.subjectMaxLength)])
44661
- : Validators.required,
44662
- ],
44663
- body: [null, this.bodyMaxLength ? Validators.compose([Validators.maxLength(this.bodyMaxLength)]) : Validators.required],
44664
- }));
44665
- this.registerSubscription(this._form
44666
- .get('type')
44667
- .valueChanges.pipe(filter(isNotNil))
44668
- .subscribe((type) => this.updateFormGroup(this._form, { type })));
44669
- // Person combo
44670
- const personAttributes = this.settings.getFieldDisplayAttributes('person', ['lastName', 'firstName', 'department.name']);
44671
- this.registerAutocompleteField('recipients', {
44672
- showAllOnFocus: false,
44673
- suggestFn: this.suggestFn,
44674
- filter: {
44675
- statusIds: [StatusIds.TEMPORARY, StatusIds.ENABLE],
44676
- },
44677
- attributes: personAttributes,
44678
- columnNames: personAttributes.map((attr) => `USER.${changeCaseToUnderscore(attr).toUpperCase()}`),
44679
- displayWith: PersonUtils.personToString,
44680
- multiple: true,
44681
- mobile: this.mobile,
44682
- });
44683
- }
44684
- isSamePerson(o1, o2) {
44685
- return EntityUtils.equals(o1, o2, 'id');
44686
- }
44687
- updateFormGroup(formGroup, opts) {
44688
- console.debug('[message-form] Updating form group...', opts);
44689
- // Recipient validator
44690
- const recipientsRequired = toBoolean(opts?.recipientRequired, opts?.type !== MessageTypes.FEED);
44691
- {
44692
- const control = formGroup.get('recipients');
44693
- if (recipientsRequired) {
44694
- if (!control.hasValidator(Validators.required)) {
44695
- control.addValidators(Validators.required);
44696
- }
44697
- control.enable();
44698
- }
44699
- else {
44700
- if (control.hasValidator(Validators.required)) {
44701
- control.removeValidators(Validators.required);
44702
- }
44703
- control.disable();
44704
- }
44705
- }
44706
- // Recipient filter validator
44707
- const recipientFilterRequired = this.canRecipientFilter && !recipientsRequired;
44708
- {
44709
- const control = formGroup.get('recipientFilter');
44710
- if (recipientFilterRequired) {
44711
- if (!control.hasValidator(Validators.required)) {
44712
- control.addValidators(Validators.required);
44713
- }
44714
- control.enable();
44715
- }
44716
- else {
44717
- if (control.hasValidator(Validators.required)) {
44718
- control.removeValidators(Validators.required);
44719
- }
44720
- control.disable();
44721
- }
44722
- }
44723
- formGroup.updateValueAndValidity();
45197
+ // can be overwritten by subclasses
45198
+ // Using local settings default value
45199
+ return this.settings.usageMode;
44724
45200
  }
44725
- markForCheck() {
44726
- this.cd.markForCheck();
45201
+ waitWhilePending(opts) {
45202
+ return AppFormUtils.waitWhilePending(this, opts);
44727
45203
  }
44728
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: MessageForm, deps: [{ token: i0.Injector }, { token: i1$3.UntypedFormBuilder }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
44729
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: MessageForm, selector: "app-message-form", inputs: { suggestFn: "suggestFn", subjectMinLength: "subjectMinLength", subjectMaxLength: "subjectMaxLength", bodyMaxLength: "bodyMaxLength", bodyAutoHeight: "bodyAutoHeight", canSelectType: "canSelectType", canRecipientFilter: "canRecipientFilter", recipientFilterCount: "recipientFilterCount" }, usesInheritance: true, ngImport: i0, template: "<form [formGroup]=\"form\" class=\"form-container\">\n <!-- type -->\n <mat-form-field *ngIf=\"canSelectType\">\n <mat-label>{{ 'SOCIAL.MESSAGE.TYPE' | translate }}</mat-label>\n <mat-select formControlName=\"type\">\n @for (item of types; track item.id) {\n @if (canRecipientFilter || item.id !== 'FEED') {\n <mat-option [value]=\"item.id\">\n <ion-icon [name]=\"item.icon\"></ion-icon>\n {{ item.label | translate }}\n </mat-option>\n }\n }\n </mat-select>\n </mat-form-field>\n\n <!-- Recipients -->\n @if ((form | formGetValue: 'type') !== 'FEED') {\n <mat-chips-field\n formControlName=\"recipients\"\n chipColor=\"accent\"\n [placeholder]=\"'SOCIAL.MESSAGE.RECIPIENTS' | translate\"\n [config]=\"autocompleteFields.recipients\"\n [equals]=\"isSamePerson\"\n ></mat-chips-field>\n } @else {\n <mat-form-field>\n <input matInput hidden formControlName=\"recipients\" type=\"text\" />\n <mat-chip-grid>\n <mat-chip-row>\n {{ 'SOCIAL.MESSAGE.RECIPIENT_FILTER_COUNT' | translate: { count: recipientFilterCount } }}\n </mat-chip-row>\n </mat-chip-grid>\n </mat-form-field>\n }\n\n <!-- Subject -->\n <mat-form-field>\n <mat-label>{{ 'SOCIAL.MESSAGE.SUBJECT' | translate }}</mat-label>\n <input matInput type=\"text\" formControlName=\"subject\" autocomplete=\"off\" required />\n <mat-error *ngIf=\"form.controls.subject.hasError('required')\" translate>ERROR.FIELD_REQUIRED</mat-error>\n <mat-error *ngIf=\"form.controls.subject.hasError('minlength')\">\n {{ 'ERROR.FIELD_MIN_LENGTH_COMPACT' | translate }}\n </mat-error>\n <mat-error *ngIf=\"form.controls.subject.hasError('maxlength')\">\n {{ 'ERROR.FIELD_MAX_LENGTH_COMPACT' | translate }}\n </mat-error>\n <mat-hint *ngIf=\"subjectMaxLength\" align=\"end\">\n {{\n 'INFO.TEXT_PROGRESS' | translate: { current: form.controls.subject.value?.length || 0, max: subjectMaxLength }\n }}\n </mat-hint>\n </mat-form-field>\n\n <!-- Body -->\n <mat-form-field>\n <mat-label>{{ 'SOCIAL.MESSAGE.BODY_HELP' | translate }}</mat-label>\n <textarea\n matInput\n type=\"text\"\n formControlName=\"body\"\n [class.fixed-height]=\"!bodyAutoHeight\"\n [cdkTextareaAutosize]=\"bodyAutoHeight\"\n (keydown.control.enter)=\"doSubmit($event)\"\n ></textarea>\n <mat-error *ngIf=\"form.controls.body.hasError('maxlength')\">\n {{ 'ERROR.FIELD_MAX_LENGTH' | translate: form.controls.body.errors.maxlength }}\n </mat-error>\n <mat-hint *ngIf=\"bodyMaxLength\" align=\"end\">\n {{ 'INFO.TEXT_PROGRESS' | translate: { current: form.controls.body.value?.length || 0, max: bodyMaxLength } }}\n </mat-hint>\n </mat-form-field>\n</form>\n", styles: ["textarea.fixed-height{height:11.5em}textarea{min-height:11.5em}\n"], dependencies: [{ kind: "directive", type: i3$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i2$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "directive", type: i1$1.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { kind: "directive", type: i1$3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$3.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$3.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$3.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i3.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "directive", type: i1$4.CdkTextareaAutosize, selector: "textarea[cdkTextareaAutosize]", inputs: ["cdkAutosizeMinRows", "cdkAutosizeMaxRows", "cdkTextareaAutosize", "placeholder"], exportAs: ["cdkTextareaAutosize"] }, { kind: "component", type: i6$3.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i2.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: MatChipsField, selector: "mat-chips-field", inputs: ["equals", "logPrefix", "formControl", "formControlName", "floatLabel", "placeholder", "suggestFn", "required", "mobile", "readonly", "clearable", "debounceTime", "displaySeparator", "displayWith", "displayAttributes", "displayColumnSizes", "displayColumnNames", "highlightAccent", "showAllOnFocus", "showPanelOnFocus", "autofocus", "config", "i18nPrefix", "noResultMessage", "panelClass", "panelWidth", "disableRipple", "matAutocompletePosition", "itemSize", "fetchMoreThreshold", "suggestLengthThreshold", "showLoadingSpinner", "chipColor", "debug", "applyImplicitValue", "dropButtonTitle", "clearButtonTitle", "trimSearchText", "hideRequiredMarker", "colSizes", "separatorKeysCodes", "appearance", "subscriptSizing", "class", "filter", "tabindex", "items"], outputs: ["click", "blur", "focus", "dropButtonClick", "keydown.escape", "keyup.enter"] }, { kind: "directive", type: AutoResizeDirective, selector: "textarea[cdkTextareaAutosize]", inputs: ["cdkAutosizeMaxRows", "cdkAutosizeMinRows"] }, { kind: "component", type: i11$1.MatChipGrid, selector: "mat-chip-grid", inputs: ["disabled", "placeholder", "required", "value", "errorStateMatcher"], outputs: ["change", "valueChange"] }, { kind: "component", type: i11$1.MatChipRow, selector: "mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]", inputs: ["editable"], outputs: ["edited"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }, { kind: "pipe", type: FormGetValuePipe, name: "formGetValue" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
44730
- }
44731
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: MessageForm, decorators: [{
44732
- type: Component,
44733
- args: [{ selector: 'app-message-form', changeDetection: ChangeDetectionStrategy.OnPush, template: "<form [formGroup]=\"form\" class=\"form-container\">\n <!-- type -->\n <mat-form-field *ngIf=\"canSelectType\">\n <mat-label>{{ 'SOCIAL.MESSAGE.TYPE' | translate }}</mat-label>\n <mat-select formControlName=\"type\">\n @for (item of types; track item.id) {\n @if (canRecipientFilter || item.id !== 'FEED') {\n <mat-option [value]=\"item.id\">\n <ion-icon [name]=\"item.icon\"></ion-icon>\n {{ item.label | translate }}\n </mat-option>\n }\n }\n </mat-select>\n </mat-form-field>\n\n <!-- Recipients -->\n @if ((form | formGetValue: 'type') !== 'FEED') {\n <mat-chips-field\n formControlName=\"recipients\"\n chipColor=\"accent\"\n [placeholder]=\"'SOCIAL.MESSAGE.RECIPIENTS' | translate\"\n [config]=\"autocompleteFields.recipients\"\n [equals]=\"isSamePerson\"\n ></mat-chips-field>\n } @else {\n <mat-form-field>\n <input matInput hidden formControlName=\"recipients\" type=\"text\" />\n <mat-chip-grid>\n <mat-chip-row>\n {{ 'SOCIAL.MESSAGE.RECIPIENT_FILTER_COUNT' | translate: { count: recipientFilterCount } }}\n </mat-chip-row>\n </mat-chip-grid>\n </mat-form-field>\n }\n\n <!-- Subject -->\n <mat-form-field>\n <mat-label>{{ 'SOCIAL.MESSAGE.SUBJECT' | translate }}</mat-label>\n <input matInput type=\"text\" formControlName=\"subject\" autocomplete=\"off\" required />\n <mat-error *ngIf=\"form.controls.subject.hasError('required')\" translate>ERROR.FIELD_REQUIRED</mat-error>\n <mat-error *ngIf=\"form.controls.subject.hasError('minlength')\">\n {{ 'ERROR.FIELD_MIN_LENGTH_COMPACT' | translate }}\n </mat-error>\n <mat-error *ngIf=\"form.controls.subject.hasError('maxlength')\">\n {{ 'ERROR.FIELD_MAX_LENGTH_COMPACT' | translate }}\n </mat-error>\n <mat-hint *ngIf=\"subjectMaxLength\" align=\"end\">\n {{\n 'INFO.TEXT_PROGRESS' | translate: { current: form.controls.subject.value?.length || 0, max: subjectMaxLength }\n }}\n </mat-hint>\n </mat-form-field>\n\n <!-- Body -->\n <mat-form-field>\n <mat-label>{{ 'SOCIAL.MESSAGE.BODY_HELP' | translate }}</mat-label>\n <textarea\n matInput\n type=\"text\"\n formControlName=\"body\"\n [class.fixed-height]=\"!bodyAutoHeight\"\n [cdkTextareaAutosize]=\"bodyAutoHeight\"\n (keydown.control.enter)=\"doSubmit($event)\"\n ></textarea>\n <mat-error *ngIf=\"form.controls.body.hasError('maxlength')\">\n {{ 'ERROR.FIELD_MAX_LENGTH' | translate: form.controls.body.errors.maxlength }}\n </mat-error>\n <mat-hint *ngIf=\"bodyMaxLength\" align=\"end\">\n {{ 'INFO.TEXT_PROGRESS' | translate: { current: form.controls.body.value?.length || 0, max: bodyMaxLength } }}\n </mat-hint>\n </mat-form-field>\n</form>\n", styles: ["textarea.fixed-height{height:11.5em}textarea{min-height:11.5em}\n"] }]
44734
- }], ctorParameters: () => [{ type: i0.Injector }, { type: i1$3.UntypedFormBuilder }, { type: i0.ChangeDetectorRef }], propDecorators: { suggestFn: [{
44735
- type: Input
44736
- }], subjectMinLength: [{
44737
- type: Input
44738
- }], subjectMaxLength: [{
44739
- type: Input
44740
- }], bodyMaxLength: [{
44741
- type: Input
44742
- }], bodyAutoHeight: [{
44743
- type: Input
44744
- }], canSelectType: [{
44745
- type: Input
44746
- }], canRecipientFilter: [{
44747
- type: Input
44748
- }], recipientFilterCount: [{
44749
- type: Input
44750
- }] } });
44751
-
44752
- class MessageModal {
44753
- settings;
44754
- viewCtrl;
44755
- accountService;
44756
- cd;
44757
- mobile;
44758
- suggestFn;
44759
- data;
44760
- canSelectType;
44761
- canRecipientFilter;
44762
- recipientFilterCount;
44763
- form;
44764
- constructor(settings, viewCtrl, accountService, cd) {
44765
- this.settings = settings;
44766
- this.viewCtrl = viewCtrl;
44767
- this.accountService = accountService;
44768
- this.cd = cd;
44769
- this.mobile = this.settings.mobile;
45204
+ async getValue() {
45205
+ const json = await this.getJsonValueToSave();
45206
+ const res = new this.dataType();
45207
+ res.fromObject(json);
45208
+ return res;
44770
45209
  }
44771
- ngOnInit() {
44772
- this.canSelectType = toBoolean(this.canSelectType, this.accountService.isAdmin());
45210
+ getJsonValueToSave() {
45211
+ return Promise.resolve(this.form.value);
44773
45212
  }
44774
- ngAfterViewInit() {
44775
- setTimeout(() => {
44776
- this.form.markAsReady({ emitEvent: false });
44777
- if (this.data) {
44778
- this.data.type = this.data.type || MessageTypes.INBOX_MESSAGE;
44779
- this.form.setValue(this.data);
44780
- }
44781
- this.form.markAsLoaded();
44782
- this.form.enable();
45213
+ /**
45214
+ * Compute the title
45215
+ *
45216
+ * @param data
45217
+ */
45218
+ async updateTitle(data) {
45219
+ data = data || this.data;
45220
+ const title = await this.computeTitle(data);
45221
+ this.titleSubject.next(title);
45222
+ // If NOT data, then add to page history
45223
+ if (!this.isNewData) {
45224
+ const page = await this.computePageHistory(title);
45225
+ if (page)
45226
+ return this.addToPageHistory(page);
45227
+ }
45228
+ }
45229
+ async addToPageHistory(page, opts) {
45230
+ if (!page)
45231
+ return; // Skip
45232
+ return this.settings.addToPageHistory(page, {
45233
+ removePathQueryParams: true,
45234
+ removeTitleSmallTag: true,
45235
+ emitEvent: false,
45236
+ ...opts,
44783
45237
  });
44784
45238
  }
44785
- cancel() {
44786
- this.viewCtrl.dismiss();
45239
+ async computePageHistory(title) {
45240
+ if (this.debug)
45241
+ console.debug('[entity-editor] Computing page history, using url: ' + this.router.url);
45242
+ return {
45243
+ title,
45244
+ path: this.router.url,
45245
+ };
44787
45246
  }
44788
- async doSubmit() {
44789
- if (this.form.disabled)
44790
- return;
44791
- if (!this.form.valid) {
44792
- await AppFormUtils.waitWhilePending(this.form);
44793
- if (this.form.invalid) {
44794
- AppFormUtils.logFormErrors(this.form.form, '[message-modal] ');
44795
- this.form.markAllAsTouched();
44796
- return;
44797
- }
44798
- }
44799
- this.markAsLoading();
44800
- try {
44801
- const data = this.form.value;
44802
- // Disable the form
44803
- this.form.disable();
44804
- const entity = Message.fromObject(data);
44805
- return this.viewCtrl.dismiss(entity);
44806
- }
44807
- catch (err) {
44808
- this.form.error = (err && err.message) || err;
44809
- this.markAsLoaded();
44810
- // Enable the form
44811
- this.form.enable();
44812
- // Reset form error on next changes
44813
- firstNotNilPromise(this.form.form.valueChanges).then(() => {
44814
- this.form.error = null;
44815
- this.markForCheck();
44816
- });
44817
- return;
45247
+ async removePageHistory(opts) {
45248
+ return this.settings.removePageHistory(this.router.url, opts);
45249
+ }
45250
+ computePageUrl(id) {
45251
+ const parentUrl = this.getParentPageUrl();
45252
+ return parentUrl && `${parentUrl}/${id}`;
45253
+ }
45254
+ getParentPageUrl(withQueryParams) {
45255
+ let parentUrl = this.defaultBackHref;
45256
+ // Remove query params
45257
+ if (withQueryParams !== true && parentUrl && parentUrl.indexOf('?') !== -1) {
45258
+ parentUrl = parentUrl.substring(0, parentUrl.indexOf('?'));
44818
45259
  }
45260
+ return parentUrl;
45261
+ }
45262
+ listenChanges(id, opts) {
45263
+ return this.dataService.listenChanges(this.data.id, {
45264
+ // Make sure to skip cache, because NOT the full graph will be fetched by subscription
45265
+ // When a changes occur, we call reload() to force refetching the full graph
45266
+ fetchPolicy: 'no-cache',
45267
+ ...opts,
45268
+ });
44819
45269
  }
44820
45270
  markForCheck() {
44821
45271
  this.cd.markForCheck();
44822
45272
  }
44823
- markAsLoading(opts) {
44824
- this.form.markAsLoading(opts);
45273
+ /**
45274
+ * Update the route, without reloading the component
45275
+ *
45276
+ * @param data
45277
+ * @param queryParams
45278
+ * @protected
45279
+ */
45280
+ async updateRoute(data, queryParams) {
45281
+ data = data || this.data;
45282
+ if (!data || !this.route)
45283
+ return false;
45284
+ const currId = this.route.snapshot.paramMap.get(this.pathIdAttribute);
45285
+ const futureId = isNotNil(data.id) ? data.id.toString() : 'new';
45286
+ if (queryParams) {
45287
+ this.queryParams = {
45288
+ ...this.queryParams,
45289
+ ...queryParams,
45290
+ };
45291
+ }
45292
+ if (currId === futureId) {
45293
+ // Update queryParam only (not the path)
45294
+ // /!\ We should get query Params from the updated route, and not from snapshot that can be outdated
45295
+ const actualQueryParams = await firstNotNilPromise(this.route.queryParams);
45296
+ if (!equals(actualQueryParams, this.queryParams)) {
45297
+ if (this.debug)
45298
+ console.debug(`${this._logPrefix}Updating route using queryParams: `, this.queryParams);
45299
+ return await this.router.navigate(['.'], {
45300
+ relativeTo: this.route,
45301
+ replaceUrl: false,
45302
+ queryParams: this.queryParams,
45303
+ state: { animated: false },
45304
+ });
45305
+ }
45306
+ }
45307
+ else {
45308
+ const path = this.computePageUrl(isNotNil(data.id) ? data.id : 'new');
45309
+ const commands = path && typeof path === 'string' ? path.split('/').slice(1) : path;
45310
+ if (isNotEmptyArray(commands)) {
45311
+ // Current route was '/new' => change to '/new?id=:id' (to allow CustomReuseStrategy to detect that route can be reused)
45312
+ if (currId === 'new' && this.queryParams[this.pathIdAttribute] !== futureId) {
45313
+ if (this.debug)
45314
+ console.debug(`${this._logPrefix}Updating route /new into /:id, using queryParams: `, queryParams);
45315
+ this.queryParams[this.pathIdAttribute] = futureId;
45316
+ const res = await this.router.navigate(['.'], {
45317
+ relativeTo: this.route,
45318
+ replaceUrl: true,
45319
+ queryParams: this.queryParams,
45320
+ state: { animated: false },
45321
+ });
45322
+ if (!res)
45323
+ return false;
45324
+ }
45325
+ if (this.debug)
45326
+ console.debug(`${this._logPrefix}Updating route to: ` + path);
45327
+ return await this.router.navigate(commands, {
45328
+ replaceUrl: true,
45329
+ queryParams: this.queryParams,
45330
+ state: { animated: false },
45331
+ });
45332
+ }
45333
+ return Promise.reject('Missing page URL');
45334
+ }
44825
45335
  }
44826
- markAsLoaded(opts) {
44827
- this.form.markAsLoaded(opts);
45336
+ /* -- private functions -- */
45337
+ /**
45338
+ * Open the first tab that is invalid
45339
+ */
45340
+ openFirstInvalidTab() {
45341
+ const invalidTabIndex = this.getFirstInvalidTabIndex();
45342
+ if (invalidTabIndex !== -1 && this.selectedTabIndex !== invalidTabIndex) {
45343
+ this.selectedTabIndex = invalidTabIndex;
45344
+ this.markForCheck();
45345
+ }
44828
45346
  }
44829
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: MessageModal, deps: [{ token: LocalSettingsService }, { token: i2$1.ModalController }, { token: AccountService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
44830
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: MessageModal, selector: "app-message-modal", inputs: { suggestFn: "suggestFn", data: "data", canSelectType: "canSelectType", canRecipientFilter: "canRecipientFilter", recipientFilterCount: "recipientFilterCount" }, viewQueries: [{ propertyName: "form", first: true, predicate: ["form"], descendants: true, static: true }], ngImport: i0, template: "<ion-header>\n <ion-toolbar color=\"secondary\">\n <ion-buttons slot=\"start\">\n <ion-button class=\"back-button\" (click)=\"cancel()\" visible-xs visible-sm visible-mobile>\n <ion-icon slot=\"icon-only\" name=\"arrow-back\"></ion-icon>\n </ion-button>\n </ion-buttons>\n\n <ion-title>\n {{ 'SOCIAL.MESSAGE.NEW.TITLE' | translate }}\n </ion-title>\n\n <ion-buttons slot=\"end\">\n <ion-button\n class=\"back-button\"\n (click)=\"doSubmit()\"\n [disabled]=\"!form.valid\"\n visible-xs\n visible-sm\n visible-mobile\n >\n <ion-icon slot=\"icon-only\" name=\"checkmark\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n\n<ion-content class=\"ion-padding\">\n <ion-item *ngIf=\"form.error\" visible-xs visible-sm visible-mobile lines=\"none\">\n <ion-icon color=\"danger\" slot=\"start\" name=\"alert-circle\"></ion-icon>\n <ion-label color=\"danger\" class=\"error\" [innerHTML]=\"form.error | translate\"></ion-label>\n </ion-item>\n\n <app-message-form\n #form\n (onSubmit)=\"doSubmit()\"\n (onCancel)=\"cancel()\"\n [canSelectType]=\"canSelectType\"\n [canRecipientFilter]=\"canRecipientFilter\"\n [recipientFilterCount]=\"recipientFilterCount\"\n [suggestFn]=\"suggestFn\"\n ></app-message-form>\n</ion-content>\n\n<ion-footer hidden-xs hidden-sm hidden-mobile>\n <ion-toolbar>\n <ion-row class=\"ion-no-padding\" nowrap>\n <ion-col></ion-col>\n\n <!-- buttons -->\n <ion-col size=\"auto\">\n <ion-button fill=\"clear\" color=\"dark\" (click)=\"cancel()\">\n <ion-label translate>COMMON.BTN_CANCEL</ion-label>\n </ion-button>\n\n <ion-button\n [fill]=\"form.invalid ? 'clear' : 'solid'\"\n [disabled]=\"form.loading || form.invalid\"\n (keyup.enter)=\"doSubmit()\"\n (click)=\"doSubmit()\"\n color=\"tertiary\"\n >\n <ion-label translate>COMMON.BTN_SEND</ion-label>\n </ion-button>\n </ion-col>\n </ion-row>\n </ion-toolbar>\n</ion-footer>\n", dependencies: [{ kind: "directive", type: i3$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i2$1.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i2$1.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$1.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i2$1.IonContent, selector: "ion-content", inputs: ["color", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$1.IonFooter, selector: "ion-footer", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$1.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$1.IonItem, selector: "ion-item", inputs: ["button", "color", "counter", "counterFormatter", "detail", "detailIcon", "disabled", "download", "fill", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "shape", "target", "type"] }, { kind: "component", type: i2$1.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$1.IonRow, selector: "ion-row" }, { kind: "component", type: i2$1.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2$1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "directive", type: i1$1.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { kind: "component", type: MessageForm, selector: "app-message-form", inputs: ["suggestFn", "subjectMinLength", "subjectMaxLength", "bodyMaxLength", "bodyAutoHeight", "canSelectType", "canRecipientFilter", "recipientFilterCount"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
45347
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AppEntityEditor, deps: "invalid", target: i0.ɵɵFactoryTarget.Directive });
45348
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.13", type: AppEntityEditor, usesInheritance: true, ngImport: i0 });
44831
45349
  }
44832
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: MessageModal, decorators: [{
44833
- type: Component,
44834
- args: [{ selector: 'app-message-modal', changeDetection: ChangeDetectionStrategy.OnPush, template: "<ion-header>\n <ion-toolbar color=\"secondary\">\n <ion-buttons slot=\"start\">\n <ion-button class=\"back-button\" (click)=\"cancel()\" visible-xs visible-sm visible-mobile>\n <ion-icon slot=\"icon-only\" name=\"arrow-back\"></ion-icon>\n </ion-button>\n </ion-buttons>\n\n <ion-title>\n {{ 'SOCIAL.MESSAGE.NEW.TITLE' | translate }}\n </ion-title>\n\n <ion-buttons slot=\"end\">\n <ion-button\n class=\"back-button\"\n (click)=\"doSubmit()\"\n [disabled]=\"!form.valid\"\n visible-xs\n visible-sm\n visible-mobile\n >\n <ion-icon slot=\"icon-only\" name=\"checkmark\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n\n<ion-content class=\"ion-padding\">\n <ion-item *ngIf=\"form.error\" visible-xs visible-sm visible-mobile lines=\"none\">\n <ion-icon color=\"danger\" slot=\"start\" name=\"alert-circle\"></ion-icon>\n <ion-label color=\"danger\" class=\"error\" [innerHTML]=\"form.error | translate\"></ion-label>\n </ion-item>\n\n <app-message-form\n #form\n (onSubmit)=\"doSubmit()\"\n (onCancel)=\"cancel()\"\n [canSelectType]=\"canSelectType\"\n [canRecipientFilter]=\"canRecipientFilter\"\n [recipientFilterCount]=\"recipientFilterCount\"\n [suggestFn]=\"suggestFn\"\n ></app-message-form>\n</ion-content>\n\n<ion-footer hidden-xs hidden-sm hidden-mobile>\n <ion-toolbar>\n <ion-row class=\"ion-no-padding\" nowrap>\n <ion-col></ion-col>\n\n <!-- buttons -->\n <ion-col size=\"auto\">\n <ion-button fill=\"clear\" color=\"dark\" (click)=\"cancel()\">\n <ion-label translate>COMMON.BTN_CANCEL</ion-label>\n </ion-button>\n\n <ion-button\n [fill]=\"form.invalid ? 'clear' : 'solid'\"\n [disabled]=\"form.loading || form.invalid\"\n (keyup.enter)=\"doSubmit()\"\n (click)=\"doSubmit()\"\n color=\"tertiary\"\n >\n <ion-label translate>COMMON.BTN_SEND</ion-label>\n </ion-button>\n </ion-col>\n </ion-row>\n </ion-toolbar>\n</ion-footer>\n" }]
44835
- }], ctorParameters: () => [{ type: LocalSettingsService }, { type: i2$1.ModalController }, { type: AccountService }, { type: i0.ChangeDetectorRef }], propDecorators: { suggestFn: [{
44836
- type: Input
44837
- }], data: [{
44838
- type: Input
44839
- }], canSelectType: [{
44840
- type: Input
44841
- }], canRecipientFilter: [{
44842
- type: Input
44843
- }], recipientFilterCount: [{
44844
- type: Input
44845
- }], form: [{
44846
- type: ViewChild,
44847
- args: ['form', { static: true }]
44848
- }] } });
45350
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AppEntityEditor, decorators: [{
45351
+ type: Directive
45352
+ }], ctorParameters: () => [{ type: i0.Injector }, { type: undefined }, { type: undefined }, { type: AppEditorOptions, decorators: [{
45353
+ type: Optional
45354
+ }] }] });
44849
45355
 
44850
45356
  class MessageModule {
44851
45357
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: MessageModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
44852
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.2.13", ngImport: i0, type: MessageModule, declarations: [MessageModal, MessageForm], imports: [CommonModule, CoreModule, SharedModule, MatChipsModule], exports: [MessageModal] });
44853
- static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: MessageModule, imports: [CommonModule, CoreModule, SharedModule, MatChipsModule] });
45358
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.2.13", ngImport: i0, type: MessageModule, declarations: [MessageModal, MessageForm], imports: [CommonModule, CoreModule, SharedModule, MatChipsModule, SharedDebugModule], exports: [MessageModal] });
45359
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: MessageModule, imports: [CommonModule, CoreModule, SharedModule, MatChipsModule, SharedDebugModule] });
44854
45360
  }
44855
45361
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: MessageModule, decorators: [{
44856
45362
  type: NgModule,
44857
45363
  args: [{
44858
- imports: [CommonModule, CoreModule, SharedModule, MatChipsModule],
45364
+ imports: [CommonModule, CoreModule, SharedModule, MatChipsModule, SharedDebugModule],
44859
45365
  declarations: [MessageModal, MessageForm],
44860
45366
  exports: [MessageModal],
44861
45367
  }]
@@ -45283,271 +45789,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
45283
45789
  }]
45284
45790
  }] });
45285
45791
 
45286
- const Mutations = {
45287
- send: gql$1 `
45288
- mutation SendMessage($data: MessageVOInput) {
45289
- done: sendMessage(message: $data)
45290
- }
45291
- `,
45292
- };
45293
- class MessageService extends BaseGraphqlService {
45294
- graphql;
45295
- translate;
45296
- modalCtrl;
45297
- toastController;
45298
- environment;
45299
- constructor(graphql, translate, modalCtrl, toastController, environment) {
45300
- super(graphql, environment);
45301
- this.graphql = graphql;
45302
- this.translate = translate;
45303
- this.modalCtrl = modalCtrl;
45304
- this.toastController = toastController;
45305
- this.environment = environment;
45306
- // For DEV only
45307
- this._debug = !environment.production;
45308
- }
45309
- /**
45310
- * Send a message to recipient(s)
45311
- *
45312
- * @param entity
45313
- * @param opts
45314
- */
45315
- async send(entity, opts) {
45316
- // Transform into json
45317
- const data = entity.asObject(MINIFY_ENTITY_FOR_POD);
45318
- const now = Date.now();
45319
- if (this._debug)
45320
- console.debug(`[message-service] Sending message...`);
45321
- try {
45322
- const { done } = await this.graphql.mutate({
45323
- mutation: Mutations.send,
45324
- variables: { data },
45325
- error: { code: SocialErrorCodes.SEND_MESSAGE_ERROR, message: 'SOCIAL.ERROR.SEND_MESSAGE_ERROR' },
45326
- });
45327
- if (this._debug)
45328
- console.debug(`[message-service] Send message [OK] in ${Date.now() - now}ms`);
45329
- if (done && (!opts || opts.showToast !== false)) {
45330
- await this.showToast({ type: 'info', message: 'SOCIAL.INFO.MESSAGE_SENT' });
45331
- }
45332
- return done;
45333
- }
45334
- catch (err) {
45335
- const error = (err && err.message) || err;
45336
- console.error(error);
45337
- // Show error
45338
- if (!opts || opts.showToast !== false) {
45339
- const message = error || 'SOCIAL.ERROR.SEND_MESSAGE_ERROR';
45340
- await this.showToast({ type: 'error', message });
45341
- }
45342
- return false;
45343
- }
45344
- }
45345
- async openComposeModal(options) {
45346
- const hasTopModal = !!(await this.modalCtrl.getTop());
45347
- const modal = await this.modalCtrl.create({
45348
- component: MessageModal,
45349
- componentProps: options,
45350
- cssClass: hasTopModal && 'stack-modal',
45351
- });
45352
- // Open the modal
45353
- await modal.present();
45354
- // On dismiss
45355
- const { data } = await modal.onDidDismiss();
45356
- if (!data || !(data instanceof Message))
45357
- return true; // CANCELLED
45358
- // Send message
45359
- return await this.send(data, { showToast: options?.showToast });
45360
- }
45361
- /* -- protected methods -- */
45362
- async showToast(opts) {
45363
- return Toasts.show(this.toastController, this.translate, {
45364
- type: 'info',
45365
- message: 'SOCIAL.INFO.MESSAGE_SENT',
45366
- ...opts,
45367
- });
45368
- }
45369
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: MessageService, deps: [{ token: GraphqlService }, { token: i1$1.TranslateService }, { token: i2$1.ModalController }, { token: i2$1.ToastController }, { token: ENVIRONMENT, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
45370
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: MessageService, providedIn: 'root' });
45371
- }
45372
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: MessageService, decorators: [{
45373
- type: Injectable,
45374
- args: [{ providedIn: 'root' }]
45375
- }], ctorParameters: () => [{ type: GraphqlService }, { type: i1$1.TranslateService }, { type: i2$1.ModalController }, { type: i2$1.ToastController }, { type: Environment, decorators: [{
45376
- type: Optional
45377
- }, {
45378
- type: Inject,
45379
- args: [ENVIRONMENT]
45380
- }] }] });
45381
-
45382
- const PersonFragments = {
45383
- person: gql$1 `
45384
- fragment PersonFragment on PersonVO {
45385
- id
45386
- firstName
45387
- lastName
45388
- email
45389
- pubkey
45390
- avatar
45391
- statusId
45392
- updateDate
45393
- creationDate
45394
- profiles
45395
- username
45396
- usernameExtranet
45397
- department {
45398
- id
45399
- label
45400
- name
45401
- logo
45402
- __typename
45403
- }
45404
- __typename
45405
- }
45406
- `,
45407
- };
45408
- // Load persons query
45409
- const PersonQueries = {
45410
- loadAll: gql$1 `
45411
- query Persons($offset: Int, $size: Int, $sortBy: String, $sortDirection: String, $filter: PersonFilterVOInput) {
45412
- data: persons(filter: $filter, offset: $offset, size: $size, sortBy: $sortBy, sortDirection: $sortDirection) {
45413
- ...PersonFragment
45414
- }
45415
- }
45416
- ${PersonFragments.person}
45417
- `,
45418
- loadAllWithTotal: gql$1 `
45419
- query PersonsWithTotal($offset: Int, $size: Int, $sortBy: String, $sortDirection: String, $filter: PersonFilterVOInput) {
45420
- data: persons(filter: $filter, offset: $offset, size: $size, sortBy: $sortBy, sortDirection: $sortDirection) {
45421
- ...PersonFragment
45422
- }
45423
- total: personsCount(filter: $filter)
45424
- }
45425
- ${PersonFragments.person}
45426
- `,
45427
- };
45428
- const PersonMutations = {
45429
- saveAll: gql$1 `
45430
- mutation savePersons($data: [PersonVOInput]) {
45431
- data: savePersons(persons: $data) {
45432
- ...PersonFragment
45433
- }
45434
- }
45435
- ${PersonFragments.person}
45436
- `,
45437
- deleteAll: gql$1 `
45438
- mutation deletePersons($ids: [Int]) {
45439
- deletePersons(ids: $ids)
45440
- }
45441
- `,
45442
- };
45443
- class PersonService extends BaseEntityService {
45444
- graphql;
45445
- platform;
45446
- network;
45447
- entities;
45448
- constructor(graphql, platform, network, entities) {
45449
- super(graphql, platform, Person, PersonFilter, {
45450
- queries: PersonQueries,
45451
- mutations: PersonMutations,
45452
- defaultSortBy: 'lastName',
45453
- defaultSortDirection: 'asc',
45454
- });
45455
- this.graphql = graphql;
45456
- this.platform = platform;
45457
- this.network = network;
45458
- this.entities = entities;
45459
- // for DEV only -----
45460
- this._debug = !environment.production;
45461
- }
45462
- async loadAll(offset, size, sortBy, sortDirection, filter, opts) {
45463
- const offline = this.network.offline && (!opts || opts.fetchPolicy !== 'network-only');
45464
- if (offline) {
45465
- return this.loadAllLocally(offset, size, sortBy, sortDirection, filter, opts);
45466
- }
45467
- return super.loadAll(offset, size, sortBy, sortDirection, filter, opts);
45468
- }
45469
- async loadAllLocally(offset, size, sortBy, sortDirection, filter, opts) {
45470
- filter = this.asFilter(filter);
45471
- const variables = {
45472
- offset: offset || 0,
45473
- size: size || 100,
45474
- sortBy: sortBy || this.defaultSortBy,
45475
- sortDirection: sortDirection || this.defaultSortDirection,
45476
- filter: filter && filter.asFilterFn(),
45477
- };
45478
- const { data, total } = await this.entities.loadAll('PersonVO', variables);
45479
- const entities = this.fromObjects(data, opts);
45480
- const res = { data: entities, total };
45481
- // Add fetch more function
45482
- const nextOffset = (offset || 0) + entities.length;
45483
- if (nextOffset < total) {
45484
- res.fetchMore = () => this.loadAllLocally(nextOffset, size, sortBy, sortDirection, filter, opts);
45485
- }
45486
- return res;
45487
- }
45488
- async suggest(value, filter, sortBy, sortDirection, opts) {
45489
- if (EntityUtils.isNotEmpty(value, 'id'))
45490
- return { data: [value] };
45491
- value = (typeof value === 'string' && value !== '*' && value) || undefined;
45492
- sortBy = sortBy || filter.searchAttribute || (filter.searchAttributes && filter.searchAttributes[0]);
45493
- return this.loadAll(0, !value ? 30 : 10, sortBy, sortDirection, {
45494
- ...filter,
45495
- searchText: value,
45496
- statusIds: (filter && filter.statusIds) || [StatusIds.ENABLE, StatusIds.TEMPORARY],
45497
- userProfiles: filter && filter.userProfiles,
45498
- }, {
45499
- withTotal: true /* need by autocomplete */,
45500
- ...opts,
45501
- });
45502
- }
45503
- async executeImport(filter, opts) {
45504
- const maxProgression = (opts && opts.maxProgression) || 100;
45505
- filter = {
45506
- ...filter,
45507
- statusIds: [StatusIds.ENABLE, StatusIds.TEMPORARY],
45508
- userProfiles: ['SUPERVISOR', 'USER', 'GUEST'],
45509
- };
45510
- console.info('[person-service] Importing persons...');
45511
- const res = await JobUtils.fetchAllPages((offset, size) => this.loadAll(offset, size, 'id', null, filter, {
45512
- debug: false,
45513
- fetchPolicy: 'network-only',
45514
- withTotal: offset === 0, // Compute total only once
45515
- toEntity: false,
45516
- }), { progression: opts?.progression, maxProgression: maxProgression * 0.9 });
45517
- // Save result locally
45518
- await this.entities.saveAll(res.data, { entityName: 'PersonVO', reset: true });
45519
- }
45520
- async loadById(id, opts) {
45521
- const { data } = await this.loadAll(0, 1, null, null, { includedIds: [id] }, { withTotal: false, ...opts });
45522
- const source = isNotEmptyArray(data) ? data[0] : { id };
45523
- return this.fromObject(source, opts);
45524
- }
45525
- async loadByPubkey(pubkey, opts) {
45526
- const { data } = await this.loadAll(0, 1, null, null, { pubkey }, { withTotal: false, ...opts });
45527
- const source = isNotEmptyArray(data) ? data[0] : { pubkey };
45528
- return this.fromObject(source, opts);
45529
- }
45530
- /* -- protected methods -- */
45531
- asObject(source) {
45532
- if (!source)
45533
- return undefined;
45534
- if (!(source instanceof Person)) {
45535
- source = Person.fromObject(source);
45536
- }
45537
- const target = source.asObject();
45538
- // Not known in server GraphQL schema
45539
- delete target.mainProfile;
45540
- target.department = source.department?.asObject();
45541
- return target;
45542
- }
45543
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: PersonService, deps: [{ token: GraphqlService }, { token: PlatformService }, { token: NetworkService }, { token: EntitiesStorage }], target: i0.ɵɵFactoryTarget.Injectable });
45544
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: PersonService, providedIn: 'root' });
45545
- }
45546
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: PersonService, decorators: [{
45547
- type: Injectable,
45548
- args: [{ providedIn: 'root' }]
45549
- }], ctorParameters: () => [{ type: GraphqlService }, { type: PlatformService }, { type: NetworkService }, { type: EntitiesStorage }] });
45550
-
45551
45792
  class UsersPage extends AppTable {
45552
45793
  accountService;
45553
45794
  validatorService;
@@ -50149,11 +50390,11 @@ class FeedTestingPage {
50149
50390
  this.cd.markForCheck();
50150
50391
  }
50151
50392
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FeedTestingPage, deps: [{ token: LocalSettingsService }, { token: i0.ChangeDetectorRef }, { token: ENVIRONMENT, optional: true }], target: i0.ɵɵFactoryTarget.Component });
50152
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: FeedTestingPage, selector: "app-feed-test", ngImport: i0, template: "<ion-header>\n <ion-toolbar color=\"primary\">\n <ion-buttons slot=\"start\">\n <ion-back-button></ion-back-button>\n </ion-buttons>\n\n <ion-title>Feed test page</ion-title>\n </ion-toolbar>\n</ion-header>\n\n<ion-content>\n <ion-grid class=\"form-container ion-padding-top\">\n <ion-row>\n <ion-col size=\"12\">\n <mat-form-field [subscriptSizing]=\"'dynamic'\">\n <mat-label>Feed URL(s)</mat-label>\n <mat-select [(value)]=\"feedUrls\" multiple>\n <mat-option *ngFor=\"let feedUrl of availableFeedUrls\" [value]=\"feedUrl\">\n {{ feedUrl }}\n </mat-option>\n <mat-option>(Use environment)</mat-option>\n </mat-select>\n </mat-form-field>\n </ion-col>\n </ion-row>\n <ion-row>\n <ion-col size=\"3\">\n <mat-form-field [subscriptSizing]=\"'dynamic'\">\n <mat-label>Header color</mat-label>\n <mat-select [(value)]=\"headerColor\">\n <mat-option *ngFor=\"let color of availableColors\" [value]=\"color\">\n {{ color }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </ion-col>\n <ion-col size=\"3\">\n <mat-form-field [subscriptSizing]=\"'dynamic'\">\n <mat-label>Card color</mat-label>\n <mat-select [(value)]=\"cardColor\">\n <mat-option *ngFor=\"let color of availableColors\" [value]=\"color\">\n {{ color }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </ion-col>\n <ion-col size=\"3\">\n <mat-form-field [subscriptSizing]=\"'dynamic'\">\n <mat-label>Shape</mat-label>\n <mat-select [(value)]=\"shape\">\n <mat-option *ngFor=\"let shape of availableShapes\" [value]=\"shape\">\n {{ shape }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </ion-col>\n <ion-col size=\"3\">\n <mat-checkbox (change)=\"debug = $event.checked\" [checked]=\"debug\">Debug ?</mat-checkbox>\n </ion-col>\n </ion-row>\n </ion-grid>\n\n <div class=\"ion-padding\">\n <app-feed\n [feedUrls]=\"feedUrls\"\n [headerColor]=\"headerColor\"\n [cardColor]=\"cardColor\"\n [shape]=\"shape\"\n [debug]=\"debug\"\n ></app-feed>\n </div>\n</ion-content>\n", dependencies: [{ kind: "directive", type: i3$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "component", type: i2$1.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$1.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i2$1.IonContent, selector: "ion-content", inputs: ["color", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$1.IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: i2$1.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$1.IonRow, selector: "ion-row" }, { kind: "component", type: i2$1.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2$1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonBackButton, selector: "ion-back-button" }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "component", type: i5.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "component", type: i6$3.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i2.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: FeedsComponent, selector: "app-feed", inputs: ["debug", "mobile", "showHeader", "showReadMoreButton", "headerColor", "cardColor", "shape", "class", "itemId", "filterItem", "feeds", "feedUrls", "maxAgeInMonths", "maxContentLength"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
50393
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: FeedTestingPage, selector: "app-feed-test", ngImport: i0, template: "<ion-header>\n <ion-toolbar color=\"primary\">\n <ion-buttons slot=\"start\">\n <ion-back-button></ion-back-button>\n </ion-buttons>\n\n <ion-title>Feed test page</ion-title>\n </ion-toolbar>\n</ion-header>\n\n<ion-content>\n <ion-grid class=\"form-container ion-padding-top\">\n <ion-row>\n <ion-col size=\"12\">\n <mat-form-field [subscriptSizing]=\"'dynamic'\">\n <mat-label>Feed URL(s)</mat-label>\n <mat-select [(value)]=\"feedUrls\" multiple>\n <mat-option *ngFor=\"let feedUrl of availableFeedUrls\" [value]=\"feedUrl\">\n {{ feedUrl }}\n </mat-option>\n <mat-option>(Use environment)</mat-option>\n </mat-select>\n </mat-form-field>\n </ion-col>\n </ion-row>\n <ion-row>\n <ion-col size=\"3\">\n <mat-form-field [subscriptSizing]=\"'dynamic'\">\n <mat-label>Header color</mat-label>\n <mat-select [(value)]=\"headerColor\">\n <mat-option *ngFor=\"let color of availableColors\" [value]=\"color\">\n {{ color }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </ion-col>\n <ion-col size=\"3\">\n <mat-form-field [subscriptSizing]=\"'dynamic'\">\n <mat-label>Card color</mat-label>\n <mat-select [(value)]=\"cardColor\">\n <mat-option *ngFor=\"let color of availableColors\" [value]=\"color\">\n {{ color }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </ion-col>\n <ion-col size=\"3\">\n <mat-form-field [subscriptSizing]=\"'dynamic'\">\n <mat-label>Shape</mat-label>\n <mat-select [(value)]=\"shape\">\n <mat-option *ngFor=\"let shape of availableShapes\" [value]=\"shape\">\n {{ shape }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </ion-col>\n <ion-col size=\"3\">\n <mat-checkbox (change)=\"debug = $event.checked\" [checked]=\"debug\">Debug ?</mat-checkbox>\n </ion-col>\n </ion-row>\n </ion-grid>\n\n <div class=\"ion-padding\">\n <app-feed\n [urls]=\"feedUrls\"\n [headerColor]=\"headerColor\"\n [cardColor]=\"cardColor\"\n [shape]=\"shape\"\n [debug]=\"debug\"\n ></app-feed>\n </div>\n</ion-content>\n", dependencies: [{ kind: "directive", type: i3$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "component", type: i2$1.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$1.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i2$1.IonContent, selector: "ion-content", inputs: ["color", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$1.IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: i2$1.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$1.IonRow, selector: "ion-row" }, { kind: "component", type: i2$1.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2$1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonBackButton, selector: "ion-back-button" }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "component", type: i5.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "component", type: i6$3.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i2.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: FeedsComponent, selector: "app-feed", inputs: ["debug", "mobile", "showHeader", "showReadMoreButton", "headerColor", "cardColor", "shape", "class", "itemId", "filterItem", "feeds", "urls", "maxAgeInMonths", "maxContentLength"], outputs: ["editItem", "deleteItem"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
50153
50394
  }
50154
50395
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FeedTestingPage, decorators: [{
50155
50396
  type: Component,
50156
- args: [{ selector: 'app-feed-test', changeDetection: ChangeDetectionStrategy.OnPush, template: "<ion-header>\n <ion-toolbar color=\"primary\">\n <ion-buttons slot=\"start\">\n <ion-back-button></ion-back-button>\n </ion-buttons>\n\n <ion-title>Feed test page</ion-title>\n </ion-toolbar>\n</ion-header>\n\n<ion-content>\n <ion-grid class=\"form-container ion-padding-top\">\n <ion-row>\n <ion-col size=\"12\">\n <mat-form-field [subscriptSizing]=\"'dynamic'\">\n <mat-label>Feed URL(s)</mat-label>\n <mat-select [(value)]=\"feedUrls\" multiple>\n <mat-option *ngFor=\"let feedUrl of availableFeedUrls\" [value]=\"feedUrl\">\n {{ feedUrl }}\n </mat-option>\n <mat-option>(Use environment)</mat-option>\n </mat-select>\n </mat-form-field>\n </ion-col>\n </ion-row>\n <ion-row>\n <ion-col size=\"3\">\n <mat-form-field [subscriptSizing]=\"'dynamic'\">\n <mat-label>Header color</mat-label>\n <mat-select [(value)]=\"headerColor\">\n <mat-option *ngFor=\"let color of availableColors\" [value]=\"color\">\n {{ color }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </ion-col>\n <ion-col size=\"3\">\n <mat-form-field [subscriptSizing]=\"'dynamic'\">\n <mat-label>Card color</mat-label>\n <mat-select [(value)]=\"cardColor\">\n <mat-option *ngFor=\"let color of availableColors\" [value]=\"color\">\n {{ color }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </ion-col>\n <ion-col size=\"3\">\n <mat-form-field [subscriptSizing]=\"'dynamic'\">\n <mat-label>Shape</mat-label>\n <mat-select [(value)]=\"shape\">\n <mat-option *ngFor=\"let shape of availableShapes\" [value]=\"shape\">\n {{ shape }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </ion-col>\n <ion-col size=\"3\">\n <mat-checkbox (change)=\"debug = $event.checked\" [checked]=\"debug\">Debug ?</mat-checkbox>\n </ion-col>\n </ion-row>\n </ion-grid>\n\n <div class=\"ion-padding\">\n <app-feed\n [feedUrls]=\"feedUrls\"\n [headerColor]=\"headerColor\"\n [cardColor]=\"cardColor\"\n [shape]=\"shape\"\n [debug]=\"debug\"\n ></app-feed>\n </div>\n</ion-content>\n" }]
50397
+ args: [{ selector: 'app-feed-test', changeDetection: ChangeDetectionStrategy.OnPush, template: "<ion-header>\n <ion-toolbar color=\"primary\">\n <ion-buttons slot=\"start\">\n <ion-back-button></ion-back-button>\n </ion-buttons>\n\n <ion-title>Feed test page</ion-title>\n </ion-toolbar>\n</ion-header>\n\n<ion-content>\n <ion-grid class=\"form-container ion-padding-top\">\n <ion-row>\n <ion-col size=\"12\">\n <mat-form-field [subscriptSizing]=\"'dynamic'\">\n <mat-label>Feed URL(s)</mat-label>\n <mat-select [(value)]=\"feedUrls\" multiple>\n <mat-option *ngFor=\"let feedUrl of availableFeedUrls\" [value]=\"feedUrl\">\n {{ feedUrl }}\n </mat-option>\n <mat-option>(Use environment)</mat-option>\n </mat-select>\n </mat-form-field>\n </ion-col>\n </ion-row>\n <ion-row>\n <ion-col size=\"3\">\n <mat-form-field [subscriptSizing]=\"'dynamic'\">\n <mat-label>Header color</mat-label>\n <mat-select [(value)]=\"headerColor\">\n <mat-option *ngFor=\"let color of availableColors\" [value]=\"color\">\n {{ color }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </ion-col>\n <ion-col size=\"3\">\n <mat-form-field [subscriptSizing]=\"'dynamic'\">\n <mat-label>Card color</mat-label>\n <mat-select [(value)]=\"cardColor\">\n <mat-option *ngFor=\"let color of availableColors\" [value]=\"color\">\n {{ color }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </ion-col>\n <ion-col size=\"3\">\n <mat-form-field [subscriptSizing]=\"'dynamic'\">\n <mat-label>Shape</mat-label>\n <mat-select [(value)]=\"shape\">\n <mat-option *ngFor=\"let shape of availableShapes\" [value]=\"shape\">\n {{ shape }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </ion-col>\n <ion-col size=\"3\">\n <mat-checkbox (change)=\"debug = $event.checked\" [checked]=\"debug\">Debug ?</mat-checkbox>\n </ion-col>\n </ion-row>\n </ion-grid>\n\n <div class=\"ion-padding\">\n <app-feed\n [urls]=\"feedUrls\"\n [headerColor]=\"headerColor\"\n [cardColor]=\"cardColor\"\n [shape]=\"shape\"\n [debug]=\"debug\"\n ></app-feed>\n </div>\n</ion-content>\n" }]
50157
50398
  }], ctorParameters: () => [{ type: LocalSettingsService }, { type: i0.ChangeDetectorRef }, { type: Environment, decorators: [{
50158
50399
  type: Optional
50159
50400
  }, {
@@ -50268,5 +50509,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
50268
50509
  * Generated bundle index. Do not edit.
50269
50510
  */
50270
50511
 
50271
- export { APP_ABOUT_DEVELOPERS, APP_ABOUT_PARTNERS, APP_CONFIG_OPTIONS, APP_DEBUG_DATA_SERVICE, APP_FEED_SERVICE, APP_FORM_ERROR_I18N_KEYS, APP_GRAPHQL_FRAGMENTS, APP_GRAPHQL_TYPE_POLICIES, APP_HOME_BUTTONS, APP_HOME_CONFIG, APP_HOTKEYS_CONFIG, APP_JOB_PROGRESSION_SERVICE, APP_LOCALES, APP_LOCAL_SETTINGS, APP_LOCAL_SETTINGS_OPTIONS, APP_LOCAL_STORAGE_TYPE_POLICIES, APP_LOGGING_SERVICE, APP_MENU_ITEMS, APP_MENU_OPTIONS, APP_NAMED_FILTER_SERVICE, APP_PROGRESS_BAR_SERVICE, APP_SETTINGS_MENU_ITEMS, APP_STORAGE, APP_STORAGE_EXPLORER_PROTECTED_KEYS, APP_TESTING_PAGES, APP_USER_EVENT_LIST_INFINITE_SCROLL_THRESHOLD, APP_USER_EVENT_SERVICE, APP_USER_SETTINGS_OPTIONS, APP_USER_TOKEN_SCOPES, AboutModal, AbstractNamedFilterService, AbstractSelectionModelPipe, AbstractTableSelectionPipe, AbstractUserEventService, Account, AccountPage, AccountService, AccountToStringPipe, AccountUtils, ActionsColumnComponent, AdminModule, AdminRoutingModule, AdminUsersModule, Alerts, AndroidOsEnvironment, AppAboutModalModule, AppAccountModule, AppAsyncTable, AppAuthForm, AppAuthModal, AppAuthModule, AppChangePasswordModule, AppChangePasswordPage, AppEditor, AppEditorOptions, AppEntityEditor, AppEntityEditorModal, AppEntityEditorModalOptions, AppEntityFormModule, AppForm, AppFormArray, AppFormButtonsBarModule, AppFormContainer, AppFormField, AppFormModule, AppFormProvider, AppFormUtils, AppGestureConfig, AppGraphQLModule, AppHomePageModule, AppIconComponent, AppIconModule, AppImageGalleryComponent, AppInMemoryTable, AppInstallUpgradeCard, AppInstallUpgradeCardModule, AppListForm, AppListFormModule, AppLoadingSpinner, AppMarkdownContent, AppMarkdownModal, AppMenuModule, AppNullForm, AppPropertiesForm, AppPropertiesFormModule, AppPropertiesTable, AppPropertiesUtils, AppPropertyUtils, AppRegisterModule, AppResetPasswordModal, AppRowField, AppSelectPeerModule, AppSettingsPageModule, AppTabEditor, AppTabEditorOptions, AppTable, AppTableModule, AppTableUtils, AppTextPopoverModule, AppUpdateOfflineModeCard, AppUpdateOfflineModeCardModule, AppValidatorService, AppendQueryParamsPipePipe, ArrayDistinctPipe, ArrayFilterPipe, ArrayFindByPropertyPipe, ArrayFirstPipe, ArrayFormTestPage, ArrayIncludesPipe, ArrayJoinPipe, ArrayLastPipe, ArrayLengthPipe, ArrayMapPipe, ArrayPluckPipe, ArraySortPipe, AsAnyPipe, AsArrayPipe, AsBooleanPipe, AsFloatLabelTypePipe, AsObservablePipe, AudioProvider, AudioTestingModule, AudioTestingPage, AuthGuardService, AutoResizeDirective, AutoTitleDirective, AutocompleteTestPage, AutofocusDirective, BadgeDirective, BadgeNumberPipe, Base58, BaseEntityService, BaseGraphqlService, BaseGraphqlServiceOptions, BaseReferential, Beans, BooleanFormatPipe, BooleanTestPage, CORE_CONFIG_OPTIONS, CORE_TESTING_PAGES, CapitalizePipe, CellValueChangeListener, ChangePasswordForm, ChipsTestPage, Color, ColorScale, ComponentDirtyGuard, ConfigFragments, ConfigService, Configuration, CoreModule, CorePipesModule, CoreTestingModule, CryptoService, CsvUtils, DATE_ISO_PATTERN, DATE_MATCH_REGEXP, DATE_PATTERN, DATE_UNIX_MS_TIMESTAMP, DATE_UNIX_TIMESTAMP, DEFAULT_JOIN_ARRAY_VALUES_SEPARATOR, DEFAULT_JOIN_PROPERTIES_SEPARATOR, DEFAULT_MENU_SHOW_WHEN, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLACEHOLDER_CHAR, DEFAULT_REQUIRED_COLUMNS, DateDiffDurationPipe, DateFormatPipe, DateFormatService, DateFromNowPipe, DateShortTestPage, DateTestPage, DateTimeTestPage, DateUtils, DebugComponent, Department, DepartmentToStringPipe, DisplayWithPipe, DragAndDropDirective, DurationPipe, DurationTestPage, ED25519_SEED_LENGTH, EMPTY_PLACEHOLDER_CHAR, EMPTY_PLACEHOLDER_CHAR_REGEXP_GLOBAL, ENTITIES_STORAGE_KEY_PREFIX, ENVIRONMENT, EmptyArrayPipe, EntitiesAsyncTableDataSource, EntitiesStorage, EntitiesTableDataSource, Entity, EntityClass, EntityClasses, EntityFilter, EntityFilterUtils, EntityMetadataComponent, EntityStore, EntityUtils, Environment, EnvironmentHttpLoader, EnvironmentLoader, ErrorCodes, EvenPipe, FeedDirective, FeedModule, FeedPage, FeedService, FeedsComponent, FileResponse, FileService, FileSizePipe, FilesUtils, FirstFalsePipe, FirstPipe, FirstTruePipe, FormArrayHelper, FormArrayTestModule, FormButtonsBarComponent, FormButtonsBarToken, FormErrorPipe, FormErrorTranslatePipe, FormErrorTranslator, FormFieldDefinitionUtils, FormFieldValuesHolder, FormGetArrayPipe, FormGetControlPipe, FormGetGroupPipe, FormGetPipe, FormGetValuePipe, GalleryTestPage, GeolocationUtils, GraphqlService, HAMMER_PRESS_TIME, HAMMER_TAP_TIME, HighlightPipe, HomePage, Hotkeys, HotkeysDialogComponent, IMAGE_DEFAULTS, IPosition, ImageAttachment, ImageAttachmentFilter, ImageAttachmentService, ImageGalleryModule, ImageGalleryTestingModule, ImageModule, ImageService, ImagesUtils, InMemoryEntitiesService, IsAllSelectedPipe, IsEmptySelectionPipe, IsLoginAccountPipe, IsMultipleSelectionPipe, IsNilOrBlankPipe, IsNilOrNaNPipe, IsNilPipe, IsNotAllSelectedPipe, IsNotEmptySelectionPipe, IsNotNilOrBlankPipe, IsNotNilOrNaNPipe, IsNotNilPipe, IsOnDeskPipe, IsOnFieldPipe, IsSelectedPipe, IsSingleSelectionPipe, IsValidDatePipe, JobModule, JobProgression, JobProgressionComponent, JobProgressionIcon, JobProgressionList, JobProgressionService, JobProgressionTestService, JobProgressionTestingPage, JobTestingModule, JobUtils, JsonFeedUtils, JsonUtils, KEYBOARD_HIDE_DELAY_MS, LAT_LONG_PATTERNS, LAT_LONG_PATTERN_MAX_DECIMALS, LAT_LONG_VALUE_MAX_DECIMALS, LatLongFormatPipe, LatLongTestPage, LatitudeFormatPipe, LocalSettingsService, LogLevel, LogUtils, Logger, LoggingService, LoggingServiceModule, LongitudeFormatPipe, MASKS, MASK_RANGES, MAT_FORM_FIELD_DEFAULT_APPEARANCE, MAT_FORM_FIELD_DEFAULT_SUBSCRIPT_SIZING, MINIFY_ENTITY_FOR_LOCAL_STORAGE, MINIFY_ENTITY_FOR_POD, MOMENT_NO_TIME_PROPERTY, MapGetPipe, MapKeysPipe, MapPipe, MapToPipe, MapValuesPipe, MarkdownDirective, MarkdownService, MarkdownTestPage, MarkdownTestingModule, MarkdownUtils, MaskitoPlaceholderPipe, MaskitoTestPage, MatAutocompleteConfigHolder, MatAutocompleteField, MatAutocompleteFieldUtils, MatBadgeTestPage, MatBooleanField, MatChipsField, MatColorPipe, MatCommonTestPage, MatDate, MatDateShort, MatDateTime, MatDuration, MatLatLongField, MatLatLongFieldInput, MatPaginatorI18n, MatStepperI18n, MatSwipeField, MaterialTestingModule, MathAbsPipe, MenuComponent, MenuItem, MenuItems, MenuOptions, MenuService, MenuTestingModule, MenuTestingPage, Message, MessageFilter, MessageForm, MessageModal, MessageModule, MessageService, MessageTypeList, MessageTypes, MimeTypes, ModalToolbarComponent, NETWORK_DEFAULT_CONNECTION_TIMEOUT, NOOP_ITEM_FILTER, NamedFilter, NamedFilterFilter, NamedFilterSelector, NamedFilterSelectorTestingModule, NamedFilterSelectorTestingPage, NavActionsColumnComponent, NetworkService, NetworkUtils, NewTokenForm, NewTokenModal, NgInitDirective, NgVarDirective, NoHtmlPipe, NotEmptyArrayPipe, NumberFormatPipe, ObservableTestPage, OddPipe, OtherMenuTestingPage, PEER_URL_REGEXP, PLUS_PLACEHOLDER_CHAR_REGEXP_GLOBAL, PRINT_ID_QUERY_PARAM, PRINT_LOADING_STORAGE_KEY_PREFIX, PRIORITIZED_AUTHORITIES, PUBKEY_REGEXP, Peer, Person, PersonFilter, PersonFragments, PersonService, PersonToStringPipe, PersonUtils, PersonValidatorService, PlatformService, PrintService, ProgressBarService, ProgressInterceptor, PropertiesFormTestPage, PropertiesFormTestingModule, PropertyEntity, PropertyEntityFilter, PropertyEntityValidator, PropertyFormatPipe, PropertyGetPipe, RESERVED_END_COLUMNS, RESERVED_START_COLUMNS, Referential, ReferentialFilter, ReferentialRef, ReferentialToStringPipe, ReferentialUtils, ReferentialValidatorService, RegExpUtils, RegisterConfirmPage, RegisterForm, RegisterModal, ResizableComponent, ResizableDirective, ResizableModule, RoundPipe, RxStateComputed, RxStateModule, RxStateProperty, RxStateRegister, RxStateSelect, SCRYPT_PARAMS, SETTINGS_COMPACT_ROWS, SETTINGS_DISPLAY_COLUMNS, SETTINGS_FILTER, SETTINGS_PAGE_SIZE, SETTINGS_SORTED_COLUMN, SETTINGS_STORAGE_KEY, SETTINGS_TRANSIENT_PROPERTIES, SHARED_MATERIAL_TESTING_PAGES, SHARED_STORAGE_TESTING_PAGES, SHARED_TESTING_PAGES, SOCIAL_CONFIG_OPTIONS, SOCIAL_TESTING_PAGES, SPACE_PLACEHOLDER_CHAR, SPACE_PLACEHOLDER_CHAR_REGEXP_GLOBAL, SafeHtmlPipe, SafeStylePipe, SelectPeerModal, SelectionLengthPipe, ServerErrorCodes, SettingsPage, SharedAsyncValidators, SharedBadgeModule, SharedDebugModule, SharedDirectivesModule, SharedFormArrayValidators, SharedFormGroupValidators, SharedHotkeysModule, SharedMarkdownModule, SharedMatAutocompleteModule, SharedMatBooleanModule, SharedMatChipsModule, SharedMatDateTimeModule, SharedMatDurationModule, SharedMatLatLongModule, SharedMatSwipeModule, SharedMaterialModule, SharedModule, SharedNamedFilterModule, SharedPipesModule, SharedRoutingModule, SharedTestingModule, SharedTestsPage, SharedTextFormModule, SharedToolbarModule, SharedValidators, SocialErrorCodes, SocialModule, SocialModuleOptionsToken, SocialTestingModule, Software, SplitArrayInChunksPipe, StartableService, StatusById, StatusIds, StatusList, StorageDrivers, StorageExplorerComponent, StorageExplorerModule, StorageExplorerTestingModule, StorageExplorerTestingRoutingModule, StorageService, StrIncludesPipe, StrLengthPipe, StrReplacePipe, SubMenuTabDirective, SwipeTestPage, TABLE_SETTINGS_ENUM, TOOLBAR_HEADER_ID, Table2TestPage, TableSelectColumnsComponent, TableTestPage, TableTestingModule, TableValidatorService, TextForm, TextFormTestingPage, TextPopover, TextPopoverTestingModule, TextPopoverTestingPage, ThrottledClickDirective, ToStringPipe, ToastTestingModule, ToastTestingPage, Toasts, TokenScope, ToolbarComponent, ToolbarToken, TranslatablePipe, TranslateContextPipe, TranslateContextService, TreeItemEntityUtils, TruncHtmlPipe, TruncTextPipe, TruncateHtmlPipe, UploadFile, UploadFileComponent, UploadFilePopover, UploadFileTestingModule, UploadFileTestingPage, UriUtils, UrlUtils, UserEventModule, UserEventNotificationIcon, UserEventNotificationList, UserEventNotificationModal, UserEventTestService, UserEventTestingModule, UserEventTestingPage, UserSettings, UserToken, UserTokenTable, UsersPage, ValueFormatPipe, VersionUtils, accountToString, adaptValueToControl, addValueInArray, arrayDistinct, arrayResize, arraySize, asInputElement, base64ArrayBuffer, booleanToString, canHaveFocus, capitalizeFirstLetter, chainPromises, changeCaseToUnderscore, clearValueInArray, collectByProperty, compareVersionNumbers, computeDecimalDegrees, computeDecimalPart, copyEntity2Form, createPromiseEvent, createPromiseEventEmitter, departmentToString, departmentsToString, disableAndClearControl, disableAndClearControls, disableControl, disableControls, emitPromiseEvent, enableControl, enableControls, enableRxStateProdMode, entityToString, equals, equalsOrNil, escapeRegExp, expansionAnimation, fadeInAnimation, fadeInOutAnimation, fadeInSlowAnimation, filterFalse, filterFormErrors, filterFormErrorsByPath, filterFormErrorsByPrefix, filterNotNil, filterNumberInput, filterTrue, findParentWithClass, firstArrayValue, firstFalse, firstFalsePromise, firstNotNil, firstNotNilPromise, firstTrue, firstTruePromise, focusInput, focusNextInput, focusPreviousInput, formatLatLong, formatLatitude, formatLongitude, fromDateISOString, fromScrollEndEvent, fromUnixMsTimestamp, fromUnixTimestamp, getCaretPosition, getColorContrast, getColorShade, getColorTint, getControlFromPath, getFocusableInputElements, getFormErrors, getFormValueFromEntity, getInputRangeFromCaretIndex, getInputSelectionRangesFromMask, getProperty, getPropertyByPath, getPropertyByPathAsString, getRandomImage, getRandomImageWithCredit, getUserAgent, hexToRgb, hexToRgbArray, initArrayControlsFromValues, interpolateString, intersectArrays, isAndroid, isBlankString, isCapacitor, isChrome, isControlHasInput, isEdge, isEmptyArray, isEntityService, isFirefox, isIOS, isInputElement, isInstanceOf, isInt, isIpad, isMacOS, isMobile, isNil, isNilOrBlank, isNilOrNaN, isNotEmptyArray, isNotNil, isNotNilBoolean, isNotNilObject, isNotNilOrBlank, isNotNilOrNaN, isNotNilString, isNumber, isNumberRange, isOnFieldMode, isPrint, isProgressEvent, isPromise, isResponseEvent, isSafari, isSameVersion, isStartableService, isTouchUi, isVersionCompatible, isWindows, joinProperties, joinPropertiesPath, lastArrayValue, logFormErrors, markAllAsTouched, markAsUntouched, markControlAsTouched, markFormGroupAsTouched, maskitoAutoSelectByMaskPattern, maskitoPrefixPlugin, matchMedia, matchUpperCase, mergeLoadResult, mixHex, moveInputCaretToSeparator, newArray, noHtml, noTrailingSlash, notNilOrDefault, nullIfNilOrBlank, nullIfUndefined, numberOrNilAttribute, numberToString, parseLatitudeOrLongitude, propertiesPathComparator, propertyComparator, propertyPathComparator, referentialToString, referentialsToString, remove, removeAll, removeDiacritics, removeDuplicatesFromArray, removeEnd, removeValueInArray, replaceAll, resetCalculatedValue, resizeArray, rgbArrayToHex, rgbToHex, round, scrollFactory, selectInputContent, selectInputContentFromEvent, selectInputRange, setCalculatedValue, setControlEnabled, setControlsEnabled, setFormErrors, setPropertyByPath, setTabIndex, sleep, slideDownAnimation, slideInAnimation, slideInOutAnimation, slideUpDownAnimation, sort, splitArrayInChunks, splitById, splitByProperty, splitDegreesToDDArray, splitDegreesToDDMMArray, splitDegreesToDDMMSSArray, startsWithUpperCase, suggestFromArray, suggestFromStringArray, tabindexComparator, testUserAgent, toBoolean, toDateISOString, toDuration, toFloat, toInt, toNotNil, toNumber, trimEmptyToNull, truncateHtml, uncapitalizeFirstLetter, undefinedIfNull, underscoreToChangeCase, updateValueAndValidity, waitFor, waitForFalse, waitForTrue, waitIdle, waitWhilePending };
50512
+ export { APP_ABOUT_DEVELOPERS, APP_ABOUT_PARTNERS, APP_CONFIG_OPTIONS, APP_DEBUG_DATA_SERVICE, APP_FEED_SERVICE, APP_FORM_ERROR_I18N_KEYS, APP_GRAPHQL_FRAGMENTS, APP_GRAPHQL_TYPE_POLICIES, APP_HOME_BUTTONS, APP_HOME_CONFIG, APP_HOTKEYS_CONFIG, APP_JOB_PROGRESSION_SERVICE, APP_LOCALES, APP_LOCAL_SETTINGS, APP_LOCAL_SETTINGS_OPTIONS, APP_LOCAL_STORAGE_TYPE_POLICIES, APP_LOGGING_SERVICE, APP_MENU_ITEMS, APP_MENU_OPTIONS, APP_NAMED_FILTER_SERVICE, APP_PROGRESS_BAR_SERVICE, APP_SETTINGS_MENU_ITEMS, APP_STORAGE, APP_STORAGE_EXPLORER_PROTECTED_KEYS, APP_TESTING_PAGES, APP_USER_EVENT_LIST_INFINITE_SCROLL_THRESHOLD, APP_USER_EVENT_SERVICE, APP_USER_SETTINGS_OPTIONS, APP_USER_TOKEN_SCOPES, AboutModal, AbstractNamedFilterService, AbstractSelectionModelPipe, AbstractTableSelectionPipe, AbstractUserEventService, Account, AccountPage, AccountService, AccountToStringPipe, AccountUtils, ActionsColumnComponent, AdminModule, AdminRoutingModule, AdminUsersModule, Alerts, AndroidOsEnvironment, AppAboutModalModule, AppAccountModule, AppAsyncTable, AppAuthForm, AppAuthModal, AppAuthModule, AppChangePasswordModule, AppChangePasswordPage, AppEditor, AppEditorOptions, AppEntityEditor, AppEntityEditorModal, AppEntityEditorModalOptions, AppEntityFormModule, AppForm, AppFormArray, AppFormButtonsBarModule, AppFormContainer, AppFormField, AppFormModule, AppFormProvider, AppFormUtils, AppGestureConfig, AppGraphQLModule, AppHomePageModule, AppIconComponent, AppIconModule, AppImageGalleryComponent, AppInMemoryTable, AppInstallUpgradeCard, AppInstallUpgradeCardModule, AppListForm, AppListFormModule, AppLoadingSpinner, AppMarkdownContent, AppMarkdownModal, AppMenuModule, AppNullForm, AppPropertiesForm, AppPropertiesFormModule, AppPropertiesTable, AppPropertiesUtils, AppPropertyUtils, AppRegisterModule, AppResetPasswordModal, AppRowField, AppSelectPeerModule, AppSettingsPageModule, AppTabEditor, AppTabEditorOptions, AppTable, AppTableModule, AppTableUtils, AppTextPopoverModule, AppUpdateOfflineModeCard, AppUpdateOfflineModeCardModule, AppValidatorService, AppendQueryParamsPipePipe, ArrayDistinctPipe, ArrayFilterPipe, ArrayFindByPropertyPipe, ArrayFirstPipe, ArrayFormTestPage, ArrayIncludesPipe, ArrayJoinPipe, ArrayLastPipe, ArrayLengthPipe, ArrayMapPipe, ArrayPluckPipe, ArraySortPipe, AsAnyPipe, AsArrayPipe, AsBooleanPipe, AsFloatLabelTypePipe, AsObservablePipe, AudioProvider, AudioTestingModule, AudioTestingPage, AuthGuardService, AutoResizeDirective, AutoTitleDirective, AutocompleteTestPage, AutofocusDirective, BadgeDirective, BadgeNumberPipe, Base58, BaseEntityService, BaseGraphqlService, BaseGraphqlServiceOptions, BaseReferential, Beans, BooleanFormatPipe, BooleanTestPage, CORE_CONFIG_OPTIONS, CORE_TESTING_PAGES, CapitalizePipe, CellValueChangeListener, ChangePasswordForm, ChipsTestPage, Color, ColorScale, ComponentDirtyGuard, ConfigFragments, ConfigService, Configuration, CoreModule, CorePipesModule, CoreTestingModule, CryptoService, CsvUtils, DATE_ISO_PATTERN, DATE_MATCH_REGEXP, DATE_PATTERN, DATE_UNIX_MS_TIMESTAMP, DATE_UNIX_TIMESTAMP, DEFAULT_JOIN_ARRAY_VALUES_SEPARATOR, DEFAULT_JOIN_PROPERTIES_SEPARATOR, DEFAULT_MENU_SHOW_WHEN, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLACEHOLDER_CHAR, DEFAULT_REQUIRED_COLUMNS, DateDiffDurationPipe, DateFormatPipe, DateFormatService, DateFromNowPipe, DateShortTestPage, DateTestPage, DateTimeTestPage, DateUtils, DebugComponent, Department, DepartmentToStringPipe, DisplayWithPipe, DragAndDropDirective, DurationPipe, DurationTestPage, ED25519_SEED_LENGTH, EMPTY_PLACEHOLDER_CHAR, EMPTY_PLACEHOLDER_CHAR_REGEXP_GLOBAL, ENTITIES_STORAGE_KEY_PREFIX, ENVIRONMENT, EmptyArrayPipe, EntitiesAsyncTableDataSource, EntitiesStorage, EntitiesTableDataSource, Entity, EntityClass, EntityClasses, EntityFilter, EntityFilterUtils, EntityMetadataComponent, EntityStore, EntityUtils, Environment, EnvironmentHttpLoader, EnvironmentLoader, ErrorCodes, EvenPipe, FeedDirective, FeedModule, FeedPage, FeedService, FeedsComponent, FileResponse, FileService, FileSizePipe, FilesUtils, FirstFalsePipe, FirstPipe, FirstTruePipe, FormArrayHelper, FormArrayTestModule, FormButtonsBarComponent, FormButtonsBarToken, FormErrorPipe, FormErrorTranslatePipe, FormErrorTranslator, FormFieldDefinitionUtils, FormFieldValuesHolder, FormGetArrayPipe, FormGetControlPipe, FormGetGroupPipe, FormGetPipe, FormGetValuePipe, GalleryTestPage, GeolocationUtils, GraphqlService, HAMMER_PRESS_TIME, HAMMER_TAP_TIME, HighlightPipe, HomePage, Hotkeys, HotkeysDialogComponent, IMAGE_DEFAULTS, IPosition, ImageAttachment, ImageAttachmentFilter, ImageAttachmentService, ImageGalleryModule, ImageGalleryTestingModule, ImageModule, ImageService, ImagesUtils, InMemoryEntitiesService, IsAllSelectedPipe, IsEmptySelectionPipe, IsLoginAccountPipe, IsMultipleSelectionPipe, IsNilOrBlankPipe, IsNilOrNaNPipe, IsNilPipe, IsNotAllSelectedPipe, IsNotEmptySelectionPipe, IsNotNilOrBlankPipe, IsNotNilOrNaNPipe, IsNotNilPipe, IsOnDeskPipe, IsOnFieldPipe, IsSelectedPipe, IsSingleSelectionPipe, IsValidDatePipe, JobModule, JobProgression, JobProgressionComponent, JobProgressionIcon, JobProgressionList, JobProgressionService, JobProgressionTestService, JobProgressionTestingPage, JobTestingModule, JobUtils, JsonFeedUtils, JsonUtils, KEYBOARD_HIDE_DELAY_MS, LAT_LONG_PATTERNS, LAT_LONG_PATTERN_MAX_DECIMALS, LAT_LONG_VALUE_MAX_DECIMALS, LatLongFormatPipe, LatLongTestPage, LatitudeFormatPipe, LocalSettingsService, LogLevel, LogUtils, Logger, LoggingService, LoggingServiceModule, LongitudeFormatPipe, MASKS, MASK_RANGES, MAT_FORM_FIELD_DEFAULT_APPEARANCE, MAT_FORM_FIELD_DEFAULT_SUBSCRIPT_SIZING, MINIFY_ENTITY_FOR_LOCAL_STORAGE, MINIFY_ENTITY_FOR_POD, MOMENT_NO_TIME_PROPERTY, MapGetPipe, MapKeysPipe, MapPipe, MapToPipe, MapValuesPipe, MarkdownDirective, MarkdownService, MarkdownTestPage, MarkdownTestingModule, MarkdownUtils, MaskitoPlaceholderPipe, MaskitoTestPage, MatAutocompleteConfigHolder, MatAutocompleteField, MatAutocompleteFieldUtils, MatBadgeTestPage, MatBooleanField, MatChipsField, MatColorPipe, MatCommonTestPage, MatDate, MatDateShort, MatDateTime, MatDuration, MatLatLongField, MatLatLongFieldInput, MatPaginatorI18n, MatStepperI18n, MatSwipeField, MaterialTestingModule, MathAbsPipe, MenuComponent, MenuItem, MenuItems, MenuOptions, MenuService, MenuTestingModule, MenuTestingPage, Message, MessageFilter, MessageForm, MessageModal, MessageModule, MessageService, MessageTypeList, MessageTypes, MimeTypes, ModalToolbarComponent, NETWORK_DEFAULT_CONNECTION_TIMEOUT, NamedFilter, NamedFilterFilter, NamedFilterSelector, NamedFilterSelectorTestingModule, NamedFilterSelectorTestingPage, NavActionsColumnComponent, NetworkService, NetworkUtils, NewTokenForm, NewTokenModal, NgInitDirective, NgVarDirective, NoHtmlPipe, NotEmptyArrayPipe, NumberFormatPipe, ObservableTestPage, OddPipe, OtherMenuTestingPage, PEER_URL_REGEXP, PLUS_PLACEHOLDER_CHAR_REGEXP_GLOBAL, PRINT_ID_QUERY_PARAM, PRINT_LOADING_STORAGE_KEY_PREFIX, PRIORITIZED_AUTHORITIES, PUBKEY_REGEXP, Peer, Person, PersonFilter, PersonFragments, PersonService, PersonToStringPipe, PersonUtils, PersonValidatorService, PlatformService, PrintService, ProgressBarService, ProgressInterceptor, PropertiesFormTestPage, PropertiesFormTestingModule, PropertyEntity, PropertyEntityFilter, PropertyEntityValidator, PropertyFormatPipe, PropertyGetPipe, RESERVED_END_COLUMNS, RESERVED_START_COLUMNS, Referential, ReferentialFilter, ReferentialRef, ReferentialToStringPipe, ReferentialUtils, ReferentialValidatorService, RegExpUtils, RegisterConfirmPage, RegisterForm, RegisterModal, ResizableComponent, ResizableDirective, ResizableModule, RoundPipe, RxStateComputed, RxStateModule, RxStateProperty, RxStateRegister, RxStateSelect, SCRYPT_PARAMS, SETTINGS_COMPACT_ROWS, SETTINGS_DISPLAY_COLUMNS, SETTINGS_FILTER, SETTINGS_PAGE_SIZE, SETTINGS_SORTED_COLUMN, SETTINGS_STORAGE_KEY, SETTINGS_TRANSIENT_PROPERTIES, SHARED_MATERIAL_TESTING_PAGES, SHARED_STORAGE_TESTING_PAGES, SHARED_TESTING_PAGES, SOCIAL_CONFIG_OPTIONS, SOCIAL_TESTING_PAGES, SPACE_PLACEHOLDER_CHAR, SPACE_PLACEHOLDER_CHAR_REGEXP_GLOBAL, SafeHtmlPipe, SafeStylePipe, SelectPeerModal, SelectionLengthPipe, ServerErrorCodes, SettingsPage, SharedAsyncValidators, SharedBadgeModule, SharedDebugModule, SharedDirectivesModule, SharedFormArrayValidators, SharedFormGroupValidators, SharedHotkeysModule, SharedMarkdownModule, SharedMatAutocompleteModule, SharedMatBooleanModule, SharedMatChipsModule, SharedMatDateTimeModule, SharedMatDurationModule, SharedMatLatLongModule, SharedMatSwipeModule, SharedMaterialModule, SharedModule, SharedNamedFilterModule, SharedPipesModule, SharedRoutingModule, SharedTestingModule, SharedTestsPage, SharedTextFormModule, SharedToolbarModule, SharedValidators, SocialErrorCodes, SocialModule, SocialModuleOptionsToken, SocialTestingModule, Software, SplitArrayInChunksPipe, StartableService, StatusById, StatusIds, StatusList, StorageDrivers, StorageExplorerComponent, StorageExplorerModule, StorageExplorerTestingModule, StorageExplorerTestingRoutingModule, StorageService, StrIncludesPipe, StrLengthPipe, StrReplacePipe, SubMenuTabDirective, SwipeTestPage, TABLE_SETTINGS_ENUM, TOOLBAR_HEADER_ID, Table2TestPage, TableSelectColumnsComponent, TableTestPage, TableTestingModule, TableValidatorService, TextForm, TextFormTestingPage, TextPopover, TextPopoverTestingModule, TextPopoverTestingPage, ThrottledClickDirective, ToStringPipe, ToastTestingModule, ToastTestingPage, Toasts, TokenScope, ToolbarComponent, ToolbarToken, TranslatablePipe, TranslateContextPipe, TranslateContextService, TreeItemEntityUtils, TruncHtmlPipe, TruncTextPipe, TruncateHtmlPipe, UploadFile, UploadFileComponent, UploadFilePopover, UploadFileTestingModule, UploadFileTestingPage, UriUtils, UrlUtils, UserEventModule, UserEventNotificationIcon, UserEventNotificationList, UserEventNotificationModal, UserEventTestService, UserEventTestingModule, UserEventTestingPage, UserSettings, UserToken, UserTokenTable, UsersPage, ValueFormatPipe, VersionUtils, accountToString, adaptValueToControl, addValueInArray, arrayDistinct, arrayResize, arraySize, asInputElement, base64ArrayBuffer, booleanToString, canHaveFocus, capitalizeFirstLetter, chainPromises, changeCaseToUnderscore, clearValueInArray, collectByProperty, compareVersionNumbers, computeDecimalDegrees, computeDecimalPart, copyEntity2Form, createPromiseEvent, createPromiseEventEmitter, departmentToString, departmentsToString, disableAndClearControl, disableAndClearControls, disableControl, disableControls, emitPromiseEvent, enableControl, enableControls, enableRxStateProdMode, entityToString, equals, equalsOrNil, escapeRegExp, expansionAnimation, fadeInAnimation, fadeInOutAnimation, fadeInSlowAnimation, filterFalse, filterFormErrors, filterFormErrorsByPath, filterFormErrorsByPrefix, filterNotNil, filterNumberInput, filterTrue, findParentWithClass, firstArrayValue, firstFalse, firstFalsePromise, firstNotNil, firstNotNilPromise, firstTrue, firstTruePromise, focusInput, focusNextInput, focusPreviousInput, formatLatLong, formatLatitude, formatLongitude, fromDateISOString, fromScrollEndEvent, fromUnixMsTimestamp, fromUnixTimestamp, getCaretPosition, getColorContrast, getColorShade, getColorTint, getControlFromPath, getFocusableInputElements, getFormErrors, getFormValueFromEntity, getInputRangeFromCaretIndex, getInputSelectionRangesFromMask, getProperty, getPropertyByPath, getPropertyByPathAsString, getRandomImage, getRandomImageWithCredit, getUserAgent, hexToRgb, hexToRgbArray, initArrayControlsFromValues, interpolateString, intersectArrays, isAndroid, isBlankString, isCapacitor, isChrome, isControlHasInput, isEdge, isEmptyArray, isEntityService, isFirefox, isIOS, isInputElement, isInstanceOf, isInt, isIpad, isMacOS, isMobile, isNil, isNilOrBlank, isNilOrNaN, isNotEmptyArray, isNotNil, isNotNilBoolean, isNotNilObject, isNotNilOrBlank, isNotNilOrNaN, isNotNilString, isNumber, isNumberRange, isOnFieldMode, isPrint, isProgressEvent, isPromise, isResponseEvent, isSafari, isSameVersion, isStartableService, isTouchUi, isVersionCompatible, isWindows, joinProperties, joinPropertiesPath, lastArrayValue, logFormErrors, markAllAsTouched, markAsUntouched, markControlAsTouched, markFormGroupAsTouched, maskitoAutoSelectByMaskPattern, maskitoPrefixPlugin, matchMedia, matchUpperCase, mergeLoadResult, mixHex, moveInputCaretToSeparator, newArray, noHtml, noTrailingSlash, notNilOrDefault, nullIfNilOrBlank, nullIfUndefined, numberOrNilAttribute, numberToString, parseLatitudeOrLongitude, propertiesPathComparator, propertyComparator, propertyPathComparator, referentialToString, referentialsToString, remove, removeAll, removeDiacritics, removeDuplicatesFromArray, removeEnd, removeValueInArray, replaceAll, resetCalculatedValue, resizeArray, rgbArrayToHex, rgbToHex, round, scrollFactory, selectInputContent, selectInputContentFromEvent, selectInputRange, setCalculatedValue, setControlEnabled, setControlsEnabled, setFormErrors, setPropertyByPath, setTabIndex, sleep, slideDownAnimation, slideInAnimation, slideInOutAnimation, slideUpDownAnimation, sort, splitArrayInChunks, splitById, splitByProperty, splitDegreesToDDArray, splitDegreesToDDMMArray, splitDegreesToDDMMSSArray, startsWithUpperCase, suggestFromArray, suggestFromStringArray, tabindexComparator, testUserAgent, toBoolean, toDateISOString, toDuration, toFloat, toInt, toNotNil, toNumber, trimEmptyToNull, truncateHtml, uncapitalizeFirstLetter, undefinedIfNull, underscoreToChangeCase, updateValueAndValidity, waitFor, waitForFalse, waitForTrue, waitIdle, waitWhilePending };
50272
50513
  //# sourceMappingURL=sumaris-net.ngx-components.mjs.map