@idea-ionic/teams
Version:
IDEA Ionic teams components
884 lines (878 loc) • 101 kB
JavaScript
import { CommonModule } from '@angular/common';
import * as i0 from '@angular/core';
import { inject, Component, Input, EventEmitter, Output, ViewChild } from '@angular/core';
import * as i1 from '@angular/forms';
import { FormsModule } from '@angular/forms';
import { NavController, AlertController, IonLabel, IonBadge, IonListHeader, IonInput, IonItem, IonContent, IonTitle, IonIcon, IonButton, IonButtons, IonToolbar, IonHeader, IonList, Platform, IonSpinner, ModalController, IonSearchbar, IonInfiniteScroll, IonInfiniteScrollContent, IonNote, IonSkeletonText, IonCardContent, IonCard, IonRefresher, IonRefresherContent, IonText } from '@ionic/angular/standalone';
import { User, Attachment, Suggestion, RCResource, RCResourceFormats, loopStringEnumValues, RCFolder, RCAttachedResource, Team } from 'idea-toolbox';
import { IDEAEnvironment, IDEALoadingService, IDEAMessageService, IDEATranslationsService, IDEATranslatePipe, IDEASelectComponent, IDEAActionSheetController, IDEALocalizedDatePipe } from '@idea-ionic/common';
import { IDEATinCanService, IDEAAWSAPIService, IDEAOfflineService, CacheModes } from '@idea-ionic/uncommon';
import { Browser } from '@capacitor/browser';
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
import heic2any from 'heic2any';
class IDEAAccountPage {
constructor() {
this._env = inject(IDEAEnvironment);
this._tc = inject(IDEATinCanService);
this._nav = inject(NavController);
this._alert = inject(AlertController);
this._loading = inject(IDEALoadingService);
this._message = inject(IDEAMessageService);
this._API = inject(IDEAAWSAPIService);
this._translate = inject(IDEATranslationsService);
}
ngOnInit() {
this.user = new User(this._tc.get('user'));
this.newEmail = this.user.email;
}
async updateEmail() {
const doUpdate = async ({ pwd, email }) => {
if (!email)
return this._message.info('IDEA_TEAMS.ACCOUNT.INVALID_EMAIL');
try {
await this._loading.show();
const body = { password: pwd, newEmail: email, project: this._env.idea.project };
await this._API.postResource('emailChangeRequests', { idea: true, body });
const alert = await this._alert.create({
header: this._translate._('COMMON.OPERATION_COMPLETED'),
subHeader: this._translate._('IDEA_TEAMS.ACCOUNT.UPDATE_EMAIL_FLOW_EXPLANATION'),
buttons: [this._translate._('COMMON.GOT_IT')]
});
alert.present();
}
catch (error) {
this._message.error('IDEA_TEAMS.ACCOUNT.OPERATION_FAILED_PASSWORD');
}
finally {
this._loading.hide();
}
};
const header = this._translate._('IDEA_TEAMS.ACCOUNT.UPDATE_EMAIL');
const subHeader = this._translate._('IDEA_TEAMS.ACCOUNT.UPDATE_EMAIL_EXPLANATION');
const inputs = [
{ name: 'pwd', type: 'password', placeholder: this._translate._('IDEA_TEAMS.ACCOUNT.YOUR_CURRENT_PASSWORD') },
{ name: 'email', placeholder: this._translate._('IDEA_TEAMS.ACCOUNT.NEW_EMAIL') }
];
const buttons = [
{ text: this._translate._('COMMON.CANCEL'), role: 'cancel' },
{ text: this._translate._('COMMON.CONFIRM'), handler: doUpdate }
];
const alert = await this._alert.create({ header, subHeader, inputs, buttons });
alert.present();
}
async updatePassword() {
const doUpdate = async ({ old, newP, new2 }) => {
if (newP.length < 8)
this._message.warning(this._translate._('IDEA_TEAMS.ACCOUNT.INVALID_PASSWORD', { n: 8 }), true);
else if (newP !== new2)
this._message.warning('IDEA_TEAMS.ACCOUNT.PASSWORD_CONFIRMATION_DONT_MATCH');
else {
try {
await this._loading.show();
const body = { action: 'UPDATE_PASSWORD', password: old, newPassword: newP };
await this._API.patchResource('users', { idea: true, resourceId: this.user.userId, body });
this._message.success('IDEA_TEAMS.ACCOUNT.PASSWORD_UPDATED');
}
catch (error) {
this._message.error('IDEA_TEAMS.ACCOUNT.OPERATION_FAILED_PASSWORD');
}
finally {
this._loading.hide();
}
}
};
const header = this._translate._('IDEA_TEAMS.ACCOUNT.UPDATE_PASSWORD');
const inputs = [
{ name: 'old', type: 'password', placeholder: this._translate._('IDEA_TEAMS.ACCOUNT.YOUR_CURRENT_PASSWORD') },
{ name: 'newP', type: 'password', placeholder: this._translate._('IDEA_TEAMS.ACCOUNT.NEW_PASSWORD_', { n: 8 }) },
{ name: 'new2', type: 'password', placeholder: this._translate._('IDEA_TEAMS.ACCOUNT.CONFIRM_NEW_PASSWORD') }
];
const buttons = [
{ text: this._translate._('COMMON.CANCEL'), role: 'cancel' },
{ text: this._translate._('COMMON.CONFIRM'), handler: doUpdate }
];
const alert = await this._alert.create({ header, inputs, buttons });
alert.present();
}
async deleteUser() {
const doDelete = async ({ pwd }) => {
try {
await this._loading.show();
await this._API.deleteResource('users', {
idea: true,
resourceId: this.user.userId,
headers: { password: pwd }
});
window.location.assign('');
}
catch (error) {
this._message.error('COMMON.OPERATION_FAILED');
}
finally {
this._loading.hide();
}
};
const header = this._translate._('IDEA_TEAMS.ACCOUNT.USER_DELETION');
const subHeader = this._translate._('IDEA_TEAMS.ACCOUNT.USER_DELETION_ARE_YOU_SURE');
const inputs = [
{ name: 'pwd', type: 'password', placeholder: this._translate._('IDEA_TEAMS.ACCOUNT.YOUR_CURRENT_PASSWORD') }
];
const buttons = [
{ text: this._translate._('COMMON.CANCEL'), role: 'cancel' },
{ text: this._translate._('COMMON.CONFIRM'), handler: doDelete }
];
const alert = await this._alert.create({ header, subHeader, inputs, buttons });
alert.present();
}
close(errorMessage) {
if (errorMessage)
this._message.error(errorMessage);
try {
this._nav.back();
}
catch (_) {
this._nav.navigateRoot(['']);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAAccountPage, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.3", type: IDEAAccountPage, isStandalone: true, selector: "account", ngImport: i0, template: `
<ion-header>
<ion-toolbar color="ideaToolbar">
<ion-buttons slot="start">
<ion-button testId="closeButton" [title]="'COMMON.CLOSE' | translate" (click)="close()">
<ion-icon name="arrow-back" slot="icon-only" />
</ion-button>
</ion-buttons>
<ion-title>{{ 'IDEA_TEAMS.ACCOUNT.ACCOUNT' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-list lines="full" class="account">
<ion-item>
<ion-input
testId="account.email"
type="text"
readonly="true"
labelPlacement="stacked"
[label]="'IDEA_TEAMS.ACCOUNT.EMAIL' | translate"
[(ngModel)]="newEmail"
/>
<ion-button
testId="setNewEmailButton"
slot="end"
fill="clear"
class="marginTop"
[title]="'IDEA_TEAMS.ACCOUNT.SET_A_NEW_EMAIL' | translate"
(click)="updateEmail()"
>
<ion-icon name="pencil" slot="icon-only" />
</ion-button>
</ion-item>
<ion-item>
<ion-input
testId="account.password"
type="text"
readonly="true"
value="********"
labelPlacement="stacked"
[label]="'IDEA_TEAMS.ACCOUNT.PASSWORD' | translate"
/>
<ion-button
testId="setNewPasswordButton"
slot="end"
fill="clear"
class="marginTop"
[title]="'IDEA_TEAMS.ACCOUNT.SET_A_NEW_PASSWORD' | translate"
(click)="updatePassword()"
>
<ion-icon name="pencil" slot="icon-only" />
</ion-button>
</ion-item>
<ion-list-header class="disruptive">
<ion-badge color="danger">
<ion-icon name="nuclear" /> {{ 'IDEA_TEAMS.ACCOUNT.DISRUPTIVE_ACTIONS' | translate }}
</ion-badge>
</ion-list-header>
<ion-item>
<ion-label class="ion-text-wrap">
{{ 'IDEA_TEAMS.ACCOUNT.USER_DELETION' | translate }}
<p>
{{ 'IDEA_TEAMS.ACCOUNT.IRREVERSIBLE_OPERATION' | translate }}
<br />
<i>{{ 'IDEA_TEAMS.ACCOUNT.YOU_MUST_LEAVE_ALL_TEAMS_FIRST' | translate }}</i>
</p>
</ion-label>
<ion-button
testId="deleteUserButton"
slot="end"
color="danger"
[title]="'IDEA_TEAMS.ACCOUNT.DELETE_PERMANENTLY_USER' | translate"
(click)="deleteUser()"
>
{{ 'IDEA_TEAMS.ACCOUNT.DELETE' | translate }}
</ion-button>
</ion-item>
</ion-list>
</ion-content>
`, isInline: true, styles: [".account{max-width:500px;margin:0 auto;background:transparent}.account ion-item{--background: var(--ion-color-white);--border-color: var(--ion-color-light)}.disruptive{margin-top:50px;padding-left:0}.marginTop{margin-top:14px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: IonBadge, selector: "ion-badge", inputs: ["color", "mode"] }, { kind: "component", type: IonListHeader, selector: "ion-list-header", inputs: ["color", "lines", "mode"] }, { kind: "component", type: IonInput, selector: "ion-input", inputs: ["accept", "autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearOnEdit", "color", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "max", "maxlength", "min", "minlength", "mode", "multiple", "name", "pattern", "placeholder", "readonly", "required", "shape", "size", "spellcheck", "step", "type", "value"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { 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: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "pipe", type: IDEATranslatePipe, name: "translate" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAAccountPage, decorators: [{
type: Component,
args: [{ selector: 'account', standalone: true, imports: [
CommonModule,
FormsModule,
IDEATranslatePipe,
IonLabel,
IonBadge,
IonListHeader,
IonInput,
IonItem,
IonContent,
IonTitle,
IonIcon,
IonButton,
IonButtons,
IonToolbar,
IonHeader,
IonList
], template: `
<ion-header>
<ion-toolbar color="ideaToolbar">
<ion-buttons slot="start">
<ion-button testId="closeButton" [title]="'COMMON.CLOSE' | translate" (click)="close()">
<ion-icon name="arrow-back" slot="icon-only" />
</ion-button>
</ion-buttons>
<ion-title>{{ 'IDEA_TEAMS.ACCOUNT.ACCOUNT' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-list lines="full" class="account">
<ion-item>
<ion-input
testId="account.email"
type="text"
readonly="true"
labelPlacement="stacked"
[label]="'IDEA_TEAMS.ACCOUNT.EMAIL' | translate"
[(ngModel)]="newEmail"
/>
<ion-button
testId="setNewEmailButton"
slot="end"
fill="clear"
class="marginTop"
[title]="'IDEA_TEAMS.ACCOUNT.SET_A_NEW_EMAIL' | translate"
(click)="updateEmail()"
>
<ion-icon name="pencil" slot="icon-only" />
</ion-button>
</ion-item>
<ion-item>
<ion-input
testId="account.password"
type="text"
readonly="true"
value="********"
labelPlacement="stacked"
[label]="'IDEA_TEAMS.ACCOUNT.PASSWORD' | translate"
/>
<ion-button
testId="setNewPasswordButton"
slot="end"
fill="clear"
class="marginTop"
[title]="'IDEA_TEAMS.ACCOUNT.SET_A_NEW_PASSWORD' | translate"
(click)="updatePassword()"
>
<ion-icon name="pencil" slot="icon-only" />
</ion-button>
</ion-item>
<ion-list-header class="disruptive">
<ion-badge color="danger">
<ion-icon name="nuclear" /> {{ 'IDEA_TEAMS.ACCOUNT.DISRUPTIVE_ACTIONS' | translate }}
</ion-badge>
</ion-list-header>
<ion-item>
<ion-label class="ion-text-wrap">
{{ 'IDEA_TEAMS.ACCOUNT.USER_DELETION' | translate }}
<p>
{{ 'IDEA_TEAMS.ACCOUNT.IRREVERSIBLE_OPERATION' | translate }}
<br />
<i>{{ 'IDEA_TEAMS.ACCOUNT.YOU_MUST_LEAVE_ALL_TEAMS_FIRST' | translate }}</i>
</p>
</ion-label>
<ion-button
testId="deleteUserButton"
slot="end"
color="danger"
[title]="'IDEA_TEAMS.ACCOUNT.DELETE_PERMANENTLY_USER' | translate"
(click)="deleteUser()"
>
{{ 'IDEA_TEAMS.ACCOUNT.DELETE' | translate }}
</ion-button>
</ion-item>
</ion-list>
</ion-content>
`, styles: [".account{max-width:500px;margin:0 auto;background:transparent}.account ion-item{--background: var(--ion-color-white);--border-color: var(--ion-color-light)}.disruptive{margin-top:50px;padding-left:0}.marginTop{margin-top:14px}\n"] }]
}] });
const ideaAccountRoutes = [{ path: '', component: IDEAAccountPage }];
class IDEAttachmentsComponent {
constructor() {
this._platform = inject(Platform);
this._loading = inject(IDEALoadingService);
this._message = inject(IDEAMessageService);
this._tc = inject(IDEATinCanService);
this._api = inject(IDEAAWSAPIService);
this._offline = inject(IDEAOfflineService);
this._translate = inject(IDEATranslationsService);
/**
* The team from which we want to load the resources. Default: try to guess current team.
*/
this.team = null;
/**
* The path to the online API resource, as an array. Don't include the team. E.g. `['entities', entityId]`.
*/
this.pathResource = [];
/**
* The array in which we want to add/remove attachments.
*/
this.attachments = null;
/**
* Regulate the mode (view/edit).
*/
this.editMode = false;
/**
* Show errors as reported from the parent component.
*/
this.errors = new Set();
/**
* The lines attribute of the item.
*/
this.lines = 'none';
/**
* Stack of errors from the last upload.
*/
this.uploadErrors = [];
}
ngOnInit() {
// if the team isn't specified, try to guess it in the usual IDEA's paths
this.team = this.team || this._tc.get('membership').teamId || this._tc.get('teamId');
this.requestURL = `teams/${this.team}/`;
if (this.pathResource && this.pathResource.length)
this.requestURL = this.requestURL.concat(this.pathResource.filter(x => x).join('/'));
}
isCapacitor() {
return this._platform.is('capacitor');
}
hasFieldAnError(field) {
return this.errors.has(field);
}
browseFiles() {
document.getElementById('attachmentPicker').click();
}
addAttachmentFromFile(ev) {
this.uploadErrors = new Array();
const files = ev.target ? ev.target.files : {};
for (let i = 0; i < files.length; i++) {
const file = files.item(i);
const fullName = file.name.split('.');
const format = fullName.pop();
const name = fullName.join('.');
this.addAttachment(name, format, file);
}
}
async takePictureAndAttach(ev) {
if (ev)
ev.stopPropagation();
if (!this._platform.is('capacitor') || !Camera)
return;
const image = await Camera.getPhoto({
quality: 90,
allowEditing: false,
source: CameraSource.Camera,
resultType: CameraResultType.Base64
});
const filename = new Date().toISOString();
const content = this.base64toBlob(image.base64String, 'image/jpeg');
this.addAttachment(filename, image.format, content);
}
base64toBlob(base64str, type) {
const binary = atob(base64str);
const array = [];
for (let i = 0; i < binary.length; i++)
array.push(binary.charCodeAt(i));
return new Blob([new Uint8Array(array)], { type });
}
async addAttachment(name, format, content) {
if (format === FileFormatTypes.HEIC) {
format = FileFormatTypes.JPEG;
content = await heic2any({ blob: content, toType: 'image/jpeg' });
}
const attachment = new Attachment({ name, format });
this.attachments.push(attachment);
try {
const signedURL = await this._api.patchResource(this.requestURL, {
body: { action: 'ATTACHMENTS_PUT', attachmentId: attachment.attachmentId }
});
await this._api.rawRequest().put(signedURL.url, content).toPromise();
attachment.attachmentId = signedURL.id;
}
catch (error) {
this.uploadErrors.push(name);
this.removeAttachment(attachment);
this._message.error('IDEA_TEAMS.ATTACHMENTS.ERROR_UPLOADING_ATTACHMENT');
}
}
removeAttachment(attachment) {
const index = this.attachments.indexOf(attachment);
if (index !== -1)
this.attachments.splice(index, 1);
}
async openAttachment(attachment) {
try {
await this._loading.show();
const { url } = await this._api.patchResource(this.requestURL, {
body: { action: 'ATTACHMENTS_GET', attachmentId: attachment.attachmentId }
});
await Browser.open({ url });
}
catch (error) {
this._message.error('IDEA_TEAMS.ATTACHMENTS.ERROR_OPENING_ATTACHMENT');
}
finally {
this._loading.hide();
}
}
getFormatIcon(format) {
switch (format) {
case FileFormatTypes.JPG:
case FileFormatTypes.JPEG:
case FileFormatTypes.PNG:
return 'image';
case FileFormatTypes.PDF:
return 'document';
default:
return 'help';
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAttachmentsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: IDEAttachmentsComponent, isStandalone: true, selector: "idea-attachments", inputs: { team: "team", pathResource: "pathResource", attachments: "attachments", editMode: "editMode", errors: "errors", lines: "lines" }, ngImport: i0, template: "@for (att of attachments; track att; let index = $index; let odd = $odd) {\n <ion-item\n class=\"attachments\"\n [class.fieldHasError]=\"hasFieldAnError('attachments[' + index + '].attachmentId')\"\n [class.odd]=\"odd\"\n [lines]=\"lines\"\n >\n @if (att.attachmentId && _offline.isOnline()) {\n <ion-button fill=\"clear\" size=\"small\" slot=\"start\" (click)=\"openAttachment(att)\">\n <ion-icon name=\"open-outline\" slot=\"icon-only\" size=\"small\" />\n </ion-button>\n }\n @if (!editMode && att.attachmentId) {\n <ion-icon slot=\"start\" color=\"medium\" [name]=\"getFormatIcon(att.format)\" [title]=\"att.format\" />\n }\n @if (editMode && att.attachmentId) {\n <ion-input\n [(ngModel)]=\"att.name\"\n (ionBlur)=\"$event.target.value = $event.target.value || _translate._('IDEA_TEAMS.ATTACHMENTS.NO_NAME')\"\n />\n }\n @if (editMode && !att.attachmentId) {\n <ion-label class=\"loadingWarning\">\n {{ 'IDEA_TEAMS.ATTACHMENTS.UPLOADING_ATTACHMENT_WARNING' | translate }}\n </ion-label>\n }\n @if (!editMode) {\n <ion-label>{{ att.name }}.{{ att.format }}</ion-label>\n }\n @if (!att.attachmentId) {\n <ion-spinner slot=\"end\" [title]=\"'IDEA_TEAMS.ATTACHMENTS.UPLOADING' | translate\" />\n }\n @if (editMode) {\n <ion-button\n slot=\"end\"\n color=\"danger\"\n fill=\"clear\"\n [title]=\"'IDEA_TEAMS.ATTACHMENTS.REMOVE_ATTACHMENT' | translate\"\n (click)=\"removeAttachment(att)\"\n >\n <ion-icon name=\"remove\" slot=\"icon-only\" />\n </ion-button>\n }\n </ion-item>\n}\n<!----->\n@if (!editMode && !attachments?.length) {\n <ion-item lines=\"none\" class=\"noAttachments\">\n <ion-label>\n {{ 'IDEA_TEAMS.ATTACHMENTS.NO_ATTACHMENT' | translate }}\n </ion-label>\n </ion-item>\n}\n<!----->\n@if (editMode) {\n <div>\n @for (err of uploadErrors; track err) {\n <ion-item class=\"attachments\" [lines]=\"lines\">\n <ion-icon name=\"alert-circle\" slot=\"start\" color=\"danger\" />\n <ion-label color=\"danger\">\n {{ err }}\n <p>{{ 'IDEA_TEAMS.ATTACHMENTS.ERROR_UPLOADING_ATTACHMENT' | translate }}</p>\n </ion-label>\n <ion-button\n slot=\"end\"\n color=\"danger\"\n fill=\"clear\"\n [title]=\"'IDEA_TEAMS.ATTACHMENTS.HIDE_ERROR' | translate\"\n (click)=\"uploadErrors.splice(uploadErrors.indexOf(err), 1)\"\n >\n <ion-icon name=\"close\" slot=\"icon-only\" />\n </ion-button>\n </ion-item>\n }\n </div>\n}\n<!----->\n@if (editMode) {\n <ion-item lines=\"none\" class=\"selectable\" (click)=\"browseFiles()\">\n <ion-label color=\"medium\">{{ 'IDEA_TEAMS.ATTACHMENTS.TAP_TO_ADD_ATTACHMENT' | translate }}</ion-label>\n <input id=\"attachmentPicker\" type=\"file\" multiple style=\"display: none\" (change)=\"addAttachmentFromFile($event)\" />\n @if (editMode) {\n <ion-icon slot=\"end\" name=\"caret-down\" class=\"selectIcon\" />\n }\n @if (editMode && isCapacitor()) {\n <ion-button slot=\"end\" fill=\"clear\" color=\"dark\" (click)=\"takePictureAndAttach($event)\">\n <ion-icon slot=\"icon-only\" name=\"camera\" />\n </ion-button>\n }\n </ion-item>\n}\n", styles: [".attachments ion-button[slot=start],.attachments ion-icon[slot=start]{margin-right:10px}.attachments .loadingWarning{font-style:italic;font-size:.8em;color:var(--ion-color-medium)}.noAttachments{font-style:italic}.selectIcon{margin:0;padding-left:4px;font-size:.8em;color:var(--ion-color-medium)}.selectable{cursor:pointer}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: IonInput, selector: "ion-input", inputs: ["accept", "autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearOnEdit", "color", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "max", "maxlength", "min", "minlength", "mode", "multiple", "name", "pattern", "placeholder", "readonly", "required", "shape", "size", "spellcheck", "step", "type", "value"] }, { 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: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "pipe", type: IDEATranslatePipe, name: "translate" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEAttachmentsComponent, decorators: [{
type: Component,
args: [{ selector: 'idea-attachments', standalone: true, imports: [CommonModule, FormsModule, IDEATranslatePipe, IonSpinner, IonLabel, IonInput, IonIcon, IonButton, IonItem], template: "@for (att of attachments; track att; let index = $index; let odd = $odd) {\n <ion-item\n class=\"attachments\"\n [class.fieldHasError]=\"hasFieldAnError('attachments[' + index + '].attachmentId')\"\n [class.odd]=\"odd\"\n [lines]=\"lines\"\n >\n @if (att.attachmentId && _offline.isOnline()) {\n <ion-button fill=\"clear\" size=\"small\" slot=\"start\" (click)=\"openAttachment(att)\">\n <ion-icon name=\"open-outline\" slot=\"icon-only\" size=\"small\" />\n </ion-button>\n }\n @if (!editMode && att.attachmentId) {\n <ion-icon slot=\"start\" color=\"medium\" [name]=\"getFormatIcon(att.format)\" [title]=\"att.format\" />\n }\n @if (editMode && att.attachmentId) {\n <ion-input\n [(ngModel)]=\"att.name\"\n (ionBlur)=\"$event.target.value = $event.target.value || _translate._('IDEA_TEAMS.ATTACHMENTS.NO_NAME')\"\n />\n }\n @if (editMode && !att.attachmentId) {\n <ion-label class=\"loadingWarning\">\n {{ 'IDEA_TEAMS.ATTACHMENTS.UPLOADING_ATTACHMENT_WARNING' | translate }}\n </ion-label>\n }\n @if (!editMode) {\n <ion-label>{{ att.name }}.{{ att.format }}</ion-label>\n }\n @if (!att.attachmentId) {\n <ion-spinner slot=\"end\" [title]=\"'IDEA_TEAMS.ATTACHMENTS.UPLOADING' | translate\" />\n }\n @if (editMode) {\n <ion-button\n slot=\"end\"\n color=\"danger\"\n fill=\"clear\"\n [title]=\"'IDEA_TEAMS.ATTACHMENTS.REMOVE_ATTACHMENT' | translate\"\n (click)=\"removeAttachment(att)\"\n >\n <ion-icon name=\"remove\" slot=\"icon-only\" />\n </ion-button>\n }\n </ion-item>\n}\n<!----->\n@if (!editMode && !attachments?.length) {\n <ion-item lines=\"none\" class=\"noAttachments\">\n <ion-label>\n {{ 'IDEA_TEAMS.ATTACHMENTS.NO_ATTACHMENT' | translate }}\n </ion-label>\n </ion-item>\n}\n<!----->\n@if (editMode) {\n <div>\n @for (err of uploadErrors; track err) {\n <ion-item class=\"attachments\" [lines]=\"lines\">\n <ion-icon name=\"alert-circle\" slot=\"start\" color=\"danger\" />\n <ion-label color=\"danger\">\n {{ err }}\n <p>{{ 'IDEA_TEAMS.ATTACHMENTS.ERROR_UPLOADING_ATTACHMENT' | translate }}</p>\n </ion-label>\n <ion-button\n slot=\"end\"\n color=\"danger\"\n fill=\"clear\"\n [title]=\"'IDEA_TEAMS.ATTACHMENTS.HIDE_ERROR' | translate\"\n (click)=\"uploadErrors.splice(uploadErrors.indexOf(err), 1)\"\n >\n <ion-icon name=\"close\" slot=\"icon-only\" />\n </ion-button>\n </ion-item>\n }\n </div>\n}\n<!----->\n@if (editMode) {\n <ion-item lines=\"none\" class=\"selectable\" (click)=\"browseFiles()\">\n <ion-label color=\"medium\">{{ 'IDEA_TEAMS.ATTACHMENTS.TAP_TO_ADD_ATTACHMENT' | translate }}</ion-label>\n <input id=\"attachmentPicker\" type=\"file\" multiple style=\"display: none\" (change)=\"addAttachmentFromFile($event)\" />\n @if (editMode) {\n <ion-icon slot=\"end\" name=\"caret-down\" class=\"selectIcon\" />\n }\n @if (editMode && isCapacitor()) {\n <ion-button slot=\"end\" fill=\"clear\" color=\"dark\" (click)=\"takePictureAndAttach($event)\">\n <ion-icon slot=\"icon-only\" name=\"camera\" />\n </ion-button>\n }\n </ion-item>\n}\n", styles: [".attachments ion-button[slot=start],.attachments ion-icon[slot=start]{margin-right:10px}.attachments .loadingWarning{font-style:italic;font-size:.8em;color:var(--ion-color-medium)}.noAttachments{font-style:italic}.selectIcon{margin:0;padding-left:4px;font-size:.8em;color:var(--ion-color-medium)}.selectable{cursor:pointer}\n"] }]
}], propDecorators: { team: [{
type: Input
}], pathResource: [{
type: Input
}], attachments: [{
type: Input
}], editMode: [{
type: Input
}], errors: [{
type: Input
}], lines: [{
type: Input
}] } });
/**
* The possibile file types (formats).
*/
var FileFormatTypes;
(function (FileFormatTypes) {
FileFormatTypes["JPG"] = "jpg";
FileFormatTypes["JPEG"] = "jpeg";
FileFormatTypes["PNG"] = "png";
FileFormatTypes["PDF"] = "pdf";
FileFormatTypes["HEIC"] = "heic";
})(FileFormatTypes || (FileFormatTypes = {}));
class IDEARCConfiguratorComponent {
constructor() {
this._message = inject(IDEAMessageService);
this._tc = inject(IDEATinCanService);
this._API = inject(IDEAAWSAPIService);
/**
* Icon select.
*/
this.iconSelect = new EventEmitter();
}
async ngOnInit() {
// if the team isn't specified, try to guess it in the usual IDEA's paths
this.team = this.team || this._tc.get('membership').teamId || this._tc.get('teamId');
try {
const folders = await this._API.getResource(`teams/${this.team}/folders`);
this.folders = folders;
this.foldersSuggestions = folders.map(x => new Suggestion({ value: x.folderId, name: x.name }));
}
catch (error) {
this._message.error('COMMON.COULDNT_LOAD_LIST');
}
}
setFolder(folderId) {
const folder = this.folders.find(f => f.folderId === folderId);
if (folder) {
this.folder.folderId = folderId;
this.folder.name = folder.name;
}
else {
this.folder.folderId = null;
this.folder.name = null;
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEARCConfiguratorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.3", type: IDEARCConfiguratorComponent, isStandalone: true, selector: "idea-rc-configurator", inputs: { team: "team", folder: "folder", label: "label", editMode: "editMode", lines: "lines", icon: "icon", iconColor: "iconColor" }, outputs: { iconSelect: "iconSelect" }, ngImport: i0, template: `
<idea-select
[data]="foldersSuggestions"
[description]="folder?.name || 'IDEA_TEAMS.RESOURCE_CENTER.NO_FOLDER_SELECTED' | translate"
[label]="label"
[placeholder]="'IDEA_TEAMS.RESOURCE_CENTER.SELECT_FOLDER' | translate"
[searchPlaceholder]="'IDEA_TEAMS.RESOURCE_CENTER.SELECT_FOLDER' | translate"
[lines]="lines"
[hideIdFromUI]="true"
[disabled]="!editMode"
[avoidAutoSelection]="true"
[icon]="icon"
[iconColor]="iconColor"
(select)="$event ? setFolder($event?.value) : null"
(iconSelect)="iconSelect.emit()"
/>
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IDEASelectComponent, selector: "idea-select", inputs: ["description", "data", "dataProvider", "label", "icon", "iconColor", "placeholder", "searchPlaceholder", "noElementsFoundText", "disabled", "tappableWhenDisabled", "obligatory", "lines", "color", "allowUnlistedValues", "allowUnlistedValuesPrefix", "sortData", "clearValueAfterSelection", "hideIdFromUI", "hideClearButton", "mustChoose", "category1", "category2", "showCategoriesFilters", "avoidAutoSelection"], outputs: ["select", "iconSelect", "selectWhenDisabled"] }, { kind: "pipe", type: IDEATranslatePipe, name: "translate" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEARCConfiguratorComponent, decorators: [{
type: Component,
args: [{
selector: 'idea-rc-configurator',
standalone: true,
imports: [CommonModule, IDEATranslatePipe, IDEASelectComponent],
template: `
<idea-select
[data]="foldersSuggestions"
[description]="folder?.name || 'IDEA_TEAMS.RESOURCE_CENTER.NO_FOLDER_SELECTED' | translate"
[label]="label"
[placeholder]="'IDEA_TEAMS.RESOURCE_CENTER.SELECT_FOLDER' | translate"
[searchPlaceholder]="'IDEA_TEAMS.RESOURCE_CENTER.SELECT_FOLDER' | translate"
[lines]="lines"
[hideIdFromUI]="true"
[disabled]="!editMode"
[avoidAutoSelection]="true"
[icon]="icon"
[iconColor]="iconColor"
(select)="$event ? setFolder($event?.value) : null"
(iconSelect)="iconSelect.emit()"
/>
`
}]
}], propDecorators: { team: [{
type: Input
}], folder: [{
type: Input
}], label: [{
type: Input
}], editMode: [{
type: Input
}], lines: [{
type: Input
}], icon: [{
type: Input
}], iconColor: [{
type: Input
}], iconSelect: [{
type: Output
}] } });
const FILE_SIZE_LIMIT_MB = 10;
const MAX_PAGE_SIZE$1 = 24;
class IDEARCResourcesComponent {
constructor() {
this._tc = inject(IDEATinCanService);
this._modal = inject(ModalController);
this._alert = inject(AlertController);
this._actions = inject(IDEAActionSheetController);
this._loading = inject(IDEALoadingService);
this._message = inject(IDEAMessageService);
this._translate = inject(IDEATranslationsService);
this._API = inject(IDEAAWSAPIService);
this._offline = inject(IDEAOfflineService);
}
ngOnInit() {
// if the team isn't specified, try to guess it in the usual IDEA's paths
this.teamId = this.teamId || this._tc.get('membership').teamId || this._tc.get('teamId');
this.loadResources();
}
async loadResources(getFromNetwork) {
try {
const useCache = getFromNetwork ? CacheModes.NETWORK_FIRST : CacheModes.CACHE_FIRST;
const resources = await this._API.getResource(`teams/${this.teamId}/folders/${this.folder.folderId}/resources`, { useCache });
this.resources = resources.map(r => new RCResource(r));
this.search(this.searchbar ? this.searchbar.value : null);
}
catch (error) {
this._message.error('IDEA_TEAMS.RESOURCE_CENTER.COULDNT_LOAD_LIST');
}
}
search(toSearch, scrollToNextPage) {
toSearch = toSearch ? toSearch.toLowerCase() : '';
this.filteredResources = (this.resources || [])
.filter(m => toSearch
.split(' ')
.every(searchTerm => [m.name, m.format].filter(f => f).some(f => f.toLowerCase().includes(searchTerm))))
.sort((a, b) => a.name.localeCompare(b.name));
if (scrollToNextPage)
this.currentPage++;
else
this.currentPage = 0;
this.filteredResources = this.filteredResources.slice(0, (this.currentPage + 1) * MAX_PAGE_SIZE$1);
if (scrollToNextPage)
setTimeout(() => scrollToNextPage.complete(), 100);
}
doRefresh(refresher) {
this.filteredResources = null;
setTimeout(() => {
this.loadResources(Boolean(refresher));
if (refresher)
refresher.complete();
}, 500); // the timeout is needed
}
async openResource(resource) {
try {
await this._loading.show();
const request = `teams/${this.teamId}/folders/${this.folder.folderId}/resources`;
const body = { action: 'GET_DOWNLOAD_URL' };
const { url } = await this._API.patchResource(request, { resourceId: resource.resourceId, body });
Browser.open({ url });
}
catch (error) {
this._message.error('COMMON.OPERATION_FAILED');
}
finally {
this._loading.hide();
}
}
getFormatIcon(format) {
switch (format) {
case RCResourceFormats.JPG:
case RCResourceFormats.JPEG:
case RCResourceFormats.PNG:
return 'image';
case RCResourceFormats.PDF:
return 'document';
default:
return 'help';
}
}
async actionsOnResource(res) {
if (!this.admin)
return;
const header = this._translate._('IDEA_TEAMS.RESOURCE_CENTER.ACTIONS_ON_RESOURCE');
const buttons = [];
buttons.push({
text: this._translate._('IDEA_TEAMS.RESOURCE_CENTER.UPLOAD_NEW_VERSION'),
icon: 'cloud-upload',
handler: () => this.browseUpdateResource(res)
});
buttons.push({
text: this._translate._('IDEA_TEAMS.RESOURCE_CENTER.RENAME'),
icon: 'text',
handler: () => this.renameResource(res)
});
buttons.push({
text: this._translate._('IDEA_TEAMS.RESOURCE_CENTER.DELETE'),
role: 'destructive',
icon: 'trash',
handler: () => this.deleteResource(res)
});
buttons.push({ text: this._translate._('COMMON.CANCEL'), role: 'cancel', icon: 'arrow-undo' });
const actions = await this._actions.create({ header, buttons });
actions.present();
}
browseUpdateResource(res) {
if (!this.admin)
return;
document.getElementById(res.resourceId.concat('_picker')).click();
}
async updateResource(res, ev) {
this.uploadErrors = new Array();
// identify the file to upload (consider only the first file selected)
const fileList = ev.target ? ev.target.files : {};
const file = fileList.item(0);
// upload the file
await this._loading.show();
await this.uploadFile(file);
this._loading.hide();
if (this.uploadErrors.length)
this._message.error('IDEA_TEAMS.RESOURCE_CENTER.ONE_OR_MORE_FILE_UPLOAD_FAILED');
else
this._message.success('IDEA_TEAMS.RESOURCE_CENTER.UPLOAD_COMPLETED');
}
async renameResource(res) {
const doRename = async ({ name }) => {
if (!name)
return;
if (this.resources.some(x => x.resourceId !== res.resourceId && x.name === name))
return this._message.error('IDEA_TEAMS.RESOURCE_CENTER.RESOURCE_WITH_SAME_NAME_ALREADY_EXISTS');
res.name = name;
try {
await this._loading.show();
const path = `teams/${this.teamId}/folders/${this.folder.folderId}/resources`;
await this._API.putResource(path, { resourceId: res.resourceId, body: res });
// full-refresh to be sure we update the cache
this.loadResources(true);
}
catch (err) {
if (err.message === 'RESOURCE_WITH_SAME_NAME_ALREADY_EXISTS')
this._message.error('IDEA_TEAMS.RESOURCE_CENTER.RESOURCE_WITH_SAME_NAME_ALREADY_EXISTS');
else
this._message.error('COMMON.OPERATION_FAILED');
}
finally {
this._loading.hide();
}
};
const header = this._translate._('IDEA_TEAMS.RESOURCE_CENTER.RENAME_RESOURCE');
const subHeader = this._translate._('IDEA_TEAMS.RESOURCE_CENTER.SELECT_RESOURCE_NAME');
const message = this._translate._('IDEA_TEAMS.RESOURCE_CENTER.NAME_MUST_BE_UNIQUE_IN_FOLDER');
const inputs = [
{ name: 'name', placeholder: this._translate._('IDEA_TEAMS.RESOURCE_CENTER.NAME'), value: res.name }
];
const buttons = [
{ text: this._translate._('COMMON.CANCEL'), role: 'cancel' },
{ text: this._translate._('COMMON.CONFIRM'), handler: doRename }
];
const alert = await this._alert.create({ header, subHeader, message, inputs, buttons });
alert.present();
}
async deleteResource(res) {
const doDelete = async () => {
try {
await this._loading.show();
const path = `teams/${this.teamId}/folders/${this.folder.folderId}/resources`;
await this._API.deleteResource(path, { resourceId: res.resourceId });
// full-refresh to be sure we update the cache
this.loadResources(true);
}
catch (error) {
this._message.error('COMMON.OPERATION_FAILED');
}
finally {
this._loading.hide();
}
};
const header = this._translate._('COMMON.ARE_YOU_SURE');
const subHeader = this._translate._('COMMON.OPERATION_IRREVERSIBLE');
const buttons = [
{ text: this._translate._('COMMON.CANCEL'), role: 'cancel' },
{ text: this._translate._('COMMON.DELETE'), handler: doDelete }
];
const alert = await this._alert.create({ header, subHeader, buttons });
alert.present();
}
browseUploadNewResource() {
if (!this.admin)
return;
// browse the local file(s)
document.getElementById('newResourcePicker').click();
}
async uploadNewResources(ev) {
this.uploadErrors = new Array();
// gather the files to upload
const fileList = ev.target ? ev.target.files : {};
const files = new Array();
for (let i = 0; i < fileList.length; i++)
files.push(fileList.item(i));
// upload each file and show the results
await this._loading.show();
files.forEach(async (file) => await this.uploadFile(file));
this._loading.hide();
if (this.uploadErrors.length)
this._message.error('IDEA_TEAMS.RESOURCE_CENTER.ONE_OR_MORE_FILE_UPLOAD_FAILED');
else
this._message.success('IDEA_TEAMS.RESOURCE_CENTER.UPLOAD_COMPLETED');
// reload the resources (force update cache)
this.loadResources(true);
}
async uploadFile(file, existingRes) {
const fullName = file.name.split('.');
const format = fullName.pop();
const name = fullName.join('.');
let resource;
if (existingRes) {
existingRes.format = format;
resource = existingRes;
}
else
resource = new RCResource({ name, format });
if (!loopStringEnumValues(RCResourceFormats).some(x => x === format)) {
this.uploadErrors.push(this._translate._('IDEA_TEAMS.RESOURCE_CENTER.INVALID_FORMAT_FILE_', { name }));
return;
}
const sizeMB = Number((file.size / 1024 / 1024).toFixed(4));
if (sizeMB > FILE_SIZE_LIMIT_MB) {
this.uploadErrors.push(this._translate._('IDEA_TEAMS.RESOURCE_CENTER.INVALID_SIZE_FILE_', { name }));
return;
}
try {
const path = `teams/${this.teamId}/folders/${this.folder.folderId}/resources`;
let req;
if (existingRes)
req = this._API.putResource(path, { resourceId: resource.resourceId, body: resource });
else
req = this._API.postResource(path, { body: resource });
const newRes = await req;
try {
const { url } = await this._API.patchResource(path, {
resourceId: newRes.resourceId,
body: { action: 'GET_UPLOAD_URL' }
});
await this._API.rawRequest().put(url, file).toPromise();
}
catch (error) {
this.uploadErrors.push(this._translate._('IDEA_TEAMS.RESOURCE_CENTER.UPLOAD_ERROR_FILE', { name }));
}
}
catch (error) {
this.uploadErrors.push(this._translate._('IDEA_TEAMS.RESOURCE_CENTER.ERROR_CREATING_RESOURCE_FILE', { name }));
}
}
close() {
this._modal.dismiss();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IDEARCResourcesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: IDEARCResourcesComponent, isStandalone: true, selector: "idea-rc-resources", inputs: { teamId: "teamId", folder: "folder", admin: "admin" }, viewQueries: [{ propertyName: "searchbar", first: true, predicate: ["searchbar"], descendants: true }], ngImport: i0, template: `
<ion-header>
<ion-toolbar color="ideaToolbar">
<ion-buttons slot="start">
<ion-button [title]="'COMMON.CLOSE' | translate" (click)="close()">
<ion-icon name="arrow-back" slot="icon-only" />
</ion-button>
</ion-buttons>
<ion-searchbar
#searchbar
[placeholder]="
'IDEA_TEAMS.RESOURCE_CENTER.SEARCH_FOR_RESOURCES_OF_FOLDER_' | translate: { folder: folder.name }
"
(ionInput)="search($event.target ? $event.target.value : '')"
/>
<ion-buttons slot="end">
@if (admin) {
<ion-button
[disabled]="_offline.isOffline()"
[title]="'IDEA_TEAMS.RESOURCE_CENTER.UPLOAD_NEW_RESOURCES' | translate"
(click)="browseUploadNewResource()"
>
<ion-icon slot="icon-only" name="cloud-upload" />
</ion-button>
}
<input
id="newResourcePicker"
type="file"
accept=".jpg,.jpeg,.png,.pdf"
multiple
style="display: none"
(change)="uploadNewResources($event)"
/>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event.target)">
<ion-refresher-content />
</ion-refresher>