diff --git a/package-lock.json b/package-lock.json index a7510273..05549643 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 2050af8f..a7296b1a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index ecd785dd..eddf8dcd 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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, diff --git a/src/app/main/content/apps/mail-ngrx/dialogs/compose/compose.component.html b/src/app/main/content/apps/mail-ngrx/dialogs/compose/compose.component.html new file mode 100644 index 00000000..26e46f24 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/dialogs/compose/compose.component.html @@ -0,0 +1,111 @@ +
+ +
+ New Message + +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ attachment-2.doc + (12 Kb) +
+ + +
+ +
+
+ attachment-1.jpg + (350 Kb) +
+ + +
+
+
+
+ +
+
+ + + +
+ + +
+
diff --git a/src/app/main/content/apps/mail-ngrx/dialogs/compose/compose.component.scss b/src/app/main/content/apps/mail-ngrx/dialogs/compose/compose.component.scss new file mode 100644 index 00000000..87d41f50 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/dialogs/compose/compose.component.scss @@ -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; + } +} diff --git a/src/app/main/content/apps/mail-ngrx/dialogs/compose/compose.component.ts b/src/app/main/content/apps/mail-ngrx/dialogs/compose/compose.component.ts new file mode 100644 index 00000000..a5ae8286 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/dialogs/compose/compose.component.ts @@ -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, + @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: [''] + }); + + } + +} diff --git a/src/app/main/content/apps/mail-ngrx/i18n/en.ts b/src/app/main/content/apps/mail-ngrx/i18n/en.ts new file mode 100644 index 00000000..1e3b4b34 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/i18n/en.ts @@ -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' + } + } +}; diff --git a/src/app/main/content/apps/mail-ngrx/i18n/tr.ts b/src/app/main/content/apps/mail-ngrx/i18n/tr.ts new file mode 100644 index 00000000..03a524de --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/i18n/tr.ts @@ -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' + } + } +}; diff --git a/src/app/main/content/apps/mail-ngrx/mail-details/mail-details.component.html b/src/app/main/content/apps/mail-ngrx/mail-details/mail-details.component.html new file mode 100644 index 00000000..765aca1c --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/mail-details/mail-details.component.html @@ -0,0 +1,140 @@ +
+ + email + + + {{ 'MAIL.SELECT_A_MESSAGE_TO_READ' | translate }} + +
+ +
+ +
+ +
+
{{mail.subject}}
+ +
+
+
+
{{(labels$ | async) | getById:labelId:'title'}}
+
+
+
+ +
+ + + +
+
+ +
+ +
+ +
+ +
+ +
+ {{mail.from.name}} + +
+ {{mail.from.name[0]}} +
+
+ +
+ +
+ {{mail.from.name}} +
+ +
+
to
+
{{mail.to[0].name}}
+
+
+
+ + + Show Details + Hide Details + + +
+ +
+ From: + To: + Date: +
+ +
+ {{mail.from.email}} + {{mail.to[0].email}} + {{mail.time}} +
+
+
+ + + + + + + + + + +
+ +
+ +
+ +
+ +
+ Attachments + ({{mail.attachments.length}}) +
+ +
+ +
+ + + +
+ View + Download +
({{attachment.size}})
+
+ +
+
+
+
diff --git a/src/app/main/content/apps/mail-ngrx/mail-details/mail-details.component.scss b/src/app/main/content/apps/mail-ngrx/mail-details/mail-details.component.scss new file mode 100644 index 00000000..eaabb60a --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/mail-details/mail-details.component.scss @@ -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; + } + } + } +} diff --git a/src/app/main/content/apps/mail-ngrx/mail-details/mail-details.component.ts b/src/app/main/content/apps/mail-ngrx/mail-details/mail-details.component.ts new file mode 100644 index 00000000..f8bbe42e --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/mail-details/mail-details.component.ts @@ -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; + @Input('mail') mailInput: Mail; + mail: Mail; + showDetails = false; + + constructor( + private mailService: MailNgrxService, + private store: Store + ) + { + 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() + { + } +} diff --git a/src/app/main/content/apps/mail-ngrx/mail-list/mail-list-item/mail-list-item.component.html b/src/app/main/content/apps/mail-ngrx/mail-list/mail-list-item/mail-list-item.component.html new file mode 100644 index 00000000..80841ec0 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/mail-list/mail-list-item/mail-list-item.component.html @@ -0,0 +1,48 @@ +
+ + + + +
+ +
+ +
+ {{mail.from?.name}} +
{{mail.from?.name[0]}}
+ {{mail.from?.name}} + attachment +
+ +
+
{{mail.time}}
+
+ +
+ +
+ +
+ +
+ {{mail.subject}} +
+ +
+ {{mail.message | htmlToPlaintext | slice:0:180}}{{mail.message.length > 180 ? '...' : ''}} +
+ +
+
+
+
{{(labels$ | async) | getById:labelId:'title'}}
+
+
+ +
+
+
+
diff --git a/src/app/main/content/apps/mail-ngrx/mail-list/mail-list-item/mail-list-item.component.scss b/src/app/main/content/apps/mail-ngrx/mail-list/mail-list-item/mail-list-item.component.scss new file mode 100644 index 00000000..45676aff --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/mail-list/mail-list-item/mail-list-item.component.scss @@ -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%; + } + } + } + } + } +} diff --git a/src/app/main/content/apps/mail-ngrx/mail-list/mail-list-item/mail-list-item.component.ts b/src/app/main/content/apps/mail-ngrx/mail-list/mail-list-item/mail-list-item.component.ts new file mode 100644 index 00000000..d4a592bc --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/mail-list/mail-list-item/mail-list-item.component.ts @@ -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; + selectedMailIds$: Observable; + + constructor( + private mailService: MailNgrxService, + private store: Store, + 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() + { + } +} diff --git a/src/app/main/content/apps/mail-ngrx/mail-list/mail-list.component.html b/src/app/main/content/apps/mail-ngrx/mail-list/mail-list.component.html new file mode 100644 index 00000000..bb5903db --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/mail-list/mail-list.component.html @@ -0,0 +1,9 @@ +
+ {{ 'MAIL.NO_MESSAGES' | translate }} +
+ +
+ + +
diff --git a/src/app/main/content/apps/mail-ngrx/mail-list/mail-list.component.scss b/src/app/main/content/apps/mail-ngrx/mail-list/mail-list.component.scss new file mode 100644 index 00000000..440b75aa --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/mail-list/mail-list.component.scss @@ -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; + } +} diff --git a/src/app/main/content/apps/mail-ngrx/mail-list/mail-list.component.ts b/src/app/main/content/apps/mail-ngrx/mail-list/mail-list.component.ts new file mode 100644 index 00000000..a4baa16e --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/mail-list/mail-list.component.ts @@ -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() + { + } +} diff --git a/src/app/main/content/apps/mail-ngrx/mail.component.html b/src/app/main/content/apps/mail-ngrx/mail.component.html new file mode 100644 index 00000000..94c74e97 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/mail.component.html @@ -0,0 +1,116 @@ +
+ + +
+ + + + + + + + + + + +
+ + +
+ +
+ + + + +
+
+ + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+ + + + + +
+ + +
+ + +
+ + +
+ +
diff --git a/src/app/main/content/apps/mail-ngrx/mail.component.scss b/src/app/main/content/apps/mail-ngrx/mail.component.scss new file mode 100644 index 00000000..9a355577 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/mail.component.scss @@ -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; + } + } + } + } + } + } +} + diff --git a/src/app/main/content/apps/mail-ngrx/mail.component.ts b/src/app/main/content/apps/mail-ngrx/mail.component.ts new file mode 100644 index 00000000..4238af45 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/mail.component.ts @@ -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; + folders$: Observable; + labels$: Observable; + currentMail$: Observable; + selectedMailIds$: Observable; + searchText$: Observable; + mails: Mail[]; + selectedMailIds: string[]; + + constructor( + private mailService: MailNgrxService, + private translationLoader: FuseTranslationLoaderService, + private store: Store, + 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(); + } +} diff --git a/src/app/main/content/apps/mail-ngrx/mail.model.ts b/src/app/main/content/apps/mail-ngrx/mail.model.ts new file mode 100644 index 00000000..42ff6cf5 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/mail.model.ts @@ -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; + } +} diff --git a/src/app/main/content/apps/mail-ngrx/mail.module.ts b/src/app/main/content/apps/mail-ngrx/mail.module.ts new file mode 100644 index 00000000..e3f3a2fc --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/mail.module.ts @@ -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 +{ +} diff --git a/src/app/main/content/apps/mail-ngrx/mail.service.ts b/src/app/main/content/apps/mail-ngrx/mail.service.ts new file mode 100644 index 00000000..e02c78dd --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/mail.service.ts @@ -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 + ) + { + 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 + { + return this.http.get('api/mail-mails'); + } + + getFolders(): Observable + { + return this.http.get('api/mail-folders'); + } + + getFilters(): Observable + { + return this.http.get('api/mail-filters'); + } + + getLabels(): Observable + { + return this.http.get('api/mail-labels'); + } + + getMails(handle): Observable + { + if ( handle.id === 'labelHandle' ) + { + const labelId = this.labelsArr.find(label => label.handle === handle.value).id; + return this.http.get('api/mail-mails?labels=' + labelId); + } + else if ( handle.id === 'filterHandle' ) + { + return this.http.get('api/mail-mails?' + handle.value + '=true'); + } + else // folderHandle + { + const folderId = this.foldersArr.find(folder => folder.handle === handle.value).id; + return this.http.get('api/mail-mails?folder=' + folderId); + } + } + + /** + * Update the mail + * @param mail + * @returns {Promise} + */ + updateMail(mail) + { + return this.http.post('api/mail-mails/' + mail.id, {...mail}); + } +} diff --git a/src/app/main/content/apps/mail-ngrx/sidenavs/main/main-sidenav.component.html b/src/app/main/content/apps/mail-ngrx/sidenavs/main/main-sidenav.component.html new file mode 100644 index 00000000..826f5e7d --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/sidenavs/main/main-sidenav.component.html @@ -0,0 +1,69 @@ + +
+ + + + + +
+ + + +
+ +
+ +
+ + + +
+ diff --git a/src/app/main/content/apps/mail-ngrx/sidenavs/main/main-sidenav.component.scss b/src/app/main/content/apps/mail-ngrx/sidenavs/main/main-sidenav.component.scss new file mode 100644 index 00000000..633e6083 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/sidenavs/main/main-sidenav.component.scss @@ -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 { + } + } +} diff --git a/src/app/main/content/apps/mail-ngrx/sidenavs/main/main-sidenav.component.ts b/src/app/main/content/apps/mail-ngrx/sidenavs/main/main-sidenav.component.ts new file mode 100644 index 00000000..b261d20a --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/sidenavs/main/main-sidenav.component.ts @@ -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; + filters$: Observable; + labels$: Observable; + + constructor( + private mailService: MailNgrxService, + public dialog: MatDialog, + private store: Store + ) + { + // 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() + { + } +} diff --git a/src/app/main/content/apps/mail-ngrx/store/actions/filters.actions.ts b/src/app/main/content/apps/mail-ngrx/store/actions/filters.actions.ts new file mode 100644 index 00000000..eb404a2f --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/store/actions/filters.actions.ts @@ -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; diff --git a/src/app/main/content/apps/mail-ngrx/store/actions/folders.actions.ts b/src/app/main/content/apps/mail-ngrx/store/actions/folders.actions.ts new file mode 100644 index 00000000..c797e2af --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/store/actions/folders.actions.ts @@ -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; diff --git a/src/app/main/content/apps/mail-ngrx/store/actions/index.ts b/src/app/main/content/apps/mail-ngrx/store/actions/index.ts new file mode 100644 index 00000000..5558d987 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/store/actions/index.ts @@ -0,0 +1,4 @@ +export * from './mails.actions'; +export * from './folders.actions'; +export * from './filters.actions'; +export * from './labels.actions'; diff --git a/src/app/main/content/apps/mail-ngrx/store/actions/labels.actions.ts b/src/app/main/content/apps/mail-ngrx/store/actions/labels.actions.ts new file mode 100644 index 00000000..03ac32af --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/store/actions/labels.actions.ts @@ -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; diff --git a/src/app/main/content/apps/mail-ngrx/store/actions/mails.actions.ts b/src/app/main/content/apps/mail-ngrx/store/actions/mails.actions.ts new file mode 100644 index 00000000..007c39d5 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/store/actions/mails.actions.ts @@ -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; diff --git a/src/app/main/content/apps/mail-ngrx/store/effects/filters.effects.ts b/src/app/main/content/apps/mail-ngrx/store/effects/filters.effects.ts new file mode 100644 index 00000000..2a073f88 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/store/effects/filters.effects.ts @@ -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} + */ + @Effect() + getFilters: Observable = + this.actions + .ofType(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))) + ); + } + )); +} diff --git a/src/app/main/content/apps/mail-ngrx/store/effects/folders.effects.ts b/src/app/main/content/apps/mail-ngrx/store/effects/folders.effects.ts new file mode 100644 index 00000000..a89270c2 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/store/effects/folders.effects.ts @@ -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} + */ + @Effect() + getFolders: Observable = + this.actions + .ofType(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))) + ); + } + )); +} diff --git a/src/app/main/content/apps/mail-ngrx/store/effects/index.ts b/src/app/main/content/apps/mail-ngrx/store/effects/index.ts new file mode 100644 index 00000000..a4c99efb --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/store/effects/index.ts @@ -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'; diff --git a/src/app/main/content/apps/mail-ngrx/store/effects/labels.effects.ts b/src/app/main/content/apps/mail-ngrx/store/effects/labels.effects.ts new file mode 100644 index 00000000..24d4b3f2 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/store/effects/labels.effects.ts @@ -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} + */ + @Effect() + getLabels: Observable = + this.actions + .ofType(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))) + ); + } + )); +} diff --git a/src/app/main/content/apps/mail-ngrx/store/effects/mails.effects.ts b/src/app/main/content/apps/mail-ngrx/store/effects/mails.effects.ts new file mode 100644 index 00000000..c88a9138 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/store/effects/mails.effects.ts @@ -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 + ) + { + this.store.select(getRouterState).subscribe(routerState => { + if ( routerState ) + { + this.routerState = routerState.state; + } + }); + } + + /** + * Get Mails with router parameters + * @type {Observable} + */ + @Effect() + getMails: Observable = + this.actions + .ofType(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} + */ + @Effect() + updateMail: Observable = + this.actions + .ofType(MailsActions.UPDATE_MAIL) + .pipe( + exhaustMap((action) => { + return this.mailService.updateMail(action.payload) + .map(() => { + return new MailsActions.UpdateMailSuccess(action.payload); + }); + }) + ); + + /** + * UpdateMails + * @type {Observable} + */ + @Effect() + updateMails: Observable = + this.actions + .ofType(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} + */ + @Effect() + setCurrentMail: Observable = + this.actions + .ofType(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} + */ + @Effect() + checkCurrentMail: Observable = + this.actions + .ofType(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} + */ + @Effect() + getMailsSuccess: Observable = + this.actions + .ofType(MailsActions.GET_MAILS_SUCCESS) + .pipe( + mergeMap(() => + [ + new MailsActions.CheckCurrentMail() + ]) + ); + /** + * On Update Mails Success + * @type {Observable} + */ + @Effect() + updateMailsSuccess: Observable = + this.actions + .ofType(MailsActions.UPDATE_MAILS_SUCCESS) + .pipe( + mergeMap(() => + [ + new MailsActions.DeselectAllMails(), + new MailsActions.GetMails() + ]) + ); + /** + * On Update Mail Success + * @type {Observable} + */ + @Effect() + updateMailSuccess: Observable = + this.actions + .ofType(MailsActions.UPDATE_MAIL_SUCCESS) + .debounceTime(500) + .pipe( + map(() => { + return new MailsActions.GetMails(); + }) + ); + + /** + * Set Folder on Selected Mails + * @type {Observable} + */ + @Effect() + setFolderOnSelectedMails: Observable = + this.actions + .ofType(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} + */ + @Effect() + addLabelOnSelectedMails: Observable = + this.actions + .ofType(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); + }) + ); +} diff --git a/src/app/main/content/apps/mail-ngrx/store/guards/index.ts b/src/app/main/content/apps/mail-ngrx/store/guards/index.ts new file mode 100644 index 00000000..1e38e558 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/store/guards/index.ts @@ -0,0 +1 @@ +export * from './resolve.guard'; diff --git a/src/app/main/content/apps/mail-ngrx/store/guards/resolve.guard.ts b/src/app/main/content/apps/mail-ngrx/store/guards/resolve.guard.ts new file mode 100644 index 00000000..46c35ebd --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/store/guards/resolve.guard.ts @@ -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 + ) + { + this.store.select(getRouterState).subscribe(routerState => { + if ( routerState ) + { + this.routerState = routerState.state; + } + }); + } + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable + { + return this.checkStore().pipe( + switchMap(() => of(true)), + catchError(() => of(false)) + ); + } + + checkStore(): Observable + { + 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} + */ + 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} + */ + 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} + */ + 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) + ); + } +} diff --git a/src/app/main/content/apps/mail-ngrx/store/index.ts b/src/app/main/content/apps/mail-ngrx/store/index.ts new file mode 100644 index 00000000..9bab4e30 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/store/index.ts @@ -0,0 +1,4 @@ +export * from './actions'; +export * from './reducers'; +export * from './selectors'; +export * from './effects'; diff --git a/src/app/main/content/apps/mail-ngrx/store/reducers/filters.reducer.ts b/src/app/main/content/apps/mail-ngrx/store/reducers/filters.reducer.ts new file mode 100644 index 00000000..eef4eb5f --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/store/reducers/filters.reducer.ts @@ -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; + } +} diff --git a/src/app/main/content/apps/mail-ngrx/store/reducers/folders.reducer.ts b/src/app/main/content/apps/mail-ngrx/store/reducers/folders.reducer.ts new file mode 100644 index 00000000..91efb30e --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/store/reducers/folders.reducer.ts @@ -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; + } +} diff --git a/src/app/main/content/apps/mail-ngrx/store/reducers/index.ts b/src/app/main/content/apps/mail-ngrx/store/reducers/index.ts new file mode 100644 index 00000000..32c38497 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/store/reducers/index.ts @@ -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( + 'mail-app' +); + +export const getAppState = createSelector( + getMailAppState, + (state: MailAppState) => state +); + +export const reducers: ActionReducerMap = { + mails : MailsReducer, + folders: FoldersReducer, + filters: FiltersReducer, + labels : LabelsReducer +}; + +export * from './mails.reducer'; +export * from './folders.reducer'; +export * from './filters.reducer'; +export * from './labels.reducer'; diff --git a/src/app/main/content/apps/mail-ngrx/store/reducers/labels.reducer.ts b/src/app/main/content/apps/mail-ngrx/store/reducers/labels.reducer.ts new file mode 100644 index 00000000..147b4f3c --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/store/reducers/labels.reducer.ts @@ -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; + } +} diff --git a/src/app/main/content/apps/mail-ngrx/store/reducers/mails.reducer.ts b/src/app/main/content/apps/mail-ngrx/store/reducers/mails.reducer.ts new file mode 100644 index 00000000..9095d7c4 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/store/reducers/mails.reducer.ts @@ -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; + } +} diff --git a/src/app/main/content/apps/mail-ngrx/store/selectors/filters.selectors.ts b/src/app/main/content/apps/mail-ngrx/store/selectors/filters.selectors.ts new file mode 100644 index 00000000..5b740ff8 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/store/selectors/filters.selectors.ts @@ -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]) +); diff --git a/src/app/main/content/apps/mail-ngrx/store/selectors/folders.selectors.ts b/src/app/main/content/apps/mail-ngrx/store/selectors/folders.selectors.ts new file mode 100644 index 00000000..814f0113 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/store/selectors/folders.selectors.ts @@ -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]) +); diff --git a/src/app/main/content/apps/mail-ngrx/store/selectors/index.ts b/src/app/main/content/apps/mail-ngrx/store/selectors/index.ts new file mode 100644 index 00000000..7e73a9a4 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/store/selectors/index.ts @@ -0,0 +1,4 @@ +export * from './mails.selectors'; +export * from './folders.selectors'; +export * from './filters.selectors'; +export * from './labels.selectors'; diff --git a/src/app/main/content/apps/mail-ngrx/store/selectors/labels.selectors.ts b/src/app/main/content/apps/mail-ngrx/store/selectors/labels.selectors.ts new file mode 100644 index 00000000..c5ae21f5 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/store/selectors/labels.selectors.ts @@ -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]) +); diff --git a/src/app/main/content/apps/mail-ngrx/store/selectors/mails.selectors.ts b/src/app/main/content/apps/mail-ngrx/store/selectors/mails.selectors.ts new file mode 100644 index 00000000..2830a57f --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/store/selectors/mails.selectors.ts @@ -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 +); diff --git a/src/app/main/content/apps/mail-ngrx/store/store.module.ts b/src/app/main/content/apps/mail-ngrx/store/store.module.ts new file mode 100644 index 00000000..32b0a5d0 --- /dev/null +++ b/src/app/main/content/apps/mail-ngrx/store/store.module.ts @@ -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 +{ +} diff --git a/src/app/navigation/i18n/en.ts b/src/app/navigation/i18n/en.ts index 57caa4bb..e6a6da10 100644 --- a/src/app/navigation/i18n/en.ts +++ b/src/app/navigation/i18n/en.ts @@ -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', diff --git a/src/app/navigation/i18n/tr.ts b/src/app/navigation/i18n/tr.ts index 629ea1a6..7eb06307 100644 --- a/src/app/navigation/i18n/tr.ts +++ b/src/app/navigation/i18n/tr.ts @@ -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', diff --git a/src/app/navigation/navigation.model.ts b/src/app/navigation/navigation.model.ts index 544008ef..074c33b8 100644 --- a/src/app/navigation/navigation.model.ts +++ b/src/app/navigation/navigation.model.ts @@ -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', diff --git a/src/app/store/actions/index.ts b/src/app/store/actions/index.ts new file mode 100644 index 00000000..baf6af0e --- /dev/null +++ b/src/app/store/actions/index.ts @@ -0,0 +1 @@ +export * from './router.action'; diff --git a/src/app/store/actions/router.action.ts b/src/app/store/actions/router.action.ts new file mode 100644 index 00000000..f766a06e --- /dev/null +++ b/src/app/store/actions/router.action.ts @@ -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; diff --git a/src/app/store/effects/index.ts b/src/app/store/effects/index.ts new file mode 100644 index 00000000..8bf11b61 --- /dev/null +++ b/src/app/store/effects/index.ts @@ -0,0 +1,5 @@ +import { RouterEffects } from './router.effect'; + +export const effects: any[] = [RouterEffects]; + +export * from './router.effect'; diff --git a/src/app/store/effects/router.effect.ts b/src/app/store/effects/router.effect.ts new file mode 100644 index 00000000..90b9f5f7 --- /dev/null +++ b/src/app/store/effects/router.effect.ts @@ -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())); +} diff --git a/src/app/store/index.ts b/src/app/store/index.ts new file mode 100644 index 00000000..115467ce --- /dev/null +++ b/src/app/store/index.ts @@ -0,0 +1,3 @@ +export * from './reducers'; +export * from './actions'; +export * from './effects'; diff --git a/src/app/store/reducers/index.ts b/src/app/store/reducers/index.ts new file mode 100644 index 00000000..59cd70be --- /dev/null +++ b/src/app/store/reducers/index.ts @@ -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; +} + +export const reducers: ActionReducerMap = { + routerReducer: fromRouter.routerReducer, +}; + +export const getRouterState = createFeatureSelector< + fromRouter.RouterReducerState +>('routerReducer'); + +export class CustomSerializer + implements fromRouter.RouterStateSerializer { + 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 }; + } +} diff --git a/src/app/store/store.module.ts b/src/app/store/store.module.ts new file mode 100644 index 00000000..29e51c04 --- /dev/null +++ b/src/app/store/store.module.ts @@ -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[] = !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 +{ +}