; 59 | 60 | constructor(private fb: FormBuilder, 61 | private store$: Store) { 62 | this.quote$ = this.store$.pipe(select(fromRoot.getQuoteState)); 63 | } 64 | 65 | ngOnInit() { 66 | this.form = this.fb.group({ 67 | email: ['wpcfan@163.com', Validators.compose([Validators.required, Validators.email])], 68 | password: ['wp123456', Validators.required] 69 | }); 70 | this.store$.dispatch({type: actions.QUOTE}); 71 | } 72 | 73 | onSubmit({value, valid}: FormGroup, e: Event) { 74 | e.preventDefault(); 75 | if (!valid) { 76 | return; 77 | } 78 | this.store$.dispatch( 79 | new authActions.LoginAction(value)); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/app/login/index.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {SharedModule} from '../shared'; 3 | import {LoginRoutingModule} from './login-routing.module'; 4 | import {LoginComponent} from './containers/login'; 5 | import {RegisterComponent} from './containers/register'; 6 | import {ForgotComponent} from './containers/forgot'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | SharedModule, 11 | LoginRoutingModule 12 | ], 13 | declarations: [LoginComponent, RegisterComponent, ForgotComponent] 14 | }) 15 | export class LoginModule { 16 | } 17 | -------------------------------------------------------------------------------- /src/app/login/login-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {LoginComponent} from './containers/login'; 4 | import {RegisterComponent} from './containers/register'; 5 | import {ForgotComponent} from './containers/forgot'; 6 | 7 | const routes: Routes = [ 8 | { 9 | path: 'login', 10 | component: LoginComponent, 11 | }, 12 | { 13 | path: 'register', 14 | component: RegisterComponent, 15 | }, 16 | { 17 | path: 'forgot', 18 | component: ForgotComponent, 19 | } 20 | ]; 21 | 22 | @NgModule({ 23 | imports: [RouterModule.forChild(routes)], 24 | exports: [RouterModule] 25 | }) 26 | export class LoginRoutingModule { 27 | } 28 | -------------------------------------------------------------------------------- /src/app/my-calendar/index.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { SharedModule } from '../shared'; 3 | import { CalendarModule, DateAdapter } from 'angular-calendar'; 4 | import { adapterFactory } from 'angular-calendar/date-adapters/date-fns'; 5 | import { CalendarRoutingModule } from './my-calendar-routing.module'; 6 | import { CalendarHomeComponent } from './calendar-home'; 7 | 8 | @NgModule({ 9 | declarations: [CalendarHomeComponent], 10 | imports: [ 11 | SharedModule, 12 | CalendarRoutingModule, 13 | CalendarModule.forRoot({ 14 | provide: DateAdapter, 15 | useFactory: adapterFactory 16 | }) 17 | ] 18 | }) 19 | export class MyCalendarModule {} 20 | -------------------------------------------------------------------------------- /src/app/my-calendar/my-calendar-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {CalendarHomeComponent} from './calendar-home'; 4 | 5 | const routes: Routes = [ 6 | {path: '', component: CalendarHomeComponent} 7 | ]; 8 | 9 | @NgModule({ 10 | imports: [RouterModule.forChild(routes)], 11 | exports: [RouterModule] 12 | }) 13 | export class CalendarRoutingModule { 14 | } 15 | -------------------------------------------------------------------------------- /src/app/project/components/invite.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Inject } from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 3 | import { NgForm } from '@angular/forms'; 4 | import { User } from '../../domain'; 5 | 6 | @Component({ 7 | selector: 'app-invite', 8 | template: ` 9 | {{ dialogTitle }}
10 | 25 | `, 26 | styles: [``] 27 | }) 28 | export class InviteComponent implements OnInit { 29 | members: User[] = []; 30 | dialogTitle: string; 31 | 32 | constructor( 33 | @Inject(MAT_DIALOG_DATA) private data: any, 34 | private dialogRef: MatDialogRef35 | ) {} 36 | 37 | ngOnInit() { 38 | this.members = [...this.data.members]; 39 | this.dialogTitle = this.data.dialogTitle 40 | ? this.data.dialogTitle 41 | : '邀请成员'; 42 | } 43 | 44 | onSubmit(ev: Event, { value, valid }: NgForm) { 45 | ev.preventDefault(); 46 | if (!valid) { 47 | return; 48 | } 49 | this.dialogRef.close(this.members); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/project/components/project-item.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | EventEmitter, 5 | HostBinding, 6 | HostListener, 7 | Input, 8 | Output 9 | } from '@angular/core'; 10 | import {cardAnim} from '../../anim'; 11 | import {Project} from '../../domain'; 12 | 13 | @Component({ 14 | selector: 'app-project-item', 15 | template: ` 16 | 17 | 43 | `, 44 | styles: [``], 45 | changeDetection: ChangeDetectionStrategy.OnPush, 46 | animations: [cardAnim], 47 | }) 48 | export class ProjectItemComponent { 49 | @Input() item: Project; 50 | @Output() itemSelected = new EventEmitter18 | 24 |19 | 20 | {{ item.name }} 21 | 22 | 23 |25 |
26 | 28 |{{ item.desc }}
27 |29 | 33 | 37 | 41 | 42 |(); 51 | @Output() launchUpdateDialog = new EventEmitter (); 52 | @Output() launchInviteDailog = new EventEmitter (); 53 | @Output() launchDeleteDailog = new EventEmitter (); 54 | @HostBinding('@card') cardState = 'out'; 55 | 56 | @HostListener('mouseenter') 57 | onMouseEnter() { 58 | this.cardState = 'hover'; 59 | } 60 | 61 | @HostListener('mouseleave') 62 | onMouseLeave() { 63 | this.cardState = 'out'; 64 | } 65 | 66 | onClick(ev: Event) { 67 | ev.preventDefault(); 68 | this.itemSelected.emit(); 69 | } 70 | 71 | openUpdateDialog(ev: Event) { 72 | ev.preventDefault(); 73 | ev.stopPropagation(); 74 | this.launchUpdateDialog.emit(); 75 | } 76 | 77 | openInviteDialog(ev: Event) { 78 | ev.preventDefault(); 79 | ev.stopPropagation(); 80 | this.launchInviteDailog.emit(); 81 | } 82 | 83 | openDeleteDialog(ev: Event) { 84 | ev.preventDefault(); 85 | ev.stopPropagation(); 86 | this.launchDeleteDailog.emit(); 87 | } 88 | 89 | constructor() {} 90 | } 91 | -------------------------------------------------------------------------------- /src/app/project/index.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {SharedModule} from '../shared'; 3 | import {ProjectRoutingModule} from './project-routing.module'; 4 | import {ProjectListComponent} from './containers/project-list'; 5 | import {NewProjectComponent} from './components/new-project'; 6 | import {InviteComponent} from './components/invite'; 7 | import {ProjectItemComponent} from './components/project-item'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | SharedModule, 12 | ProjectRoutingModule 13 | ], 14 | exports: [ProjectListComponent], 15 | entryComponents: [NewProjectComponent, InviteComponent], 16 | declarations: [ 17 | ProjectListComponent, 18 | NewProjectComponent, 19 | InviteComponent, 20 | ProjectItemComponent 21 | ] 22 | }) 23 | export class ProjectModule { 24 | } 25 | -------------------------------------------------------------------------------- /src/app/project/project-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {ProjectListComponent} from './containers/project-list'; 4 | 5 | const routes: Routes = [ 6 | {path: '', component: ProjectListComponent} 7 | ]; 8 | 9 | @NgModule({ 10 | imports: [RouterModule.forChild(routes)], 11 | exports: [RouterModule] 12 | }) 13 | export class ProjectRoutingModule { 14 | } 15 | -------------------------------------------------------------------------------- /src/app/reducers/auth.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import * as fromAuth from './auth.reducer'; 2 | import {reducer} from './auth.reducer'; 3 | import * as actions from '../actions/auth.action'; 4 | import {async} from '@angular/core/testing'; 5 | 6 | describe('测试 AuthReducer', () => { 7 | describe('未定义Action', () => { 8 | it('应该返回一个默认状态', async(() => { 9 | const action = {} as any; 10 | const result = reducer(undefined, action); 11 | expect(result).toEqual(fromAuth.initialState); 12 | })); 13 | }); 14 | 15 | describe('登录成功', () => { 16 | it('应该返回一个 Err 为 undefined 而 userId 不为空的 Auth 对象', async(() => { 17 | const action = new actions.LoginSuccessAction({ 18 | token: '', 19 | user: { 20 | id: '1', 21 | email: '123@123.com', 22 | password: '123456' 23 | } 24 | }); 25 | const result = reducer(undefined, action); 26 | expect(result).toEqual({token: '', userId: '1'}); 27 | expect(result.err).toBeUndefined(); 28 | })); 29 | }); 30 | 31 | describe('登录失败', () => { 32 | it('应该返回一个 Err 不为 undefined 而 User 为 undefined Auth 对象', async(() => { 33 | const action = new actions.LoginFailAction({ 34 | status: 501, 35 | message: 'Server Error' 36 | }); 37 | const result = reducer(undefined, action); 38 | expect(result.err).toBeDefined(); 39 | expect(result.user).toBeUndefined(); 40 | })); 41 | }); 42 | 43 | describe('注册成功', () => { 44 | it('应该返回一个 Err 为 undefined 而 User 不为空的 Auth 对象', async(() => { 45 | const action = new actions.RegisterSuccessAction({ 46 | token: '', 47 | user: { 48 | id: '123abc', 49 | name: 'wang', 50 | email: 'wang@163.com' 51 | } 52 | }); 53 | const result = reducer(undefined, action); 54 | expect(result).toEqual({token: '', userId: '123abc'}); 55 | expect(result.err).toBeUndefined(); 56 | })); 57 | }); 58 | 59 | describe('注册失败', () => { 60 | it('应该返回一个 Err 不为 undefined 而 User 为 undefined Auth 对象', async(() => { 61 | const action = new actions.RegisterFailAction({ 62 | status: 501, 63 | message: 'Server Error' 64 | }); 65 | const result = reducer(undefined, action); 66 | expect(result.err).toBeDefined(); 67 | expect(result.user).toBeUndefined(); 68 | })); 69 | }); 70 | }); 71 | 72 | -------------------------------------------------------------------------------- /src/app/reducers/auth.reducer.ts: -------------------------------------------------------------------------------- 1 | import {Auth} from '../domain'; 2 | import * as actions from '../actions/auth.action'; 3 | 4 | export const initialState: Auth = {}; 5 | 6 | export function reducer(state: Auth = initialState, action: actions.Actions): Auth { 7 | switch (action.type) { 8 | case actions.LOGIN_SUCCESS: 9 | case actions.REGISTER_SUCCESS: { 10 | const auth = action.payload; 11 | return { 12 | token: auth.token, 13 | userId: auth.user ? auth.user.id : undefined 14 | }; 15 | } 16 | case actions.LOGIN_FAIL: 17 | case actions.REGISTER_FAIL: { 18 | return {err: action.payload}; 19 | } 20 | default: { 21 | return state; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/reducers/project.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Project } from '../domain'; 2 | import { createSelector } from '@ngrx/store'; 3 | import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; 4 | import * as actions from '../actions/project.action'; 5 | 6 | export interface State extends EntityState { 7 | selectedId: string | null; 8 | } 9 | 10 | export function sortByName(a: Project, b: Project): number { 11 | return a.name.localeCompare(b.name); 12 | } 13 | 14 | export const adapter: EntityAdapter = createEntityAdapter ({ 15 | selectId: (project: Project) => project.id, 16 | sortComparer: sortByName, 17 | }); 18 | 19 | export const initialState: State = adapter.getInitialState({ 20 | // additional entity state properties 21 | selectedId: null 22 | }); 23 | 24 | export function reducer(state = initialState, action: actions.Actions): State { 25 | switch (action.type) { 26 | case actions.ADD_SUCCESS: 27 | return { ...adapter.addOne(action.payload, state), selectedId: null }; 28 | case actions.DELETE_SUCCESS: 29 | return { ...adapter.removeOne( action.payload.id, state), selectedId: null }; 30 | case actions.INVITE_SUCCESS: 31 | case actions.UPDATE_LISTS_SUCCESS: 32 | case actions.UPDATE_SUCCESS: 33 | case actions.INSERT_FILTER_SUCCESS: 34 | return { ...adapter.updateOne({ id: action.payload.id, changes: action.payload }, state), selectedId: null }; 35 | case actions.LOADS_SUCCESS: 36 | return { ...adapter.addMany(action.payload, state), selectedId: null }; 37 | case actions.SELECT: 38 | return { ...state, selectedId: action.payload.id }; 39 | default: 40 | return state; 41 | } 42 | } 43 | 44 | export const getSelectedId = (state: State) => state.selectedId; 45 | -------------------------------------------------------------------------------- /src/app/reducers/quote.reducer.ts: -------------------------------------------------------------------------------- 1 | import {Quote} from '../domain'; 2 | import * as actions from '../actions/quote.action'; 3 | 4 | export const initialState: Quote = { 5 | cn: '满足感在于不断的努力,而不是现有成就。全心努力定会胜利满满。', 6 | en: 'Satisfaction lies in the effort, not in the attainment. Full effort is full victory. ', 7 | pic: 'assets/img/quote_fallback.jpg', 8 | }; 9 | 10 | export function reducer(state: Quote = initialState, action: actions.Actions): Quote { 11 | switch (action.type) { 12 | case actions.QUOTE_SUCCESS: 13 | return {...action.payload}; 14 | case actions.QUOTE_FAIL: 15 | default: 16 | return state; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/reducers/role.reducer.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/app/reducers/role.reducer.ts -------------------------------------------------------------------------------- /src/app/reducers/task-filter-vm.reducer.ts: -------------------------------------------------------------------------------- 1 | import { TaskFilterVM } from '../vm'; 2 | import { 3 | getDefaultTaskFilterVM, 4 | getTaskFilterVM, 5 | getOwnerVMs 6 | } from '../utils/task-filter.util'; 7 | import * as actions from '../actions/task-filter-vm.action'; 8 | import * as taskFilterActions from '../actions/task-filter.action'; 9 | 10 | export const initialState: TaskFilterVM = getDefaultTaskFilterVM(); 11 | 12 | export function reducer(state = initialState, action: actions.Actions | taskFilterActions.Actions): TaskFilterVM { 13 | switch (action.type) { 14 | case taskFilterActions.LOAD_SUCCESS: 15 | return getTaskFilterVM(action.payload); 16 | case actions.LOAD_OWNERS_SUCCESS: 17 | return { ...state, ownerVMs: getOwnerVMs(action.payload) }; 18 | case actions.UPDATE: 19 | return { ...action.payload }; 20 | default: 21 | return state; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/reducers/task-filter.reducer.ts: -------------------------------------------------------------------------------- 1 | import { TaskFilter } from '../domain'; 2 | import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; 3 | import { getDefaultTaskFilter } from '../utils/task-filter.util'; 4 | import * as actions from '../actions/task-filter.action'; 5 | import * as projectActions from '../actions/project.action'; 6 | 7 | export const initialState: TaskFilter = getDefaultTaskFilter(); 8 | 9 | export function reducer(state = initialState, action: actions.Actions | projectActions.Actions): TaskFilter { 10 | switch (action.type) { 11 | case actions.LOAD_SUCCESS: 12 | return { ...action.payload }; 13 | case actions.UPDATE_SUCCESS: 14 | return { ...action.payload }; 15 | default: 16 | return state; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/reducers/task-history.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Task, TaskHistory } from '../domain'; 2 | import { TaskVM } from '../vm'; 3 | import { createSelector } from '@ngrx/store'; 4 | import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; 5 | import * as actions from '../actions/task-history.action'; 6 | import * as taskActions from '../actions/task.action'; 7 | 8 | export interface State extends EntityState { 9 | selectedTask: TaskVM | null; 10 | updatedTask: TaskVM | null; 11 | } 12 | 13 | export const adapter: EntityAdapter = createEntityAdapter ({ 14 | }); 15 | 16 | export const initialState: State = adapter.getInitialState({ 17 | selectedTask: null, 18 | updatedTask: null, 19 | }); 20 | 21 | export function reducer(state = initialState, action: actions.Actions | taskActions.Actions): State { 22 | switch (action.type) { 23 | case taskActions.SELECT: 24 | return { ids: [], entities: {}, selectedTask: action.payload, updatedTask: null }; 25 | case taskActions.UPDATING: 26 | return { ...state, updatedTask: action.payload }; 27 | case actions.LOAD_SUCCESS: 28 | return { ...adapter.addAll(action.payload, state) }; 29 | case actions.ADD_SUCCESS: 30 | return { ...adapter.addOne(action.payload, state) }; 31 | default: 32 | return state; 33 | } 34 | } 35 | 36 | export const getSelectedTask = (state: State): TaskVM | null => state.selectedTask; 37 | export const getUpdatedTask = (state: State): TaskVM | null => state.updatedTask; 38 | -------------------------------------------------------------------------------- /src/app/reducers/task-list.reducer.ts: -------------------------------------------------------------------------------- 1 | import {TaskList, Project} from '../domain'; 2 | import {createSelector} from '@ngrx/store'; 3 | import {EntityState, EntityAdapter, createEntityAdapter} from '@ngrx/entity'; 4 | import * as _ from 'lodash'; 5 | import * as actions from '../actions/task-list.action'; 6 | import * as prjActions from '../actions/project.action'; 7 | 8 | export interface State extends EntityState { 9 | } 10 | 11 | export function sortByOrder(a: TaskList, b: TaskList): number { 12 | return a.order > b.order ? 1 : a.order === b.order ? 0 : -1; 13 | } 14 | 15 | export const adapter: EntityAdapter = createEntityAdapter ({ 16 | selectId: (taskList: TaskList) => taskList.id, 17 | sortComparer: sortByOrder, 18 | }); 19 | 20 | export const initialState: State = adapter.getInitialState(); 21 | 22 | const delListByPrj = (state: State, action: prjActions.DeleteProjectSuccessAction) => { 23 | const project = action.payload; 24 | const taskListIds = project.taskLists; 25 | return adapter.removeMany(taskListIds, state); 26 | }; 27 | 28 | const swapOrder = (state: State, action: actions.SwapOrderSuccessAction) => { 29 | const taskLists = action.payload; 30 | if (taskLists === null) { 31 | return state; 32 | } 33 | return adapter.updateMany(taskLists.map((tl: TaskList) => ({id: tl.id, changes: tl})), state); 34 | }; 35 | 36 | export function reducer(state: State = initialState, action: actions.Actions | prjActions.Actions): State { 37 | switch (action.type) { 38 | case actions.ADD_SUCCESS: 39 | return {...adapter.addOne(action.payload, state)}; 40 | case actions.DELETE_SUCCESS: 41 | return {...adapter.removeOne( action.payload.id, state)}; 42 | case actions.UPDATE_SUCCESS: 43 | return {...adapter.updateOne({id: action.payload.id, changes: action.payload}, state)}; 44 | case actions.SWAP_ORDER_SUCCESS: 45 | return {...swapOrder(state, action)}; 46 | case actions.LOADS_SUCCESS: 47 | return {...adapter.addMany(action.payload, state)}; 48 | case prjActions.DELETE_SUCCESS: 49 | return {...delListByPrj(state, action)}; 50 | default: 51 | return state; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/reducers/theme.reducer.ts: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions/theme.action'; 2 | 3 | export interface State { 4 | darkmode: boolean; 5 | } 6 | 7 | export const initialState: State = { 8 | darkmode: false 9 | }; 10 | 11 | export function reducer(state = initialState, action: actions.Actions): State { 12 | switch (action.type) { 13 | case actions.SWITCH_THEME: { 14 | return {...state, darkmode: action.payload}; 15 | } 16 | default: { 17 | return state; 18 | } 19 | } 20 | } 21 | 22 | export const getTheme = (state: State) => state.darkmode; 23 | -------------------------------------------------------------------------------- /src/app/reducers/user.reducer.ts: -------------------------------------------------------------------------------- 1 | import {createSelector} from '@ngrx/store'; 2 | import {EntityState, EntityAdapter, createEntityAdapter} from '@ngrx/entity'; 3 | import * as actions from '../actions/user.action'; 4 | import * as authActions from '../actions/auth.action'; 5 | import {User, Auth} from '../domain'; 6 | 7 | export interface State extends EntityState {} 8 | 9 | export function sortByOrder(a: User, b: User): number { 10 | return a.email.localeCompare(b.email); 11 | } 12 | 13 | export const adapter: EntityAdapter = createEntityAdapter ({ 14 | selectId: (user: User) => user.id, 15 | sortComparer: sortByOrder, 16 | }); 17 | 18 | export const initialState: State = adapter.getInitialState(); 19 | 20 | const register = (state: State, action: authActions.LoginSuccessAction | authActions.RegisterSuccessAction): State => { 21 | const auth = action.payload; 22 | return ( state.ids).indexOf( auth.userId) === -1 ? 23 | {...adapter.addOne( auth.user, state)} : state; 24 | }; 25 | 26 | export function reducer(state: State = initialState, action: actions.Actions | authActions.Actions): State { 27 | switch (action.type) { 28 | case authActions.LOGIN_SUCCESS: 29 | case authActions.REGISTER_SUCCESS: 30 | return register(state, action); 31 | case actions.ADD_USER_PROJECT_SUCCESS: 32 | return {...adapter.addOne( action.payload, state)}; 33 | case actions.REMOVE_USER_PROJECT_SUCCESS: 34 | return {...adapter.removeOne( action.payload.id, state)}; 35 | case actions.SEARCH_USERS_SUCCESS: 36 | case actions.LOAD_USERS_BY_PRJ_SUCCESS: 37 | case actions.BATCH_UPDATE_USER_PROJECT_SUCCESS: 38 | return {...adapter.addMany( action.payload, state)}; 39 | default: { 40 | return state; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app/services/auth-guard.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { async, inject, TestBed } from '@angular/core/testing'; 3 | import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | import { AuthGuardService } from './auth-guard.service'; 6 | import { Store, StoreModule, select } from '@ngrx/store'; 7 | import { getAuth, reducers, metaReducers, State } from '../reducers'; 8 | import * as authActions from '../actions/auth.action'; 9 | import { withLatestFrom } from 'rxjs/operators'; 10 | 11 | const mockSnapshot: any = jasmine 12 | .createSpyObj ('RouterStateSnapshot', ['toString']); 13 | 14 | @Component({ 15 | template: ` ` 16 | }) 17 | class RoutingComponent { 18 | } 19 | 20 | @Component({ 21 | template: `` 22 | }) 23 | class DummyComponent { 24 | } 25 | 26 | 27 | describe('测试路由守卫服务:AuthGuardService', () => { 28 | beforeEach(async(() => { 29 | TestBed.configureTestingModule({ 30 | imports: [ 31 | RouterTestingModule.withRoutes([ 32 | { path: 'route1', component: DummyComponent }, 33 | { path: 'route2', component: DummyComponent }, 34 | ]), 35 | StoreModule.forRoot(reducers, { metaReducers: metaReducers }), 36 | ], 37 | declarations: [DummyComponent, RoutingComponent], 38 | providers: [ 39 | AuthGuardService, 40 | { provide: RouterStateSnapshot, useValue: mockSnapshot } 41 | ] 42 | }).compileComponents(); 43 | })); 44 | 45 | it('不应该允许绕过守卫', 46 | async(inject([AuthGuardService, Store], 47 | (service: AuthGuardService, store$: Store ) => { 48 | const fixture = TestBed.createComponent(RoutingComponent); 49 | const guard$ = service.canActivate(new ActivatedRouteSnapshot(), mockSnapshot); 50 | const auth$ = store$.pipe(select(getAuth)); 51 | const merge$ = guard$.pipe( 52 | withLatestFrom( 53 | auth$, (g, a) => ({ result: g, auth: a.err === undefined && a.user !== undefined }))); 54 | merge$.subscribe(r => { 55 | expect(r.result).toBe(r.auth); 56 | }); 57 | store$.dispatch({ 58 | type: authActions.LOGIN_SUCCESS, 59 | payload: { 60 | token: 'xxxx', 61 | user: { id: 'xxxx', email: 'abc@dev.local', name: 'xxxx', password: 'sssss' } 62 | } 63 | }); 64 | }))); 65 | 66 | }); 67 | -------------------------------------------------------------------------------- /src/app/services/auth-guard.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | import { Store, select } from '@ngrx/store'; 5 | import { map, defaultIfEmpty } from 'rxjs/operators'; 6 | import * as routerActions from '../actions/router.action'; 7 | import * as fromRoot from '../reducers'; 8 | 9 | @Injectable() 10 | export class AuthGuardService implements CanActivate { 11 | /** 12 | * 构造函数用于注入服务的依赖以及进行必要的初始化 13 | * 14 | * @param router 路由注入,用于导航处理 15 | * @param store$ redux store注入,用于状态管理 16 | */ 17 | constructor(private store$: Store ) { 18 | } 19 | 20 | /** 21 | * 用于判断是否可以激活该路由 22 | * 23 | * @param route 24 | */ 25 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { 26 | return this.checkAuth(); 27 | } 28 | 29 | checkAuth(): Observable { 30 | return this.store$ 31 | .pipe( 32 | select(s => s.auth), 33 | map(auth => { 34 | const result = auth.token !== undefined && auth.token !== null; 35 | if (!result) { 36 | this.store$.dispatch(new routerActions.Go({ path: ['/login'] })); 37 | } 38 | return result; 39 | }), 40 | defaultIfEmpty(false) 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app/services/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, inject, TestBed } from '@angular/core/testing'; 2 | import { HttpClient, HttpClientModule } from '@angular/common/http'; 3 | import { 4 | HttpClientTestingModule, 5 | HttpTestingController 6 | } from '@angular/common/http/testing'; 7 | import { AuthService } from './auth.service'; 8 | import { User } from '../domain'; 9 | 10 | describe('测试鉴权服务:AuthService', () => { 11 | beforeEach(async(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [HttpClientTestingModule], 14 | providers: [AuthService] 15 | }); 16 | })); 17 | 18 | afterEach(inject( 19 | [HttpTestingController], 20 | (backend: HttpTestingController) => { 21 | backend.verify(); 22 | } 23 | )); 24 | 25 | it('注册后应该返回一个 Observable ', async( 26 | inject( 27 | [AuthService, HttpTestingController], 28 | (service: AuthService, mockBackend: HttpTestingController) => { 29 | const mockUser: User = { 30 | id: undefined, 31 | name: 'someuser@dev.local', 32 | password: '123abc', 33 | email: 'someuser@dev.local' 34 | }; 35 | const mockResponse = { 36 | id: 'obj123abc', 37 | name: 'someuser@dev.local', 38 | email: 'someuser@dev.local', 39 | password: '123abc' 40 | }; 41 | service.register(mockUser).subscribe(auth => { 42 | expect(auth.token).toBeDefined(); 43 | expect(auth.userId).toEqual(mockUser.id); 44 | }); 45 | 46 | mockBackend 47 | .expectOne('users') 48 | .flush(JSON.stringify(mockResponse), { 49 | status: 200, 50 | statusText: 'Ok' 51 | }); 52 | } 53 | ) 54 | )); 55 | }); 56 | -------------------------------------------------------------------------------- /src/app/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import { HttpHeaders, HttpClient, HttpParams } from '@angular/common/http'; 3 | import { Observable, throwError } from 'rxjs'; 4 | import { Auth, User } from '../domain'; 5 | import { map, switchMap } from 'rxjs/operators'; 6 | 7 | /** 8 | * 认证服务主要用于用户的注册和登录功能 9 | */ 10 | @Injectable() 11 | export class AuthService { 12 | private headers = new HttpHeaders({ 13 | 'Content-Type': 'application/json' 14 | }); 15 | 16 | private token = 17 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' + 18 | '.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9' + 19 | '.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ'; 20 | 21 | /** 22 | * 构造函数用于注入服务的依赖以及进行必要的初始化 23 | * 24 | * @param http 注入Http 25 | * @param config 注入基础配置 26 | */ 27 | constructor( 28 | private http: HttpClient, 29 | @Inject('BASE_CONFIG') private config: { uri: string } 30 | ) {} 31 | 32 | /** 33 | * 使用用户提供的个人信息进行注册,成功则返回 User,否则抛出异常 34 | * 35 | * @param user 用户信息,id 属性会被忽略,因为服务器端会创建新的 id 36 | */ 37 | register(user: User): Observable { 38 | const params = new HttpParams().set('email', user.email); 39 | const uri = `${this.config.uri}/users`; 40 | return this.http.get(uri, { params }).pipe( 41 | switchMap(res => { 42 | if (( res).length > 0) { 43 | return throwError('username existed'); 44 | } 45 | return this.http 46 | .post(uri, JSON.stringify(user), { headers: this.headers }) 47 | .pipe(map(r => ({ token: this.token, user: r }))); 48 | }) 49 | ); 50 | } 51 | 52 | /** 53 | * 使用用户名和密码登录 54 | * 55 | * @param email 用户名 56 | * @param password 密码(明文),服务器会进行加密处理 57 | */ 58 | login(email: string, password: string): Observable { 59 | const uri = `${this.config.uri}/users`; 60 | const params = new HttpParams() 61 | .set('email', email) 62 | .set('password', password); 63 | return this.http.get(uri, { params }).pipe( 64 | map(res => { 65 | const users = res; 66 | if (users.length === 0) { 67 | throw new Error('Username or password incorrect'); 68 | } 69 | return { 70 | token: this.token, 71 | user: users[0] 72 | }; 73 | }) 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/app/services/index.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { AuthService } from './auth.service'; 3 | import { ProjectService } from './project.service'; 4 | import { QuoteService } from './quote.service'; 5 | import { TaskListService } from './task-list.service'; 6 | import { TaskService } from './task.service'; 7 | import { AuthGuardService } from './auth-guard.service'; 8 | import { UserService } from './user.service'; 9 | import { MyCalService } from './my-cal.service'; 10 | import { TaskHistoryService } from './task-history.service'; 11 | import { TaskFilterService } from './task-filter.service'; 12 | 13 | export { 14 | AuthGuardService, 15 | AuthService, 16 | ProjectService, 17 | QuoteService, 18 | TaskListService, 19 | TaskService, 20 | TaskFilterService, 21 | TaskHistoryService, 22 | UserService, 23 | MyCalService, 24 | }; 25 | 26 | @NgModule() 27 | export class ServicesModule { 28 | static forRoot() { 29 | return { 30 | ngModule: ServicesModule, 31 | providers: [ 32 | AuthGuardService, 33 | AuthService, 34 | ProjectService, 35 | QuoteService, 36 | TaskListService, 37 | TaskService, 38 | TaskFilterService, 39 | TaskHistoryService, 40 | UserService, 41 | MyCalService, 42 | ] 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/services/my-cal.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import { HttpClient, HttpParams } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { Task } from '../domain'; 5 | import { CalendarEvent } from 'angular-calendar'; 6 | import { endOfDay, startOfDay } from 'date-fns'; 7 | import { map } from 'rxjs/operators'; 8 | 9 | const colors: any = { 10 | red: { 11 | primary: '#ad2121', 12 | secondary: '#FAE3E3' 13 | }, 14 | blue: { 15 | primary: '#1e90ff', 16 | secondary: '#D1E8FF' 17 | }, 18 | yellow: { 19 | primary: '#e3bc08', 20 | secondary: '#FDF1BA' 21 | } 22 | }; 23 | 24 | const getPriorityColor = (priority: number) => { 25 | switch (priority) { 26 | case 1: 27 | return colors.red; 28 | case 2: 29 | return colors.yellow; 30 | case 3: 31 | default: 32 | return colors.blue; 33 | } 34 | }; 35 | 36 | @Injectable() 37 | export class MyCalService { 38 | constructor(@Inject('BASE_CONFIG') private config: { uri: string }, private http: HttpClient) { 39 | } 40 | 41 | getUserTasks(userId: string): Observable { 42 | const uri = `${this.config.uri}/tasks`; 43 | const params = new HttpParams() 44 | .set('ownerId', userId); 45 | return this.http.get(uri, { params }) 46 | .pipe( 47 | map((tasks: Task[]) => tasks.map( 48 | (task: Task) => ({ 49 | start: startOfDay( task.createDate), 50 | end: task.dueDate ? endOfDay( task.dueDate) : endOfDay( task.createDate), 51 | title: task.desc, 52 | color: getPriorityColor(task.priority) 53 | }) 54 | )) 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/services/quote.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { Quote } from '../domain'; 5 | 6 | @Injectable() 7 | export class QuoteService { 8 | // private uri: string = 'https://api.hzy.pw/saying/v1/ciba'; 9 | constructor(@Inject('BASE_CONFIG') private config: { uri: string }, 10 | private http: HttpClient) { 11 | } 12 | 13 | getQuote(): Observable { 14 | const uri = `${this.config.uri}/quotes/${Math.floor(Math.random() * 10)}`; 15 | return this.http.get(uri); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/services/task-filter.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { TaskFilter } from '../domain'; 5 | 6 | @Injectable() 7 | export class TaskFilterService { 8 | private readonly domain: string = 'taskFilter'; 9 | private headers = new HttpHeaders().set('Content-Type', 'application/json'); 10 | 11 | constructor(@Inject('BASE_CONFIG') private config: { uri: string }, private http: HttpClient) { 12 | } 13 | 14 | addTaskFilter(filter: TaskFilter): Observable{ 15 | const uri = `${this.config.uri}/${this.domain}`; 16 | return this.http.post (uri, JSON.stringify(filter), { headers: this.headers }); 17 | } 18 | 19 | getTaskFilter(id: string): Observable { 20 | const uri = `${this.config.uri}/${this.domain}/${id}`; 21 | return this.http.get (uri); 22 | } 23 | 24 | updateTaskFilter(filter: TaskFilter): Observable { 25 | const uri = `${this.config.uri}/${this.domain}/${filter.id}`; 26 | return this.http 27 | .patch (uri, JSON.stringify(filter), { headers: this.headers }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/services/task-history.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { TaskHistory } from '../domain'; 5 | 6 | @Injectable() 7 | export class TaskHistoryService { 8 | private readonly domain = 'taskHistory'; 9 | private headers = new HttpHeaders().set('Content-Type', 'application/json'); 10 | 11 | constructor(@Inject('BASE_CONFIG') private config: { uri: string }, private http: HttpClient) { 12 | } 13 | 14 | addTaskHistory(history: TaskHistory): Observable { 15 | const uri = `${this.config.uri}/${this.domain}`; 16 | return this.http.post (uri, JSON.stringify(history), { headers: this.headers }); 17 | } 18 | 19 | getTaskHistory(taskId: string): Observable { 20 | const uri = `${this.config.uri}/${this.domain}`; 21 | const params = new HttpParams() 22 | .set('taskId', taskId); 23 | 24 | return this.http.get (uri, { params }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/services/task-list.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import { HttpHeaders, HttpParams, HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { merge } from 'rxjs'; 5 | import { reduce, map, mapTo } from 'rxjs/operators'; 6 | import { concat } from 'rxjs'; 7 | import { Project, TaskList, Task } from '../domain'; 8 | 9 | @Injectable() 10 | export class TaskListService { 11 | private readonly domain = 'taskLists'; 12 | private headers = new HttpHeaders().set('Content-Type', 'application/json'); 13 | 14 | constructor( 15 | @Inject('BASE_CONFIG') private config: { uri: string }, 16 | private http: HttpClient 17 | ) {} 18 | 19 | add(taskList: TaskList): Observable { 20 | const uri = `${this.config.uri}/${this.domain}`; 21 | return this.http.post (uri, JSON.stringify(taskList), { 22 | headers: this.headers 23 | }); 24 | } 25 | 26 | update(taskList: TaskList): Observable { 27 | const uri = `${this.config.uri}/${this.domain}/${taskList.id}`; 28 | const toUpdate = { 29 | name: taskList.name 30 | }; 31 | return this.http.patch (uri, JSON.stringify(toUpdate), { 32 | headers: this.headers 33 | }); 34 | } 35 | 36 | del(taskList: TaskList): Observable { 37 | const uri = `${this.config.uri}/${this.domain}/${taskList.id}`; 38 | return this.http.delete(uri).pipe(mapTo(taskList)); 39 | } 40 | 41 | // GET /tasklist 42 | get(projectId: string): Observable { 43 | const uri = `${this.config.uri}/${this.domain}`; 44 | const params = new HttpParams().set('projectId', projectId); 45 | return this.http.get (uri, { params }); 46 | } 47 | 48 | swapOrder(src: TaskList, target: TaskList): Observable { 49 | const dragUri = `${this.config.uri}/${this.domain}/${src.id}`; 50 | const dropUri = `${this.config.uri}/${this.domain}/${target.id}`; 51 | const drag$ = this.http.patch ( 52 | dragUri, 53 | JSON.stringify({ order: target.order }), 54 | { headers: this.headers } 55 | ); 56 | const drop$ = this.http.patch ( 57 | dropUri, 58 | JSON.stringify({ order: src.order }), 59 | { headers: this.headers } 60 | ); 61 | return concat(drag$, drop$).pipe( 62 | reduce((r: TaskList[], x: TaskList) => [...r, x], []) 63 | ); 64 | } 65 | 66 | initializeTaskLists(prj: Project): Observable { 67 | const id = prj.id; 68 | return merge( 69 | this.add({ id: undefined, name: '待办', projectId: id, order: 1 }), 70 | this.add({ id: undefined, name: '进行中', projectId: id, order: 2 }), 71 | this.add({ id: undefined, name: '已完成', projectId: id, order: 3 }) 72 | ).pipe( 73 | reduce((r: TaskList[], x: TaskList) => [...r, x], []), 74 | map((tls: TaskList[]) => ({ 75 | ...prj, 76 | taskLists: tls.map(tl => tl.id) 77 | })) 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/app/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import { HttpHeaders, HttpClient, HttpParams } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { User, Project } from '../domain'; 5 | import { from } from 'rxjs'; 6 | import { switchMap, filter, reduce } from 'rxjs/operators'; 7 | 8 | @Injectable() 9 | export class UserService { 10 | private readonly domain = 'users'; 11 | private headers = new HttpHeaders().set('Content-Type', 'application/json'); 12 | 13 | constructor( 14 | @Inject('BASE_CONFIG') private config: { uri: string }, 15 | private http: HttpClient 16 | ) {} 17 | 18 | searchUsers(filterStr: string): Observable { 19 | const uri = `${this.config.uri}/${this.domain}`; 20 | const params = new HttpParams().set('email_like', filterStr); 21 | return this.http.get (uri, { params }); 22 | } 23 | 24 | getUsersByProject(projectId: string): Observable { 25 | const uri = `${this.config.uri}/users`; 26 | const params = new HttpParams().set('projectId', projectId); 27 | return this.http.get (uri, { params }); 28 | } 29 | 30 | addProjectRef(user: User, projectId: string): Observable { 31 | const uri = `${this.config.uri}/${this.domain}/${user.id}`; 32 | const projectIds = user.projectIds ? user.projectIds : []; 33 | return this.http.patch ( 34 | uri, 35 | JSON.stringify({ projectIds: [...projectIds, projectId] }), 36 | { headers: this.headers } 37 | ); 38 | } 39 | 40 | removeProjectRef(user: User, projectId: string): Observable { 41 | const uri = `${this.config.uri}/${this.domain}/${user.id}`; 42 | const projectIds = user.projectIds ? user.projectIds : []; 43 | const index = projectIds.indexOf(projectId); 44 | const toUpdate = [ 45 | ...projectIds.slice(0, index), 46 | ...projectIds.slice(index + 1) 47 | ]; 48 | return this.http.patch ( 49 | uri, 50 | JSON.stringify({ projectIds: toUpdate }), 51 | { headers: this.headers } 52 | ); 53 | } 54 | 55 | batchUpdateProjectRef(project: Project): Observable { 56 | const projectId = project.id; 57 | const memberIds = project.members ? project.members : []; 58 | return from(memberIds).pipe( 59 | switchMap(id => { 60 | const uri = `${this.config.uri}/${this.domain}/${id}`; 61 | return this.http.get(uri); 62 | }), 63 | filter( 64 | (user: User) => 65 | user.projectIds ? user.projectIds.indexOf(projectId) < 0 : false 66 | ), 67 | switchMap((u: User) => this.addProjectRef(u, projectId)), 68 | reduce((users: User[], curr: User) => [...users, curr], []) 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/app/shared/adapters/date-formats.ts: -------------------------------------------------------------------------------- 1 | import { MatDateFormats } from '@angular/material/core'; 2 | 3 | export const MD_FNS_DATE_FORMATS: MatDateFormats = { 4 | parse: { 5 | dateInput: null 6 | }, 7 | display: { 8 | dateInput: { year: 'numeric', month: 'numeric', day: 'numeric' }, 9 | monthYearLabel: { year: 'numeric', month: 'short' }, 10 | dateA11yLabel: { year: 'numeric', month: 'long', day: 'numeric' }, 11 | monthYearA11yLabel: { year: 'numeric', month: 'long' } 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/app/shared/adapters/datepicker-i18n.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | 4 | /** Datepicker data that requires internationalization. */ 5 | @Injectable() 6 | export class DatepickerI18n { 7 | /** 8 | * Stream that emits whenever the labels here are changed. Use this to notify 9 | * components if the labels have changed after initialization. 10 | */ 11 | changes: Subject = new Subject (); 12 | 13 | /** A label for the calendar popup (used by screen readers). */ 14 | calendarLabel = '日历'; 15 | 16 | /** A label for the button used to open the calendar popup (used by screen readers). */ 17 | openCalendarLabel = '打开日历'; 18 | 19 | /** A label for the previous month button (used by screen readers). */ 20 | prevMonthLabel = '上月'; 21 | 22 | /** A label for the next month button (used by screen readers). */ 23 | nextMonthLabel = '下月'; 24 | 25 | /** A label for the previous year button (used by screen readers). */ 26 | prevYearLabel = '前一年'; 27 | 28 | /** A label for the next year button (used by screen readers). */ 29 | nextYearLabel = '下一年'; 30 | 31 | /** A label for the 'switch to month view' button (used by screen readers). */ 32 | switchToMonthViewLabel = '切换月视图'; 33 | 34 | /** A label for the 'switch to year view' button (used by screen readers). */ 35 | switchToYearViewLabel = '切换年视图'; 36 | } 37 | -------------------------------------------------------------------------------- /src/app/shared/components/confirm-dialog.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 3 | 4 | export interface ConfirmDialog { 5 | title: string; 6 | content: string; 7 | confirmAction: string; 8 | } 9 | 10 | @Component({ 11 | selector: 'app-confirm-dialog', 12 | template: ` 13 | {{ dialog.title }}
14 |{{ dialog.content }}15 |16 | 19 | 27 |28 | `, 29 | styles: [``], 30 | changeDetection: ChangeDetectionStrategy.OnPush 31 | }) 32 | export class ConfirmDialogComponent { 33 | dialog: ConfirmDialog; 34 | 35 | constructor( 36 | @Inject(MAT_DIALOG_DATA) private data: any, 37 | private dialogRef: MatDialogRef38 | ) { 39 | if (this.data.dialog !== undefined || this.data.dialog !== null) { 40 | this.dialog = this.data.dialog; 41 | } 42 | } 43 | 44 | handleAction(result: boolean) { 45 | this.dialogRef.close(result); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/shared/components/image-list-select/image-list-select.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{ title }} 3 |8 |4 | 5 | 7 |6 |
9 |25 | -------------------------------------------------------------------------------- /src/app/shared/components/image-list-select/image-list-select.component.scss: -------------------------------------------------------------------------------- 1 | mat-icon.avatar { 2 | overflow: hidden; 3 | width: 64px; 4 | height: 64px; 5 | border-radius: 50%; 6 | margin: 12px; 7 | } 8 | 9 | .scroll-container { 10 | overflow-y: scroll; 11 | height: 200px; 12 | } 13 | 14 | .image-container { 15 | position: relative; 16 | display: inline-block; 17 | } 18 | 19 | .image-container img { 20 | display: block; 21 | } 22 | 23 | .image-container .after { 24 | position: absolute; 25 | top: 0; 26 | left: 0; 27 | width: 100%; 28 | height: 100%; 29 | display: none; 30 | color: #FFF; 31 | } 32 | 33 | .image-container:hover .after { 34 | display: block; 35 | background: rgba(0, 0, 0, .6); 36 | } 37 | 38 | .image-container .after .zoom { 39 | color: #DDD; 40 | font-size: 48px; 41 | position: absolute; 42 | top: 50%; 43 | left: 50%; 44 | margin: -30px 0 0 -19px; 45 | height: 50px; 46 | width: 45px; 47 | cursor: pointer; 48 | } 49 | 50 | .image-container .after .zoom:hover { 51 | color: #FFF; 52 | } 53 | 54 | .cover { 55 | width: 150px; 56 | } 57 | -------------------------------------------------------------------------------- /src/app/shared/components/image-list-select/image-list-select.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { MatGridListModule } from '@angular/material/grid-list'; 3 | import { MatIconModule } from '@angular/material/icon'; 4 | import { ImageListSelectComponent } from './'; 5 | 6 | describe('ImageListSelectComponent', () => { 7 | let component: ImageListSelectComponent; 8 | let fixture: ComponentFixture10 | 24 |11 | 23 |12 |22 |13 | 14 | 16 |15 |
17 | 18 |21 |checked 19 | 20 |; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ImageListSelectComponent], 13 | imports: [MatGridListModule, MatIconModule] 14 | }).compileComponents(); 15 | })); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(ImageListSelectComponent); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/shared/components/image-list-select/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | EventEmitter, 5 | forwardRef, 6 | Input, 7 | Output 8 | } from '@angular/core'; 9 | import { 10 | ControlValueAccessor, 11 | FormControl, 12 | NG_VALIDATORS, 13 | NG_VALUE_ACCESSOR 14 | } from '@angular/forms'; 15 | 16 | @Component({ 17 | selector: 'app-image-list-select', 18 | templateUrl: './image-list-select.component.html', 19 | styleUrls: ['./image-list-select.component.scss'], 20 | providers: [ 21 | { 22 | provide: NG_VALUE_ACCESSOR, 23 | useExisting: forwardRef(() => ImageListSelectComponent), 24 | multi: true 25 | }, 26 | { 27 | provide: NG_VALIDATORS, 28 | useExisting: forwardRef(() => ImageListSelectComponent), 29 | multi: true 30 | } 31 | ], 32 | changeDetection: ChangeDetectionStrategy.OnPush 33 | }) 34 | export class ImageListSelectComponent implements ControlValueAccessor { 35 | selected: string; 36 | @Input() title = '选择封面:'; 37 | @Input() items: string[] = []; 38 | @Input() cols = 8; 39 | @Input() rowHeight = '64px'; 40 | @Input() itemWidth = '80px'; 41 | @Input() useSvgIcon = false; 42 | @Output() itemChange = new EventEmitter (); 43 | 44 | // 这里是做一个空函数体,真正使用的方法在 registerOnChange 中 45 | // 由框架注册,然后我们使用它把变化发回表单 46 | // 注意,和 EventEmitter 尽管很像,但发送回的对象不同 47 | private propagateChange = (_: any) => {}; 48 | 49 | // 写入控件值 50 | public writeValue(obj: any) { 51 | if (obj && obj !== '') { 52 | this.selected = obj; 53 | } 54 | } 55 | 56 | // 当表单控件值改变时,函数 fn 会被调用 57 | // 这也是我们把变化 emit 回表单的机制 58 | public registerOnChange(fn: any) { 59 | this.propagateChange = fn; 60 | } 61 | 62 | // 验证表单,验证结果正确返回 null 否则返回一个验证结果对象 63 | public validate(c: FormControl) { 64 | return this.selected 65 | ? null 66 | : { 67 | imageListSelect: { 68 | valid: false 69 | } 70 | }; 71 | } 72 | 73 | // 这里没有使用,用于注册 touched 状态 74 | public registerOnTouched() {} 75 | 76 | // 列表元素选择发生改变触发 77 | onChange(i: number) { 78 | this.selected = this.items[i]; 79 | // 更新表单 80 | this.propagateChange(this.items[i]); 81 | this.itemChange.emit(this.items[i]); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/app/task/components/copy-task.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, OnInit } from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 3 | import { Observable } from 'rxjs'; 4 | import { TaskList } from '../../domain'; 5 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 6 | 7 | @Component({ 8 | selector: 'app-copy-task', 9 | template: ` 10 | 37 | `, 38 | styles: [``] 39 | }) 40 | export class CopyTaskComponent implements OnInit { 41 | form: FormGroup; 42 | dialogTitle: string; 43 | lists$: Observable ; 44 | 45 | constructor( 46 | private fb: FormBuilder, 47 | @Inject(MAT_DIALOG_DATA) private data: any, 48 | private dialogRef: MatDialogRef 49 | ) {} 50 | 51 | ngOnInit() { 52 | this.lists$ = this.data.lists; 53 | this.dialogTitle = '移动所有任务'; 54 | this.form = this.fb.group({ 55 | targetList: ['', Validators.required] 56 | }); 57 | } 58 | 59 | onSubmit({ value, valid }: FormGroup, ev: Event) { 60 | ev.preventDefault(); 61 | if (!valid) { 62 | return; 63 | } 64 | this.dialogRef.close({ 65 | srcListId: this.data.srcListId, 66 | targetListId: value.targetList 67 | }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/app/task/components/new-task-list.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | Inject, 5 | OnInit 6 | } from '@angular/core'; 7 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 8 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 9 | 10 | @Component({ 11 | selector: 'app-new-task-list', 12 | template: ` 13 | 32 | `, 33 | styles: [ 34 | ` 35 | :host { 36 | margin: 0; 37 | padding: 0; 38 | display: flex; 39 | flex-direction: column; 40 | flex-wrap: nowrap; 41 | } 42 | ` 43 | ], 44 | changeDetection: ChangeDetectionStrategy.OnPush 45 | }) 46 | export class NewTaskListComponent implements OnInit { 47 | form: FormGroup; 48 | dialogTitle: string; 49 | 50 | constructor( 51 | private fb: FormBuilder, 52 | @Inject(MAT_DIALOG_DATA) private data: any, 53 | private dialogRef: MatDialogRef 54 | ) {} 55 | 56 | ngOnInit() { 57 | if (!this.data.name) { 58 | this.form = this.fb.group({ 59 | name: [ 60 | '', 61 | Validators.compose([Validators.required, Validators.maxLength(10)]) 62 | ] 63 | }); 64 | this.dialogTitle = '创建列表:'; 65 | } else { 66 | this.form = this.fb.group({ 67 | name: [ 68 | this.data.name, 69 | Validators.compose([Validators.required, Validators.maxLength(10)]) 70 | ] 71 | }); 72 | this.dialogTitle = '修改列表:'; 73 | } 74 | } 75 | 76 | onSubmit(ev: Event) { 77 | ev.preventDefault(); 78 | if (!this.form.valid) { 79 | return; 80 | } 81 | this.dialogRef.close(this.form.value.name); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/app/task/components/quick-task.ts: -------------------------------------------------------------------------------- 1 | import { Component, Output, EventEmitter, HostListener, ChangeDetectionStrategy } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-quick-task', 5 | template: ` 6 | 7 | 8 | 11 | 12 | `, 13 | styles: [``], 14 | changeDetection: ChangeDetectionStrategy.OnPush 15 | }) 16 | export class QuickTaskComponent { 17 | 18 | desc: string; 19 | @Output() quickTask = new EventEmitter(); 20 | 21 | constructor() { } 22 | 23 | @HostListener('keyup.enter') 24 | sendQuickTask() { 25 | if (!this.desc || this.desc.length === 0 || !this.desc.trim() || this.desc.length > 20) { 26 | return; 27 | } 28 | this.quickTask.emit(this.desc); 29 | this.desc = ''; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/task/components/task-history-item/index.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core'; 2 | import { TaskHistoryVM } from '../../../vm'; 3 | 4 | @Component({ 5 | selector: 'app-task-history-item', 6 | templateUrl: './task-history-item.component.html', 7 | styleUrls: ['./task-history-item.component.scss'], 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export class TaskHistoryItemComponent implements OnInit { 11 | 12 | @Input() item: TaskHistoryVM; 13 | 14 | constructor() { } 15 | 16 | ngOnInit() { 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/app/task/components/task-history-item/task-history-item.component.html: -------------------------------------------------------------------------------- 1 | 2 |11 | -------------------------------------------------------------------------------- /src/app/task/components/task-history-item/task-history-item.component.scss: -------------------------------------------------------------------------------- 1 | .task-history-item { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | font-size: 12px; 6 | color: #A6A6A6; 7 | margin-bottom: 10px; 8 | } 9 | 10 | .task-history-item-title { 11 | display: flex; 12 | flex-direction: row; 13 | align-items: center; 14 | // width: 100%; //task-history-item default align-items is stretch 15 | } 16 | 17 | .task-history-item-title-icon { 18 | font-size: 18px; 19 | line-height: 24px; 20 | } 21 | 22 | .task-history-item-title-desc { 23 | margin-left: 10px; 24 | } 25 | 26 | .task-history-item-title-date { 27 | flex: 1 1 auto; 28 | text-align: end; 29 | } 30 | 31 | .task-history-item-desc { 32 | margin-left: 34px; 33 | border-left: 5px solid #A6A6A6; 34 | padding-left: 10px; 35 | color: #383838; 36 | } 37 | -------------------------------------------------------------------------------- /src/app/task/components/task-item/index.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component, EventEmitter, HostListener, Input, OnInit, Output} from '@angular/core'; 2 | import {Task} from '../../../domain'; 3 | import {itemAnim} from '../../../anim/item.anim'; 4 | import {TaskVM} from '../../../vm/task.vm'; 5 | 6 | @Component({ 7 | selector: 'app-task-item', 8 | templateUrl: './task-item.component.html', 9 | styleUrls: ['./task-item.component.scss'], 10 | changeDetection: ChangeDetectionStrategy.OnPush, 11 | animations: [itemAnim] 12 | }) 13 | export class TaskItemComponent implements OnInit { 14 | 15 | @Output() taskComplete = new EventEmitter(); 16 | @Output() taskClick = new EventEmitter(); 17 | @Input() item: TaskVM; 18 | avatar: string; 19 | widerPriority = 'in'; 20 | 21 | constructor() { 22 | } 23 | 24 | ngOnInit() { 25 | this.avatar = (this.item.owner) ?3 |7 |{{ item.icon }} 4 | {{ item.title }} 5 | {{ item.dateDesc }} 6 |8 | {{ item.content }} 9 |10 |this.item.owner.avatar : 'unassigned'; 26 | } 27 | 28 | onCheckboxClick(ev: Event) { 29 | ev.stopPropagation(); 30 | } 31 | 32 | checkboxChanged() { 33 | this.taskComplete.emit(); 34 | } 35 | 36 | itemClicked(ev: Event) { 37 | ev.preventDefault(); 38 | this.taskClick.emit(); 39 | } 40 | 41 | @HostListener('mouseenter') 42 | handleMouseEnter() { 43 | this.widerPriority = 'out'; 44 | } 45 | 46 | @HostListener('mouseleave') 47 | handleMouseLeave() { 48 | this.widerPriority = 'in'; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/app/task/components/task-item/task-item.component.html: -------------------------------------------------------------------------------- 1 | 13 | 29 | -------------------------------------------------------------------------------- /src/app/task/components/task-item/task-item.component.scss: -------------------------------------------------------------------------------- 1 | mat-icon.avatar { 2 | overflow: hidden; 3 | width: 64px; 4 | height: 64px; 5 | border-radius: 50%; 6 | margin: 12px; 7 | order: 3; 8 | } 9 | 10 | .completed { 11 | opacity: 0.64; 12 | color: #d9d9d9; 13 | text-decoration: line-through; 14 | } 15 | 16 | .priority-normal { 17 | border-left: 3px solid #a6a6a6; 18 | } 19 | 20 | .priority-important { 21 | border-left: 3px solid #ffaf38; 22 | } 23 | 24 | .priority-emergency { 25 | border-left: 3px solid red; 26 | } 27 | 28 | .checkbox-section { 29 | border: 0 solid #a6a6a6; 30 | } 31 | 32 | .duedate { 33 | background-color: #ff4f3e; 34 | color: #fff; 35 | } 36 | 37 | .alarm { 38 | font-size: 18px; 39 | } 40 | 41 | .bottom-bar { 42 | margin-top: 3px; 43 | margin-bottom: 2px; 44 | font-size: 10px; 45 | width: 100%; 46 | order: 1; 47 | display: flex; 48 | flex: 0 0 18px; 49 | } 50 | 51 | .status { 52 | order: -1; 53 | } 54 | 55 | .content { 56 | order: 1; 57 | width: 100%; 58 | padding: 5px; 59 | } 60 | 61 | .container { 62 | width: 100%; 63 | border-radius: 3px; 64 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); 65 | } 66 | 67 | .drag-start { 68 | opacity: 0.5; 69 | border: #ff525b dashed 2px; 70 | } 71 | 72 | :host { 73 | width: 100%; 74 | } 75 | -------------------------------------------------------------------------------- /src/app/task/components/task-list-header.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component, EventEmitter, Input, Output} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-task-list-header', 5 | template: ` 6 |18 | 19 |20 | {{ item.desc }} 21 |22 | 26 |27 | 28 | 7 |22 |8 |10 |{{ header }}
9 |11 | 15 |16 |17 | 20 |21 |23 | 27 | 38 | `, 39 | styles: [` 40 | .material-icon{ 41 | line-height: 1; 42 | } 43 | 44 | .fill{ 45 | text-align: center; 46 | } 47 | 48 | .header-container{ 49 | width: 100%; 50 | } 51 | `], 52 | changeDetection: ChangeDetectionStrategy.OnPush 53 | }) 54 | export class TaskListHeaderComponent { 55 | @Output() changeListName = new EventEmitter28 | 32 | 33 | 37 | (); 56 | @Output() deleteList = new EventEmitter (); 57 | @Output() moveAllTasks = new EventEmitter (); 58 | @Output() newTask = new EventEmitter (); 59 | @Input() header = ''; 60 | 61 | constructor() { 62 | } 63 | 64 | onChangeListName(ev: Event) { 65 | ev.preventDefault(); 66 | this.changeListName.emit(); 67 | } 68 | 69 | onMoveAllTasks(ev: Event) { 70 | ev.preventDefault(); 71 | this.moveAllTasks.emit(); 72 | } 73 | 74 | onDeleteList(ev: Event) { 75 | ev.preventDefault(); 76 | this.deleteList.emit(); 77 | } 78 | 79 | addNewTask(ev: Event) { 80 | ev.preventDefault(); 81 | this.newTask.emit(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/app/task/components/task-list.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-task-list', 5 | template: ` 6 | 7 | 9 | `, 10 | styles: [``], 11 | changeDetection: ChangeDetectionStrategy.OnPush 12 | }) 13 | export class TaskListComponent {} 14 | -------------------------------------------------------------------------------- /src/app/task/containers/task-home/task-home.component.html: -------------------------------------------------------------------------------- 1 | 7 |8 | 8 |27 | 28 | 31 | -------------------------------------------------------------------------------- /src/app/task/containers/task-home/task-home.component.scss: -------------------------------------------------------------------------------- 1 | .drag-start { 2 | opacity: 0.5; 3 | border: #ff525b dashed 2px; 4 | } 5 | 6 | .drag-enter { 7 | background-color: dimgray; 8 | } 9 | 10 | .task-lists-navigation { 11 | display: flex; 12 | flex-direction: row; 13 | justify-content: flex-end; 14 | align-items: center; 15 | height: 50px; 16 | color: #808080; 17 | background: #F5F5F5; 18 | border-bottom: 1px solid #D9D9D9; 19 | } 20 | 21 | .task-lists-navigation-menu-container { 22 | display: flex; 23 | flex-direction: row; 24 | padding-left: 10px; 25 | padding-right: 10px; 26 | border-left: 1px solid #808080; 27 | cursor: pointer; 28 | } 29 | 30 | .task-lists-navigation-menu-container:hover { 31 | color: #3da8f5; 32 | } 33 | 34 | .task-lists-navigation-opened { 35 | color: #3da8f5; 36 | } 37 | 38 | .task-lists-navigation-menu-icon { 39 | font-size: 18px; 40 | line-height: 22px; 41 | } 42 | 43 | .task-lists-navigation-menu-text { 44 | font-size: 14px; 45 | } 46 | 47 | .task-lists-content-container { 48 | flex: 1; 49 | position: relative; 50 | } 51 | 52 | mat-sidenav-container { 53 | position: absolute; 54 | } 55 | 56 | .list-container { 57 | min-height: 100%; 58 | overflow-y: auto; 59 | overflow-x: hidden; 60 | } 61 | 62 | .task-lists { 63 | height: 100%; 64 | overflow-x: auto; 65 | } 66 | 67 | .fab-button { 68 | position: fixed; 69 | right: 32px; 70 | bottom: 96px; 71 | z-index: 998; 72 | } 73 | 74 | :host { 75 | display: flex; 76 | flex-direction: column; 77 | flex: 1 78 | } 79 | -------------------------------------------------------------------------------- /src/app/task/index.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { SharedModule } from '../shared'; 3 | import { TaskListComponent } from './components/task-list'; 4 | import { TaskItemComponent } from './components/task-item'; 5 | import { TaskRoutingModule } from './task-routing.module'; 6 | import { TaskHomeComponent } from './containers/task-home'; 7 | import { TaskListHeaderComponent } from './components/task-list-header'; 8 | import { NewTaskComponent } from './components/new-task'; 9 | import { NewTaskListComponent } from './components/new-task-list'; 10 | import { CopyTaskComponent } from './components/copy-task'; 11 | import { QuickTaskComponent } from './components/quick-task'; 12 | import { TaskHistoryItemComponent } from './components/task-history-item'; 13 | import { TaskFilterNavComponent } from './components/task-filter-nav'; 14 | 15 | @NgModule({ 16 | imports: [ 17 | SharedModule, 18 | TaskRoutingModule 19 | ], 20 | declarations: [ 21 | TaskListComponent, 22 | TaskItemComponent, 23 | TaskHomeComponent, 24 | TaskListHeaderComponent, 25 | NewTaskComponent, 26 | NewTaskListComponent, 27 | CopyTaskComponent, 28 | QuickTaskComponent, 29 | TaskHistoryItemComponent, 30 | TaskFilterNavComponent, 31 | ], 32 | entryComponents: [ 33 | NewTaskComponent, 34 | NewTaskListComponent, 35 | CopyTaskComponent 36 | ] 37 | }) 38 | export class TaskModule { 39 | } 40 | -------------------------------------------------------------------------------- /src/app/task/task-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {TaskHomeComponent} from './containers/task-home'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: TaskHomeComponent, 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class TaskRoutingModule { 17 | } 18 | -------------------------------------------------------------------------------- /src/app/utils/area.util.ts: -------------------------------------------------------------------------------- 1 | import { city_data } from './area.data'; 2 | 3 | export const getProvinces = () => { 4 | const provinces: string[] = []; 5 | for (const province in city_data) { 6 | if (province) { 7 | provinces.push(province); 8 | } 9 | } 10 | return [...provinces]; 11 | }; 12 | 13 | export const getCitiesByProvince = (province: string) => { 14 | if (!province || !city_data[province]) { 15 | return []; 16 | } 17 | const cities = city_data[province]; 18 | const citiesByProvice: string[] = []; 19 | for (const city in cities) { 20 | if (city) { 21 | citiesByProvice.push(city); 22 | } 23 | } 24 | return [...citiesByProvice]; 25 | }; 26 | 27 | export const getAreasByCity = (province: string, city: string) => { 28 | if (!province || !city || !city_data[province][city]) { 29 | return []; 30 | } 31 | const areas = city_data[province][city]; 32 | return [...areas]; 33 | }; 34 | -------------------------------------------------------------------------------- /src/app/utils/date.util.ts: -------------------------------------------------------------------------------- 1 | import { isValid, parseISO, format } from 'date-fns'; 2 | export const isValidDate = (dateStr: string) => { 3 | const date = parseISO(dateStr); 4 | return isValid(date); 5 | }; 6 | 7 | export const convertToDate = (date: Date) => { 8 | return format(date, 'yyyy-MM-dd'); 9 | }; 10 | -------------------------------------------------------------------------------- /src/app/utils/identity.util.ts: -------------------------------------------------------------------------------- 1 | import { GB2260 } from './identity.data'; 2 | 3 | export function extractInfo(idNo: string) { 4 | const addrPart = idNo.substring(0, 6); // 前六位地址码 5 | const birthPart = idNo.substring(6, 14); // 八位生日 6 | const genderPart = parseInt(idNo.substring(14, 17), 10); // 性别 7 | 8 | return { 9 | addrCode: addrPart, 10 | dateOfBirth: birthPart, 11 | gender: genderPart % 2 !== 0 12 | }; 13 | } 14 | 15 | export function isValidAddr(code: string): boolean { 16 | return GB2260[code] !== undefined; 17 | } 18 | 19 | export const getAddrByCode = (code: string) => { 20 | const provinceStr = GB2260[code.substring(0, 2) + '0000']; 21 | const cityStr = GB2260[code.substring(0, 4) + '00']; 22 | const districtStr = GB2260[code]; 23 | const city = cityStr.replace(provinceStr, ''); 24 | const district = districtStr.replace(cityStr, ''); 25 | return { 26 | province: provinceStr, 27 | city: city, 28 | district: district 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/app/utils/reduer.util.ts: -------------------------------------------------------------------------------- 1 | export function covertArrToObj9 | 26 |10 | 13 |11 | 12 |14 |25 |17 | 24 |19 | 20 |21 | 22 | 23 |(arr: T[]) { 2 | return arr.reduce((entities, obj: T) => ({...entities, [ obj.id]: obj}), {}); 3 | } 4 | 5 | export function buildObjFromArr (arr: string[], dict: {[id: string]: T}) { 6 | return arr.reduce((entities, id) => ({...entities, [id]: dict[id]}), {}); 7 | } 8 | 9 | export function loadCollection (state: {ids: string[]; entities: {[id: string]: T}}, collection: T[]) { 10 | const newItems = collection.filter(item => !state.entities[ item.id]); 11 | const newIds = newItems.map(item => item.id); 12 | const newEntities = covertArrToObj (newItems); 13 | return { 14 | ids: [...state.ids, ...newIds], 15 | entities: {...state.entities, ...newEntities} 16 | }; 17 | } 18 | 19 | export function updateOne (state: {ids: string[]; entities: {[id: string]: T}}, updated: T) { 20 | const entities = {...state.entities, [ updated.id]: updated}; 21 | return {...state, entities: entities}; 22 | } 23 | 24 | export function deleteOne (state: {ids: string[]; entities: {[id: string]: T}}, deleted: T) { 25 | const newIds = state.ids.filter(id => id !== deleted.id); 26 | const newEntities = buildObjFromArr(newIds, state.entities); 27 | return {ids: newIds, entities: newEntities}; 28 | } 29 | 30 | export function addOne (state: {ids: string[]; entities: {[id: string]: T}}, added: T) { 31 | const newIds = [...state.ids, added.id]; 32 | const newEntities = {...state.entities, [ added.id]: added}; 33 | return {ids: newIds, entities: newEntities}; 34 | } 35 | -------------------------------------------------------------------------------- /src/app/utils/router.util.ts: -------------------------------------------------------------------------------- 1 | import { RouterStateSerializer } from '@ngrx/router-store'; 2 | import { RouterStateSnapshot, Params } from '@angular/router'; 3 | 4 | /** 5 | * The RouterStateSerializer takes the current RouterStateSnapshot 6 | * and returns any pertinent information needed. The snapshot contains 7 | * all information about the state of the router at the given point in time. 8 | * The entire snapshot is complex and not always needed. In this case, you only 9 | * need the URL and query parameters from the snapshot in the store. Other items could be 10 | * returned such as route parameters and static route data. 11 | */ 12 | 13 | export interface RouterStateUrl { 14 | url: string; 15 | queryParams: Params; 16 | } 17 | 18 | export class CustomRouterStateSerializer 19 | implements RouterStateSerializer { 20 | serialize(routerState: RouterStateSnapshot): RouterStateUrl { 21 | const { url } = routerState; 22 | const queryParams = routerState.root.queryParams; 23 | 24 | return { url, queryParams }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/utils/svg.util.ts: -------------------------------------------------------------------------------- 1 | import { MatIconRegistry } from '@angular/material/icon'; 2 | import { DomSanitizer } from '@angular/platform-browser'; 3 | import * as _ from 'lodash'; 4 | 5 | /** 6 | * a utility to load all needed svg resources to the app for mat-icon to use 7 | * 8 | * @param ir a MatIconRegistry instance to use external svg resources for mat-icon use 9 | * @param ds a DomSanitizer instance to bypass security and return a url 10 | */ 11 | export const loadSvgResources = (ir: MatIconRegistry, ds: DomSanitizer) => { 12 | const imgDir = 'assets/img'; 13 | const avatarDir = `${imgDir}/avatar`; 14 | const sidebarDir = `${imgDir}/sidebar`; 15 | const iconDir = `${imgDir}/icons`; 16 | const dayDir = `${imgDir}/days`; 17 | ir.addSvgIconSetInNamespace( 18 | 'avatars', 19 | ds.bypassSecurityTrustResourceUrl(`${avatarDir}/avatars.svg`) 20 | ) 21 | .addSvgIcon( 22 | 'unassigned', 23 | ds.bypassSecurityTrustResourceUrl(`${avatarDir}/unassigned.svg`) 24 | ) 25 | .addSvgIcon( 26 | 'project', 27 | ds.bypassSecurityTrustResourceUrl(`${sidebarDir}/project.svg`) 28 | ) 29 | .addSvgIcon( 30 | 'projects', 31 | ds.bypassSecurityTrustResourceUrl(`${sidebarDir}/projects.svg`) 32 | ) 33 | .addSvgIcon( 34 | 'month', 35 | ds.bypassSecurityTrustResourceUrl(`${sidebarDir}/month.svg`) 36 | ) 37 | .addSvgIcon( 38 | 'week', 39 | ds.bypassSecurityTrustResourceUrl(`${sidebarDir}/week.svg`) 40 | ) 41 | .addSvgIcon( 42 | 'day', 43 | ds.bypassSecurityTrustResourceUrl(`${sidebarDir}/day.svg`) 44 | ) 45 | .addSvgIcon( 46 | 'move', 47 | ds.bypassSecurityTrustResourceUrl(`${iconDir}/move.svg`) 48 | ); 49 | const days = _.range(1, 31); 50 | days.forEach(day => 51 | ir.addSvgIcon( 52 | `day${day}`, 53 | ds.bypassSecurityTrustResourceUrl(`${dayDir}/day${day}.svg`) 54 | ) 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /src/app/utils/type.util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This function coerces a string into a string literal type. 3 | * Using tagged union types in TypeScript 2.0, this enables 4 | * powerful typechecking of our reducers. 5 | * 6 | * Since every action label passes through this function it 7 | * is a good place to ensure all of our action labels 8 | * are unique. 9 | */ 10 | const typeCache: { [label: string]: boolean } = {}; 11 | 12 | export function type (label: T | ''): T { 13 | if (typeCache[ label]) { 14 | throw new Error(`Action type "${label}" is not unique"`); 15 | } 16 | typeCache[ label] = true; 17 | return label; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/utils/vm.util.ts: -------------------------------------------------------------------------------- 1 | import {TaskVM, TaskListVM, ProjectVM} from '../vm'; 2 | import {Task, TaskList, Project, User} from '../domain'; 3 | 4 | export const covertToTask = (taskVM: TaskVM): Task => { 5 | return { 6 | id: taskVM.id, 7 | desc: taskVM.desc, 8 | completed: taskVM.completed, 9 | priority: taskVM.priority, 10 | taskListId: taskVM.taskListId, 11 | dueDate: taskVM.dueDate, 12 | createDate: taskVM.createDate, 13 | reminder: taskVM.reminder, 14 | remark: taskVM.remark, 15 | ownerId: taskVM.owner ? taskVM.owner.id : '', 16 | participantIds: taskVM.participants ? taskVM.participants.map(user => user.id) : [] 17 | }; 18 | }; 19 | 20 | export const converToTaskList = (taskListVM: TaskListVM): TaskList => { 21 | return { 22 | id: taskListVM.id, 23 | name: taskListVM.name, 24 | order: taskListVM.order, 25 | projectId: taskListVM.projectId, 26 | taskIds: taskListVM.tasks.map((task: TaskVM) => task.id) 27 | }; 28 | }; 29 | 30 | export const convertToProject = (projectVM: ProjectVM): Project => { 31 | return { 32 | id: projectVM.id, 33 | name: projectVM.name, 34 | coverImg: projectVM.coverImg, 35 | desc: projectVM.desc, 36 | enabled: projectVM.enabled, 37 | members: projectVM.members ? projectVM.members.map((user: User) => user.id) : [], 38 | taskLists: projectVM.taskLists ? projectVM.taskLists.map((tl: TaskListVM) => tl.id) : [] 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /src/app/vm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './project.vm'; 2 | export * from './task-list.vm'; 3 | export * from './task.vm'; 4 | export * from './task-history.vm'; 5 | export * from './task-filter.vm'; 6 | -------------------------------------------------------------------------------- /src/app/vm/project.vm.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../domain/user'; 2 | import { TaskListVM } from './task-list.vm'; 3 | 4 | export interface ProjectVM { 5 | id: string | null; 6 | name: string; 7 | desc?: string; 8 | coverImg?: string; 9 | enabled?: boolean; 10 | taskLists?: TaskListVM[]; 11 | members?: User[]; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/vm/task-filter.vm.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../domain'; 2 | 3 | export interface TaskFilterVM { 4 | id: string | undefined; 5 | projectId: string; 6 | desc?: string; 7 | sort: string; 8 | hasOwner: boolean; 9 | hasDueDate: boolean; 10 | hasCreateDate: boolean; 11 | hasPriority: boolean; 12 | customCreateDate: TaskFilterCustomDate; 13 | sortVMs: TaskFilterItemVM[]; 14 | ownerVMs: TaskFilterOwnerVM[]; 15 | dueDateVMs: TaskFilterItemVM[]; 16 | createDateVMs: TaskFilterItemVM[]; 17 | priorityVMs: TaskFilterPriorityVM[]; 18 | categoryVMs: TaskFilterItemVM[]; 19 | } 20 | 21 | export interface TaskFilterItemVM { 22 | label: string; 23 | value: string; 24 | checked: boolean; 25 | hasExtra?: boolean; 26 | } 27 | 28 | export interface TaskFilterOwnerVM { 29 | owner?: User; 30 | checked: boolean; 31 | } 32 | 33 | export interface TaskFilterPriorityVM { 34 | label: string; 35 | value: number; 36 | checked: boolean; 37 | } 38 | 39 | export interface TaskFilterCustomDate { 40 | startDate: Date; 41 | endDate: Date; 42 | } 43 | -------------------------------------------------------------------------------- /src/app/vm/task-history.vm.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../domain'; 2 | 3 | export interface TaskHistoryVM { 4 | id?: string; 5 | taskId: string; 6 | icon?: string; 7 | title: string; 8 | content?: string; 9 | date: Date; 10 | dateDesc: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/vm/task-list.vm.ts: -------------------------------------------------------------------------------- 1 | import { TaskVM } from './task.vm'; 2 | 3 | export interface TaskListVM { 4 | id?: string | null; 5 | name: string; 6 | projectId: string; 7 | order: number; 8 | tasks: TaskVM[]; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/vm/task.vm.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../domain/user'; 2 | export interface TaskVM { 3 | id?: string; 4 | taskListId: string; 5 | desc: string; 6 | completed: boolean; 7 | owner?: User; 8 | participants?: User[]; 9 | dueDate?: Date; 10 | priority: number; 11 | // order: number; 12 | remark?: string; 13 | // tags?: string[]; 14 | reminder?: Date; 15 | createDate?: Date; 16 | } 17 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/img/avatar/unassigned.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/covers/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/0.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/0_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/0_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/1.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/10.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/10_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/10_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/11.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/11_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/11_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/12.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/12_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/12_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/13.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/13_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/13_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/14.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/14_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/14_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/15.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/15_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/15_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/16.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/16_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/16_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/17.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/17_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/17_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/18.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/18_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/18_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/19.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/19.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/19_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/19_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/1_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/1_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/2.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/20.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/20_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/20_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/21.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/21.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/21_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/21_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/22.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/22.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/22_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/22_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/23.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/23.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/23_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/23_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/24.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/24.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/24_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/24_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/25.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/25.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/25_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/25_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/26.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/26.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/26_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/26_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/27.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/27.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/27_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/27_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/28.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/28.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/28_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/28_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/29.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/29.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/29_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/29_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/2_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/2_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/3.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/30.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/30.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/30_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/30_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/31.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/31.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/31_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/31_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/32.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/32.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/32_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/32_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/33.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/33.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/33_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/33_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/34.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/34.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/34_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/34_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/35.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/35.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/35_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/35_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/36.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/36.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/36_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/36_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/37.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/37.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/37_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/37_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/38.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/38.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/38_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/38_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/39.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/39.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/39_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/39_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/3_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/3_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/4.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/4_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/4_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/5.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/5_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/5_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/6.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/6_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/6_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/7.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/7_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/7_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/8.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/8_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/8_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/9.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/9_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/9_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/days/day1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/days/day11.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/days/day14.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/days/day17.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/days/day4.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/days/day7.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/developer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/developer.png -------------------------------------------------------------------------------- /src/assets/img/icons/add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/icons/burger-navigation.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/icons/delete.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/icons/move.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/not-found.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/not-found.png -------------------------------------------------------------------------------- /src/assets/img/quote_fallback.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/quote_fallback.jpg -------------------------------------------------------------------------------- /src/assets/img/quotes/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/quotes/0.jpg -------------------------------------------------------------------------------- /src/assets/img/quotes/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/quotes/1.jpg -------------------------------------------------------------------------------- /src/assets/img/quotes/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/quotes/2.jpg -------------------------------------------------------------------------------- /src/assets/img/quotes/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/quotes/3.jpg -------------------------------------------------------------------------------- /src/assets/img/quotes/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/quotes/4.jpg -------------------------------------------------------------------------------- /src/assets/img/quotes/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/quotes/5.jpg -------------------------------------------------------------------------------- /src/assets/img/quotes/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/quotes/6.jpg -------------------------------------------------------------------------------- /src/assets/img/quotes/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/quotes/7.jpg -------------------------------------------------------------------------------- /src/assets/img/quotes/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/quotes/8.jpg -------------------------------------------------------------------------------- /src/assets/img/quotes/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/quotes/9.jpg -------------------------------------------------------------------------------- /src/assets/img/sidebar/day.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/sidebar/month.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/sidebar/project.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/sidebar/projects.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/sidebar/week.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/environments/environment.hmr.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | hmr: true 4 | }; 5 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | hmr: false 4 | }; 5 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false, 8 | hmr: false 9 | }; 10 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/favicon.ico -------------------------------------------------------------------------------- /src/hmr.ts: -------------------------------------------------------------------------------- 1 | import { NgModuleRef, ApplicationRef } from '@angular/core'; 2 | import { createNewHosts } from '@angularclass/hmr'; 3 | 4 | export const hmrBootstrap = (module: any, bootstrap: () => Promise >) => { 5 | let ngModule: NgModuleRef ; 6 | module.hot.accept(); 7 | bootstrap().then(mod => ngModule = mod); 8 | module.hot.dispose(() => { 9 | const appRef: ApplicationRef = ngModule.injector.get(ApplicationRef); 10 | const elements = appRef.components.map(c => c.location.nativeElement); 11 | const makeVisible = createNewHosts(elements); 12 | ngModule.destroy(); 13 | makeVisible(); 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 企业协作平台 6 |7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | Loading... 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/main.server.ts: -------------------------------------------------------------------------------- 1 | import { environment } from './environments/environment'; 2 | import { enableProdMode } from '@angular/core'; 3 | 4 | if (environment.production) { 5 | enableProdMode(); 6 | } 7 | 8 | export {AppServerModule} from './app/app.server.module'; 9 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | import { hmrBootstrap } from './hmr'; 7 | /** 8 | * HammerJS needed to import here to make universal build works 9 | */ 10 | import 'hammerjs'; 11 | 12 | if (environment.production) { 13 | enableProdMode(); 14 | } 15 | 16 | document.addEventListener('DOMContentLoaded', () => { 17 | platformBrowserDynamic() 18 | .bootstrapModule(AppModule) 19 | .catch(err => console.log(err)); 20 | }); 21 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 24 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 25 | /** Evergreen browsers require these. **/ 26 | // import 'core-js/es6/reflect'; 27 | 28 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | /*************************************************************************************************** 31 | * Zone JS is required by Angular itself. 32 | */ 33 | import 'zone.js/dist/zone'; // Included with Angular CLI. 34 | 35 | /*************************************************************************************************** 36 | * APPLICATION IMPORTS 37 | */ 38 | 39 | /** 40 | * Date, currency, decimal and percent pipes. 41 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 42 | */ 43 | // import 'intl'; // Run `npm install --save intl`. 44 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @import 'theme.scss'; 2 | 3 | @mixin md-icon-size($size: 24px) { 4 | font-size: $size; 5 | height: $size; 6 | width: $size; 7 | line-height: $size; 8 | } 9 | 10 | html, 11 | body, 12 | app-root, 13 | mat-sidenav-container { 14 | margin: 0; 15 | width: 100%; 16 | height: 100%; 17 | } 18 | 19 | .site { 20 | width: 100%; 21 | min-height: 100%; 22 | } 23 | 24 | .full-width { 25 | width: 100%; 26 | } 27 | 28 | .fill-remaining-space { 29 | // 使用 flexbox 填充剩余空间 30 | // @angular/material 中的很多控件使用了 flex 布局 31 | flex: 1 1 auto; 32 | } 33 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare const __karma__: any; 17 | declare const require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "strict": false, 6 | "strictNullChecks": true, 7 | "skipLibCheck": true, 8 | "baseUrl": "", 9 | "types": [] 10 | }, 11 | "exclude": [ 12 | "test.ts", 13 | "**/*.spec.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "baseUrl": "./", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "**/*.spec.ts" 12 | ], 13 | "angularCompilerOptions": { 14 | "entryModule": "app/app.server.module#AppServerModule" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "", 6 | "types": ["jasmine", "node"] 7 | }, 8 | "files": ["test.ts", "polyfills.ts"], 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | 4 | interface NodeModule { 5 | id: string; 6 | [key:string]: any; 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "downlevelIteration": true, 6 | "importHelpers": true, 7 | "module": "esnext", 8 | "outDir": "./dist/out-tsc", 9 | "sourceMap": true, 10 | "declaration": false, 11 | "moduleResolution": "node", 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "target": "es2015", 15 | "typeRoots": ["node_modules/@types"], 16 | "lib": ["es2017", "dom"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["node_modules/codelyzer"], 3 | "rules": { 4 | "arrow-return-shorthand": true, 5 | "callable-types": true, 6 | "class-name": true, 7 | "comment-format": [true, "check-space"], 8 | "curly": true, 9 | "deprecation": { 10 | "severity": "warn" 11 | }, 12 | "eofline": true, 13 | "forin": true, 14 | "import-blacklist": [true, "rxjs/Rx"], 15 | "import-spacing": true, 16 | "indent": [true, "spaces"], 17 | "interface-over-type-literal": true, 18 | "label-position": true, 19 | "max-line-length": [true, 140], 20 | "member-access": false, 21 | "member-ordering": [ 22 | true, 23 | { 24 | "order": [ 25 | "static-field", 26 | "instance-field", 27 | "static-method", 28 | "instance-method" 29 | ] 30 | } 31 | ], 32 | "no-arg": true, 33 | "no-bitwise": true, 34 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], 35 | "no-construct": true, 36 | "no-debugger": true, 37 | "no-duplicate-super": true, 38 | "no-empty": false, 39 | "no-empty-interface": true, 40 | "no-eval": true, 41 | "no-inferrable-types": [true, "ignore-params"], 42 | "no-misused-new": true, 43 | "no-non-null-assertion": true, 44 | "no-shadowed-variable": true, 45 | "no-string-literal": false, 46 | "no-string-throw": true, 47 | "no-switch-case-fall-through": true, 48 | "no-trailing-whitespace": true, 49 | "no-unnecessary-initializer": true, 50 | "no-unused-expression": true, 51 | "no-use-before-declare": true, 52 | "no-var-keyword": true, 53 | "object-literal-sort-keys": false, 54 | "one-line": [ 55 | true, 56 | "check-open-brace", 57 | "check-catch", 58 | "check-else", 59 | "check-whitespace" 60 | ], 61 | "prefer-const": true, 62 | "quotemark": [true, "single"], 63 | "radix": true, 64 | "semicolon": [true, "always"], 65 | "triple-equals": [true, "allow-null-check"], 66 | "typedef-whitespace": [ 67 | true, 68 | { 69 | "call-signature": "nospace", 70 | "index-signature": "nospace", 71 | "parameter": "nospace", 72 | "property-declaration": "nospace", 73 | "variable-declaration": "nospace" 74 | } 75 | ], 76 | "unified-signatures": true, 77 | "variable-name": false, 78 | "whitespace": [ 79 | true, 80 | "check-branch", 81 | "check-decl", 82 | "check-operator", 83 | "check-separator", 84 | "check-type" 85 | ], 86 | "no-output-on-prefix": true, 87 | "no-inputs-metadata-property": true, 88 | "no-outputs-metadata-property": true, 89 | "no-host-metadata-property": true, 90 | "no-input-rename": true, 91 | "no-output-rename": true, 92 | "use-lifecycle-interface": true, 93 | "use-pipe-transform-interface": true, 94 | "component-class-suffix": true, 95 | "directive-class-suffix": true 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "modules", 3 | "out": "doc", 4 | "theme": "default", 5 | "ignoreCompilerErrors": "true", 6 | "experimentalDecorators": "true", 7 | "emitDecoratorMetadata": "true", 8 | "target": "ES5", 9 | "moduleResolution": "node", 10 | "preserveConstEnums": "true", 11 | "stripInternal": "true", 12 | "suppressExcessPropertyErrors": "true", 13 | "suppressImplicitAnyIndexErrors": "true", 14 | "module": "commonjs" 15 | } 16 | -------------------------------------------------------------------------------- /webpack.server.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = { 5 | mode: 'none', 6 | entry: { 7 | server: './server.ts' 8 | }, 9 | target: 'node', 10 | resolve: { extensions: ['.ts', '.js'] }, 11 | optimization: { 12 | minimize: false 13 | }, 14 | output: { 15 | // Puts the output at the root of the dist folder 16 | path: path.join(__dirname, 'dist'), 17 | filename: '[name].js' 18 | }, 19 | module: { 20 | rules: [ 21 | { test: /\.ts$/, loader: 'ts-loader' }, 22 | { 23 | // Mark files inside `@angular/core` as using SystemJS style dynamic imports. 24 | // Removing this will cause deprecation warnings to appear. 25 | test: /(\\|\/)@angular(\\|\/)core(\\|\/).+\.js$/, 26 | parser: { system: true } 27 | } 28 | ] 29 | }, 30 | plugins: [ 31 | new webpack.ContextReplacementPlugin( 32 | // fixes WARNING Critical dependency: the request of a dependency is an expression 33 | /(.+)?angular(\\|\/)core(.+)?/, 34 | path.join(__dirname, 'src'), // location of your src 35 | {} // a map of your routes 36 | ), 37 | new webpack.ContextReplacementPlugin( 38 | // fixes WARNING Critical dependency: the request of a dependency is an expression 39 | /(.+)?express(\\|\/)(.+)?/, 40 | path.join(__dirname, 'src'), 41 | {} 42 | ) 43 | ] 44 | }; 45 | --------------------------------------------------------------------------------