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:
Joseph Milazzo 2022-09-23 17:41:29 -05:00 committed by GitHub
parent ab0f13ef74
commit 9d7476a367
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
79 changed files with 3026 additions and 157 deletions

View file

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

View file

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

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