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 9c374cb2cfce899f4c636960c79925addbcb49c4..eee2b4dd5b170b2faa87f01e6ecf47d70334d463 100755 --- a/Frontend Angular 4/src/app/layout/grupos/grupos.component.html +++ b/Frontend Angular 4/src/app/layout/grupos/grupos.component.html @@ -60,7 +60,7 @@ [destroyOnHide]="false" > <li [ngbNavItem]="1"> - <a ngbNavLink>Alumnos</a> + <a ngbNavLink>Users</a> <ng-template ngbNavContent> <div class="card"> <div> @@ -90,7 +90,6 @@ <i class="fa fa-users"></i> </button> <button - *ngIf="roleCanDestroyGroup()" class="btn btn-sm btn-secondary pull-right" style="cursor: pointer; margin-top: -35px; margin-right: 71px" (click)="openDestroyGroupModal()" @@ -136,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 }}" @@ -149,9 +148,31 @@ > <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.name }} </p> @@ -162,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> @@ -182,14 +210,14 @@ <div [ngbNavOutlet]="nav" class="mt-2" *ngIf="grupoSeleccionado"></div> </div> <div class="col-lg-7"> - <div class="card" *ngIf="selectedUser"> + <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 selectedUser.archivos" + *ngFor="let entrega of selectedUserDirectorioActual" (click)="seleccionarEntrega(entrega)" class="col-sm-3 col-4 matefun-file-wrapper" > @@ -201,7 +229,7 @@ <p>{{ entrega.nombre }}</p> </div> <div - *ngIf="selectedUser.archivos.length == 0" + *ngIf="selectedUser && selectedUserDirectorioActual.length == 0" style="width: 100%; text-align: center" > <i @@ -253,14 +281,46 @@ 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 + 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> <div class="pull-left"> Nombre: {{ archivoSeleccionado?.nombre }} - Creado: @@ -296,3 +356,45 @@ *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> 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 d4802809c148c09f6fd8e43a423077e1fa6824c2..6e9b47134467b6e4e25d4e11082b52f8ddee7965 100755 --- a/Frontend Angular 4/src/app/layout/grupos/grupos.component.ts +++ b/Frontend Angular 4/src/app/layout/grupos/grupos.component.ts @@ -1,7 +1,11 @@ import { ChangeDetectorRef, Component } from "@angular/core"; import { Router } from "@angular/router"; -import { Archivo, Evaluacion } from "../../shared/objects/archivo-types"; +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"; @@ -11,11 +15,23 @@ 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 { roleCanManageGroupUsers, roleCanDestroyGroup } from "app/utils"; +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 } from "app/shared"; +import { DestroyGroupModal, RequestFeedbackModal } from "app/shared"; + +const STARTS_WITH_CAPITAL_LETTER_REGEX = /^[A-Z]/; + +const ID_ROOT_DIR = -1; @Component({ selector: "grupos", @@ -23,12 +39,14 @@ import { DestroyGroupModal } from "app/shared"; }) export class GruposComponent { archivos: Archivo[] = []; + selectedUserArchivos: Archivo[] = []; grupos: Group[] = []; grupoSeleccionado: Group = undefined; selectedUser: Usuario = undefined; archivoSeleccionado: Archivo = undefined; + selectedFile: Archivo = undefined; tipoArchivo: string = undefined; @@ -39,8 +57,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. @@ -68,6 +90,51 @@ 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; + constructor( private router: Router, private authService: AuthenticationService, @@ -76,6 +143,10 @@ export class GruposComponent { private sessionService: SessionService, public translate: TranslateService, public groupService: GroupService, + public groupFileService: GroupFileService, + public documentService: DocumentService, + public groupDocumentService: GroupDocumentService, + public fileService: FileService, private modalService: NgbModal, private changeDetectorRef: ChangeDetectorRef ) { @@ -144,9 +215,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.) @@ -183,37 +257,181 @@ export class GruposComponent { // ); } + /** + * 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.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.selectedUser = undefined; } - seleccionarUser(alumno) { - if (!(typeof alumno === "undefined")) { - this.selectedUser = alumno; - this.ordenarArchivosAlumno(); + 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; + }, + (error) => console.log(error) + ); + } + + canRequestFeedback() { + return ( + !!this.grupoSeleccionado.id && + !!this.selectedFile && + this.selectedFile.feedbackRequested !== undefined && + !this.selectedFile.feedbackRequested + ); + } + + requestFeedback() { + this.openRequestFeedbackModal(); + } + + seleccionarArchivo(archivo) { + this.selectedFile = archivo; + if (archivo.directorio) { + this.directorioActual = archivo; + // this.loadFilesAndFolders(this.grupoSeleccionado.id, archivo.id); this.archivoSeleccionado = undefined; + // this.selectedUser = undefined; + 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) + ); } } - seleccionarArchivo(archivo) { + /** + * Actualiza el estado del archivo seleccionado, así como la preview del + * contenido de dicho archivo. + * @param archivo Archivo a seleccionar + */ + actualizarArchivoSeleccionado(archivo: Archivo) { this.archivoSeleccionado = archivo; - this.selectedUser = undefined; - this.tipoArchivo = "compartido"; + 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.selectedUser = 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); + this.actualizarArchivoSeleccionado(archivoDocument); + }, + (error) => console.log(error) + ); + // this.selectedUser = undefined; this.tipoArchivo = "entrega"; } @@ -231,14 +449,35 @@ export class GruposComponent { ); } - openDestroyGroupModal() { - if (!this.grupoSeleccionado) { + openRequestFeedbackModal() { + if (!this.grupoSeleccionado.id) { this.showNotification("warning", "i18n.warning.group.noSelected"); return; } - if (!roleCanDestroyGroup(this.grupoSeleccionado.role)) { - this.showNotification("error", "i18n.warning.group.noPermission"); + 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() {} + + openDestroyGroupModal() { + if (!this.grupoSeleccionado) { + this.showNotification("warning", "i18n.warning.group.noSelected"); return; } @@ -249,6 +488,8 @@ export class GruposComponent { 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", { @@ -382,14 +623,131 @@ export class GruposComponent { // } 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 3e4f59afcb81f54694c11933b9e2fe1d143290a2..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,8 @@ 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"; @@ -35,6 +37,8 @@ import { TitleCaseModule } from "../shared/modules/titlecase.module"; HaskellService, DocumentService, FileService, + GroupDocumentService, + GroupFileService, GroupService, ], }) 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..20238240c3a020e9a8f775769d5ef343483f03c6 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,83 @@ export class MateFunComponent implements OnInit, OnChanges { }); } + canRequestFeedback() { + // TODO: Quiero que un moderator pueda pedir feedback de un archivo que no es suyo? + 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 +590,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(); @@ -588,38 +744,42 @@ export class MateFunComponent implements OnInit, OnChanges { 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((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() { @@ -767,15 +927,35 @@ export class MateFunComponent implements OnInit, OnChanges { 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; - this.editor.setOption("readOnly", readOnly); + 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", + 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 +982,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/destroy-group-modal/destroy-group-modal.component.ts b/Frontend Angular 4/src/app/shared/components/destroy-group-modal/destroy-group-modal.component.ts index 62e1962aebeea8aecfe4f8067f3f78784cc48798..f4b4582fbb8c65df9b93ed204a3030dd4f8ed79a 100644 --- 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 @@ -17,11 +17,12 @@ import { GroupService } from "../../services/group.service"; 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 = "Destruir Grupo"; + title: string; message: string; constructor( @@ -32,7 +33,13 @@ export class DestroyGroupModal implements OnInit { ngOnInit(): void { this.groupName = `${this.groupName}`; - this.message = `¿Estás seguro de que deseas destruir el grupo ${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); } diff --git a/Frontend Angular 4/src/app/shared/components/index.ts b/Frontend Angular 4/src/app/shared/components/index.ts index d7aadd0fa72ba8c689acfccbb9d9728da622c91e..ebf34903707e3fb4f16ff194ba22aa18a7a7c733 100755 --- a/Frontend Angular 4/src/app/shared/components/index.ts +++ b/Frontend Angular 4/src/app/shared/components/index.ts @@ -12,3 +12,5 @@ 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"; 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..ee43f0938544d359f620f2166ab362a053af9a33 --- /dev/null +++ b/Frontend Angular 4/src/app/shared/components/request-feedback-modal/request-feedback-modal.component.ts @@ -0,0 +1,65 @@ +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 { + // TODO: Hacer todo este modal basándome en lo que ya está que viene del DestroyGroupModal, + // y luego terminar de hacer la llamada y todo en el GroupComponent + @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..550dbe3c6e5716b2ae73d597cb24d78362f224b1 --- /dev/null +++ b/Frontend Angular 4/src/app/shared/components/return-feedback-modal/return-feedback-modal.component.ts @@ -0,0 +1,68 @@ +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 { + // TODO: Hacer todo este modal basándome en lo que ya está que viene del DestroyGroupModal, + // y luego terminar de hacer la llamada y todo en el GroupComponent + @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 1a9848d10b0b3ef51f1f2beddd77ddb3fb1a18bf..953ce88817c005602e655c4bbc6ae5bbebc78082 100755 --- a/Frontend Angular 4/src/app/shared/config.ts +++ b/Frontend Angular 4/src/app/shared/config.ts @@ -56,3 +56,19 @@ 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"; 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..111b577baf3a0f16921894b8ec93f07c77cc21eb 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() {} } 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 9883de73421cd71259c668c9433b8d0ad9508c65..4978249cbead78eb5e44ea96f47142d61e193f52 100644 --- a/Frontend Angular 4/src/app/shared/services/document.service.ts +++ b/Frontend Angular 4/src/app/shared/services/document.service.ts @@ -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(), 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 f1d56c3f52e1cac2da5e7877e38cf5eed2724a31..e67468b7ceaa65a788bb6adeb9c07a9ac6fdf2a4 100644 --- a/Frontend Angular 4/src/app/shared/services/file.service.ts +++ b/Frontend Angular 4/src/app/shared/services/file.service.ts @@ -107,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) => { @@ -145,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..c4aca2791df5c2b99d9359c41a6246348ffbd492 --- /dev/null +++ b/Frontend Angular 4/src/app/shared/services/group-document.service.ts @@ -0,0 +1,307 @@ +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", + }); + } + + // getDocuments(ids: number[]): Observable<{ documents: MDocument[] }> { + // const params = new HttpParams({ + // fromObject: { "document_ids[]": ids }, + // }); + // return this.http + // .get<{ documents: MDocument[] }>(GET_DOCUMENTS, { + // headers: this.getHeaders(), + // params: params, + // }) + // .pipe(catchError(this.handleError)); + // } + + getGroupDocument( + groupId: number, + id: number, + userId: number + ): Observable<{ document: MDocument }> { + return this.http + .get<{ document: MDocument }>( + GET_GROUP_DOCUMENT.replace(/:group_id/g, String(groupId)).replace( + /:id/g, + String(id) + ), + { + headers: this.getHeaders(), + params: new HttpParams().set("user_id", userId), + } + ) + .pipe(catchError(this.handleError)); + } + + // documentToArchivo(document: MDocument): Archivo { + // return { + // id: document.id, + // documentId: document.id, + // nombre: document.title, + // contenido: document.text_doc, + // fechaCreacion: document.created_at, + // cedulaCreador: document.users[0]?.email, + // editable: true, + // padreId: document.parent_id ? document.parent_id : -1, + // fileId: document.file_id, + // archivoOrigenId: null, + // archivos: [], + // directorio: false, + // estado: "activo", + // eliminado: false, + // evaluacion: null, + // users: document.users, + // role: document.role, + // }; + // } + + // archivoToDocument(archivo: Archivo): MDocument { + // return { + // id: archivo.id, + // title: archivo.nombre, + // text_doc: archivo.contenido, + // created_at: archivo.fechaCreacion, + // parent_id: archivo.padreId, + // role: archivo.role, + // users: [], + // }; + // } + + // 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: params }, + // { + // observe: "response", + // headers: this.postHeaders(), + // } + // ); + // } + + // // Legacy method, need to check if it's still used + // getArchivos(cedula: string): Observable<Archivo[]> { + // let headers = this.getHeaders(); + // let params: HttpParams = new HttpParams(); + // params = params.set("cedula", cedula); + // let httpOptions = { headers: headers, params: params }; + + // return this.http + // .get<Archivo[]>(SERVER + "/servicios/archivo", httpOptions) + // .pipe(catchError(this.handleError)); + // } + + // // Legacy method, need to check if it's still used + // getArchivosCompartidosAlumno(cedula: string): Observable<Archivo[]> { + // let headers = this.getHeaders(); + // let params: HttpParams = new HttpParams(); + // params = params.set("cedula", cedula); + // params = params.set("compartidos", "true"); + // let httpOptions = { headers: headers, params: params }; + + // return this.http + // .get<Archivo[]>(SERVER + "/servicios/archivo", httpOptions) + // .pipe(catchError(this.handleError)); + // } + + // createFile( + // file_title, + // file_binary_doc_text + // ): Observable<HttpResponse<Object>> { + // return this.http.post<Object>( + // CREATE_DOCUMENT, + // { + // document: { title: file_title, text_doc: file_binary_doc_text }, + // }, + // { + // observe: "response", + // headers: this.postHeaders(), + // } + // ); + // } + + // updateDocument( + // file_id, + // file_title, + // save_document_content = false + // ): Observable<HttpResponse<Object>> { + // return this.http.patch<Object>( + // `${UPDATE_DOCUMENT}/${file_id}`, + // { + // document: { + // title: file_title, + // save_document_content: save_document_content, + // }, + // }, + // { + // observe: "response", + // headers: this.postHeaders(), + // } + // ); + // } + + // destroyAuxDocument(): Observable<HttpResponse<Object>> { + // return this.http.delete<Object>(DESTROY_AUX_DOCUMENT, { + // observe: "response", + // headers: this.postHeaders(), + // }); + // } + + // // Legacy method, need to check if it's still used + // eliminarArchivo(archivoId): Observable<Response> { + // let headers = this.getHeaders(); + // let httpOptions = { headers: headers }; + + // return this.http + // .delete<Response>(SERVER + "/servicios/archivo/" + archivoId, httpOptions) + // .pipe(catchError(this.handleError)); + // } + + // // Legacy method, need to check if it's still used + // getCopiaArchivoCompartidoGrupo(cedula, archivoId): Observable<Archivo> { + // let headers = this.getHeaders(); + // let params: HttpParams = new HttpParams(); + // params = params.set("cedula", cedula); + // let httpOptions = { headers: headers, params: params }; + + // return this.http + // .get<Archivo>( + // SERVER + "/servicios/archivo/compartido/" + archivoId, + // httpOptions + // ) + // .pipe(catchError(this.handleError)); + // } + + // // Legacy method, need to check if it's still used + // compartirArchivoGrupo(grupo, archivoId): Observable<Archivo> { + // let headers = this.getHeaders(); + // let httpOptions = { headers: headers }; + // var archId = { + // id: archivoId, + // }; + // return this.http + // .post<Archivo>( + // SERVER + + // "/servicios/grupo/" + + // grupo.liceoId + + // "/" + + // grupo.anio + + // "/" + + // grupo.grado + + // "/" + + // grupo.grupo + + // "/archivo", + // archId, + // httpOptions + // ) + // .pipe(catchError(this.handleError)); + // } + + // // Legacy method, need to check if it's still used + // calificarArchivo(archivoId, estado, evaluacion): Observable<Evaluacion> { + // let headers = this.getHeaders(); + // let httpOptions = { headers: headers }; + // return this.http + // .post<Evaluacion>( + // SERVER + + // "/servicios/archivo/" + + // archivoId + + // "/" + + // estado + + // "/evaluacion", + // evaluacion, + // httpOptions + // ) + // .pipe(catchError(this.handleError)); + // } + + // // Legacy method, need to check if it's still used + // 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<Group[]>(SERVER + "/servicios/grupo", httpOptions) + // .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..210aef59375a912b97975a9a60ed058a71d466a4 --- /dev/null +++ b/Frontend Angular 4/src/app/shared/services/group-file.service.ts @@ -0,0 +1,264 @@ +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, +} 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(), + } + ); + } + + /** + * 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 index f1335f842dc8c7ead62c6330e59e948272de55fe..bf3e8d87add435c9e1707ba0deef7919be7fb965 100644 --- a/Frontend Angular 4/src/app/shared/services/group.service.ts +++ b/Frontend Angular 4/src/app/shared/services/group.service.ts @@ -19,6 +19,8 @@ import { DELETE_GROUP, GET_GROUP, GET_GROUPS, + CREATE_GROUP_FILE_FEEDBACK_REQUEST, + GET_GROUP_USER_ROLE, } from "../config"; @Injectable() @@ -210,6 +212,48 @@ export class GroupService { // }); // } + 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); + } + /** * Handle HTTP error */ diff --git a/Frontend Angular 4/src/app/shared/shared.module.ts b/Frontend Angular 4/src/app/shared/shared.module.ts index 06214deb85b5d6f3b19cd45883bbfa2bbf2582ec..8012c632e46723e9ef40e341cb6eb6fc6f19c08f 100644 --- a/Frontend Angular 4/src/app/shared/shared.module.ts +++ b/Frontend Angular 4/src/app/shared/shared.module.ts @@ -3,22 +3,26 @@ 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 { CreateGroupModal } from "./components"; -import { ImportFileModal } from "./components"; -import { ShareFileModal } from "./components"; -import { ManageGroupUsersModal } 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 { MatIconModule } from "@angular/material/icon"; import { NgbDropdownModule } from "@ng-bootstrap/ng-bootstrap"; -import { AddUsersModal } from "./components"; -import { ActionConfirmationModal } from "./components"; -import { DestroyGroupModal } from "./components"; +import { + ActionConfirmationModal, + AddUsersModal, + ChipInputComponent, + CreateFileModal, + CreateGroupModal, + DestroyGroupModal, + FolderInput, + ImportFileModal, + ManageGroupUsersModal, + RequestFeedbackModal, + ReturnFeedbackModal, + SelectComponent, + ShareFileModal, +} from "./components"; @NgModule({ imports: [ TitleCaseModule, @@ -43,6 +47,8 @@ import { DestroyGroupModal } from "./components"; AddUsersModal, ActionConfirmationModal, DestroyGroupModal, + RequestFeedbackModal, + ReturnFeedbackModal, ], exports: [MainButtonComponent, CreateFileModal], }) diff --git a/Frontend Angular 4/src/assets/i18n/en.json b/Frontend Angular 4/src/assets/i18n/en.json index 246f22045bac4c673a3075c574829b78305c8cfa..8b9a351bd282c48a5c02a31c580df3b5460da1b8 100755 --- a/Frontend Angular 4/src/assets/i18n/en.json +++ b/Frontend Angular 4/src/assets/i18n/en.json @@ -33,7 +33,8 @@ "close": "close", "qualify": "qualify", "confirm": "confirm", - "signup": "sign up" + "signup": "sign up", + "requestFeedback": "Request feedback" }, "links": { "register": "Create account", diff --git a/Frontend Angular 4/src/assets/i18n/es.json b/Frontend Angular 4/src/assets/i18n/es.json index 44ac256f4b2bfbc7da3ed5d38419d1c1f36e4e70..440668e93b2cc57517e4e8f8d3255d292a0f3f07 100755 --- a/Frontend Angular 4/src/assets/i18n/es.json +++ b/Frontend Angular 4/src/assets/i18n/es.json @@ -33,7 +33,8 @@ "close": "cerrar", "qualify": "calificar", "confirm": "confirmar", - "signup": "registrarse" + "signup": "registrarse", + "requestFeedback": "Solicitar retroalimentación" }, "links": { "register": "Registrarse", 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; }