diff --git a/rest/index.php b/rest/index.php index e2185efab130f107448fb50610a602487d31e5b6..707ea8d28dbc02adf5aad94037d0b64876830d63 100755 --- a/rest/index.php +++ b/rest/index.php @@ -359,8 +359,10 @@ $app->get('/categories', \Resource\controllers\ResController::class . ':getCateg $app->get('/resources/{resId}/users/{userId}/isDestinationChanging', \Action\controllers\PreProcessActionController::class . ':isDestinationChanging'); $app->get('/resources/{resId}/users/{userId}/groups/{groupId}/baskets/{basketId}/processingData', \Resource\controllers\ResController::class . ':getProcessingData'); $app->post('/resources/{resId}/follow', \Resource\controllers\UserFollowedResourceController::class . ':follow'); -$app->delete('/resources/{resId}/unfollow', \Resource\controllers\UserFollowedResourceController::class . ':unFollow'); +$app->delete('/resources/unfollow', \Resource\controllers\UserFollowedResourceController::class . ':unFollow'); $app->get('/followedResources', \Resource\controllers\UserFollowedResourceController::class . ':getFollowedResources'); +$app->get('/followedResources/{resId}/baskets', \Resource\controllers\UserFollowedResourceController::class . ':getBasketsFromFolder'); +$app->get('/followedResources/filters', \Resource\controllers\UserFollowedResourceController::class . ':getFilters'); //ResourcesList $app->get('/resourcesList/users/{userId}/groups/{groupId}/baskets/{basketId}', \Resource\controllers\ResourceListController::class . ':get'); diff --git a/src/app/resource/controllers/UserFollowedResourceController.php b/src/app/resource/controllers/UserFollowedResourceController.php index 10ab9655575d7978489c215a38290b03df456a69..08fa70606dcff83df31a3c34156e6c107e0554ac 100644 --- a/src/app/resource/controllers/UserFollowedResourceController.php +++ b/src/app/resource/controllers/UserFollowedResourceController.php @@ -16,11 +16,15 @@ namespace Resource\controllers; use Attachment\models\AttachmentModel; +use Basket\models\BasketModel; use Group\controllers\PrivilegeController; +use Resource\models\ResModel; use Resource\models\ResourceListModel; use Resource\models\UserFollowedResourceModel; +use Respect\Validation\Validator; use Slim\Http\Request; use Slim\Http\Response; +use SrcCore\controllers\PreparedClauseController; class UserFollowedResourceController { @@ -47,25 +51,30 @@ class UserFollowedResourceController return $response->withStatus(204); } - public function unFollow(Request $request, Response $response, array $args) + public function unFollow(Request $request, Response $response) { - if (!ResController::hasRightByResId(['resId' => [$args['resId']], 'userId' => $GLOBALS['id']])){ + $body = $request->getParsedBody(); + + if (!ResController::hasRightByResId(['resId' => $body['resources'], 'userId' => $GLOBALS['id']])){ return $response->withStatus(403)->withJson(['errors' => 'Document out of perimeter']); } - $following = UserFollowedResourceModel::get([ - 'where' => ['user_id = ?', 'res_id = ?'], - 'data' => [$GLOBALS['id'], $args['resId']] - ]); + foreach ($body['resources'] as $resId) { + $following = UserFollowedResourceModel::get([ + 'where' => ['user_id = ?', 'res_id = ?'], + 'data' => [$GLOBALS['id'], $resId] + ]); - if (empty($following)) { - return $response->withStatus(204); + if (empty($following)) { + continue; + } + + UserFollowedResourceModel::delete([ + 'userId' => $GLOBALS['id'], + 'resId' => $resId + ]); } - UserFollowedResourceModel::delete([ - 'userId' => $GLOBALS['id'], - 'resId' => $args['resId'] - ]); return $response->withStatus(204); } @@ -152,4 +161,61 @@ class UserFollowedResourceController return $response->withJson(['resources' => $formattedResources, 'countResources' => $count, 'allResources' => $allResources]); } + + public function getBasketsFromFolder(Request $request, Response $response, array $args) + { + if (!Validator::numeric()->notEmpty()->validate($args['resId'])) { + return $response->withStatus(400)->withJson(['errors' => 'Route resId is not an integer']); + } + + $followedResource = UserFollowedResourceModel::get(['select' => [1], 'where' => ['user_id = ?', 'res_id = ?'], 'data' => [$GLOBALS['id'], $args['resId']]]); + if (empty($followedResource)) { + return $response->withStatus(403)->withJson(['errors' => 'Resource out of perimeter']); + } + + $baskets = BasketModel::getWithPreferences([ + 'select' => ['baskets.id', 'baskets.basket_name', 'baskets.basket_clause', 'users_baskets_preferences.group_serial_id', 'usergroups.group_desc'], + 'where' => ['users_baskets_preferences.user_serial_id = ?'], + 'data' => [$GLOBALS['id']] + ]); + $groupsBaskets = []; + $inCheckedBaskets = []; + $outCheckedBaskets = []; + foreach ($baskets as $basket) { + if (in_array($basket['id'], $outCheckedBaskets)) { + continue; + } else { + if (!in_array($basket['id'], $inCheckedBaskets)) { + $preparedClause = PreparedClauseController::getPreparedClause(['clause' => $basket['basket_clause'], 'login' => $GLOBALS['userId']]); + $resource = ResModel::getOnView(['select' => [1], 'where' => ['res_id = ?', "({$preparedClause})"], 'data' => [$args['resId']]]); + if (empty($resource)) { + $outCheckedBaskets[] = $basket['id']; + continue; + } + } + $inCheckedBaskets[] = $basket['id']; + $groupsBaskets[] = ['groupId' => $basket['group_serial_id'], 'groupName' => $basket['group_desc'], 'basketId' => $basket['id'], 'basketName' => $basket['basket_name']]; + } + } + + return $response->withJson(['groupsBaskets' => $groupsBaskets]); + } + + public function getFilters(Request $request, Response $response, array $args) + { + $followedResources = UserFollowedResourceModel::get(['select' => ['res_id'], 'where' => ['user_id = ?'], 'data' => [$GLOBALS['id']]]); + $followedResources = array_column($followedResources, 'res_id'); + + if (empty($followedResources)) { + return $response->withJson(['entities' => [], 'priorities' => [], 'categories' => [], 'statuses' => [], 'entitiesChildren' => []]); + } + + $where = ['(res_id in (?))']; + $queryData = [$followedResources]; + $queryParams = $request->getQueryParams(); + + $filters = ResourceListController::getFormattedFilters(['where' => $where, 'queryData' => $queryData, 'queryParams' => $queryParams]); + + return $response->withJson($filters); + } } diff --git a/src/frontend/app/app-routing.module.ts b/src/frontend/app/app-routing.module.ts index 41268b5ec65e1333facfcff2070b6f6609311cac..ff34433d6698471024c655d4c4ace76db22253b0 100755 --- a/src/frontend/app/app-routing.module.ts +++ b/src/frontend/app/app-routing.module.ts @@ -16,6 +16,7 @@ import { IndexationComponent } from './indexation/indexation.compon import { ForgotPasswordComponent } from './login/forgotPassword/forgotPassword.component'; import { UpdatePasswordComponent } from './login/updatePassword/updatePassword.component'; import { ProcessComponent } from './process/process.component'; +import {FollowedDocumentListComponent} from "./home/followed-list/followed-document-list.component"; @NgModule({ imports: [ @@ -34,6 +35,7 @@ import { ProcessComponent } from './process/process.component'; { path: 'indexing/:groupId', canActivate: [AppGuard],component: IndexationComponent }, { path: 'forgot-password', component: ForgotPasswordComponent }, { path: 'update-password', component: UpdatePasswordComponent }, + { path: 'followed', canActivate: [AppGuard], component: FollowedDocumentListComponent }, { path: '**', redirectTo: 'home', pathMatch: 'full' }, ], { useHash: true }), ], diff --git a/src/frontend/app/app.module.ts b/src/frontend/app/app.module.ts index 73c6b1f41d7abadc42fd35b87914961b7f1af9cb..3894add127f01227523c91513cff18a2d3e0edbe 100755 --- a/src/frontend/app/app.module.ts +++ b/src/frontend/app/app.module.ts @@ -32,6 +32,9 @@ import { FolderMenuComponent } from './folder/folder-menu/folder import { FolderUpdateComponent } from './folder/folder-update/folder-update.component'; import { FolderActionListComponent } from './folder/folder-action-list/folder-action-list.component'; +import { FollowedDocumentListComponent } from './home/followed-list/followed-document-list.component'; +import { FollowedActionListComponent } from './home/followed-action-list/followed-action-list.component'; + /*ACTIONS PAGES */ import { ConfirmActionComponent } from './actions/confirm-action/confirm-action.component'; import { DisabledBasketPersistenceActionComponent } from './actions/disabled-basket-persistence-action/disabled-basket-persistence-action.component'; @@ -157,7 +160,9 @@ import { ContactModalComponent } from './administration/contact/modal/contact-mo MailResumeComponent, ContactsListComponent, ContactsListModalComponent, - ContactModalComponent + ContactModalComponent, + FollowedDocumentListComponent, + FollowedActionListComponent ], entryComponents: [ CustomSnackbarComponent, diff --git a/src/frontend/app/home/followed-action-list/followed-action-list.component.html b/src/frontend/app/home/followed-action-list/followed-action-list.component.html new file mode 100644 index 0000000000000000000000000000000000000000..41a15dabee19f8a4ec70385d4815892b24aa93c1 --- /dev/null +++ b/src/frontend/app/home/followed-action-list/followed-action-list.component.html @@ -0,0 +1,30 @@ +<button *ngIf="!contextMode" mat-stroked-button [matMenuTriggerFor]="menu">{{lang.actionsAlt}}<mat-icon matSuffix + class="fa fa-caret-down"></mat-icon> +</button> +<span *ngIf="contextMode" [matMenuTriggerFor]="menu" #menu2 style="position: absolute;" + [style.left]="contextMenuPosition.x" [style.top]="contextMenuPosition.y"></span> + +<mat-menu #menu="matMenu" [class]="'actionListMenu'"> + <div *ngIf="contextMode && this.selectedRes.length == 1" + style="text-align: center;font-size: 10px;color: white;background: #135F7F;padding: 5px;font-weight: bold;"> + {{this.contextMenuTitle}} + </div> + <button *ngIf="selectedRes.length == 1" mat-menu-item [matMenuTriggerFor]="baskets" (menuOpened)="getBaskets()"> + <span>Accéder à la bannette...</span> + </button> + <mat-menu #baskets="matMenu" [class]="'folderListMenu'"> + <ng-container *ngFor="let group of basketList.groups | sortBy : 'groupName'"> + <div class="basketGroupName">{{group.groupName}}</div> + <ng-container *ngFor="let basket of basketList.list | sortBy : 'basketName'"> + <button mat-menu-item *ngIf="basket.groupId === group.groupId" class="labelBasket" (click)="goTo(basket)" + title="{{basket.basketName}}">{{basket.basketName}} + </button> + </ng-container> + </ng-container> + </mat-menu> + <mat-divider></mat-divider> + <button mat-menu-item class="folderAction" (click)="unFollow()" [disabled]="selectedRes.length === 0"> + <mat-icon class="far fa-star fa-2x"></mat-icon> + <span>{{lang.untrackThisMail}}</span> + </button> +</mat-menu> diff --git a/src/frontend/app/home/followed-action-list/followed-action-list.component.scss b/src/frontend/app/home/followed-action-list/followed-action-list.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..f47252e51a1c5ecd91c2ed91094a537fb95b8055 --- /dev/null +++ b/src/frontend/app/home/followed-action-list/followed-action-list.component.scss @@ -0,0 +1,37 @@ +@import '../../../css/vars.scss'; + +.mat-icon { + height: auto; +} + +.mat-stroked-button { + border-radius: 20px; + border-color: #135F7F; + color: #135F7F; +} + +::ng-deep.actionListMenu { + .mat-menu-content { + padding-top: 0; + padding-bottom: 0; + } +} + +.folderAction { + .mat-icon { + font-size:20px; + color: $warn; + } +} + +.basketGroupName { + color: $primary; + padding-left: 10px; + padding-right: 10px; +} + +.labelBasket { + color: rgb(102, 102, 102); + font-size: 12px; + font-weight: bold; +} \ No newline at end of file diff --git a/src/frontend/app/home/followed-action-list/followed-action-list.component.ts b/src/frontend/app/home/followed-action-list/followed-action-list.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..41df9c5cafe10e7ec6345329726884d8efe83152 --- /dev/null +++ b/src/frontend/app/home/followed-action-list/followed-action-list.component.ts @@ -0,0 +1,116 @@ +import { Component, OnInit, Input, ViewChild, Output, EventEmitter } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { LANG } from '../../translate.component'; +import { NotificationService } from '../../notification.service'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { MatMenuTrigger } from '@angular/material/menu'; + +import { Router } from '@angular/router'; +import { ConfirmComponent } from '../../../plugins/modal/confirm.component'; +import { filter, exhaustMap, tap, map } from 'rxjs/operators'; +import { HeaderService } from '../../../service/header.service'; + +@Component({ + selector: 'app-followed-action-list', + templateUrl: "followed-action-list.component.html", + styleUrls: ['followed-action-list.component.scss'], + providers: [NotificationService], +}) +export class FollowedActionListComponent implements OnInit { + + lang: any = LANG; + loading: boolean = false; + + @ViewChild(MatMenuTrigger, { static: false }) contextMenu: MatMenuTrigger; + @Output() triggerEvent = new EventEmitter<string>(); + + contextMenuPosition = { x: '0px', y: '0px' }; + contextMenuTitle = ''; + currentAction: any = {}; + basketInfo: any = {}; + contextResId = 0; + currentLock: any = null; + arrRes: any[] = []; + + actionsList: any[] = []; + basketList: any = { + groups: [], + list: [] + }; + + @Input('selectedRes') selectedRes: any; + @Input('totalRes') totalRes: number; + @Input('contextMode') contextMode: boolean; + @Input('currentFolderInfo') currentFolderInfo: any; + + @Output('refreshEvent') refreshEvent = new EventEmitter<string>(); + @Output('refreshPanelFolders') refreshPanelFolders = new EventEmitter<string>(); + + constructor( + public http: HttpClient, + private notify: NotificationService, + public dialog: MatDialog, + private router: Router, + private headerService: HeaderService, + ) { } + + dialogRef: MatDialogRef<any>; + + ngOnInit(): void { } + + open(x: number, y: number, row: any) { + + console.log('menu opened'); + // Adjust the menu anchor position + this.contextMenuPosition.x = x + 'px'; + this.contextMenuPosition.y = y + 'px'; + + this.contextMenuTitle = row.chrono; + this.contextResId = row.resId; + + // Opens the menu + this.contextMenu.openMenu(); + + // prevents default + return false; + } + + refreshFolders() { + this.refreshPanelFolders.emit(); + } + + refreshDaoAfterAction() { + this.refreshEvent.emit(); + } + + unFollow() { + this.dialogRef = this.dialog.open(ConfirmComponent, { autoFocus: false, disableClose: true, data: { title: this.lang.delete, msg: this.lang.stopFollowingAlert } }); + + this.dialogRef.afterClosed().pipe( + filter((data: string) => data === 'ok'), + exhaustMap(() => this.http.request('DELETE', '../../rest/resources/unfollow' , { body: { resources: this.selectedRes } })), + tap((data: any) => { + this.notify.success(this.lang.removedFromFolder); + this.refreshFolders(); + this.refreshDaoAfterAction(); + }) + ).subscribe(); + } + + getBaskets() { + this.http.get('../../rest/followedResources/' + this.selectedRes + '/baskets').pipe( + tap((data: any) => { + this.basketList.groups = data.groupsBaskets.filter((x: any, i: any, a: any) => x && a.map((info: any) => info.groupId).indexOf(x.groupId) === i); + this.basketList.list = data.groupsBaskets; + }) + ).subscribe(); + } + + goTo(basket: any) { + if (this.contextMenuTitle !== this.lang.undefined && this.contextMenuTitle !== '') { + this.router.navigate(['/basketList/users/' + this.headerService.user.id + '/groups/' + basket.groupId + '/baskets/' + basket.basketId], { queryParams: { chrono: '"' + this.contextMenuTitle + '"' } }); + } else { + this.router.navigate(['/basketList/users/' + this.headerService.user.id + '/groups/' + basket.groupId + '/baskets/' + basket.basketId]); + } + } +} diff --git a/src/frontend/app/home/followed-list/followed-document-list.component.html b/src/frontend/app/home/followed-list/followed-document-list.component.html new file mode 100644 index 0000000000000000000000000000000000000000..458225923c0fbf65187437d8f8e98f6bc3ef4801 --- /dev/null +++ b/src/frontend/app/home/followed-list/followed-document-list.component.html @@ -0,0 +1,175 @@ +<mat-sidenav-container class="maarch-container"> + <mat-sidenav #snavLeft class="panel-left" #snav [mode]="appService.getViewMode() ? 'over' : 'side'" + [fixedInViewport]="appService.getViewMode()" [opened]="appService.getViewMode() ? false : true" + autoFocus="false" style="overflow-x:hidden;" [ngStyle]="{'width': appService.getViewMode() ? '80%' : '350px'}"> + <header-panel [snavLeft]="snav"></header-panel> + <menu-shortcut></menu-shortcut> + <menu-nav></menu-nav> + <basket-home #basketHome [snavL]="snav"></basket-home> + <mat-divider></mat-divider> + <panel-folder #panelFolder *ngIf="folderInfo.id !== '' && folderInfo.id !== undefined" + [selectedId]="folderInfo.id" [showTree]="false"></panel-folder> + </mat-sidenav> + <mat-sidenav-content> + <mat-card id="viewThumbnail" style="display:none;position: fixed;z-index: 2;margin-left: 1px;"> + <img style="max-height: 100vh;" src="{{thumbnailUrl}}" /> + </mat-card> + <div class="bg-head"> + <div class="bg-head-title" [class.customContainerRight]="appService.getViewMode()"> + <div class="bg-head-title-label"> + <header-left [snavLeft]="snav"></header-left> + </div> + <div class="bg-head-title-tool"> + <header-right></header-right> + </div> + </div> + <div class="bg-head-content" [class.fullContainer]="appService.getViewMode()"> + <app-filters-tool style="flex:1;overflow-x: auto;overflow-y: hidden;" #filtersTool + [listProperties]="this.listProperties" [snavR]="snav2" [totalRes]="allResInBasket.length" + [selectedRes]="selectedRes" [routeDatas]="'/rest/followedResources/filters'" + (toggleAllRes)="toggleAllRes($event)" (refreshEventAfterAction)="refreshDaoAfterAction()" + (refreshEvent)="refreshDao()" [title]="lang.searchMailInFolder"> + </app-filters-tool> + </div> + </div> + <div class="container" [class.fullContainer]="appService.getViewMode()"> + <div class="container-content"> + <div class="example-loading-shade" *ngIf="isLoadingResults"> + <mat-spinner *ngIf="isLoadingResults"></mat-spinner> + </div> + <div class="table-head"> + <div class="table-head-result"> + <mat-checkbox color="primary" + [checked]="selectedRes.length == allResInBasket.length && selectedRes.length > 0" + [indeterminate]="selectedRes.length > 0 && selectedRes.length < allResInBasket.length" + style="margin: 10px;padding-right: 10px;" title="{{lang.selectAllResInBasket}}" + (change)="toggleAllRes($event)"></mat-checkbox> {{resultsLength}} + {{lang.records | ucfirst}} <small *ngIf="selectedRes.length > 0">- {{selectedRes.length}} + {{lang.selected}}</small> + </div> + <div class="table-head-tool"> + <span style="float:right;height: 56px;margin-top: 5px;"> + <mat-paginator #paginatorResultList [length]="resultsLength" [pageSizeOptions]="[10, 25, 50, 100, 150]" + class="paginatorResultList"></mat-paginator> + </span> + <span style="float:right;height: 56px;margin-top: 11px;"> + <app-followed-action-list #actionsList [contextMode]="false" [currentFolderInfo]="folderInfo" + [totalRes]="allResInBasket.length" [selectedRes]="selectedRes" + (refreshEvent)="refreshDaoAfterAction()"> + </app-followed-action-list> + </span> + </div> + </div> + <div style="height:90%;overflow:auto;position:absolute;width:100%;"> + <table #tableBasketListSort="matSort" cdkDropList id="folder-list" + [cdkDropListConnectedTo]="listTodrag()" [cdkDropListData]="data" + [cdkDropListDisabled]="dragInit || appService.getViewMode()" mat-table [dataSource]="data" + matSort matSortActive="resId" matSortDisableClear matSortDirection="asc" style="width:100%;"> + + <ng-container matColumnDef="resId"> + <td mat-cell *matCellDef="let row" + style="padding:0;border-top: solid 1px rgba(0, 0, 0, 0.12);" + [class.selected-data]="row.checked"> + <div class="main-info"> + <span style="width:50px;"> + <mat-checkbox color="primary" [checked]="row.checked" + (change)="toggleRes($event,row)" (click)="$event.stopPropagation();"> + </mat-checkbox> + </span> + <span *ngIf="!appService.getViewMode()" style="cursor:pointer;" + class="main-info-status"> + <mat-icon *ngIf="row.isLocked !== true" title="{{row.statusLabel}}" + [ngStyle]="{'color': row.priorityColor}" color="primary" + class="{{row.statusImage.charAt(0)}}{{row.statusImage.charAt(1)}} {{row.statusImage}} {{row.statusImage.charAt(0)}}{{row.statusImage.charAt(1)}}-2x"> + </mat-icon> + <span *ngIf="row.confidentiality === 'Y'" + class="watermark">{{lang.confidential}}</span> + <mat-icon *ngIf="row.isLocked === true" + title="{{lang.warnLockResInProgress}} : {{row.locker}}" style="color: red;" + class="fa fa-lock fa-2x"> + </mat-icon> + </span> + <span *ngIf="!appService.getViewMode()" class="main-info-data" + style="width:200px;text-align:center;cursor:pointer;"> + <ng-container + *ngIf="row.chrono == lang.undefined && row.barcode != lang.undefined"> + <span style="color: rgba(0,0,0,0.4);font-size: 90%;"> + <i title="{{lang.barcode}}" class="fas fa-barcode"></i> + {{row.barcode}}</span> + </ng-container> + <ng-container *ngIf="row.barcode == lang.undefined"> + {{row.chrono}} + </ng-container> + </span> + <span class="main-info-data" style="font-weight:bold;flex:1;cursor:pointer;" + [class.undefined]="row.subject == lang.undefined" + title="{{row.subject}}">{{row.subject | shorten: 150: '...'}}</span> + <span class="main-info-action"> + <button mat-icon-button title="{{lang.notes}}" + (click)="$event.stopPropagation();togglePanel('note',row)" + [class.noData]="row.countNotes == 0"> + <mat-icon matBadgeHidden="{{row.countNotes == 0}}" fontSet="fas" + matBadge="{{row.countNotes}}" fontIcon="fa-comments fa-2x" + [color]="snav2.opened && row.checked && currentMode == 'note' ? 'primary' : ''"> + </mat-icon> + </button> + <button mat-icon-button title="{{lang.attachments}}" + (click)="$event.stopPropagation();togglePanel('attachment',row)" + [class.noData]="row.countAttachments == 0"> + <mat-icon matBadgeHidden="{{row.countAttachments == 0}}" fontSet="fas" + matBadge="{{row.countAttachments}}" fontIcon="fa-paperclip fa-2x" + [color]="snav2.opened && row.checked && currentMode == 'attachment' ? 'primary' : ''"> + </mat-icon> + </button> + <button mat-icon-button title="{{lang.diffusionList}}" + (click)="$event.stopPropagation();togglePanel('diffusion',row)"> + <mat-icon fontSet="fas" fontIcon="fa-sitemap fa-2x" + [color]="snav2.opened && row.checked && currentMode == 'diffusion' ? 'primary' : ''"> + </mat-icon> + </button> + <button mat-icon-button title="{{lang.linkDetails}}" + (click)="$event.stopPropagation();goToDetail(row);" + (mouseenter)="viewThumbnail(row);" (mouseleave)="closeThumbnail();"> + <mat-icon fontSet="fas" fontIcon="fa-info-circle fa-2x"></mat-icon> + </button> + </span> + </div> + </td> + </ng-container> + <tr mat-row *matRowDef="let row; columns: displayedColumnsBasket;" + (contextmenu)="open($event,row);" (click)="open($event,row);" class="rowData" + style="cursor: pointer;" [class.locked]="row.isLocked == true" cdkDrag + (cdkDragStarted)="selectSpecificRes(row);" [cdkDragData]="row"> + <div class="example-custom-placeholder" *cdkDragPlaceholder></div> + <div class="dragPreview" *cdkDragPreview><i class="fas fa-envelope-open-text fa-2x"></i> + <br /> + {{lang.classifyInFolder}} : + <b>{{row.chrono}}</b> + </div> + </tr> + </table> + </div> + <div class="table-head"> + </div> + </div> + </div> + </mat-sidenav-content> + <mat-sidenav #snav2 [fixedInViewport]="appService.getViewMode()" position='end' + [opened]="appService.getViewMode() ? false : false" [mode]="appService.getViewMode() ? 'over' : 'side'" + class="panel-right" style="overflow-x:hidden;" [class.docView]="innerHtml" + [ngStyle]="{'width': appService.getViewMode() ? '80%' : '30%'}" autoFocus="false"> + <div *ngIf="innerHtml" [matTooltip]="currentChrono" [innerHTML]="innerHtml" + style="height: 100%;overflow: hidden;"></div> + + <div style="display:flex;"> + <button mat-icon-button (click)="snav2.close()" style="font-size: 20px;color:#666;"> + <mat-icon class="fa fa-arrow-right"></mat-icon> + </button> + </div> + <app-panel-list #appPanelList (refreshBadgeNotes)="refreshBadgeNotes($event)"></app-panel-list> + <mat-divider></mat-divider> + </mat-sidenav> +</mat-sidenav-container> +<app-followed-action-list #actionsListContext [contextMode]="true" [currentFolderInfo]="folderInfo" + [totalRes]="allResInBasket.length" [selectedRes]="selectedRes" (refreshEvent)="refreshDaoAfterAction()"> +</app-followed-action-list> diff --git a/src/frontend/app/home/followed-list/followed-document-list.component.scss b/src/frontend/app/home/followed-list/followed-document-list.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..6ce6d16afd178e5e22029298a68e94635238c26b --- /dev/null +++ b/src/frontend/app/home/followed-list/followed-document-list.component.scss @@ -0,0 +1,91 @@ +@import '../../../css/vars.scss'; + +.dragPreview { + text-align: center; + border-radius: 5px; + background: white; + padding: 10px; + box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12); + color: $primary; +} + +.banner-opened { + display: flex; + flex-direction: column; + margin: 10px; + border-radius: 20px; + border: solid 1px #ccc; + position: relative; + padding: 20px; + background: lighten($primary, 10%); + width: 100%; + + .title { + white-space: pre; + overflow: hidden; + max-width: 85%; + text-overflow: ellipsis; + z-index: 1; + font-size: 20px; + font-weight: bold; + letter-spacing: 2px; + position: absolute; + top: -18px; + left: 20px; + padding: 0px; + margin: 0px; + color: white; + + &-divider { + position: absolute; + width: 99%; + z-index: -1; + top: 17px; + background: lighten($primary, 10%); + height: 1px; + } + } + + .content { + display: flex; + font-size: 16px; + + &-item { + flex: 1; + padding-left: 20px; + padding-right: 20px; + } + + .private { + font-style: italic; + display: flex; + justify-content: flex-end; + align-items: center; + } + } +} + +.banner-closed { + display: flex; + flex-direction: column; + margin: 10px; + position: relative; + width: 100%; + + .title { + white-space: pre; + overflow: hidden; + max-width: 85%; + text-overflow: ellipsis; + z-index: 1; + font-size: 20px; + font-weight: bold; + letter-spacing: 2px; + position: absolute; + top: -18px; + left: 20px; + padding: 0px; + margin: 0px; + color: white; + } +} \ No newline at end of file diff --git a/src/frontend/app/home/followed-list/followed-document-list.component.ts b/src/frontend/app/home/followed-list/followed-document-list.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..3d9cc1ce5e04235d5223d90fca3a8445400671e1 --- /dev/null +++ b/src/frontend/app/home/followed-list/followed-document-list.component.ts @@ -0,0 +1,353 @@ +import { Component, OnInit, ViewChild, EventEmitter, ViewContainerRef } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { LANG } from '../../translate.component'; +import { merge, Observable, of as observableOf, Subject, of, Subscription } from 'rxjs'; +import { NotificationService } from '../../notification.service'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSidenav } from '@angular/material/sidenav'; +import { MatSort } from '@angular/material/sort'; + +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { startWith, switchMap, map, catchError, takeUntil, tap, exhaustMap, filter } from 'rxjs/operators'; +import { ActivatedRoute, Router } from '@angular/router'; +import { HeaderService } from '../../../service/header.service'; + +import { Overlay } from '@angular/cdk/overlay'; +import { PanelListComponent } from '../../list/panel/panel-list.component'; +import { AppService } from '../../../service/app.service'; +import { BasketHomeComponent } from '../../basket/basket-home.component'; +import { ConfirmComponent } from '../../../plugins/modal/confirm.component'; +import { FollowedActionListComponent } from '../followed-action-list/followed-action-list.component'; +import { FiltersListService } from '../../../service/filtersList.service'; +import { trigger, transition, style, animate } from '@angular/animations'; + + +declare function $j(selector: any): any; + +@Component({ + templateUrl: "followed-document-list.component.html", + styleUrls: ['followed-document-list.component.scss'], + providers: [NotificationService, AppService] +}) +export class FollowedDocumentListComponent implements OnInit { + + lang: any = LANG; + + loading: boolean = false; + docUrl: string = ''; + public innerHtml: SafeHtml; + basketUrl: string; + homeData: any; + + injectDatasParam = { + resId: 0, + editable: false + }; + currentResource: any = {}; + + filtersChange = new EventEmitter(); + + dragInit: boolean = true; + + dialogRef: MatDialogRef<any>; + + @ViewChild('snav', { static: true }) sidenavLeft: MatSidenav; + @ViewChild('snav2', { static: true }) sidenavRight: MatSidenav; + + displayedColumnsBasket: string[] = ['resId']; + + displayedMainData: any = [ + { + 'value': 'chrono', + 'cssClasses': ['softColorData', 'align_centerData', 'chronoData'], + 'icon': '' + }, + { + 'value': 'subject', + 'cssClasses': ['longData'], + 'icon': '' + } + ]; + + resultListDatabase: ResultListHttpDao | null; + data: any; + resultsLength = 0; + isLoadingResults = true; + listProperties: any = {}; + currentChrono: string = ''; + currentMode: string = ''; + + thumbnailUrl: string = ''; + + selectedRes: Array<number> = []; + allResInBasket: number[] = []; + selectedDiffusionTab: number = 0; + folderInfo: any = { + id: 0, + 'label': '', + 'ownerDisplayName': '', + 'entitiesSharing': [] + }; + + private destroy$ = new Subject<boolean>(); + + @ViewChild('actionsListContext', { static: true }) actionsList: FollowedActionListComponent; + @ViewChild('appPanelList', { static: true }) appPanelList: PanelListComponent; + + currentSelectedChrono: string = ''; + + @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator; + @ViewChild('tableBasketListSort', { static: true }) sort: MatSort; + @ViewChild('basketHome', { static: true }) basketHome: BasketHomeComponent; + + constructor( + private router: Router, + private route: ActivatedRoute, + public http: HttpClient, + public dialog: MatDialog, + private sanitizer: DomSanitizer, + private headerService: HeaderService, + public filtersListService: FiltersListService, + private notify: NotificationService, + public overlay: Overlay, + public viewContainerRef: ViewContainerRef, + public appService: AppService) { + + $j("link[href='merged_css.php']").remove(); + } + + ngOnInit(): void { + + this.loading = false; + + this.isLoadingResults = false; + + this.route.params.subscribe(params => { + + this.dragInit = true; + this.destroy$.next(true); + + this.http.get('../../rest/followedResources') + .subscribe((data: any) => { + this.headerService.setHeader(this.lang.followedMail, '', 'fas fa-star'); + setTimeout(() => { + this.basketHome.togglePanel(false); + }, 200); + + }); + this.basketUrl = '../../rest/followedResources'; + this.filtersListService.filterMode = false; + this.selectedRes = []; + this.sidenavRight.close(); + window['MainHeaderComponent'].setSnav(this.sidenavLeft); + window['MainHeaderComponent'].setSnavRight(null); + + this.listProperties = this.filtersListService.initListsProperties(this.headerService.user.id, 0, null, 'followed'); + + setTimeout(() => { + this.dragInit = false; + }, 1000); + this.initResultList(); + + }, + (err: any) => { + this.notify.handleErrors(err); + }); + } + + ngOnDestroy() { + this.destroy$.next(true); + } + + initResultList() { + this.resultListDatabase = new ResultListHttpDao(this.http, this.filtersListService); + // If the user changes the sort order, reset back to the first page. + this.paginator.pageIndex = this.listProperties.page; + this.paginator.pageSize = this.listProperties.pageSize; + this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); + + // When list is refresh (sort, page, filters) + merge(this.sort.sortChange, this.paginator.page, this.filtersChange) + .pipe( + takeUntil(this.destroy$), + startWith({}), + switchMap(() => { + this.isLoadingResults = true; + return this.resultListDatabase!.getRepoIssues( + this.sort.active, this.sort.direction, this.paginator.pageIndex, this.basketUrl, this.filtersListService.getUrlFilters(), this.paginator.pageSize); + }), + map(data => { + // Flip flag to show that loading has finished. + this.isLoadingResults = false; + data = this.processPostData(data); + this.resultsLength = data.countResources; + this.allResInBasket = data.allResources; + return data.resources; + }), + catchError((err: any) => { + this.notify.handleErrors(err); + this.router.navigate(['/home']); + this.isLoadingResults = false; + return observableOf([]); + }) + ).subscribe(data => this.data = data); + } + + goTo(row: any) { + this.filtersListService.filterMode = false; + if (this.docUrl == '../../rest/resources/' + row.resId + '/content' && this.sidenavRight.opened) { + this.sidenavRight.close(); + } else { + this.docUrl = '../../rest/resources/' + row.resId + '/content'; + this.currentChrono = row.chrono; + this.innerHtml = this.sanitizer.bypassSecurityTrustHtml( + "<iframe style='height:100%;width:100%;' src='" + this.docUrl + "' class='embed-responsive-item'>" + + "</iframe>"); + this.sidenavRight.open(); + } + } + + goToDetail(row: any) { + location.href = "index.php?page=details&dir=indexing_searching&id=" + row.resId; + } + + togglePanel(mode: string, row: any) { + let thisSelect = { checked: true }; + let thisDeselect = { checked: false }; + row.checked = true; + this.toggleAllRes(thisDeselect); + this.toggleRes(thisSelect, row); + + if (this.currentResource.resId == row.resId && this.sidenavRight.opened && this.currentMode == mode) { + this.sidenavRight.close(); + } else { + this.currentMode = mode; + this.currentResource = row; + this.appPanelList.loadComponent(mode, row); + this.sidenavRight.open(); + } + } + + refreshBadgeNotes(nb: number) { + this.currentResource.countNotes = nb; + } + + refreshBadgeAttachments(nb: number) { + this.currentResource.countAttachments = nb; + } + + refreshDao() { + this.paginator.pageIndex = this.listProperties.page; + this.filtersChange.emit(); + } + + refreshDaoAfterAction() { + this.sidenavRight.close(); + this.refreshDao(); + const e: any = { checked: false }; + this.toggleAllRes(e); + } + + viewThumbnail(row: any) { + this.thumbnailUrl = '../../rest/resources/' + row.resId + '/thumbnail'; + $j('#viewThumbnail').show(); + $j('#listContent').css({ "overflow": "hidden" }); + } + + closeThumbnail() { + $j('#viewThumbnail').hide(); + $j('#listContent').css({ "overflow": "auto" }); + } + + processPostData(data: any) { + data.resources.forEach((element: any) => { + // Process main datas + Object.keys(element).forEach((key) => { + if (key == 'statusImage' && element[key] == null) { + element[key] = 'fa-question undefined'; + } else if ((element[key] == null || element[key] == '') && ['closingDate', 'countAttachments', 'countNotes', 'display'].indexOf(key) === -1) { + element[key] = this.lang.undefined; + } + }); + + element['checked'] = this.selectedRes.indexOf(element['resId']) !== -1; + }); + + return data; + } + + toggleRes(e: any, row: any) { + if (e.checked) { + if (this.selectedRes.indexOf(row.resId) === -1) { + this.selectedRes.push(row.resId); + row.checked = true; + } + } else { + let index = this.selectedRes.indexOf(row.resId); + this.selectedRes.splice(index, 1); + row.checked = false; + } + } + + toggleAllRes(e: any) { + this.selectedRes = []; + if (e.checked) { + this.data.forEach((element: any) => { + element['checked'] = true; + }); + this.selectedRes = JSON.parse(JSON.stringify(this.allResInBasket)); + } else { + this.data.forEach((element: any) => { + element['checked'] = false; + }); + } + } + + selectSpecificRes(row: any) { + let thisSelect = { checked: true }; + let thisDeselect = { checked: false }; + + this.toggleAllRes(thisDeselect); + this.toggleRes(thisSelect, row); + } + + open({ x, y }: MouseEvent, row: any) { + console.log('opening menu...'); + let thisSelect = { checked: true }; + let thisDeselect = { checked: false }; + if (row.checked === false) { + row.checked = true; + this.toggleAllRes(thisDeselect); + this.toggleRes(thisSelect, row); + } + this.actionsList.open(x, y, row); + + // prevents default + return false; + } + + listTodrag() { + // return this.foldersService.getDragIds(); + } +} +export interface BasketList { + folder: any; + resources: any[]; + countResources: number; + allResources: number[]; +} + +export class ResultListHttpDao { + + constructor(private http: HttpClient, private filtersListService: FiltersListService) { } + + getRepoIssues(sort: string, order: string, page: number, href: string, filters: string, pageSize: number): Observable<BasketList> { + this.filtersListService.updateListsPropertiesPage(page); + this.filtersListService.updateListsPropertiesPageSize(pageSize); + let offset = page * pageSize; + const requestUrl = `${href}?limit=${pageSize}&offset=${offset}${filters}`; + + return this.http.get<BasketList>(requestUrl); + } +} diff --git a/src/frontend/app/indexation/indexing-form/indexing-form.component.ts b/src/frontend/app/indexation/indexing-form/indexing-form.component.ts index 91b3087e9dc66a6795ebf4d550b8e63ccbc50d81..51e9387a19aba7d078ddb69910501836d265aaef 100644 --- a/src/frontend/app/indexation/indexing-form/indexing-form.component.ts +++ b/src/frontend/app/indexation/indexing-form/indexing-form.component.ts @@ -879,16 +879,14 @@ export class IndexingFormComponent implements OnInit { if (this.mode !== 'indexation') { if (this.arrFormControl['mailÂtracking'].value) { - - this.http.put(`../../rest/resources/${this.resId}/follow`, {}).pipe( + this.http.post(`../../rest/resources/${this.resId}/follow`, {}).pipe( catchError((err: any) => { this.notify.handleErrors(err); return of(false); }) ).subscribe(); } else { - - this.http.delete(`../../rest/resources/${this.resId}/unfollow`, {}).pipe( + this.http.request('DELETE', '../../rest/resources/unfollow' , { body: { resources: [this.resId] } }).pipe( catchError((err: any) => { this.notify.handleErrors(err); return of(false); diff --git a/src/frontend/app/menu/menuNav.component.html b/src/frontend/app/menu/menuNav.component.html index 65570821dfcf8088bd591b11af58eed0c3292ad7..f4cb17a5e26e15938b9ddb2ecc40952e3a5f6a16 100755 --- a/src/frontend/app/menu/menuNav.component.html +++ b/src/frontend/app/menu/menuNav.component.html @@ -1,4 +1,4 @@ -<mat-nav-list *ngIf="router.url != '/home' && router.url != '/profile' && router.url != '/about-us' && router.url != '/administration' && router.url.indexOf('/basketList') == -1 && router.url.indexOf('/folders') == -1"> +<mat-nav-list *ngIf="router.url != '/home' && router.url != '/profile' && router.url != '/about-us' && router.url != '/administration' && router.url.indexOf('/basketList') == -1 && router.url.indexOf('/folders') == -1 && router.url != '/followed'"> <a mat-list-item (click)="backClicked()"> <mat-icon color="primary" mat-list-icon class="fa fa-chevron-left"></mat-icon> <p mat-line> @@ -6,4 +6,4 @@ </p> </a> </mat-nav-list> -<mat-divider *ngIf="router.url != '/home' && router.url != '/about-us' && router.url != '/administration' && router.url.indexOf('/basketList') == -1 && router.url.indexOf('/folders') == -1"></mat-divider> \ No newline at end of file +<mat-divider *ngIf="router.url != '/home' && router.url != '/about-us' && router.url != '/administration' && router.url.indexOf('/basketList') == -1 && router.url.indexOf('/folders') == -1"></mat-divider> diff --git a/src/frontend/lang/lang-en.ts b/src/frontend/lang/lang-en.ts index 8eef1283ff136d657a596c0bc7682a84f7855035..74026fe92a87e04f6c328f1c45f6b8d5088bc057 100755 --- a/src/frontend/lang/lang-en.ts +++ b/src/frontend/lang/lang-en.ts @@ -1373,4 +1373,6 @@ export const LANG_EN = { "see": "see", "notSavedBecauseInvalid": "Configuration not updated because some datas are invalid", "deleteAll": "Delete all", + "followedMail": "Followed mail(s)", + "stopFollowingAlert": "Do you want to stop following these mails ?" }; diff --git a/src/frontend/lang/lang-fr.ts b/src/frontend/lang/lang-fr.ts index 66e9cdd7f3692f89d286c98d602f8320ee967f1d..b76201ed0d84e649a05021a1f6b2552efa4f09eb 100755 --- a/src/frontend/lang/lang-fr.ts +++ b/src/frontend/lang/lang-fr.ts @@ -1413,4 +1413,6 @@ export const LANG_FR = { "see": "voir", "notSavedBecauseInvalid": "La configuration n'a pas été mise à jour car des données sont invalides", "deleteAll": "Tout supprimer", + "followedMail": "Courrier(s) suivi(s)", + "stopFollowingAlert": "Voulez-vous arrêter de suivre ce(s) courrier(s) ?" }; diff --git a/src/frontend/lang/lang-nl.ts b/src/frontend/lang/lang-nl.ts index badb6c670cc505a2e379f9a7574ba108d6bc562a..81005db16f16aa12397f7b2e6fcac8daf0776177 100755 --- a/src/frontend/lang/lang-nl.ts +++ b/src/frontend/lang/lang-nl.ts @@ -1398,4 +1398,6 @@ export const LANG_NL = { "see": "see", //_TO_TRANSLATE "notSavedBecauseInvalid": "Configuration not updated because some datas are invalid", //_TO_TRANSLATE "deleteAll": "Delete all", //_TO_TRANSLATE + "followedMail": "Followed mail(s)", //_TO_TRANSLATE + "stopFollowingAlert": "Do you want to stop following these mails ?", //_TO_TRANSLATE };