diff --git a/Frontend Angular 4/src/app/layout/grupos/grupos.component.html b/Frontend Angular 4/src/app/layout/grupos/grupos.component.html index ceb2c6c2b22382839648bdf0e892e23be96ff47a..eb4a69e437facc755d9b6405541822345d7e78cc 100755 --- a/Frontend Angular 4/src/app/layout/grupos/grupos.component.html +++ b/Frontend Angular 4/src/app/layout/grupos/grupos.component.html @@ -13,8 +13,20 @@ <div class="row" style="margin-top: 20px"> <div class="col-lg-5"> <div class="card" *ngIf="grupoSeleccionado == undefined"> - <div class="card-header"> + <div class="card-header flex justify-between"> <div *ngIf="grupoSeleccionado == undefined">Grupos</div> + <button + ngbPopover="{{ 'i18n.action.new' | translate | titleCase }}" + triggers="mouseenter:mouseleave" + placement="bottom" + type="button" + class="btn btn-sm btn-secondary" + aria-haspopup="true" + aria-expanded="false" + (click)="openCreateGroupModal(false)" + > + <i aria-hidden="true" class="fa fa-plus"></i> + </button> </div> <div class="card-block" *ngIf="grupoSeleccionado == undefined"> <div @@ -34,7 +46,7 @@ > <i class="fa fa-users matefun-fa-user" aria-hidden="true"></i> <p> - {{ grupo.grado + "°" + grupo.grupo + " - " + grupo.anio }} + {{ grupo.name }} </p> </div> </div> @@ -48,7 +60,7 @@ [destroyOnHide]="false" > <li [ngbNavItem]="1"> - <a ngbNavLink>Alumnos</a> + <a ngbNavLink>Users</a> <ng-template ngbNavContent> <div class="card"> <div> @@ -64,17 +76,36 @@ > <i class="fa fa-arrow-up"></i> </button> + <button + *ngIf="roleCanManageUsers()" + class="btn btn-sm btn-secondary pull-right" + style="cursor: pointer; margin-top: -35px; margin-right: 36px" + (click)="openManageGroupUsersModal()" + ngbPopover="{{ + 'i18n.msg.group.manageGroupUsers' | translate | titleCase + }}" + placement="bottom" + triggers="mouseenter:mouseleave" + > + <i class="fa fa-users"></i> + </button> + <button + class="btn btn-sm btn-secondary pull-right" + style="cursor: pointer; margin-top: -35px; margin-right: 71px" + (click)="openDestroyGroupModal()" + ngbPopover="{{ + 'i18n.msg.group.destroyGroupTooltip' | translate | titleCase + }}" + placement="bottom" + triggers="mouseenter:mouseleave" + > + <i class="fa fa-close"></i> + </button> <p class="pull-right" - style="margin-top: -34px; margin-right: 60px" + style="margin-top: -34px; margin-right: 128px" > - {{ - grupoSeleccionado.grado + - "°" + - grupoSeleccionado.grupo + - " - " + - grupoSeleccionado.anio - }} + {{ grupoSeleccionado.name }} </p> </div> <div class="card-block"> @@ -83,8 +114,8 @@ style="min-height: 100px; overflow-y: scroll" > <div - *ngFor="let alumno of grupoSeleccionado.alumnos" - (click)="seleccionarAlumno(alumno)" + *ngFor="let user of grupoSeleccionado.users" + (click)="seleccionarUser(user)" class="col-sm-3 matefun-group-wrapper" > <i @@ -92,7 +123,7 @@ aria-hidden="true" ></i> <p> - {{ alumno.apellido + ", " + alumno.nombre }} + {{ user.username }} </p> </div> </div> @@ -104,11 +135,11 @@ <a ngbNavLink>Archivos</a> <ng-template ngbNavContent> <div class="card"> - <div> + <div class="relative pull-right right-0"> <button class="btn btn-sm btn-secondary pull-right" style="cursor: pointer; margin-top: -35px; margin-right: 1px" - (click)="desseleccionarGrupo()" + (click)="navBack()" ngbPopover="{{ 'i18n.action.goBack' | translate | titleCase }}" @@ -117,17 +148,33 @@ > <i class="fa fa-arrow-up"></i> </button> + <button + ngbPopover="{{ 'i18n.action.new' | translate | titleCase }}" + triggers="mouseenter:mouseleave" + placement="bottom" + type="button" + data-toggle="dropdown" + class="btn btn-sm btn-secondary pull-right" + style="cursor: pointer; margin-top: -35px; margin-right: 36px" + aria-haspopup="true" + aria-expanded="false" + > + <i aria-hidden="true" class="fa fa-plus"></i> + </button> + <div class="dropdown-menu right-0" style="left: unset"> + <a class="dropdown-item" (click)="mkFile(true)"> + {{ "i18n.object.file" | translate | titleCase }} + </a> + <div role="separator" class="dropdown-divider"></div> + <a class="dropdown-item" (click)="mkFile(false)"> + {{ "i18n.object.folder" | translate | titleCase }} + </a> + </div> <p class="pull-right" - style="margin-top: -34px; margin-right: 60px" + style="margin-top: -34px; margin-right: 95px" > - {{ - grupoSeleccionado.grado + - "°" + - grupoSeleccionado.grupo + - " - " + - grupoSeleccionado.anio - }} + {{ grupoSeleccionado.name }} </p> </div> <div class="card-block"> @@ -136,12 +183,19 @@ style="min-height: 100px; overflow-y: scroll" > <div - *ngFor="let archivo of grupoSeleccionado.archivos" + *ngFor="let archivo of directorioActual.archivos" (click)="seleccionarArchivo(archivo)" class="col-sm-3 col-4 matefun-group-wrapper" > <i + *ngIf="archivo.directorio" + class="fa fa-folder matefun-fa-folder" + aria-hidden="true" + ></i> + <i + *ngIf="!archivo.directorio" class="fa fa-file-text matefun-fa-file" + [class.text-blue-400]="archivo.feedbackRequested" aria-hidden="true" ></i> <p>{{ archivo.nombre }}</p> @@ -156,14 +210,14 @@ <div [ngbNavOutlet]="nav" class="mt-2" *ngIf="grupoSeleccionado"></div> </div> <div class="col-lg-7"> - <div class="card" *ngIf="alumnoSeleccionado"> + <div class="card" *ngIf="selectedUser && !archivoSeleccionado"> <div class="card-block"> <div class="row listadoEntregasAlumnoGrupos" style="min-height: 100px; overflow-y: scroll" > <div - *ngFor="let entrega of alumnoSeleccionado.archivos" + *ngFor="let entrega of selectedUserDirectorioActual" (click)="seleccionarEntrega(entrega)" class="col-sm-3 col-4 matefun-file-wrapper" > @@ -175,7 +229,7 @@ <p>{{ entrega.nombre }}</p> </div> <div - *ngIf="alumnoSeleccionado.archivos.length == 0" + *ngIf="selectedUser && selectedUserDirectorioActual.length == 0" style="width: 100%; text-align: center" > <i @@ -187,10 +241,8 @@ class="fa fa-file-text" ></i> <p> - No hay entregas del alumno: - {{ - alumnoSeleccionado.nombre + " " + alumnoSeleccionado.apellido - }} + No hay entregas del usuario: + {{ selectedUser.username }} </p> </div> </div> @@ -198,9 +250,7 @@ </div> <div class="card" - *ngIf=" - alumnoSeleccionado == undefined && archivoSeleccionado == undefined - " + *ngIf="selectedUser == undefined && archivoSeleccionado == undefined" > <div class="card-block"> <div @@ -231,14 +281,71 @@ Calificar </button> <button - *ngIf="esArchivoGrupo()" - ngbPopover="Cargar/Editar" + ngbPopover="{{ 'i18n.action.load' | translate | titleCase }}/{{ + 'i18n.action.edit' | translate | titleCase + }}" placement="bottom" triggers="mouseenter:mouseleave" class="btn btn-sm btn-secondary pull-left mr-2" - (click)="cargarArchivoCompartido()" + (click)="cargarArchivo()" > - <i class="fa fa-pencil"></i> + <i aria-hidden="true" class="fa fa-pencil"></i> + </button> + <button + ngbPopover="{{ 'i18n.action.delete' | translate | titleCase }}" + placement="bottom" + triggers="mouseenter:mouseleave" + class="btn btn-sm btn-secondary pull-left mr-2" + (click)="mostrarEliminarDialogo()" + > + <i aria-hidden="true" class="fa fa-remove"></i> + </button> + <button + ngbPopover="{{ 'i18n.action.move' | translate | titleCase }} {{ + 'i18n.object.file' | translate | titleCase + }}" + placement="bottom" + triggers="mouseenter:mouseleave" + class="btn btn-sm btn-secondary pull-left mr-2" + (click)="seleccionarDirectorioAMover()" + > + <i aria-hidden="true" class="fa fa-cut"></i> + </button> + <button + *ngIf="canRequestFeedback()" + ngbPopover="{{ + 'i18n.action.requestFeedback' | translate | titleCase + }} {{ 'i18n.object.file' | translate | titleCase }}" + placement="bottom" + triggers="mouseenter:mouseleave" + class="btn btn-sm btn-secondary pull-left mr-2" + (click)="requestFeedback()" + > + <i aria-hidden="true" class="fa fa-exchange"></i> + </button> + <button + *ngIf="canAssignFile()" + ngbPopover="{{ + 'i18n.action.assignFile' | translate | titleCase + }} {{ archivoSeleccionado.nombre }}" + placement="bottom" + triggers="mouseenter:mouseleave" + class="btn btn-sm btn-secondary pull-left mr-2" + (click)="openAssignFileModal()" + > + <i aria-hidden="true" class="fa fa-share"></i> + </button> + <button + *ngIf="canReturnFeedback()" + ngbPopover="{{ + 'i18n.action.returnFeedback' | translate | titleCase + }} {{ 'i18n.object.file' | translate | titleCase }}" + placement="bottom" + triggers="mouseenter:mouseleave" + class="btn btn-sm btn-secondary pull-left mr-2" + (click)="returnFeedback()" + > + <i aria-hidden="true" class="fa fa-exchange"></i> </button> <div class="pull-left"> Nombre: {{ archivoSeleccionado?.nombre }} - Creado: @@ -274,3 +381,64 @@ *ngIf="modalQualifyDelivery" > </matefun-modal-calificar-entrega> + +<matefun-modal-nuevo-archivo + confirmLabel="{{ 'i18n.action.create' | translate | titleCase }}" + fileDescriptionLabel="{{ 'i18n.object.descr' | translate | titleCase }}:" + fileNameLabel="{{ 'i18n.object.name' | translate | titleCase }}:" + header="{{ 'i18n.action.new' | translate | titleCase }} + {{ + (modalTypeIsFile ? 'i18n.object.file' : 'i18n.object.folder') + | translate + | titleCase + }} " + [opened]="modalCreateFileOpened" + [typeOfFile]="modalTypeIsFile ? 'file' : 'directory'" + (close)="modalCreateFile = false" + (confirmFileCreation)=" + modalTypeIsFile + ? confirmDocumentCreation($event) + : confirmFileCreation($event) + " + *ngIf="modalCreateFile" +> +</matefun-modal-nuevo-archivo> + +<matefun-modal-seleccionar-directorio + confirmLabel="{{ 'i18n.action.move' | translate | titleCase }} {{ + 'i18n.object.here' | translate + }}" + [currentDirectory]="currentDirOfFileToMove" + [fileIdToMove]="archivoSeleccionado ? archivoSeleccionado.id : -1" + fileNameLabel="{{ 'i18n.object.name' | translate | titleCase }}" + header="{{ 'i18n.msg.file.move' | translate }}" + [initialPath]="currentPath" + navigateBackLabel="{{ 'i18n.action.goBack' | translate | titleCase }}" + [opened]="modalMoveFileOpened" + type-of-modal="move" + (close)="modalMoveFile = false" + (confirmFileCreation)="confirmFileMove($event)" + (navBack)="navigateBackModal($event)" + (navTo)="currentDirOfFileToMove = $event.detail" + *ngIf="modalMoveFile" +> +</matefun-modal-seleccionar-directorio> + +<matefun-modal-borrar-archivo + bodyDescription="{{ 'i18n.msg.file.delete' | translate : fileNameToRemove }}" + cancelLabel="{{ 'i18n.action.cancel' | translate | titleCase }}" + confirmLabel="{{ 'i18n.action.delete' | translate | titleCase }}" + header="{{ 'i18n.action.delete' | translate | titleCase }} + {{ + (modalTypeIsFile ? 'i18n.object.file' : 'i18n.object.folder') + | translate + | titleCase + }}" + [opened]="modalRemoveFileOpened" + [typeOfFile]="modalTypeIsFile ? 'file' : 'directory'" + (close)="modalRemoveFile = false" + (cancelAction)="modalRemoveFileOpened = false" + (removeFile)="confirmFileDeletion()" + *ngIf="modalRemoveFile" +> +</matefun-modal-borrar-archivo> diff --git a/Frontend Angular 4/src/app/layout/grupos/grupos.component.ts b/Frontend Angular 4/src/app/layout/grupos/grupos.component.ts index a41d57e515742ec20ebd45668740d87c0558f17e..45e72b6aeb19ba2d79f0c920aa76f6529cc5ecd9 100755 --- a/Frontend Angular 4/src/app/layout/grupos/grupos.component.ts +++ b/Frontend Angular 4/src/app/layout/grupos/grupos.component.ts @@ -1,8 +1,12 @@ -import { Component } from "@angular/core"; +import { ChangeDetectorRef, Component } from "@angular/core"; import { Router } from "@angular/router"; -import { Archivo, Evaluacion } from "../../shared/objects/archivo-types"; -import { Grupo } from "../../shared/objects/grupo"; +import { + Archivo, + Evaluacion, + MDocument, +} from "../../shared/objects/archivo-types"; +import { Group } from "../../shared/objects/grupo"; import { Usuario } from "../../shared/objects/usuario"; import { AuthenticationService } from "../../shared/services/authentication.service"; import { HaskellService } from "../../shared/services/haskell.service"; @@ -10,6 +14,29 @@ import { SessionService } from "../../shared/services/session.service"; import { NgbPopoverConfig, NgbPopover } from "@ng-bootstrap/ng-bootstrap"; import { NotificacionService } from "../../shared/services/notificacion.service"; import { TranslateService } from "@ngx-translate/core"; +import { GroupService } from "../../shared/services/group.service"; +import { GroupFileService } from "../../shared/services/group-file.service"; +import { FileService } from "../../shared/services/file.service"; +import { DocumentService } from "../../shared/services/document.service"; +import { GroupDocumentService } from "../../shared/services/group-document.service"; +import { + roleCanManageGroupUsers, + roleCanDestroyGroup, + findInTree, +} from "app/utils"; +import { NgbModal } from "@ng-bootstrap/ng-bootstrap"; +import { CreateGroupModal } from "../../shared/components/create-group-modal/create-group-modal.component"; +import { ManageGroupUsersModal } from "app/shared/components/manage-group-users-modal/manage-group-users-modal.component"; +import { + DestroyGroupModal, + RequestFeedbackModal, + ReturnFeedbackModal, + AssignFileModal, +} from "app/shared"; + +const STARTS_WITH_CAPITAL_LETTER_REGEX = /^[A-Z]/; + +const ID_ROOT_DIR = -1; @Component({ selector: "grupos", @@ -17,12 +44,14 @@ import { TranslateService } from "@ngx-translate/core"; }) export class GruposComponent { archivos: Archivo[] = []; - grupos: Grupo[] = []; - grupoSeleccionado: Grupo = undefined; + selectedUserArchivos: Archivo[] = []; + grupos: Group[] = []; + grupoSeleccionado: Group = undefined; - alumnoSeleccionado: Usuario = undefined; + selectedUser: Usuario = undefined; archivoSeleccionado: Archivo = undefined; + selectedFile: Archivo = undefined; tipoArchivo: string = undefined; @@ -33,6 +62,12 @@ export class GruposComponent { configCodeMirror = JSON.parse(localStorage.getItem("codeMirrorConfig")); translateService: any; + selectedUserFileArray: any; + selectedUserDirectorioActual: any; + + createModalRef: any; + + preview: string = ""; // - - - - - - - - - - - - Modal show confirm - - - - - - - - - - - - /** * Con `true` se renderiza el modal de calificar entrega. @@ -60,13 +95,69 @@ export class GruposComponent { }, ]; + // - - - - - - - - - - - - - Modal create file - - - - - - - - - - - - - + /** + * Con `true` se renderiza el modal de crear un archivo + */ + modalCreateFile = false; + + /** + * Con `true` se configura el modal para que se muestre la interfaz de + * agregar/borrar archivo. De otro modo, se muestra la interfaz de + * agregar/borrar directorio + */ + modalTypeIsFile = true; + + /** + * Con `true` se indica que el modal -de crear un archivo- se quiere abrir. + * Útil para avisar al modal que anime el dismiss antes de que se elimine del + * DOM + */ + modalCreateFileOpened = true; + + /** + * Directorio actual sobre el cual será desplegada la lista de directorios + * del modal mover archivo. + */ + currentDirOfFileToMove: Archivo; + + /** + * Determina la ruta en donde se encuentra ubicado el archivo/directorio + * seleccionado actualmente. + */ + currentPath: string = "/"; + + // - - - - - - - - - - - - - Modal move file - - - - - - - - - - - - - + /** + * Con `true` se renderiza el modal de mover un archivo + */ + modalMoveFile = false; + + /** + * Con `true` se indica que el modal -de mover un archivo- se quiere abrir. + * Útil para avisar al modal que anime el dismiss antes de que se elimine del + * DOM + */ + modalMoveFileOpened = true; + + fileNameToRemove: { fileName: string } = { fileName: "" }; + modalRemoveFile = false; + modalRemoveFileOpened = true; + constructor( private router: Router, private authService: AuthenticationService, private haskellService: HaskellService, private notifService: NotificacionService, private sessionService: SessionService, - public translate: TranslateService + public translate: TranslateService, + public groupService: GroupService, + public groupFileService: GroupFileService, + public documentService: DocumentService, + public groupDocumentService: GroupDocumentService, + public fileService: FileService, + private modalService: NgbModal, + private changeDetectorRef: ChangeDetectorRef ) { this.translateService = translate; this.directorioActual = {}; @@ -91,6 +182,12 @@ export class GruposComponent { ngOnInit() { // let cedula = this.authService.getUser().cedula; // cedula + this.confirmGroupCreation = this.confirmGroupCreation.bind(this); + this.confirmGroupUpdated = this.confirmGroupUpdated.bind(this); + this.confirmGroupDestroyed = this.confirmGroupDestroyed.bind(this); + this.confirmFeedbackRequsted = this.confirmFeedbackRequsted.bind(this); + this.confirmFeedbackReturned = this.confirmFeedbackReturned.bind(this); + this.loading = true; // this.haskellService.getGrupos(cedula).subscribe( // (grupos) => { @@ -100,6 +197,19 @@ export class GruposComponent { // }, // (error) => console.log(error) // ); + this.loadGroups(); + } + + loadGroups(next?: () => void) { + this.groupService.getGroups().subscribe( + (grupos) => { + this.grupos = grupos.groups; + // this.ordenarGrupos(); + this.loading = false; + if (next) next(); + }, + (error) => console.log(error) + ); } showNotification(type: "error" | "success" | "warning", traslation: string) { @@ -116,9 +226,12 @@ export class GruposComponent { } ordenarArchivos() { - this.grupoSeleccionado.archivos = this.grupoSeleccionado.archivos.sort( - this.ordenarAlph - ); + // var tipo = this.sortFunction; + // if (tipo === "tipo") { + // this.ordenarMixto(); + // } else if (tipo === "fecha") { + // this.ordenarFechaCreacion(); + // } } //ordeno los archivos del alumno (los archivos entregados.) @@ -150,51 +263,481 @@ export class GruposComponent { } ordenarAlumnos() { - this.grupoSeleccionado.alumnos = this.grupoSeleccionado.alumnos.sort( - this.ordenarAlumnosF - ); + // this.grupoSeleccionado.alumnos = this.grupoSeleccionado.alumnos.sort( + // this.ordenarAlumnosF + // ); + } + + /** + * Renderiza en pantalla el modal de agregar archivos o directorios, + * dependiendo del valor de `modalTypeIsFile`. + * @param modalTypeIsFile Con `true` se indica que el modal a renderizar es el de agregar archivo. De otro modo, se renderiza el de agregar directorio. + */ + mkFile(modalTypeIsFile: boolean) { + this.modalTypeIsFile = modalTypeIsFile; + + // Mostrar el modal + this.modalCreateFile = true; + this.modalCreateFileOpened = true; } seleccionarGrupo(grupo) { this.grupoSeleccionado = grupo; this.ordenarAlumnos(); - this.ordenarArchivos(); - this.archivoSeleccionado = undefined; - this.alumnoSeleccionado = undefined; + this.loadFilesAndFolders(grupo.id); + this.selectedUser = undefined; + } + + loadFilesAndFolders(grupoId: number, directorioActualId = null) { + this.groupFileService.getGroupFiles(grupoId).subscribe( + (files) => { + this.tree = this.fileService.fileToArchivo(files["files"]); + + this.archivos = [this.tree]; + this.loading = false; + + if (directorioActualId) { + this.directorioActual = findInTree(this.archivos, directorioActualId); + } else { + this.directorioActual = this.tree; + } + this.ordenarArchivos(); + this.sessionService.setArchivosTree(this.tree); + + this.currentDirOfFileToMove = this.directorioActual; + }, + (error) => console.log(error) + ); } desseleccionarGrupo() { this.grupoSeleccionado = undefined; this.archivoSeleccionado = undefined; - this.alumnoSeleccionado = undefined; + this.selectedUser = undefined; } - seleccionarAlumno(alumno) { - if (!(typeof alumno === "undefined")) { - this.alumnoSeleccionado = alumno; - this.ordenarArchivosAlumno(); - this.archivoSeleccionado = undefined; + seleccionarUser(user) { + this.groupFileService + .getFeedbackRequestedGroupFiles(this.grupoSeleccionado.id, user.id) + .subscribe( + (files) => { + this.selectedUserFileArray = files["files"].map((file) => + this.fileService.fileToArchivo(file) + ); + this.selectedUserArchivos = [this.selectedUserFileArray]; + this.loading = false; + + this.selectedUserDirectorioActual = this.selectedUserFileArray; + this.ordenarArchivos(); + this.sessionService.setArchivosTree(this.selectedUserFileArray); + + this.selectedUser = user; + + this.archivoSeleccionado = undefined; + this.selectedFile = undefined; // Probando + }, + (error) => console.log(error) + ); + } + + canRequestFeedback() { + return ( + !!this.grupoSeleccionado.id && + !!this.selectedFile && + this.selectedFile.feedbackRequested !== undefined && + !this.selectedFile.feedbackRequested + ); + } + + requestFeedback() { + this.openRequestFeedbackModal(); + } + + canReturnFeedback() { + return ( + !!this.grupoSeleccionado && + !!this.selectedFile && + this.selectedFile.feedbackRequested !== undefined && + this.selectedFile.feedbackRequested && + this.groupService.userCanReturnFeedback(this.grupoSeleccionado.role) + ); + } + + returnFeedback() { + this.openReturnFeedbackModal(); + } + + openReturnFeedbackModal() { + if (!this.grupoSeleccionado) { + this.showNotification("warning", "i18n.warning.group.noSelected"); + return; + } + + if (!this.selectedFile.fileId) { + this.showNotification("warning", "i18n.warning.file.noSelected"); + return; + } + + this.createModalRef = this.modalService.open(ReturnFeedbackModal); + + this.createModalRef.componentInstance.confirmFeedbackReturned = + this.confirmFeedbackReturned; + this.createModalRef.componentInstance.groupName = null; + this.createModalRef.componentInstance.groupId = this.grupoSeleccionado.id; + this.createModalRef.componentInstance.fileName = this.selectedFile.nombre; + this.createModalRef.componentInstance.fileId = this.selectedFile.fileId; + this.createModalRef.componentInstance.userId = + this.selectedUser?.id || + JSON.parse(localStorage.getItem("currentUser"))["id"]; + } + + confirmFeedbackReturned() { + if (!!this.selectedUser) { + this.selectedFile = null; + this.archivoSeleccionado = null; + this.seleccionarUser(this.selectedUser); + } else { + this.loadFilesAndFolders( + this.grupoSeleccionado.id, + this.directorioActual.id + ); } } seleccionarArchivo(archivo) { + this.selectedFile = archivo; + if (archivo.directorio) { + this.directorioActual = archivo; + this.archivoSeleccionado = undefined; + this.selectedFile = undefined; // Probando + this.archivoSeleccionado = archivo; + this.currentPath += `${archivo.nombre}/`; + this.preview = ""; + } else { + this.selectedUser = undefined; + this.tipoArchivo = "compartido"; + this.groupDocumentService + .getGroupDocument( + this.grupoSeleccionado.id, + archivo.documentId, + this.selectedUser?.id + ) + .subscribe( + (data) => { + const document = data["document"]; + const archivoDocument = + this.documentService.documentToArchivo(document); + this.actualizarArchivoSeleccionado(archivoDocument); + }, + (error) => console.log(error) + ); + } + } + + /** + * Actualiza el estado del archivo seleccionado, así como la preview del + * contenido de dicho archivo. + * @param archivo Archivo a seleccionar + */ + actualizarArchivoSeleccionado(archivo: Archivo) { + if (!!this.selectedFile) { + archivo.feedbackRequested = this.selectedFile.feedbackRequested; + } this.archivoSeleccionado = archivo; - this.alumnoSeleccionado = undefined; - this.tipoArchivo = "compartido"; + this.selectedFile = archivo; + this.preview = !!archivo?.contenido ? archivo.contenido : ""; + } + + /** + * Navega hacia el directorio padre en la vista principal de directorios. + * @param shouldUpdateSelectedFile Determina si se debe actualizar el archivo seleccionado cuando se navega hacia atrás + */ + navBack(shouldUpdateSelectedFile = true) { + const { padreId } = this.directorioActual; + + if (padreId === ID_ROOT_DIR) { + return; + } + + // Actualiza el current path + const lastDirectoryIndex = this.currentPath.lastIndexOf( + `${this.directorioActual.nombre}` + ); + + this.currentPath = this.currentPath.substring(0, lastDirectoryIndex); + + const padre = findInTree(this.archivos, padreId); + + if (shouldUpdateSelectedFile) { + // Cuando se selecciona un directorio cuyo id es el root, significa que ese + // es el último archivo de la rama de directorios. En otras palabras, el + // root se identifica porque su padreId es ID_ROOT_DIR. + const archivoSeleccionado = + padre.padreId == ID_ROOT_DIR ? undefined : padre; + this.actualizarArchivoSeleccionado(archivoSeleccionado); + } + + // Actualiza la vista de directorios y archivos + this.directorioActual = padre; } seleccionarEntrega(entrega) { - this.archivoSeleccionado = entrega; - this.alumnoSeleccionado = undefined; + this.groupDocumentService + .getGroupDocument( + this.grupoSeleccionado.id, + entrega.documentId, + this.selectedUser.id + ) + .subscribe( + (data) => { + // this.archivoSeleccionado = entrega; + const document = data["document"]; + const archivoDocument = + this.documentService.documentToArchivo(document); + archivoDocument.feedbackRequested = true; + this.actualizarArchivoSeleccionado(archivoDocument); + }, + (error) => console.log(error) + ); + // this.selectedUser = undefined; this.tipoArchivo = "entrega"; } + roleCanManageUsers(): boolean { + return ( + !!this.grupoSeleccionado && + roleCanManageGroupUsers(this.grupoSeleccionado.role) + ); + } + + roleCanDestroyGroup(): boolean { + return ( + !!this.grupoSeleccionado && + roleCanDestroyGroup(this.grupoSeleccionado.role) + ); + } + + openRequestFeedbackModal() { + if (!this.grupoSeleccionado.id) { + this.showNotification("warning", "i18n.warning.group.noSelected"); + return; + } + + if (!this.selectedFile.id) { + this.showNotification("warning", "i18n.warning.file.noSelected"); + return; + } + + this.createModalRef = this.modalService.open(RequestFeedbackModal); + + this.createModalRef.componentInstance.groupName = + this.grupoSeleccionado.name; + this.createModalRef.componentInstance.groupId = this.grupoSeleccionado.id; + this.createModalRef.componentInstance.fileName = + this.archivoSeleccionado.nombre; + this.createModalRef.componentInstance.fileId = + this.archivoSeleccionado.fileId; + this.createModalRef.componentInstance.confirmFeedbackRequested = + this.confirmFeedbackRequsted; + } + + confirmFeedbackRequsted() { + this.archivoSeleccionado.feedbackRequested = true; + this.selectedFile.feedbackRequested = true; + const fileInDirectory = this.directorioActual.archivos.find( + (archivo) => archivo.documentId === this.archivoSeleccionado.id + ); + if (!!fileInDirectory) { + fileInDirectory.feedbackRequested = true; + } + this.changeDetectorRef.detectChanges(); + } + + mostrarEliminarDialogo() { + // Chequear si se seleccionó un archivo + if (!this.archivoSeleccionado) { + this.showNotification("warning", "i18n.warning.file.noSelected"); + return; + } + + this.fileNameToRemove = { + fileName: this.archivoSeleccionado?.nombre || this.selectedFile?.nombre, + }; + + // Determina el tipo de modal a renderizar + this.modalTypeIsFile = !this.archivoSeleccionado.directorio; + + // Mostrar el modal + this.modalRemoveFile = true; + this.modalRemoveFileOpened = true; + } + + confirmFileDeletion() { + let fileIdToDelete = null; + let navBack = false; + if (this.archivoSeleccionado?.fileId) { + fileIdToDelete = this.archivoSeleccionado.fileId; + } else if (this.selectedFile?.fileId) { + fileIdToDelete = this.selectedFile.fileId; + } else if (this.archivoSeleccionado?.id) { + fileIdToDelete = this.archivoSeleccionado.id; + navBack = true; + } + + this.groupFileService + .deleteGroupFile( + this.grupoSeleccionado.id, + this.archivoSeleccionado?.fileId || + this.selectedFile?.fileId || + this.archivoSeleccionado?.id + ) + .subscribe( + (_) => { + this.modalRemoveFileOpened = false; + if (navBack) { + this.navBack(); + } + this.loadFilesAndFolders( + this.grupoSeleccionado.id, + this.directorioActual.id + ); + }, + (error) => console.log(error) + ); + } + + openDestroyGroupModal() { + if (!this.grupoSeleccionado) { + this.showNotification("warning", "i18n.warning.group.noSelected"); + return; + } + + this.createModalRef = this.modalService.open(DestroyGroupModal); + + this.createModalRef.componentInstance.groupName = + this.grupoSeleccionado.name; + this.createModalRef.componentInstance.groupId = this.grupoSeleccionado.id; + this.createModalRef.componentInstance.confirmGroupDestroyed = + this.confirmGroupDestroyed; + this.createModalRef.componentInstance.userRole = + this.grupoSeleccionado.role; + + // this.translateService + // .get("i18n.warning.group.destroy", { + // group: this.grupoSeleccionado.name, + // }) + // .subscribe((res) => { + // if (confirm(res)) { + // this.groupService.destroyGroup(this.grupoSeleccionado.id).subscribe( + // (data) => { + // this.loadGroups(); + // this.grupoSeleccionado = undefined; + // }, + // (error) => { + // this.notifService.error(error); + // } + // ); + } + + openAssignFileModal() { + if (!this.grupoSeleccionado) { + this.showNotification("warning", "i18n.warning.group.noSelected"); + return; + } + + if (!this.selectedFile.fileId) { + this.showNotification("warning", "i18n.warning.file.noSelected"); + return; + } + + this.createModalRef = this.modalService.open(AssignFileModal); + + this.createModalRef.componentInstance.groupName = null; + this.createModalRef.componentInstance.groupId = this.grupoSeleccionado.id; + this.createModalRef.componentInstance.fileName = this.selectedFile.nombre; + this.createModalRef.componentInstance.fileId = this.selectedFile.fileId; + this.createModalRef.componentInstance.users = this.grupoSeleccionado.users; + this.createModalRef.componentInstance.userFilter = "role"; + this.createModalRef.componentInstance.confirmFileAssigned = + this.confirmFileAssigned; + } + + confirmFileAssigned() {} + + canAssignFile() { + return ( + !!this.grupoSeleccionado && + !!this.selectedFile && + !this.selectedUser && + this.groupService.userCanAssignFiles(this.grupoSeleccionado.role) + ); + } + + confirmGroupDestroyed() { + this.desseleccionarGrupo(); + this.loadGroups(); + } + mostrarModalCalificarEntrega() { // Mostrar el modal this.modalQualifyDelivery = true; this.modalQualifyDeliveryOpened = true; } + openCreateGroupModal() { + this.createModalRef = this.modalService.open(CreateGroupModal); + + this.createModalRef.componentInstance.modalTitle = "Nuevo Grupo"; + this.createModalRef.componentInstance.confirmGroupCreation = + this.confirmGroupCreation; + } + + confirmGroupCreation(nombre: string) { + if (nombre == undefined || nombre == "") { + this.showNotification("error", "i18n.warning.group.invalidName"); + return; + } + + /** Expresión regular para chequear que el nombre esté empiece con mayúscula */ + var regex = /^[A-Z]/; + if (!regex.test(nombre)) { + this.showNotification("error", "i18n.warning.group.capitalLetter"); + return; + } + + this.groupService.createGroup(nombre).subscribe( + (data) => { + this.loadGroups(); + + this.grupoSeleccionado = data.body["group"]; + this.createModalRef.close(); + }, + (error) => { + this.notifService.error(error); + } + ); + } + + confirmGroupUpdated() { + this.loadGroups(() => { + this.grupoSeleccionado = this.grupos.find( + (grupo) => grupo.id == this.grupoSeleccionado.id + ); + }); + } + + openManageGroupUsersModal() { + this.createModalRef = this.modalService.open(ManageGroupUsersModal); + + this.createModalRef.componentInstance.modalTitle = + "Administrar usuarios del grupo"; + this.createModalRef.componentInstance.groupName = + this.grupoSeleccionado.name; + this.createModalRef.componentInstance.groupId = this.grupoSeleccionado.id; + this.createModalRef.componentInstance.confirmGroupUpdated = + this.confirmGroupUpdated; + } + confirmFileQualify(event: CustomEvent<any>) { const { descripcion, estado, nota } = event.detail; @@ -234,25 +777,142 @@ export class GruposComponent { } esArchivoGrupo() { - if ( - this.archivoSeleccionado && - this.grupoSeleccionado && - this.grupoSeleccionado.archivos.some( - (arch) => arch.id == this.archivoSeleccionado.id - ) - ) { - return true; - } else { - return false; - } + // if ( + // this.archivoSeleccionado && + // this.grupoSeleccionado && + // this.grupoSeleccionado.archivos.some( + // (arch) => arch.id == this.archivoSeleccionado.id + // ) + // ) { + // return true; + // } else { + // return false; + // } + return true; } - cargarArchivoCompartido() { + cargarArchivo() { if (!this.archivoSeleccionado || this.archivoSeleccionado.directorio) { this.showNotification("warning", "i18n.warning.file.noSelected"); return; } this.sessionService.setArchivo(this.archivoSeleccionado); - this.router.navigate(["/matefun"]); + const userId = !!this.selectedUser + ? this.selectedUser.id + : JSON.parse(localStorage.getItem("currentUser"))["id"]; + + this.router.navigate([ + `/grupos/${this.grupoSeleccionado.id}/users/${userId}/matefun/${this.archivoSeleccionado.id}`, + ]); + } + + seleccionarDirectorioAMover() { + if (!this.archivoSeleccionado) { + this.showNotification("warning", "i18n.warning.file.noSelected"); + return; + } + + // Si el archivo es del alumno, se puede mover. + // No se controla por creador, dado que los compartidos mantienen este atributo + // if (!this.archivos.some((arch) => arch.id == this.archivoSeleccionado.id)) { + // this.showNotification("warning", "i18n.warning.file.noPermissionMove"); + // return; + // } + + // this.navBack(false); + if (this.archivoSeleccionado.directorio) { + this.currentDirOfFileToMove = this.directorioActual.parent; + } else { + this.currentDirOfFileToMove = this.directorioActual; + } + + // Mostrar el modal + this.modalMoveFile = true; + this.modalMoveFileOpened = true; + } + + /** + * Valida y confirma la creación del archivo. + * @param event Evento devuelto por el modal asociado para crear el archivo. En el detalle contiene el `nombre` y `descripcion` del archivo que se desea agregar. + */ + confirmFileCreation(event: CustomEvent) { + const { nombre, descripcion } = event.detail; + + // Antes que nada, se chequea que empiece con mayúscula + if (!STARTS_WITH_CAPITAL_LETTER_REGEX.test(nombre)) { + this.showNotification("warning", "i18n.warning.file.capitalLetter"); + return; + } + + const archivo = new Archivo(); + // archivo.cedulaCreador = this.directorioActual.cedulaCreador; // cedula + archivo.contenido = this.modalTypeIsFile ? "" : descripcion || ""; + archivo.directorio = !this.modalTypeIsFile; + archivo.editable = true; + archivo.fechaCreacion = new Date(); + archivo.nombre = nombre; + archivo.padreId = this.directorioActual.id; + archivo.archivos = []; + + const that = this; + const idDirectorioActual = this.directorioActual.id; + + const file = this.fileService.archivoToFile(archivo); + this.groupFileService + .createGroupFile(this.grupoSeleccionado.id, file) + .subscribe( + () => { + that.loadFilesAndFolders(idDirectorioActual); + + // Cerrar el modal en caso de éxito + that.modalCreateFileOpened = false; + }, + (error) => { + that.notifService.error(error.text()); + + // Cerrar el modal en caso de error + that.modalCreateFileOpened = false; + } + ); + } + + /** + * Valida y confirma la creación del archivo. + * @param event Evento devuelto por el modal asociado para crear el archivo. En el detalle contiene el `nombre` y `descripcion` del archivo que se desea agregar. + */ + confirmDocumentCreation(event: CustomEvent) { + const { nombre } = event.detail; + + // Antes que nada, se chequea que empiece con mayúscula + if (!STARTS_WITH_CAPITAL_LETTER_REGEX.test(nombre)) { + this.showNotification("warning", "i18n.warning.file.capitalLetter"); + return; + } + + const document = new MDocument(); + document.text_doc = ""; + document.title = nombre; + document.parent_id = this.directorioActual.id; + + const that = this; + const idDirectorioActual = this.directorioActual.id; + + this.documentService + .createDocument(document, this.grupoSeleccionado.id) + .subscribe( + () => { + that.loadFilesAndFolders( + this.grupoSeleccionado.id, + idDirectorioActual + ); + + that.modalCreateFileOpened = false; + }, + (error) => { + that.notifService.error(error.text()); + + that.modalCreateFileOpened = false; + } + ); } } diff --git a/Frontend Angular 4/src/app/layout/layout-routing.module.ts b/Frontend Angular 4/src/app/layout/layout-routing.module.ts index 0e510bbbf313e7941ae83f5ab80d39bb75aec17a..9a4cbb8b60560d497c24f760a49b9c0211be4e5b 100755 --- a/Frontend Angular 4/src/app/layout/layout-routing.module.ts +++ b/Frontend Angular 4/src/app/layout/layout-routing.module.ts @@ -17,6 +17,11 @@ const routes: Routes = [ loadChildren: () => import("./matefun/matefun.module").then((m) => m.MateFunModule), }, + { + path: "grupos/:groupId/users/:groupUserId/matefun/:id", + loadChildren: () => + import("./matefun/matefun.module").then((m) => m.MateFunModule), + }, { path: "archivos", loadChildren: () => diff --git a/Frontend Angular 4/src/app/layout/layout.module.ts b/Frontend Angular 4/src/app/layout/layout.module.ts index 5159725bfc441c4d72eebd9e0fbc29d682ffa378..ca4ed8ee8e05b73190189f9a7e3167c4368d5c22 100755 --- a/Frontend Angular 4/src/app/layout/layout.module.ts +++ b/Frontend Angular 4/src/app/layout/layout.module.ts @@ -10,6 +10,9 @@ import { AuthenticationService } from "../shared/services/authentication.service import { HaskellService } from "../shared/services/haskell.service"; import { DocumentService } from "../shared/services/document.service"; import { FileService } from "../shared/services/file.service"; +import { GroupDocumentService } from "../shared/services/group-document.service"; +import { GroupFileService } from "../shared/services/group-file.service"; +import { GroupService } from "../shared/services/group.service"; import { LtCodemirrorModule } from "lt-codemirror"; import { CodemirrorModule } from "@ctrl/ngx-codemirror"; import { NotificacionModule } from "../notificacion/notificacion.module"; @@ -34,6 +37,9 @@ import { TitleCaseModule } from "../shared/modules/titlecase.module"; HaskellService, DocumentService, FileService, + GroupDocumentService, + GroupFileService, + GroupService, ], }) export class LayoutModule {} diff --git a/Frontend Angular 4/src/app/layout/matefun/matefun.component.html b/Frontend Angular 4/src/app/layout/matefun/matefun.component.html index 15d5a3eae914138397edff6f99518365dce6bd2c..7dbe99cc4aab0c17d980977ac55f9c886dc7a311 100755 --- a/Frontend Angular 4/src/app/layout/matefun/matefun.component.html +++ b/Frontend Angular 4/src/app/layout/matefun/matefun.component.html @@ -169,6 +169,32 @@ > <i class="fa fa-plus"></i> </button> + <button + *ngIf="canRequestFeedback()" + style="float: right" + (click)="requestFeedback()" + class="btn btn-sm btn-secondary" + ngbPopover="{{ + 'i18n.action.requestFeedback' | translate | titleCase + }} {{ 'i18n.object.file' | translate | titleCase }}" + triggers="mouseenter:mouseleave" + placement="bottom" + > + <i class="fa fa-exchange"></i> + </button> + <button + *ngIf="canReturnFeedback()" + style="float: right" + (click)="returnFeedback()" + class="btn btn-sm btn-secondary" + ngbPopover="{{ + 'i18n.action.returnFeedback' | translate | titleCase + }} {{ 'i18n.object.file' | translate | titleCase }}" + triggers="mouseenter:mouseleave" + placement="bottom" + > + <i class="fa fa-exchange"></i> + </button> <ng-template #popoverContent style="width: 15em"> <div style="width: 12em"> <div class="form-group"> diff --git a/Frontend Angular 4/src/app/layout/matefun/matefun.component.ts b/Frontend Angular 4/src/app/layout/matefun/matefun.component.ts index 5f65fc1d10c4175b71b93455462a47062f386b04..1906dd16ffef31d0ce8d097e163629d5fe74ba3e 100755 --- a/Frontend Angular 4/src/app/layout/matefun/matefun.component.ts +++ b/Frontend Angular 4/src/app/layout/matefun/matefun.component.ts @@ -14,6 +14,7 @@ import { HaskellService } from "../../shared/services/haskell.service"; import { DocumentService } from "../../shared/services/document.service"; import { UserService } from "../../shared/services/user.service"; import { FileService } from "../../shared/services/file.service"; +import { GroupFileService } from "../../shared/services/group-file.service"; import { WebsocketService } from "../../shared/services/websocket.service"; import { ActionCableService } from "app/shared/services/actioncable.service"; import { UsuarioService } from "../../shared/services/usuario.service"; @@ -41,7 +42,12 @@ import { CodeMirrorBinding } from "y-codemirror"; import { NgbModal } from "@ng-bootstrap/ng-bootstrap"; import { CreateFileModal } from "../../shared/components/create-file-modal/create-file-modal.component"; import { ImportFileModal } from "../../shared/components/import-file-modal/import-file-modal.component"; -import { ShareFileModal } from "app/shared"; +import { + ShareFileModal, + RequestFeedbackModal, + ReturnFeedbackModal, +} from "app/shared"; +import { GroupDocumentService } from "../../shared/services/group-document.service"; import * as Y from "yjs"; @@ -67,7 +73,8 @@ import "./codemirror/addons/functions_definition_EN.js"; import "./codemirror/addons/functions_definition_ES.js"; import { Action } from "rxjs/internal/scheduler/Action"; import { findInTree, roleCanShareFile } from "app/utils"; -import { filter } from "rxjs"; +import { filter, Observable } from "rxjs"; +import { GroupService } from "app/shared/services/group.service"; var codeMirrorRef: any; var componentRef: any; @@ -80,6 +87,8 @@ var focus: any; providers: [WebsocketService, NgbPopoverConfig, UsuarioService], }) export class MateFunComponent implements OnInit, OnChanges { + currentUserGroupRole: string; + documentBelongsToCurrentUser: boolean; titlecasePipe: any; translateService: any; consoleDisable: boolean = false; @@ -180,6 +189,8 @@ export class MateFunComponent implements OnInit, OnChanges { activeTabId = 1; ydoc: null | Y.Doc = null; documentId: number | null = null; + groupId: number | null = null; + groupUserId: number | null = null; editorContainer: Element; editor: CodeMirror.Editor; yUndoManager: Y.UndoManager; @@ -224,6 +235,7 @@ export class MateFunComponent implements OnInit, OnChanges { private documentService: DocumentService, private userService: UserService, private fileService: FileService, + private groupFileService: GroupFileService, private authService: AuthenticationService, private ghciService: GHCIService, private notifService: NotificacionService, @@ -231,6 +243,8 @@ export class MateFunComponent implements OnInit, OnChanges { private usuarioService: UsuarioService, public translate: TranslateService, private actionCableService: ActionCableService, + public groupDocumentService: GroupDocumentService, + public groupService: GroupService, private router: Router, private route: ActivatedRoute, private modalService: NgbModal, @@ -399,10 +413,51 @@ export class MateFunComponent implements OnInit, OnChanges { getCurrentDocument(nextFunction: () => void = null) { this.documentId = parseInt(this.route.snapshot.paramMap.get("id")); - this.documentService.getDocument(this.documentId).subscribe({ + this.groupId = parseInt(this.route.snapshot.paramMap.get("groupId")); + this.groupUserId = parseInt( + this.route.snapshot.paramMap.get("groupUserId") + ); + let documentSubscription: Observable<{ + document: MDocument; + }> = null; + let fileSubscriptionService: { + getFile: (id: number) => Observable<{ + file: MFile; + }>; + } = null; + // TODO: Do something similar to this but for files, + // also the redirect to here when choosing a file in the groups page + // is setting the wrong user id + if (!!this.groupId && !!this.groupUserId) { + documentSubscription = this.groupDocumentService.getGroupDocument( + this.groupId, + this.documentId, + this.groupUserId + ); + + fileSubscriptionService = + this.groupFileService.getSpecificUserFileWithSetGroup( + this.groupId, + this.groupUserId + ); + } else { + documentSubscription = this.documentService.getDocument(this.documentId); + fileSubscriptionService = this.fileService; + } + documentSubscription.subscribe({ next: (data) => { this.archivo = this.documentService.documentToArchivo(data.document); - if (!!nextFunction) { + if (this.router.url.includes("grupos")) { + fileSubscriptionService + .getFile(this.archivo.fileId) + .subscribe((file) => { + this.archivo.feedbackRequested = file["file"].feedback_requested; + + if (!!nextFunction) { + nextFunction(); + } + }); + } else if (!!nextFunction) { nextFunction(); } }, @@ -412,6 +467,82 @@ export class MateFunComponent implements OnInit, OnChanges { }); } + canRequestFeedback() { + return ( + !!this.groupId && + !!this.archivo && + this.archivo.feedbackRequested !== undefined && + !this.archivo.feedbackRequested && + this.documentBelongsToCurrentUser + ); + } + + canReturnFeedback() { + return ( + !!this.groupId && + !!this.archivo && + this.archivo.feedbackRequested !== undefined && + this.archivo.feedbackRequested && + this.groupService.userCanReturnFeedback(this.currentUserGroupRole) + ); + // TODO: Do this and the other function + } + + requestFeedback() { + this.openRequestFeedbackModal(); + } + + returnFeedback() { + this.openReturnFeedbackModal(); + } + + openRequestFeedbackModal() { + if (!this.groupId) { + this.showNotification("warning", "i18n.warning.group.noSelected"); + return; + } + + if (!this.archivo.fileId) { + this.showNotification("warning", "i18n.warning.file.noSelected"); + return; + } + + this.createModalRef = this.modalService.open(RequestFeedbackModal); + + this.createModalRef.componentInstance.groupName = null; + this.createModalRef.componentInstance.groupId = this.groupId; + this.createModalRef.componentInstance.fileName = this.archivo.nombre; + this.createModalRef.componentInstance.fileId = this.archivo.fileId; + this.createModalRef.componentInstance.confirmFeedbackRequested = + this.confirmFeedbackRequsted; + } + + confirmFeedbackRequsted() {} + + openReturnFeedbackModal() { + if (!this.groupId) { + this.showNotification("warning", "i18n.warning.group.noSelected"); + return; + } + + if (!this.archivo.fileId) { + this.showNotification("warning", "i18n.warning.file.noSelected"); + return; + } + + this.createModalRef = this.modalService.open(ReturnFeedbackModal); + + this.createModalRef.componentInstance.groupName = null; + this.createModalRef.componentInstance.groupId = this.groupId; + this.createModalRef.componentInstance.fileName = this.archivo.nombre; + this.createModalRef.componentInstance.fileId = this.archivo.fileId; + this.createModalRef.componentInstance.userId = this.groupUserId; + this.createModalRef.componentInstance.confirmFeedbackReturned = + this.confirmFeedbackReturned; + } + + confirmFeedbackReturned() {} + ngOnChanges() { // const docId = this.route.snapshot.paramMap.get("id"); // this.confirmFileCreation = this.confirmFileCreation.bind(this); @@ -458,10 +589,34 @@ export class MateFunComponent implements OnInit, OnChanges { this.configCodeMirrorDefinicion["readOnly"] = true; // This was always on ngOnInit + + this.groupId = !!this.route.snapshot.paramMap.get("groupId") + ? parseInt(this.route.snapshot.paramMap.get("groupId")) + : null; + this.groupUserId = !!this.route.snapshot.paramMap.get("groupUserId") + ? parseInt(this.route.snapshot.paramMap.get("groupUserId")) + : null; + if (!!this.route.snapshot.paramMap.get("id")) { this.getCurrentDocument(() => { this.copiaContenidoArchivo = this.archivo.contenido; this.copiaNombreArchivo = this.archivo.nombre; + + if ( + this.router.url.includes("grupos") && + !!this.groupId && + !!this.groupUserId + ) { + this.groupFileService + .getSpecificUserGroupFiles(this.groupId, this.groupUserId) + .subscribe((data) => { + this.importFilesData(data); + }); + } else { + this.fileService.getFiles().subscribe((data) => { + this.importFilesData(data); + }); + } }); } else { this.newFile(); @@ -587,39 +742,30 @@ export class MateFunComponent implements OnInit, OnChanges { } else { this.configCodeMirror.mode.name = "matefun-ES"; } + } - this.fileService.getFiles().subscribe((files) => { - let matefun_files = files["files"]; - matefun_files = this.fileService.completeFiles(matefun_files); - this.sessionService.setUserFiles(matefun_files); - let fileArchivos = this.fileService.fileToArchivo(matefun_files); - - this.archivosTree = fileArchivos; - if (!!this.editor) { - (this.editor as any).options.files = this.archivosTree; - } - this.sessionService.setArchivosTree(this.archivosTree); - }); - - this.fileService.getFiles().subscribe((data) => { - let matefun_files = data["files"]; - matefun_files = this.fileService.completeFiles(matefun_files); - const tree = this.fileService.fileToArchivo(matefun_files); - this.archivosTree = tree; - this.sessionService.setArchivosTree(tree); - const file = findInTree([tree], this.archivo.fileId); - const directorioActual = findInTree([tree], file.padreId); - this.sessionService.setDirectorioActual(directorioActual); - this.documentService - .getDocuments( - directorioActual.archivos.map((archivo) => { - return archivo.documentId; - }) - ) - .subscribe((data) => { - this.sessionService.setCurrentDirectoryDocuments(data["documents"]); - }); - }); + importFilesData(data) { + let matefun_files = data["files"]; + matefun_files = this.fileService.completeFiles(matefun_files); + this.sessionService.setUserFiles(matefun_files); + const tree = this.fileService.fileToArchivo(matefun_files); + this.archivosTree = tree; + if (!!this.editor) { + (this.editor as any).options.files = this.archivosTree; + } + this.sessionService.setArchivosTree(tree); + const file = findInTree([tree], this.archivo.fileId); + const directorioActual = findInTree([tree], file.padreId); + this.sessionService.setDirectorioActual(directorioActual); + this.documentService + .getDocuments( + directorioActual.archivos.map((archivo) => { + return archivo.documentId; + }) + ) + .subscribe((data) => { + this.sessionService.setCurrentDirectoryDocuments(data["documents"]); + }); } ngAfterViewInit() { @@ -764,18 +910,40 @@ export class MateFunComponent implements OnInit, OnChanges { initializeEditor() { let readOnly = false; + if (!!this.archivo.users) { if (this.archivo.users.length > 0) { const id = JSON.parse(localStorage.getItem("currentUser"))["id"]; - readOnly = - this.archivo.users.find((user) => { - return user.id === id; - }).role === "viewer"; + const user = this.archivo.users.find((user) => { + return user.id === id; + }); + this.documentBelongsToCurrentUser = !!user; + + if (!!user) { + readOnly = user.role === "viewer"; + } + + if ((!user || readOnly == true) && !!this.groupId) { + const id = JSON.parse(localStorage.getItem("currentUser"))["id"]; + this.groupService.getUserRole(this.groupId, id).subscribe((data) => { + this.currentUserGroupRole = data.role.role; - this.editor.setOption("readOnly", readOnly); + this.editor.setOption( + "readOnly", + !this.groupService.userCanEditAnyGroupFile( + this.currentUserGroupRole + ) + ); + }); + } else { + this.editor.setOption("readOnly", readOnly); + } } else { + this.documentBelongsToCurrentUser = true; this.editor.setOption("readOnly", false); } + } else { + this.documentBelongsToCurrentUser = true; } if (!!this.binding) { @@ -802,11 +970,22 @@ export class MateFunComponent implements OnInit, OnChanges { this.ydoc = yDocument; + let params: Record<string, string> = { + doc_id: this.documentId?.toString(), + }; + if (!!this.groupId && !!this.groupUserId) { + params = { + ...params, + user_id: this.groupUserId.toString(), + group_id: this.groupId.toString(), + }; + } + this.provider = new WebsocketProvider( this.ydoc, this.actionCableService.consumer, "ApplicationCable::DocumentChannel", - { doc_id: this.documentId?.toString() } + params ); let awareness = undefined; diff --git a/Frontend Angular 4/src/app/shared/components/action-confirmation-modal/action-confirmation-modal.component.html b/Frontend Angular 4/src/app/shared/components/action-confirmation-modal/action-confirmation-modal.component.html new file mode 100644 index 0000000000000000000000000000000000000000..f80e61cb641a4c8ddacc5dc720cb83c0efbf5a47 --- /dev/null +++ b/Frontend Angular 4/src/app/shared/components/action-confirmation-modal/action-confirmation-modal.component.html @@ -0,0 +1,27 @@ +<div class="modal-header"> + <span class="modal-title font-bold" id="modal-title">{{ title }}</span> + <button + aria-label="Cerrar diálogo" + (click)="cancelAction()" + part="close-button" + class="close-button fa fa-close" + type="button" + ></button> +</div> +<div class="modal-body"> + <div class="mb-3"> + {{ message }} + </div> + <div class="flex justify-end"> + <button + (click)="cancelAction()" + class="btn btn-secondary mr-3" + slot="secondary" + > + Cancelar + </button> + <button (click)="confirmAction()" class="btn btn-primary" slot="primary"> + Confirmar + </button> + </div> +</div> diff --git a/Frontend Angular 4/src/app/shared/components/action-confirmation-modal/action-confirmation-modal.component.ts b/Frontend Angular 4/src/app/shared/components/action-confirmation-modal/action-confirmation-modal.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..47b26b3520a71277d158e4031f28e0f558d4ae69 --- /dev/null +++ b/Frontend Angular 4/src/app/shared/components/action-confirmation-modal/action-confirmation-modal.component.ts @@ -0,0 +1,15 @@ +import { Component, ChangeDetectionStrategy, Input } from "@angular/core"; + +import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap"; + +@Component({ + selector: "app-action-confirmation-modal", + templateUrl: "./action-confirmation-modal.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ActionConfirmationModal { + @Input() title: string; + @Input() message: string; + @Input() confirmAction: () => void; + @Input() cancelAction: () => void; +} diff --git a/Frontend Angular 4/src/app/shared/components/assign-file-modal/assign-file-modal.component.html b/Frontend Angular 4/src/app/shared/components/assign-file-modal/assign-file-modal.component.html new file mode 100644 index 0000000000000000000000000000000000000000..a8a4e237cc407b6b56ae8bb1fcf2448a6e2dbc83 --- /dev/null +++ b/Frontend Angular 4/src/app/shared/components/assign-file-modal/assign-file-modal.component.html @@ -0,0 +1,33 @@ +<div class="modal-header"> + <span class="modal-title font-bold" id="modal-title">{{ title }}</span> + <button + aria-label="Cerrar diálogo" + (click)="modal.dismiss('Close assign file modal')" + part="close-button" + class="close-button fa fa-close" + type="button" + ></button> +</div> +<div class="modal-body"> + <div class="flex flex-col justify-between w-full"> + <label for="new-users">Users</label> + <div class="flex justify-between w-full items-center"> + <app-checkbox-select + [(entities)]="users" + entitiesName="user" + filterName="role" + class="w-full" + ></app-checkbox-select> + </div> + </div> + <div class="flex justify-end mt-2"> + <button + (click)="assignFile()" + [disabled]="selectedUsers().length === 0" + class="btn btn-primary" + slot="primary" + > + Asignar + </button> + </div> +</div> diff --git a/Frontend Angular 4/src/app/shared/components/assign-file-modal/assign-file-modal.component.ts b/Frontend Angular 4/src/app/shared/components/assign-file-modal/assign-file-modal.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..d7136f21da6e7ebe19bf8c7031e48e15273d6150 --- /dev/null +++ b/Frontend Angular 4/src/app/shared/components/assign-file-modal/assign-file-modal.component.ts @@ -0,0 +1,79 @@ +import { + Component, + ChangeDetectionStrategy, + Input, + OnInit, + ChangeDetectorRef, +} from "@angular/core"; + +import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap"; + +import { GroupFileService } from "../../services/group-file.service"; + +import { selectableUser } from "../../objects/archivo-types"; + +@Component({ + selector: "app-assign-file-modal", + templateUrl: "./assign-file-modal.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AssignFileModal implements OnInit { + @Input() fileName: string; + @Input() groupName: string | null; + @Input() groupId: number; + @Input() fileId: number; + + @Input() users: selectableUser[] = []; + @Input() userFilter: string = null; + + /** + * Se dispara cuando se quiere administrar los usuarios del grupo + */ + @Input() confirmFileAssigned: () => void; + title = "Asignar archivo"; + + constructor( + public modal: NgbActiveModal, + private groupFileService: GroupFileService, + private changeDetectorRef: ChangeDetectorRef + ) {} + + ngOnInit(): void { + this.fileName = `${this.fileName}`; + + if (!!this.groupName) { + this.groupName = `${this.groupName}`; + this.title = `¿Asignar archivo ${this.fileName} en el grupo ${this.groupName}?`; + } else { + this.title = `¿Asignar archivo ${this.fileName}?`; + } + + this.assignFile = this.assignFile.bind(this); + this.cancel = this.cancel.bind(this); + } + + selectedUsers() { + return this.users.filter((user) => user.selected); + } + + assignFile(): void { + this.groupFileService + .assignFile( + this.groupId, + this.fileId, + this.users.filter((user) => user.selected).map((user) => user.id), + [] // subgroups.map((subgroup) => subgroup.id) + ) + .subscribe({ + next: (_) => { + this.confirmFileAssigned(); + this.modal.close(); + }, + error: (error) => console.log(error), + }); + } + + cancel(): void { + this.modal.dismiss(); + } +} diff --git a/Frontend Angular 4/src/app/shared/components/checkbox-select/checkbox-select.component.html b/Frontend Angular 4/src/app/shared/components/checkbox-select/checkbox-select.component.html new file mode 100644 index 0000000000000000000000000000000000000000..80297d1ac59e6b9bf30d961d922c789b3c4c74d0 --- /dev/null +++ b/Frontend Angular 4/src/app/shared/components/checkbox-select/checkbox-select.component.html @@ -0,0 +1,38 @@ +<div class="flex flex-col justify-between w-full"> + <label for="new-users">Something</label> +</div> +<table class="table-fixed w-96 mx-auto"> + <thead> + <tr> + <th> + <mat-checkbox + [checked]="allEntitiesSelected()" + (change)="onCheckboxChange($event.checked)" + > + </mat-checkbox> + </th> + <th>{{ entitiesName }}</th> + <th *ngIf="{filterName}">{{ filterName }}</th> + </tr> + </thead> + <tbody> + <tr + *ngFor="let entity of entities" + (mouseup)="selectDeselectEntity(entity)" + class="hover:bg-gray-200" + > + <th> + <mat-checkbox + #inputCheckbox + [checked]="entity.selected" + (change)="selectDeselectEntity(entity)" + > + </mat-checkbox> + </th> + <td>{{ entity.username }}</td> + <ng-container *ngIf="{filterName}"> + <td>{{ entity[filterName] }}</td> + </ng-container> + </tr> + </tbody> +</table> diff --git a/Frontend Angular 4/src/app/shared/components/checkbox-select/checkbox-select.component.ts b/Frontend Angular 4/src/app/shared/components/checkbox-select/checkbox-select.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..4aff60643190e88dfa010d8229e1d5cdf28ed26f --- /dev/null +++ b/Frontend Angular 4/src/app/shared/components/checkbox-select/checkbox-select.component.ts @@ -0,0 +1,64 @@ +import { + Component, + ChangeDetectionStrategy, + Input, + Output, + EventEmitter, + OnInit, + ChangeDetectorRef, + OnChanges, + SimpleChanges, +} from "@angular/core"; + +import { selectableUser } from "../../objects/archivo-types"; + +import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap"; + +@Component({ + selector: "app-checkbox-select", + templateUrl: "./checkbox-select.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CheckboxSelect implements OnInit, OnChanges { + @Input() entitiesName: string; + @Input() entities: selectableUser[] = []; + @Input() filterName: string; + @Output() entitiesChanged = new EventEmitter<selectableUser[]>(); + + constructor( + public modal: NgbActiveModal, + private changeDetectorRef: ChangeDetectorRef + ) {} + + ngOnInit(): void {} + + ngOnChanges(changes: SimpleChanges): void { + // if (!!changes.users) { + // this.users.forEach((user) => { + // this.usersObject[user.id] = user.role; + // }); + // } + } + + selectDeselectEntity(entity: selectableUser): void { + entity.selected = !entity.selected; + this.entitiesChange(this.entities); + } + + entitiesChange(entities: selectableUser[]): void { + this.entities = entities; + this.entitiesChanged.emit(this.entities); + this.changeDetectorRef.detectChanges(); + } + + allEntitiesSelected() { + return this.entities.every((entity) => entity.selected); + } + + onCheckboxChange(checked: boolean): void { + this.entities.forEach((entity) => { + entity.selected = checked; + }); + this.entitiesChange(this.entities); + } +} diff --git a/Frontend Angular 4/src/app/shared/components/create-group-modal/create-group-modal.component.html b/Frontend Angular 4/src/app/shared/components/create-group-modal/create-group-modal.component.html new file mode 100644 index 0000000000000000000000000000000000000000..b49e958e9ef667c882d1f416b85b18f2c596c38a --- /dev/null +++ b/Frontend Angular 4/src/app/shared/components/create-group-modal/create-group-modal.component.html @@ -0,0 +1,33 @@ +<div class="modal-header"> + <span class="modal-title font-bold" id="modal-title">{{ modalTitle }}</span> + <button + aria-label="Cerrar diálogo" + (click)="modal.dismiss('Close create group modal')" + part="close-button" + class="close-button fa fa-close" + type="button" + ></button> +</div> +<div class="modal-body"> + <div class="flex flex-col space-between"> + <label for="document-title">Group Name</label> + <input + type="text" + [(ngModel)]="groupName" + placeholder="Enter group name" + ngbAutofocus + id="group-name" + class="name-input border-opacity-25 border-black border-2 rounded w-full px-2 py-3 text-base focus-visible:border-focused-blue focus:outline-transparent" + /> + </div> + <div class="flex justify-end mt-4"> + <button + (click)="confirmGroupCreation(groupName)" + [disabled]="!groupName" + class="btn btn-primary" + slot="primary" + > + Crear Grupo + </button> + </div> +</div> diff --git a/Frontend Angular 4/src/app/shared/components/create-group-modal/create-group-modal.component.ts b/Frontend Angular 4/src/app/shared/components/create-group-modal/create-group-modal.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..05cba8db5067dd2c7a07ef601ba0895bb91bfdcd --- /dev/null +++ b/Frontend Angular 4/src/app/shared/components/create-group-modal/create-group-modal.component.ts @@ -0,0 +1,38 @@ +import { + Component, + ChangeDetectionStrategy, + Input, + OnInit, +} from "@angular/core"; + +import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap"; + +import { MFile } from "../../objects/archivo-types"; +import { Group } from "../../objects/grupo"; +import { FileService } from "../../services/file.service"; +import { GroupService } from "../../services/group.service"; + +@Component({ + selector: "app-create-group-modal", + templateUrl: "./create-group-modal.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CreateGroupModal implements OnInit { + @Input() modalTitle: string; + @Input() groupName: string; + + /** + * Se dispara cuando se confirma la creación del archivo en el directorio actual. + */ + @Input() confirmGroupCreation: (title: string) => void; + + constructor( + public modal: NgbActiveModal, + private groupService: GroupService + ) {} + + ngOnInit(): void { + this.groupName = `${this.groupName || ""}`; + this.modalTitle = `${this.modalTitle}`; + } +} diff --git a/Frontend Angular 4/src/app/shared/components/destroy-group-modal/destroy-group-modal.component.html b/Frontend Angular 4/src/app/shared/components/destroy-group-modal/destroy-group-modal.component.html new file mode 100644 index 0000000000000000000000000000000000000000..0451b0ce81c5184153c2fcf24d5de5b2b621d11e --- /dev/null +++ b/Frontend Angular 4/src/app/shared/components/destroy-group-modal/destroy-group-modal.component.html @@ -0,0 +1,6 @@ +<app-action-confirmation-modal + [title]="title" + [message]="message" + [confirmAction]="destroyGroup" + [cancelAction]="cancel" +></app-action-confirmation-modal> diff --git a/Frontend Angular 4/src/app/shared/components/destroy-group-modal/destroy-group-modal.component.ts b/Frontend Angular 4/src/app/shared/components/destroy-group-modal/destroy-group-modal.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..f4b4582fbb8c65df9b93ed204a3030dd4f8ed79a --- /dev/null +++ b/Frontend Angular 4/src/app/shared/components/destroy-group-modal/destroy-group-modal.component.ts @@ -0,0 +1,60 @@ +import { + Component, + ChangeDetectionStrategy, + Input, + OnInit, +} from "@angular/core"; + +import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap"; + +import { GroupService } from "../../services/group.service"; + +@Component({ + selector: "app-destroy-group-modal", + templateUrl: "./destroy-group-modal.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DestroyGroupModal implements OnInit { + @Input() groupName: string; + @Input() groupId: number; + @Input() userRole: string; + /** + * Se dispara cuando se quiere administrar los usuarios del grupo + */ + @Input() confirmGroupDestroyed: () => void; + title: string; + message: string; + + constructor( + public modal: NgbActiveModal, + private groupService: GroupService + ) {} + + ngOnInit(): void { + this.groupName = `${this.groupName}`; + + if (this.userRole === "owner") { + this.title = "Destruir Grupo"; + this.message = `¿Estás seguro de que deseas destruir el grupo ${this.groupName}?`; + } else { + this.title = "Abandonar Grupo"; + this.message = `¿Estás seguro de que desea abandonar el grupo ${this.groupName}?`; + } + this.destroyGroup = this.destroyGroup.bind(this); + this.cancel = this.cancel.bind(this); + } + + destroyGroup(): void { + this.groupService.destroyGroup(this.groupId).subscribe({ + next: (_) => { + this.confirmGroupDestroyed(); + this.modal.close(); + }, + error: (error) => console.log(error), + }); + } + + cancel(): void { + this.modal.dismiss(); + } +} diff --git a/Frontend Angular 4/src/app/shared/components/index.ts b/Frontend Angular 4/src/app/shared/components/index.ts index 381d0ed39e0186d8d7561acd39e24b27d9b25d42..88229485e2bf876c7d75857d5905cd79a375f820 100755 --- a/Frontend Angular 4/src/app/shared/components/index.ts +++ b/Frontend Angular 4/src/app/shared/components/index.ts @@ -8,3 +8,11 @@ export * from "./share-file-modal/share-file-modal.component"; export * from "./chip-input/chip-input.component"; export * from "./select/select.component"; export * from "./add-users-modal/add-users-modal.component"; +export * from "./create-group-modal/create-group-modal.component"; +export * from "./manage-group-users-modal/manage-group-users-modal.component"; +export * from "./action-confirmation-modal/action-confirmation-modal.component"; +export * from "./destroy-group-modal/destroy-group-modal.component"; +export * from "./request-feedback-modal/request-feedback-modal.component"; +export * from "./return-feedback-modal/return-feedback-modal.component"; +export * from "./assign-file-modal/assign-file-modal.component"; +export * from "./checkbox-select/checkbox-select.component"; diff --git a/Frontend Angular 4/src/app/shared/components/manage-group-users-modal/manage-group-users-modal.component.html b/Frontend Angular 4/src/app/shared/components/manage-group-users-modal/manage-group-users-modal.component.html new file mode 100644 index 0000000000000000000000000000000000000000..891497b1ca02d6d123f122d2a4b03c09befa507f --- /dev/null +++ b/Frontend Angular 4/src/app/shared/components/manage-group-users-modal/manage-group-users-modal.component.html @@ -0,0 +1,11 @@ +<app-add-users-modal + [modalTitle]="modalTitle" + [(chips)]="chips" + [newUserRolesEnum]="newUsersPermissionsEnum" + [newUserRole]="newUsersPermission" + (close)="modal.dismiss('Close manage user\'s group modal')" + [users]="usersCopy" + [rolesField]="groupRole" + [requiredRole]="'Owner'" + [save]="save" +></app-add-users-modal> diff --git a/Frontend Angular 4/src/app/shared/components/manage-group-users-modal/manage-group-users-modal.component.ts b/Frontend Angular 4/src/app/shared/components/manage-group-users-modal/manage-group-users-modal.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..a8dfef43c4791a79fe2ad3db4116a1518fe77e44 --- /dev/null +++ b/Frontend Angular 4/src/app/shared/components/manage-group-users-modal/manage-group-users-modal.component.ts @@ -0,0 +1,69 @@ +import { + Component, + ChangeDetectionStrategy, + Input, + OnInit, + ChangeDetectorRef, +} from "@angular/core"; + +import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap"; + +import { User } from "../../objects/archivo-types"; +import { Group } from "../../objects/grupo"; +import { GroupService } from "../../services/group.service"; + +@Component({ + selector: "app-manage-group-users-modal", + templateUrl: "./manage-group-users-modal.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ManageGroupUsersModal implements OnInit { + @Input() modalTitle: string; + @Input() groupName: string; + @Input() groupId: number; + group: Group; + newUsersPermissionsEnum: string[] = [ + "Owner", + "Manager", + "Moderator", + "Member", + ]; + newUsersPermission = this.newUsersPermissionsEnum[3]; + users: User[] = []; + usersCopy: User[] = []; + groupRole = "role"; + /** + * Se dispara cuando se quiere administrar los usuarios del grupo + */ + @Input() confirmGroupUpdated: () => void; + + chips: string[] = []; + + constructor( + public modal: NgbActiveModal, + private groupService: GroupService, + private changeDetectorRef: ChangeDetectorRef + ) {} + + ngOnInit(): void { + this.groupService.getGroup(this.groupId).subscribe((groupResponse) => { + this.group = groupResponse.group; + this.users = groupResponse.group.users; + this.usersCopy = [...this.users]; + this.changeDetectorRef.detectChanges(); + }); + this.groupName = `${this.groupName}`; + this.modalTitle = `${this.modalTitle}`; + this.save = this.save.bind(this); + } + + save(userData: { id?: number; username: string; role: string }[]): void { + this.groupService.updateGroup(this.group.id, userData).subscribe({ + next: (_) => { + this.confirmGroupUpdated(); + this.modal.close(); + }, + error: (error) => console.log(error), + }); + } +} diff --git a/Frontend Angular 4/src/app/shared/components/request-feedback-modal/request-feedback-modal.component.html b/Frontend Angular 4/src/app/shared/components/request-feedback-modal/request-feedback-modal.component.html new file mode 100644 index 0000000000000000000000000000000000000000..626e7c6ab99157c67b1b6d00cae37ac92ae214e1 --- /dev/null +++ b/Frontend Angular 4/src/app/shared/components/request-feedback-modal/request-feedback-modal.component.html @@ -0,0 +1,6 @@ +<app-action-confirmation-modal + [title]="title" + [message]="message" + [confirmAction]="requestFeedback" + [cancelAction]="cancel" +></app-action-confirmation-modal> diff --git a/Frontend Angular 4/src/app/shared/components/request-feedback-modal/request-feedback-modal.component.ts b/Frontend Angular 4/src/app/shared/components/request-feedback-modal/request-feedback-modal.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..968d5e14c22db44915a25ca06f2967fea8cf029c --- /dev/null +++ b/Frontend Angular 4/src/app/shared/components/request-feedback-modal/request-feedback-modal.component.ts @@ -0,0 +1,63 @@ +import { + Component, + ChangeDetectionStrategy, + Input, + OnInit, +} from "@angular/core"; + +import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap"; + +import { GroupFileService } from "../../services/group-file.service"; + +@Component({ + selector: "app-request-feedback-modal", + templateUrl: "./request-feedback-modal.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RequestFeedbackModal implements OnInit { + @Input() fileName: string; + @Input() groupName: string | null; + @Input() groupId: number; + @Input() fileId: number; + + /** + * Se dispara cuando se quiere administrar los usuarios del grupo + */ + @Input() confirmFeedbackRequested: () => void; + title = "Pedir retroalimentación"; + message: string; + + constructor( + public modal: NgbActiveModal, + private groupFileService: GroupFileService + ) {} + + ngOnInit(): void { + this.fileName = `${this.fileName}`; + + if (!!this.groupName) { + this.groupName = `${this.groupName}`; + this.message = `¿Desea pedir retroalimentación del archivo ${this.fileName} en el grupo ${this.groupName}?`; + } else { + this.message = `¿Desea pedir retroalimentación del archivo ${this.fileName}?`; + } + + this.requestFeedback = this.requestFeedback.bind(this); + this.cancel = this.cancel.bind(this); + } + + requestFeedback(): void { + this.groupFileService.requestFeedback(this.groupId, this.fileId).subscribe({ + next: (_) => { + this.confirmFeedbackRequested(); + + this.modal.close(); + }, + error: (error) => console.log(error), + }); + } + + cancel(): void { + this.modal.dismiss(); + } +} diff --git a/Frontend Angular 4/src/app/shared/components/request-group-file-feedback-modal/request-group-file-feedback-modal.component.html b/Frontend Angular 4/src/app/shared/components/request-group-file-feedback-modal/request-group-file-feedback-modal.component.html new file mode 100644 index 0000000000000000000000000000000000000000..0451b0ce81c5184153c2fcf24d5de5b2b621d11e --- /dev/null +++ b/Frontend Angular 4/src/app/shared/components/request-group-file-feedback-modal/request-group-file-feedback-modal.component.html @@ -0,0 +1,6 @@ +<app-action-confirmation-modal + [title]="title" + [message]="message" + [confirmAction]="destroyGroup" + [cancelAction]="cancel" +></app-action-confirmation-modal> diff --git a/Frontend Angular 4/src/app/shared/components/request-group-file-feedback-modal/request-group-file-feedback-modal.component.ts b/Frontend Angular 4/src/app/shared/components/request-group-file-feedback-modal/request-group-file-feedback-modal.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..1f81dd51a21b8122c9ac87550ed15ba5a994741c --- /dev/null +++ b/Frontend Angular 4/src/app/shared/components/request-group-file-feedback-modal/request-group-file-feedback-modal.component.ts @@ -0,0 +1,54 @@ +import { + Component, + ChangeDetectionStrategy, + Input, + OnInit, +} from "@angular/core"; + +import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap"; + +import { GroupService } from "../../services/group.service"; + +@Component({ + selector: "app-destroy-group-modal", + templateUrl: "./destroy-group-modal.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RequestGroupFileModal implements OnInit { + @Input() groupName: string; + @Input() fileName: string; + @Input() groupId: number; + @Input() fileId: number; + /** + * Se dispara cuando se quiere administrar los usuarios del grupo + */ + title: string; + message: string; + + constructor( + public modal: NgbActiveModal, + private groupService: GroupService + ) {} + + ngOnInit(): void { + this.groupName = `${this.groupName}`; + + this.title = "Pedir Retroalimentación"; + this.message = `¿Estás seguro de que desea pedir retroalimentación para el archivo ${this.fileName}? \n Esta decisión no se puede deshacer.`; + this.requestFeedback = this.requestFeedback.bind(this); + this.cancel = this.cancel.bind(this); + } + + requestFeedback(): void { + this.groupService.requestFeedback(this.groupId, this.fileId).subscribe({ + next: (_) => { + this.modal.close(); + }, + error: (error) => console.log(error), + }); + } + + cancel(): void { + this.modal.dismiss(); + } +} diff --git a/Frontend Angular 4/src/app/shared/components/return-feedback-modal/return-feedback-modal.component.html b/Frontend Angular 4/src/app/shared/components/return-feedback-modal/return-feedback-modal.component.html new file mode 100644 index 0000000000000000000000000000000000000000..8f8bd9d54121b24dccc760a9b5bb966afc823b0e --- /dev/null +++ b/Frontend Angular 4/src/app/shared/components/return-feedback-modal/return-feedback-modal.component.html @@ -0,0 +1,6 @@ +<app-action-confirmation-modal + [title]="title" + [message]="message" + [confirmAction]="returnFeedback" + [cancelAction]="cancel" +></app-action-confirmation-modal> diff --git a/Frontend Angular 4/src/app/shared/components/return-feedback-modal/return-feedback-modal.component.ts b/Frontend Angular 4/src/app/shared/components/return-feedback-modal/return-feedback-modal.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..422ebadf23a709d598f4afd5660c650a042af730 --- /dev/null +++ b/Frontend Angular 4/src/app/shared/components/return-feedback-modal/return-feedback-modal.component.ts @@ -0,0 +1,66 @@ +import { + Component, + ChangeDetectionStrategy, + Input, + OnInit, +} from "@angular/core"; + +import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap"; + +import { GroupFileService } from "../../services/group-file.service"; + +@Component({ + selector: "app-return-feedback-modal", + templateUrl: "./return-feedback-modal.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ReturnFeedbackModal implements OnInit { + @Input() fileName: string; + @Input() groupName: string | null; + @Input() groupId: number; + @Input() fileId: number; + @Input() userId: number; + + /** + * Se dispara cuando se quiere administrar los usuarios del grupo + */ + @Input() confirmFeedbackReturned: () => void; + title = "Dar retroalimentación"; + message: string; + + constructor( + public modal: NgbActiveModal, + private groupFileService: GroupFileService + ) {} + + ngOnInit(): void { + this.fileName = `${this.fileName}`; + + if (!!this.groupName) { + this.groupName = `${this.groupName}`; + this.message = `¿Desea dar retroalimentación del archivo ${this.fileName} en el grupo ${this.groupName}?`; + } else { + this.message = `¿Desea dar retroalimentación del archivo ${this.fileName}?`; + } + + this.returnFeedback = this.returnFeedback.bind(this); + this.cancel = this.cancel.bind(this); + } + + returnFeedback(): void { + this.groupFileService + .returnFeedback(this.groupId, this.fileId, this.userId) + .subscribe({ + next: (_) => { + this.confirmFeedbackReturned(); + + this.modal.close(); + }, + error: (error) => console.log(error), + }); + } + + cancel(): void { + this.modal.dismiss(); + } +} diff --git a/Frontend Angular 4/src/app/shared/config.ts b/Frontend Angular 4/src/app/shared/config.ts index f93b27ef46901b3b099edebbddfdeab7ed7edb59..01e774f111ad6e97e22a9733e1f63b536c87376f 100755 --- a/Frontend Angular 4/src/app/shared/config.ts +++ b/Frontend Angular 4/src/app/shared/config.ts @@ -50,3 +50,27 @@ export const CREATE_DOCUMENT = SERVER + "/api/v2/documents"; export const UPDATE_DOCUMENT = SERVER + "/api/v2/documents"; export const DESTROY_AUX_DOCUMENT = SERVER + "/api/v2/aux_documents"; + +export const GET_GROUP = SERVER + "/api/v2/groups/:id"; +export const GET_GROUPS = SERVER + "/api/v2/groups"; +export const CREATE_GROUP = SERVER + "/api/v2/groups"; +export const UPDATE_GROUP = SERVER + "/api/v2/groups"; +export const DELETE_GROUP = SERVER + "/api/v2/groups"; + +export const GET_GROUP_DOCUMENT = + SERVER + "/api/v2/groups/:group_id/documents/:id"; +export const GET_GROUP_FILE = SERVER + "/api/v2/groups/:group_id/files/:id"; +export const GET_GROUP_FILES = SERVER + "/api/v2/groups/:group_id/files"; +export const GET_GROUP_FILES_LISTS = + SERVER + "/api/v2/groups/:group_id/files/lists"; +export const CREATE_GROUP_FILE = SERVER + "/api/v2/groups/:group_id/files"; +export const UPDATE_GROUP_FILE = SERVER + "/api/v2/groups/:group_id/files"; +export const DELETE_GROUP_FILE = SERVER + "/api/v2/groups/:group_id/files"; +export const GET_GROUP_FEEDBACK_REQUESTED_FILES = + SERVER + "/api/v2/groups/:group_id/feedback_requested_files"; +export const CREATE_GROUP_FILE_FEEDBACK_REQUEST = + SERVER + "/api/v2/groups/:group_id/feedback_requests"; +export const GET_GROUP_USER_ROLE = + SERVER + "/api/v2/groups/:group_id/users/:user_id/role"; +export const ASSIGN_FILE = + SERVER + "/api/v2/groups/:group_id/files/:file_id/assignment"; diff --git a/Frontend Angular 4/src/app/shared/objects/archivo-types.ts b/Frontend Angular 4/src/app/shared/objects/archivo-types.ts index 15e62bc2f682a5f41e282ee7d6e732866bf6587e..e2dfc84351363d8e382656768649c58642ef085f 100755 --- a/Frontend Angular 4/src/app/shared/objects/archivo-types.ts +++ b/Frontend Angular 4/src/app/shared/objects/archivo-types.ts @@ -22,9 +22,11 @@ export class Archivo { eliminado: boolean; evaluacion: Evaluacion; documentId: number; + groupId?: number; fileId?: number; users?: User[]; role?: string; + feedbackRequested?: boolean; constructor() {} } @@ -39,8 +41,10 @@ export class MFile { parent_id?: number; directory: boolean; children?: MFile[]; + group_id?: number; users?: User[]; role?: string; + feedback_requested?: boolean; constructor() {} } @@ -71,6 +75,10 @@ export class User { role?: string; } +export class selectableUser extends User { + selected: boolean; +} + export class Grupo { anio: number; grado: number; diff --git a/Frontend Angular 4/src/app/shared/objects/grupo.ts b/Frontend Angular 4/src/app/shared/objects/grupo.ts index 9c9679ea7f3b6ad3e1a70627a3364a8ecd3b9ca6..832f2203eac529ce7b2b06e4515901bd51c506d9 100755 --- a/Frontend Angular 4/src/app/shared/objects/grupo.ts +++ b/Frontend Angular 4/src/app/shared/objects/grupo.ts @@ -1,27 +1,10 @@ -import { Archivo } from "./archivo-types"; -import { Usuario } from "./usuario"; +import { MFile } from "./archivo-types"; +import { User } from "./archivo-types"; -export class Grupo { - anio: number; - grado: number; - grupo: string; - liceoId: number; - archivos: Archivo[]; - alumnos: Usuario[]; - - constructor( - anio: number, - grado: number, - grupo: string, - liceoId: number, - archivos: Archivo[], - alumnos: Usuario[] - ) { - this.anio = anio; - this.grado = grado; - this.grupo = grupo; - this.liceoId = liceoId; - this.archivos = archivos; - this.alumnos = alumnos; - } +export class Group { + id: number; + name: string; + files?: MFile[]; + users?: User[]; + role?: string; } diff --git a/Frontend Angular 4/src/app/shared/services/document.service.ts b/Frontend Angular 4/src/app/shared/services/document.service.ts index e1df5247de7b5a732bd6aa89afa0e665758d5784..4978249cbead78eb5e44ea96f47142d61e193f52 100644 --- a/Frontend Angular 4/src/app/shared/services/document.service.ts +++ b/Frontend Angular 4/src/app/shared/services/document.service.ts @@ -13,7 +13,7 @@ import { MFile, MDocument, } from "../objects/archivo-types"; -import { Grupo } from "../objects/grupo"; +import { Group } from "../objects/grupo"; import { SERVER } from "../config"; import { TranslateService } from "@ngx-translate/core"; @@ -114,10 +114,17 @@ export class DocumentService { }; } - createDocument(document: MDocument): Observable<HttpResponse<MDocument>> { + createDocument( + document: MDocument, + groupId?: number + ): Observable<HttpResponse<MDocument>> { + const params = { ...document }; + if (groupId) { + params["group_id"] = groupId; + } return this.http.post<MDocument>( CREATE_DOCUMENT, - { document }, + { document: params }, { observe: "response", headers: this.postHeaders(), @@ -262,14 +269,14 @@ export class DocumentService { } // Legacy method, need to check if it's still used - getGrupos(cedula: string): Observable<Grupo[]> { + getGrupos(cedula: string): Observable<Group[]> { let headers = this.getHeaders(); let params: HttpParams = new HttpParams(); params = params.set("cedula", cedula); let httpOptions = { headers: headers, params: params }; return this.http - .get<Grupo[]>(SERVER + "/servicios/grupo", httpOptions) + .get<Group[]>(SERVER + "/servicios/grupo", httpOptions) .pipe(catchError(this.handleError)); } diff --git a/Frontend Angular 4/src/app/shared/services/file.service.ts b/Frontend Angular 4/src/app/shared/services/file.service.ts index b900b573ddce9a147dfb7fcbae0657b107cfe20f..e67468b7ceaa65a788bb6adeb9c07a9ac6fdf2a4 100644 --- a/Frontend Angular 4/src/app/shared/services/file.service.ts +++ b/Frontend Angular 4/src/app/shared/services/file.service.ts @@ -8,7 +8,6 @@ import { HttpResponse, } from "@angular/common/http"; import { Archivo, Evaluacion, MFile } from "../objects/archivo-types"; -import { Grupo } from "../objects/grupo"; import { SERVER } from "../config"; import { TranslateService } from "@ngx-translate/core"; @@ -108,6 +107,8 @@ export class FileService { archivo.evaluacion = null; archivo.users = file.users; archivo.role = file.role; + archivo.groupId = file.group_id; + archivo.feedbackRequested = file.feedback_requested; if (!!file.children) { archivo.archivos = file.children.map<Archivo>((child) => { @@ -146,6 +147,8 @@ export class FileService { children: [], directory: archivo.directorio, role: archivo.role, + group_id: archivo.groupId, + feedback_requested: archivo.feedbackRequested, }; file.children = archivo.archivos.map<MFile>((archivoHijo) => { diff --git a/Frontend Angular 4/src/app/shared/services/group-document.service.ts b/Frontend Angular 4/src/app/shared/services/group-document.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..a76638cad6223e59cc51b3499a22a3d02739df8e --- /dev/null +++ b/Frontend Angular 4/src/app/shared/services/group-document.service.ts @@ -0,0 +1,99 @@ +import { throwError as observableThrowError, Observable } from "rxjs"; +import { Injectable } from "@angular/core"; +import { Router } from "@angular/router"; +import { + HttpClient, + HttpHeaders, + HttpParams, + HttpResponse, +} from "@angular/common/http"; +import { + Archivo, + Evaluacion, + MFile, + MDocument, +} from "../objects/archivo-types"; +import { Group } from "../objects/grupo"; + +import { SERVER } from "../config"; +import { TranslateService } from "@ngx-translate/core"; +import { AuthenticationService } from "./authentication.service"; +import { catchError } from "rxjs/operators"; +import { GET_GROUP_DOCUMENT } from "../config"; + +@Injectable() +export class GroupDocumentService { + translateService: any; + + /** + * Creates a new DocumentService with the injected HttpClient. + * @param {HttpClient} http - The injected HttpClient. + * @constructor + */ + constructor( + private http: HttpClient, + private router: Router, + private authService: AuthenticationService, + public translate: TranslateService + ) { + this.translateService = translate; + } + + private getHeaders() { + return new HttpHeaders({ + "Content-Type": "application/json", + Authorization: "Bearer " + this.authService.getToken(), + }); + } + + private postHeaders() { + return new HttpHeaders({ + "Content-Type": "application/json", + Accept: "application/json", + "Access-Control-Allow-Headers": "Content-Type", + }); + } + + getGroupDocument( + groupId: number, + id: number, + userId: number + ): Observable<{ document: MDocument }> { + let params = {}; + if (!!userId) { + params = new HttpParams().set("user_id", userId); + } + return this.http + .get<{ document: MDocument }>( + GET_GROUP_DOCUMENT.replace(/:group_id/g, String(groupId)).replace( + /:id/g, + String(id) + ), + { + headers: this.getHeaders(), + params: params, + } + ) + .pipe(catchError(this.handleError)); + } + + /** + * Handle HTTP error + */ + private handleError(error: any) { + if (error.status == 401) { + this.translateService + .get("i18n.code") + .subscribe((res) => this.router.navigate(["/" + res + "/login"])); + } + // In a real world app, we might use a remote logging infrastructure + // We'd also dig deeper into the error to get a better message + let errMsg = error.message + ? error.message + : error.status + ? `${error.status} - ${error.statusText}` + : "Server error"; + console.error(errMsg); // log to console instead + return observableThrowError(errMsg); + } +} diff --git a/Frontend Angular 4/src/app/shared/services/group-file.service.ts b/Frontend Angular 4/src/app/shared/services/group-file.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..e08db79a082739df50ad924a93a3d51bdd8f1961 --- /dev/null +++ b/Frontend Angular 4/src/app/shared/services/group-file.service.ts @@ -0,0 +1,284 @@ +import { throwError as observableThrowError, Observable } from "rxjs"; +import { Injectable } from "@angular/core"; +import { Router } from "@angular/router"; +import { + HttpClient, + HttpHeaders, + HttpParams, + HttpResponse, +} from "@angular/common/http"; +import { Archivo, Evaluacion, MFile } from "../objects/archivo-types"; + +import { TranslateService } from "@ngx-translate/core"; +import { AuthenticationService } from "./authentication.service"; +import { catchError } from "rxjs/operators"; +import { + CREATE_GROUP_FILE, + UPDATE_GROUP_FILE, + DELETE_GROUP_FILE, + GET_GROUP_FILE, + GET_GROUP_FILES, + GET_GROUP_FILES_LISTS, + GET_GROUP_FEEDBACK_REQUESTED_FILES, + ASSIGN_FILE, +} from "../config"; + +@Injectable() +export class GroupFileService { + translateService: any; + + /** + * Creates a new FileService with the injected HttpClient. + * @param {HttpClient} http - The injected HttpClient. + * @constructor + */ + constructor( + private http: HttpClient, + private router: Router, + private authService: AuthenticationService, + public translate: TranslateService + ) { + this.translateService = translate; + } + + private getHeaders() { + return new HttpHeaders({ + "Content-Type": "application/json", + Authorization: "Bearer " + this.authService.getToken(), + }); + } + + private postHeaders() { + return new HttpHeaders({ + "Content-Type": "application/json", + Accept: "application/json", + "Access-Control-Allow-Headers": "Content-Type", + }); + } + + getGroupFile(groupId: number, id: number): Observable<{ file: MFile }> { + return this.http + .get<{ file: MFile }>( + GET_GROUP_FILE.replace(/:group_id/g, String(groupId)).replace( + /:id/g, + String(id) + ), + { + headers: this.getHeaders(), + } + ) + .pipe(catchError(this.handleError)); + } + + getSpecificUserFileWithSetGroup(groupId: number, userId: number) { + return { + getFile: (id: number) => { + return this.getSpecificUserGroupFile(groupId, userId, id); + }, + }; + } + + getGroupFiles(groupId: number): Observable<MFile[]> { + return this.http + .get<MFile[]>(GET_GROUP_FILES.replace(/:group_id/g, String(groupId)), { + headers: this.getHeaders(), + }) + .pipe(catchError(this.handleError)); + } + + getSpecificUserGroupFiles( + groupId: number, + userId: number + ): Observable<MFile[]> { + return this.http + .get<MFile[]>(GET_GROUP_FILES.replace(/:group_id/g, String(groupId)), { + headers: this.getHeaders(), + params: new HttpParams().set("user_id", userId), + }) + .pipe(catchError(this.handleError)); + } + + getSpecificUserGroupFile( + groupId: number, + userId: number, + fileId: number + ): Observable<{ file: MFile }> { + return this.http + .get<{ file: MFile }>( + GET_GROUP_FILE.replace(/:group_id/g, String(groupId)).replace( + /:id/g, + String(fileId) + ), + { + headers: this.getHeaders(), + params: new HttpParams().set("user_id", userId), + } + ) + .pipe(catchError(this.handleError)); + } + + getGroupFilesList( + groupId: number, + options = {} + ): Observable<{ files: MFile[] }> { + const siblingsOfId = options["siblingsOfId"]; + return this.http + .get<{ files: MFile[] }>( + GET_GROUP_FILES_LISTS.replace(/:group_id/g, String(groupId)), + { + headers: this.getHeaders(), + params: siblingsOfId + ? new HttpParams().set("siblings_of_id", siblingsOfId) + : null, + } + ) + .pipe(catchError(this.handleError)); + } + + getFeedbackRequestedGroupFiles( + groupId: number, + userId?: number + ): Observable<MFile[]> { + let params = {}; + if (userId) { + params["user_id"] = userId; + } + return this.http + .get<MFile[]>( + GET_GROUP_FEEDBACK_REQUESTED_FILES.replace( + /:group_id/g, + String(groupId) + ), + { + headers: this.getHeaders(), + params: params, + } + ) + .pipe(catchError(this.handleError)); + } + + createGroupFile( + groupId: number, + file: MFile + ): Observable<HttpResponse<Object>> { + return this.http.post<Object>( + CREATE_GROUP_FILE.replace(/:group_id/g, String(groupId)), + { + file: { + title: file.title, + parent_id: file.parent_id, + directory: file.directory, + }, + }, + { + observe: "response", + headers: this.postHeaders(), + } + ); + } + + updateGroupFile( + groupId: number, + file_id, + file_parent_id, + users + ): Observable<HttpResponse<Object>> { + const update_params = {}; + if (file_parent_id) { + update_params["parent_id"] = file_parent_id; + } + if (users) { + users = users.map((user) => { + return { ...user, role: user.role.toLowerCase() }; + }); + update_params["users"] = users; + } + + return this.http.patch<Object>( + `${UPDATE_GROUP_FILE.replace(/:group_id/g, String(groupId))}/${file_id}`, + { file: update_params }, + { + observe: "response", + headers: this.postHeaders(), + } + ); + } + + requestFeedback( + groupId: number, + fileId: number + ): Observable<HttpResponse<Object>> { + return this.http.patch<Object>( + `${UPDATE_GROUP_FILE.replace(/:group_id/g, String(groupId))}/${fileId}`, + { file: { feedback_requested: true } }, + { + observe: "response", + headers: this.postHeaders(), + } + ); + } + + returnFeedback( + groupId: number, + fileId: number, + userId: number + ): Observable<HttpResponse<Object>> { + return this.http.patch<Object>( + `${UPDATE_GROUP_FILE.replace(/:group_id/g, String(groupId))}/${fileId}`, + { file: { feedback_requested: false }, user_id: userId }, + { + observe: "response", + headers: this.postHeaders(), + } + ); + } + + deleteGroupFile(groupId: number, file_id): Observable<HttpResponse<Object>> { + return this.http.delete<Object>( + `${DELETE_GROUP_FILE.replace(/:group_id/g, String(groupId))}/${file_id}`, + { + observe: "response", + headers: this.postHeaders(), + } + ); + } + + assignFile( + groupId: number, + fileId: number, + usersIds, + subgroupsIds + ): Observable<HttpResponse<Object>> { + return this.http.post<Object>( + `${ASSIGN_FILE.replace(/:group_id/g, String(groupId)).replace( + /:file_id/g, + String(fileId) + )}`, + { assignment: { user_ids: usersIds } }, + { + observe: "response", + headers: this.postHeaders(), + } + ); + } + + /** + * Handle HTTP error + */ + private handleError(error: any) { + if (error.status == 401) { + this.translateService + .get("i18n.code") + .subscribe((res) => this.router.navigate(["/" + res + "/login"])); + } + // In a real world app, we might use a remote logging infrastructure + // We'd also dig deeper into the error to get a better message + let errMsg = error.message + ? error.message + : error.status + ? `${error.status} - ${error.statusText}` + : "Server error"; + console.error(errMsg); // log to console instead + return observableThrowError(errMsg); + } +} diff --git a/Frontend Angular 4/src/app/shared/services/group.service.ts b/Frontend Angular 4/src/app/shared/services/group.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..94080d5303dcdc5cd907b2017ca84138ff9c9eee --- /dev/null +++ b/Frontend Angular 4/src/app/shared/services/group.service.ts @@ -0,0 +1,268 @@ +import { throwError as observableThrowError, Observable } from "rxjs"; +import { Injectable } from "@angular/core"; +import { Router } from "@angular/router"; +import { + HttpClient, + HttpHeaders, + HttpParams, + HttpResponse, +} from "@angular/common/http"; +import { Archivo, Evaluacion, MFile } from "../objects/archivo-types"; +import { Group } from "../objects/grupo"; + +import { TranslateService } from "@ngx-translate/core"; +import { AuthenticationService } from "./authentication.service"; +import { catchError } from "rxjs/operators"; +import { + CREATE_GROUP, + UPDATE_GROUP, + DELETE_GROUP, + GET_GROUP, + GET_GROUPS, + CREATE_GROUP_FILE_FEEDBACK_REQUEST, + GET_GROUP_USER_ROLE, +} from "../config"; + +@Injectable() +export class GroupService { + translateService: any; + + /** + * Creates a new GroupService with the injected HttpClient. + * @param {HttpClient} http - The injected HttpClient. + * @constructor + */ + constructor( + private http: HttpClient, + private router: Router, + private authService: AuthenticationService, + public translate: TranslateService + ) { + this.translateService = translate; + } + + private getHeaders() { + return new HttpHeaders({ + "Content-Type": "application/json", + Authorization: "Bearer " + this.authService.getToken(), + }); + } + + private postHeaders() { + return new HttpHeaders({ + "Content-Type": "application/json", + Accept: "application/json", + "Access-Control-Allow-Headers": "Content-Type", + }); + } + + getGroup(id: number): Observable<{ group: Group }> { + return this.http + .get<{ group: Group }>(GET_GROUP.replace(/:id/g, String(id)), { + headers: this.getHeaders(), + }) + .pipe(catchError(this.handleError)); + } + + getGroups(): Observable<{ groups: Group[] }> { + return this.http + .get<{ groups: Group[] }>(GET_GROUPS, { + headers: this.getHeaders(), + }) + .pipe(catchError(this.handleError)); + } + + fileToArchivo(file: MFile, parent: Archivo = null): Archivo { + let archivo = new Archivo(); + archivo; + archivo.id = file.id; + archivo.documentId = file.document_id; + archivo.nombre = file.title; + archivo.contenido = ""; + archivo.fechaCreacion = file.created_at; + archivo.cedulaCreador = file.users[0]?.email; + archivo.editable = true; + archivo.padreId = + file.ancestry === "/" + ? -1 + : parseInt(file.ancestry.slice(0, -1).split("/").pop()); + archivo.archivos = []; + archivo.parent = parent; + archivo.archivoOrigenId = null; + archivo.directorio = file.directory; + archivo.estado = "activo"; + archivo.eliminado = false; + archivo.evaluacion = null; + archivo.users = file.users; + archivo.role = file.role; + + if (!!file.children) { + archivo.archivos = file.children.map<Archivo>((child) => { + return this.fileToArchivo(child, archivo); + }); + } + + return archivo; + } + + createGroup(name: string): Observable<HttpResponse<Object>> { + return this.http.post<Object>( + CREATE_GROUP, + { + group: { + name: name, + }, + }, + { + observe: "response", + headers: this.postHeaders(), + } + ); + } + + archivoToFile(archivo: Archivo, parent: MFile = null): MFile { + let file = { + id: archivo.id, + document_id: archivo.documentId, + title: archivo.nombre, + created_at: archivo.fechaCreacion, + parent_id: archivo.padreId, + parent: parent, + children: [], + directory: archivo.directorio, + role: archivo.role, + }; + + file.children = archivo.archivos.map<MFile>((archivoHijo) => { + return this.archivoToFile(archivoHijo, file); + }); + + return file; + } + + completeFiles(file: MFile, parent: MFile = null): MFile { + file.parent = parent; + file.children.forEach((child) => { + this.completeFiles(child, file); + }); + + return file; + } + + filterOnlyDirectories(file: MFile, parent: MFile = null): MFile { + let newFile = { + id: file.id, + document_id: file.document_id, + title: file.title, + created_at: file.created_at, + parent_id: file.parent_id, + parent: parent, + children: [], + directory: file.directory, + }; + + newFile.children = file.children + .filter((child) => child.directory) + .map((child) => this.filterOnlyDirectories(child, newFile)); + + return newFile; + } + + updateGroup(group_id, users): Observable<HttpResponse<Object>> { + const update_params = {}; + if (users) { + users = users.map((user) => { + return { ...user, role: user.role.toLowerCase() }; + }); + update_params["users"] = users; + } + + return this.http.patch<Object>( + `${UPDATE_GROUP}/${group_id}`, + { group: update_params }, + { + observe: "response", + headers: this.postHeaders(), + } + ); + } + + destroyGroup(group_id): Observable<HttpResponse<Object>> { + return this.http.delete<Object>(`${DELETE_GROUP}/${group_id}`, { + observe: "response", + headers: this.postHeaders(), + }); + } + + // destroyAuxFile(): void { + // this.http.delete<Object>(DESTROY_AUX_DOCUMENT, { + // headers: this.postHeaders(), + // }); + // } + + requestFeedback( + groupId: number, + fileId: number + ): Observable<HttpResponse<Object>> { + return this.http.patch<Object>( + `${CREATE_GROUP_FILE_FEEDBACK_REQUEST.replace( + /:group_id/g, + String(groupId) + )}`, + { feedback_request: { file_id: fileId } }, + { + observe: "response", + headers: this.postHeaders(), + } + ); + } + + getUserRole( + groupId: number, + userId: number + ): Observable<{ role: { role: string } }> { + return this.http + .get<{ role: { role: string } }>( + GET_GROUP_USER_ROLE.replace(/:group_id/g, String(groupId)).replace( + /:user_id/g, + String(userId) + ), + { + headers: this.getHeaders(), + } + ) + .pipe(catchError(this.handleError)); + } + + userCanEditAnyGroupFile(role: string): boolean { + return role === "owner" || role === "moderator" || role === "manager"; + } + + userCanReturnFeedback(role: string): boolean { + return this.userCanEditAnyGroupFile(role); + } + + userCanAssignFiles(role: string): boolean { + return this.userCanEditAnyGroupFile(role); + } + + /** + * Handle HTTP error + */ + private handleError(error: any) { + if (error.status == 401) { + this.translateService + .get("i18n.code") + .subscribe((res) => this.router.navigate(["/" + res + "/login"])); + } + // In a real world app, we might use a remote logging infrastructure + // We'd also dig deeper into the error to get a better message + let errMsg = error.message + ? error.message + : error.status + ? `${error.status} - ${error.statusText}` + : "Server error"; + console.error(errMsg); // log to console instead + return observableThrowError(errMsg); + } +} diff --git a/Frontend Angular 4/src/app/shared/services/haskell.service.ts b/Frontend Angular 4/src/app/shared/services/haskell.service.ts index 5702c47ba90041a8411762f8fcfb7410f2927a29..7d086cabe4c056d24bbf7bd7d545ecec83cf511d 100755 --- a/Frontend Angular 4/src/app/shared/services/haskell.service.ts +++ b/Frontend Angular 4/src/app/shared/services/haskell.service.ts @@ -3,7 +3,7 @@ import { Injectable } from "@angular/core"; import { Router } from "@angular/router"; import { HttpClient, HttpHeaders, HttpParams } from "@angular/common/http"; import { Archivo, Evaluacion } from "../objects/archivo-types"; -import { Grupo } from "../objects/grupo"; +import { Group } from "../objects/grupo"; import { SERVER } from "../config"; import { TranslateService } from "@ngx-translate/core"; @@ -145,14 +145,14 @@ export class HaskellService { .pipe(catchError(this.handleError)); } - getGrupos(cedula: string): Observable<Grupo[]> { + getGrupos(cedula: string): Observable<Group[]> { let headers = this.getHeaders(); let params: HttpParams = new HttpParams(); params = params.set("cedula", cedula); let httpOptions = { headers: headers, params: params }; return this.http - .get<Grupo[]>(SERVER + "/servicios/grupo", httpOptions) + .get<Group[]>(SERVER + "/servicios/grupo", httpOptions) .pipe(catchError(this.handleError)); } diff --git a/Frontend Angular 4/src/app/shared/services/session.service.ts b/Frontend Angular 4/src/app/shared/services/session.service.ts index 697c1a7b1678f348e10b1e33464ce0bffe8e8038..4ce6bfbe820c37ca1d024652e539f2ce87a2ee56 100755 --- a/Frontend Angular 4/src/app/shared/services/session.service.ts +++ b/Frontend Angular 4/src/app/shared/services/session.service.ts @@ -1,6 +1,6 @@ import { Injectable } from "@angular/core"; import { Archivo, MFile, MDocument } from "../objects/archivo-types"; -import { Grupo } from "../objects/grupo"; +import { Group } from "../objects/grupo"; @Injectable() export class SessionService { @@ -11,7 +11,7 @@ export class SessionService { directorioActual: any; archivosCompartidos: any; file: MFile; - grupos: Grupo[]; + grupos: Group[]; currentDirectoryDocuments: MDocument[]; public setArchivo(archivo) { @@ -40,7 +40,7 @@ export class SessionService { return this.file; } - public setGrupos(grupos: Grupo[]) { + public setGrupos(grupos: Group[]) { this.grupos = grupos; } diff --git a/Frontend Angular 4/src/app/shared/shared.module.ts b/Frontend Angular 4/src/app/shared/shared.module.ts index 73c823e535a89d4010fac0385cded1ebd07c59d7..bf77a6d71358c0605253f44a46192c45ad171555 100644 --- a/Frontend Angular 4/src/app/shared/shared.module.ts +++ b/Frontend Angular 4/src/app/shared/shared.module.ts @@ -3,19 +3,29 @@ import { MainButtonComponent } from "./components/main-button/main-button.compon import { TitleCaseModule } from "../shared/modules/titlecase.module"; import { I18nModule } from "../shared/modules/translate/i18n.module"; import { CommonModule } from "@angular/common"; -import { CreateFileModal } from "./components"; -import { ImportFileModal } from "./components"; -import { ShareFileModal } from "./components"; import { FormsModule } from "@angular/forms"; -import { FolderInput } from "./components"; -import { SelectComponent } from "./components"; -import { ChipInputComponent } from "./components"; import { MatFormFieldModule } from "@angular/material/form-field"; import { MatChipsModule } from "@angular/material/chips"; +import { MatCheckboxModule } from "@angular/material/checkbox"; import { MatIconModule } from "@angular/material/icon"; import { NgbDropdownModule } from "@ng-bootstrap/ng-bootstrap"; -import { AddUsersModal } from "./components"; - +import { + ActionConfirmationModal, + AddUsersModal, + ChipInputComponent, + CreateFileModal, + CreateGroupModal, + DestroyGroupModal, + FolderInput, + ImportFileModal, + ManageGroupUsersModal, + RequestFeedbackModal, + ReturnFeedbackModal, + SelectComponent, + ShareFileModal, + AssignFileModal, + CheckboxSelect, +} from "./components"; @NgModule({ imports: [ TitleCaseModule, @@ -24,18 +34,27 @@ import { AddUsersModal } from "./components"; FormsModule, MatFormFieldModule, MatChipsModule, + MatCheckboxModule, MatIconModule, NgbDropdownModule, ], declarations: [ MainButtonComponent, CreateFileModal, + CreateGroupModal, FolderInput, ImportFileModal, ShareFileModal, + ManageGroupUsersModal, ChipInputComponent, SelectComponent, AddUsersModal, + ActionConfirmationModal, + DestroyGroupModal, + RequestFeedbackModal, + ReturnFeedbackModal, + AssignFileModal, + CheckboxSelect, ], exports: [MainButtonComponent, CreateFileModal], }) diff --git a/Frontend Angular 4/src/app/utils.ts b/Frontend Angular 4/src/app/utils.ts index 35a774c95b5b672d81f2af0339ce32fce0e99512..f66570fd54dde6b1c2dbd8d50540dde5bc00daab 100644 --- a/Frontend Angular 4/src/app/utils.ts +++ b/Frontend Angular 4/src/app/utils.ts @@ -41,3 +41,11 @@ export function findInTree<T extends MFile | Archivo>( export function roleCanShareFile(role: string): boolean { return role === "owner" || role === "manager"; } + +export function roleCanManageGroupUsers(role: string): boolean { + return role === "owner" || role === "manager"; +} + +export function roleCanDestroyGroup(role: string): boolean { + return role === "owner"; +} diff --git a/Frontend Angular 4/src/assets/i18n/en.json b/Frontend Angular 4/src/assets/i18n/en.json index 75eebe99dd64ca40081fb00774e733c03c7833a8..4f83be6543748af4c0c30382df7fcd0f8394d1f3 100755 --- a/Frontend Angular 4/src/assets/i18n/en.json +++ b/Frontend Angular 4/src/assets/i18n/en.json @@ -33,7 +33,10 @@ "close": "close", "qualify": "qualify", "confirm": "confirm", - "signup": "sign up" + "signup": "sign up", + "requestFeedback": "Request feedback", + "returnFeedback": "Return feedback", + "assignFile": "Assign file" }, "links": { "register": "Create account", @@ -134,6 +137,10 @@ "success": "Please check your email", "error": "An error occurred while sending the email" }, + "group": { + "manageGroupUsers": "Manage group users", + "destroyGroupTooltip": "Destroy Group" + }, "updatePassword": { "success": "Your password has been updated, you will be redirected shortly", "error": "An error occurred while updating your password", @@ -159,7 +166,9 @@ "invalidName": "Invalid file name." }, "group": { - "select": "Select a group" + "select": "Select a group", + "capitalLetter": "Group name must start with upper case.", + "invalidName": "Invalid group name." } }, "shortcuts": { diff --git a/Frontend Angular 4/src/assets/i18n/es.json b/Frontend Angular 4/src/assets/i18n/es.json index 55e091b35a0ef601f9dc6ce14d311d6e11884bf2..e7f10863633287a43f8708af28243b4811935495 100755 --- a/Frontend Angular 4/src/assets/i18n/es.json +++ b/Frontend Angular 4/src/assets/i18n/es.json @@ -33,7 +33,10 @@ "close": "cerrar", "qualify": "calificar", "confirm": "confirmar", - "signup": "registrarse" + "signup": "registrarse", + "requestFeedback": "Solicitar retroalimentación", + "returnFeedback": "Devolver retroalimentación", + "assignFile": "Asignar archivo" }, "links": { "register": "Registrarse", @@ -134,6 +137,10 @@ "success": "Favor busque en su correo el email", "error": "Error al enviar el email" }, + "group": { + "manageGroupUsers": "Administrar usuarios del grupo", + "destroyGroupTooltip": "Destruir Grupo" + }, "updatePassword": { "success": "Su contraseña ha sido actualizada, será redirigido en 5 segundos", "error": "Ha ocurrido un error al actualizar su contraseña", @@ -159,7 +166,9 @@ "invalidName": "Nombre de archivo inválido." }, "group": { - "select": "Seleccione un grupo" + "select": "Seleccione un grupo", + "capitalLetter": "Nombre del grupo debe iniciar con mayúscula.", + "invalidName": "Nombre de grupo inválido." } }, "shortcuts": { diff --git a/Frontend Angular 4/src/styles/console.css b/Frontend Angular 4/src/styles/console.css index 4a44020d73acfef4efbe3d6f7c30a74e7da9ad84..6441af37415a27b6a41e2ccdf74d9c43630779a4 100755 --- a/Frontend Angular 4/src/styles/console.css +++ b/Frontend Angular 4/src/styles/console.css @@ -70,7 +70,7 @@ .nomArchivoInp { /*width: 55% !important;*/ - width: calc(100% - 315px); + width: calc(100% - 345px); float: left; }