Send To Device Support (#1557)
* Tweaked the logging output * Started implementing some basic idea for devices * Updated Email Service with new API routes * Implemented basic DB structure and some APIs to prep for the UI and flows. * Added an abstract class to make Unit testing easier. * Removed dependency we don't need * Updated the UI to be able to show devices and add new devices. Email field will update the platform if the user hasn't interacted with it already. * Added ability to delete a device as well * Basic ability to send files to devices works * Refactored Action code to pass ActionItem back and allow for dynamic children based on an Observable (api). Hooked in ability to send a chapter to a device. There is no logic in the FE to validate type. * Fixed a broken unit test * Implemented the ability to edit a device * Code cleanup * Fixed a bad success message * Fixed broken unit test from updating mock layer
This commit is contained in:
parent
ab0f13ef74
commit
9d7476a367
79 changed files with 3026 additions and 157 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable, OnDestroy } from '@angular/core';
|
||||
import { Observable, of, ReplaySubject, Subject } from 'rxjs';
|
||||
import { of, ReplaySubject, Subject } from 'rxjs';
|
||||
import { filter, map, switchMap, takeUntil } from 'rxjs/operators';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { Preferences } from '../_models/preferences/preferences';
|
||||
|
|
@ -10,6 +10,7 @@ import { EVENTS, MessageHubService } from './message-hub.service';
|
|||
import { ThemeService } from './theme.service';
|
||||
import { InviteUserResponse } from '../_models/invite-user-response';
|
||||
import { UserUpdateEvent } from '../_models/events/user-update-event';
|
||||
import { DeviceService } from './device.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
|
@ -66,7 +67,7 @@ export class AccountService implements OnDestroy {
|
|||
return this.httpClient.get<string[]>(this.baseUrl + 'account/roles');
|
||||
}
|
||||
|
||||
login(model: {username: string, password: string}): Observable<any> {
|
||||
login(model: {username: string, password: string}) {
|
||||
return this.httpClient.post<User>(this.baseUrl + 'account/login', model).pipe(
|
||||
map((response: User) => {
|
||||
const user = response;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { map, Observable, shareReplay } from 'rxjs';
|
||||
import { Chapter } from '../_models/chapter';
|
||||
import { CollectionTag } from '../_models/collection-tag';
|
||||
import { Device } from '../_models/device/device';
|
||||
import { Library } from '../_models/library';
|
||||
import { ReadingList } from '../_models/reading-list';
|
||||
import { Series } from '../_models/series';
|
||||
import { Volume } from '../_models/volume';
|
||||
import { AccountService } from './account.service';
|
||||
import { DeviceService } from './device.service';
|
||||
|
||||
export enum Action {
|
||||
AddTo = -2,
|
||||
Others = -1,
|
||||
Submenu = -1,
|
||||
/**
|
||||
* Mark entity as read
|
||||
*/
|
||||
|
|
@ -78,14 +80,26 @@ export enum Action {
|
|||
* Remove from user's Want to Read List
|
||||
*/
|
||||
RemoveFromWantToReadList = 16,
|
||||
/**
|
||||
* Send to a device
|
||||
*/
|
||||
SendTo = 17,
|
||||
}
|
||||
|
||||
export interface ActionItem<T> {
|
||||
title: string;
|
||||
action: Action;
|
||||
callback: (action: Action, data: T) => void;
|
||||
callback: (action: ActionItem<T>, data: T) => void;
|
||||
requiresAdmin: boolean;
|
||||
children: Array<ActionItem<T>>;
|
||||
/**
|
||||
* Indicates that there exists a separate list will be loaded from an API
|
||||
*/
|
||||
dynamicList?: Observable<{title: string, data: any}[]> | undefined;
|
||||
/**
|
||||
* Extra data that needs to be sent back from the card item. Used mainly for dynamicList. This will be the item from dyanamicList return
|
||||
*/
|
||||
_extra?: any;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
|
|
@ -109,7 +123,7 @@ export class ActionFactoryService {
|
|||
isAdmin = false;
|
||||
hasDownloadRole = false;
|
||||
|
||||
constructor(private accountService: AccountService) {
|
||||
constructor(private accountService: AccountService, private deviceService: DeviceService) {
|
||||
this.accountService.currentUser$.subscribe((user) => {
|
||||
if (user) {
|
||||
this.isAdmin = this.accountService.hasAdminRole(user);
|
||||
|
|
@ -123,35 +137,35 @@ export class ActionFactoryService {
|
|||
});
|
||||
}
|
||||
|
||||
getLibraryActions(callback: (action: Action, library: Library) => void) {
|
||||
getLibraryActions(callback: (action: ActionItem<Library>, library: Library) => void) {
|
||||
return this.applyCallbackToList(this.libraryActions, callback);
|
||||
}
|
||||
|
||||
getSeriesActions(callback: (action: Action, series: Series) => void) {
|
||||
getSeriesActions(callback: (action: ActionItem<Series>, series: Series) => void) {
|
||||
return this.applyCallbackToList(this.seriesActions, callback);
|
||||
}
|
||||
|
||||
getVolumeActions(callback: (action: Action, volume: Volume) => void) {
|
||||
getVolumeActions(callback: (action: ActionItem<Volume>, volume: Volume) => void) {
|
||||
return this.applyCallbackToList(this.volumeActions, callback);
|
||||
}
|
||||
|
||||
getChapterActions(callback: (action: Action, chapter: Chapter) => void) {
|
||||
getChapterActions(callback: (action: ActionItem<Chapter>, chapter: Chapter) => void) {
|
||||
return this.applyCallbackToList(this.chapterActions, callback);
|
||||
}
|
||||
|
||||
getCollectionTagActions(callback: (action: Action, collectionTag: CollectionTag) => void) {
|
||||
getCollectionTagActions(callback: (action: ActionItem<CollectionTag>, collectionTag: CollectionTag) => void) {
|
||||
return this.applyCallbackToList(this.collectionTagActions, callback);
|
||||
}
|
||||
|
||||
getReadingListActions(callback: (action: Action, readingList: ReadingList) => void) {
|
||||
getReadingListActions(callback: (action: ActionItem<ReadingList>, readingList: ReadingList) => void) {
|
||||
return this.applyCallbackToList(this.readingListActions, callback);
|
||||
}
|
||||
|
||||
getBookmarkActions(callback: (action: Action, series: Series) => void) {
|
||||
getBookmarkActions(callback: (action: ActionItem<Series>, series: Series) => void) {
|
||||
return this.applyCallbackToList(this.bookmarkActions, callback);
|
||||
}
|
||||
|
||||
dummyCallback(action: Action, data: any) {}
|
||||
dummyCallback(action: ActionItem<any>, data: any) {}
|
||||
|
||||
_resetActions() {
|
||||
this.libraryActions = [
|
||||
|
|
@ -163,7 +177,7 @@ export class ActionFactoryService {
|
|||
children: [],
|
||||
},
|
||||
{
|
||||
action: Action.Others,
|
||||
action: Action.Submenu,
|
||||
title: 'Others',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: true,
|
||||
|
|
@ -212,7 +226,7 @@ export class ActionFactoryService {
|
|||
children: [],
|
||||
},
|
||||
{
|
||||
action: Action.AddTo,
|
||||
action: Action.Submenu,
|
||||
title: 'Add to',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false,
|
||||
|
|
@ -262,7 +276,7 @@ export class ActionFactoryService {
|
|||
children: [],
|
||||
},
|
||||
{
|
||||
action: Action.Others,
|
||||
action: Action.Submenu,
|
||||
title: 'Others',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false,
|
||||
|
|
@ -315,7 +329,7 @@ export class ActionFactoryService {
|
|||
children: [],
|
||||
},
|
||||
{
|
||||
action: Action.AddTo,
|
||||
action: Action.Submenu,
|
||||
title: 'Add to',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false,
|
||||
|
|
@ -368,7 +382,7 @@ export class ActionFactoryService {
|
|||
children: [],
|
||||
},
|
||||
{
|
||||
action: Action.AddTo,
|
||||
action: Action.Submenu,
|
||||
title: 'Add to',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false,
|
||||
|
|
@ -397,6 +411,24 @@ export class ActionFactoryService {
|
|||
requiresAdmin: false,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
action: Action.Submenu,
|
||||
title: 'Send To',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false,
|
||||
children: [
|
||||
{
|
||||
action: Action.SendTo,
|
||||
title: '',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false,
|
||||
dynamicList: this.deviceService.devices$.pipe(map(devices => devices.map(d => {
|
||||
return {'title': d.name, 'data': d};
|
||||
}), shareReplay())),
|
||||
children: []
|
||||
}
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
this.readingListActions = [
|
||||
|
|
@ -441,7 +473,7 @@ export class ActionFactoryService {
|
|||
];
|
||||
}
|
||||
|
||||
private applyCallback(action: ActionItem<any>, callback: (action: Action, data: any) => void) {
|
||||
private applyCallback(action: ActionItem<any>, callback: (action: ActionItem<any>, data: any) => void) {
|
||||
action.callback = callback;
|
||||
|
||||
if (action.children === null || action.children?.length === 0) return;
|
||||
|
|
@ -451,7 +483,7 @@ export class ActionFactoryService {
|
|||
});
|
||||
}
|
||||
|
||||
private applyCallbackToList(list: Array<ActionItem<any>>, callback: (action: Action, data: any) => void): Array<ActionItem<any>> {
|
||||
private applyCallbackToList(list: Array<ActionItem<any>>, callback: (action: ActionItem<any>, data: any) => void): Array<ActionItem<any>> {
|
||||
const actions = list.map((a) => {
|
||||
return { ...a };
|
||||
});
|
||||
|
|
|
|||
48
UI/Web/src/app/_services/device.service.ts
Normal file
48
UI/Web/src/app/_services/device.service.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ReplaySubject, shareReplay, switchMap, take, tap } from 'rxjs';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { Device } from '../_models/device/device';
|
||||
import { DevicePlatform } from '../_models/device/device-platform';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DeviceService {
|
||||
|
||||
baseUrl = environment.apiUrl;
|
||||
|
||||
private devicesSource: ReplaySubject<Device[]> = new ReplaySubject<Device[]>(1);
|
||||
public devices$ = this.devicesSource.asObservable().pipe(shareReplay());
|
||||
|
||||
|
||||
constructor(private httpClient: HttpClient) {
|
||||
this.httpClient.get<Device[]>(this.baseUrl + 'device', {}).subscribe(data => {
|
||||
this.devicesSource.next(data);
|
||||
});
|
||||
}
|
||||
|
||||
createDevice(name: string, platform: DevicePlatform, emailAddress: string) {
|
||||
return this.httpClient.post(this.baseUrl + 'device/create', {name, platform, emailAddress}, {responseType: 'text' as 'json'});
|
||||
}
|
||||
|
||||
updateDevice(id: number, name: string, platform: DevicePlatform, emailAddress: string) {
|
||||
return this.httpClient.post(this.baseUrl + 'device/update', {id, name, platform, emailAddress}, {responseType: 'text' as 'json'});
|
||||
}
|
||||
|
||||
deleteDevice(id: number) {
|
||||
return this.httpClient.delete(this.baseUrl + 'device?deviceId=' + id);
|
||||
}
|
||||
|
||||
getDevices() {
|
||||
return this.httpClient.get<Device[]>(this.baseUrl + 'device', {}).pipe(tap(data => {
|
||||
this.devicesSource.next(data);
|
||||
}));
|
||||
}
|
||||
|
||||
sendTo(chapterId: number, deviceId: number) {
|
||||
return this.httpClient.post(this.baseUrl + 'device/send-to', {deviceId, chapterId}, {responseType: 'text' as 'json'});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue