mirror of
https://github.com/richard-loafle/fuse-angular.git
synced 2025-01-10 04:25:08 +00:00
(Mail-ngrx) Ngrx version of Mail App added.
This commit is contained in:
parent
242feaa169
commit
abede386c8
33
package-lock.json
generated
33
package-lock.json
generated
|
@ -245,6 +245,26 @@
|
|||
"integrity": "sha1-NOZY7T2jfyOwogDi2lqJvpK7IJ8=",
|
||||
"dev": true
|
||||
},
|
||||
"@ngrx/effects": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-4.1.1.tgz",
|
||||
"integrity": "sha1-y3WLhSeWSyWOpBlR9ZqhROPvn64="
|
||||
},
|
||||
"@ngrx/router-store": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrx/router-store/-/router-store-4.1.1.tgz",
|
||||
"integrity": "sha1-F/rHwPX/3e+LdemnTtLLCQdPO8o="
|
||||
},
|
||||
"@ngrx/store": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrx/store/-/store-4.1.1.tgz",
|
||||
"integrity": "sha1-aA403yd16IUnVO13f/rJW9gbfeA="
|
||||
},
|
||||
"@ngrx/store-devtools": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrx/store-devtools/-/store-devtools-4.1.1.tgz",
|
||||
"integrity": "sha1-IHRcOcdWD9wF+k8iY4RCp+x91nY="
|
||||
},
|
||||
"@ngtools/json-schema": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@ngtools/json-schema/-/json-schema-1.1.0.tgz",
|
||||
|
@ -2352,6 +2372,11 @@
|
|||
"integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=",
|
||||
"dev": true
|
||||
},
|
||||
"deep-freeze-strict": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/deep-freeze-strict/-/deep-freeze-strict-1.1.1.tgz",
|
||||
"integrity": "sha1-d9BYPKJKab5LvZrC+uQV1VUj5bA="
|
||||
},
|
||||
"default-require-extensions": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-1.0.0.tgz",
|
||||
|
@ -6470,6 +6495,14 @@
|
|||
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=",
|
||||
"dev": true
|
||||
},
|
||||
"ngrx-store-freeze": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ngrx-store-freeze/-/ngrx-store-freeze-0.2.0.tgz",
|
||||
"integrity": "sha1-dMIxlHu+GTivci9qcmJNxpeI058=",
|
||||
"requires": {
|
||||
"deep-freeze-strict": "1.1.1"
|
||||
}
|
||||
},
|
||||
"ngx-color-picker": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ngx-color-picker/-/ngx-color-picker-5.0.5.tgz",
|
||||
|
|
|
@ -43,6 +43,11 @@
|
|||
"highlight.js": "9.12.0",
|
||||
"intl": "1.2.5",
|
||||
"moment": "2.19.3",
|
||||
"@ngrx/effects": "4.1.1",
|
||||
"@ngrx/router-store": "4.1.1",
|
||||
"@ngrx/store": "4.1.1",
|
||||
"@ngrx/store-devtools": "4.1.1",
|
||||
"ngrx-store-freeze": "0.2.0",
|
||||
"ngx-color-picker": "5.0.5",
|
||||
"ngx-cookie-service": "1.0.9",
|
||||
"perfect-scrollbar": "1.3.0",
|
||||
|
|
|
@ -21,12 +21,17 @@ import { ServicesModule } from './main/content/services/services.module';
|
|||
import { FuseAngularMaterialModule } from './main/content/components/angular-material/angular-material.module';
|
||||
import { MarkdownModule } from 'angular2-markdown';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { AppStoreModule } from './store/store.module';
|
||||
|
||||
const appRoutes: Routes = [
|
||||
{
|
||||
path : 'apps/mail',
|
||||
loadChildren: './main/content/apps/mail/mail.module#FuseMailModule'
|
||||
},
|
||||
{
|
||||
path : 'apps/mail-ngrx',
|
||||
loadChildren: './main/content/apps/mail-ngrx/mail.module#FuseMailNgrxModule'
|
||||
},
|
||||
{
|
||||
path : 'apps/chat',
|
||||
loadChildren: './main/content/apps/chat/chat.module#FuseChatModule'
|
||||
|
@ -77,6 +82,7 @@ const appRoutes: Routes = [
|
|||
delay : 0,
|
||||
passThruUnknownUrl: true
|
||||
}),
|
||||
AppStoreModule,
|
||||
FuseMainModule,
|
||||
ProjectModule,
|
||||
PagesModule,
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
<div class="dialog-content-wrapper">
|
||||
<mat-toolbar matDialogTitle class="mat-accent m-0">
|
||||
<div fxFlex fxLayout="row" fxLayoutAlign="space-between center">
|
||||
<span class="title dialog-title">New Message</span>
|
||||
<button mat-button class="mat-icon-button"
|
||||
(click)="dialogRef.close()"
|
||||
aria-label="Close dialog">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</mat-toolbar>
|
||||
|
||||
<div mat-dialog-content class="p-24 m-0" fusePerfectScrollbar>
|
||||
|
||||
<form name="composeForm" [formGroup]="composeForm" class="compose-form" fxLayout="column" fxFlex>
|
||||
|
||||
<mat-form-field>
|
||||
<input matInput name="from"
|
||||
placeholder="From"
|
||||
formControlName="from"
|
||||
type="email">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<input matInput name="to"
|
||||
placeholder="To"
|
||||
formControlName="to"
|
||||
type="email" required>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<input matInput
|
||||
name="cc"
|
||||
placeholder="Cc"
|
||||
formControlName="cc"
|
||||
type="email">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<input matInput
|
||||
name="bcc"
|
||||
placeholder="Bcc"
|
||||
formControlName="bcc"
|
||||
type="email">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<input matInput name="subject"
|
||||
placeholder="Subject"
|
||||
formControlName="subject">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<textarea matInput name="message"
|
||||
placeholder="Message"
|
||||
formControlName="message"
|
||||
rows="6">
|
||||
</textarea>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="attachment-list">
|
||||
|
||||
<div class="attachment" fxLayout="row" fxLayoutAlign="space-between center">
|
||||
<div>
|
||||
<span class="filename">attachment-2.doc</span>
|
||||
<span class="size">(12 Kb)</span>
|
||||
</div>
|
||||
|
||||
<button mat-icon-button aria-label="Delete attachment">
|
||||
<mat-icon class="s-16">close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="attachment" fxLayout="row" fxLayoutAlign="space-between center">
|
||||
<div>
|
||||
<span class="filename">attachment-1.jpg</span>
|
||||
<span class="size">(350 Kb)</span>
|
||||
</div>
|
||||
|
||||
<button mat-icon-button aria-label="Delete attachment">
|
||||
<mat-icon class="s-16">close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div mat-dialog-actions class="m-0 p-16" fxLayout="row" fxLayoutAlign="space-between center">
|
||||
<div>
|
||||
<button mat-raised-button
|
||||
(click)="dialogRef.close(['send',composeForm])"
|
||||
class="save-button mat-accent"
|
||||
[disabled]="composeForm.invalid"
|
||||
aria-label="SAVE">
|
||||
SEND
|
||||
</button>
|
||||
|
||||
<button mat-icon-button matTooltip="Attach a file">
|
||||
<mat-icon>attach_file</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button mat-button
|
||||
class="mat-icon-button"
|
||||
(click)="dialogRef.close(['delete',composeForm])"
|
||||
aria-label="Delete"
|
||||
matTooltip="Delete">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,40 @@
|
|||
.mail-compose-dialog {
|
||||
.mat-dialog-container {
|
||||
padding: 0;
|
||||
width: 720px;
|
||||
|
||||
.compose-form {
|
||||
|
||||
.mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
font-size: 13px;
|
||||
padding-top: 16px;
|
||||
|
||||
.attachment {
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid rgba(0, 0, 0, 0.16);
|
||||
padding-left: 16px;
|
||||
margin-top: 8px;
|
||||
border-radius: 2px;
|
||||
|
||||
.filename {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-content-wrapper {
|
||||
max-height: 85vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import { Component, Inject, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector : 'fuse-mail-compose',
|
||||
templateUrl : './compose.component.html',
|
||||
styleUrls : ['./compose.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class FuseMailNgrxComposeDialogComponent implements OnInit
|
||||
{
|
||||
composeForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<FuseMailNgrxComposeDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) private data: any,
|
||||
private formBuilder: FormBuilder
|
||||
)
|
||||
{
|
||||
this.composeForm = this.createComposeForm();
|
||||
}
|
||||
|
||||
ngOnInit()
|
||||
{
|
||||
}
|
||||
|
||||
createComposeForm()
|
||||
{
|
||||
return this.formBuilder.group({
|
||||
from : {
|
||||
value : ['johndoe@creapond.com'],
|
||||
disabled: [true]
|
||||
},
|
||||
to : [''],
|
||||
cc : [''],
|
||||
bcc : [''],
|
||||
subject: [''],
|
||||
message: ['']
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
14
src/app/main/content/apps/mail-ngrx/i18n/en.ts
Normal file
14
src/app/main/content/apps/mail-ngrx/i18n/en.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
export const locale = {
|
||||
lang: 'en',
|
||||
data: {
|
||||
'MAIL': {
|
||||
'COMPOSE' : 'COMPOSE',
|
||||
'FOLDERS' : 'FOLDERS',
|
||||
'FILTERS' : 'FILTERS',
|
||||
'LABELS' : 'LABELS',
|
||||
'NO_MESSAGES' : 'There are no messages!',
|
||||
'SELECT_A_MESSAGE_TO_READ': 'Select a message to read',
|
||||
'SEARCH_PLACEHOLDER': 'Search for an e-mail or contact'
|
||||
}
|
||||
}
|
||||
};
|
14
src/app/main/content/apps/mail-ngrx/i18n/tr.ts
Normal file
14
src/app/main/content/apps/mail-ngrx/i18n/tr.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
export const locale = {
|
||||
lang: 'tr',
|
||||
data: {
|
||||
'MAIL': {
|
||||
'COMPOSE' : 'YENİ E-POSTA',
|
||||
'FOLDERS' : 'KLASÖRLER',
|
||||
'FILTERS' : 'FİLTRELER',
|
||||
'LABELS' : 'ETİKETLER',
|
||||
'NO_MESSAGES' : 'Mesajiniz bulunmamakta!',
|
||||
'SELECT_A_MESSAGE_TO_READ': 'Okumak için bir mesaj seçin',
|
||||
'SEARCH_PLACEHOLDER' : 'E-mail yada bir kişi arayın'
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,140 @@
|
|||
<div *ngIf="!mail" fxLayout="column" fxLayoutAlign="center center" fxFlex>
|
||||
<mat-icon class="s-128 mb-16 select-message-icon">
|
||||
email
|
||||
</mat-icon>
|
||||
<span class="select-message-text hint-text">
|
||||
<span>{{ 'MAIL.SELECT_A_MESSAGE_TO_READ' | translate }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="mail">
|
||||
|
||||
<div class="mail-header" fxLayout="row" fxLayoutAlign="space-between center">
|
||||
|
||||
<div>
|
||||
<div class="subject" flex>{{mail.subject}}</div>
|
||||
|
||||
<div class="labels" fxLayout="row" fxLayoutWrap>
|
||||
<div class="label" *ngFor="let labelId of mail.labels"
|
||||
fxLayout="row" fxLayoutAlign="start center">
|
||||
<div class="label-color" [ngStyle]="{'background-color': (labels$ | async) | getById:labelId:'color'}"></div>
|
||||
<div class="label-title">{{(labels$ | async) | getById:labelId:'title'}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions" fxLayout="row" fxLayoutAlign="start center">
|
||||
<button mat-button class="mat-icon-button" (click)="toggleStar($event)" aria-label="Toggle star">
|
||||
<mat-icon *ngIf="mail.starred">star</mat-icon>
|
||||
<mat-icon *ngIf="!mail.starred">star_outline</mat-icon>
|
||||
</button>
|
||||
|
||||
<button mat-button class="mat-icon-button" (click)="toggleImportant($event)" aria-label="Toggle important">
|
||||
<mat-icon *ngIf="mail.important">label</mat-icon>
|
||||
<mat-icon *ngIf="!mail.important">label_outline</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mail-content">
|
||||
|
||||
<div class="info" fxLayout="row" fxLayoutAlign="space-between start">
|
||||
|
||||
<div fxFlex fxLayout="column" fxLayoutAlign="start start">
|
||||
|
||||
<div fxLayout="row" fxLayoutAlign="start start">
|
||||
|
||||
<div>
|
||||
<img *ngIf="mail.from.avatar" alt="{{mail.from.name}}"
|
||||
src="{{mail.from.avatar}}" class="avatar"/>
|
||||
|
||||
<div *ngIf="!mail.from.avatar" class="avatar" ms-random-class="vm.colors">
|
||||
{{mail.from.name[0]}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div fxLayout="column" fxLayoutAlign="start start">
|
||||
|
||||
<div class="name">
|
||||
{{mail.from.name}}
|
||||
</div>
|
||||
|
||||
<div class="to" fxLayout="row" fxLayoutAlign="start center">
|
||||
<div class="to-text">to</div>
|
||||
<div>{{mail.to[0].name}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="toggle-details" (click)="showDetails = !showDetails">
|
||||
<span *ngIf="!showDetails">Show Details</span>
|
||||
<span *ngIf="showDetails">Hide Details</span>
|
||||
</a>
|
||||
|
||||
<div *ngIf="showDetails" class="details" fxLayout="row" fxLayoutAlign="start start">
|
||||
|
||||
<div fxLayout="column">
|
||||
<span class="title">From:</span>
|
||||
<span class="title">To:</span>
|
||||
<span class="title">Date:</span>
|
||||
</div>
|
||||
|
||||
<div fxLayout="column">
|
||||
<span class="detail">{{mail.from.email}}</span>
|
||||
<span class="detail">{{mail.to[0].email}}</span>
|
||||
<span class="detail">{{mail.time}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button mat-button [matMenuTriggerFor]="moreMenu" aria-label="More" class="mat-icon-button"
|
||||
(click)="$event.stopPropagation()">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
|
||||
<mat-menu #moreMenu="matMenu">
|
||||
<button mat-menu-item aria-label="Reply">
|
||||
<mat-icon>reply</mat-icon>
|
||||
<span>Reply</span>
|
||||
</button>
|
||||
|
||||
<button mat-menu-item aria-label="Forward">
|
||||
<mat-icon>forward</mat-icon>
|
||||
<span>Forward</span>
|
||||
</button>
|
||||
|
||||
<button mat-menu-item aria-label="Print">
|
||||
<mat-icon>print</mat-icon>
|
||||
<span>Print</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
|
||||
<div [innerHTML]="mail.message"></div>
|
||||
|
||||
</div>
|
||||
|
||||
<div *ngIf="mail.attachments" class="mail-attachments">
|
||||
|
||||
<div class="title">
|
||||
<span>Attachments</span>
|
||||
({{mail.attachments.length}})
|
||||
</div>
|
||||
|
||||
<div class="attachment-list" fxLayout="row" fxLayoutWrap>
|
||||
|
||||
<div class="attachment" fxLayout="column"
|
||||
*ngFor="let attachment of mail.attachments">
|
||||
|
||||
<img class="preview" src="{{attachment.preview}}">
|
||||
|
||||
<div fxLayout="column">
|
||||
<a href="#" onclick="event.preventDefault()">View</a>
|
||||
<a href="#" onclick="event.preventDefault()">Download</a>
|
||||
<div class="size">({{attachment.size}})</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,118 @@
|
|||
@import 'src/app/core/scss/fuse';
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
|
||||
.select-message-text {
|
||||
font-size: 24px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.mail-header {
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
|
||||
.actions {
|
||||
min-width: 88px;
|
||||
}
|
||||
|
||||
.subject {
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 11px;
|
||||
border-radius: 2px;
|
||||
margin: 4px 4px 4px 0;
|
||||
padding: 3px 8px;
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
|
||||
.label-color {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-right: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mail-content {
|
||||
padding: 24px 0;
|
||||
|
||||
.to {
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
|
||||
.to-text {
|
||||
margin-right: 4px;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
padding-bottom: 16px;
|
||||
|
||||
.avatar {
|
||||
margin-right: 16px;
|
||||
background-color: mat-color($accent);
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-right: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.toggle-details {
|
||||
user-select: none;
|
||||
text-decoration: underline;
|
||||
padding-top: 16px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.details {
|
||||
padding-top: 8px;
|
||||
|
||||
.title {
|
||||
font-weight: 500;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.detail {
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.mail-attachments {
|
||||
padding: 24px 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
|
||||
.title {
|
||||
margin-bottom: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.attachment {
|
||||
|
||||
.preview {
|
||||
width: 100px;
|
||||
margin: 0 16px 8px 0;
|
||||
}
|
||||
|
||||
.link {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.size {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Mail } from '../mail.model';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Store } from '@ngrx/store';
|
||||
import * as fromStore from '../store';
|
||||
import { MailNgrxService } from '../mail.service';
|
||||
|
||||
@Component({
|
||||
selector : 'fuse-mail-details',
|
||||
templateUrl : './mail-details.component.html',
|
||||
styleUrls : ['./mail-details.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class FuseMailNgrxDetailsComponent implements OnInit, OnDestroy, OnChanges
|
||||
{
|
||||
labels$: Observable<any>;
|
||||
@Input('mail') mailInput: Mail;
|
||||
mail: Mail;
|
||||
showDetails = false;
|
||||
|
||||
constructor(
|
||||
private mailService: MailNgrxService,
|
||||
private store: Store<fromStore.MailAppState>
|
||||
)
|
||||
{
|
||||
this.labels$ = this.store.select(fromStore.getLabelsArr);
|
||||
}
|
||||
|
||||
ngOnInit()
|
||||
{
|
||||
}
|
||||
|
||||
ngOnChanges()
|
||||
{
|
||||
this.updateModel(this.mailInput);
|
||||
}
|
||||
|
||||
toggleStar(event)
|
||||
{
|
||||
event.stopPropagation();
|
||||
this.mail.toggleStar();
|
||||
this.updateMail();
|
||||
}
|
||||
|
||||
toggleImportant(event)
|
||||
{
|
||||
event.stopPropagation();
|
||||
this.mail.toggleImportant();
|
||||
this.updateMail();
|
||||
}
|
||||
|
||||
updateModel(data)
|
||||
{
|
||||
this.mail = !data ? null : new Mail({...data});
|
||||
}
|
||||
|
||||
updateMail()
|
||||
{
|
||||
this.store.dispatch(new fromStore.UpdateMail(this.mail));
|
||||
this.updateModel(this.mail);
|
||||
}
|
||||
|
||||
ngOnDestroy()
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
<div fxLayout="row" fxLayoutAlign="start center">
|
||||
|
||||
<mat-checkbox [checked]="selected" (change)="onSelectedChange()"
|
||||
(click)="$event.stopPropagation();">
|
||||
</mat-checkbox>
|
||||
|
||||
<div class="info" fxFlex FlexLayout="column">
|
||||
|
||||
<div class="row-1" fxLayout="row" fxLayoutAlign="start center">
|
||||
|
||||
<div class="name" fxLayout="row" fxLayoutAlign="start center" fxFlex>
|
||||
<img class="avatar" *ngIf="mail.from?.avatar" alt="{{mail.from?.name}}" src="{{mail.from?.avatar}}"/>
|
||||
<div class="avatar" *ngIf="!mail.from?.avatar">{{mail.from?.name[0]}}</div>
|
||||
<span class="text-truncate" *ngIf="mail?.from">{{mail.from?.name}}</span>
|
||||
<mat-icon *ngIf="mail.hasAttachments">attachment</mat-icon>
|
||||
</div>
|
||||
|
||||
<div fxLayout="row" fxLayoutAlign="start center">
|
||||
<div class="time">{{mail.time}}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row-2" fxLayout="row" fxLayoutAlign="start center">
|
||||
|
||||
<div fxLayout="column" fxLayoutAlign="start start">
|
||||
|
||||
<div class="subject text-truncate">
|
||||
{{mail.subject}}
|
||||
</div>
|
||||
|
||||
<div class="message text-truncate" *ngIf="mail?.message">
|
||||
{{mail.message | htmlToPlaintext | slice:0:180}}{{mail.message.length > 180 ? '...' : ''}}
|
||||
</div>
|
||||
|
||||
<div class="labels" fxLayout="row" fxLayoutWrap fxHide fxShow.gt-sm>
|
||||
<div class="label" *ngFor="let labelId of mail.labels"
|
||||
fxLayout="row" fxLayoutAlign="start center">
|
||||
<div class="label-color"
|
||||
[ngStyle]="{'background-color': (labels$ | async) | getById:labelId:'color'}"></div>
|
||||
<div class="label-title">{{(labels$ | async) | getById:labelId:'title'}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,129 @@
|
|||
@import 'src/app/core/scss/fuse';
|
||||
|
||||
:host {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
cursor: pointer;
|
||||
|
||||
&.unread {
|
||||
background: #FFFFFF;
|
||||
|
||||
.info {
|
||||
|
||||
.name {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.row-2 {
|
||||
|
||||
.subject {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.labels {
|
||||
background: #FFFFFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: #FFF8E1;
|
||||
|
||||
.info {
|
||||
|
||||
.row-2 {
|
||||
|
||||
.labels {
|
||||
background: #FFF8E1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.current-mail {
|
||||
background: #E3F2FD;
|
||||
|
||||
.info {
|
||||
|
||||
.row-2 {
|
||||
|
||||
.labels {
|
||||
background: #E3F2FD;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
overflow: hidden;
|
||||
width: 0;
|
||||
margin-left: 16px;
|
||||
position: relative;
|
||||
|
||||
.row-1 {
|
||||
margin-bottom: 8px;
|
||||
|
||||
.name {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
|
||||
.avatar {
|
||||
min-width: 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
background-color: mat-color($accent);
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-left: 4px;
|
||||
|
||||
.mat-icon-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.row-2 {
|
||||
|
||||
.subject,
|
||||
.message {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.message {
|
||||
position: relative;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
}
|
||||
|
||||
.labels {
|
||||
position: absolute;
|
||||
background: #FFFFFF;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
padding-left: 6px;
|
||||
|
||||
.label {
|
||||
font-size: 11px;
|
||||
border-radius: 2px;
|
||||
margin: 0 4px 0 0;
|
||||
padding: 3px 8px;
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
|
||||
.label-color {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-right: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Mail } from '../../mail.model';
|
||||
import { MailNgrxService } from '../../mail.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import * as fromStore from '../../store';
|
||||
|
||||
@Component({
|
||||
selector : 'fuse-mail-list-item',
|
||||
templateUrl : './mail-list-item.component.html',
|
||||
styleUrls : ['./mail-list-item.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class FuseMailNgrxListItemComponent implements OnInit, OnDestroy
|
||||
{
|
||||
@Input() mail: Mail;
|
||||
@HostBinding('class.selected') selected: boolean;
|
||||
labels$: Observable<any>;
|
||||
selectedMailIds$: Observable<any>;
|
||||
|
||||
constructor(
|
||||
private mailService: MailNgrxService,
|
||||
private store: Store<fromStore.MailAppState>,
|
||||
private cd: ChangeDetectorRef
|
||||
)
|
||||
{
|
||||
this.labels$ = this.store.select(fromStore.getLabelsArr);
|
||||
this.selectedMailIds$ = this.store.select(fromStore.getSelectedMailIds);
|
||||
this.selected = false;
|
||||
}
|
||||
|
||||
ngOnInit()
|
||||
{
|
||||
// Set the initial values
|
||||
this.mail = new Mail(this.mail);
|
||||
|
||||
this.selectedMailIds$.subscribe((selectedMailIds) => {
|
||||
this.selected = selectedMailIds.length > 0 && selectedMailIds.find(id => id === this.mail.id) !== undefined;
|
||||
this.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
refresh()
|
||||
{
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onSelectedChange()
|
||||
{
|
||||
this.store.dispatch(new fromStore.ToggleInSelectedMails(this.mail.id));
|
||||
}
|
||||
|
||||
ngOnDestroy()
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<div *ngIf="mails.length === 0" fxLayout="column" fxLayoutAlign="center center" fxFlexFill>
|
||||
<span class="no-messages-text hint-text">{{ 'MAIL.NO_MESSAGES' | translate }}</span>
|
||||
</div>
|
||||
|
||||
<div class="mail-list">
|
||||
<fuse-mail-list-item matRipple *ngFor="let mail of mails" [mail]="mail" (click)="readMail(mail.id)"
|
||||
[ngClass]="{'current-mail':mail?.id == currentMail?.id}">
|
||||
</fuse-mail-list-item>
|
||||
</div>
|
|
@ -0,0 +1,21 @@
|
|||
:host {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
border-right: 1px solid rgba(0, 0, 0, .12);
|
||||
|
||||
.no-messages-text {
|
||||
font-size: 24px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.mail-list {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Mail } from '../mail.model';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { MailNgrxService } from '../mail.service';
|
||||
|
||||
@Component({
|
||||
selector : 'fuse-mail-list',
|
||||
templateUrl : './mail-list.component.html',
|
||||
styleUrls : ['./mail-list.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class FuseMailNgrxListComponent implements OnInit, OnDestroy
|
||||
{
|
||||
@Input() mails: Mail[];
|
||||
currentMail: Mail;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private mailService: MailNgrxService,
|
||||
private router: Router
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
ngOnInit()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Read mail
|
||||
* @param mailId
|
||||
*/
|
||||
readMail(mailId)
|
||||
{
|
||||
const labelHandle = this.route.snapshot.params.labelHandle,
|
||||
filterHandle = this.route.snapshot.params.filterHandle,
|
||||
folderHandle = this.route.snapshot.params.folderHandle;
|
||||
|
||||
if ( labelHandle )
|
||||
{
|
||||
this.router.navigate(['apps/mail-ngrx/label/' + labelHandle + '/' + mailId]);
|
||||
}
|
||||
else if ( filterHandle )
|
||||
{
|
||||
this.router.navigate(['apps/mail-ngrx/filter/' + filterHandle + '/' + mailId]);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.router.navigate(['apps/mail-ngrx/' + folderHandle + '/' + mailId]);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy()
|
||||
{
|
||||
}
|
||||
}
|
116
src/app/main/content/apps/mail-ngrx/mail.component.html
Normal file
116
src/app/main/content/apps/mail-ngrx/mail.component.html
Normal file
|
@ -0,0 +1,116 @@
|
|||
<div id="mail" class="page-layout carded left-sidenav" fusePerfectScrollbar>
|
||||
|
||||
<!-- TOP BACKGROUND -->
|
||||
<div class="top-bg mat-accent-bg"></div>
|
||||
<!-- / TOP BACKGROUND -->
|
||||
|
||||
<mat-sidenav-container>
|
||||
|
||||
<!-- SIDENAV -->
|
||||
<mat-sidenav class="sidenav mat-sidenav-opened" align="start" mode="side" opened="true"
|
||||
fuseMatSidenavHelper="carded-left-sidenav" mat-is-locked-open="gt-md">
|
||||
<fuse-mail-main-sidenav></fuse-mail-main-sidenav>
|
||||
</mat-sidenav>
|
||||
<!-- / SIDENAV -->
|
||||
|
||||
<!-- CENTER -->
|
||||
<div class="center">
|
||||
|
||||
<!-- CONTENT HEADER -->
|
||||
<div class="header" fxLayout="row" fxLayoutAlign="start center">
|
||||
|
||||
<div class="search-wrapper" fxFlex fxLayout="row" fxLayoutAlign="start center">
|
||||
|
||||
<button mat-button class="mat-icon-button sidenav-toggle"
|
||||
fuseMatSidenavToggler="carded-left-sidenav"
|
||||
fxHide.gt-md aria-label="Toggle Sidenav">
|
||||
<mat-icon>menu</mat-icon>
|
||||
</button>
|
||||
|
||||
<div class="search mat-white-bg" flex fxLayout="row" fxLayoutAlign="start center">
|
||||
<mat-icon>search</mat-icon>
|
||||
<input [formControl]="searchInput" [placeholder]="'MAIL.SEARCH_PLACEHOLDER' | translate" fxFlex>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / CONTENT HEADER -->
|
||||
|
||||
<!-- CONTENT CARD -->
|
||||
<div class="content-card mat-white-bg" [ngClass]="{'current-mail-selected':currentMail}">
|
||||
|
||||
<!-- CONTENT TOOLBAR -->
|
||||
<div class="toolbar px-24 py-8">
|
||||
|
||||
<div class="mail-selection" fxFlex="row" fxLayoutAlign="start center">
|
||||
|
||||
<mat-checkbox (click)="toggleSelectAll($event)"
|
||||
[checked]="hasSelectedMails"
|
||||
[indeterminate]="isIndeterminate">
|
||||
</mat-checkbox>
|
||||
|
||||
<button mat-icon-button [matMenuTriggerFor]="selectMenu">
|
||||
<mat-icon>arrow_drop_down</mat-icon>
|
||||
</button>
|
||||
<mat-menu #selectMenu="matMenu">
|
||||
<button mat-menu-item (click)="selectAllMails()">All</button>
|
||||
<button mat-menu-item (click)="deselectAllMails()">None</button>
|
||||
<button mat-menu-item (click)="selectMailsByParameter('read', true)">Read</button>
|
||||
<button mat-menu-item (click)="selectMailsByParameter('read', false)">Unread</button>
|
||||
<button mat-menu-item (click)="selectMailsByParameter('starred', true)">Starred</button>
|
||||
<button mat-menu-item (click)="selectMailsByParameter('starred', false)">Unstarred</button>
|
||||
<button mat-menu-item (click)="selectMailsByParameter('important', true)">Important</button>
|
||||
<button mat-menu-item (click)="selectMailsByParameter('important', false)">Unimportant</button>
|
||||
</mat-menu>
|
||||
|
||||
<div class="toolbar-separator" *ngIf="hasSelectedMails"></div>
|
||||
|
||||
<button mat-icon-button (click)="setFolderOnSelectedMails(4)" *ngIf="hasSelectedMails">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
|
||||
<button mat-icon-button [matMenuTriggerFor]="folderMenu" *ngIf="hasSelectedMails">
|
||||
<mat-icon>folder</mat-icon>
|
||||
</button>
|
||||
<mat-menu #folderMenu="matMenu">
|
||||
<button mat-menu-item *ngFor="let folder of folders$ | async"
|
||||
(click)="setFolderOnSelectedMails(folder.id)">{{folder.title}}
|
||||
</button>
|
||||
</mat-menu>
|
||||
|
||||
<button mat-icon-button [matMenuTriggerFor]="labelMenu" *ngIf="hasSelectedMails">
|
||||
<mat-icon>label</mat-icon>
|
||||
</button>
|
||||
<mat-menu #labelMenu="matMenu">
|
||||
<button mat-menu-item *ngFor="let label of labels$ | async"
|
||||
(click)="toggleLabelOnSelectedMails(label.id)">{{label.title}}
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
|
||||
<div *ngIf="currentMail" fxHide.gt-lg>
|
||||
<button mat-icon-button (click)="deSelectCurrentMail()">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / CONTENT TOOLBAR -->
|
||||
|
||||
<!-- CONTENT -->
|
||||
<div class="content" fxLayoutAlign="row">
|
||||
|
||||
<fuse-mail-list fusePerfectScrollbar fxFlex [mails]="mails$ | async"></fuse-mail-list>
|
||||
|
||||
<fuse-mail-details [mail]="currentMail$ | async" fusePerfectScrollbar fxFlex></fuse-mail-details>
|
||||
|
||||
</div>
|
||||
<!-- / CONTENT -->
|
||||
|
||||
</div>
|
||||
<!-- / CONTENT CARD -->
|
||||
|
||||
</div>
|
||||
<!-- / CENTER -->
|
||||
|
||||
</mat-sidenav-container>
|
||||
|
||||
</div>
|
81
src/app/main/content/apps/mail-ngrx/mail.component.scss
Normal file
81
src/app/main/content/apps/mail-ngrx/mail.component.scss
Normal file
|
@ -0,0 +1,81 @@
|
|||
@import "src/app/core/scss/fuse";
|
||||
|
||||
:host {
|
||||
width: 100%;
|
||||
|
||||
.center {
|
||||
|
||||
.header {
|
||||
|
||||
.search-wrapper {
|
||||
@include mat-elevation(7);
|
||||
|
||||
.sidenav-toggle {
|
||||
margin: 0;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: #FFF;
|
||||
border-radius: 0;
|
||||
border-right: 1px solid rgba(0, 0, 0, .12);
|
||||
}
|
||||
|
||||
.search {
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
line-height: 56px;
|
||||
padding: 18px;
|
||||
|
||||
input {
|
||||
height: 56px;
|
||||
padding-left: 16px;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-card {
|
||||
|
||||
@include media-breakpoint(xs) {
|
||||
|
||||
fuse-mail-list {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
fuse-mail-list,
|
||||
fuse-mail-details {
|
||||
flex: 1 0 100%;
|
||||
}
|
||||
|
||||
fuse-mail-details {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
&.current-mail-selected {
|
||||
|
||||
.toolbar {
|
||||
padding-left: 16px !important;
|
||||
|
||||
.mail-selection {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
|
||||
fuse-mail-list {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
fuse-mail-details {
|
||||
display: flex !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
128
src/app/main/content/apps/mail-ngrx/mail.component.ts
Normal file
128
src/app/main/content/apps/mail-ngrx/mail.component.ts
Normal file
|
@ -0,0 +1,128 @@
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MailNgrxService } from './mail.service';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { Mail } from './mail.model';
|
||||
import { FuseTranslationLoaderService } from '../../../../core/services/translation-loader.service';
|
||||
import { locale as english } from './i18n/en';
|
||||
import { locale as turkish } from './i18n/tr';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import * as fromStore from './store';
|
||||
|
||||
@Component({
|
||||
selector : 'fuse-mail',
|
||||
templateUrl : './mail.component.html',
|
||||
styleUrls : ['./mail.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class FuseMailNgrxComponent implements OnInit, OnDestroy
|
||||
{
|
||||
hasSelectedMails: boolean;
|
||||
isIndeterminate: boolean;
|
||||
searchInput: FormControl;
|
||||
mails$: Observable<any>;
|
||||
folders$: Observable<any>;
|
||||
labels$: Observable<any>;
|
||||
currentMail$: Observable<Mail>;
|
||||
selectedMailIds$: Observable<string[]>;
|
||||
searchText$: Observable<string>;
|
||||
mails: Mail[];
|
||||
selectedMailIds: string[];
|
||||
|
||||
constructor(
|
||||
private mailService: MailNgrxService,
|
||||
private translationLoader: FuseTranslationLoaderService,
|
||||
private store: Store<fromStore.MailAppState>,
|
||||
private cd: ChangeDetectorRef
|
||||
)
|
||||
{
|
||||
this.searchInput = new FormControl('');
|
||||
this.translationLoader.loadTranslations(english, turkish);
|
||||
this.currentMail$ = this.store.select(fromStore.getCurrentMail);
|
||||
this.mails$ = this.store.select(fromStore.getMailsArr);
|
||||
this.folders$ = this.store.select(fromStore.getFoldersArr);
|
||||
this.labels$ = this.store.select(fromStore.getLabelsArr);
|
||||
this.selectedMailIds$ = this.store.select(fromStore.getSelectedMailIds);
|
||||
this.searchText$ = this.store.select(fromStore.getSearchText);
|
||||
this.mails = [];
|
||||
this.selectedMailIds = [];
|
||||
}
|
||||
|
||||
ngOnInit()
|
||||
{
|
||||
this.mails$.subscribe(mails => {
|
||||
this.mails = mails;
|
||||
});
|
||||
|
||||
this.selectedMailIds$
|
||||
.subscribe(selectedMailIds => {
|
||||
this.selectedMailIds = selectedMailIds;
|
||||
this.hasSelectedMails = selectedMailIds.length > 0;
|
||||
this.isIndeterminate = (selectedMailIds.length !== this.mails.length && selectedMailIds.length > 0);
|
||||
this.refresh();
|
||||
});
|
||||
|
||||
this.searchText$.subscribe(searchText => {
|
||||
this.searchInput.setValue(searchText);
|
||||
});
|
||||
|
||||
this.searchInput.valueChanges
|
||||
.debounceTime(300)
|
||||
.distinctUntilChanged()
|
||||
.subscribe(searchText => {
|
||||
this.store.dispatch(new fromStore.SetSearchText(searchText));
|
||||
});
|
||||
}
|
||||
|
||||
toggleSelectAll(ev)
|
||||
{
|
||||
ev.preventDefault();
|
||||
|
||||
if ( this.selectedMailIds.length && this.selectedMailIds.length > 0 )
|
||||
{
|
||||
this.deselectAllMails();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.selectAllMails();
|
||||
}
|
||||
}
|
||||
|
||||
selectAllMails()
|
||||
{
|
||||
this.store.dispatch(new fromStore.SelectAllMails());
|
||||
}
|
||||
|
||||
deselectAllMails()
|
||||
{
|
||||
this.store.dispatch(new fromStore.DeselectAllMails());
|
||||
}
|
||||
|
||||
selectMailsByParameter(parameter, value)
|
||||
{
|
||||
this.store.dispatch(new fromStore.SelectMailsByParameter({
|
||||
parameter,
|
||||
value
|
||||
}));
|
||||
}
|
||||
|
||||
toggleLabelOnSelectedMails(labelId)
|
||||
{
|
||||
this.store.dispatch(new fromStore.AddLabelOnSelectedMails(labelId));
|
||||
}
|
||||
|
||||
setFolderOnSelectedMails(folderId)
|
||||
{
|
||||
this.store.dispatch(new fromStore.SetFolderOnSelectedMails(folderId));
|
||||
}
|
||||
|
||||
refresh()
|
||||
{
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
ngOnDestroy()
|
||||
{
|
||||
this.cd.detach();
|
||||
}
|
||||
}
|
56
src/app/main/content/apps/mail-ngrx/mail.model.ts
Normal file
56
src/app/main/content/apps/mail-ngrx/mail.model.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
export class Mail
|
||||
{
|
||||
id: string;
|
||||
from: {
|
||||
name: string,
|
||||
avatar: string,
|
||||
email: string
|
||||
};
|
||||
to: {
|
||||
name: string,
|
||||
email: string
|
||||
}[];
|
||||
subject: string;
|
||||
message: string;
|
||||
time: string;
|
||||
read: boolean;
|
||||
starred: boolean;
|
||||
important: boolean;
|
||||
hasAttachments: boolean;
|
||||
attachments: {
|
||||
type: string,
|
||||
fileName: string,
|
||||
preview: string,
|
||||
url: string,
|
||||
size: string
|
||||
}[];
|
||||
labels: string[];
|
||||
folder: string;
|
||||
|
||||
constructor(mail)
|
||||
{
|
||||
this.id = mail.id;
|
||||
this.from = mail.from;
|
||||
this.to = mail.to;
|
||||
this.subject = mail.subject;
|
||||
this.message = mail.message;
|
||||
this.time = mail.time;
|
||||
this.read = mail.read;
|
||||
this.starred = mail.starred;
|
||||
this.important = mail.important;
|
||||
this.hasAttachments = mail.hasAttachments;
|
||||
this.attachments = mail.attachments;
|
||||
this.labels = mail.labels;
|
||||
this.folder = mail.folder;
|
||||
}
|
||||
|
||||
toggleStar()
|
||||
{
|
||||
this.starred = !this.starred;
|
||||
}
|
||||
|
||||
toggleImportant()
|
||||
{
|
||||
this.important = !this.important;
|
||||
}
|
||||
}
|
73
src/app/main/content/apps/mail-ngrx/mail.module.ts
Normal file
73
src/app/main/content/apps/mail-ngrx/mail.module.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { SharedModule } from '../../../../core/modules/shared.module';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { FuseMailNgrxComponent } from './mail.component';
|
||||
import { FuseMailNgrxMainSidenavComponent } from './sidenavs/main/main-sidenav.component';
|
||||
import { FuseMailNgrxListItemComponent } from './mail-list/mail-list-item/mail-list-item.component';
|
||||
import { FuseMailNgrxListComponent } from './mail-list/mail-list.component';
|
||||
import { FuseMailNgrxDetailsComponent } from './mail-details/mail-details.component';
|
||||
import { MailNgrxService } from './mail.service';
|
||||
import { FuseMailNgrxComposeDialogComponent } from './dialogs/compose/compose.component';
|
||||
import { MailAppStoreModule } from './store/store.module';
|
||||
import * as fromGuards from './store/guards/index';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path : 'label/:labelHandle',
|
||||
component : FuseMailNgrxComponent,
|
||||
canActivate: [fromGuards.ResolveGuard]
|
||||
},
|
||||
{
|
||||
path : 'label/:labelHandle/:mailId',
|
||||
component : FuseMailNgrxComponent,
|
||||
canActivate: [fromGuards.ResolveGuard]
|
||||
},
|
||||
{
|
||||
path : 'filter/:filterHandle',
|
||||
component: FuseMailNgrxComponent,
|
||||
canActivate: [fromGuards.ResolveGuard]
|
||||
},
|
||||
{
|
||||
path : 'filter/:filterHandle/:mailId',
|
||||
component: FuseMailNgrxComponent,
|
||||
canActivate: [fromGuards.ResolveGuard]
|
||||
},
|
||||
{
|
||||
path : ':folderHandle',
|
||||
component: FuseMailNgrxComponent,
|
||||
canActivate: [fromGuards.ResolveGuard]
|
||||
},
|
||||
{
|
||||
path : ':folderHandle/:mailId',
|
||||
component: FuseMailNgrxComponent,
|
||||
canActivate: [fromGuards.ResolveGuard]
|
||||
},
|
||||
{
|
||||
path : '**',
|
||||
redirectTo: 'inbox'
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
declarations : [
|
||||
FuseMailNgrxComponent,
|
||||
FuseMailNgrxListComponent,
|
||||
FuseMailNgrxListItemComponent,
|
||||
FuseMailNgrxDetailsComponent,
|
||||
FuseMailNgrxMainSidenavComponent,
|
||||
FuseMailNgrxComposeDialogComponent
|
||||
],
|
||||
imports : [
|
||||
SharedModule,
|
||||
RouterModule.forChild(routes),
|
||||
MailAppStoreModule
|
||||
],
|
||||
providers : [
|
||||
MailNgrxService,
|
||||
fromGuards.ResolveGuard
|
||||
],
|
||||
entryComponents: [FuseMailNgrxComposeDialogComponent]
|
||||
})
|
||||
export class FuseMailNgrxModule
|
||||
{
|
||||
}
|
87
src/app/main/content/apps/mail-ngrx/mail.service.ts
Normal file
87
src/app/main/content/apps/mail-ngrx/mail.service.ts
Normal file
|
@ -0,0 +1,87 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Mail } from './mail.model';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { MailAppState } from './store/reducers';
|
||||
import { getFiltersArr, getFoldersArr, getLabelsArr, getMailsArr } from './store/selectors';
|
||||
|
||||
@Injectable()
|
||||
export class MailNgrxService
|
||||
{
|
||||
foldersArr: any;
|
||||
filtersArr: any;
|
||||
labelsArr: any;
|
||||
selectedMails: Mail[];
|
||||
mails: Mail[];
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private store: Store<MailAppState>
|
||||
)
|
||||
{
|
||||
this.store.select(getFoldersArr).subscribe(folders => {
|
||||
this.foldersArr = folders;
|
||||
});
|
||||
this.store.select(getFiltersArr).subscribe(filters => {
|
||||
this.filtersArr = filters;
|
||||
});
|
||||
this.store.select(getLabelsArr).subscribe(labels => {
|
||||
this.labelsArr = labels;
|
||||
});
|
||||
this.store.select(getMailsArr).subscribe(mails => {
|
||||
this.mails = mails;
|
||||
});
|
||||
|
||||
this.selectedMails = [];
|
||||
}
|
||||
|
||||
getAllMails(): Observable<Mail[]>
|
||||
{
|
||||
return this.http.get<Mail[]>('api/mail-mails');
|
||||
}
|
||||
|
||||
getFolders(): Observable<any>
|
||||
{
|
||||
return this.http.get('api/mail-folders');
|
||||
}
|
||||
|
||||
getFilters(): Observable<any>
|
||||
{
|
||||
return this.http.get('api/mail-filters');
|
||||
}
|
||||
|
||||
getLabels(): Observable<any>
|
||||
{
|
||||
return this.http.get('api/mail-labels');
|
||||
}
|
||||
|
||||
getMails(handle): Observable<Mail[]>
|
||||
{
|
||||
if ( handle.id === 'labelHandle' )
|
||||
{
|
||||
const labelId = this.labelsArr.find(label => label.handle === handle.value).id;
|
||||
return this.http.get<Mail[]>('api/mail-mails?labels=' + labelId);
|
||||
}
|
||||
else if ( handle.id === 'filterHandle' )
|
||||
{
|
||||
return this.http.get<Mail[]>('api/mail-mails?' + handle.value + '=true');
|
||||
}
|
||||
else // folderHandle
|
||||
{
|
||||
const folderId = this.foldersArr.find(folder => folder.handle === handle.value).id;
|
||||
return this.http.get<any>('api/mail-mails?folder=' + folderId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the mail
|
||||
* @param mail
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
updateMail(mail)
|
||||
{
|
||||
return this.http.post('api/mail-mails/' + mail.id, {...mail});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
<!-- SIDENAV HEADER -->
|
||||
<div fxLayout="column" fxLayoutAlign="space-between start"
|
||||
class="header p-24 pb-4" ngClass="mat-accent-bg" ngClass.gt-md="white-fg">
|
||||
|
||||
<div class="logo" fxFlex fxLayout="row" fxLayoutAlign="start center">
|
||||
<mat-icon class="logo-icon s-32">mail</mat-icon>
|
||||
<span class="logo-text">Mailbox Ngrx</span>
|
||||
</div>
|
||||
|
||||
<div class="account" fxLayout="column">
|
||||
<div class="title">John Doe</div>
|
||||
<mat-form-field floatPlaceholder="never">
|
||||
<mat-select class="account-selection" placeholder="Mail Selection"
|
||||
[ngModel]="selectedAccount">
|
||||
<mat-option *ngFor="let account of (accounts | keys)" [value]="account.key">
|
||||
{{account.value}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- / SIDENAV HEADER -->
|
||||
|
||||
<!-- SIDENAV CONTENT -->
|
||||
<div class="content" fusePerfectScrollbar>
|
||||
|
||||
<div class="p-24">
|
||||
<button mat-raised-button fxFlex
|
||||
class="mat-accent compose-dialog-button"
|
||||
(click)="composeDialog()"
|
||||
aria-label="Compose">
|
||||
{{ 'MAIL.COMPOSE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="nav">
|
||||
|
||||
<div class="nav-subheader">{{ 'MAIL.FOLDERS' | translate }}</div>
|
||||
|
||||
<div class="nav-item" *ngFor="let folder of (folders$ | async)">
|
||||
<a class="nav-link" matRipple [routerLink]="'/apps/mail-ngrx/' + folder.handle" routerLinkActive="active">
|
||||
<mat-icon class="nav-link-icon" *ngIf="folder.icon">{{folder.icon}}</mat-icon>
|
||||
<span>{{folder.title}}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-subheader">{{ 'MAIL.FILTERS' | translate }}</div>
|
||||
|
||||
<div class="nav-item" *ngFor="let filter of (filters$ | async)">
|
||||
<a class="nav-link" matRipple [routerLink]="'/apps/mail-ngrx/filter/' + filter.handle" routerLinkActive="active">
|
||||
<mat-icon class="nav-link-icon" *ngIf="filter.icon">{{filter.icon}}</mat-icon>
|
||||
<span>{{filter.title}}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-subheader">{{ 'MAIL.LABELS' | translate }}</div>
|
||||
|
||||
<div class="nav-item" *ngFor="let label of (labels$ | async)">
|
||||
<a class="nav-link" matRipple [routerLink]="'/apps/mail-ngrx/label/' + label.handle" routerLinkActive="active">
|
||||
<mat-icon class="nav-link-icon" [ngStyle]="{'color':label.color}">label</mat-icon>
|
||||
<span>{{label.title}}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- / SIDENAV CONTENT -->
|
|
@ -0,0 +1,30 @@
|
|||
:host {
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.header {
|
||||
|
||||
.logo {
|
||||
|
||||
.logo-icon {
|
||||
margin: 0 16px 0 0;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.account {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
|
||||
.compose-dialog-button {
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FuseMailNgrxComposeDialogComponent } from '../../dialogs/compose/compose.component';
|
||||
import { MatDialog } from '@angular/material';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Store } from '@ngrx/store';
|
||||
import * as fromStore from './../../store';
|
||||
import { MailNgrxService } from '../../mail.service';
|
||||
|
||||
@Component({
|
||||
selector : 'fuse-mail-main-sidenav',
|
||||
templateUrl : './main-sidenav.component.html',
|
||||
styleUrls : ['./main-sidenav.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class FuseMailNgrxMainSidenavComponent implements OnInit, OnDestroy
|
||||
{
|
||||
labels: any[];
|
||||
accounts: object;
|
||||
selectedAccount: string;
|
||||
dialogRef: any;
|
||||
|
||||
folders$: Observable<any>;
|
||||
filters$: Observable<any>;
|
||||
labels$: Observable<any>;
|
||||
|
||||
constructor(
|
||||
private mailService: MailNgrxService,
|
||||
public dialog: MatDialog,
|
||||
private store: Store<fromStore.MailAppState>
|
||||
)
|
||||
{
|
||||
// Data
|
||||
this.accounts = {
|
||||
'creapond' : 'johndoe@creapond.com',
|
||||
'withinpixels': 'johndoe@withinpixels.com'
|
||||
};
|
||||
|
||||
this.selectedAccount = 'creapond';
|
||||
|
||||
this.folders$ = this.store.select(fromStore.getFoldersArr);
|
||||
this.filters$ = this.store.select(fromStore.getFiltersArr);
|
||||
this.labels$ = this.store.select(fromStore.getLabelsArr);
|
||||
}
|
||||
|
||||
ngOnInit()
|
||||
{
|
||||
}
|
||||
|
||||
composeDialog()
|
||||
{
|
||||
this.dialogRef = this.dialog.open(FuseMailNgrxComposeDialogComponent, {
|
||||
panelClass: 'mail-compose-dialog'
|
||||
});
|
||||
this.dialogRef.afterClosed()
|
||||
.subscribe(response => {
|
||||
if ( !response )
|
||||
{
|
||||
return;
|
||||
}
|
||||
const actionType: string = response[0];
|
||||
const formData: FormGroup = response[1];
|
||||
switch ( actionType )
|
||||
{
|
||||
/**
|
||||
* Send
|
||||
*/
|
||||
case 'send':
|
||||
console.log('new Mail', formData.getRawValue());
|
||||
break;
|
||||
/**
|
||||
* Delete
|
||||
*/
|
||||
case 'delete':
|
||||
console.log('delete Mail');
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy()
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import { Action } from '@ngrx/store';
|
||||
|
||||
export const GET_FILTERS = '[FILTERS] GET FILTERS';
|
||||
export const GET_FILTERS_SUCCESS = '[FILTERS] GET FILTERS SUCCESS';
|
||||
export const GET_FILTERS_FAILED = '[FILTERS] GET FILTERS FAILED';
|
||||
|
||||
/**
|
||||
* Get Filters
|
||||
*/
|
||||
export class GetFilters implements Action
|
||||
{
|
||||
readonly type = GET_FILTERS;
|
||||
|
||||
constructor(public payload: any)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Filters Success
|
||||
*/
|
||||
export class GetFiltersSuccess implements Action
|
||||
{
|
||||
readonly type = GET_FILTERS_SUCCESS;
|
||||
|
||||
constructor(public payload: any)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Filters Failed
|
||||
*/
|
||||
export class GetFiltersFailed implements Action
|
||||
{
|
||||
readonly type = GET_FILTERS_FAILED;
|
||||
|
||||
constructor(public payload: string)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
export type FiltersActionsAll
|
||||
= GetFilters
|
||||
| GetFiltersSuccess
|
||||
| GetFiltersFailed;
|
|
@ -0,0 +1,46 @@
|
|||
import { Action } from '@ngrx/store';
|
||||
|
||||
export const GET_FOLDERS = '[FOLDERS] GET FOLDERS';
|
||||
export const GET_FOLDERS_SUCCESS = '[FOLDERS] GET FOLDERS SUCCESS';
|
||||
export const GET_FOLDERS_FAILED = '[FOLDERS] GET FOLDERS FAILED';
|
||||
|
||||
/**
|
||||
* Get Folders
|
||||
*/
|
||||
export class GetFolders implements Action
|
||||
{
|
||||
readonly type = GET_FOLDERS;
|
||||
|
||||
constructor(public payload: any)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Folders Success
|
||||
*/
|
||||
export class GetFoldersSuccess implements Action
|
||||
{
|
||||
readonly type = GET_FOLDERS_SUCCESS;
|
||||
|
||||
constructor(public payload: any)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Folders Failed
|
||||
*/
|
||||
export class GetFoldersFailed implements Action
|
||||
{
|
||||
readonly type = GET_FOLDERS_FAILED;
|
||||
|
||||
constructor(public payload: string)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
export type FoldersActionsAll
|
||||
= GetFolders
|
||||
| GetFoldersSuccess
|
||||
| GetFoldersFailed;
|
|
@ -0,0 +1,4 @@
|
|||
export * from './mails.actions';
|
||||
export * from './folders.actions';
|
||||
export * from './filters.actions';
|
||||
export * from './labels.actions';
|
|
@ -0,0 +1,46 @@
|
|||
import { Action } from '@ngrx/store';
|
||||
|
||||
export const GET_LABELS = '[LABELS] GET LABELS';
|
||||
export const GET_LABELS_SUCCESS = '[LABELS] GET LABELS SUCCESS';
|
||||
export const GET_LABELS_FAILED = '[LABELS] GET LABELS FAILED';
|
||||
|
||||
/**
|
||||
* Get Labels
|
||||
*/
|
||||
export class GetLabels implements Action
|
||||
{
|
||||
readonly type = GET_LABELS;
|
||||
|
||||
constructor(public payload: any)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Labels Success
|
||||
*/
|
||||
export class GetLabelsSuccess implements Action
|
||||
{
|
||||
readonly type = GET_LABELS_SUCCESS;
|
||||
|
||||
constructor(public payload: any)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Labels Failed
|
||||
*/
|
||||
export class GetLabelsFailed implements Action
|
||||
{
|
||||
readonly type = GET_LABELS_FAILED;
|
||||
|
||||
constructor(public payload: string)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
export type LabelsActionsAll
|
||||
= GetLabels
|
||||
| GetLabelsSuccess
|
||||
| GetLabelsFailed;
|
|
@ -0,0 +1,243 @@
|
|||
import { Action } from '@ngrx/store';
|
||||
import { Mail } from '../../mail.model';
|
||||
|
||||
export const GET_MAILS = '[MAILS] GET MAILS';
|
||||
export const GET_MAILS_SUCCESS = '[MAILS] GET MAILS SUCCESS';
|
||||
export const GET_MAILS_FAILED = '[MAILS] GET MAILS FAILED';
|
||||
export const SET_CURRENT_MAIL = '[MAILS] SET CURRENT MAIL';
|
||||
export const SET_CURRENT_MAIL_SUCCESS = '[MAILS] SET CURRENT MAIL SUCCESS';
|
||||
export const CHECK_CURRENT_MAIL = '[MAILS] CHECK CURRENT MAIL';
|
||||
export const UPDATE_MAIL = '[MAILS] UPDATE MAIL';
|
||||
export const UPDATE_MAIL_SUCCESS = '[MAILS] UPDATE MAIL SUCCESS';
|
||||
export const UPDATE_MAILS = '[MAILS] UPDATE MAILS';
|
||||
export const UPDATE_MAILS_SUCCESS = '[MAILS] UPDATE MAILS SUCCESS';
|
||||
export const SET_SEARCH_TEXT = '[MAILS] SET SEARCH TEXT';
|
||||
export const SELECT_ALL_MAILS = '[MAILS] SELECT ALL MAILS';
|
||||
export const DESELECT_ALL_MAILS = '[MAILS] DESELECT ALL MAILS';
|
||||
export const TOGGLE_IN_SELECTED_MAILS = '[MAILS] TOGGLE IN SELECTED MAILS';
|
||||
export const SELECT_MAILS_BY_PARAMETER = '[MAILS] SELECT MAILS BY PARAMETER';
|
||||
export const SET_FOLDER_ON_SELECTED_MAILS = '[MAILS] SET FOLDER ON SELECTED MAILS';
|
||||
export const ADD_LABEL_ON_SELECTED_MAILS = '[MAILS] ADD LABEL ON SELECTED MAILS';
|
||||
|
||||
/**
|
||||
* Get Mails
|
||||
*/
|
||||
export class GetMails implements Action
|
||||
{
|
||||
readonly type = GET_MAILS;
|
||||
|
||||
constructor()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Mails Success
|
||||
*/
|
||||
export class GetMailsSuccess implements Action
|
||||
{
|
||||
readonly type = GET_MAILS_SUCCESS;
|
||||
|
||||
constructor(public payload: any)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Mails Failed
|
||||
*/
|
||||
export class GetMailsFailed implements Action
|
||||
{
|
||||
readonly type = GET_MAILS_FAILED;
|
||||
|
||||
constructor(public payload: string)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Current Mail
|
||||
*/
|
||||
export class SetCurrentMail implements Action
|
||||
{
|
||||
readonly type = SET_CURRENT_MAIL;
|
||||
|
||||
constructor(public payload: string)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Current Mail Success
|
||||
*/
|
||||
export class SetCurrentMailSuccess implements Action
|
||||
{
|
||||
readonly type = SET_CURRENT_MAIL_SUCCESS;
|
||||
|
||||
constructor(public payload: any)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Current Mail
|
||||
*/
|
||||
export class CheckCurrentMail implements Action
|
||||
{
|
||||
readonly type = CHECK_CURRENT_MAIL;
|
||||
|
||||
constructor()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Mail
|
||||
*/
|
||||
export class UpdateMail implements Action
|
||||
{
|
||||
readonly type = UPDATE_MAIL;
|
||||
|
||||
constructor(public payload: any)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Mail Success
|
||||
*/
|
||||
export class UpdateMailSuccess implements Action
|
||||
{
|
||||
readonly type = UPDATE_MAIL_SUCCESS;
|
||||
|
||||
constructor(public payload: Mail)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Mails
|
||||
*/
|
||||
export class UpdateMails implements Action
|
||||
{
|
||||
readonly type = UPDATE_MAILS;
|
||||
|
||||
constructor(public payload: Mail[])
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Mails Success
|
||||
*/
|
||||
export class UpdateMailsSuccess implements Action
|
||||
{
|
||||
readonly type = UPDATE_MAILS_SUCCESS;
|
||||
|
||||
constructor()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Search Text
|
||||
*/
|
||||
export class SetSearchText implements Action
|
||||
{
|
||||
readonly type = SET_SEARCH_TEXT;
|
||||
|
||||
constructor(public payload: string)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select All Mails
|
||||
*/
|
||||
export class SelectAllMails implements Action
|
||||
{
|
||||
readonly type = SELECT_ALL_MAILS;
|
||||
|
||||
constructor()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deselect All Mails
|
||||
*/
|
||||
export class DeselectAllMails implements Action
|
||||
{
|
||||
readonly type = DESELECT_ALL_MAILS;
|
||||
|
||||
constructor()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle In Selected Mails
|
||||
*/
|
||||
export class ToggleInSelectedMails implements Action
|
||||
{
|
||||
readonly type = TOGGLE_IN_SELECTED_MAILS;
|
||||
|
||||
constructor(public payload: string)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select Mails by Parameter
|
||||
*/
|
||||
export class SelectMailsByParameter implements Action
|
||||
{
|
||||
readonly type = SELECT_MAILS_BY_PARAMETER;
|
||||
|
||||
constructor(public payload: any)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Folder on Selected Mails
|
||||
*/
|
||||
export class SetFolderOnSelectedMails implements Action
|
||||
{
|
||||
readonly type = SET_FOLDER_ON_SELECTED_MAILS;
|
||||
|
||||
constructor(public payload: string)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add label on Selected Mails
|
||||
*/
|
||||
export class AddLabelOnSelectedMails implements Action
|
||||
{
|
||||
readonly type = ADD_LABEL_ON_SELECTED_MAILS;
|
||||
|
||||
constructor(public payload: string)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
export type MailsActionsAll
|
||||
= GetMails
|
||||
| GetMailsSuccess
|
||||
| GetMailsFailed
|
||||
| SetCurrentMail
|
||||
| SetCurrentMailSuccess
|
||||
| CheckCurrentMail
|
||||
| UpdateMail
|
||||
| UpdateMailSuccess
|
||||
| UpdateMails
|
||||
| UpdateMailsSuccess
|
||||
| SetSearchText
|
||||
| SelectAllMails
|
||||
| DeselectAllMails
|
||||
| ToggleInSelectedMails
|
||||
| SelectMailsByParameter
|
||||
| SetFolderOnSelectedMails
|
||||
| AddLabelOnSelectedMails;
|
|
@ -0,0 +1,40 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Actions, Effect } from '@ngrx/effects';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/operator/delay';
|
||||
import 'rxjs/add/operator/map';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import { catchError, map, switchMap } from 'rxjs/operators';
|
||||
import * as FiltersActions from '../actions/filters.actions';
|
||||
import { MailNgrxService } from '../../mail.service';
|
||||
|
||||
@Injectable()
|
||||
export class FiltersEffect
|
||||
{
|
||||
constructor(
|
||||
private actions: Actions,
|
||||
private mailService: MailNgrxService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filters from Server
|
||||
* @type {Observable<any>}
|
||||
*/
|
||||
@Effect()
|
||||
getFilters: Observable<FiltersActions.FiltersActionsAll> =
|
||||
this.actions
|
||||
.ofType<FiltersActions.GetFilters>(FiltersActions.GET_FILTERS)
|
||||
.pipe(
|
||||
switchMap((action) => {
|
||||
return this.mailService.getFilters()
|
||||
.pipe(
|
||||
map((filters: any) => {
|
||||
return new FiltersActions.GetFiltersSuccess(filters);
|
||||
}),
|
||||
catchError(err => of(new FiltersActions.GetFiltersFailed(err)))
|
||||
);
|
||||
}
|
||||
));
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Actions, Effect } from '@ngrx/effects';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/operator/delay';
|
||||
import 'rxjs/add/operator/map';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import { catchError, map, switchMap } from 'rxjs/operators';
|
||||
import * as FoldersActions from '../actions/folders.actions';
|
||||
import { MailNgrxService } from '../../mail.service';
|
||||
|
||||
@Injectable()
|
||||
export class FoldersEffect
|
||||
{
|
||||
constructor(
|
||||
private actions: Actions,
|
||||
private mailService: MailNgrxService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Folders from Server
|
||||
* @type {Observable<any>}
|
||||
*/
|
||||
@Effect()
|
||||
getFolders: Observable<FoldersActions.FoldersActionsAll> =
|
||||
this.actions
|
||||
.ofType<FoldersActions.GetFolders>(FoldersActions.GET_FOLDERS)
|
||||
.pipe(
|
||||
switchMap((action) => {
|
||||
return this.mailService.getFolders()
|
||||
.pipe(
|
||||
map((folders: any) => {
|
||||
return new FoldersActions.GetFoldersSuccess(folders);
|
||||
}),
|
||||
catchError(err => of(new FoldersActions.GetFoldersFailed(err)))
|
||||
);
|
||||
}
|
||||
));
|
||||
}
|
16
src/app/main/content/apps/mail-ngrx/store/effects/index.ts
Normal file
16
src/app/main/content/apps/mail-ngrx/store/effects/index.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { MailsEffect } from './mails.effects';
|
||||
import { FoldersEffect } from './folders.effects';
|
||||
import { FiltersEffect } from './filters.effects';
|
||||
import { LabelsEffect } from './labels.effects';
|
||||
|
||||
export const effects = [
|
||||
MailsEffect,
|
||||
FoldersEffect,
|
||||
FiltersEffect,
|
||||
LabelsEffect
|
||||
];
|
||||
|
||||
export * from './mails.effects';
|
||||
export * from './folders.effects';
|
||||
export * from './filters.effects';
|
||||
export * from './labels.effects';
|
|
@ -0,0 +1,40 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Actions, Effect } from '@ngrx/effects';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/operator/delay';
|
||||
import 'rxjs/add/operator/map';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import { catchError, map, switchMap } from 'rxjs/operators';
|
||||
import * as LabelsActions from '../actions/labels.actions';
|
||||
import { MailNgrxService } from '../../mail.service';
|
||||
|
||||
@Injectable()
|
||||
export class LabelsEffect
|
||||
{
|
||||
constructor(
|
||||
private actions: Actions,
|
||||
private mailService: MailNgrxService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Labels from Server
|
||||
* @type {Observable<any>}
|
||||
*/
|
||||
@Effect()
|
||||
getLabels: Observable<LabelsActions.LabelsActionsAll> =
|
||||
this.actions
|
||||
.ofType<LabelsActions.GetLabels>(LabelsActions.GET_LABELS)
|
||||
.pipe(
|
||||
switchMap((action) => {
|
||||
return this.mailService.getLabels()
|
||||
.pipe(
|
||||
map((labels: any) => {
|
||||
return new LabelsActions.GetLabelsSuccess(labels);
|
||||
}),
|
||||
catchError(err => of(new LabelsActions.GetLabelsFailed(err)))
|
||||
);
|
||||
}
|
||||
));
|
||||
}
|
|
@ -0,0 +1,254 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Action, Store } from '@ngrx/store';
|
||||
import { Actions, Effect } from '@ngrx/effects';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import { map, mergeMap, exhaustMap, withLatestFrom } from 'rxjs/operators';
|
||||
import { getRouterState, State } from '../../../../../../store/reducers';
|
||||
import { getMailsState } from '../selectors';
|
||||
import * as MailsActions from '../actions/mails.actions';
|
||||
import * as fromRoot from '../../../../../../store';
|
||||
|
||||
import { MailNgrxService } from '../../mail.service';
|
||||
import { Mail } from '../../mail.model';
|
||||
|
||||
@Injectable()
|
||||
export class MailsEffect
|
||||
{
|
||||
routerState: any;
|
||||
|
||||
constructor(
|
||||
private actions: Actions,
|
||||
private mailService: MailNgrxService,
|
||||
private store: Store<State>
|
||||
)
|
||||
{
|
||||
this.store.select(getRouterState).subscribe(routerState => {
|
||||
if ( routerState )
|
||||
{
|
||||
this.routerState = routerState.state;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Mails with router parameters
|
||||
* @type {Observable<any>}
|
||||
*/
|
||||
@Effect()
|
||||
getMails: Observable<MailsActions.MailsActionsAll> =
|
||||
this.actions
|
||||
.ofType<MailsActions.GetMails>(MailsActions.GET_MAILS)
|
||||
.pipe(
|
||||
exhaustMap((action) => {
|
||||
|
||||
let handle = {
|
||||
id : '',
|
||||
value: ''
|
||||
};
|
||||
|
||||
const routeParams = Observable.of('labelHandle', 'filterHandle', 'folderHandle');
|
||||
routeParams.subscribe(param => {
|
||||
if ( this.routerState.params[param] )
|
||||
{
|
||||
handle = {
|
||||
id : param,
|
||||
value: this.routerState.params[param]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return this.mailService.getMails(handle)
|
||||
.map((mails: Mail[]) => {
|
||||
|
||||
return new MailsActions.GetMailsSuccess({
|
||||
loaded: handle,
|
||||
mails : mails
|
||||
});
|
||||
})
|
||||
.catch(err => of(new MailsActions.GetMailsFailed(err)));
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Update Mail
|
||||
* @type {Observable<any>}
|
||||
*/
|
||||
@Effect()
|
||||
updateMail: Observable<MailsActions.MailsActionsAll> =
|
||||
this.actions
|
||||
.ofType<MailsActions.UpdateMail>(MailsActions.UPDATE_MAIL)
|
||||
.pipe(
|
||||
exhaustMap((action) => {
|
||||
return this.mailService.updateMail(action.payload)
|
||||
.map(() => {
|
||||
return new MailsActions.UpdateMailSuccess(action.payload);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* UpdateMails
|
||||
* @type {Observable<any>}
|
||||
*/
|
||||
@Effect()
|
||||
updateMails: Observable<MailsActions.MailsActionsAll> =
|
||||
this.actions
|
||||
.ofType<MailsActions.UpdateMails>(MailsActions.UPDATE_MAILS)
|
||||
.pipe(
|
||||
exhaustMap((action) => {
|
||||
return Observable.forkJoin(
|
||||
action.payload.map(mail => this.mailService.updateMail(mail)),
|
||||
() => {
|
||||
return new MailsActions.UpdateMailsSuccess();
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Set Current Mail
|
||||
* @type {Observable<SetCurrentMailSuccess>}
|
||||
*/
|
||||
@Effect()
|
||||
setCurrentMail: Observable<Action> =
|
||||
this.actions
|
||||
.ofType<MailsActions.SetCurrentMail>(MailsActions.SET_CURRENT_MAIL)
|
||||
.pipe(
|
||||
withLatestFrom(this.store.select(getMailsState)),
|
||||
map(([action, state]) => {
|
||||
return new MailsActions.SetCurrentMailSuccess(state.entities[action.payload]);
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Check Current Mail
|
||||
* Navigate to parent directory if not exist in mail list
|
||||
* Update Current Mail if exist in mail list
|
||||
* @type {Observable<any>}
|
||||
*/
|
||||
@Effect()
|
||||
checkCurrentMail: Observable<Action> =
|
||||
this.actions
|
||||
.ofType<MailsActions.CheckCurrentMail>(MailsActions.CHECK_CURRENT_MAIL)
|
||||
.pipe(
|
||||
withLatestFrom(this.store.select(getMailsState)),
|
||||
map(([action, state]) => {
|
||||
|
||||
if ( !state.entities[this.routerState.params.mailId] )
|
||||
{
|
||||
return new fromRoot.Go({path: [this.routerState.url.replace(this.routerState.params.mailId, '')]});
|
||||
}
|
||||
return new MailsActions.SetCurrentMailSuccess(state.entities[this.routerState.params.mailId]);
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* On Get Mails Success
|
||||
* @type {Observable<CheckCurrentMail>}
|
||||
*/
|
||||
@Effect()
|
||||
getMailsSuccess: Observable<MailsActions.MailsActionsAll> =
|
||||
this.actions
|
||||
.ofType<MailsActions.GetMailsSuccess>(MailsActions.GET_MAILS_SUCCESS)
|
||||
.pipe(
|
||||
mergeMap(() =>
|
||||
[
|
||||
new MailsActions.CheckCurrentMail()
|
||||
])
|
||||
);
|
||||
/**
|
||||
* On Update Mails Success
|
||||
* @type {Observable<DeselectAllMails | GetMails>}
|
||||
*/
|
||||
@Effect()
|
||||
updateMailsSuccess: Observable<MailsActions.MailsActionsAll> =
|
||||
this.actions
|
||||
.ofType<MailsActions.UpdateMailsSuccess>(MailsActions.UPDATE_MAILS_SUCCESS)
|
||||
.pipe(
|
||||
mergeMap(() =>
|
||||
[
|
||||
new MailsActions.DeselectAllMails(),
|
||||
new MailsActions.GetMails()
|
||||
])
|
||||
);
|
||||
/**
|
||||
* On Update Mail Success
|
||||
* @type {Observable<GetMails>}
|
||||
*/
|
||||
@Effect()
|
||||
updateMailSuccess: Observable<MailsActions.MailsActionsAll> =
|
||||
this.actions
|
||||
.ofType<MailsActions.UpdateMailSuccess>(MailsActions.UPDATE_MAIL_SUCCESS)
|
||||
.debounceTime(500)
|
||||
.pipe(
|
||||
map(() => {
|
||||
return new MailsActions.GetMails();
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Set Folder on Selected Mails
|
||||
* @type {Observable<UpdateMails>}
|
||||
*/
|
||||
@Effect()
|
||||
setFolderOnSelectedMails: Observable<MailsActions.MailsActionsAll> =
|
||||
this.actions
|
||||
.ofType<MailsActions.SetFolderOnSelectedMails>(MailsActions.SET_FOLDER_ON_SELECTED_MAILS)
|
||||
.pipe(
|
||||
withLatestFrom(
|
||||
this.store.select(getMailsState)),
|
||||
map(([action, state]) => {
|
||||
const entities = {...state.entities};
|
||||
let mailsToUpdate = [];
|
||||
state.selectedMailIds
|
||||
.map(id => {
|
||||
mailsToUpdate = [
|
||||
...mailsToUpdate,
|
||||
entities[id] = {
|
||||
...entities[id],
|
||||
folder: action.payload
|
||||
}
|
||||
];
|
||||
});
|
||||
return new MailsActions.UpdateMails(mailsToUpdate);
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Add Label on Selected Mails
|
||||
* @type {Observable<UpdateMails>}
|
||||
*/
|
||||
@Effect()
|
||||
addLabelOnSelectedMails: Observable<MailsActions.MailsActionsAll> =
|
||||
this.actions
|
||||
.ofType<MailsActions.AddLabelOnSelectedMails>(MailsActions.ADD_LABEL_ON_SELECTED_MAILS)
|
||||
.pipe(
|
||||
withLatestFrom(this.store.select(getMailsState)),
|
||||
map(([action, state]) => {
|
||||
|
||||
const entities = {...state.entities};
|
||||
let mailsToUpdate = [];
|
||||
|
||||
state.selectedMailIds
|
||||
.map(id => {
|
||||
|
||||
let labels = [...entities[id].labels];
|
||||
|
||||
if ( !entities[id].labels.includes(action.payload) )
|
||||
{
|
||||
labels = [...labels, action.payload];
|
||||
}
|
||||
|
||||
mailsToUpdate = [
|
||||
...mailsToUpdate,
|
||||
entities[id] = {
|
||||
...entities[id],
|
||||
labels
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
return new MailsActions.UpdateMails(mailsToUpdate);
|
||||
})
|
||||
);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './resolve.guard';
|
|
@ -0,0 +1,134 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import { map, switchMap, catchError, tap, take, filter } from 'rxjs/operators';
|
||||
import 'rxjs/add/observable/forkJoin';
|
||||
import { MailAppState } from '../reducers';
|
||||
import * as fromStore from '../index';
|
||||
import { getFiltersLoaded, getFoldersLoaded, getLabelsLoaded, getMailsLoaded } from '../selectors';
|
||||
import { RouterStateSnapshot } from '@angular/router/src/router_state';
|
||||
import { getRouterState } from '../../../../../../store/reducers';
|
||||
|
||||
@Injectable()
|
||||
export class ResolveGuard implements CanActivate
|
||||
{
|
||||
routerState: any;
|
||||
|
||||
constructor(
|
||||
private store: Store<MailAppState>
|
||||
)
|
||||
{
|
||||
this.store.select(getRouterState).subscribe(routerState => {
|
||||
if ( routerState )
|
||||
{
|
||||
this.routerState = routerState.state;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean>
|
||||
{
|
||||
return this.checkStore().pipe(
|
||||
switchMap(() => of(true)),
|
||||
catchError(() => of(false))
|
||||
);
|
||||
}
|
||||
|
||||
checkStore(): Observable<any>
|
||||
{
|
||||
return Observable
|
||||
.forkJoin(
|
||||
this.getFolders(),
|
||||
this.getFilters(),
|
||||
this.getLabels()
|
||||
)
|
||||
.pipe(
|
||||
filter(([foldersLoaded, filtersLoaded, labelsLoaded]) => filtersLoaded && foldersLoaded && labelsLoaded),
|
||||
take(1),
|
||||
switchMap(() =>
|
||||
this.getMails()
|
||||
),
|
||||
take(1),
|
||||
map(() => this.store.dispatch(new fromStore.SetCurrentMail(this.routerState.params.mailId)))
|
||||
);
|
||||
}
|
||||
|
||||
getFolders()
|
||||
{
|
||||
return this.store.select(getFoldersLoaded)
|
||||
.pipe(
|
||||
tap(loaded => {
|
||||
if ( !loaded )
|
||||
{
|
||||
this.store.dispatch(new fromStore.GetFolders([]));
|
||||
}
|
||||
}),
|
||||
filter(loaded => loaded),
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Filters
|
||||
* @returns {Observable<any>}
|
||||
*/
|
||||
getFilters()
|
||||
{
|
||||
return this.store.select(getFiltersLoaded)
|
||||
.pipe(
|
||||
tap(loaded => {
|
||||
if ( !loaded )
|
||||
{
|
||||
this.store.dispatch(new fromStore.GetFilters([]));
|
||||
}
|
||||
}),
|
||||
filter(loaded => loaded),
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Labels
|
||||
* @returns {Observable<any>}
|
||||
*/
|
||||
getLabels()
|
||||
{
|
||||
return this.store.select(getLabelsLoaded)
|
||||
.pipe(
|
||||
tap(loaded => {
|
||||
if ( !loaded )
|
||||
{
|
||||
this.store.dispatch(new fromStore.GetLabels([]));
|
||||
}
|
||||
}),
|
||||
filter(loaded => loaded),
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Mails
|
||||
* @returns {Observable<any>}
|
||||
*/
|
||||
getMails()
|
||||
{
|
||||
return this.store.select(getMailsLoaded)
|
||||
.pipe(
|
||||
tap((loaded: any) => {
|
||||
|
||||
if ( !this.routerState.params[loaded.id] || this.routerState.params[loaded.id] !== loaded.value )
|
||||
{
|
||||
this.store.dispatch(new fromStore.GetMails());
|
||||
this.store.dispatch(new fromStore.SetSearchText(''));
|
||||
this.store.dispatch(new fromStore.DeselectAllMails());
|
||||
}
|
||||
}),
|
||||
filter((loaded: any) => {
|
||||
return this.routerState.params[loaded.id] && this.routerState.params[loaded.id] === loaded.value;
|
||||
}),
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
}
|
4
src/app/main/content/apps/mail-ngrx/store/index.ts
Normal file
4
src/app/main/content/apps/mail-ngrx/store/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export * from './actions';
|
||||
export * from './reducers';
|
||||
export * from './selectors';
|
||||
export * from './effects';
|
|
@ -0,0 +1,53 @@
|
|||
import * as FiltersActions from '../actions/filters.actions';
|
||||
|
||||
export interface FiltersState
|
||||
{
|
||||
entities: { [id: number]: any };
|
||||
loading: boolean;
|
||||
loaded: boolean;
|
||||
}
|
||||
|
||||
export const FiltersInitialState: FiltersState = {
|
||||
entities: {},
|
||||
loading : false,
|
||||
loaded : false
|
||||
};
|
||||
|
||||
export function FiltersReducer(state = FiltersInitialState, action: FiltersActions.FiltersActionsAll): FiltersState
|
||||
{
|
||||
switch ( action.type )
|
||||
{
|
||||
case FiltersActions.GET_FILTERS:
|
||||
return {
|
||||
...state,
|
||||
loading: true,
|
||||
loaded : false
|
||||
};
|
||||
case FiltersActions.GET_FILTERS_SUCCESS:
|
||||
|
||||
const filters = action.payload;
|
||||
const entities = filters.reduce(
|
||||
(_entities: { [id: number]: any }, filter: any) => {
|
||||
return {
|
||||
..._entities,
|
||||
[filter.id]: filter
|
||||
};
|
||||
}, {});
|
||||
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
loaded : true,
|
||||
entities
|
||||
};
|
||||
|
||||
case FiltersActions.GET_FILTERS_FAILED:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
loaded : false
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import * as FoldersActions from '../actions/folders.actions';
|
||||
|
||||
export interface FoldersState
|
||||
{
|
||||
entities: { [id: number]: any };
|
||||
loading: boolean;
|
||||
loaded: boolean;
|
||||
}
|
||||
|
||||
export const FoldersInitialState: FoldersState = {
|
||||
entities: {},
|
||||
loading : false,
|
||||
loaded : false
|
||||
};
|
||||
|
||||
export function FoldersReducer(state = FoldersInitialState, action: FoldersActions.FoldersActionsAll): FoldersState
|
||||
{
|
||||
switch ( action.type )
|
||||
{
|
||||
case FoldersActions.GET_FOLDERS:
|
||||
return {
|
||||
...state,
|
||||
loading: true,
|
||||
loaded : false
|
||||
};
|
||||
case FoldersActions.GET_FOLDERS_SUCCESS:
|
||||
|
||||
const folders = action.payload;
|
||||
const entities = folders.reduce(
|
||||
(_entities: { [id: number]: any }, folder: any) => {
|
||||
return {
|
||||
..._entities,
|
||||
[folder.id]: folder
|
||||
};
|
||||
}, {});
|
||||
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
loaded : true,
|
||||
entities
|
||||
};
|
||||
|
||||
case FoldersActions.GET_FOLDERS_FAILED:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
loaded : false
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
34
src/app/main/content/apps/mail-ngrx/store/reducers/index.ts
Normal file
34
src/app/main/content/apps/mail-ngrx/store/reducers/index.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { ActionReducerMap, createFeatureSelector, createSelector } from '@ngrx/store';
|
||||
import { MailsReducer, MailsState } from './mails.reducer';
|
||||
import { FoldersReducer, FoldersState } from './folders.reducer';
|
||||
import { FiltersReducer, FiltersState } from './filters.reducer';
|
||||
import { LabelsReducer, LabelsState } from './labels.reducer';
|
||||
|
||||
export interface MailAppState
|
||||
{
|
||||
mails: MailsState;
|
||||
folders: FoldersState;
|
||||
filters: FiltersState;
|
||||
labels: LabelsState;
|
||||
}
|
||||
|
||||
export const getMailAppState = createFeatureSelector<MailAppState>(
|
||||
'mail-app'
|
||||
);
|
||||
|
||||
export const getAppState = createSelector(
|
||||
getMailAppState,
|
||||
(state: MailAppState) => state
|
||||
);
|
||||
|
||||
export const reducers: ActionReducerMap<MailAppState> = {
|
||||
mails : MailsReducer,
|
||||
folders: FoldersReducer,
|
||||
filters: FiltersReducer,
|
||||
labels : LabelsReducer
|
||||
};
|
||||
|
||||
export * from './mails.reducer';
|
||||
export * from './folders.reducer';
|
||||
export * from './filters.reducer';
|
||||
export * from './labels.reducer';
|
|
@ -0,0 +1,53 @@
|
|||
import * as LabelsActions from '../actions/labels.actions';
|
||||
|
||||
export interface LabelsState
|
||||
{
|
||||
entities: { [id: number]: any };
|
||||
loading: boolean;
|
||||
loaded: boolean;
|
||||
}
|
||||
|
||||
export const LabelsInitialState: LabelsState = {
|
||||
entities: {},
|
||||
loading : false,
|
||||
loaded : false
|
||||
};
|
||||
|
||||
export function LabelsReducer(state = LabelsInitialState, action: LabelsActions.LabelsActionsAll): LabelsState
|
||||
{
|
||||
switch ( action.type )
|
||||
{
|
||||
case LabelsActions.GET_LABELS:
|
||||
return {
|
||||
...state,
|
||||
loading: true,
|
||||
loaded : false
|
||||
};
|
||||
case LabelsActions.GET_LABELS_SUCCESS:
|
||||
|
||||
const labels = action.payload;
|
||||
const entities = labels.reduce(
|
||||
(_entities: { [id: number]: any }, label: any) => {
|
||||
return {
|
||||
..._entities,
|
||||
[label.id]: label
|
||||
};
|
||||
}, {});
|
||||
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
loaded : true,
|
||||
entities
|
||||
};
|
||||
|
||||
case LabelsActions.GET_LABELS_FAILED:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
loaded : false
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
import * as MailsActions from '../actions/mails.actions';
|
||||
import { Mail } from '../../mail.model';
|
||||
|
||||
export interface MailsState
|
||||
{
|
||||
entities: { [id: number]: Mail };
|
||||
currentMail: any;
|
||||
selectedMailIds: string[];
|
||||
searchText: string;
|
||||
loading: boolean;
|
||||
loaded: any;
|
||||
}
|
||||
|
||||
export const MailsInitialState: MailsState = {
|
||||
entities : {},
|
||||
currentMail : null,
|
||||
selectedMailIds: [],
|
||||
searchText : '',
|
||||
loading : false,
|
||||
loaded : false
|
||||
};
|
||||
|
||||
export function MailsReducer(state = MailsInitialState, action: MailsActions.MailsActionsAll): MailsState
|
||||
{
|
||||
switch ( action.type )
|
||||
{
|
||||
case MailsActions.GET_MAILS:
|
||||
{
|
||||
return {
|
||||
...state,
|
||||
loading: true
|
||||
};
|
||||
}
|
||||
|
||||
case MailsActions.GET_MAILS_SUCCESS:
|
||||
{
|
||||
|
||||
const mails = action.payload.mails;
|
||||
const loaded = action.payload.loaded;
|
||||
const entities = mails.reduce(
|
||||
(_entities: { [id: number]: Mail }, mail: Mail) => {
|
||||
return {
|
||||
..._entities,
|
||||
[mail.id]: mail
|
||||
};
|
||||
}, {});
|
||||
|
||||
return {
|
||||
...state,
|
||||
entities,
|
||||
loading: false,
|
||||
loaded
|
||||
};
|
||||
}
|
||||
|
||||
case MailsActions.GET_MAILS_FAILED:
|
||||
{
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
loaded : false
|
||||
};
|
||||
}
|
||||
|
||||
case MailsActions.SET_CURRENT_MAIL_SUCCESS:
|
||||
{
|
||||
return {
|
||||
...state,
|
||||
currentMail: action.payload
|
||||
};
|
||||
}
|
||||
|
||||
case MailsActions.UPDATE_MAIL_SUCCESS:
|
||||
{
|
||||
return {
|
||||
...state,
|
||||
entities: {
|
||||
...state.entities,
|
||||
[action.payload.id]: action.payload
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
case MailsActions.SET_SEARCH_TEXT:
|
||||
{
|
||||
|
||||
return {
|
||||
...state,
|
||||
searchText: action.payload
|
||||
};
|
||||
}
|
||||
|
||||
case MailsActions.TOGGLE_IN_SELECTED_MAILS:
|
||||
{
|
||||
|
||||
const mailId = action.payload;
|
||||
|
||||
let selectedMailIds = [...state.selectedMailIds];
|
||||
|
||||
if ( selectedMailIds.find(id => id === mailId) !== undefined )
|
||||
{
|
||||
selectedMailIds = selectedMailIds.filter(id => id !== mailId);
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedMailIds = [...selectedMailIds, mailId];
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedMailIds
|
||||
};
|
||||
}
|
||||
|
||||
case MailsActions.SELECT_ALL_MAILS:
|
||||
{
|
||||
const arr = Object.keys(state.entities).map(k => state.entities[k]);
|
||||
|
||||
const selectedMailIds = arr.map(mail => mail.id);
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedMailIds
|
||||
};
|
||||
}
|
||||
|
||||
case MailsActions.DESELECT_ALL_MAILS:
|
||||
{
|
||||
return {
|
||||
...state,
|
||||
selectedMailIds: []
|
||||
};
|
||||
}
|
||||
|
||||
case MailsActions.SELECT_MAILS_BY_PARAMETER:
|
||||
{
|
||||
const filter = action.payload;
|
||||
const arr = Object.keys(state.entities).map(k => state.entities[k]);
|
||||
const selectedMailIds = arr.filter(mail => mail[filter.parameter] === filter.value)
|
||||
.map(mail => mail.id);
|
||||
return {
|
||||
...state,
|
||||
selectedMailIds
|
||||
};
|
||||
}
|
||||
|
||||
case MailsActions.SET_FOLDER_ON_SELECTED_MAILS:
|
||||
{
|
||||
const entities = {...state.entities};
|
||||
|
||||
state.selectedMailIds.map(id => {
|
||||
entities[id] = {
|
||||
...entities[id],
|
||||
folder: action.payload
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
entities
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { createSelector } from '@ngrx/store';
|
||||
import { FiltersState, getMailAppState, MailAppState } from '../reducers';
|
||||
|
||||
export const getFiltersState = createSelector(
|
||||
getMailAppState,
|
||||
(state: MailAppState) => state.filters
|
||||
);
|
||||
|
||||
export const getFilters = createSelector(
|
||||
getFiltersState,
|
||||
(state: FiltersState) => state.entities
|
||||
);
|
||||
|
||||
export const getFiltersLoaded = createSelector(
|
||||
getFiltersState,
|
||||
(state: FiltersState) => state.loaded
|
||||
);
|
||||
|
||||
export const getFiltersArr = createSelector(
|
||||
getFilters,
|
||||
(entities) => Object.keys(entities).map((id) => entities[id])
|
||||
);
|
|
@ -0,0 +1,22 @@
|
|||
import { createSelector } from '@ngrx/store';
|
||||
import { FoldersState, getMailAppState, MailAppState } from '../reducers';
|
||||
|
||||
export const getFoldersState = createSelector(
|
||||
getMailAppState,
|
||||
(state: MailAppState) => state.folders
|
||||
);
|
||||
|
||||
export const getFolders = createSelector(
|
||||
getFoldersState,
|
||||
(state: FoldersState) => state.entities
|
||||
);
|
||||
|
||||
export const getFoldersLoaded = createSelector(
|
||||
getFoldersState,
|
||||
(state: FoldersState) => state.loaded
|
||||
);
|
||||
|
||||
export const getFoldersArr = createSelector(
|
||||
getFolders,
|
||||
(entities) => Object.keys(entities).map((id) => entities[id])
|
||||
);
|
|
@ -0,0 +1,4 @@
|
|||
export * from './mails.selectors';
|
||||
export * from './folders.selectors';
|
||||
export * from './filters.selectors';
|
||||
export * from './labels.selectors';
|
|
@ -0,0 +1,22 @@
|
|||
import { createSelector } from '@ngrx/store';
|
||||
import { LabelsState, getMailAppState, MailAppState } from '../reducers';
|
||||
|
||||
export const getLabelsState = createSelector(
|
||||
getMailAppState,
|
||||
(state: MailAppState) => state.labels
|
||||
);
|
||||
|
||||
export const getLabels = createSelector(
|
||||
getLabelsState,
|
||||
(state: LabelsState) => state.entities
|
||||
);
|
||||
|
||||
export const getLabelsLoaded = createSelector(
|
||||
getLabelsState,
|
||||
(state: LabelsState) => state.loaded
|
||||
);
|
||||
|
||||
export const getLabelsArr = createSelector(
|
||||
getLabels,
|
||||
(entities) => Object.keys(entities).map((id) => entities[id])
|
||||
);
|
|
@ -0,0 +1,42 @@
|
|||
import { createSelector } from '@ngrx/store';
|
||||
import { getMailAppState, MailAppState, MailsState } from '../reducers';
|
||||
import { FuseUtils } from '../../../../../../core/fuseUtils';
|
||||
|
||||
export const getMailsState = createSelector(
|
||||
getMailAppState,
|
||||
(state: MailAppState) => state.mails
|
||||
);
|
||||
|
||||
export const getMails = createSelector(
|
||||
getMailsState,
|
||||
(state: MailsState) => state.entities
|
||||
);
|
||||
|
||||
export const getMailsLoaded = createSelector(
|
||||
getMailsState,
|
||||
(state: MailsState) => state.loaded
|
||||
);
|
||||
|
||||
export const getSearchText = createSelector(
|
||||
getMailsState,
|
||||
(state: MailsState) => state.searchText
|
||||
);
|
||||
|
||||
export const getMailsArr = createSelector(
|
||||
getMails,
|
||||
getSearchText,
|
||||
(entities, searchText) => {
|
||||
const arr = Object.keys(entities).map((id) => entities[id]);
|
||||
return FuseUtils.filterArrayByString(arr, searchText);
|
||||
}
|
||||
);
|
||||
|
||||
export const getCurrentMail = createSelector(
|
||||
getMailsState,
|
||||
(state: MailsState) => state.currentMail
|
||||
);
|
||||
|
||||
export const getSelectedMailIds = createSelector(
|
||||
getMailsState,
|
||||
(state: MailsState) => state.selectedMailIds
|
||||
);
|
16
src/app/main/content/apps/mail-ngrx/store/store.module.ts
Normal file
16
src/app/main/content/apps/mail-ngrx/store/store.module.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { StoreModule } from '@ngrx/store';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { reducers } from './reducers';
|
||||
import { effects } from './effects';
|
||||
|
||||
@NgModule({
|
||||
imports : [
|
||||
StoreModule.forFeature('mail-app', reducers),
|
||||
EffectsModule.forFeature(effects)
|
||||
],
|
||||
providers: []
|
||||
})
|
||||
export class MailAppStoreModule
|
||||
{
|
||||
}
|
|
@ -10,6 +10,10 @@ export const locale = {
|
|||
'TITLE': 'Mail',
|
||||
'BADGE': '25'
|
||||
},
|
||||
'MAIL_NGRX' : {
|
||||
'TITLE': 'Mail Ngrx',
|
||||
'BADGE': '13'
|
||||
},
|
||||
'CHAT' : 'Chat',
|
||||
'FILE_MANAGER': 'File Manager',
|
||||
'CONTACTS' : 'Contacts',
|
||||
|
|
|
@ -10,6 +10,10 @@ export const locale = {
|
|||
'TITLE': 'Posta',
|
||||
'BADGE': '15'
|
||||
},
|
||||
'MAIL_NGRX' : {
|
||||
'TITLE': 'Posta Ngrx',
|
||||
'BADGE': '13'
|
||||
},
|
||||
'CHAT' : 'Sohbet',
|
||||
'FILE_MANAGER': 'Dosya Yöneticisi',
|
||||
'CONTACTS' : 'Kişiler',
|
||||
|
|
|
@ -94,6 +94,20 @@ export class FuseNavigationModel implements FuseNavigationModelInterface
|
|||
'fg' : '#FFFFFF'
|
||||
}
|
||||
},
|
||||
{
|
||||
'id' : 'mail-ngrx',
|
||||
'title' : 'Mail Ngrx',
|
||||
'translate': 'NAV.MAIL_NGRX.TITLE',
|
||||
'type' : 'item',
|
||||
'icon' : 'email',
|
||||
'url' : '/apps/mail-ngrx',
|
||||
'badge' : {
|
||||
'title' : 13,
|
||||
'translate': 'NAV.MAIL_NGRX.BADGE',
|
||||
'bg' : '#EC0C8E',
|
||||
'fg' : '#FFFFFF'
|
||||
}
|
||||
},
|
||||
{
|
||||
'id' : 'chat',
|
||||
'title' : 'Chat',
|
||||
|
|
1
src/app/store/actions/index.ts
Normal file
1
src/app/store/actions/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './router.action';
|
27
src/app/store/actions/router.action.ts
Normal file
27
src/app/store/actions/router.action.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { Action } from '@ngrx/store';
|
||||
import { NavigationExtras } from '@angular/router';
|
||||
|
||||
export const GO = '[Router] Go';
|
||||
export const BACK = '[Router] Back';
|
||||
export const FORWARD = '[Router] Forward';
|
||||
|
||||
export class Go implements Action {
|
||||
readonly type = GO;
|
||||
constructor(
|
||||
public payload: {
|
||||
path: any[];
|
||||
query?: object;
|
||||
extras?: NavigationExtras;
|
||||
}
|
||||
) {}
|
||||
}
|
||||
|
||||
export class Back implements Action {
|
||||
readonly type = BACK;
|
||||
}
|
||||
|
||||
export class Forward implements Action {
|
||||
readonly type = FORWARD;
|
||||
}
|
||||
|
||||
export type Actions = Go | Back | Forward;
|
5
src/app/store/effects/index.ts
Normal file
5
src/app/store/effects/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { RouterEffects } from './router.effect';
|
||||
|
||||
export const effects: any[] = [RouterEffects];
|
||||
|
||||
export * from './router.effect';
|
35
src/app/store/effects/router.effect.ts
Normal file
35
src/app/store/effects/router.effect.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Location } from '@angular/common';
|
||||
|
||||
import { Effect, Actions } from '@ngrx/effects';
|
||||
import * as RouterActions from '../actions/router.action';
|
||||
|
||||
import { tap, map } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class RouterEffects {
|
||||
constructor(
|
||||
private actions$: Actions,
|
||||
private router: Router,
|
||||
private location: Location
|
||||
) {}
|
||||
|
||||
@Effect({ dispatch: false })
|
||||
navigate$ = this.actions$.ofType(RouterActions.GO).pipe(
|
||||
map((action: RouterActions.Go) => action.payload),
|
||||
tap(({ path, query: queryParams, extras }) => {
|
||||
this.router.navigate(path, { queryParams, ...extras });
|
||||
})
|
||||
);
|
||||
|
||||
@Effect({ dispatch: false })
|
||||
navigateBack$ = this.actions$
|
||||
.ofType(RouterActions.BACK)
|
||||
.pipe(tap(() => this.location.back()));
|
||||
|
||||
@Effect({ dispatch: false })
|
||||
navigateForward$ = this.actions$
|
||||
.ofType(RouterActions.FORWARD)
|
||||
.pipe(tap(() => this.location.forward()));
|
||||
}
|
3
src/app/store/index.ts
Normal file
3
src/app/store/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from './reducers';
|
||||
export * from './actions';
|
||||
export * from './effects';
|
42
src/app/store/reducers/index.ts
Normal file
42
src/app/store/reducers/index.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
RouterStateSnapshot,
|
||||
Params,
|
||||
} from '@angular/router';
|
||||
import { createFeatureSelector, ActionReducerMap } from '@ngrx/store';
|
||||
|
||||
import * as fromRouter from '@ngrx/router-store';
|
||||
|
||||
export interface RouterStateUrl {
|
||||
url: string;
|
||||
queryParams: Params;
|
||||
params: Params;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
routerReducer: fromRouter.RouterReducerState<RouterStateUrl>;
|
||||
}
|
||||
|
||||
export const reducers: ActionReducerMap<State> = {
|
||||
routerReducer: fromRouter.routerReducer,
|
||||
};
|
||||
|
||||
export const getRouterState = createFeatureSelector<
|
||||
fromRouter.RouterReducerState<RouterStateUrl>
|
||||
>('routerReducer');
|
||||
|
||||
export class CustomSerializer
|
||||
implements fromRouter.RouterStateSerializer<RouterStateUrl> {
|
||||
serialize(routerState: RouterStateSnapshot): RouterStateUrl {
|
||||
const { url } = routerState;
|
||||
const { queryParams } = routerState.root;
|
||||
|
||||
let state: ActivatedRouteSnapshot = routerState.root;
|
||||
while (state.firstChild) {
|
||||
state = state.firstChild;
|
||||
}
|
||||
const { params } = state;
|
||||
|
||||
return { url, queryParams, params };
|
||||
}
|
||||
}
|
31
src/app/store/store.module.ts
Normal file
31
src/app/store/store.module.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { MetaReducer, StoreModule } from '@ngrx/store';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { storeFreeze } from 'ngrx-store-freeze';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
|
||||
import { reducers, effects, CustomSerializer } from './index';
|
||||
|
||||
export const metaReducers: MetaReducer<any>[] = !environment.production
|
||||
? [storeFreeze]
|
||||
: [];
|
||||
|
||||
@NgModule({
|
||||
imports : [
|
||||
StoreModule.forRoot(reducers, {metaReducers}),
|
||||
EffectsModule.forRoot(effects),
|
||||
!environment.production ? StoreDevtoolsModule.instrument() : [],
|
||||
StoreRouterConnectingModule
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide : RouterStateSerializer,
|
||||
useClass: CustomSerializer
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export class AppStoreModule
|
||||
{
|
||||
}
|
Loading…
Reference in New Issue
Block a user