Skip to content

Instantly share code, notes, and snippets.

@AzrizHaziq
Last active June 14, 2020 02:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AzrizHaziq/519bbe99f71c21615517135c3ba8a87b to your computer and use it in GitHub Desktop.
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
// 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$
);
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
, []);
};
// 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$
);
// 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
, []);
};
// 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();
}
}
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
}))
);
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