Last active
June 14, 2020 02:04
-
-
Save AzrizHaziq/519bbe99f71c21615517135c3ba8a87b to your computer and use it in GitHub Desktop.
Rxjs - State management, please don't blindly look at import statement as I delete some loc, please use browser-search (CTRL/CMD + F) to look at variables
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// tslint:disable:no-reserved-keywords | |
import { switchMap } from 'rxjs/operators'; | |
import { Observable, merge, Subject, of } from 'rxjs'; | |
export enum DOC_ACTION { | |
DnD = 'drag-and-drop', | |
UPLOAD_DOC = 'upload-doc', | |
RENAME_DOC = 'rename-doc', | |
DELETE_DOC = 'delete-doc' | |
} | |
interface IDocAction<T> { | |
type: DOC_ACTION; | |
payload: T; | |
} | |
export const docActionCompose = <T>(type: DOC_ACTION) => (payload: T): Observable<IDocAction<T>> => of({ | |
type, | |
payload | |
}); | |
// UPLOAD DOC | |
export interface IUploadDocAction { docs: FileHierarchy.IDocument[]; parentId: string; } | |
export const uploadDoc = new Subject(); | |
export const uploadDocAction = ({ docs, parentId }: IUploadDocAction): void => uploadDoc.next({ docs, parentId }); | |
export const uploadDoc$: Observable<any> = uploadDoc.asObservable().pipe( | |
switchMap(docActionCompose<IUploadDocAction>(DOC_ACTION.UPLOAD_DOC)) | |
); | |
// RENAME | |
export interface IRenameDocAction { docId: string; newDocName: string; } | |
export const renameDoc = new Subject(); | |
export const renameDocAction = ({ docId, newDocName }: IRenameDocAction): void => renameDoc.next({ docId, newDocName }); | |
export const renameDoc$: Observable<any> = renameDoc.asObservable().pipe( | |
switchMap(docActionCompose<IRenameDocAction>(DOC_ACTION.RENAME_DOC)) | |
); | |
// DELETE | |
export const deleteDoc = new Subject(); | |
export const deleteDocAction = (docId: string): void => deleteDoc.next(docId); | |
export const deleteDoc$: Observable<any> = deleteDoc.asObservable().pipe( | |
switchMap(docActionCompose<string>(DOC_ACTION.DELETE_DOC)) | |
); | |
export const docActions$ = merge( | |
uploadDoc$, | |
renameDoc$, | |
deleteDoc$ | |
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { IUploadDocAction, IRenameDocAction } from './doc.action'; | |
export const deleteDocs = ( | |
files: FileHierarchy.roots, | |
action: { payload: string } | |
): FileHierarchy.roots => { | |
const { payload: docId } = action; | |
return files.reduce((acc: any, file: FileHierarchy.DynamicFileHierarchyTypings) => | |
file.folder | |
// look further inside folder | |
? [...acc, { ...file, documents: deleteDocs(file.documents, action) }] | |
: file.id === docId | |
? acc // remove the doc here | |
: [...acc, file] // dont do anything to diff docId | |
// tslint:disable-next-line:align | |
, []); | |
}; | |
export const uploadNewDoc = ( | |
files: FileHierarchy.roots, | |
action: { payload: IUploadDocAction } | |
): FileHierarchy.roots => { | |
const { payload: { docs, parentId } } = action; | |
function uploadInNestedFolder(): FileHierarchy.roots { | |
return files.reduce((acc: any, file: FileHierarchy.DynamicFileHierarchyTypings) => { | |
return file.folder | |
? file.id === parentId | |
// append new folder at the end of parent folder. | |
? [...acc, { ...file, documents: [...file.documents, ...docs] }] | |
// look further inside folder | |
: [...acc, { ...file, documents: uploadNewDoc(file.documents, action) }] | |
// dont do anything to doc | |
: [...acc, file]; | |
} | |
// tslint:disable-next-line:align | |
, []); | |
} | |
return parentId | |
? [...uploadInNestedFolder()] // append nested folder | |
: [...files, ...docs]; // append to root | |
}; | |
export const renameDocHelper = ( | |
files: FileHierarchy.roots, | |
action: { payload: IRenameDocAction } | |
): FileHierarchy.roots => { | |
const { docId, newDocName } = action.payload; | |
return files.reduce((acc: any, file: FileHierarchy.DynamicFileHierarchyTypings) => | |
file.folder | |
? [...acc, { ...file, documents: renameDocHelper(file.documents, action) }] | |
: file.id === docId | |
? [...acc, { ...file, filename: newDocName }] | |
: [...acc, file] | |
// tslint:disable-next-line:align | |
, []); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Folder actions | |
// tslint:disable:no-reserved-keywords | |
import { switchMap } from 'rxjs/operators'; | |
import { Observable, merge, Subject, of } from 'rxjs'; | |
export enum FOLDER_ACTION { | |
NEW_FOLDER = 'new-folder', | |
RENAME_FOLDER = 'rename-folder', | |
DELETE_FOLDER = 'delete-folder', | |
// meta action | |
HIDE_NEW_FOLDER = 'hide-new-folder', | |
SHOW_NEW_FOLDER = 'show-new-folder' | |
} | |
interface IFolderAction<T> { | |
type: FOLDER_ACTION; | |
payload: T; | |
} | |
export const folderActionCompose = <T>(type: FOLDER_ACTION) => (payload: T): Observable<IFolderAction<T>> => of({ | |
type, | |
payload | |
}); | |
// META: SHOW NEW FOLDER | |
export interface IShowNewFolderAction { | |
parentId: string; | |
} | |
export const showNewFolder = new Subject(); | |
export const showNewFolderAction = ({ parentId }: IShowNewFolderAction): void => showNewFolder.next({ parentId }); | |
export const showNewFolder$: Observable<any> = showNewFolder.asObservable().pipe( | |
switchMap(folderActionCompose(FOLDER_ACTION.SHOW_NEW_FOLDER)) | |
); | |
// META: HIDE NEW FOLDER | |
export const hideNewFolder = new Subject(); | |
export const hideNewFolderAction = (): void => hideNewFolder.next(); | |
export const hideNewFolder$: Observable<any> = hideNewFolder.asObservable().pipe( | |
switchMap(folderActionCompose(FOLDER_ACTION.HIDE_NEW_FOLDER)) | |
); | |
// SAVE NEW FOLDER | |
export interface INewFolderAction { | |
parentId: string; | |
folder: FileHierarchy.IFolder; | |
} | |
export const newFolder = new Subject(); | |
export const newFolderAction = ({ parentId, folder }: INewFolderAction): void => | |
newFolder.next({ parentId, folder }); | |
export const newFolder$: Observable<any> = newFolder.asObservable().pipe( | |
switchMap(folderActionCompose(FOLDER_ACTION.NEW_FOLDER)) | |
); | |
// RENAME FOLDER | |
export interface IRenameFolderAction { | |
folderId: string; | |
newFolderName: string; | |
} | |
export const renameFolder = new Subject(); | |
export const renameFolderAction = ({ folderId, newFolderName }: IRenameFolderAction): void => | |
renameFolder.next({ folderId, newFolderName }); | |
export const renameFolder$: Observable<any> = renameFolder.asObservable().pipe( | |
switchMap(folderActionCompose(FOLDER_ACTION.RENAME_FOLDER)) | |
); | |
// DELETE FOLDER | |
export const deleteFolder = new Subject(); | |
export const deleteFolderAction = (folderId: string): void => deleteFolder.next(folderId); | |
export const deleteFolder$: Observable<any> = deleteFolder.asObservable().pipe( | |
switchMap(folderActionCompose(FOLDER_ACTION.DELETE_FOLDER)) | |
); | |
// Folder Open/Close state | |
export let isFolderOpen: { [k: string]: boolean } = {}; | |
export const folderStateHelper = { | |
toggle(id: string) { | |
if (!isFolderOpen.hasOwnProperty(id)) { | |
return; | |
} | |
isFolderOpen[id] = !isFolderOpen[id]; | |
}, | |
setState(id: string, bool: boolean) { | |
// dont add any falsy id | |
if (!id && !isFolderOpen.hasOwnProperty(id)) { | |
return; | |
} | |
isFolderOpen[id] = bool; | |
}, | |
getState(id: string): boolean { | |
return isFolderOpen[id]; | |
}, | |
resetState() { | |
isFolderOpen = {}; | |
} | |
}; | |
export const folderActions$ = merge( | |
showNewFolder$, | |
hideNewFolder$, | |
newFolder$, | |
renameFolder$, | |
deleteFolder$ | |
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// folder reducer | |
import { IRenameDocAction, IRenameFolderAction } from '@app/shared/file'; | |
export const attachEmptyNewFolder = ( | |
files: FileHierarchy.roots, | |
{ payload: { parentId }}: { payload: { parentId: string }} | |
): FileHierarchy.roots => { | |
const emptyFolder = { | |
// id will be set with parentId | |
title: '', | |
folder: true, | |
createdAt: '', | |
updatedAt: '', | |
documents: [], | |
meta: { | |
newFolderAction: true | |
} | |
}; | |
// tslint:disable-next-line | |
function addEmptyFolder(files: FileHierarchy.roots, parentId: string): FileHierarchy.roots { | |
return files.reduce((acc, file: FileHierarchy.DynamicFileHierarchyTypings) => { | |
return file.folder | |
? file.id === parentId | |
? [...acc, { ...file, documents: [...file.documents, { ...emptyFolder, id: parentId }] }] | |
: [...acc, { ...file, documents: addEmptyFolder(file.documents, parentId) }] | |
// don't do anything to document type | |
: [...acc, file]; | |
// tslint:disable-next-line:align | |
}, []); | |
} | |
return parentId | |
? [...addEmptyFolder(files, parentId)] | |
: [...files, { ...emptyFolder, id: null }]; | |
}; | |
export const detachEmptyNewFolder = (files: FileHierarchy.roots): FileHierarchy.roots => { | |
return files.reduce((acc: any, file: FileHierarchy.DynamicFileHierarchyTypings) => { | |
return file.folder | |
? (file.meta && file.meta.newFolderAction) | |
? [...acc] // remove folder with meta | |
: [...acc, { ...file, documents: detachEmptyNewFolder(file.documents) }] // nested folder | |
: [...acc, file]; // don't do anything to document type | |
// tslint:disable-next-line:align | |
}, []); | |
}; | |
export const addNewFolder = ( | |
files: FileHierarchy.roots, | |
{ payload: { parentId, folder } }: { payload: { parentId: string, folder: FileHierarchy.IFolder } } | |
): FileHierarchy.roots => { | |
// tslint:disable-next-line | |
function appendFolder(files: FileHierarchy.roots, parentId: string): FileHierarchy.roots { | |
return files.reduce((acc, file: FileHierarchy.DynamicFileHierarchyTypings) => { | |
return file.folder | |
? file.id === parentId | |
? [...acc, { ...file, documents: [...file.documents, folder] }] // append folder | |
: [...acc, { ...file, documents: appendFolder(file.documents, parentId) }] // nested folder | |
: [...acc, file]; // don't do anything to document type | |
// tslint:disable-next-line:align | |
}, []); | |
} | |
return parentId | |
? [...appendFolder(files, parentId)] | |
: [...files, folder]; | |
}; | |
export const deleteExistingFolder = ( | |
files: FileHierarchy.roots, | |
action: { payload: string } | |
): FileHierarchy.roots => { | |
const { payload: folderId } = action; | |
return files.reduce((acc: any, file: FileHierarchy.DynamicFileHierarchyTypings) => | |
file.folder | |
? file.id === folderId | |
? acc // remove folder | |
: [...acc, { ...file, documents: deleteExistingFolder(file.documents, action) }] // look further inside folder | |
: [...acc, file] // don't do anything to doc | |
// tslint:disable-next-line:align | |
, []); | |
}; | |
export const renameFolderHelper = ( | |
files: FileHierarchy.roots, | |
action: { payload: IRenameFolderAction } | |
): FileHierarchy.roots => { | |
const { folderId, newFolderName } = action.payload; | |
return files.reduce((acc: any, file: FileHierarchy.DynamicFileHierarchyTypings) => | |
file.folder | |
? file.id === folderId | |
? [...acc, { ...file, title: newFolderName }] | |
: [...acc, { ...file, documents: renameFolderHelper(file.documents, action) }] | |
: [...acc, file ] | |
// tslint:disable-next-line:align | |
, []); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// How to use | |
// tslint:disable:align member-ordering max-line-length max-file-line-count | |
import { BaseComponent } from '@app/commons'; | |
import { MessageService } from '@core/message'; | |
import { ConfirmPopupService } from './common'; | |
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; | |
import { PlatformState } from '@app/shared/platform/state/platform-state'; | |
import { ContentReadingService } from '@app/modules/content-reading/services'; | |
import { FileHierarchyAccessEnum } from '@app/enum/file-hierarchy-access.enum'; | |
import { ChangeDetectionStrategy, Component, OnInit, Input } from '@angular/core'; | |
import { timer, Observable, EMPTY, from, of, iif, combineLatest, throwError } from 'rxjs'; | |
import { tap, map, take, catchError, filter, finalize, switchMap, shareReplay } from 'rxjs/operators'; | |
import { | |
hierarchyState$, | |
folderStateHelper, | |
uploadDocAction, | |
renameDocAction, | |
deleteDocAction, | |
newFolderAction, | |
renameFolderAction, | |
deleteFolderAction, | |
showNewFolderAction, | |
hideNewFolderAction, | |
initHierarchyAction, | |
DocumentActionService, | |
calculateNumberOfFiles, | |
filesHierarchyTransformer, | |
DocumentUploadModalComponent | |
} from '@app/shared/file'; | |
@Component({ | |
selector: 'abc-abc', | |
templateUrl: './abc.component.html', | |
styleUrls: ['./abc.component.scss'], | |
changeDetection: ChangeDetectionStrategy.OnPush | |
}) | |
export class ABCComponent extends BaseComponent implements OnInit { | |
@Input() | |
public fileHierarchies: FileHierarchy.roots = []; | |
public FileHierarchyAccessEnum = FileHierarchyAccessEnum; | |
public state$: Observable<FileHierarchy.roots> = hierarchyState$; | |
public numberOfFiles$: Observable<number> = hierarchyState$.pipe( | |
map<FileHierarchy.roots, number>(calculateNumberOfFiles) | |
); | |
constructor( | |
private modalService: NgbModal, | |
private messageService: MessageService, | |
private confirmPopupService: ConfirmPopupService, | |
private documentActionService: DocumentActionService, | |
private contentReadingService: ContentReadingService | |
) { | |
super(); | |
} | |
docDeleteAction({ id: docId, filename }: FileHierarchy.IDocument) { | |
const popup = this.confirmPopupService.openPopup({ | |
title: 'portal.file.delete_doc.title', | |
subTitle: filename, | |
description: 'portal.file.delete_doc.message', | |
width: 460 | |
}); | |
from(popup).pipe( | |
this.unSubscribeOnDestroy, | |
switchMap(() => this.contentReadingService.deleteFile(true, docId).pipe( | |
catchError(e => { | |
this.messageService.showErrorMessage('portal.file.delete_doc.error_document'); | |
return throwError(e); | |
}), | |
tap(() => this.messageService.showSuccessMessage('portal.file.delete_doc.success_document')), | |
tap(_ => deleteDocAction(docId)) | |
)) | |
).subscribe(); | |
} | |
renameAction(file: FileHierarchy.DynamicFileHierarchyTypings, newName: string): void { | |
const renameFolder$ = of('').pipe( | |
tap(() => renameFolderAction({ folderId: file.id, newFolderName: newName })), | |
switchMap(() => this.contentReadingService.renameFolder({ folderName: newName, folderId: file.id }).pipe( | |
catchError(() => { | |
renameFolderAction({ folderId: file.id, newFolderName: file.title }); | |
return EMPTY; | |
}) | |
)) | |
); | |
const renameDoc$ = of('').pipe( | |
// @ts-ignore | |
tap(() => renameDocAction({ docId: file.id, newDocName: `${newName}.${file.extension}` })), | |
switchMap(() => this.contentReadingService.renameDoc({ fileName: newName, docId: file.id }).pipe( | |
catchError(() => { | |
// @ts-ignore | |
renameDocAction({ docId: file.id, newDocName: file.filename }); | |
return EMPTY; | |
})) | |
) | |
); | |
iif( | |
() => file.folder, | |
renameFolder$, | |
renameDoc$ | |
).pipe(take(1)).subscribe(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { mergeMap } from 'rxjs/operators'; | |
import { Observable, Subject, of } from 'rxjs'; | |
export enum HierarchiesAction { | |
INIT_STATE = 'initState' | |
} | |
export const initState = new Subject(); | |
export const initHierarchyAction = (payload: FileHierarchy.roots) => initState.next(payload); | |
export const initState$: Observable<any> = initState.asObservable().pipe( | |
mergeMap(payload => of({ | |
type: HierarchiesAction.INIT_STATE, | |
payload | |
})) | |
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Observable, merge } from 'rxjs'; | |
import { docActions$, DOC_ACTION } from './doc/doc.action'; | |
import { scan, shareReplay, startWith, tap } from 'rxjs/operators'; | |
import { HierarchiesAction, initState$ } from './main/main.action'; | |
import { folderActions$, FOLDER_ACTION } from './folder/folder.action'; | |
import { deleteDocs, uploadNewDoc, renameDocHelper } from './doc/doc.helper'; | |
import { | |
addNewFolder, | |
renameFolderHelper, | |
attachEmptyNewFolder, | |
detachEmptyNewFolder, | |
deleteExistingFolder | |
} from './folder/folder.helper'; | |
function createReducer(initialState, handlers) { | |
return function reducer(state = initialState, action) { | |
if (handlers.hasOwnProperty(action.type)) { | |
return handlers[action.type](state, action); | |
} else { | |
return state; | |
} | |
}; | |
} | |
const mainReducer = createReducer([], { | |
[HierarchiesAction.INIT_STATE]: (file: FileHierarchy.roots, { payload }) => [...payload], | |
// Doc Reducers | |
[DOC_ACTION.DELETE_DOC]: deleteDocs, | |
[DOC_ACTION.UPLOAD_DOC]: uploadNewDoc, | |
[DOC_ACTION.RENAME_DOC]: renameDocHelper, | |
// Folder Reducers | |
[FOLDER_ACTION.NEW_FOLDER]: addNewFolder, | |
[FOLDER_ACTION.RENAME_FOLDER]: renameFolderHelper, | |
[FOLDER_ACTION.DELETE_FOLDER]: deleteExistingFolder, | |
[FOLDER_ACTION.SHOW_NEW_FOLDER]: attachEmptyNewFolder, | |
[FOLDER_ACTION.HIDE_NEW_FOLDER]: detachEmptyNewFolder | |
}); | |
const actions$ = merge( | |
initState$, | |
docActions$, | |
folderActions$ | |
); | |
// main store. | |
export const hierarchyState$: Observable<FileHierarchy.roots> = actions$.pipe( | |
startWith({ type: HierarchiesAction.INIT_STATE, payload: [] }), | |
scan(mainReducer, []), | |
shareReplay(1) | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment