Compare commits

...

25 Commits

Author SHA1 Message Date
Sercan Yemen
e477f797d0 Updated Material Moment Adapter package 2017-12-14 16:06:07 +03:00
Sercan Yemen
fb196c3864 Angular & Angular Material versions updated 2017-12-14 15:51:09 +03:00
mustafahlvc
5cf44962fc (Mail) back arrow visibility updated for responsive 2017-12-14 12:27:08 +03:00
mustafahlvc
06b0c3775a (Mail-ngrx) missing deSelectCurrentFunction() added to mail.component,
back arrow visibility updated for responsive
2017-12-14 12:25:48 +03:00
mustafahlvc
26690990f0 (Mail-ngrx) currentMail fixes on html 2017-12-14 11:52:53 +03:00
mustafahlvc
377092d9ec (Mail-ngrx) unread, selected class added for mail list item 2017-12-14 11:10:35 +03:00
mustafahlvc
abede386c8 (Mail-ngrx) Ngrx version of Mail App added. 2017-12-12 22:31:50 +03:00
Sercan Yemen
242feaa169 Updated Angular to 5.0.1
+ Updated Angular Material to 5.0.0 stable
+ Install the ngx-dnd package from its original location
+ Updated various other packages
+ Increased the Fuse version
2017-12-08 10:34:39 +03:00
Sercan Yemen
7c2494a82c Fixed: Shortcut items duplicates on search in some cases 2017-12-08 10:31:39 +03:00
Sercan Yemen
5c2e717a40 Make sure the nav item has children before trying to get them
+ Added LICENSE file
+ Renamed the KnowledgeBase demo module
2017-12-08 09:34:30 +03:00
Sercan Yemen
6ae0a9760d Fix the missing backgrounds
+ Increased the shadow weight of the content on Auth, Coming Soon and Maintenance pages
2017-12-06 14:39:24 +03:00
Sercan Yemen
2a10f3e443 Moved the navigation.model.ts into its own folder
+ Added support for translations in nav items
+ Added badge support for collapsable nav items
+ Initialize the navigation from app.component rather then navigation.service
2017-12-06 14:10:48 +03:00
Sercan Yemen
db7a00440c Re-activated mail search
+ Small formatting in mail translation
2017-12-06 14:09:31 +03:00
Sercan Yemen
0e1c589399 Use absolute paths for importing fuse.scss 2017-12-06 11:40:43 +03:00
Sercan Yemen
2f419b1af5 Removed unused HttpModule import 2017-12-06 11:40:16 +03:00
Sercan Yemen
effd3cefcb Scrumboard and Contacts app dialogs responsive fixes 2017-12-06 11:35:58 +03:00
Sercan Yemen
21fd488a8e ngx-color-picker style adjustments
+ Calendar event form responsive fixes
2017-12-06 11:26:57 +03:00
Sercan Yemen
37a5c69269 Removed Google Calendar images as they are no longer available freely from Google 2017-12-06 10:34:40 +03:00
Sercan Yemen
9f440b1bf2 Some code inspection fixes 2017-11-30 15:55:00 +03:00
Sercan Yemen
a65f61cce4 Form Stepper examples
+ Simplified the mat-select style
+ Further simplified the mat-select in dashboard widgets
2017-11-30 15:37:32 +03:00
Sercan Yemen
0d8fe0be72 Trigger expand/collapse of the navigation on ngOnInit to update the active item 2017-11-30 10:37:35 +03:00
Sercan Yemen
2bbc90af64 Perfect scrollbar shouldn't be destroyed if it's not available
+ Print media css classes updated to hide the perfect scrollbars
2017-11-30 10:22:18 +03:00
Sercan Yemen
ad21d9fed5 Further improvements to Auth pages 2017-11-30 10:21:22 +03:00
Sercan Yemen
4c6ef29e20 Fixed: Behavior Subject must be array 2017-11-28 10:48:52 +03:00
Sercan Yemen
a74c5108fd Fixed: Couple small issues with Auth forms
Added custom validator for Password Matching to Register forms
2017-11-28 10:42:41 +03:00
162 changed files with 4588 additions and 595 deletions

1
LICENSE Normal file
View File

@@ -0,0 +1 @@
https://themeforest.net/licenses/terms/regular

550
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "fuse2",
"version": "1.2.3",
"license": "",
"version": "1.3.0",
"license": "https://themeforest.net/licenses/terms/regular",
"scripts": {
"ng": "ng",
"start": "ng serve",
@@ -16,56 +16,61 @@
"private": true,
"dependencies": {
"@agm/core": "1.0.0-beta.2",
"@angular/animations": "5.0.3",
"@angular/cdk": "5.0.0-rc.1",
"@angular/common": "5.0.3",
"@angular/compiler": "5.0.3",
"@angular/core": "5.0.3",
"@angular/animations": "5.1.1",
"@angular/cdk": "5.0.1",
"@angular/common": "5.1.1",
"@angular/compiler": "5.1.1",
"@angular/core": "5.1.1",
"@angular/flex-layout": "2.0.0-beta.10-4905443",
"@angular/forms": "5.0.3",
"@angular/http": "5.0.3",
"@angular/material": "5.0.0-rc.1",
"@angular/material-moment-adapter": "5.0.0-rc.1",
"@angular/platform-browser": "5.0.3",
"@angular/platform-browser-dynamic": "5.0.3",
"@angular/router": "5.0.3",
"@ngx-translate/core": "8.0.0",
"@angular/forms": "5.1.1",
"@angular/http": "5.1.1",
"@angular/material": "5.0.1",
"@angular/material-moment-adapter": "5.0.1",
"@angular/platform-browser": "5.1.1",
"@angular/platform-browser-dynamic": "5.1.1",
"@angular/router": "5.1.1",
"@ngx-translate/core": "9.0.1",
"@swimlane/ngx-charts": "7.0.1",
"@swimlane/ngx-datatable": "11.1.4",
"@withinpixels/ngx-dnd": "3.1.0",
"@swimlane/ngx-datatable": "11.1.5",
"@swimlane/ngx-dnd": "3.1.0",
"angular-calendar": "0.22.1",
"angular-in-memory-web-api": "0.5.1",
"angular-in-memory-web-api": "0.5.2",
"angular2-markdown": "1.6.0",
"classlist.js": "1.1.20150312",
"core-js": "2.5.1",
"core-js": "2.5.3",
"d3": "4.12.0",
"hammerjs": "2.0.8",
"highlight.js": "9.12.0",
"intl": "1.2.5",
"moment": "2.19.2",
"ngx-color-picker": "5.0.4",
"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.1.0",
"ngx-cookie-service": "1.0.9",
"perfect-scrollbar": "1.2.0",
"rxjs": "5.5.2",
"perfect-scrollbar": "1.3.0",
"rxjs": "5.5.5",
"web-animations-js": "2.3.1",
"zone.js": "0.8.18"
},
"devDependencies": {
"@angular/cli": "1.5.4",
"@angular/compiler-cli": "5.0.3",
"@angular/language-service": "5.0.3",
"@angular/cli": "1.6.1",
"@angular/compiler-cli": "5.1.1",
"@angular/language-service": "5.1.1",
"@angularclass/hmr": "2.1.3",
"@types/jasmine": "2.5.54",
"@types/jasminewd2": "2.0.3",
"@types/node": "6.0.92",
"codelyzer": "4.0.1",
"@types/node": "6.0.94",
"codelyzer": "4.0.2",
"jasmine-core": "2.6.4",
"jasmine-spec-reporter": "4.1.1",
"karma": "1.7.1",
"karma-chrome-launcher": "2.1.1",
"karma-cli": "1.0.1",
"karma-coverage-istanbul-reporter": "1.3.0",
"karma-jasmine": "1.1.0",
"karma-jasmine": "1.1.1",
"karma-jasmine-html-reporter": "0.2.2",
"protractor": "5.1.2",
"ts-node": "3.2.2",

View File

@@ -1,6 +1,12 @@
import { Component } from '@angular/core';
import { FuseSplashScreenService } from './core/services/splash-screen.service';
import { TranslateService } from '@ngx-translate/core';
import { FuseTranslationLoaderService } from './core/services/translation-loader.service';
import { FuseNavigationService } from './core/components/navigation/navigation.service';
import { FuseNavigationModel } from './navigation/navigation.model';
import { locale as navigationEnglish } from './navigation/i18n/en';
import { locale as navigationTurkish } from './navigation/i18n/tr';
@Component({
selector : 'fuse-root',
@@ -10,8 +16,10 @@ import { TranslateService } from '@ngx-translate/core';
export class AppComponent
{
constructor(
private fuseNavigationService: FuseNavigationService,
private fuseSplashScreen: FuseSplashScreenService,
private translate: TranslateService
private translate: TranslateService,
private translationLoader: FuseTranslationLoaderService
)
{
// Add languages
@@ -22,5 +30,11 @@ export class AppComponent
// Use a language
this.translate.use('en');
// Set the navigation model
this.fuseNavigationService.setNavigationModel(new FuseNavigationModel());
// Set the navigation translations
this.translationLoader.loadTranslations(navigationEnglish, navigationTurkish);
}
}

View File

@@ -1,6 +1,5 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpModule } from '@angular/http';
import { HttpClientModule } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule, Routes } from '@angular/router';
@@ -22,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'
@@ -68,23 +72,19 @@ const appRoutes: Routes = [
],
imports : [
BrowserModule,
HttpModule,
HttpClientModule,
BrowserAnimationsModule,
RouterModule.forRoot(appRoutes),
SharedModule,
MarkdownModule.forRoot(),
TranslateModule.forRoot(),
InMemoryWebApiModule.forRoot(FuseFakeDbService, {
delay : 0,
passThruUnknownUrl: true
}),
AppStoreModule,
FuseMainModule,
ProjectModule,
PagesModule,
UIModule,
ServicesModule,

View File

@@ -1,6 +1,10 @@
<a class="nav-link" matRipple>
<mat-icon class="nav-link-icon" *ngIf="item.icon">{{item.icon}}</mat-icon>
<span class="nav-link-title">{{item.title}}</span>
<span class="nav-link-title" [translate]="item.translate">{{item.title}}</span>
<span class="nav-link-badge" *ngIf="item.badge" [translate]="item.badge.translate"
[ngStyle]="{'background-color': item.badge.bg,'color': item.badge.fg}">
{{item.badge.title}}
</span>
<mat-icon class="collapse-arrow">keyboard_arrow_right</mat-icon>
</a>

View File

@@ -1,8 +1,8 @@
<a class="nav-link" *ngIf="item.url" [routerLink]="[item.url]" routerLinkActive="active"
[routerLinkActiveOptions]="{exact: item.exactMatch || false}" matRipple>
<mat-icon class="nav-link-icon" *ngIf="item.icon">{{item.icon}}</mat-icon>
<span class="nav-link-title">{{item.title}}</span>
<span class="nav-link-badge" *ngIf="item.badge"
<span class="nav-link-title" [translate]="item.translate">{{item.title}}</span>
<span class="nav-link-badge" *ngIf="item.badge" [translate]="item.badge.translate"
[ngStyle]="{'background-color': item.badge.bg,'color': item.badge.fg}">
{{item.badge.title}}
</span>
@@ -10,8 +10,8 @@
<span class="nav-link" *ngIf="item.function" (click)="item.function()" matRipple>
<mat-icon class="nav-link-icon" *ngIf="item.icon">{{item.icon}}</mat-icon>
<span class="nav-link-title">{{item.title}}</span>
<span class="nav-link-badge" *ngIf="item.badge"
<span class="nav-link-title" [translate]="item.translate">{{item.title}}</span>
<span class="nav-link-badge" *ngIf="item.badge" [translate]="item.badge.translate"
[ngStyle]="{'background-color': item.badge.bg,'color': item.badge.fg}">
{{item.badge.title}}
</span>

View File

@@ -0,0 +1,5 @@
export interface FuseNavigationModelInterface
{
model: any[];
}

View File

@@ -1,6 +1,6 @@
import { EventEmitter, Injectable } from '@angular/core';
import { NavigationModel } from '../../../navigation.model';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { FuseNavigationModelInterface } from './navigation.model';
@Injectable()
export class FuseNavigationService
@@ -8,13 +8,11 @@ export class FuseNavigationService
onNavCollapseToggle = new EventEmitter<any>();
onNavCollapseToggled = new EventEmitter<any>();
onNavigationModelChange: BehaviorSubject<any> = new BehaviorSubject({});
navigationModel: NavigationModel;
navigationModel: FuseNavigationModelInterface;
flatNavigation: any[] = [];
constructor()
{
this.navigationModel = new NavigationModel();
this.onNavigationModelChange.next(this.navigationModel.model);
}
/**
@@ -155,8 +153,15 @@ export class FuseNavigationService
*/
getFlatNavigation(navigationItems?)
{
// If navigation items not provided,
// that means we are running the function
// for the first time...
if ( !navigationItems )
{
// Reset the flat navigation
this.flatNavigation = [];
// Get the entire navigation model
navigationItems = this.navigationModel.model;
}
@@ -181,7 +186,10 @@ export class FuseNavigationService
if ( navItem.type === 'collapse' || navItem.type === 'group' )
{
this.getFlatNavigation(navItem.children);
if ( navItem.children )
{
this.getFlatNavigation(navItem.children);
}
}
}

View File

@@ -1,8 +1,13 @@
<a class="nav-link" matRipple (click)="toggleOpen($event)">
<mat-icon class="nav-link-icon" *ngIf="item.icon">{{item.icon}}</mat-icon>
<span class="nav-link-title">{{item.title}}</span>
<span class="nav-link-title" [translate]="item.translate">{{item.title}}</span>
<span class="nav-link-badge" *ngIf="item.badge" [translate]="item.badge.translate"
[ngStyle]="{'background-color': item.badge.bg,'color': item.badge.fg}">
{{item.badge.title}}
</span>
<mat-icon class="collapse-arrow">keyboard_arrow_right</mat-icon>
</a>
<div class="children" [@slideInOut]="isOpen">
<ng-container *ngFor="let item of item.children">
<fuse-nav-vertical-item *ngIf="item.type=='item'" [item]="item"></fuse-nav-vertical-item>

View File

@@ -69,6 +69,20 @@ export class FuseNavVerticalCollapseComponent implements OnInit
);
}
ngOnInit()
{
// Check if the url can be found in
// one of the children of this item
if ( this.isUrlInChildren(this.item, this.router.url) )
{
this.expand();
}
else
{
this.collapse();
}
}
/**
* Toggle collapse
*
@@ -108,6 +122,7 @@ export class FuseNavVerticalCollapseComponent implements OnInit
{
return;
}
this.isOpen = false;
this.navigationService.onNavCollapseToggle.emit();
}
@@ -175,8 +190,4 @@ export class FuseNavVerticalCollapseComponent implements OnInit
return false;
}
ngOnInit()
{
}
}

View File

@@ -1,6 +1,7 @@
<div class="group-title">
<span class="hint-text">{{ item.title }}</span>
<span class="hint-text" [translate]="item.translate">{{ item.title }}</span>
</div>
<div class="group-items">
<ng-container *ngFor="let item of item.children">
<fuse-nav-vertical-group *ngIf="item.type=='group'" [item]="item"></fuse-nav-vertical-group>

View File

@@ -1,8 +1,8 @@
<a class="nav-link" *ngIf="item.url" [routerLink]="[item.url]" routerLinkActive="active"
[routerLinkActiveOptions]="{exact: item.exactMatch || false}" matRipple>
<mat-icon class="nav-link-icon" *ngIf="item.icon">{{item.icon}}</mat-icon>
<span class="nav-link-title">{{item.title}}</span>
<span class="nav-link-badge" *ngIf="item.badge"
<span class="nav-link-title" [translate]="item.translate">{{item.title}}</span>
<span class="nav-link-badge" *ngIf="item.badge" [translate]="item.badge.translate"
[ngStyle]="{'background-color': item.badge.bg,'color': item.badge.fg}">
{{item.badge.title}}
</span>
@@ -10,8 +10,8 @@
<span class="nav-link" *ngIf="item.function" (click)="item.function()" matRipple>
<mat-icon class="nav-link-icon" *ngIf="item.icon">{{item.icon}}</mat-icon>
<span class="nav-link-title">{{item.title}}</span>
<span class="nav-link-badge" *ngIf="item.badge"
<span class="nav-link-title" [translate]="item.translate">{{item.title}}</span>
<span class="nav-link-badge" *ngIf="item.badge" [translate]="item.badge.translate"
[ngStyle]="{'background-color': item.badge.bg,'color': item.badge.fg}">
{{item.badge.title}}
</span>

View File

@@ -62,4 +62,23 @@ fuse-widget {
transform: rotateY(360deg);
}
}
.mat-form-field {
&.mat-form-field-type-mat-select {
.mat-input-wrapper {
padding: 16px 0;
.mat-input-infix {
border: none;
padding: 0;
}
}
.mat-input-underline {
display: none;
}
}
}
}

View File

@@ -27,7 +27,9 @@ export class FuseWidgetComponent implements OnInit, AfterContentInit
setTimeout(() => {
this.toggleButtons.forEach(flipButton => {
this.renderer.listen(flipButton.el.nativeElement, 'click', () => {
this.renderer.listen(flipButton.el.nativeElement, 'click', (event) => {
event.preventDefault();
event.stopPropagation();
this.toggle();
});
});

View File

@@ -54,7 +54,7 @@ export class FusePerfectScrollbarDirective implements OnInit, AfterViewInit, OnD
ngOnDestroy()
{
if ( !this.isInitialized )
if ( !this.isInitialized || !this.ps )
{
return;
}

View File

@@ -5,7 +5,7 @@ import { CommonModule } from '@angular/common';
import { MaterialModule } from './material.module';
import { FlexLayoutModule } from '@angular/flex-layout';
import { ColorPickerModule } from 'ngx-color-picker';
import { NgxDnDModule } from '@withinpixels/ngx-dnd';
import { NgxDnDModule } from '@swimlane/ngx-dnd';
import { NgxDatatableModule } from '@swimlane/ngx-datatable';
import { FuseMatSidenavHelperDirective, FuseMatSidenavTogglerDirective } from '../directives/fuse-mat-sidenav-helper/fuse-mat-sidenav-helper.directive';

View File

@@ -28,3 +28,51 @@
.mat-form-field-underline {
background-color: rgba(0, 0, 0, 0.12);
}
// Fix: "Some idiots using table-cell and inline-table in mat-select"
.mat-form-field {
&.mat-form-field-type-mat-select {
.mat-input-infix {
display: inline-flex;
width: auto;
.mat-select-trigger {
display: inline-flex;
align-items: center;
width: 100%;
.mat-select-value {
display: flex;
max-width: none;
margin-right: 8px;
}
.mat-select-arrow-wrapper {
display: inline-flex;
}
}
}
}
}
// Fix: "Stepper icons are broken due to Fuse's icon helpers"
mat-horizontal-stepper,
mat-vertical-stepper {
mat-step-header {
mat-icon {
height: 16px !important;
width: 16px !important;
min-width: 0 !important;
min-height: 0 !important;
color: rgba(255, 255, 255, 0.87) !important;
}
}
}
mat-vertical-stepper {
padding: 16px 0;
}

View File

@@ -47,14 +47,20 @@
}
.nav-link-badge {
display: flex;
align-items: center;
min-width: 20px;
height: 20px;
line-height: 20px;
padding: 0 7px;
font-size: 11px;
font-weight: 500;
border-radius: 20px;
transition: opacity 0.2s ease-in-out 0.1s;
margin-left: 8px;
+ .collapse-arrow {
margin-left: 8px;
}
}
&:hover {

View File

@@ -42,8 +42,8 @@
fuse-footer,
fuse-quick-panel,
fuse-theme-options,
.ps > .ps__scrollbar-x-rail,
.ps > .ps__scrollbar-y-rail {
.ps > .ps__rail-x,
.ps > .ps__rail-y {
display: none !important;
}

View File

@@ -3,20 +3,21 @@
.color-picker {
height: auto !important;
border: none !important;
@include mat-elevation(4);
.preset-area {
//padding: 4px 15px;
padding: 0 0 12px 12px !important;
padding: 0 0 16px 16px !important;
height: 140px;
overflow-y: auto;
overflow-x: hidden;
> hr {
display: none;
}
.preset-label {
display: none;
}
.preset-color {
@@ -25,7 +26,8 @@
margin: 0 !important;
border: none !important;
border-radius: 0 !important;
&:nth-child(14n+3) {
&:nth-child(14n+1) {
clear: both;
}
}

View File

@@ -216,52 +216,52 @@
}
&.Jan {
background-image: url('/assets/images/backgrounds/january.jpg');
background-position: 0 45%;
background-image: url('/assets/images/backgrounds/winter.jpg');
background-position: 0 65%;
}
&.Feb {
background-image: url('/assets/images/backgrounds/february.jpg');
background-position: 0 50%;
background-image: url('/assets/images/backgrounds/winter.jpg');
background-position: 0 65%;
}
&.Mar {
background-image: url('/assets/images/backgrounds/march.jpg');
background-position: 0 45%;
background-image: url('/assets/images/backgrounds/spring.jpg');
background-position: 0 19%;
}
&.Apr {
background-image: url('/assets/images/backgrounds/april.jpg');
background-position: 0 48%;
background-image: url('/assets/images/backgrounds/spring.jpg');
background-position: 0 19%;
}
&.May {
background-image: url('/assets/images/backgrounds/may.jpg');
background-position: 0 47%;
background-image: url('/assets/images/backgrounds/spring.jpg');
background-position: 0 19%;
}
&.Jun {
background-image: url('/assets/images/backgrounds/june.jpg');
background-position: 0 48%;
background-image: url('/assets/images/backgrounds/summer.jpg');
background-position: 0 91%;
}
&.Jul {
background-image: url('/assets/images/backgrounds/july.jpg');
background-position: 0 3%;
background-image: url('/assets/images/backgrounds/summer.jpg');
background-position: 0 91%;
}
&.Aug {
background-image: url('/assets/images/backgrounds/august.jpg');
background-position: 0 61%;
background-image: url('/assets/images/backgrounds/summer.jpg');
background-position: 0 91%;
}
&.Sep {
background-image: url('/assets/images/backgrounds/september.jpg');
background-position: 0 58%;
background-image: url('/assets/images/backgrounds/autumn.jpg');
background-position: 0 40%;
}
&.Oct {
background-image: url('/assets/images/backgrounds/october.jpg');
background-position: 0 50%;
background-image: url('/assets/images/backgrounds/autumn.jpg');
background-position: 0 40%;
}
&.Nov {
background-image: url('/assets/images/backgrounds/november.jpg');
background-position: 0 46%;
background-image: url('/assets/images/backgrounds/autumn.jpg');
background-position: 0 40%;
}
&.Dec {
background-image: url('/assets/images/backgrounds/december.jpg');
background-position: 0 43%;
background-image: url('/assets/images/backgrounds/winter.jpg');
background-position: 0 65%;
}
.header-content {
@@ -299,7 +299,7 @@
.add-event-button {
position: absolute;
right: 18px;
bottom: -32px;
bottom: -26px;
z-index: 10;
}

View File

@@ -32,9 +32,11 @@
</mat-slide-toggle>
</div>
<div class="py-16" fxFlex="1 0 auto" fxLayout="row" formGroupName="color">
<mat-form-field class="mr-24" fxFlex>
<div class="py-16" fxFlex="1 0 auto" fxLayout="column" fxLayout.gt-xs="row" formGroupName="color">
<mat-form-field class="mr-sm-24" fxFlex>
<input matInput
class="primary-color-input"
name="primary color"
formControlName="primary"
placeholder="Primary color"
@@ -44,8 +46,10 @@
[style.background]="event.color.primary"
(colorPickerChange)="event.color.primary = $event; eventForm.patchValue({color:{primary:$event}})"/>
</mat-form-field>
<mat-form-field fxFlex>
<input matInput
class="secondary-color-input"
name="secondary color"
formControlName="secondary"
placeholder="Secondary color"
@@ -55,11 +59,12 @@
[style.background]="event.color.secondary"
(colorPickerChange)="event.color.secondary = $event; eventForm.patchValue({color:{secondary:$event}})"/>
</mat-form-field>
</div>
<div fxFlex="1 0 auto" fxLayout="row">
<div fxFlex="1 0 auto" fxLayout="column" fxLayout.gt-xs="row">
<mat-form-field class="mr-24" fxFlex>
<mat-form-field class="mr-sm-24" fxFlex>
<input matInput [matDatepicker]="startDatePicker" placeholder="Start Date"
name="start"
formControlName="start">
@@ -73,9 +78,9 @@
</div>
<div fxFlex="1 0 auto" fxLayout="row">
<div fxFlex="1 0 auto" fxLayout="column" fxLayout.gt-xs="row">
<mat-form-field class="mr-24" fxFlex>
<mat-form-field class="mr-sm-24" fxFlex>
<input matInput [matDatepicker]="endDatePicker" placeholder="End Date"
name="end"
formControlName="end">
@@ -98,12 +103,12 @@
<mat-form-field formGroupName="meta" class="w-100-p">
<textarea matInput
formControlName="notes"
placeholder="Notes"
mat-maxlength="250"
max-rows="4">
</textarea>
<textarea matInput
formControlName="notes"
placeholder="Notes"
mat-maxlength="250"
max-rows="4">
</textarea>
</mat-form-field>
</form>

View File

@@ -1,8 +1,17 @@
@import "src/app/core/scss/fuse";
.event-form-dialog {
@include media-breakpoint('xs') {
width: 100%;
}
@include media-breakpoint-up('xs') {
width: 480px;
}
.mat-dialog-container {
padding: 0;
width: 480px;
}
.dialog-content-wrapper {
@@ -10,4 +19,9 @@
display: flex;
flex-direction: column;
}
.primary-color-input,
.secondary-color-input {
padding: 6px 8px;
}
}

View File

@@ -2,15 +2,18 @@
.contact-form-dialog {
@include media-breakpoint('xs') {
width: 100%;
}
@include media-breakpoint-up('xs') {
width: 400px;
}
.mat-dialog-container {
padding: 0;
width: 400px;
overflow: hidden;
@include media-breakpoint('xs') {
width: auto;
}
.mat-toolbar {
min-height: initial;
}

View File

@@ -10,7 +10,7 @@ import { Subject } from 'rxjs/Subject';
@Injectable()
export class ContactsService implements Resolve<any>
{
onContactsChanged: BehaviorSubject<any> = new BehaviorSubject({});
onContactsChanged: BehaviorSubject<any> = new BehaviorSubject([]);
onSelectedContactsChanged: BehaviorSubject<any> = new BehaviorSubject([]);
onUserDataChanged: BehaviorSubject<any> = new BehaviorSubject([]);
onSearchTextChanged: Subject<any> = new Subject();

View File

@@ -703,7 +703,7 @@
<div class="pl-16 pr-8 py-16" fxLayout="row" fxLayoutAlign="space-between center">
<div class="h3">{{dateNow | date: 'EEEE, h:m:ss'}}</div>
<div class="h3">{{dateNow | date: 'EEEE, HH:mm:ss'}}</div>
<div>
<button mat-icon-button [matMenuTriggerFor]="moreMenu" aria-label="more">

View File

@@ -1,9 +1,5 @@
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { EcommerceDashboardService } from './dashboard.service';
import * as shape from 'd3-shape';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
import { DataSource } from '@angular/cdk/collections';
import { fuseAnimations } from '../../../../../core/animations';
@Component({
@@ -23,14 +19,10 @@ export class FuseEcommerceDashboardComponent implements OnInit, OnDestroy
widget6: any = {};
widget7: any = {};
dateNow = Date.now();
constructor(private projectsDashboardService: EcommerceDashboardService)
{
this.projects = this.projectsDashboardService.projects;
this.selectedProject = this.projects[0];
this.widgets = this.projectsDashboardService.widgets;
/**

View File

@@ -1,4 +1,4 @@
import { Component, ElementRef, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { EcommerceOrderService } from './order.service';
import { fuseAnimations } from '../../../../../core/animations';
import 'rxjs/add/operator/startWith';
@@ -10,9 +10,6 @@ import 'rxjs/add/observable/fromEvent';
import { Subscription } from 'rxjs/Subscription';
import { Order } from './order.model';
import { FormBuilder, FormGroup } from '@angular/forms';
import { FuseUtils } from '../../../../../core/fuseUtils';
import { MatSnackBar } from '@angular/material';
import { Location } from '@angular/common';
import { orderStatuses } from './order-statuses';
@Component({
@@ -32,8 +29,6 @@ export class FuseEcommerceOrderComponent implements OnInit, OnDestroy
constructor(
private orderService: EcommerceOrderService,
private formBuilder: FormBuilder,
public snackBar: MatSnackBar,
private location: Location
)
{

View File

@@ -1,4 +1,4 @@
import { Component, ElementRef, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { EcommerceProductService } from './product.service';
import { fuseAnimations } from '../../../../../core/animations';
import 'rxjs/add/operator/startWith';

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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: ['']
});
}
}

View 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'
}
}
};

View 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'
}
}
};

View File

@@ -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>

View File

@@ -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;
}
}
}
}

View File

@@ -0,0 +1,77 @@
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);
this.markAsRead();
}
markAsRead()
{
if ( this.mail && !this.mail.read )
{
this.mail.markRead();
this.updateMail();
}
}
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()
{
}
}

View File

@@ -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>

View File

@@ -0,0 +1,130 @@
@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;
background: #FAFAFA;
&.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%;
}
}
}
}
}
}

View File

@@ -0,0 +1,58 @@
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;
@HostBinding('class.unread') unread: 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.unread = !this.mail.read;
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()
{
}
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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[];
@Input() 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()
{
}
}

View 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$ | async}">
<!-- 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$ | async" fxHide.gt-xs>
<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" [currentMail]="currentMail$ | 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>

View 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;
}
}
}
}
}
}
}

View File

@@ -0,0 +1,133 @@
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));
}
deSelectCurrentMail()
{
this.store.dispatch(new fromStore.SetCurrentMail(''));
}
refresh()
{
this.cd.markForCheck();
}
ngOnDestroy()
{
this.cd.detach();
}
}

View File

@@ -0,0 +1,66 @@
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;
}
markRead()
{
this.read = true;
}
markUnRead()
{
this.read = false;
}
}

View 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
{
}

View 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});
}
}

View File

@@ -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 -->

View File

@@ -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 {
}
}
}

View File

@@ -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()
{
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,4 @@
export * from './mails.actions';
export * from './folders.actions';
export * from './filters.actions';
export * from './labels.actions';

View File

@@ -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;

View File

@@ -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;

View File

@@ -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)))
);
}
));
}

View File

@@ -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)))
);
}
));
}

View 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';

View File

@@ -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)))
);
}
));
}

View File

@@ -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);
})
);
}

View File

@@ -0,0 +1 @@
export * from './resolve.guard';

View File

@@ -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)
);
}
}

View File

@@ -0,0 +1,4 @@
export * from './actions';
export * from './reducers';
export * from './selectors';
export * from './effects';

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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';

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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])
);

View File

@@ -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])
);

View File

@@ -0,0 +1,4 @@
export * from './mails.selectors';
export * from './folders.selectors';
export * from './filters.selectors';
export * from './labels.selectors';

View File

@@ -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])
);

View File

@@ -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
);

View 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
{
}

View File

@@ -8,7 +8,7 @@ export const locale = {
'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'
'SEARCH_PLACEHOLDER' : 'Search for an e-mail or contact'
}
}
};

View File

@@ -85,7 +85,7 @@
</mat-menu>
</div>
<div *ngIf="currentMail" fxHide.gt-lg>
<div *ngIf="currentMail" fxHide.gt-xs>
<button mat-icon-button (click)="deSelectCurrentMail()">
<mat-icon>arrow_back</mat-icon>
</button>

View File

@@ -4,6 +4,8 @@ import { Subscription } from 'rxjs/Subscription';
import { FormControl } from '@angular/forms';
import { Mail } from './mail.model';
import { FuseTranslationLoaderService } from '../../../../core/services/translation-loader.service';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import { locale as english } from './i18n/en';
import { locale as turkish } from './i18n/tr';
@@ -80,12 +82,12 @@ export class FuseMailComponent implements OnInit, OnDestroy
}
});
/*this.searchInput.valueChanges
this.searchInput.valueChanges
.debounceTime(300)
.distinctUntilChanged()
.subscribe(searchText => {
this.mailService.onSearchTextChanged.next(searchText);
});*/
});
}
ngOnDestroy()

View File

@@ -4,7 +4,7 @@ import { Observable } from 'rxjs/Observable';
import { HttpClient } from '@angular/common/http';
import { Mail } from './mail.model';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { FuseUtils } from 'app/core/fuseUtils';
import { FuseUtils } from '../../../../core/fuseUtils';
@Injectable()
export class MailService implements Resolve<any>

View File

@@ -8,7 +8,7 @@
</button>
<form *ngIf="formActive" class="new-list-form" [formGroup]="form" (submit)="onFormSubmit()"
fxFlex="1 0 auto" fxFlex="row" fxLayoutAlign="start center">
fxLayout="row" fxLayoutAlign="start center" fxFlex="1 0 auto">
<input formControlName="name" #nameInput fxFlex placeholder="Write a list Name">

View File

@@ -131,8 +131,8 @@
<div *ngIf="card" mat-dialog-content class="p-24 m-0" fusePerfectScrollbar>
<div fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="space-between center"
fxLayout.xs="column" fxLayoutAlign="center center">
<div fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="space-between center"
fxLayout="column" fxLayoutAlign="center center">
<!-- BREADCRUMB -->
<div class="card-breadcrumb mb-16 mb-sm-0" fxLayout="row" fxLayoutAlign="start center">

View File

@@ -2,9 +2,16 @@
.scrumboard-card-dialog {
@include media-breakpoint('xs') {
width: 100%;
}
@include media-breakpoint-up('xs') {
width: 720px;
}
.mat-dialog-container {
padding: 0;
width: 720px;
.mat-toolbar {
@@ -162,9 +169,16 @@
background-size: contain;
background-position: 50% 50%;
background-repeat: no-repeat;
margin-right: 24px;
font-weight: 500;
color: rgba(0, 0, 0, 0.6);
@include media-breakpoint('xs') {
margin-bottom: 24px;
}
@include media-breakpoint-up('xs') {
margin-right: 24px;
}
}
.attachment-content {

View File

@@ -7,8 +7,7 @@
<form [formGroup]="form" (ngSubmit)="onFormSubmit()"
class="board-name-form" fxFlex="1 0 auto"
*ngIf="formActive" fxFlex="row">
class="board-name-form" fxLayout="row" fxFlex="1 0 auto" *ngIf="formActive">
<input formControlName="name" #nameInput fxFlex="1 0 auto" placeholder="Write a board name">

View File

@@ -3,8 +3,8 @@
</div>
<form [formGroup]="form" (ngSubmit)="onFormSubmit()"
class="list-header-name-form" fxFlex="1 0 auto"
*ngIf="formActive" fxFlex="row">
class="list-header-name-form" fxLayout="row" fxFlex="1 0 auto"
*ngIf="formActive">
<input formControlName="name" #nameInput fxFlex placeholder="Write a list Name">

View File

@@ -1,11 +1,10 @@
import { Component, Input, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { FuseUtils } from '../../../../../../core/fuseUtils';
import { ScrumboardService } from 'app/main/content/apps/scrumboard/scrumboard.service';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs/Subscription';
import { MatDialog, MatDialogRef } from '@angular/material';
import { FuseScrumboardCardDialogComponent } from '../dialogs/card/card.component';
import { FuseConfirmDialogComponent } from '../../../../../../core/components/confirm-dialog/confirm-dialog.component';
import { ScrumboardService } from '../../scrumboard.service';
import { Card } from '../../card.model';
import { FusePerfectScrollbarDirective } from '../../../../../../core/directives/fuse-perfect-scrollbar/fuse-perfect-scrollbar.directive';
@@ -24,7 +23,6 @@ export class FuseScrumboardBoardListComponent implements OnInit, OnDestroy
@ViewChild(FusePerfectScrollbarDirective) listScroll: FusePerfectScrollbarDirective;
onBoardChanged: Subscription;
confirmDialogRef: MatDialogRef<FuseConfirmDialogComponent>;
constructor(

View File

@@ -18,7 +18,7 @@
<p>
<code>fuse-navigation</code> is a custom built Fuse component allows you to create a multi-level collapsable
navigation.
navigation. It has built-in support for translations using <b>ngx-translate</b> module.
</p>
<div class="my-48">
@@ -40,6 +40,11 @@
<b>Collapsable</b> and <b>Item</b>. These items can be mixed and matched to create unique and complex
navigation layouts.
</p>
<p class="py-8">
Navigation model can be found in <code>src/app/navigation</code> folder along with its translation
files. Navigation model and its translation files are set in <b>app.component.ts</b> file. Check that
file for more detailed usage example.
</p>
</div>
<div class="my-48">
@@ -48,9 +53,10 @@
<fuse-hljs lang="json" class="source-code">
<textarea #source hidden="hidden">
{
'title': 'COMPONENTS',
'type' : 'group',
'children': [
'title' : 'COMPONENTS',
'translate': 'NAV.COMPONENTS',
'type' : 'group',
'children' : [
{
'title': 'ngx-datatable',
'type' : 'item',
@@ -69,10 +75,11 @@
<fuse-hljs lang="json" class="source-code">
<textarea #source hidden="hidden">
{
'title' : 'Datatables',
'type' : 'collapse',
'icon' : 'border_all',
'children': [
'title' : 'Datatables',
'translate': 'NAV.DATATABLES',
'type' : 'collapse',
'icon' : 'border_all',
'children' : [
{
'title': 'ngx-datatable',
'type' : 'nav-item',
@@ -91,10 +98,11 @@
<fuse-hljs lang="json" class="source-code">
<textarea #source hidden="hidden">
{
'title': 'Countdown',
'type' : 'item',
'icon' : 'settings_input_component',
'url' : '/components/countdown'
'title' : 'Countdown',
'translate': 'NAV.COUNTDOWN',
'type' : 'item',
'icon' : 'settings_input_component',
'url' : '/components/countdown'
},
</textarea>
</fuse-hljs>

View File

@@ -17,7 +17,8 @@
</div>
<div id="forgot-password-form-wrapper" fusePerfectScrollbar *fuseIfOnDom [@animate]="{value:'*',params:{delay:'300ms',x:'100%'}}">
<div id="forgot-password-form-wrapper" fusePerfectScrollbar *fuseIfOnDom
[@animate]="{value:'*',params:{delay:'300ms',x:'100%'}}">
<div id="forgot-password-form">

View File

@@ -5,7 +5,7 @@
#forgot-password {
width: 100%;
overflow: hidden;
background: url('/assets/images/backgrounds/march.jpg') no-repeat;
background: url('/assets/images/backgrounds/dark-material-bg.jpg') no-repeat;
background-size: cover;
#forgot-password-intro {
@@ -39,7 +39,7 @@
min-width: 400px;
max-width: 400px;
background: #FFFFFF;
@include mat-elevation(7);
@include mat-elevation(16);
@include media-breakpoint('sm') {
width: 360px;

View File

@@ -1,5 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { FuseConfigService } from '../../../../../core/services/config.service';
import { fuseAnimations } from '../../../../../core/animations';
@@ -57,7 +56,7 @@ export class FuseForgotPassword2Component implements OnInit
this.forgotPasswordFormErrors[field] = {};
// Get the control
const control = this.forgotPasswordFormErrors.get(field);
const control = this.forgotPasswordForm.get(field);
if ( control && control.dirty && !control.valid )
{

View File

@@ -1,6 +1,7 @@
<div id="forgot-password" fxLayout="column" fusePerfectScrollbar>
<div id="forgot-password-form-wrapper" fxLayout="column" fxLayoutAlign="center center" *fuseIfOnDom [@animate]="{value:'*',params:{duration:'300ms',y:'100px'}}">
<div id="forgot-password-form-wrapper" fxLayout="column" fxLayoutAlign="center center" *fuseIfOnDom
[@animate]="{value:'*',params:{duration:'300ms',y:'100px'}}">
<div id="forgot-password-form">

View File

@@ -1,11 +1,11 @@
@import "../../../../../core/scss/fuse";
@import "src/app/core/scss/fuse";
:host {
#forgot-password {
width: 100%;
overflow: auto;
background: url('/assets/images/backgrounds/march.jpg') no-repeat;
background: url('/assets/images/backgrounds/dark-material-bg.jpg') no-repeat;
background-size: cover;
#forgot-password-form-wrapper {
@@ -22,7 +22,7 @@
padding: 32px;
text-align: center;
background: #FFFFFF;
@include mat-elevation(7);
@include mat-elevation(16);
@include media-breakpoint('xs') {
padding: 24px;

View File

@@ -56,7 +56,7 @@ export class FuseForgotPasswordComponent implements OnInit
this.forgotPasswordFormErrors[field] = {};
// Get the control
const control = this.forgotPasswordFormErrors.get(field);
const control = this.forgotPasswordForm.get(field);
if ( control && control.dirty && !control.valid )
{

View File

@@ -5,7 +5,7 @@
#lock {
width: 100%;
overflow: auto;
background: url('/assets/images/backgrounds/march.jpg') no-repeat;
background: url('/assets/images/backgrounds/dark-material-bg.jpg') no-repeat;
background-size: cover;
#lock-form-wrapper {
@@ -23,7 +23,7 @@
max-width: 420px;
padding: 48px 32px 32px 32px;
background: #FFFFFF;
@include mat-elevation(7);
@include mat-elevation(16);
@include media-breakpoint('xs') {
padding: 24px;

View File

@@ -1,5 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { FuseConfigService } from '../../../../../core/services/config.service';
import { fuseAnimations } from '../../../../../core/animations';
@@ -55,7 +54,7 @@ export class FuseLockComponent implements OnInit
{
for ( const field in this.lockFormErrors )
{
if ( this.lockFormErrors.hasOwnProperty(field) )
if ( !this.lockFormErrors.hasOwnProperty(field) )
{
continue;
}

View File

@@ -17,7 +17,8 @@
</div>
<div id="login-form-wrapper" fusePerfectScrollbar *fuseIfOnDom [@animate]="{value:'*',params:{delay:'300ms',x:'100%'}}">
<div id="login-form-wrapper" fusePerfectScrollbar *fuseIfOnDom
[@animate]="{value:'*',params:{delay:'300ms',x:'100%'}}">
<div id="login-form">
@@ -41,7 +42,7 @@
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Password" formControlName="password">
<input matInput type="password" placeholder="Password" formControlName="password">
<mat-error *ngIf="loginFormErrors.password.required">
Password is required
</mat-error>

View File

@@ -5,7 +5,7 @@
#login {
width: 100%;
overflow: hidden;
background: url('/assets/images/backgrounds/march.jpg') no-repeat;
background: url('/assets/images/backgrounds/dark-material-bg.jpg') no-repeat;
background-size: cover;
#login-intro {
@@ -39,7 +39,7 @@
min-width: 400px;
max-width: 400px;
background: #FFFFFF;
@include mat-elevation(7);
@include mat-elevation(16);
@include media-breakpoint('sm') {
width: 360px;

View File

@@ -1,5 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { FuseConfigService } from '../../../../../core/services/config.service';
import { fuseAnimations } from '../../../../../core/animations';

View File

@@ -23,7 +23,7 @@
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Password" formControlName="password">
<input matInput type="password" placeholder="Password" formControlName="password">
<mat-error *ngIf="loginFormErrors.password.required">
Password is required
</mat-error>

Some files were not shown because too many files have changed in this diff Show More