UNPKG

@idea-ionic/teams

Version:
1 lines 115 kB
{"version":3,"file":"idea-ionic-teams.mjs","sources":["../../../modules/teams/src/account/account.page.ts","../../../modules/teams/src/account/account.routes.ts","../../../modules/teams/src/attachments/attachments.component.ts","../../../modules/teams/src/attachments/attachments.component.html","../../../modules/teams/src/resourceCenter/RCConfigurator.component.ts","../../../modules/teams/src/resourceCenter/RCResources.component.ts","../../../modules/teams/src/resourceCenter/RCFolders.component.ts","../../../modules/teams/src/resourceCenter/RCPicker.component.ts","../../../modules/teams/src/teams/teams.page.ts","../../../modules/teams/src/teams/teams.routes.ts","../../../modules/teams/idea-ionic-teams.ts"],"sourcesContent":["import { CommonModule } from '@angular/common';\nimport { Component, OnInit, inject } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport {\n AlertController,\n NavController,\n IonList,\n IonHeader,\n IonToolbar,\n IonButtons,\n IonButton,\n IonIcon,\n IonTitle,\n IonContent,\n IonItem,\n IonInput,\n IonListHeader,\n IonBadge,\n IonLabel\n} from '@ionic/angular/standalone';\nimport { User } from 'idea-toolbox';\nimport {\n IDEAEnvironment,\n IDEALoadingService,\n IDEAMessageService,\n IDEATranslatePipe,\n IDEATranslationsService\n} from '@idea-ionic/common';\nimport { IDEATinCanService, IDEAAWSAPIService } from '@idea-ionic/uncommon';\n\n@Component({\n selector: 'account',\n standalone: true,\n imports: [\n CommonModule,\n FormsModule,\n IDEATranslatePipe,\n IonLabel,\n IonBadge,\n IonListHeader,\n IonInput,\n IonItem,\n IonContent,\n IonTitle,\n IonIcon,\n IonButton,\n IonButtons,\n IonToolbar,\n IonHeader,\n IonList\n ],\n template: `\n <ion-header>\n <ion-toolbar color=\"ideaToolbar\">\n <ion-buttons slot=\"start\">\n <ion-button testId=\"closeButton\" [title]=\"'COMMON.CLOSE' | translate\" (click)=\"close()\">\n <ion-icon name=\"arrow-back\" slot=\"icon-only\" />\n </ion-button>\n </ion-buttons>\n <ion-title>{{ 'IDEA_TEAMS.ACCOUNT.ACCOUNT' | translate }}</ion-title>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <ion-list lines=\"full\" class=\"account\">\n <ion-item>\n <ion-input\n testId=\"account.email\"\n type=\"text\"\n readonly=\"true\"\n labelPlacement=\"stacked\"\n [label]=\"'IDEA_TEAMS.ACCOUNT.EMAIL' | translate\"\n [(ngModel)]=\"newEmail\"\n />\n <ion-button\n testId=\"setNewEmailButton\"\n slot=\"end\"\n fill=\"clear\"\n class=\"marginTop\"\n [title]=\"'IDEA_TEAMS.ACCOUNT.SET_A_NEW_EMAIL' | translate\"\n (click)=\"updateEmail()\"\n >\n <ion-icon name=\"pencil\" slot=\"icon-only\" />\n </ion-button>\n </ion-item>\n <ion-item>\n <ion-input\n testId=\"account.password\"\n type=\"text\"\n readonly=\"true\"\n value=\"********\"\n labelPlacement=\"stacked\"\n [label]=\"'IDEA_TEAMS.ACCOUNT.PASSWORD' | translate\"\n />\n <ion-button\n testId=\"setNewPasswordButton\"\n slot=\"end\"\n fill=\"clear\"\n class=\"marginTop\"\n [title]=\"'IDEA_TEAMS.ACCOUNT.SET_A_NEW_PASSWORD' | translate\"\n (click)=\"updatePassword()\"\n >\n <ion-icon name=\"pencil\" slot=\"icon-only\" />\n </ion-button>\n </ion-item>\n <ion-list-header class=\"disruptive\">\n <ion-badge color=\"danger\">\n <ion-icon name=\"nuclear\" /> {{ 'IDEA_TEAMS.ACCOUNT.DISRUPTIVE_ACTIONS' | translate }}\n </ion-badge>\n </ion-list-header>\n <ion-item>\n <ion-label class=\"ion-text-wrap\">\n {{ 'IDEA_TEAMS.ACCOUNT.USER_DELETION' | translate }}\n <p>\n {{ 'IDEA_TEAMS.ACCOUNT.IRREVERSIBLE_OPERATION' | translate }}\n <br />\n <i>{{ 'IDEA_TEAMS.ACCOUNT.YOU_MUST_LEAVE_ALL_TEAMS_FIRST' | translate }}</i>\n </p>\n </ion-label>\n <ion-button\n testId=\"deleteUserButton\"\n slot=\"end\"\n color=\"danger\"\n [title]=\"'IDEA_TEAMS.ACCOUNT.DELETE_PERMANENTLY_USER' | translate\"\n (click)=\"deleteUser()\"\n >\n {{ 'IDEA_TEAMS.ACCOUNT.DELETE' | translate }}\n </ion-button>\n </ion-item>\n </ion-list>\n </ion-content>\n `,\n styles: [\n `\n .account {\n max-width: 500px;\n margin: 0 auto;\n background: transparent;\n ion-item {\n --background: var(--ion-color-white);\n --border-color: var(--ion-color-light);\n }\n }\n .disruptive {\n margin-top: 50px;\n padding-left: 0;\n }\n .marginTop {\n margin-top: 14px;\n }\n `\n ]\n})\nexport class IDEAAccountPage implements OnInit {\n protected _env = inject(IDEAEnvironment);\n private _tc = inject(IDEATinCanService);\n private _nav = inject(NavController);\n private _alert = inject(AlertController);\n private _loading = inject(IDEALoadingService);\n private _message = inject(IDEAMessageService);\n private _API = inject(IDEAAWSAPIService);\n private _translate = inject(IDEATranslationsService);\n\n user: User;\n newEmail: string;\n newPassword: string;\n newPasswordConfirm: string;\n\n ngOnInit(): void {\n this.user = new User(this._tc.get('user'));\n this.newEmail = this.user.email;\n }\n\n async updateEmail(): Promise<void> {\n const doUpdate = async ({ pwd, email }: any): Promise<void> => {\n if (!email) return this._message.info('IDEA_TEAMS.ACCOUNT.INVALID_EMAIL');\n try {\n await this._loading.show();\n const body = { password: pwd, newEmail: email, project: this._env.idea.project };\n await this._API.postResource('emailChangeRequests', { idea: true, body });\n const alert = await this._alert.create({\n header: this._translate._('COMMON.OPERATION_COMPLETED'),\n subHeader: this._translate._('IDEA_TEAMS.ACCOUNT.UPDATE_EMAIL_FLOW_EXPLANATION'),\n buttons: [this._translate._('COMMON.GOT_IT')]\n });\n alert.present();\n } catch (error) {\n this._message.error('IDEA_TEAMS.ACCOUNT.OPERATION_FAILED_PASSWORD');\n } finally {\n this._loading.hide();\n }\n };\n\n const header = this._translate._('IDEA_TEAMS.ACCOUNT.UPDATE_EMAIL');\n const subHeader = this._translate._('IDEA_TEAMS.ACCOUNT.UPDATE_EMAIL_EXPLANATION');\n const inputs: any[] = [\n { name: 'pwd', type: 'password', placeholder: this._translate._('IDEA_TEAMS.ACCOUNT.YOUR_CURRENT_PASSWORD') },\n { name: 'email', placeholder: this._translate._('IDEA_TEAMS.ACCOUNT.NEW_EMAIL') }\n ];\n const buttons = [\n { text: this._translate._('COMMON.CANCEL'), role: 'cancel' },\n { text: this._translate._('COMMON.CONFIRM'), handler: doUpdate }\n ];\n const alert = await this._alert.create({ header, subHeader, inputs, buttons });\n alert.present();\n }\n async updatePassword(): Promise<void> {\n const doUpdate = async ({ old, newP, new2 }: any): Promise<void> => {\n if (newP.length < 8)\n this._message.warning(this._translate._('IDEA_TEAMS.ACCOUNT.INVALID_PASSWORD', { n: 8 }), true);\n else if (newP !== new2) this._message.warning('IDEA_TEAMS.ACCOUNT.PASSWORD_CONFIRMATION_DONT_MATCH');\n else {\n try {\n await this._loading.show();\n const body = { action: 'UPDATE_PASSWORD', password: old, newPassword: newP };\n await this._API.patchResource('users', { idea: true, resourceId: this.user.userId, body });\n this._message.success('IDEA_TEAMS.ACCOUNT.PASSWORD_UPDATED');\n } catch (error) {\n this._message.error('IDEA_TEAMS.ACCOUNT.OPERATION_FAILED_PASSWORD');\n } finally {\n this._loading.hide();\n }\n }\n };\n\n const header = this._translate._('IDEA_TEAMS.ACCOUNT.UPDATE_PASSWORD');\n const inputs: any[] = [\n { name: 'old', type: 'password', placeholder: this._translate._('IDEA_TEAMS.ACCOUNT.YOUR_CURRENT_PASSWORD') },\n { name: 'newP', type: 'password', placeholder: this._translate._('IDEA_TEAMS.ACCOUNT.NEW_PASSWORD_', { n: 8 }) },\n { name: 'new2', type: 'password', placeholder: this._translate._('IDEA_TEAMS.ACCOUNT.CONFIRM_NEW_PASSWORD') }\n ];\n const buttons = [\n { text: this._translate._('COMMON.CANCEL'), role: 'cancel' },\n { text: this._translate._('COMMON.CONFIRM'), handler: doUpdate }\n ];\n const alert = await this._alert.create({ header, inputs, buttons });\n alert.present();\n }\n\n async deleteUser(): Promise<void> {\n const doDelete = async ({ pwd }: any): Promise<void> => {\n try {\n await this._loading.show();\n await this._API.deleteResource('users', {\n idea: true,\n resourceId: this.user.userId,\n headers: { password: pwd }\n });\n window.location.assign('');\n } catch (error) {\n this._message.error('COMMON.OPERATION_FAILED');\n } finally {\n this._loading.hide();\n }\n };\n\n const header = this._translate._('IDEA_TEAMS.ACCOUNT.USER_DELETION');\n const subHeader = this._translate._('IDEA_TEAMS.ACCOUNT.USER_DELETION_ARE_YOU_SURE');\n const inputs: any[] = [\n { name: 'pwd', type: 'password', placeholder: this._translate._('IDEA_TEAMS.ACCOUNT.YOUR_CURRENT_PASSWORD') }\n ];\n const buttons = [\n { text: this._translate._('COMMON.CANCEL'), role: 'cancel' },\n { text: this._translate._('COMMON.CONFIRM'), handler: doDelete }\n ];\n const alert = await this._alert.create({ header, subHeader, inputs, buttons });\n alert.present();\n }\n\n close(errorMessage?: string): void {\n if (errorMessage) this._message.error(errorMessage);\n try {\n this._nav.back();\n } catch (_) {\n this._nav.navigateRoot(['']);\n }\n }\n}\n","import { Routes } from '@angular/router';\n\nimport { IDEAAccountPage } from './account.page';\n\nexport const ideaAccountRoutes: Routes = [{ path: '', component: IDEAAccountPage }];\n","import { CommonModule } from '@angular/common';\nimport { Component, inject, Input, OnInit } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { Platform, IonItem, IonButton, IonIcon, IonInput, IonLabel, IonSpinner } from '@ionic/angular/standalone';\nimport { Browser } from '@capacitor/browser';\nimport { Camera, CameraResultType, CameraSource } from '@capacitor/camera';\nimport heic2any from 'heic2any';\nimport { Attachment } from 'idea-toolbox';\nimport { IDEALoadingService, IDEAMessageService, IDEATranslatePipe, IDEATranslationsService } from '@idea-ionic/common';\nimport { IDEAAWSAPIService, IDEAOfflineService, IDEATinCanService } from '@idea-ionic/uncommon';\n\n@Component({\n selector: 'idea-attachments',\n standalone: true,\n imports: [CommonModule, FormsModule, IDEATranslatePipe, IonSpinner, IonLabel, IonInput, IonIcon, IonButton, IonItem],\n templateUrl: 'attachments.component.html',\n styleUrls: ['attachments.component.scss']\n})\nexport class IDEAttachmentsComponent implements OnInit {\n private _platform = inject(Platform);\n private _loading = inject(IDEALoadingService);\n private _message = inject(IDEAMessageService);\n private _tc = inject(IDEATinCanService);\n private _api = inject(IDEAAWSAPIService);\n _offline = inject(IDEAOfflineService);\n _translate = inject(IDEATranslationsService);\n\n /**\n * The team from which we want to load the resources. Default: try to guess current team.\n */\n @Input() team: string | null = null;\n /**\n * The path to the online API resource, as an array. Don't include the team. E.g. `['entities', entityId]`.\n */\n @Input() pathResource: string[] = [];\n /**\n * The array in which we want to add/remove attachments.\n */\n @Input() attachments: Attachment[] | null = null;\n /**\n * Regulate the mode (view/edit).\n */\n @Input() editMode = false;\n /**\n * Show errors as reported from the parent component.\n */\n @Input() errors = new Set<string>();\n /**\n * The lines attribute of the item.\n */\n @Input() lines = 'none';\n\n /**\n * URL towards to make API requests, based on the path of the resource.\n */\n requestURL: string;\n /**\n * Stack of errors from the last upload.\n */\n uploadErrors: string[] = [];\n\n ngOnInit(): void {\n // if the team isn't specified, try to guess it in the usual IDEA's paths\n this.team = this.team || this._tc.get('membership').teamId || this._tc.get('teamId');\n this.requestURL = `teams/${this.team}/`;\n if (this.pathResource && this.pathResource.length)\n this.requestURL = this.requestURL.concat(this.pathResource.filter(x => x).join('/'));\n }\n\n isCapacitor(): boolean {\n return this._platform.is('capacitor');\n }\n\n hasFieldAnError(field: string): boolean {\n return this.errors.has(field);\n }\n\n browseFiles(): void {\n document.getElementById('attachmentPicker').click();\n }\n addAttachmentFromFile(ev: any): void {\n this.uploadErrors = new Array<string>();\n const files: FileList = ev.target ? ev.target.files : {};\n for (let i = 0; i < files.length; i++) {\n const file = files.item(i);\n const fullName = file.name.split('.');\n const format = fullName.pop();\n const name = fullName.join('.');\n this.addAttachment(name, format, file);\n }\n }\n async takePictureAndAttach(ev: Event): Promise<void> {\n if (ev) ev.stopPropagation();\n if (!this._platform.is('capacitor') || !Camera) return;\n const image = await Camera.getPhoto({\n quality: 90,\n allowEditing: false,\n source: CameraSource.Camera,\n resultType: CameraResultType.Base64\n });\n const filename = new Date().toISOString();\n const content = this.base64toBlob(image.base64String, 'image/jpeg');\n this.addAttachment(filename, image.format, content);\n }\n private base64toBlob(base64str: string, type: string): Blob {\n const binary = atob(base64str);\n const array = [];\n for (let i = 0; i < binary.length; i++) array.push(binary.charCodeAt(i));\n return new Blob([new Uint8Array(array)], { type });\n }\n private async addAttachment(name: string, format: string, content: any): Promise<void> {\n if (format === FileFormatTypes.HEIC) {\n format = FileFormatTypes.JPEG;\n content = await heic2any({ blob: content, toType: 'image/jpeg' });\n }\n const attachment = new Attachment({ name, format });\n this.attachments.push(attachment);\n try {\n const signedURL = await this._api.patchResource(this.requestURL, {\n body: { action: 'ATTACHMENTS_PUT', attachmentId: attachment.attachmentId }\n });\n await this._api.rawRequest().put(signedURL.url, content).toPromise();\n attachment.attachmentId = signedURL.id;\n } catch (error) {\n this.uploadErrors.push(name);\n this.removeAttachment(attachment);\n this._message.error('IDEA_TEAMS.ATTACHMENTS.ERROR_UPLOADING_ATTACHMENT');\n }\n }\n\n removeAttachment(attachment: Attachment): void {\n const index = this.attachments.indexOf(attachment);\n if (index !== -1) this.attachments.splice(index, 1);\n }\n\n async openAttachment(attachment: Attachment): Promise<void> {\n try {\n await this._loading.show();\n const { url } = await this._api.patchResource(this.requestURL, {\n body: { action: 'ATTACHMENTS_GET', attachmentId: attachment.attachmentId }\n });\n await Browser.open({ url });\n } catch (error) {\n this._message.error('IDEA_TEAMS.ATTACHMENTS.ERROR_OPENING_ATTACHMENT');\n } finally {\n this._loading.hide();\n }\n }\n\n getFormatIcon(format: string): string {\n switch (format) {\n case FileFormatTypes.JPG:\n case FileFormatTypes.JPEG:\n case FileFormatTypes.PNG:\n return 'image';\n case FileFormatTypes.PDF:\n return 'document';\n default:\n return 'help';\n }\n }\n}\n\n/**\n * The possibile file types (formats).\n */\nenum FileFormatTypes {\n JPG = 'jpg',\n JPEG = 'jpeg',\n PNG = 'png',\n PDF = 'pdf',\n HEIC = 'heic'\n}\n","@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","import { CommonModule } from '@angular/common';\nimport { Component, Input, Output, EventEmitter, OnInit, inject } from '@angular/core';\nimport { RCConfiguredFolder, RCFolder, Suggestion } from 'idea-toolbox';\nimport { IDEAMessageService, IDEASelectComponent, IDEATranslatePipe } from '@idea-ionic/common';\nimport { IDEAAWSAPIService, IDEATinCanService } from '@idea-ionic/uncommon';\n\n@Component({\n selector: 'idea-rc-configurator',\n standalone: true,\n imports: [CommonModule, IDEATranslatePipe, IDEASelectComponent],\n template: `\n <idea-select\n [data]=\"foldersSuggestions\"\n [description]=\"folder?.name || 'IDEA_TEAMS.RESOURCE_CENTER.NO_FOLDER_SELECTED' | translate\"\n [label]=\"label\"\n [placeholder]=\"'IDEA_TEAMS.RESOURCE_CENTER.SELECT_FOLDER' | translate\"\n [searchPlaceholder]=\"'IDEA_TEAMS.RESOURCE_CENTER.SELECT_FOLDER' | translate\"\n [lines]=\"lines\"\n [hideIdFromUI]=\"true\"\n [disabled]=\"!editMode\"\n [avoidAutoSelection]=\"true\"\n [icon]=\"icon\"\n [iconColor]=\"iconColor\"\n (select)=\"$event ? setFolder($event?.value) : null\"\n (iconSelect)=\"iconSelect.emit()\"\n />\n `\n})\nexport class IDEARCConfiguratorComponent implements OnInit {\n private _message = inject(IDEAMessageService);\n private _tc = inject(IDEATinCanService);\n private _API = inject(IDEAAWSAPIService);\n\n /**\n * The team from which we want to load the resources. Default: try to guess current team.\n */\n @Input() team: string;\n /**\n * The folder we want to configure with the Resource Center folder.\n */\n @Input() folder: RCConfiguredFolder;\n /**\n * The label for the field.\n */\n @Input() label: string;\n /**\n * Regulate the mode (view/edit).\n */\n @Input() editMode: boolean;\n /**\n * The lines attribute of the item.\n */\n @Input() lines: string;\n /**\n * The icon for the field.\n */\n @Input() icon: string;\n /**\n * The color of the icon.\n */\n @Input() iconColor: string;\n /**\n * Icon select.\n */\n @Output() iconSelect = new EventEmitter<void>();\n\n folders: RCFolder[];\n foldersSuggestions: Suggestion[];\n\n async ngOnInit(): Promise<void> {\n // if the team isn't specified, try to guess it in the usual IDEA's paths\n this.team = this.team || this._tc.get('membership').teamId || this._tc.get('teamId');\n try {\n const folders: RCFolder[] = await this._API.getResource(`teams/${this.team}/folders`);\n this.folders = folders;\n this.foldersSuggestions = folders.map(x => new Suggestion({ value: x.folderId, name: x.name }));\n } catch (error) {\n this._message.error('COMMON.COULDNT_LOAD_LIST');\n }\n }\n\n setFolder(folderId?: string): void {\n const folder = this.folders.find(f => f.folderId === folderId);\n if (folder) {\n this.folder.folderId = folderId;\n this.folder.name = folder.name;\n } else {\n this.folder.folderId = null;\n this.folder.name = null;\n }\n }\n}\n","import { CommonModule } from '@angular/common';\nimport { ViewChild, Component, Input, OnInit, inject } from '@angular/core';\nimport {\n IonInfiniteScroll,\n AlertController,\n ModalController,\n IonRefresher,\n IonSearchbar,\n IonHeader,\n IonToolbar,\n IonButtons,\n IonButton,\n IonIcon,\n IonContent,\n IonRefresherContent,\n IonList,\n IonCard,\n IonCardContent,\n IonListHeader,\n IonLabel,\n IonItem,\n IonSkeletonText,\n IonNote,\n IonInfiniteScrollContent\n} from '@ionic/angular/standalone';\nimport { Browser } from '@capacitor/browser';\nimport { loopStringEnumValues, RCFolder, RCResource, RCResourceFormats } from 'idea-toolbox';\nimport {\n IDEALoadingService,\n IDEAMessageService,\n IDEATranslationsService,\n IDEAActionSheetController,\n IDEATranslatePipe,\n IDEALocalizedDatePipe\n} from '@idea-ionic/common';\nimport { CacheModes, IDEAAWSAPIService, IDEAOfflineService, IDEATinCanService } from '@idea-ionic/uncommon';\n\nconst FILE_SIZE_LIMIT_MB = 10;\n\nconst MAX_PAGE_SIZE = 24;\n\n@Component({\n selector: 'idea-rc-resources',\n standalone: true,\n imports: [\n CommonModule,\n IDEATranslatePipe,\n IDEALocalizedDatePipe,\n IonSearchbar,\n IonInfiniteScroll,\n IonInfiniteScrollContent,\n IonNote,\n IonSkeletonText,\n IonItem,\n IonLabel,\n IonListHeader,\n IonCardContent,\n IonCard,\n IonList,\n IonRefresher,\n IonRefresherContent,\n IonContent,\n IonIcon,\n IonButton,\n IonButtons,\n IonToolbar,\n IonHeader\n ],\n template: `\n <ion-header>\n <ion-toolbar color=\"ideaToolbar\">\n <ion-buttons slot=\"start\">\n <ion-button [title]=\"'COMMON.CLOSE' | translate\" (click)=\"close()\">\n <ion-icon name=\"arrow-back\" slot=\"icon-only\" />\n </ion-button>\n </ion-buttons>\n <ion-searchbar\n #searchbar\n [placeholder]=\"\n 'IDEA_TEAMS.RESOURCE_CENTER.SEARCH_FOR_RESOURCES_OF_FOLDER_' | translate: { folder: folder.name }\n \"\n (ionInput)=\"search($event.target ? $event.target.value : '')\"\n />\n <ion-buttons slot=\"end\">\n @if (admin) {\n <ion-button\n [disabled]=\"_offline.isOffline()\"\n [title]=\"'IDEA_TEAMS.RESOURCE_CENTER.UPLOAD_NEW_RESOURCES' | translate\"\n (click)=\"browseUploadNewResource()\"\n >\n <ion-icon slot=\"icon-only\" name=\"cloud-upload\" />\n </ion-button>\n }\n <input\n id=\"newResourcePicker\"\n type=\"file\"\n accept=\".jpg,.jpeg,.png,.pdf\"\n multiple\n style=\"display: none\"\n (change)=\"uploadNewResources($event)\"\n />\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content>\n <ion-refresher slot=\"fixed\" (ionRefresh)=\"doRefresh($event.target)\">\n <ion-refresher-content />\n </ion-refresher>\n <ion-list class=\"aList\">\n @if (uploadErrors?.length) {\n <ion-card color=\"danger\">\n <ion-card-content>\n <b>{{ 'IDEA_TEAMS.RESOURCE_CENTER.THE_FOLLOWING_FILES_FAILED_UPLOAD' | translate }}:</b>\n <ul>\n @for (err of uploadErrors; track err) {\n <li>{{ err }}</li>\n }\n </ul>\n </ion-card-content>\n </ion-card>\n }\n <ion-list-header>\n <ion-label>\n <h2>{{ folder.name }}</h2>\n </ion-label>\n </ion-list-header>\n @if (!filteredResources) {\n <ion-item>\n <ion-label>\n <ion-skeleton-text animated style=\"width: 50%\" />\n </ion-label>\n </ion-item>\n }\n @if (filteredResources && !filteredResources.length) {\n <ion-item class=\"noElements\">\n <ion-label>{{ 'COMMON.NO_ELEMENT_FOUND' | translate }}</ion-label>\n </ion-item>\n }\n @for (r of filteredResources; track r) {\n <ion-item>\n <ion-button\n slot=\"start\"\n fill=\"clear\"\n [title]=\"'IDEA_TEAMS.RESOURCE_CENTER.OPEN_RESOURCE' | translate\"\n [disabled]=\"_offline.isOffline()\"\n (click)=\"openResource(r)\"\n >\n <ion-icon name=\"open-outline\" slot=\"icon-only\" />\n </ion-button>\n <ion-icon slot=\"start\" color=\"medium\" [name]=\"getFormatIcon(r.format)\" [title]=\"r.format\" />\n <ion-label>\n {{ r.name }}\n <p>\n {{ 'IDEA_TEAMS.RESOURCE_CENTER.LASTLY_UPDATED_X_AGO' | translate: { time: (r.version | dateLocale) } }}\n </p>\n </ion-label>\n <ion-note slot=\"end\">{{ r.format }}</ion-note>\n @if (admin) {\n <ion-button\n color=\"medium\"\n fill=\"clear\"\n slot=\"end\"\n [title]=\"'IDEA_TEAMS.RESOURCE_CENTER.ACTIONS_ON_THE_RESOURCE' | translate\"\n [disabled]=\"_offline.isOffline()\"\n (click)=\"actionsOnResource(r)\"\n >\n <ion-icon name=\"ellipsis-vertical\" slot=\"icon-only\" />\n </ion-button>\n }\n <input\n [id]=\"r.resourceId.concat('_picker')\"\n type=\"file\"\n accept=\".jpg,.jpeg,.png,.pdf\"\n style=\"display: none\"\n (change)=\"updateResource(r, $event)\"\n />\n </ion-item>\n }\n <ion-infinite-scroll (ionInfinite)=\"search(searchbar?.value, $event.target)\">\n <ion-infinite-scroll-content />\n </ion-infinite-scroll>\n </ion-list>\n </ion-content>\n `\n})\nexport class IDEARCResourcesComponent implements OnInit {\n private _tc = inject(IDEATinCanService);\n private _modal = inject(ModalController);\n private _alert = inject(AlertController);\n private _actions = inject(IDEAActionSheetController);\n private _loading = inject(IDEALoadingService);\n private _message = inject(IDEAMessageService);\n private _translate = inject(IDEATranslationsService);\n private _API = inject(IDEAAWSAPIService);\n _offline = inject(IDEAOfflineService);\n\n /**\n * The id of the team from which we want to load the resources. Default: try to guess current team.\n */\n @Input() teamId: string;\n /**\n * The Resource Center's folder of which to show the resources.\n */\n @Input() folder: RCFolder;\n /**\n * Whether the user has permissions to manage the resource center.\n */\n @Input() admin: boolean;\n\n resources: RCResource[];\n filteredResources: RCResource[];\n currentPage: number;\n\n @ViewChild('searchbar') searchbar: IonSearchbar;\n\n uploadErrors: string[];\n\n ngOnInit(): void {\n // if the team isn't specified, try to guess it in the usual IDEA's paths\n this.teamId = this.teamId || this._tc.get('membership').teamId || this._tc.get('teamId');\n this.loadResources();\n }\n\n async loadResources(getFromNetwork?: boolean): Promise<void> {\n try {\n const useCache = getFromNetwork ? CacheModes.NETWORK_FIRST : CacheModes.CACHE_FIRST;\n const resources: RCResource[] = await this._API.getResource(\n `teams/${this.teamId}/folders/${this.folder.folderId}/resources`,\n { useCache }\n );\n this.resources = resources.map(r => new RCResource(r));\n this.search(this.searchbar ? this.searchbar.value : null);\n } catch (error) {\n this._message.error('IDEA_TEAMS.RESOURCE_CENTER.COULDNT_LOAD_LIST');\n }\n }\n\n search(toSearch?: string, scrollToNextPage?: IonInfiniteScroll): void {\n toSearch = toSearch ? toSearch.toLowerCase() : '';\n\n this.filteredResources = (this.resources || [])\n .filter(m =>\n toSearch\n .split(' ')\n .every(searchTerm => [m.name, m.format].filter(f => f).some(f => f.toLowerCase().includes(searchTerm)))\n )\n .sort((a, b): number => a.name.localeCompare(b.name));\n\n if (scrollToNextPage) this.currentPage++;\n else this.currentPage = 0;\n this.filteredResources = this.filteredResources.slice(0, (this.currentPage + 1) * MAX_PAGE_SIZE);\n\n if (scrollToNextPage) setTimeout((): Promise<void> => scrollToNextPage.complete(), 100);\n }\n doRefresh(refresher?: IonRefresher): void {\n this.filteredResources = null;\n setTimeout((): void => {\n this.loadResources(Boolean(refresher));\n if (refresher) refresher.complete();\n }, 500); // the timeout is needed\n }\n\n async openResource(resource: RCResource): Promise<void> {\n try {\n await this._loading.show();\n const request = `teams/${this.teamId}/folders/${this.folder.folderId}/resources`;\n const body = { action: 'GET_DOWNLOAD_URL' };\n const { url } = await this._API.patchResource(request, { resourceId: resource.resourceId, body });\n Browser.open({ url });\n } catch (error) {\n this._message.error('COMMON.OPERATION_FAILED');\n } finally {\n this._loading.hide();\n }\n }\n\n getFormatIcon(format: RCResourceFormats): string {\n switch (format) {\n case RCResourceFormats.JPG:\n case RCResourceFormats.JPEG:\n case RCResourceFormats.PNG:\n return 'image';\n case RCResourceFormats.PDF:\n return 'document';\n default:\n return 'help';\n }\n }\n\n async actionsOnResource(res: RCResource): Promise<void> {\n if (!this.admin) return;\n const header = this._translate._('IDEA_TEAMS.RESOURCE_CENTER.ACTIONS_ON_RESOURCE');\n const buttons = [];\n buttons.push({\n text: this._translate._('IDEA_TEAMS.RESOURCE_CENTER.UPLOAD_NEW_VERSION'),\n icon: 'cloud-upload',\n handler: (): void => this.browseUpdateResource(res)\n });\n buttons.push({\n text: this._translate._('IDEA_TEAMS.RESOURCE_CENTER.RENAME'),\n icon: 'text',\n handler: (): Promise<void> => this.renameResource(res)\n });\n buttons.push({\n text: this._translate._('IDEA_TEAMS.RESOURCE_CENTER.DELETE'),\n role: 'destructive',\n icon: 'trash',\n handler: (): Promise<void> => this.deleteResource(res)\n });\n buttons.push({ text: this._translate._('COMMON.CANCEL'), role: 'cancel', icon: 'arrow-undo' });\n const actions = await this._actions.create({ header, buttons });\n actions.present();\n }\n browseUpdateResource(res: RCResource): void {\n if (!this.admin) return;\n document.getElementById(res.resourceId.concat('_picker')).click();\n }\n async updateResource(res: RCResource, ev: any): Promise<void> {\n this.uploadErrors = new Array<string>();\n // identify the file to upload (consider only the first file selected)\n const fileList: FileList = ev.target ? ev.target.files : {};\n const file = fileList.item(0);\n // upload the file\n await this._loading.show();\n await this.uploadFile(file);\n this._loading.hide();\n if (this.uploadErrors.length) this._message.error('IDEA_TEAMS.RESOURCE_CENTER.ONE_OR_MORE_FILE_UPLOAD_FAILED');\n else this._message.success('IDEA_TEAMS.RESOURCE_CENTER.UPLOAD_COMPLETED');\n }\n async renameResource(res: RCResource): Promise<void> {\n const doRename = async ({ name }: any): Promise<void> => {\n if (!name) return;\n if (this.resources.some(x => x.resourceId !== res.resourceId && x.name === name))\n return this._message.error('IDEA_TEAMS.RESOURCE_CENTER.RESOURCE_WITH_SAME_NAME_ALREADY_EXISTS');\n res.name = name;\n try {\n await this._loading.show();\n const path = `teams/${this.teamId}/folders/${this.folder.folderId}/resources`;\n await this._API.putResource(path, { resourceId: res.resourceId, body: res });\n // full-refresh to be sure we update the cache\n this.loadResources(true);\n } catch (err) {\n if ((err as any).message === 'RESOURCE_WITH_SAME_NAME_ALREADY_EXISTS')\n this._message.error('IDEA_TEAMS.RESOURCE_CENTER.RESOURCE_WITH_SAME_NAME_ALREADY_EXISTS');\n else this._message.error('COMMON.OPERATION_FAILED');\n } finally {\n this._loading.hide();\n }\n };\n\n const header = this._translate._('IDEA_TEAMS.RESOURCE_CENTER.RENAME_RESOURCE');\n const subHeader = this._translate._('IDEA_TEAMS.RESOURCE_CENTER.SELECT_RESOURCE_NAME');\n const message = this._translate._('IDEA_TEAMS.RESOURCE_CENTER.NAME_MUST_BE_UNIQUE_IN_FOLDER');\n const inputs: any[] = [\n { name: 'name', placeholder: this._translate._('IDEA_TEAMS.RESOURCE_CENTER.NAME'), value: res.name }\n ];\n const buttons = [\n { text: this._translate._('COMMON.CANCEL'), role: 'cancel' },\n { text: this._translate._('COMMON.CONFIRM'), handler: doRename }\n ];\n const alert = await this._alert.create({ header, subHeader, message, inputs, buttons });\n alert.present();\n }\n async deleteResource(res: RCResource): Promise<void> {\n const doDelete = async (): Promise<void> => {\n try {\n await this._loading.show();\n const path = `teams/${this.teamId}/folders/${this.folder.folderId}/resources`;\n await this._API.deleteResource(path, { resourceId: res.resourceId });\n // full-refresh to be sure we update the cache\n this.loadResources(true);\n } catch (error) {\n this._message.error('COMMON.OPERATION_FAILED');\n } finally {\n this._loading.hide();\n }\n };\n\n const header = this._translate._('COMMON.ARE_YOU_SURE');\n const subHeader = this._translate._('COMMON.OPERATION_IRREVERSIBLE');\n const buttons = [\n { text: this._translate._('COMMON.CANCEL'), role: 'cancel' },\n { text: this._translate._('COMMON.DELETE'), handler: doDelete }\n ];\n const alert = await this._alert.create({ header, subHeader, buttons });\n alert.present();\n }\n\n browseUploadNewResource(): void {\n if (!this.admin) return;\n // browse the local file(s)\n document.getElementById('newResourcePicker').click();\n }\n async uploadNewResources(ev: any): Promise<void> {\n this.uploadErrors = new Array<string>();\n // gather the files to upload\n const fileList: FileList = ev.target ? ev.target.files : {};\n const files = new Array<File>();\n for (let i = 0; i < fileList.length; i++) files.push(fileList.item(i));\n // upload each file and show the results\n await this._loading.show();\n files.forEach(async file => await this.uploadFile(file));\n this._loading.hide();\n if (this.uploadErrors.length) this._message.error('IDEA_TEAMS.RESOURCE_CENTER.ONE_OR_MORE_FILE_UPLOAD_FAILED');\n else this._message.success('IDEA_TEAMS.RESOURCE_CENTER.UPLOAD_COMPLETED');\n // reload the resources (force update cache)\n this.loadResources(true);\n }\n\n async uploadFile(file: File, existingRes?: RCResource): Promise<void> {\n const fullName = file.name.split('.');\n const format = fullName.pop();\n const name = fullName.join('.');\n let resource: RCResource;\n if (existingRes) {\n existingRes.format = format as RCResourceFormats;\n resource = existingRes;\n } else resource = new RCResource({ name, format });\n\n if (!loopStringEnumValues(RCResourceFormats).some(x => x === format)) {\n this.uploadErrors.push(this._translate._('IDEA_TEAMS.RESOURCE_CENTER.INVALID_FORMAT_FILE_', { name }));\n return;\n }\n\n const sizeMB = Number((file.size / 1024 / 1024).toFixed(4));\n if (sizeMB > FILE_SIZE_LIMIT_MB) {\n this.uploadErrors.push(this._translate._('IDEA_TEAMS.RESOURCE_CENTER.INVALID_SIZE_FILE_', { name }));\n return;\n }\n\n try {\n const path = `teams/${this.teamId}/folders/${this.folder.folderId}/resources`;\n let req: Promise<RCResource>;\n if (existingRes) req = this._API.putResource(path, { resourceId: resource.resourceId, body: resource });\n else req = this._API.postResource(path, { body: resource });\n const newRes: RCResource = await req;\n try {\n const { url } = await this._API.patchResource(path, {\n resourceId: newRes.resourceId,\n body: { action: 'GET_UPLOAD_URL' }\n });\n await this._API.rawRequest().put(url, file).toPromise();\n } catch (error) {\n this.uploadErrors.push(this._translate._('IDEA_TEAMS.RESOURCE_CENTER.UPLOAD_ERROR_FILE', { name }));\n }\n } catch (error) {\n this.uploadErrors.push(this._translate._('IDEA_TEAMS.RESOURCE_CENTER.ERROR_CREATING_RESOURCE_FILE', { name }));\n }\n }\n\n close(): void {\n this._modal.dismiss();\n }\n}\n","import { CommonModule } from '@angular/common';\nimport { ViewChild, Component, Input, OnInit, inject } from '@angular/core';\nimport {\n IonInfiniteScroll,\n AlertController,\n ModalController,\n IonRefresher,\n IonSearchbar,\n IonHeader,\n IonToolbar,\n IonButtons,\n IonButton,\n IonIcon,\n IonContent,\n IonRefresherContent,\n IonList,\n IonItem,\n IonLabel,\n IonInfiniteScrollContent,\n IonSkeletonText\n} from '@ionic/angular/standalone';\nimport { RCFolder } from 'idea-toolbox';\nimport { IDEALoadingService, IDEAMessageService, IDEATranslatePipe, IDEATranslationsService } from '@idea-ionic/common';\nimport { CacheModes, IDEAAWSAPIService, IDEAOfflineService, IDEATinCanService } from '@idea-ionic/uncommon';\n\nimport { IDEARCResourcesComponent } from './RCResources.component';\n\nconst MAX_PAGE_SIZE = 24;\n\n@Component({\n selector: 'idea-rc-folders',\n standalone: true,\n imports: [\n CommonModule,\n IDEATranslatePipe,\n IonSkeletonText,\n IonInfiniteScroll,\n IonInfiniteScrollContent,\n IonLabel,\n IonItem,\n IonList,\n IonRefresher,\n IonRefresherContent,\n IonContent,\n IonIcon,\n IonButton,\n IonButtons,\n IonToolbar,\n IonHeader,\n IonSearchbar\n ],\n template: `\n <ion-header>\n <ion-toolbar color=\"ideaToolbar\">\n <ion-buttons slot=\"start\">\n <ion-button [title]=\"'COMMON.CLOSE' | translate\" (click)=\"close()\">\n <ion-icon name=\"arrow-back\" slot=\"icon-only\" />\n </ion-button>\n </ion-buttons>\n <ion-searchbar\n #searchbar\n [placeholder]=\"'IDEA_TEAMS.RESOURCE_CENTER.SEARCH_FOR_FOLDERS' | translate\"\n (ionInput)=\"search($event.target ? $event.target.value : '')\"\n />\n <ion-buttons slot=\"end\">\n @if (admin) {\n <ion-button\n [disabled]=\"_offline.isOffline()\"\n [title]=\"'IDEA_TEAMS.RESOURCE_CENTER.CREATE_NEW_FOLDER' | translate\"\n (click)=\"newFolder()\"\n >\n <ion-icon slot=\"icon-only\" name=\"add\" />\n </ion-button>\n }\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content>\n <ion-refresher slot=\"fixed\" (ionRefresh)=\"doRefresh($event.target)\">\n <ion-refresher-content />\n </ion-refresher>\n <ion-list class=\"aList\">\n @if (!filteredFolders) {\n <ion-item>\n <ion-label>\n <ion-skeleton-text animated style=\"width: 50%\" />\n </ion-label>\n </ion-item>\n }\n @if (filteredFolders && !filteredFolders.length) {\n <ion-item class=\"noElements\">\n <ion-label>{{ 'COMMON.NO_ELEMENT_FOUND' | translate }}</ion-label>\n </ion-item>\n }\n @for (folder of filteredFolders; track folder) {\n <ion-item button (click)=\"openFolder(folder)\">\n <ion-label>{{ folder.name }}</ion-label>\n <ion-button\n color=\"medium\"\n fill=\"clear\"\n slot=\"end\"\n [title]=\"'IDEA_TEAMS.RESOURCE_CENTER.RENAME' | translate\"\n [disabled]=\"_offline.isOffline()\"\n (click)=\"renameFolder(folder, $event)\"\n >\n <ion-icon name=\"pencil\" slot=\"icon-only\" />\n </ion-button>\n <ion-button\n color=\"danger\"\n fill=\"clear\"\n slot=\"end\"\n [title]=\"'IDEA_TEAMS.RESOURCE_CENTER.DELETE' | translate\"\n [disabled]=\"_offline.isOffline()\"\n (click)=\"deleteFolder(folder, $event)\"\n >\n <ion-icon name=\"trash\" slot=\"icon-only\" />\n </ion-button>\n </ion-item>\n }\n </ion-list>\n <ion-infinite-scroll (ionInfinite)=\"search(searchbar?.value, $event.target)\">\n <ion-infinite-scroll-content />\n </ion-infinite-scroll>\n </ion-content>\n `\n})\nexport class IDEARCFoldersComponent implements OnInit {\n private _tc = inject(IDEATinCanService);\n private _modal = inject(ModalController);\n private _alert = inject(AlertController);\n private _loading = inject(IDEALoadingService);\n private _message = inject(IDEAMessageService);\n private _translate = inject(IDEATranslationsService);\n private _API = inject(IDEAAWSAPIService);\n _offline = inject(IDEAOfflineService);\n\n /**\n * The id of the team from which we want to load the resources. Default: try to guess current team.\n */\n @Input() teamId: string;\n /**\n * Whether the user has permissions to manage the resource center.\n */\n @Input() admin: boolean;\n\n folders: RCFolder[];\n filteredFolders: RCFolder[];\n currentPage: number;\n\n @ViewChild('searchbar') searchbar: IonSearchbar;\n\n ngOnInit(): void {\n // if the team isn't specified, try to guess it in the usual IDEA's paths\n this.teamId = this.teamId || this._tc.get('membership').teamId || this._tc.get('teamId');\n this.loadFolders();\n }\n async loadFolders(getFromNetwork?: boolean): Promise<void> {\n try {\n const folders: RCFolder[] = await this._API.getResource(`teams/${this.teamId}/folders`, {\n useCache: getFromNetwork ? CacheModes.NETWORK_FIRST : CacheModes.CACHE_FIRST\n });\n this.folders = folders.map(f => new RCFolder(f));\n this.search(this.searchbar ? this.searchbar.value : null);\n } catch (error) {\n this._message.error('IDEA_TEAMS.RESOURCE_CENTER.COULDNT_LOAD_LIST');\n }\n }\n\n search(toSearch?: string, scrollToNextPage?: IonInfiniteScroll): void {\n toSearch = toSearch ? toSearch.toLowerCase() : '';\n\n this.filteredFolders = (this.folders || [])\n .filter(m =>\n toSearch.split(' ').every(searchTerm => [m.name].filter(f => f).some(f => f.toLowerCase().includes(searchTerm)))\n )\n .sort((a, b): number => a.name.localeCompare(b.name));\n\n if (scrollToNextPage) this.currentPage++;\n else this.currentPage = 0;\n this.filteredFolders = this.filteredFolders.slice(0, (this.currentPage + 1) * MAX_PAGE_SIZE);\n\n if (scrollToNextPage) setTimeout((): Promise<void> => scrollToNextPage.complete(), 100);\n }\n doRefresh(refresher?: IonRefresher): void {\n this.filteredFolders = null;\n setTimeout((): void => {\n this.loadFolders(Boolean(refresher));\n if (refresher) refresher.complete();\n }, 500); // the timeout is needed\n }\n\n openFolder(folder: RCFolder): void {\n this._modal\n .create({ component: IDEARCResourcesComponent, componentProps: { folder, admin: this.admin } })\n .then(modal => modal.present());\n }\n async newFolder(): Promise<void> {\n if (!this.admin) return;\n\n const doCreate = async ({ name }: any): Promise<void> => {\n if (!name) return;\n if (this.folders.some(x => x.name === name))\n return this._message.error('IDEA_TEAMS.RESOURCE_CENTER.FOLDER_WITH_SAME_NAME_ALREADY_EXISTS');\n try {\n await this._loading.show();\n await this._API.postResource(`teams/${this.teamId}/folders`, { body: { name: name } });\n // full-refresh to be sure we update the cache\n this.loadFolders(true);\n } catch (err: any) {\n if (err.message === 'FOLDER_WITH_SAME_NAME_ALREADY_EXISTS')\n this._message.error('IDEA_TEAMS.RESOURCE_CENTER.FOLDER_WITH_SAME_NAME_ALREADY_EXISTS');\n else this._message.error('COMMON.OPERATION_FAILED');\n } finally {\n this._loading.hide();\n }\n };\n\n const header = this._translate._('IDEA_TEAMS.RESOURCE_CENTER.CREATE_NEW_FOLDER');\n const subHeader = this._translate._('IDEA_TEAMS.RESOURCE_CENTER.SELECT_FOLDER_NAME');\n const message = this._translate._('IDEA_TEAMS.RESOURCE_CENTER.NAME_MUST_BE_UNIQUE_IN_RC');\n const inputs: any[] = [{ name: 'name', placeholder: this._translate._('IDEA_TEAMS.RESOURCE_CENTER.NAME') }];\n const buttons = [\n { text: this._translate._('COMMON.CANCEL'), role: 'cancel' },\n { text: this._translate._('COMMON.CONFIRM'), handler: doCreate }\n ];\n const alert = await this._alert.create({ header, subHeader, message, inputs, buttons });\n alert.present();\n }\n\n async renameFolder(folder: RCFolder, event?: any): Promise<void> {\n if (event) event.stopPropagation();\n if (!this.admin) return;\n\n const doRemove = async ({ name }: any): Promise<void> => {\n if (!name) return;\n if (this.folders.some(x => x.folderId !== folder.folderId && x.name === name))\n return this._message.error('IDEA_TEAMS.RESOURCE_CENTER.FOLDER_WITH_SAME_NAME_ALREADY_EXISTS');\n folder.name = name;\n try {\n await this._loading.show();\n await this._API.putResource(`teams/${this.teamId}/folders`, { resourceId: folder.folderId, body: folder });\n // full-refresh to be sure we update the cache\n this.loadFolders(true);\n } catch (err: any) {\n if (err.message === 'FOLDER_WITH_SAME_NAME_ALREADY_EXISTS')\n this._message.error('IDEA_TEAMS.RESOURCE_CENTER.FOLDER_WITH_SAME_NAME_ALREADY_EXISTS');\n else this._message.error('COMMON.OPERATION_FAILED');\n } finally {\n this._loading.hide();\n }\n };\n\n const header = this._translate._('IDEA_TEAMS.RESOURCE_CENTER.RENAME_FOLDER');\n const subHeader = this._translate._('IDEA_TEAMS.RESOURCE_CENTER.SELECT_FOLDER_NAME');\n const message = this._translate._('IDEA_TEAMS.RESOURCE_CENTER.NAME_MUST_BE_UNIQUE_IN_RC');\n const inputs: any[] = [\n { name: 'name', placeholder: this._translate._('IDEA_TEAMS.RESOURCE_CENTER.NAME'), value: folder.name }\n ];\n const buttons = [\n { text: this._translate._('COMMON.CANCEL'), role: 'cancel' },\n { text: this._translate._('COMMON.CONFIRM'), handler: doRemove }\n ];\n const alert = await this._alert.create({ header, subHeader, message, inputs, buttons });\n alert.present();\n }\n async deleteFolder(folder: RCFolder, event?: any): Promise<void> {\n if (event) event.stopPropagation();\n if (!this.admin) return;\n\n const doDelete = async ():