UNPKG

@idea-ionic/common

Version:
1,122 lines (1,108 loc) 477 kB
import * as i0 from '@angular/core'; import { inject, Input, Component, Injectable, InjectionToken, EventEmitter, ChangeDetectorRef, Pipe, ViewChild, Output, HostListener, Injector, ElementRef, ViewContainerRef, Directive, NgModule, SecurityContext } from '@angular/core'; import { PopoverController, IonIcon, IonButton, IonLabel, IonRow, IonCol, IonGrid, IonContent, Platform, ActionSheetController, NavController, ToastController, IonCard, IonCardHeader, IonCardContent, IonCardTitle, IonImg, LoadingController, IonItem, IonInput, IonNote, IonSpinner, IonItemDivider, IonReorderGroup, IonReorder, ModalController, IonTitle, IonButtons, IonToolbar, IonHeader, IonList, IonListHeader, IonThumbnail, IonTextarea, IonText, AlertController, IonAccordionGroup, IonAccordion, IonBadge, IonInfiniteScroll, IonInfiniteScrollContent, IonSearchbar, IonAvatar, IonCheckbox, IonChip, IonSelect, IonSelectOption, IonToggle, IonPopover } from '@ionic/angular/standalone'; import * as i1$1 from '@angular/common'; import { DatePipe, CommonModule } from '@angular/common'; import * as i1 from '@angular/forms'; import { FormsModule } from '@angular/forms'; import { Browser } from '@capacitor/browser'; import { Languages, mdToHtml, Label, TIMEZONE_OFFSETS, getStringEnumKeyByValue, LanguagesISO639, AppStatus, Attachment, AttachmentSection, Suggestion, COLORS, CustomFieldTypes, loopStringEnumValues, Ionicons, CustomFieldMeta, CustomSectionMeta, EmailData, PDFTemplateSectionTypes, PDFTemplateSection, PDFTemplateSimpleField, PDFTemplateComplexField, Signature } from 'idea-toolbox'; import { Storage } from '@ionic/storage-angular'; import SignaturePad from 'signature_pad'; import { DomSanitizer } from '@angular/platform-browser'; /** * It's an alternative for desktop devices to the traditional ActionSheet. * It shares (almost) the same inputs so they are interchangeable. */ class IDEAActionSheetComponent { constructor() { this._popover = inject(PopoverController); /** * An array of buttons for the actions panel. */ this.buttons = []; } ngOnInit() { // based on the input, changes the way the UI behaves this.withIcons = this.buttons.some(b => b.icon); } buttonClicked(button) { if (button.handler) button.handler(); this._popover.dismiss(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAActionSheetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: IDEAActionSheetComponent, isStandalone: true, selector: "idea-action-sheet", inputs: { buttons: "buttons", cssClass: "cssClass", header: "header", subHeader: "subHeader" }, ngImport: i0, template: ` <ion-content [class]="cssClass"> <ion-grid class="ion-padding"> @if (header) { <ion-row class="headerRow"> <ion-col class="ion-text-center"> <ion-label class="ion-text-wrap"> {{ header }} @if (subHeader) { <p>{{ subHeader }}</p> } </ion-label> </ion-col> </ion-row> } <ion-row class="ion-justify-content-center buttonsRow"> @for (button of buttons; track button) { <ion-col [size]="withIcons ? 6 : 12"> <ion-button fill="clear" expand="full" color="medium" [class.withIcon]="withIcons" [class.destructive]="button.role === 'destructive'" [class.cancel]="button.role === 'cancel'" (click)="buttonClicked(button)" > <div> @if (withIcons) { <ion-icon [icon]="button.icon || 'flash'" /> } @if (withIcons) { <br /> } <ion-label class="ion-text-wrap">{{ button.text }}</ion-label> </div> </ion-button> </ion-col> } </ion-row> </ion-grid> </ion-content> `, isInline: true, styles: ["ion-row.headerRow{margin-top:8px;margin-bottom:16px;font-size:1.2em;font-weight:500}ion-row.buttonsRow{margin-bottom:8px}ion-row.buttonsRow ion-button{text-transform:none;--ion-color-base: var(--ion-text-color-step-350) !important}ion-row.buttonsRow ion-button.destructive{--ion-color-base: var(--ion-color-danger) !important}ion-row.buttonsRow ion-button.cancel{--ion-color-base: var(--ion-text-color-step-650) !important}ion-row.buttonsRow ion-button.withIcon{height:100%}ion-row.buttonsRow ion-button.withIcon div{display:flex;flex-flow:column nowrap;align-items:center}ion-row.buttonsRow ion-button.withIcon div ion-icon{font-size:1.8em}ion-row.buttonsRow ion-button.withIcon div br{content:\"\";margin-bottom:10px}.action-sheet-cancel ion-icon{opacity:.8}\n"], dependencies: [{ kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: 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: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: IonRow, selector: "ion-row" }, { kind: "component", type: 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: IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAActionSheetComponent, decorators: [{ type: Component, args: [{ selector: 'idea-action-sheet', standalone: true, imports: [IonIcon, IonButton, IonLabel, IonRow, IonCol, IonGrid, IonContent], template: ` <ion-content [class]="cssClass"> <ion-grid class="ion-padding"> @if (header) { <ion-row class="headerRow"> <ion-col class="ion-text-center"> <ion-label class="ion-text-wrap"> {{ header }} @if (subHeader) { <p>{{ subHeader }}</p> } </ion-label> </ion-col> </ion-row> } <ion-row class="ion-justify-content-center buttonsRow"> @for (button of buttons; track button) { <ion-col [size]="withIcons ? 6 : 12"> <ion-button fill="clear" expand="full" color="medium" [class.withIcon]="withIcons" [class.destructive]="button.role === 'destructive'" [class.cancel]="button.role === 'cancel'" (click)="buttonClicked(button)" > <div> @if (withIcons) { <ion-icon [icon]="button.icon || 'flash'" /> } @if (withIcons) { <br /> } <ion-label class="ion-text-wrap">{{ button.text }}</ion-label> </div> </ion-button> </ion-col> } </ion-row> </ion-grid> </ion-content> `, styles: ["ion-row.headerRow{margin-top:8px;margin-bottom:16px;font-size:1.2em;font-weight:500}ion-row.buttonsRow{margin-bottom:8px}ion-row.buttonsRow ion-button{text-transform:none;--ion-color-base: var(--ion-text-color-step-350) !important}ion-row.buttonsRow ion-button.destructive{--ion-color-base: var(--ion-color-danger) !important}ion-row.buttonsRow ion-button.cancel{--ion-color-base: var(--ion-text-color-step-650) !important}ion-row.buttonsRow ion-button.withIcon{height:100%}ion-row.buttonsRow ion-button.withIcon div{display:flex;flex-flow:column nowrap;align-items:center}ion-row.buttonsRow ion-button.withIcon div ion-icon{font-size:1.8em}ion-row.buttonsRow ion-button.withIcon div br{content:\"\";margin-bottom:10px}.action-sheet-cancel ion-icon{opacity:.8}\n"] }] }], propDecorators: { buttons: [{ type: Input }], cssClass: [{ type: Input }], header: [{ type: Input }], subHeader: [{ type: Input }] } }); /** * It's an alternative to the traditional ActionSheetController. * It shares (almost) the same inputs, so they are interchangeable. */ class IDEAActionSheetController { constructor() { this._platform = inject(Platform); this._actions = inject(ActionSheetController); this._popover = inject(PopoverController); } /** * Based on the platform, open the traditional or the customised ActionSheet. */ create(options, forceCustom) { if ((this._platform.is('mobile') || this._platform.width() < 576) && !forceCustom) return this._actions.create(options); else return this._popover.create({ backdropDismiss: options.backdropDismiss, component: IDEAActionSheetComponent, componentProps: options, cssClass: 'actionSheetPopover' }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAActionSheetController, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAActionSheetController, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAActionSheetController, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); /** * The token to inject the app configurations in the module. */ const IDEAEnvironment = new InjectionToken('IDEA environment configuration'); /** * Translations service. */ class IDEATranslationsService { constructor() { this._env = inject(IDEAEnvironment); /** * Base folder containing the translations. */ this.basePath = 'assets/i18n/'; /** * Template matcher to interpolate complex strings (e.g. `{{user}}`). */ this.templateMatcher = /{{\s?([^{}\s]*)\s?}}/g; /** * The translations. */ this.translations = {}; /** * Some default interpolation parameters to add to istant translations. */ this.defaultInterpolations = {}; /** * To subscribe to language changes. */ this.onLangChange = new EventEmitter(); this.modulesPath = [''].concat(this._env.idea.ionicExtraModules || []); } /** * Initialize the service. */ async init(languages = ['en'], defaultLang = 'en') { this.setLangs(languages); this.setDefaultLang(defaultLang); let lang = this.getBrowserLang(); if (!languages.includes(lang)) lang = this.getDefaultLang(); await this.use(lang, true); } /** * Set the available languages. */ setLangs(langs) { this.langs = langs.slice(); } /** * Returns an array of currently available languages. */ getLangs() { return this.langs; } /** * Get the fallback language. */ getDefaultLang() { return this.defaultLang; } /** * Sets the default language to use as a fallback. */ setDefaultLang(lang) { if (this.langs.includes(lang)) this.defaultLang = lang; else this.defaultLang = this.langs[0]; } /** * Get the languages in IdeaX format. */ languages() { return new Languages({ available: this.langs, default: this.defaultLang }); } /** * Returns the language code name from the browser, e.g. "it" */ getBrowserLang() { if (typeof window === 'undefined' || typeof window.navigator === 'undefined') return undefined; let browserLang = window.navigator.languages ? window.navigator.languages[0] : null; browserLang = browserLang || window.navigator.language || window.navigator.browserLanguage || window.navigator.userLanguage; if (typeof browserLang === 'undefined') return undefined; if (browserLang.indexOf('-') !== -1) browserLang = browserLang.split('-')[0]; if (browserLang.indexOf('_') !== -1) browserLang = browserLang.split('_')[0]; return browserLang; } /** * The lang currently used. */ getCurrentLang() { return this.currentLang; } /** * Set a language to use. */ use(lang, force) { return new Promise(resolve => { const changed = lang !== this.currentLang; if (!changed && !force) return; // check whether the language is among the available ones; otherwise, fallback to default if (!this.langs.includes(lang)) lang = this.defaultLang; // load translations this.loadTranlations(lang).then(() => { // set the lang this.currentLang = lang; // emit the change if (changed) this.onLangChange.emit(lang); // resolve resolve(); }); }); } /** * Set some parameters to automatically provide to translation actions. */ setDefaultInterpolations(defaultParams) { this.defaultInterpolations = defaultParams || {}; } /** * Get a translated term by key in the current language, optionally interpolating variables (e.g. `{{user}}`). * If the term doesn't exist in the current language, it is searched in the default language. */ instant(key, interpolateParams) { return this.instantInLanguage(this.currentLang, key, interpolateParams); } /** * Get a translated term by key in the selected language, optionally interpolating variables (e.g. `{{user}}`). * If the term doesn't exist in the current language, it is searched in the default language. */ instantInLanguage(language, key, interpolateParams) { const params = { ...this.defaultInterpolations, ...(interpolateParams || {}) }; if (!this.isDefined(key) || !key.length) return; let res = this.interpolate(this.getValue(this.translations[language], key), params); if (res === undefined && this.defaultLang !== null && this.defaultLang !== language) res = this.interpolate(this.getValue(this.translations[this.defaultLang], key), params); return res; } /** * Shortcut to instant. */ _(key, interpolateParams) { return this.instant(key, interpolateParams); } /** * Translate (instant) and transform an expected markdown string into HTML. */ _md(key, interpolateParams) { return mdToHtml(this._(key, interpolateParams)); } /** * Return a Label containing all the available translations of a key. */ getLabelByKey(key, interpolateParams) { const label = new Label(null, this.languages()); this.langs.forEach(lang => (label[lang] = this.instantInLanguage(lang, key, interpolateParams))); return label; } /** * Return the translation in the current language of a label. */ translateLabel(label) { return label.translate(this.getCurrentLang(), this.languages()); } /** * Shortcut to translateLabel. */ _label(label) { return this.translateLabel(label); } /** * Load the translations from the files. */ loadTranlations(lang) { return new Promise(resolve => { this.translations = {}; this.translations[this.defaultLang] = {}; let promises = this.modulesPath.map(m => this.loadTranslationFileHelper(this.basePath.concat(m), this.defaultLang)); if (lang !== this.defaultLang) { this.translations[lang] = {}; promises = promises.concat(this.modulesPath.map(m => this.loadTranslationFileHelper(this.basePath.concat(m), lang))); } Promise.all(promises).then(() => resolve()); }); } /** * Load a file into the translations. */ async loadTranslationFileHelper(path, lang) { const res = await fetch(`${path.slice(-1) === '/' ? path : path.concat('/')}${lang}.json`, { method: 'GET', cache: 'no-cache' // to avoid issues upon releases }); if (res.status !== 200) return; const obj = await res.json(); for (const key in obj) if (obj[key]) this.translations[lang][key] = obj[key]; } /** * Interpolates a string to replace parameters. * "This is a {{ key }}" ==> "This is a value", with params = { key: "value" } */ interpolate(expr, params) { if (!params || !expr) return expr; return expr.replace(this.templateMatcher, (substring, b) => { const r = this.getValue(params, b); return this.isDefined(r) ? r : substring; }); } /** * Gets a value from an object by composed key. * getValue({ key1: { keyA: 'valueI' }}, 'key1.keyA') ==> 'valueI' */ getValue(target, key) { const keys = typeof key === 'string' ? key.split('.') : [key]; key = ''; do { key += keys.shift(); if (this.isDefined(target) && this.isDefined(target[key]) && (typeof target[key] === 'object' || !keys.length)) { target = target[key]; key = ''; } else if (!keys.length) target = undefined; else key += '.'; } while (keys.length); return target; } /** * Helper to quicly check if the value is defined. */ isDefined(value) { return value !== undefined && value !== null; } /** * Format a date in the current locale (optionally forcing a timeZone). */ formatDate(value, pattern = 'mediumDate', timeZone) { const timeZoneOffset = timeZone ? TIMEZONE_OFFSETS[timeZone] : undefined; const datePipe = new DatePipe(this.getCurrentLang(), timeZoneOffset); return datePipe.transform(value, pattern); } /** * Get a readable string to represent the current language (standard ISO639). */ getLanguageNameByKey(lang) { return getStringEnumKeyByValue(LanguagesISO639, lang || this.getCurrentLang()); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEATranslationsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEATranslationsService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEATranslationsService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); class IDEATranslatePipe { constructor() { this._translate = inject(IDEATranslationsService); this._ref = inject(ChangeDetectorRef); /** * The value to display. */ this.value = ''; } updateValue(key, interpolateParams) { const res = this._translate.instant(key, interpolateParams); this.value = res !== undefined ? res : key; this.lastKey = key; this._ref.markForCheck(); } transform(query, ...args) { if (!query || !query.length) return query; // if we ask another time for the same key, return the last value if (equals(query, this.lastKey) && equals(args, this.lastParams)) return this.value; let interpolateParams; if (args[0] !== undefined && args[0] !== null && args.length) { if (typeof args[0] === 'string' && args[0].length) { // we accept objects written in the template such as {n:1}, {'n':1}, {n:'v'} // which is why we might need to change it to real JSON objects such as {"n":1} or {"n":"v"} const validArgs = args[0] .replace(/(\')?([a-zA-Z0-9_]+)(\')?(\s)?:/g, '"$2":') .replace(/:(\s)?(\')(.*?)(\')/g, ':"$3"'); try { interpolateParams = JSON.parse(validArgs); } catch (e) { throw new SyntaxError(`Wrong parameter in TranslatePipe. Expected a valid Object, received: ${args[0]}`); } } else if (typeof args[0] === 'object' && !Array.isArray(args[0])) interpolateParams = args[0]; } // store the query, in case it changes this.lastKey = query; // store the params, in case they change this.lastParams = args; // set the value this.updateValue(query, interpolateParams); // if there is a subscription to onLangChange, clean it this._dispose(); // subscribe to onLangChange event, in case the language changes if (!this.onLangChange) { this.onLangChange = this._translate.onLangChange.subscribe(() => { if (this.lastKey) { this.lastKey = null; // we want to make sure it doesn't return the same value until it's been updated this.updateValue(query, interpolateParams); } }); } return this.value; } _dispose() { if (this.onLangChange !== undefined) { this.onLangChange.unsubscribe(); this.onLangChange = undefined; } } ngOnDestroy() { this._dispose(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEATranslatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.1.3", ngImport: i0, type: IDEATranslatePipe, isStandalone: true, name: "translate", pure: false }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEATranslatePipe }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEATranslatePipe, decorators: [{ type: Injectable }, { type: Pipe, args: [{ name: 'translate', pure: false, standalone: true }] }] }); /** * Determines if two objects or two values are equivalent. * * Two objects or values are considered equivalent if at least one of the following is true: * * * Both objects or values pass `===` comparison. * * Both objects or values are of the same type and all of their properties are equal by * comparing them with `equals`. * * @param o1 Object or value to compare. * @param o2 Object or value to compare. * @returns true if arguments are equal. */ function equals(o1, o2) { if (o1 === o2) return true; if (o1 === null || o2 === null) return false; if (o1 !== o1 && o2 !== o2) return true; // NaN === NaN const t1 = typeof o1, t2 = typeof o2; let length, key, keySet; if (t1 === t2 && t1 === 'object') { if (Array.isArray(o1)) { if (!Array.isArray(o2)) return false; if ((length = o1.length) === o2.length) { for (key = 0; key < length; key++) { if (!equals(o1[key], o2[key])) return false; } return true; } } else { if (Array.isArray(o2)) { return false; } keySet = Object.create(null); for (key in o1) { if (o1[key]) { if (!equals(o1[key], o2[key])) { return false; } keySet[key] = true; } } for (key in o2) { if (o2[key]) if (!(key in keySet) && typeof o2[key] !== 'undefined') { return false; } } return true; } } return false; } /** * To communicate with an AWS API Gateway istance. * Lighter, alternative version of _IDEAAWSAPIService_. */ class IDEAApiService { constructor() { this._env = inject(IDEAEnvironment); this._platform = inject(Platform); /** * Some custom headers to set so that they are used in any API request. */ this.defaultHeaders = {}; this.baseURL = 'https://'.concat([this._env.idea.api?.url, this._env.idea.api?.stage].filter(x => x).join('/')); this.appVersion = this._env.idea.app?.version || '?'; this.appBundle = this._env.idea.app?.bundle; } /** * Execute an online API request. * @param path resource path (e.g. `['users', userId]`) * @param method HTTP method * @param options the request options */ async request(path, method, options = {}) { const url = this.baseURL.concat('/', Array.isArray(path) ? path.join('/') : path); const builtInHeaders = {}; if (this.authToken) { if (typeof this.authToken === 'function') builtInHeaders.Authorization = await this.authToken(); else builtInHeaders.Authorization = this.authToken; } if (this.apiKey) builtInHeaders['X-API-Key'] = this.apiKey; const headers = { ...builtInHeaders, ...this.defaultHeaders, ...options.headers }; const searchParams = new URLSearchParams(); searchParams.append('_v', this.appVersion); searchParams.append('_p', this._platform.platforms().join(' ')); if (this.appBundle) searchParams.append('_b', this.appBundle); if (options.params) { for (const paramName in options.params) { const param = options.params[paramName]; if (Array.isArray(param)) for (const arrayElement of param) searchParams.append(paramName, arrayElement); else searchParams.append(paramName, param); } } let body = null; if (options.body) body = JSON.stringify(options.body); const res = await fetch(url.concat('?', searchParams.toString()), { method, headers, body }); if (res.status === 200) return await res.json(); let errMessage; try { errMessage = (await res.json()).message; } catch (err) { errMessage = 'Operation failed'; } throw new Error(errMessage); } /** * GET request. */ async getResource(path, options) { return await this.request(path, 'GET', options); } /** * POST request. */ async postResource(path, options) { return await this.request(path, 'POST', options); } /** * PUT request. */ async putResource(path, options) { return await this.request(path, 'PUT', options); } /** * PATCH request. */ async patchResource(path, options) { return await this.request(path, 'PATCH', options); } /** * DELETE request. */ async deleteResource(path, options) { return await this.request(path, 'DELETE', options); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAApiService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAApiService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); class IDEAStorageService { constructor() { this._storage = inject(Storage); this.theStorage = null; this.init(); } async init() { const storage = await this._storage.create(); this.theStorage = storage; } async ready() { return new Promise(resolve => this.readyHelper(resolve)); } readyHelper(resolve) { if (this.theStorage) resolve(); else setTimeout(() => this.readyHelper(resolve), 100); } async set(key, value) { return await this.theStorage?.set(key, value); } async get(key) { return await this.theStorage?.get(key); } async remove(key) { return await this.theStorage?.remove(key); } async clear() { return await this.theStorage?.clear(); } async keys() { return await this.theStorage?.keys(); } async length() { return await this.theStorage?.length(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAStorageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAStorageService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAStorageService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); /** * Check whether the app has some status message or update to handle. */ class IDEAAppStatusService { constructor() { this._env = inject(IDEAEnvironment); this._nav = inject(NavController); this._toast = inject(ToastController); this._translate = inject(IDEATranslationsService); this._api = inject(IDEAApiService); this._storage = inject(IDEAStorageService); this.storageKey == (this._env.idea.project || 'app').concat('_LAST_MESSAGE'); this.statusFileURL = `${window.location.hostname === 'localhost' ? '' : window.location.origin}/assets/status.json`; } /** * Check the app's status and take according actions. */ async check(options = {}) { const appStatus = await this.getStatus(options.viaApi); if (appStatus.inMaintenance || appStatus.mustUpdate) await this._nav.navigateRoot(['app-status']); else await this.presentToast(appStatus, { color: options.toastColor, position: options.toastPosition }); return appStatus; } async getStatus(viaApi = false) { if (this.appStatus) return this.appStatus; this.appStatus = await (viaApi ? this.getStatusFromApi() : this.getStatusFromAsset()); return this.appStatus; } async getStatusFromApi() { return new AppStatus(await this._api.getResource(['status'])); } async getStatusFromAsset() { const res = await fetch(this.statusFileURL, { method: 'GET', cache: 'no-cache' }); if (res.status !== 200) throw new Error('Status not found'); const statusFromS3 = await res.json(); return new AppStatus({ version: this._env.idea.app.version, inMaintenance: statusFromS3.maintenance, mustUpdate: statusFromS3.minVersion ? statusFromS3.minVersion > this._env.idea.app.version : false, content: statusFromS3.messages[this._env.idea.app.version], latestVersion: statusFromS3.latestVersion }); } async presentToast(appStatus, options = {}) { let message = appStatus.content || ''; if (!message && this._env.idea.app.version < appStatus.latestVersion) message = this._translate._('IDEA_COMMON.APP_STATUS.NEW_VERSION', { newVersion: appStatus.latestVersion }); if (!message) return; const messageAlreadyRead = await this._storage.get(this.storageKey); if (messageAlreadyRead === message) return; // user already saw this message const dismissMessage = () => this._storage.set(this.storageKey, message); const buttons = [ { text: this._translate._('IDEA_COMMON.APP_STATUS.GOT_IT'), role: 'cancel', handler: dismissMessage } ]; const color = options.color || 'dark'; const position = options.position || 'bottom'; const toast = await this._toast.create({ message, buttons, position, color }); await toast.present(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAAppStatusService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAAppStatusService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAAppStatusService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); /** * Handle blocking status messaging for the app. */ class IDEAAppStatusPage { constructor() { this._env = inject(IDEAEnvironment); this._platform = inject(Platform); this._translate = inject(IDEATranslationsService); this._appStatus = inject(IDEAAppStatusService); this.appIconURI = '/assets/icons/icon.svg'; this.appleStoreURL = this._env.idea.app?.appleStoreURL; this.googleStoreURL = this._env.idea.app?.googleStoreURL; } async ionViewWillEnter() { this.status = await this._appStatus.check(); if (this.status.content) this.htmlContent = mdToHtml(this.status.content); } getTitle() { if (this.status.inMaintenance) return this._translate._('IDEA_COMMON.APP_STATUS.MAINTENANCE'); if (this.status.mustUpdate) return this._translate._('IDEA_COMMON.APP_STATUS.MUST_UPDATE'); return this._translate._('IDEA_COMMON.APP_STATUS.EVERYTHING_IS_OK'); } isAndroid() { return this._platform.is('android'); } isIOS() { return this._platform.is('ios'); } async openGoogleStoreLink() { await Browser.open({ url: this.googleStoreURL }); } async opeAppleStoreLink() { await Browser.open({ url: this.appleStoreURL }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAAppStatusPage, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: IDEAAppStatusPage, isStandalone: true, selector: "idea-app-status", ngImport: i0, template: ` @if (status) { <ion-content class="ion-padding"> <div class="maxWidthContainer"> <ion-card color="dark"> <ion-card-header> <ion-card-title class="ion-text-center"> <h3>{{ getTitle() }}</h3> </ion-card-title> </ion-card-header> <ion-card-content class="ion-align-items-center"> <ion-img class="logo" [src]="appIconURI" (ionError)="$event.target.style.display = 'none'" /> @if (htmlContent) { <p class="htmlContent" [innerHTML]="htmlContent"></p> } @if (status.mustUpdate) { <ion-grid> <ion-row> <ion-col class="ion-text-center"> @if (isIOS() && appleStoreURL) { <ion-button (click)="opeAppleStoreLink()"> <ion-icon slot="start" name="logo-apple-appstore" /> {{ 'IDEA_COMMON.APP_STATUS.UPDATE' | translate }} </ion-button> } @if (isAndroid() && googleStoreURL) { <ion-button (click)="openGoogleStoreLink()"> <ion-icon slot="start" name="logo-google-playstore" /> {{ 'IDEA_COMMON.APP_STATUS.UPDATE' | translate }} </ion-button> } </ion-col> </ion-row> </ion-grid> } </ion-card-content> </ion-card> </div> </ion-content> } `, isInline: true, styles: ["ion-img.logo{width:100px;margin:0 auto 24px}p.htmlContent{margin:0 0 24px;padding:20px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonCardHeader, selector: "ion-card-header", inputs: ["color", "mode", "translucent"] }, { kind: "component", type: IonCardContent, selector: "ion-card-content", inputs: ["mode"] }, { kind: "component", type: IonCardTitle, selector: "ion-card-title", inputs: ["color", "mode"] }, { kind: "component", type: IonImg, selector: "ion-img", inputs: ["alt", "src"] }, { kind: "component", type: IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: IonRow, selector: "ion-row" }, { kind: "component", type: 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: 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: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "pipe", type: IDEATranslatePipe, name: "translate" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAAppStatusPage, decorators: [{ type: Component, args: [{ selector: 'idea-app-status', standalone: true, imports: [ CommonModule, FormsModule, IonContent, IonCard, IonCardHeader, IonCardContent, IonCardTitle, IonImg, IonGrid, IonRow, IonCol, IonButton, IonIcon, IDEATranslatePipe ], template: ` @if (status) { <ion-content class="ion-padding"> <div class="maxWidthContainer"> <ion-card color="dark"> <ion-card-header> <ion-card-title class="ion-text-center"> <h3>{{ getTitle() }}</h3> </ion-card-title> </ion-card-header> <ion-card-content class="ion-align-items-center"> <ion-img class="logo" [src]="appIconURI" (ionError)="$event.target.style.display = 'none'" /> @if (htmlContent) { <p class="htmlContent" [innerHTML]="htmlContent"></p> } @if (status.mustUpdate) { <ion-grid> <ion-row> <ion-col class="ion-text-center"> @if (isIOS() && appleStoreURL) { <ion-button (click)="opeAppleStoreLink()"> <ion-icon slot="start" name="logo-apple-appstore" /> {{ 'IDEA_COMMON.APP_STATUS.UPDATE' | translate }} </ion-button> } @if (isAndroid() && googleStoreURL) { <ion-button (click)="openGoogleStoreLink()"> <ion-icon slot="start" name="logo-google-playstore" /> {{ 'IDEA_COMMON.APP_STATUS.UPDATE' | translate }} </ion-button> } </ion-col> </ion-row> </ion-grid> } </ion-card-content> </ion-card> </div> </ion-content> } `, styles: ["ion-img.logo{width:100px;margin:0 auto 24px}p.htmlContent{margin:0 0 24px;padding:20px}\n"] }] }], ctorParameters: () => [] }); const ideaAppStatusRoutes = [{ path: '', component: IDEAAppStatusPage }]; class IDEALoadingService { constructor() { this._loading = inject(LoadingController); this._translate = inject(IDEATranslationsService); } /** * Show a loading animation. * @param content loading message */ async show(content) { const message = content || this._translate._('IDEA_COMMON.LOADING.PLEASE_WAIT'); this.loadingElement = await this._loading.create({ message }); return await this.loadingElement.present(); } /** * Change the content of the loading animation, while it's already on. * @param content new loading message */ setContent(content) { if (this.loadingElement) this.loadingElement.textContent = content; } /** * Hide the loading animation. */ async hide() { if (this.loadingElement) return await this.loadingElement.dismiss(); else return false; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEALoadingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEALoadingService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEALoadingService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class IDEAMessageService { constructor() { this._toast = inject(ToastController); this._translate = inject(IDEATranslationsService); } /** * Show a generic message toast. * @param message message to show * @param color Ionic colors defined in the theme */ async show(message, color, dontTranslate) { message = dontTranslate ? message : this._translate._(message); const duration = 3000; const position = 'bottom'; const buttons = [{ text: 'X', role: 'cancel' }]; const toast = await this._toast.create({ message, duration, position, color, buttons }); return await toast.present(); } /** * Show an info message toast. * @param message message to show */ async info(message, dontTranslate) { return await this.show(message, 'dark', dontTranslate); } /** * Show a success message toast. * @param message message to show */ async success(message, dontTranslate) { return await this.show(message, 'success', dontTranslate); } /** * Show an error message toast. * @param message message to show */ async error(message, dontTranslate) { return await this.show(message, 'danger', dontTranslate); } /** * Show an warning message toast. * @param message message to show */ async warning(message, dontTranslate) { return await this.show(message, 'warning', dontTranslate); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAMessageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAMessageService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAMessageService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class IDEAAttachmentsService { constructor() { this._env = inject(IDEAEnvironment); this._api = inject(IDEAApiService); } /** * Upload a new attachment related to an entity and return the `attachmentId`. */ async uploadAndGetId(file, entityPath, options = {}) { const { maxFileUploadSizeMB } = this._env.idea.app; if (maxFileUploadSizeMB && bytesToMegaBytes(file.size) > maxFileUploadSizeMB) throw new Error('File is too big'); const body = { action: options.customAction || 'GET_ATTACHMENT_UPLOAD_URL' }; const { url, id } = await this._api.patchResource(entityPath, { body }); await fetch(url, { method: 'PUT', body: file, headers: { 'Content-Type': file.type } }); return id; } /** * Get the URL to download an attachment related to an entity. */ async getDownloadURL(attachment, entityPath, options = {}) { const body = { action: options.customAction || 'GET_ATTACHMENT_DOWNLOAD_URL', attachmentId: typeof attachment === 'string' ? attachment : attachment.attachmentId }; const { url } = await this._api.patchResource(entityPath, { body }); return url; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAAttachmentsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAAttachmentsService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAAttachmentsService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); /** * Approximate conversion of bytes in MB. */ const bytesToMegaBytes = (bytes) => bytes / 1024 ** 2; class IDEAAttachmentsComponent { constructor() { this._env = inject(IDEAEnvironment); this._loading = inject(IDEALoadingService); this._message = inject(IDEAMessageService); this._translate = inject(IDEATranslationsService); this._attachments = inject(IDEAAttachmentsService); /** * The list of accepted formats. */ this.acceptedFormats = ['image/*', '.pdf', '.doc', '.docx', '.xls', '.xlsx']; /** * Whether to accept multiple files as target for the browse function. */ this.multiple = false; /** * Whether we are viewing or editing the attachments. */ this.disabled = false; /** * Trigger to download a file by URL. */ this.download = new EventEmitter(); this.uploadErrors = []; this.maxSize = this._env.idea.app.maxFileUploadSizeMB; } async browseFiles() { this.attachmentPicker.nativeElement.click(); } addAttachmentsFromPicker(target) { this.uploadErrors = []; for (let i = 0; i < target.files.length; i++) { const file = target.files.item(i); const fullName = file.name.split('.'); const format = fullName.pop(); const name = fullName.join('.'); this.addAttachmentToListAndUpload(new Attachment({ name, format }), file); } // empty the file picker to allow the upload of new files with the same name target.value = null; } async addAttachmentToListAndUpload(attachment, file) { try { this.attachments.push(attachment); attachment.attachmentId = await this._attachments.uploadAndGetId(file, this.entityPath); } catch (err) { if (err.message === 'File is too big') err.message = this._translate._('IDEA_COMMON.ATTACHMENTS.FILE_IS_TOO_BIG', { maxSize: this.maxSize }); this.uploadErrors.push({ file: attachment.name, error: err.message }); this.removeAttachment(