Skip to content

Commit

Permalink
feat: added DbxFirebaseModelHistoryPopoverButtonComponent
Browse files Browse the repository at this point in the history
  • Loading branch information
dereekb committed Mar 27, 2023
1 parent 26523ea commit ce8a720
Show file tree
Hide file tree
Showing 27 changed files with 412 additions and 53 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<dbx-content-page dbxAppContextState="public">
<dbx-content-container>
<h2>History</h2>
<p>View recently viewed items here.</p>
<dbx-firebase-model-type-instance-list [state$]="state$"></dbx-firebase-model-type-instance-list>
<dbx-section-page header="History">
<div sectionHeader>
<dbx-spacer></dbx-spacer>
<dbx-firebase-model-history-popover-button></dbx-firebase-model-history-popover-button>
</div>
<dbx-firebase-model-history [historyFilter]="historyFilter"></dbx-firebase-model-history>
</dbx-section-page>
</dbx-content-container>
</dbx-content-page>
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { Component } from '@angular/core';
import { DbxFirebaseModelTrackerService } from '@dereekb/dbx-firebase';
import { DbxFirebaseModelTrackerHistoryFilter, DbxFirebaseModelTrackerService } from '@dereekb/dbx-firebase';
import { loadingStateFromObs } from '@dereekb/rxjs';

@Component({
templateUrl: './history.component.html'
})
export class DemoAppHistoryComponent {
readonly historyPairs$ = this.dbxFirebaseModelTrackerService.loadHistoryPairs();
readonly state$ = loadingStateFromObs(this.historyPairs$);
readonly historyFilter: DbxFirebaseModelTrackerHistoryFilter = {};

constructor(readonly dbxFirebaseModelTrackerService: DbxFirebaseModelTrackerService) {}
}
3 changes: 2 additions & 1 deletion packages/dbx-firebase/src/lib/model/model.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { NgModule } from '@angular/core';
import { DbxFirebaseModelHistoryModule } from './modules/model/model.history.module';
import { DbxFirebaseModelTypesModule } from './modules/model/model.types.module';
import { DbxFirebaseModelStoreModule } from './modules/store/model.store.module';

@NgModule({
exports: [DbxFirebaseModelStoreModule, DbxFirebaseModelTypesModule]
exports: [DbxFirebaseModelStoreModule, DbxFirebaseModelHistoryModule, DbxFirebaseModelTypesModule]
})
export class DbxFirebaseModelModule {}
4 changes: 4 additions & 0 deletions packages/dbx-firebase/src/lib/model/modules/model/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export * from './model.history.component';
export * from './model.history.module';
export * from './model.history.popover.button.component';
export * from './model.history.popover.component';
export * from './model.tracker.service';
export * from './model.types.service';
export * from './model.types.module';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Component, Input, OnDestroy } from '@angular/core';
import { AnchorForValueFunction } from '@dereekb/dbx-web';
import { loadingStateFromObs } from '@dereekb/rxjs';
import { Maybe } from '@dereekb/util';
import { BehaviorSubject, switchMap, shareReplay } from 'rxjs';
import { DbxFirebaseModelTrackerHistoryFilter, DbxFirebaseModelTrackerService } from './model.tracker.service';
import { DbxFirebaseModelTypesServiceInstancePair } from './model.types.service';

@Component({
selector: 'dbx-firebase-model-history',
template: `
<dbx-firebase-model-type-instance-list [state$]="state$" [dbxListItemModifier] [dbxListItemAnchorModifier]="anchorForItem">
<ng-content empty select="[empty]"></ng-content>
</dbx-firebase-model-type-instance-list>
`
})
export class DbxFirebaseModelHistoryComponent implements OnDestroy {
private _historyFilter = new BehaviorSubject<Maybe<DbxFirebaseModelTrackerHistoryFilter>>(undefined);

@Input()
anchorForItem?: Maybe<AnchorForValueFunction<DbxFirebaseModelTypesServiceInstancePair>>;

@Input()
get historyFilter() {
return this._historyFilter.value;
}

set historyFilter(historyFilter: Maybe<DbxFirebaseModelTrackerHistoryFilter>) {
this._historyFilter.next(historyFilter);
}

readonly historyFilter$ = this._historyFilter.asObservable();

readonly historyPairs$ = this.historyFilter$.pipe(
switchMap((x) => this.dbxFirebaseModelTrackerService.filterHistoryPairs(x)),
shareReplay(1)
);

readonly state$ = loadingStateFromObs(this.historyPairs$);

constructor(readonly dbxFirebaseModelTrackerService: DbxFirebaseModelTrackerService) {}

ngOnDestroy(): void {
this._historyFilter.complete();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { DbxButtonModule, DbxListLayoutModule, DbxModelInfoModule, DbxPopoverInteractionModule, DbxRouterListModule } from '@dereekb/dbx-web';
import { DbxFirebaseModelHistoryComponent } from './model.history.component';
import { DbxFirebaseModelHistoryPopoverButtonComponent } from './model.history.popover.button.component';
import { DbxFirebaseModelHistoryPopoverComponent } from './model.history.popover.component';
import { DbxFirebaseModelTypesModule } from './model.types.module';

const declarations = [DbxFirebaseModelHistoryComponent, DbxFirebaseModelHistoryPopoverButtonComponent, DbxFirebaseModelHistoryPopoverComponent];

@NgModule({
imports: [CommonModule, DbxButtonModule, DbxRouterListModule, DbxPopoverInteractionModule, DbxModelInfoModule, DbxListLayoutModule, DbxFirebaseModelTypesModule],
declarations,
exports: declarations
})
export class DbxFirebaseModelHistoryModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ChangeDetectionStrategy, Component, ElementRef, Input, ViewChild } from '@angular/core';
import { AbstractPopoverRefDirective, DbxPopoverService } from '@dereekb/dbx-web';
import { NgPopoverRef } from 'ng-overlay-container';
import { DbxFirebaseModelHistoryPopoverComponent, DbxFirebaseModelHistoryPopoverParams } from './model.history.popover.component';

export type DbxFirebaseModelHistoryPopoverButtonConfig = DbxFirebaseModelHistoryPopoverParams;

@Component({
selector: 'dbx-firebase-model-history-popover-button',
template: `
<dbx-icon-button #button (buttonClick)="showHistoryPopover()" icon="history"></dbx-icon-button>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DbxFirebaseModelHistoryPopoverButtonComponent extends AbstractPopoverRefDirective<unknown, unknown> {
@ViewChild('button', { read: ElementRef, static: false })
buttonElement!: ElementRef;

@Input()
config?: DbxFirebaseModelHistoryPopoverButtonConfig;

constructor(private readonly popupService: DbxPopoverService) {
super();
}

protected override _makePopoverRef(origin?: ElementRef): NgPopoverRef<unknown, unknown> {
const config = this.config;

if (!origin) {
throw new Error('Missing origin.');
}

return DbxFirebaseModelHistoryPopoverComponent.openPopover(this.popupService, {
origin,
...config
});
}

showHistoryPopover(): void {
const origin = this.buttonElement.nativeElement;
this.showPopover(origin);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<dbx-popover-content>
<!-- Header -->
<dbx-popover-header [icon]="icon" [header]="header"></dbx-popover-header>
<!-- Content -->
<dbx-popover-scroll-content>
<dbx-firebase-model-history [historyFilter]="historyFilter" [anchorForItem]="anchorForItem">
<dbx-list-empty-content empty>
<p>{{ emptyText }}</p>
</dbx-list-empty-content>
</dbx-firebase-model-history>
</dbx-popover-scroll-content>
</dbx-popover-content>
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Component, ElementRef, Type, OnInit, OnDestroy } from '@angular/core';
import { NgPopoverRef } from 'ng-overlay-container';
import { AbstractPopoverDirective, AnchorForValueFunction, DbxPopoverComponent, DbxPopoverKey, DbxPopoverService } from '@dereekb/dbx-web';
import { Maybe } from '@dereekb/util';
import { DbxFirebaseModelTypesServiceInstancePair } from './model.types.service';
import { DbxFirebaseModelTrackerHistoryFilter } from './model.tracker.service';

export interface DbxFirebaseModelHistoryPopoverParams {
/**
* Custom icon
*
* Defaults to "history"
*/
icon?: string;
/**
* Custom header text
*
* Defaults to "History"
*/
header?: string;
/**
* Custom empty text when no items exist.
*/
emptyText?: string;
/**
* Origin to add the popover to.
*/
origin: ElementRef;
/**
* Optional config to pass to the DbxFirebaseHistoryComponent
*/
historyFilter?: Maybe<DbxFirebaseModelTrackerHistoryFilter>;
/**
* Anchor
*/
anchorForItem?: Maybe<AnchorForValueFunction<DbxFirebaseModelTypesServiceInstancePair>>;
}

export const DEFAULT_FIREBASE_HISTORY_COMPONENT_POPOVER_KEY = 'history';

@Component({
templateUrl: './model.history.popover.component.html'
})
export class DbxFirebaseModelHistoryPopoverComponent extends AbstractPopoverDirective<unknown, DbxFirebaseModelHistoryPopoverParams> {
static openPopover(popupService: DbxPopoverService, { origin, header, icon, emptyText, historyFilter, anchorForItem }: DbxFirebaseModelHistoryPopoverParams, popoverKey?: DbxPopoverKey): NgPopoverRef {
return popupService.open({
key: popoverKey ?? DEFAULT_FIREBASE_HISTORY_COMPONENT_POPOVER_KEY,
origin,
componentClass: DbxFirebaseModelHistoryPopoverComponent,
data: {
header,
icon,
emptyText,
historyFilter,
anchorForItem
} as DbxFirebaseModelHistoryPopoverParams
});
}

constructor(popover: DbxPopoverComponent<unknown, DbxFirebaseModelHistoryPopoverParams>) {
super(popover);
}

get params(): DbxFirebaseModelHistoryPopoverParams {
return this.popover.data as DbxFirebaseModelHistoryPopoverParams;
}

get icon() {
return this.params.icon ?? 'history';
}

get header() {
return this.params.header ?? 'History';
}

get emptyText() {
return this.params.header ?? 'History is empty.';
}

get historyFilter() {
return this.params.historyFilter;
}

get anchorForItem() {
return this.params.anchorForItem;
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,87 @@
import { map, Observable, switchMap } from 'rxjs';
import { ArrayOrValue, DecisionFunction, Maybe, asArray, invertDecision } from '@dereekb/util';
import { FirestoreModelIdentity } from '@dereekb/firebase';
import { map, Observable, switchMap, shareReplay, startWith, combineLatest, identity, first, of } from 'rxjs';
import { Injectable } from '@angular/core';
import { allDbxModelViewTrackerEventModelKeys, DbxModelTrackerService } from '@dereekb/dbx-web';
import { DbxFirebaseModelTypesService, DbxFirebaseModelTypesServiceInstancePair } from './model.types.service';
import { filterItemsWithObservableDecision, invertObservableDecision, ObservableDecisionFunction, tapLog } from '@dereekb/rxjs';

export interface DbxFirebaseModelTrackerFilterItem {
instancePair: DbxFirebaseModelTypesServiceInstancePair;
identity: FirestoreModelIdentity;
}

export interface DbxFirebaseModelTrackerHistoryFilter {
/**
* Whether or not to exclude the values instead of include them.
*/
invertFilter?: boolean;
/**
* Identity types to filter on.
*/
identity?: Maybe<ArrayOrValue<FirestoreModelIdentity>>;
/**
* Arbitrary filter function to filter individual items.
*/
filterItem?: ObservableDecisionFunction<DbxFirebaseModelTrackerFilterItem>;
}

@Injectable({
providedIn: 'root'
})
export class DbxFirebaseModelTrackerService {
// TODO: Expose as observables directly that update when history events change.
readonly historyKeys$ = this.dbxModelTrackerService.newEvent$.pipe(
startWith(null),
switchMap(() => this.loadHistoryKeys()),
shareReplay(1)
);

readonly historyPairs$ = this.dbxModelTrackerService.newEvent$.pipe(
startWith(null),
switchMap(() => this.loadHistoryPairs()), // TODO: Improve efficiency of this to only load/remove items for new keys
shareReplay(1)
);

readonly filterItemHistoryPairs$: Observable<DbxFirebaseModelTrackerFilterItem[]> = this.historyPairs$.pipe(
switchMap((x) => {
const typeObs = x.map((instancePair) => instancePair.instance.identity$.pipe(map((identity) => ({ instancePair, identity }))));
return combineLatest(typeObs).pipe(first());
}),
shareReplay(1)
);

/**
* Filters from historyPairs$ using the input filter configuration, if it is provided.
*
* @param filter
* @returns
*/
filterHistoryPairs(filter?: Maybe<DbxFirebaseModelTrackerHistoryFilter>): Observable<DbxFirebaseModelTypesServiceInstancePair[]> {
if (filter && (filter?.identity || filter?.filterItem)) {
const { invertFilter = false, identity, filterItem } = filter;
const allowedIdentities = new Set(asArray(identity));
const baseIsAllowedIdentityFn: DecisionFunction<FirestoreModelIdentity> = identity ? (x) => allowedIdentities.has(x) : () => true;
const isAllowedIdentityFn = invertDecision(baseIsAllowedIdentityFn, invertFilter);

const baseFilterItemFn: ObservableDecisionFunction<DbxFirebaseModelTrackerFilterItem> = filterItem ? invertObservableDecision(filterItem, invertFilter) : () => of(true);

const filterItemFn: ObservableDecisionFunction<DbxFirebaseModelTrackerFilterItem> = (x) => {
if (isAllowedIdentityFn(x.identity)) {
return baseFilterItemFn(x);
} else {
return of(false);
}
};

return this.filterItemHistoryPairs$.pipe(
filterItemsWithObservableDecision(filterItemFn),
map((x) => x.map((y) => y.instancePair)),
shareReplay(1)
);
} else {
return this.historyPairs$;
}
}

loadHistoryKeys() {
return this.dbxModelTrackerService.getAllViewEvents().pipe(map(allDbxModelViewTrackerEventModelKeys));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ export class DbxFirebaseModelTypeInstanceViewComponent extends AbstractDbxSelect

@Component({
template: `
<div>
<p>{{ title }}</p>
</div>
<span>{{ title }}</span>
`
})
export class DbxFirebaseModelTypeInstanceViewItemComponent extends AbstractDbxValueListViewItemComponent<DbxFirebaseModelTypesServiceInstancePair> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { distinctUntilChanged, map, Observable, shareReplay, switchMap, combineLatest } from 'rxjs';
import { distinctUntilChanged, map, Observable, shareReplay, switchMap, combineLatest, of } from 'rxjs';
import { FirestoreCollectionType, FirestoreDocument, FirestoreModelIdentity, FirestoreModelKey } from '@dereekb/firebase';
import { DbxModelTypeInfo, DbxModelTypesMap, DbxModelTypesService } from '@dereekb/dbx-web';
import { ArrayOrValue, asArray, FactoryWithRequiredInput, Maybe, ModelTypeString } from '@dereekb/util';
Expand Down Expand Up @@ -122,7 +122,7 @@ export type DbxFirebaseModelTypesServiceInstancePairForKeysFactory = (keys: Arra
export function dbxFirebaseModelTypesServiceInstancePairForKeysFactory(service: DbxFirebaseModelTypesService): DbxFirebaseModelTypesServiceInstancePairForKeysFactory {
return (keys: ArrayOrValue<ObservableOrValue<FirestoreModelKey>>) => {
const instances = asArray(keys).map((x) => service.instanceForKey(x).instancePair$);
return combineLatest(instances);
return instances.length ? combineLatest(instances) : of([]);
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { AbstractSubscriptionDirective } from '@dereekb/dbx-core';
import { DbxListViewWrapper } from '@dereekb/dbx-web';
import { Directive, Host, OnInit } from '@angular/core';
import { DbxFirebaseCollectionStoreDirective } from './store.collection.directive';
import { tapLog } from '@dereekb/rxjs';

/**
* Directive that connects a host DbxListView to a DbxFirebaseCollectionStoreDirective to pass data for rendering items from a collection and query parameters.
Expand All @@ -13,7 +12,7 @@ import { tapLog } from '@dereekb/rxjs';
export class DbxFirebaseCollectionListDirective<T> extends AbstractSubscriptionDirective implements OnInit {
constructor(readonly dbxFirebaseCollectionStoreDirective: DbxFirebaseCollectionStoreDirective<T>, @Host() readonly dbxListViewWrapper: DbxListViewWrapper<T>) {
super();
this.dbxListViewWrapper.state$ = this.dbxFirebaseCollectionStoreDirective.pageLoadingState$.pipe(tapLog('xxx'));
this.dbxListViewWrapper.state$ = this.dbxFirebaseCollectionStoreDirective.pageLoadingState$;
}

ngOnInit(): void {
Expand Down
Loading

0 comments on commit ce8a720

Please sign in to comment.