@idea-ionic/common
Version:
IDEA Ionic common components
1,122 lines (1,108 loc) • 477 kB
JavaScript
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(