UNPKG

@idea-ionic/teams

Version:
884 lines (878 loc) 101 kB
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>