feat(marketplace): extract common components (#1338)

* feat(marketplace): extract common components

* chore: fix service provide

* feat(markdown): allow Observable content

* chore: remove unnecessary module import

* minor styling for marketplacee list

* only show loading for marketplace show if version change

* chore: get rid of unnecessary server request

* chore: fix version switching

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
This commit is contained in:
Alex Inkin
2022-03-21 22:50:06 +03:00
committed by GitHub
parent 8b286431e6
commit 7ea3aefdd5
111 changed files with 1064 additions and 803 deletions

View File

@@ -8,9 +8,8 @@ import { HttpClientModule } from '@angular/common/http'
import { ApiService } from './services/api/api.service' import { ApiService } from './services/api/api.service'
import { MockApiService } from './services/api/mock-api.service' import { MockApiService } from './services/api/mock-api.service'
import { LiveApiService } from './services/api/live-api.service' import { LiveApiService } from './services/api/live-api.service'
import { HttpService } from './services/http.service'
import { GlobalErrorHandler } from './services/global-error-handler.service' import { GlobalErrorHandler } from './services/global-error-handler.service'
import { WorkspaceConfig } from '@start9labs/shared' import { AbstractApiService, WorkspaceConfig } from '@start9labs/shared'
const { useMocks } = require('../../../../config.json') as WorkspaceConfig const { useMocks } = require('../../../../config.json') as WorkspaceConfig
@@ -29,14 +28,11 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
{ {
provide: ApiService, provide: ApiService,
useFactory: (http: HttpService) => { useClass: useMocks ? MockApiService : LiveApiService,
if (useMocks) { },
return new MockApiService() {
} else { provide: AbstractApiService,
return new LiveApiService(http) useExisting: ApiService,
}
},
deps: [HttpService],
}, },
{ provide: ErrorHandler, useClass: GlobalErrorHandler }, { provide: ErrorHandler, useClass: GlobalErrorHandler },
], ],

View File

@@ -0,0 +1,9 @@
<ion-button
*ngFor="let cat of categories"
fill="clear"
class="category"
[class.category_selected]="cat === category"
(click)="switchCategory(cat)"
>
{{ cat }}
</ion-button>

View File

@@ -0,0 +1,14 @@
:host {
display: block;
}
.category {
font-weight: 300;
color: var(--ion-color-dark-shade);
&_selected {
font-weight: bold;
font-size: 17px;
color: var(--color);
}
}

View File

@@ -0,0 +1,32 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
Output,
} from '@angular/core'
@Component({
selector: 'marketplace-categories',
templateUrl: 'categories.component.html',
styleUrls: ['categories.component.scss'],
host: {
class: 'hidden-scrollbar ion-text-center',
},
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CategoriesComponent {
@Input()
categories = new Set<string>()
@Input()
category = ''
@Output()
readonly categoryChange = new EventEmitter<string>()
switchCategory(category: string): void {
this.category = category
this.categoryChange.emit(category)
}
}

View File

@@ -0,0 +1,12 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular'
import { CategoriesComponent } from './categories.component'
@NgModule({
imports: [CommonModule, IonicModule],
declarations: [CategoriesComponent],
exports: [CategoriesComponent],
})
export class CategoriesModule {}

View File

@@ -0,0 +1,10 @@
<ion-item [routerLink]="['/marketplace', pkg.manifest.id]">
<ion-thumbnail slot="start">
<img alt="" [src]="'data:image/png;base64,' + pkg.icon | trustUrl" />
</ion-thumbnail>
<ion-label>
<h2 class="pkg-title">{{ pkg.manifest.title }}</h2>
<h3>{{ pkg.manifest.description.short }}</h3>
<ng-content></ng-content>
</ion-label>
</ion-item>

View File

@@ -0,0 +1,4 @@
.pkg-title {
font-family: 'Montserrat', sans-serif;
font-weight: bold;
}

View File

@@ -0,0 +1,14 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplacePkg } from '../../../types/marketplace-pkg'
@Component({
selector: 'marketplace-item',
templateUrl: 'item.component.html',
styleUrls: ['item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ItemComponent {
@Input()
pkg: MarketplacePkg
}

View File

@@ -0,0 +1,13 @@
import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { SharedPipesModule } from '@start9labs/shared'
import { ItemComponent } from './item.component'
@NgModule({
imports: [IonicModule, RouterModule, SharedPipesModule],
declarations: [ItemComponent],
exports: [ItemComponent],
})
export class ItemModule {}

View File

@@ -0,0 +1,15 @@
<ion-grid>
<ion-row>
<ion-col sizeSm="8" offset-sm="2">
<ion-toolbar color="transparent">
<ion-searchbar
enterkeyhint="search"
color="dark"
debounce="250"
[ngModel]="query"
(ngModelChange)="onModelChange($event)"
></ion-searchbar>
</ion-toolbar>
</ion-col>
</ion-row>
</ion-grid>

View File

@@ -0,0 +1,4 @@
:host {
display: block;
padding-bottom: 32px;
}

View File

@@ -0,0 +1,26 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
Output,
} from '@angular/core'
@Component({
selector: 'marketplace-search',
templateUrl: 'search.component.html',
styleUrls: ['search.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SearchComponent {
@Input()
query = ''
@Output()
readonly queryChange = new EventEmitter<string>()
onModelChange(query: string) {
this.query = query
this.queryChange.emit(query)
}
}

View File

@@ -0,0 +1,12 @@
import { NgModule } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { IonicModule } from '@ionic/angular'
import { SearchComponent } from './search.component'
@NgModule({
imports: [IonicModule, FormsModule],
declarations: [SearchComponent],
exports: [SearchComponent],
})
export class SearchModule {}

View File

@@ -0,0 +1,8 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
@Component({
selector: 'marketplace-skeleton',
templateUrl: 'skeleton.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SkeletonComponent {}

View File

@@ -0,0 +1,12 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular'
import { SkeletonComponent } from './skeleton.component'
@NgModule({
imports: [CommonModule, IonicModule],
declarations: [SkeletonComponent],
exports: [SkeletonComponent],
})
export class SkeletonModule {}

View File

@@ -0,0 +1,32 @@
<ion-content>
<ng-container *ngIf="notes$ | async as notes; else loading">
<div *ngFor="let note of notes | keyvalue: asIsOrder">
<ion-button
expand="full"
color="light"
class="version-button"
[class.ion-activated]="isSelected(note.key)"
(click)="setSelected(note.key)"
>
<p class="version">{{ note.key | displayEmver }}</p>
</ion-button>
<ion-card
elementRef
#element="elementRef"
class="panel"
color="light"
[id]="note.key"
[style.maxHeight.px]="getDocSize(note.key, element)"
>
<ion-text
id="release-notes"
[innerHTML]="note.value | markdown"
></ion-text>
</ion-card>
</div>
</ng-container>
<ng-template #loading>
<text-spinner text="Loading Release Notes"></text-spinner>
</ng-template>
</ion-content>

View File

@@ -1,3 +1,7 @@
:host {
flex: 1 1 0;
}
.panel { .panel {
margin: 0; margin: 0;
padding: 0 24px; padding: 0 24px;

View File

@@ -0,0 +1,38 @@
import { ChangeDetectionStrategy, Component, ElementRef } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { AbstractMarketplaceService } from '../../services/marketplace.service'
@Component({
selector: 'release-notes',
templateUrl: './release-notes.component.html',
styleUrls: ['./release-notes.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReleaseNotesComponent {
private readonly pkgId = this.route.snapshot.paramMap.get('pkgId')
private selected: string | null = null
readonly notes$ = this.marketplaceService.getReleaseNotes(this.pkgId)
constructor(
private readonly route: ActivatedRoute,
private readonly marketplaceService: AbstractMarketplaceService,
) {}
isSelected(key: string): boolean {
return this.selected === key
}
setSelected(selected: string) {
this.selected = this.isSelected(selected) ? null : selected
}
getDocSize(key: string, { nativeElement }: ElementRef<HTMLElement>) {
return this.isSelected(key) ? nativeElement.scrollHeight : 0
}
asIsOrder(a: any, b: any) {
return 0
}
}

View File

@@ -1,20 +1,25 @@
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { MarkdownPage } from './markdown.page'
import { import {
EmverPipesModule,
MarkdownPipeModule, MarkdownPipeModule,
TextSpinnerComponentModule, TextSpinnerComponentModule,
ElementModule,
} from '@start9labs/shared' } from '@start9labs/shared'
import { ReleaseNotesComponent } from './release-notes.component'
@NgModule({ @NgModule({
declarations: [MarkdownPage],
imports: [ imports: [
CommonModule, CommonModule,
IonicModule, IonicModule,
MarkdownPipeModule,
TextSpinnerComponentModule, TextSpinnerComponentModule,
EmverPipesModule,
MarkdownPipeModule,
ElementModule,
], ],
exports: [MarkdownPage], declarations: [ReleaseNotesComponent],
exports: [ReleaseNotesComponent],
}) })
export class MarkdownPageModule {} export class ReleaseNotesModule {}

View File

@@ -0,0 +1,14 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplacePkg } from '../../../types/marketplace-pkg'
@Component({
selector: 'marketplace-about',
templateUrl: 'about.component.html',
styleUrls: ['about.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AboutComponent {
@Input()
pkg: MarketplacePkg
}

View File

@@ -0,0 +1,20 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import { EmverPipesModule, MarkdownPipeModule } from '@start9labs/shared'
import { AboutComponent } from './about.component'
@NgModule({
imports: [
CommonModule,
RouterModule,
IonicModule,
MarkdownPipeModule,
EmverPipesModule,
],
declarations: [AboutComponent],
exports: [AboutComponent],
})
export class AboutModule {}

View File

@@ -6,17 +6,17 @@ import {
Output, Output,
} from '@angular/core' } from '@angular/core'
import { AlertController, ModalController } from '@ionic/angular' import { AlertController, ModalController } from '@ionic/angular'
import { MarketplacePkg } from '@start9labs/marketplace' import { displayEmver, Emver, MarkdownComponent } from '@start9labs/shared'
import { displayEmver, Emver } from '@start9labs/shared'
import { MarkdownPage } from 'src/app/modals/markdown/markdown.page' import { AbstractMarketplaceService } from '../../../services/marketplace.service'
import { MarketplacePkg } from '../../../types/marketplace-pkg'
@Component({ @Component({
selector: 'marketplace-show-additional', selector: 'marketplace-additional',
templateUrl: 'marketplace-show-additional.component.html', templateUrl: 'additional.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class MarketplaceShowAdditionalComponent { export class AdditionalComponent {
@Input() @Input()
pkg: MarketplacePkg pkg: MarketplacePkg
@@ -27,6 +27,7 @@ export class MarketplaceShowAdditionalComponent {
private readonly alertCtrl: AlertController, private readonly alertCtrl: AlertController,
private readonly modalCtrl: ModalController, private readonly modalCtrl: ModalController,
private readonly emver: Emver, private readonly emver: Emver,
private readonly marketplaceService: AbstractMarketplaceService,
) {} ) {}
async presentAlertVersions() { async presentAlertVersions() {
@@ -58,12 +59,14 @@ export class MarketplaceShowAdditionalComponent {
} }
async presentModalMd(title: string) { async presentModalMd(title: string) {
const content = this.marketplaceService.getPackageMarkdown(
title,
this.pkg.manifest.id,
)
const modal = await this.modalCtrl.create({ const modal = await this.modalCtrl.create({
componentProps: { componentProps: { title, content },
title, component: MarkdownComponent,
contentUrl: `/marketplace${this.pkg[title]}`,
},
component: MarkdownPage,
}) })
await modal.present() await modal.present()

View File

@@ -0,0 +1,12 @@
import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular'
import { MarkdownModule } from '@start9labs/shared'
import { AdditionalComponent } from './additional.component'
@NgModule({
imports: [IonicModule, MarkdownModule],
declarations: [AdditionalComponent],
exports: [AdditionalComponent],
})
export class AdditionalModule {}

View File

@@ -0,0 +1,30 @@
<ion-item-divider>Dependencies</ion-item-divider>
<ion-grid>
<ion-row>
<ion-col
*ngFor="let dep of pkg.manifest.dependencies | keyvalue"
sizeSm="12"
sizeMd="6"
>
<ion-item [routerLink]="['/marketplace', dep.key]">
<ion-thumbnail slot="start">
<img alt="" [src]="getImg(dep.key) | trustUrl" />
</ion-thumbnail>
<ion-label>
<h2>
{{ pkg['dependency-metadata'][dep.key].title }}
<ng-container [ngSwitch]="dep.value.requirement.type">
<span *ngSwitchCase="'required'">(required)</span>
<span *ngSwitchCase="'opt-out'">(required by default)</span>
<span *ngSwitchCase="'opt-in'">(optional)</span>
</ng-container>
</h2>
<p>
<small>{{ dep.value.version | displayEmver }}</small>
</p>
<p>{{ dep.value.description }}</p>
</ion-label>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>

View File

@@ -0,0 +1,17 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplacePkg } from '../../../types/marketplace-pkg'
@Component({
selector: 'marketplace-dependencies',
templateUrl: 'dependencies.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DependenciesComponent {
@Input()
pkg: MarketplacePkg
getImg(key: string): string {
return 'data:image/png;base64,' + this.pkg['dependency-metadata'][key].icon
}
}

View File

@@ -0,0 +1,20 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import { EmverPipesModule, SharedPipesModule } from '@start9labs/shared'
import { DependenciesComponent } from './dependencies.component'
@NgModule({
imports: [
CommonModule,
RouterModule,
IonicModule,
SharedPipesModule,
EmverPipesModule,
],
declarations: [DependenciesComponent],
exports: [DependenciesComponent],
})
export class DependenciesModule {}

View File

@@ -0,0 +1,29 @@
<ion-grid>
<ion-row>
<ion-col sizeXs="12" sizeSm="12" sizeMd="9" sizeLg="9" sizeXl="9">
<div class="header">
<img
class="logo"
alt=""
[src]="'data:image/png;base64,' + pkg.icon | trustUrl"
/>
<div class="text">
<h1 class="title">{{ pkg.manifest.title }}</h1>
<p class="version">{{ pkg.manifest.version | displayEmver }}</p>
<ng-content></ng-content>
</div>
</div>
</ion-col>
<ion-col
sizeXl="3"
sizeLg="3"
sizeMd="3"
sizeSm="12"
sizeXs="12"
class="ion-align-self-center"
>
<ng-content select="[slot='controls']"></ng-content>
</ion-col>
</ion-row>
<ng-content select="ion-row"></ng-content>
</ion-grid>

View File

@@ -0,0 +1,26 @@
.header {
font-family: 'Montserrat', sans-serif;
padding: 2%;
}
.logo {
min-width: 15%;
max-width: 18%;
}
.text {
margin-left: 5%;
display: inline-block;
vertical-align: top;
}
.title {
margin: 0 0 0 -2px;
font-size: calc(20px + 3vw);
}
.version {
padding: 4px 0 12px 0;
margin: 0;
font-size: calc(10px + 1vw);
}

View File

@@ -0,0 +1,14 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplacePkg } from '../../../types/marketplace-pkg'
@Component({
selector: 'marketplace-package',
templateUrl: 'package.component.html',
styleUrls: ['package.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PackageComponent {
@Input()
pkg: MarketplacePkg
}

View File

@@ -0,0 +1,12 @@
import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular'
import { EmverPipesModule, SharedPipesModule } from '@start9labs/shared'
import { PackageComponent } from './package.component'
@NgModule({
imports: [IonicModule, SharedPipesModule, EmverPipesModule],
declarations: [PackageComponent],
exports: [PackageComponent],
})
export class PackageModule {}

View File

@@ -1,8 +1,8 @@
import { Pipe, PipeTransform } from '@angular/core' import { NgModule, Pipe, PipeTransform } from '@angular/core'
import Fuse from 'fuse.js/dist/fuse.min.js' import Fuse from 'fuse.js/dist/fuse.min.js'
import { LocalPkg } from '../types/local-pkg'
import { MarketplacePkg } from '../types/marketplace-pkg' import { MarketplacePkg } from '../types/marketplace-pkg'
import { MarketplaceManifest } from '../types/marketplace-manifest'
const defaultOps = { const defaultOps = {
isCaseSensitive: false, isCaseSensitive: false,
@@ -31,9 +31,9 @@ const defaultOps = {
export class FilterPackagesPipe implements PipeTransform { export class FilterPackagesPipe implements PipeTransform {
transform( transform(
packages: MarketplacePkg[] | null, packages: MarketplacePkg[] | null,
local: Record<string, LocalPkg>,
query: string, query: string,
category: string, category: string,
local: Record<string, { manifest: MarketplaceManifest }> = {},
): MarketplacePkg[] | null { ): MarketplacePkg[] | null {
if (!packages) { if (!packages) {
return null return null
@@ -63,3 +63,9 @@ export class FilterPackagesPipe implements PipeTransform {
.map(p => p.item) .map(p => p.item)
} }
} }
@NgModule({
declarations: [FilterPackagesPipe],
exports: [FilterPackagesPipe],
})
export class FilterPackagesPipeModule {}

View File

@@ -1,10 +0,0 @@
import { NgModule } from '@angular/core'
import { InstallProgressPipe } from './install-progress.pipe'
import { TrustPipe } from './trust.pipe'
import { FilterPackagesPipe } from './filter-packages.pipe'
@NgModule({
declarations: [InstallProgressPipe, TrustPipe, FilterPackagesPipe],
exports: [InstallProgressPipe, TrustPipe, FilterPackagesPipe],
})
export class MarketplacePipesModule {}

View File

@@ -2,16 +2,31 @@
* Public API Surface of @start9labs/marketplace * Public API Surface of @start9labs/marketplace
*/ */
export * from './pipes/install-progress.pipe' export * from './pages/list/categories/categories.component'
export * from './pipes/trust.pipe' export * from './pages/list/categories/categories.module'
export * from './pipes/marketplace-pipes.module' export * from './pages/list/item/item.component'
export * from './pages/list/item/item.module'
export * from './pages/list/search/search.component'
export * from './pages/list/search/search.module'
export * from './pages/list/skeleton/skeleton.component'
export * from './pages/list/skeleton/skeleton.module'
export * from './pages/release-notes/release-notes.component'
export * from './pages/release-notes/release-notes.module'
export * from './pages/show/about/about.component'
export * from './pages/show/about/about.module'
export * from './pages/show/additional/additional.component'
export * from './pages/show/additional/additional.module'
export * from './pages/show/dependencies/dependencies.component'
export * from './pages/show/dependencies/dependencies.module'
export * from './pages/show/package/package.component'
export * from './pages/show/package/package.module'
export * from './pipes/filter-packages.pipe'
export * from './services/marketplace.service' export * from './services/marketplace.service'
export * from './types/local-pkg' export * from './types/dependency'
export * from './types/marketplace' export * from './types/marketplace'
export * from './types/marketplace-data' export * from './types/marketplace-data'
export * from './types/marketplace-manifest' export * from './types/marketplace-manifest'
export * from './types/marketplace-pkg' export * from './types/marketplace-pkg'
export * from './utils/spread-progress'

View File

@@ -13,5 +13,7 @@ export abstract class AbstractMarketplaceService {
abstract getPackages(): Observable<MarketplacePkg[]> abstract getPackages(): Observable<MarketplacePkg[]>
abstract getPackageMarkdown(type: string, pkgId: string): Observable<string>
abstract getPackage(id: string, version: string): Observable<MarketplacePkg> abstract getPackage(id: string, version: string): Observable<MarketplacePkg>
} }

View File

@@ -0,0 +1,17 @@
export interface Dependency<T> {
version: string
requirement:
| {
type: 'opt-in'
how: string
}
| {
type: 'opt-out'
how: string
}
| {
type: 'required'
}
description: string | null
config: T
}

View File

@@ -1,8 +0,0 @@
import { PackageState } from '@start9labs/shared'
import { MarketplaceManifest } from './marketplace-manifest'
export interface LocalPkg {
state: PackageState
manifest: MarketplaceManifest
}

View File

@@ -1,6 +1,8 @@
import { Url } from '@start9labs/shared' import { Url } from '@start9labs/shared'
export interface MarketplaceManifest { import { Dependency } from './dependency'
export interface MarketplaceManifest<T = unknown> {
id: string id: string
title: string title: string
version: string version: string
@@ -22,4 +24,5 @@ export interface MarketplaceManifest {
start: string | null start: string | null
stop: string | null stop: string | null
} }
dependencies: Record<string, Dependency<T>>
} }

View File

@@ -1,5 +0,0 @@
import { LocalPkg } from '../types/local-pkg'
export function spreadProgress(pkg: LocalPkg) {
pkg['install-progress'] = { ...pkg['install-progress'] }
}

View File

@@ -0,0 +1,29 @@
<ion-header>
<ion-toolbar>
<ion-title>{{ title | titlecase }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="dismiss()">
<ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item *ngIf="error$ | async as error">
<ion-label>
<ion-text safeLinks color="danger">{{ error }}</ion-text>
</ion-label>
</ion-item>
<div
*ngIf="content$ | async as result; else loading"
safeLinks
class="content-padding"
[innerHTML]="result | markdown"
></div>
<ng-template #loading>
<text-spinner [text]="'Loading ' + title | titlecase"></text-spinner>
</ng-template>
</ion-content>

View File

@@ -0,0 +1,47 @@
import { Component, Input, Optional } from '@angular/core'
import { ModalController } from '@ionic/angular'
import { getErrorMessage } from '../../services/error-toast.service'
import { AbstractApiService } from '../../services/api.service'
import { defer, from, isObservable, Observable, of } from 'rxjs'
import { catchError, ignoreElements, share } from 'rxjs/operators'
@Component({
selector: 'markdown',
templateUrl: './markdown.component.html',
styleUrls: ['./markdown.component.scss'],
})
export class MarkdownComponent {
@Input() contentUrl?: string
@Input() content?: string | Observable<string>
@Input() title = ''
private readonly data$ = defer(() => this.contentObservable).pipe(share())
readonly error$ = this.data$.pipe(
ignoreElements(),
catchError(e => of(getErrorMessage(e))),
)
readonly content$ = this.data$.pipe(catchError(() => of([])))
constructor(
private readonly modalCtrl: ModalController,
@Optional()
private readonly embassyApi: AbstractApiService | null,
) {}
async dismiss() {
return this.modalCtrl.dismiss(true)
}
private get contentObservable() {
if (isObservable(this.content)) {
return this.content
}
return this.contentUrl
? from(this.embassyApi?.getStatic(this.contentUrl))
: of(this.content)
}
}

View File

@@ -0,0 +1,21 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { MarkdownPipeModule } from '../../pipes/markdown/markdown.module'
import { SafeLinksModule } from '../../directives/safe-links/safe-links.module'
import { TextSpinnerComponentModule } from '../text-spinner/text-spinner.component.module'
import { MarkdownComponent } from './markdown.component'
@NgModule({
declarations: [MarkdownComponent],
imports: [
CommonModule,
IonicModule,
MarkdownPipeModule,
TextSpinnerComponentModule,
SafeLinksModule,
],
exports: [MarkdownComponent],
})
export class MarkdownModule {}

View File

@@ -0,0 +1,27 @@
import { AfterViewInit, Directive, ElementRef, Inject } from '@angular/core'
import { DOCUMENT } from '@angular/common'
// TODO: Refactor to use `MutationObserver` so it works with dynamic content
@Directive({
selector: '[safeLinks]',
})
export class SafeLinksDirective implements AfterViewInit {
constructor(
@Inject(DOCUMENT) private readonly document: Document,
private readonly elementRef: ElementRef<HTMLElement>,
) {}
ngAfterViewInit() {
Array.from(this.document.links)
.filter(
link =>
link.hostname !== this.document.location.hostname &&
this.elementRef.nativeElement.contains(link),
)
.forEach(link => {
link.target = '_blank'
link.setAttribute('rel', 'noreferrer')
link.classList.add('externalLink')
})
}
}

View File

@@ -0,0 +1,8 @@
import { NgModule } from '@angular/core'
import { SafeLinksDirective } from './safe-links.directive'
@NgModule({
declarations: [SafeLinksDirective],
exports: [SafeLinksDirective],
})
export class SafeLinksModule {}

View File

@@ -1,9 +1,10 @@
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { IncludesPipe } from './includes.pipe' import { IncludesPipe } from './includes.pipe'
import { EmptyPipe } from './empty.pipe' import { EmptyPipe } from './empty.pipe'
import { TrustUrlPipe } from './trust.pipe'
@NgModule({ @NgModule({
declarations: [IncludesPipe, EmptyPipe], declarations: [IncludesPipe, EmptyPipe, TrustUrlPipe],
exports: [IncludesPipe, EmptyPipe], exports: [IncludesPipe, EmptyPipe, TrustUrlPipe],
}) })
export class SharedPipesModule {} export class SharedPipesModule {}

View File

@@ -2,9 +2,9 @@ import { Pipe, PipeTransform } from '@angular/core'
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser' import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'
@Pipe({ @Pipe({
name: 'trust', name: 'trustUrl',
}) })
export class TrustPipe implements PipeTransform { export class TrustUrlPipe implements PipeTransform {
constructor(public readonly sanitizer: DomSanitizer) {} constructor(public readonly sanitizer: DomSanitizer) {}
transform(base64Icon: string): SafeResourceUrl { transform(base64Icon: string): SafeResourceUrl {

View File

@@ -2,11 +2,15 @@
* Public API Surface of @start9labs/shared * Public API Surface of @start9labs/shared
*/ */
export * from './components/markdown/markdown.component'
export * from './components/markdown/markdown.module'
export * from './components/text-spinner/text-spinner.component.module' export * from './components/text-spinner/text-spinner.component.module'
export * from './components/text-spinner/text-spinner.component' export * from './components/text-spinner/text-spinner.component'
export * from './directives/element/element.directive' export * from './directives/element/element.directive'
export * from './directives/element/element.module' export * from './directives/element/element.module'
export * from './directives/safe-links/safe-links.directive'
export * from './directives/safe-links/safe-links.module'
export * from './pipes/emver/emver.module' export * from './pipes/emver/emver.module'
export * from './pipes/emver/emver.pipe' export * from './pipes/emver/emver.pipe'
@@ -15,19 +19,17 @@ export * from './pipes/markdown/markdown.pipe'
export * from './pipes/shared/shared.module' export * from './pipes/shared/shared.module'
export * from './pipes/shared/empty.pipe' export * from './pipes/shared/empty.pipe'
export * from './pipes/shared/includes.pipe' export * from './pipes/shared/includes.pipe'
export * from './pipes/shared/trust.pipe'
export * from './pipes/unit-conversion/unit-conversion.module' export * from './pipes/unit-conversion/unit-conversion.module'
export * from './pipes/unit-conversion/unit-conversion.pipe' export * from './pipes/unit-conversion/unit-conversion.pipe'
export * from './services/api.service'
export * from './services/destroy.service' export * from './services/destroy.service'
export * from './services/emver.service' export * from './services/emver.service'
export * from './services/error-toast.service' export * from './services/error-toast.service'
export * from './types/dependent-info'
export * from './types/install-progress'
export * from './types/package-state'
export * from './types/progress-data'
export * from './types/url' export * from './types/url'
export * from './types/workspace-config' export * from './types/workspace-config'
export * from './util/misc.util' export * from './util/misc.util'
export * from './util/package-loading-progress' export * from './util/unused'

View File

@@ -0,0 +1,4 @@
export abstract class AbstractApiService {
// for getting static files: ex icons, instructions, licenses
abstract getStatic(url: string): Promise<string>
}

View File

@@ -1,56 +1,3 @@
import { OperatorFunction } from 'rxjs'
import { map } from 'rxjs/operators'
export function trace<T>(t: T): T {
console.log(`TRACE`, t)
return t
}
// curried description. This allows e.g somePromise.thentraceDesc('my result'))
export function traceDesc<T>(description: string): (t: T) => T {
return t => {
console.log(`TRACE`, description, t)
return t
}
}
// for use in observables. This allows e.g. someObservable.pipe(traceM('my result'))
// the practical equivalent of `tap(t => console.log(t, description))`
export function traceWheel<T>(description?: string): OperatorFunction<T, T> {
return description ? map(traceDesc(description)) : map(trace)
}
export function traceThrowDesc<T>(description: string, t: T | undefined): T {
if (!t) throw new Error(description)
return t
}
export function inMs(
count: number,
unit: 'days' | 'hours' | 'minutes' | 'seconds',
) {
switch (unit) {
case 'seconds':
return count * 1000
case 'minutes':
return inMs(count * 60, 'seconds')
case 'hours':
return inMs(count * 60, 'minutes')
case 'days':
return inMs(count * 24, 'hours')
}
}
// arr1 - arr2
export function diff<T>(arr1: T[], arr2: T[]): T[] {
return arr1.filter(x => !arr2.includes(x))
}
// arr1 & arr2
export function both<T>(arr1: T[], arr2: T[]): T[] {
return arr1.filter(x => arr2.includes(x))
}
export function isObject(val: any): boolean { export function isObject(val: any): boolean {
return val && typeof val === 'object' && !Array.isArray(val) return val && typeof val === 'object' && !Array.isArray(val)
} }
@@ -63,79 +10,6 @@ export function pauseFor(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms)) return new Promise(resolve => setTimeout(resolve, ms))
} }
export function toObject<T>(t: T[], map: (t0: T) => string): Record<string, T> {
return t.reduce((acc, next) => {
acc[map(next)] = next
return acc
}, {} as Record<string, T>)
}
export function update<T>(
t: Record<string, T>,
u: Record<string, T>,
): Record<string, T> {
return { ...t, ...u }
}
export function deepCloneUnknown<T>(value: T): T {
if (typeof value !== 'object' || value === null) {
return value
}
if (Array.isArray(value)) {
return deepCloneArray(value)
}
return deepCloneObject(value)
}
export function deepCloneObject<T>(source: T) {
const result = {}
Object.keys(source).forEach(key => {
const value = source[key]
result[key] = deepCloneUnknown(value)
}, {})
return result as T
}
export function deepCloneArray(collection: any) {
return collection.map(value => {
return deepCloneUnknown(value)
})
}
export function partitionArray<T>(
ts: T[],
condition: (t: T) => boolean,
): [T[], T[]] {
const yes = [] as T[]
const no = [] as T[]
ts.forEach(t => {
if (condition(t)) {
yes.push(t)
} else {
no.push(t)
}
})
return [yes, no]
}
export function uniqueBy<T>(
ts: T[],
uniqueBy: (t: T) => string,
prioritize: (t1: T, t2: T) => T,
) {
return Object.values(
ts.reduce((acc, next) => {
const previousValue = acc[uniqueBy(next)]
if (previousValue) {
acc[uniqueBy(next)] = prioritize(acc[uniqueBy(next)], previousValue)
} else {
acc[uniqueBy(next)] = previousValue
}
return acc
}, {}),
)
}
export function capitalizeFirstLetter(string: string): string { export function capitalizeFirstLetter(string: string): string {
return string.charAt(0).toUpperCase() + string.slice(1) return string.charAt(0).toUpperCase() + string.slice(1)
} }

View File

@@ -0,0 +1,130 @@
import { OperatorFunction } from 'rxjs'
import { map } from 'rxjs/operators'
/**
* These utils are not used anywhere
* They are candidates for removal
*/
export function trace<T>(t: T): T {
console.log(`TRACE`, t)
return t
}
// curried description. This allows e.g somePromise.thentraceDesc('my result'))
export function traceDesc<T>(description: string): (t: T) => T {
return t => {
console.log(`TRACE`, description, t)
return t
}
}
// for use in observables. This allows e.g. someObservable.pipe(traceM('my result'))
// the practical equivalent of `tap(t => console.log(t, description))`
export function traceWheel<T>(description?: string): OperatorFunction<T, T> {
return description ? map(traceDesc(description)) : map(trace)
}
export function traceThrowDesc<T>(description: string, t: T | undefined): T {
if (!t) throw new Error(description)
return t
}
export function inMs(
count: number,
unit: 'days' | 'hours' | 'minutes' | 'seconds',
) {
switch (unit) {
case 'seconds':
return count * 1000
case 'minutes':
return inMs(count * 60, 'seconds')
case 'hours':
return inMs(count * 60, 'minutes')
case 'days':
return inMs(count * 24, 'hours')
}
}
// arr1 - arr2
export function diff<T>(arr1: T[], arr2: T[]): T[] {
return arr1.filter(x => !arr2.includes(x))
}
// arr1 & arr2
export function both<T>(arr1: T[], arr2: T[]): T[] {
return arr1.filter(x => arr2.includes(x))
}
export function toObject<T>(t: T[], map: (t0: T) => string): Record<string, T> {
return t.reduce((acc, next) => {
acc[map(next)] = next
return acc
}, {} as Record<string, T>)
}
export function deepCloneUnknown<T>(value: T): T {
if (typeof value !== 'object' || value === null) {
return value
}
if (Array.isArray(value)) {
return deepCloneArray(value)
}
return deepCloneObject(value)
}
export function deepCloneObject<T>(source: T) {
const result = {}
Object.keys(source).forEach(key => {
const value = source[key]
result[key] = deepCloneUnknown(value)
}, {})
return result as T
}
export function deepCloneArray(collection: any) {
return collection.map(value => {
return deepCloneUnknown(value)
})
}
export function partitionArray<T>(
ts: T[],
condition: (t: T) => boolean,
): [T[], T[]] {
const yes = [] as T[]
const no = [] as T[]
ts.forEach(t => {
if (condition(t)) {
yes.push(t)
} else {
no.push(t)
}
})
return [yes, no]
}
export function update<T>(
t: Record<string, T>,
u: Record<string, T>,
): Record<string, T> {
return { ...t, ...u }
}
export function uniqueBy<T>(
ts: T[],
uniqueBy: (t: T) => string,
prioritize: (t1: T, t2: T) => T,
) {
return Object.values(
ts.reduce((acc, next) => {
const previousValue = acc[uniqueBy(next)]
if (previousValue) {
acc[uniqueBy(next)] = prioritize(acc[uniqueBy(next)], previousValue)
} else {
acc[uniqueBy(next)] = previousValue
}
return acc
}, {}),
)
}

View File

@@ -12,7 +12,6 @@ import { PatchDbServiceFactory } from './services/patch-db/patch-db.factory'
import { ConfigService } from './services/config.service' import { ConfigService } from './services/config.service'
import { QrCodeModule } from 'ng-qrcode' import { QrCodeModule } from 'ng-qrcode'
import { OSWelcomePageModule } from './modals/os-welcome/os-welcome.module' import { OSWelcomePageModule } from './modals/os-welcome/os-welcome.module'
import { MarkdownPageModule } from './modals/markdown/markdown.module'
import { PatchDbService } from './services/patch-db/patch-db.service' import { PatchDbService } from './services/patch-db/patch-db.service'
import { LocalStorageBootstrap } from './services/patch-db/local-storage-bootstrap' import { LocalStorageBootstrap } from './services/patch-db/local-storage-bootstrap'
import { FormBuilder } from '@angular/forms' import { FormBuilder } from '@angular/forms'
@@ -22,7 +21,11 @@ import { GlobalErrorHandler } from './services/global-error-handler.service'
import { MockApiService } from './services/api/embassy-mock-api.service' import { MockApiService } from './services/api/embassy-mock-api.service'
import { LiveApiService } from './services/api/embassy-live-api.service' import { LiveApiService } from './services/api/embassy-live-api.service'
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor' import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'
import { SharedPipesModule, WorkspaceConfig } from '@start9labs/shared' import {
MarkdownModule,
SharedPipesModule,
WorkspaceConfig,
} from '@start9labs/shared'
import { MarketplaceModule } from './marketplace.module' import { MarketplaceModule } from './marketplace.module'
const { useMocks } = require('../../../../config.json') as WorkspaceConfig const { useMocks } = require('../../../../config.json') as WorkspaceConfig
@@ -45,7 +48,7 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig
}), }),
QrCodeModule, QrCodeModule,
OSWelcomePageModule, OSWelcomePageModule,
MarkdownPageModule, MarkdownModule,
GenericInputComponentModule, GenericInputComponentModule,
MonacoEditorModule, MonacoEditorModule,
SharedPipesModule, SharedPipesModule,

View File

@@ -10,10 +10,10 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
import { import {
ErrorToastService, ErrorToastService,
getErrorMessage, getErrorMessage,
DependentInfo,
isEmptyObject, isEmptyObject,
isObject, isObject,
} from '@start9labs/shared' } from '@start9labs/shared'
import { DependentInfo } from 'src/app/types/dependent-info'
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component' import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards' import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
import { ConfigSpec } from 'src/app/pkg-config/config-types' import { ConfigSpec } from 'src/app/pkg-config/config-types'

View File

@@ -1,29 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-title>{{ title | titlecase }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="dismiss()">
<ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<text-spinner *ngIf="loading; else loaded" [text]="'Loading ' + title | titlecase"></text-spinner>
<ng-template #loaded>
<ion-item *ngIf="loadingError; else noError">
<ion-label>
<ion-text color="danger">
{{ loadingError }}
</ion-text>
</ion-label>
</ion-item>
<ng-template #noError>
<div class="content-padding" [innerHTML]="content | markdown"></div>
</ng-template>
</ng-template>
</ion-content>

View File

@@ -1,47 +0,0 @@
import { Component, Input } from '@angular/core'
import { ModalController, IonicSafeString } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { getErrorMessage, pauseFor } from '@start9labs/shared'
@Component({
selector: 'markdown',
templateUrl: './markdown.page.html',
styleUrls: ['./markdown.page.scss'],
})
export class MarkdownPage {
@Input() contentUrl?: string
@Input() content?: string
@Input() title: string
loading = true
loadingError: string | IonicSafeString
constructor(
private readonly modalCtrl: ModalController,
private readonly embassyApi: ApiService,
) {}
async ngOnInit() {
try {
if (!this.content) {
this.content = await this.embassyApi.getStatic(this.contentUrl)
}
this.loading = false
await pauseFor(50)
const links = document.links
for (let i = 0, linksLength = links.length; i < linksLength; i++) {
if (links[i].hostname != window.location.hostname) {
links[i].target = '_blank'
links[i].setAttribute('rel', 'noreferrer')
links[i].className += ' externalLink'
}
}
} catch (e) {
this.loadingError = getErrorMessage(e)
this.loading = false
}
}
async dismiss() {
return this.modalCtrl.dismiss(true)
}
}

View File

@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'
import { NavController } from '@ionic/angular' import { NavController } from '@ionic/angular'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { PackageState } from '@start9labs/shared' import { PackageState } from 'src/app/types/package-state'
import { import {
PackageStatus, PackageStatus,
PrimaryStatus, PrimaryStatus,

View File

@@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { InstallProgress, ProgressData } from '@start9labs/shared' import { InstallProgress } from 'src/app/types/install-progress'
import { ProgressData } from 'src/app/types/progress-data'
@Component({ @Component({
selector: 'app-show-progress', selector: 'app-show-progress',

View File

@@ -10,11 +10,8 @@ import {
PackageDataEntry, PackageDataEntry,
Status, Status,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { import { ErrorToastService } from '@start9labs/shared'
isEmptyObject, import { PackageState } from 'src/app/types/package-state'
ErrorToastService,
PackageState,
} from '@start9labs/shared'
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component' import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
import { import {
AlertController, AlertController,

View File

@@ -1,6 +1,7 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { ProgressData, packageLoadingProgress } from '@start9labs/shared' import { ProgressData } from 'src/app/types/progress-data'
import { packageLoadingProgress } from 'src/app/util/package-loading-progress'
@Pipe({ @Pipe({
name: 'installState', name: 'installState',

View File

@@ -2,8 +2,8 @@ import { Inject, Pipe, PipeTransform } from '@angular/core'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { DOCUMENT } from '@angular/common' import { DOCUMENT } from '@angular/common'
import { AlertController, ModalController, NavController } from '@ionic/angular' import { AlertController, ModalController, NavController } from '@ionic/angular'
import { MarkdownComponent } from '@start9labs/shared'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { MarkdownPage } from 'src/app/modals/markdown/markdown.page'
import { ModalService } from 'src/app/services/modal.service' import { ModalService } from 'src/app/services/modal.service'
export interface Button { export interface Button {
@@ -105,7 +105,7 @@ export class ToButtonsPipe implements PipeTransform {
title: 'Instructions', title: 'Instructions',
contentUrl: pkg['static-files']['instructions'], contentUrl: pkg['static-files']['instructions'],
}, },
component: MarkdownPage, component: MarkdownComponent,
}) })
await modal.present() await modal.present()

View File

@@ -8,7 +8,8 @@ import {
DependencyErrorType, DependencyErrorType,
PackageDataEntry, PackageDataEntry,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { DependentInfo, exists } from '@start9labs/shared' import { exists } from '@start9labs/shared'
import { DependentInfo } from 'src/app/types/dependent-info'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { ModalService } from 'src/app/services/modal.service' import { ModalService } from 'src/app/services/modal.service'

View File

@@ -5,9 +5,8 @@ import * as yaml from 'js-yaml'
import { take } from 'rxjs/operators' import { take } from 'rxjs/operators'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { debounce } from '../../../../../../shared/src/util/misc.util'
import { GenericFormPage } from '../../../modals/generic-form/generic-form.page' import { GenericFormPage } from '../../../modals/generic-form/generic-form.page'
import { ErrorToastService } from '@start9labs/shared' import { debounce, ErrorToastService } from '@start9labs/shared'
@Component({ @Component({
selector: 'dev-config', selector: 'dev-config',

View File

@@ -3,10 +3,12 @@ import { ActivatedRoute } from '@angular/router'
import { ModalController } from '@ionic/angular' import { ModalController } from '@ionic/angular'
import { take } from 'rxjs/operators' import { take } from 'rxjs/operators'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ErrorToastService } from '@start9labs/shared' import {
debounce,
ErrorToastService,
MarkdownComponent,
} from '@start9labs/shared'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { debounce } from '../../../../../../shared/src/util/misc.util'
import { MarkdownPage } from '../../../modals/markdown/markdown.page'
@Component({ @Component({
selector: 'dev-instructions', selector: 'dev-instructions',
@@ -44,7 +46,7 @@ export class DevInstructionsPage {
title: 'Instructions Sample', title: 'Instructions Sample',
content: this.code, content: this.code,
}, },
component: MarkdownPage, component: MarkdownComponent,
}) })
await modal.present() await modal.present()

View File

@@ -1,62 +1,34 @@
<h1 class="heading ion-text-center">{{ name }}</h1> <h1 class="heading ion-text-center">{{ name }}</h1>
<ion-grid class="grid"> <marketplace-search [(query)]="query"></marketplace-search>
<ion-row>
<ion-col sizeSm="8" offset-sm="2">
<ion-toolbar color="transparent">
<ion-searchbar
enterkeyhint="search"
color="dark"
debounce="250"
[(ngModel)]="query"
></ion-searchbar>
</ion-toolbar>
</ion-col>
</ion-row>
</ion-grid>
<ng-container *ngIf="pkgs && categories; else loading"> <ng-container *ngIf="pkgs && categories; else loading">
<div class="hidden-scrollbar ion-text-center"> <marketplace-categories
<ion-button [categories]="categories"
*ngFor="let cat of categories" [category]="category"
fill="clear" (categoryChange)="onCategoryChange($event)"
class="category" ></marketplace-categories>
[class.category_selected]="isSelected(cat)"
(click)="switchCategory(cat)"
>
{{ cat }}
</ion-button>
</div>
<div class="divider"></div> <div class="divider"></div>
<ion-grid *ngIf="pkgs | filterPackages: localPkgs:query:category as filtered"> <ion-grid *ngIf="pkgs | filterPackages: query:category:localPkgs as filtered">
<div *ngIf="!filtered.length && category === 'updates'" class="ion-padding"> <div *ngIf="!filtered.length && category === 'updates'" class="ion-padding">
<h1>All services are up to date!</h1> <h1>All services are up to date!</h1>
</div> </div>
<ion-row> <ion-row>
<ion-col *ngFor="let pkg of filtered" sizeXs="12" sizeSm="12" sizeMd="6"> <ion-col *ngFor="let pkg of filtered" sizeXs="12" sizeSm="12" sizeMd="6">
<ion-item [routerLink]="['/marketplace', pkg.manifest.id]"> <marketplace-item [pkg]="pkg">
<ion-thumbnail slot="start"> <marketplace-status
<img alt="" [src]="'data:image/png;base64,' + pkg.icon | trust" /> class="status"
</ion-thumbnail> [pkg]="localPkgs[pkg.manifest.id]"
<ion-label> ></marketplace-status>
<h2 class="pkg-title"> </marketplace-item>
{{ pkg.manifest.title }}
</h2>
<h3>{{ pkg.manifest.description.short }}</h3>
<marketplace-status
class="status"
[pkg]="localPkgs[pkg.manifest.id]"
></marketplace-status>
</ion-label>
</ion-item>
</ion-col> </ion-col>
</ion-row> </ion-row>
</ion-grid> </ion-grid>
</ng-container> </ng-container>
<ng-template #loading> <ng-template #loading>
<marketplace-list-skeleton></marketplace-list-skeleton> <marketplace-skeleton></marketplace-skeleton>
</ng-template> </ng-template>

View File

@@ -12,35 +12,6 @@
text-align: center; text-align: center;
} }
.grid {
padding-bottom: 32px;
}
.pkg-title {
font-family: 'Montserrat', sans-serif;
font-weight: bold;
}
.eos-item {
--border-style: none;
--background: linear-gradient(
45deg,
var(--ion-color-dark) -380%,
var(--ion-color-medium) 100%
);
}
.category {
font-weight: 300;
color: var(--ion-color-dark-shade);
&_selected {
font-weight: bold;
font-size: 17px;
color: var(--color);
}
}
.status { .status {
font-size: 14px; font-size: 14px;
} }

View File

@@ -1,5 +1,7 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { LocalPkg, MarketplacePkg } from '@start9labs/marketplace' import { MarketplacePkg } from '@start9labs/marketplace'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
@Component({ @Component({
selector: 'marketplace-list-content', selector: 'marketplace-list-content',
@@ -12,7 +14,7 @@ export class MarketplaceListContentComponent {
pkgs: MarketplacePkg[] | null = null pkgs: MarketplacePkg[] | null = null
@Input() @Input()
localPkgs: Record<string, LocalPkg> = {} localPkgs: Record<string, PackageDataEntry> = {}
@Input() @Input()
categories: Set<string> | null = null categories: Set<string> | null = null
@@ -23,11 +25,7 @@ export class MarketplaceListContentComponent {
category = 'featured' category = 'featured'
query = '' query = ''
isSelected(category: string) { onCategoryChange(category: string): void {
return category === this.category && !this.query
}
switchCategory(category: string): void {
this.category = category this.category = category
this.query = '' this.query = ''
} }

View File

@@ -1,7 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>

View File

@@ -1,8 +0,0 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
@Component({
selector: 'marketplace-list-header',
templateUrl: 'marketplace-list-header.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarketplaceListHeaderComponent {}

View File

@@ -1,8 +0,0 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
@Component({
selector: 'marketplace-list-skeleton',
templateUrl: 'marketplace-list-skeleton.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarketplaceListSkeletonComponent {}

View File

@@ -8,13 +8,17 @@ import {
EmverPipesModule, EmverPipesModule,
TextSpinnerComponentModule, TextSpinnerComponentModule,
} from '@start9labs/shared' } from '@start9labs/shared'
import { MarketplacePipesModule } from '@start9labs/marketplace' import {
FilterPackagesPipeModule,
CategoriesModule,
ItemModule,
SearchModule,
SkeletonModule,
} from '@start9labs/marketplace'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module' import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module'
import { MarketplaceListPage } from './marketplace-list.page' import { MarketplaceListPage } from './marketplace-list.page'
import { MarketplaceListHeaderComponent } from './marketplace-list-header/marketplace-list-header.component'
import { MarketplaceListSkeletonComponent } from './marketplace-list-skeleton/marketplace-list-skeleton.component'
import { MarketplaceListContentComponent } from './marketplace-list-content/marketplace-list-content.component' import { MarketplaceListContentComponent } from './marketplace-list-content/marketplace-list-content.component'
const routes: Routes = [ const routes: Routes = [
@@ -33,21 +37,15 @@ const routes: Routes = [
TextSpinnerComponentModule, TextSpinnerComponentModule,
SharedPipesModule, SharedPipesModule,
EmverPipesModule, EmverPipesModule,
MarketplacePipesModule, FilterPackagesPipeModule,
MarketplaceStatusModule, MarketplaceStatusModule,
BadgeMenuComponentModule, BadgeMenuComponentModule,
ItemModule,
CategoriesModule,
SearchModule,
SkeletonModule,
], ],
declarations: [ declarations: [MarketplaceListPage, MarketplaceListContentComponent],
MarketplaceListPage, exports: [MarketplaceListPage, MarketplaceListContentComponent],
MarketplaceListHeaderComponent,
MarketplaceListContentComponent,
MarketplaceListSkeletonComponent,
],
exports: [
MarketplaceListPage,
MarketplaceListHeaderComponent,
MarketplaceListContentComponent,
MarketplaceListSkeletonComponent,
],
}) })
export class MarketplaceListPageModule {} export class MarketplaceListPageModule {}

View File

@@ -1,4 +1,10 @@
<marketplace-list-header></marketplace-list-header> <ion-header>
<ion-toolbar>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding"> <ion-content class="ion-padding">
<marketplace-list-content <marketplace-list-content

View File

@@ -4,20 +4,20 @@ import { filter, first, map, startWith, switchMapTo, tap } from 'rxjs/operators'
import { exists, isEmptyObject } from '@start9labs/shared' import { exists, isEmptyObject } from '@start9labs/shared'
import { import {
AbstractMarketplaceService, AbstractMarketplaceService,
LocalPkg,
MarketplacePkg, MarketplacePkg,
spreadProgress,
} from '@start9labs/marketplace' } from '@start9labs/marketplace'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { spreadProgress } from '../utils/spread-progress'
@Component({ @Component({
selector: 'marketplace-list', selector: 'marketplace-list',
templateUrl: './marketplace-list.page.html', templateUrl: './marketplace-list.page.html',
}) })
export class MarketplaceListPage { export class MarketplaceListPage {
readonly localPkgs$: Observable<Record<string, LocalPkg>> = defer(() => readonly localPkgs$: Observable<Record<string, PackageDataEntry>> = defer(
this.patch.watch$('package-data'), () => this.patch.watch$('package-data'),
).pipe( ).pipe(
filter(data => exists(data) && !isEmptyObject(data)), filter(data => exists(data) && !isEmptyObject(data)),
tap(pkgs => Object.values(pkgs).forEach(spreadProgress)), tap(pkgs => Object.values(pkgs).forEach(spreadProgress)),

View File

@@ -1,13 +0,0 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplacePkg } from '@start9labs/marketplace'
@Component({
selector: 'marketplace-show-about',
templateUrl: 'marketplace-show-about.component.html',
styleUrls: ['marketplace-show-about.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarketplaceShowAboutComponent {
@Input()
pkg: MarketplacePkg
}

View File

@@ -3,11 +3,14 @@ import { AlertController, ModalController, NavController } from '@ionic/angular'
import { import {
AbstractMarketplaceService, AbstractMarketplaceService,
MarketplacePkg, MarketplacePkg,
LocalPkg,
} from '@start9labs/marketplace' } from '@start9labs/marketplace'
import { pauseFor, PackageState } from '@start9labs/shared' import { pauseFor } from '@start9labs/shared'
import { Manifest } from 'src/app/services/patch-db/data-model' import { PackageState } from 'src/app/types/package-state'
import {
Manifest,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component' import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards' import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
import { LocalStorageService } from 'src/app/services/local-storage.service' import { LocalStorageService } from 'src/app/services/local-storage.service'
@@ -22,7 +25,7 @@ export class MarketplaceShowControlsComponent {
pkg: MarketplacePkg pkg: MarketplacePkg
@Input() @Input()
localPkg: LocalPkg localPkg: PackageDataEntry
readonly PackageState = PackageState readonly PackageState = PackageState

View File

@@ -1,32 +0,0 @@
<ng-container *ngIf="!(dependencies | empty)">
<ion-item-divider>Dependencies</ion-item-divider>
<ion-grid>
<ion-row>
<ion-col
*ngFor="let dep of dependencies | keyvalue"
sizeSm="12"
sizeMd="6"
>
<ion-item [routerLink]="['/marketplace', dep.key]">
<ion-thumbnail slot="start">
<img alt="" [src]="getImg(dep.key) | trust" />
</ion-thumbnail>
<ion-label>
<h2>
{{ pkg['dependency-metadata'][dep.key].title }}
<ng-container [ngSwitch]="dep.value.requirement.type">
<span *ngSwitchCase="'required'">(required)</span>
<span *ngSwitchCase="'opt-out'">(required by default)</span>
<span *ngSwitchCase="'opt-in'">(optional)</span>
</ng-container>
</h2>
<p>
<small>{{ dep.value.version | displayEmver }}</small>
</p>
<p>{{ dep.value.description }}</p>
</ion-label>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>
</ng-container>

View File

@@ -1,23 +0,0 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplacePkg } from '@start9labs/marketplace'
import { DependencyInfo, Manifest } from 'src/app/services/patch-db/data-model'
@Component({
selector: 'marketplace-show-dependencies',
templateUrl: 'marketplace-show-dependencies.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarketplaceShowDependenciesComponent {
@Input()
pkg: MarketplacePkg
get dependencies(): DependencyInfo {
// TODO: Fix type
return (this.pkg.manifest as Manifest).dependencies
}
getImg(key: string): string {
return 'data:image/png;base64,' + this.pkg['dependency-metadata'][key].icon
}
}

View File

@@ -1,5 +1,5 @@
<!-- auto-config --> <!-- auto-config -->
<ion-item lines="none" *ngIf="dependentInfo" class="rec-item"> <ion-item *ngIf="dependentInfo" lines="none" class="rec-item">
<ion-label> <ion-label>
<h2 class="heading"> <h2 class="heading">
<ion-text class="title"> <ion-text class="title">

View File

@@ -1,6 +1,12 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import {
ChangeDetectionStrategy,
Component,
Inject,
Input,
} from '@angular/core'
import { MarketplacePkg } from '@start9labs/marketplace' import { MarketplacePkg } from '@start9labs/marketplace'
import { DependentInfo } from '@start9labs/shared' import { DOCUMENT } from '@angular/common'
import { DependentInfo } from 'src/app/types/dependent-info'
@Component({ @Component({
selector: 'marketplace-show-dependent', selector: 'marketplace-show-dependent',
@@ -12,7 +18,10 @@ export class MarketplaceShowDependentComponent {
@Input() @Input()
pkg: MarketplacePkg pkg: MarketplacePkg
readonly dependentInfo?: DependentInfo = history.state?.dependentInfo readonly dependentInfo?: DependentInfo =
this.document.defaultView?.history.state?.dependentInfo
constructor(@Inject(DOCUMENT) private readonly document: Document) {}
get title(): string { get title(): string {
return this.pkg?.manifest.title || '' return this.pkg?.manifest.title || ''

View File

@@ -8,16 +8,18 @@ import {
MarkdownPipeModule, MarkdownPipeModule,
TextSpinnerComponentModule, TextSpinnerComponentModule,
} from '@start9labs/shared' } from '@start9labs/shared'
import { MarketplacePipesModule } from '@start9labs/marketplace' import {
PackageModule,
AboutModule,
AdditionalModule,
DependenciesModule,
} from '@start9labs/marketplace'
import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module' import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module'
import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module' import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module'
import { MarketplaceShowPage } from './marketplace-show.page' import { MarketplaceShowPage } from './marketplace-show.page'
import { MarketplaceShowHeaderComponent } from './marketplace-show-header/marketplace-show-header.component' import { MarketplaceShowHeaderComponent } from './marketplace-show-header/marketplace-show-header.component'
import { MarketplaceShowDependentComponent } from './marketplace-show-dependent/marketplace-show-dependent.component' import { MarketplaceShowDependentComponent } from './marketplace-show-dependent/marketplace-show-dependent.component'
import { MarketplaceShowDependenciesComponent } from './marketplace-show-dependencies/marketplace-show-dependencies.component'
import { MarketplaceShowAdditionalComponent } from './marketplace-show-additional/marketplace-show-additional.component'
import { MarketplaceShowAboutComponent } from './marketplace-show-about/marketplace-show-about.component'
import { MarketplaceShowControlsComponent } from './marketplace-show-controls/marketplace-show-controls.component' import { MarketplaceShowControlsComponent } from './marketplace-show-controls/marketplace-show-controls.component'
const routes: Routes = [ const routes: Routes = [
@@ -36,27 +38,24 @@ const routes: Routes = [
SharedPipesModule, SharedPipesModule,
EmverPipesModule, EmverPipesModule,
MarkdownPipeModule, MarkdownPipeModule,
MarketplacePipesModule,
MarketplaceStatusModule, MarketplaceStatusModule,
InstallWizardComponentModule, InstallWizardComponentModule,
PackageModule,
AboutModule,
DependenciesModule,
AdditionalModule,
], ],
declarations: [ declarations: [
MarketplaceShowPage, MarketplaceShowPage,
MarketplaceShowHeaderComponent, MarketplaceShowHeaderComponent,
MarketplaceShowControlsComponent, MarketplaceShowControlsComponent,
MarketplaceShowDependentComponent, MarketplaceShowDependentComponent,
MarketplaceShowAboutComponent,
MarketplaceShowDependenciesComponent,
MarketplaceShowAdditionalComponent,
], ],
exports: [ exports: [
MarketplaceShowPage, MarketplaceShowPage,
MarketplaceShowHeaderComponent, MarketplaceShowHeaderComponent,
MarketplaceShowControlsComponent, MarketplaceShowControlsComponent,
MarketplaceShowDependentComponent, MarketplaceShowDependentComponent,
MarketplaceShowAboutComponent,
MarketplaceShowDependenciesComponent,
MarketplaceShowAdditionalComponent,
], ],
}) })
export class MarketplaceShowPageModule {} export class MarketplaceShowPageModule {}

View File

@@ -2,72 +2,53 @@
<ion-content class="ion-padding"> <ion-content class="ion-padding">
<ng-container *ngIf="pkg$ | async as pkg else loading"> <ng-container *ngIf="pkg$ | async as pkg else loading">
<ion-grid> <ng-container *ngIf="!(pkg | empty)">
<ion-row> <marketplace-package [pkg]="pkg">
<ion-col sizeXs="12" sizeSm="12" sizeMd="9" sizeLg="9" sizeXl="9"> <marketplace-status
<div class="header"> class="status"
<img alt="" [src]="getIcon(pkg.icon) | trust" /> [pkg]="localPkg$ | async"
<div class="header-text"> ></marketplace-status>
<h1 class="header-title">{{ pkg.manifest.title }}</h1> <marketplace-show-controls
<p class="header-version"> slot="controls"
{{ pkg.manifest.version | displayEmver }} [pkg]="pkg"
</p> [localPkg]="localPkg$ | async"
<marketplace-status ></marketplace-show-controls>
class="header-status" <ion-row *ngIf="localPkg$ | async">
[pkg]="localPkg$ | async" <ion-col
></marketplace-status> sizeXl="3"
</div> sizeLg="3"
</div> sizeMd="3"
</ion-col> sizeSm="12"
<ion-col sizeXs="12"
sizeXl="3" class="ion-align-self-center"
sizeLg="3"
sizeMd="3"
sizeSm="12"
sizeXs="12"
class="ion-align-self-center"
>
<marketplace-show-controls
[pkg]="pkg"
[localPkg]="localPkg$ | async"
></marketplace-show-controls>
</ion-col>
</ion-row>
<ion-row *ngIf="localPkg$ | async">
<ion-col
sizeXl="3"
sizeLg="3"
sizeMd="3"
sizeSm="12"
sizeXs="12"
class="ion-align-self-center"
>
<ion-button
expand="block"
fill="outline"
color="primary"
[routerLink]="['/services', pkg.manifest.id]"
> >
View Service <ion-button
</ion-button> expand="block"
</ion-col> fill="outline"
</ion-row> color="primary"
</ion-grid> [routerLink]="['/services', pkg.manifest.id]"
>
View Service
</ion-button>
</ion-col>
</ion-row>
</marketplace-package>
<marketplace-show-dependent [pkg]="pkg"></marketplace-show-dependent> <marketplace-show-dependent [pkg]="pkg"></marketplace-show-dependent>
<ion-item-group> <ion-item-group>
<marketplace-show-about [pkg]="pkg"></marketplace-show-about> <marketplace-about [pkg]="pkg"></marketplace-about>
<marketplace-dependencies
*ngIf="!(pkg.manifest.dependencies | empty)"
[pkg]="pkg"
></marketplace-dependencies>
</ion-item-group>
<marketplace-show-dependencies <marketplace-additional
[pkg]="pkg" [pkg]="pkg"
></marketplace-show-dependencies> (version)="loadVersion$.next($event)"
</ion-item-group> ></marketplace-additional>
</ng-container>
<marketplace-show-additional
[pkg]="pkg"
(version)="loadVersion$.next($event)"
></marketplace-show-additional>
</ng-container> </ng-container>
<ng-template #loading> <ng-template #loading>

View File

@@ -1,30 +1,3 @@
.header { .status {
font-family: 'Montserrat', sans-serif; font-size: calc(16px + 1vw);
padding: 2%;
img {
min-width: 15%;
max-width: 18%;
}
.header-text {
margin-left: 5%;
display: inline-block;
vertical-align: top;
.header-title {
margin: 0 0 0 -2px;
font-size: calc(20px + 3vw);
}
.header-version {
padding: 4px 0 12px 0;
margin: 0;
font-size: calc(10px + 1vw);
}
.header-status {
font-size: calc(16px + 1vw);
}
}
} }

View File

@@ -2,21 +2,15 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { ErrorToastService } from '@start9labs/shared' import { ErrorToastService } from '@start9labs/shared'
import { import {
LocalPkg,
MarketplacePkg, MarketplacePkg,
AbstractMarketplaceService, AbstractMarketplaceService,
spreadProgress,
} from '@start9labs/marketplace' } from '@start9labs/marketplace'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { BehaviorSubject, defer, Observable, of } from 'rxjs' import { BehaviorSubject, defer, Observable, of } from 'rxjs'
import { import { catchError, filter, shareReplay, switchMap, tap } from 'rxjs/operators'
catchError,
filter, import { spreadProgress } from '../utils/spread-progress'
shareReplay,
startWith,
switchMap,
tap,
} from 'rxjs/operators'
@Component({ @Component({
selector: 'marketplace-show', selector: 'marketplace-show',
@@ -32,16 +26,14 @@ export class MarketplaceShowPage {
readonly localPkg$ = defer(() => readonly localPkg$ = defer(() =>
this.patch.watch$('package-data', this.pkgId), this.patch.watch$('package-data', this.pkgId),
).pipe( ).pipe(
filter<LocalPkg>(Boolean), filter<PackageDataEntry>(Boolean),
tap(spreadProgress), tap(spreadProgress),
shareReplay({ bufferSize: 1, refCount: true }), shareReplay({ bufferSize: 1, refCount: true }),
) )
readonly pkg$: Observable<MarketplacePkg> = this.loadVersion$.pipe( readonly pkg$: Observable<MarketplacePkg> = this.loadVersion$.pipe(
switchMap(version => switchMap(version =>
this.marketplaceService this.marketplaceService.getPackage(this.pkgId, version),
.getPackage(this.pkgId, version)
.pipe(startWith(null)),
), ),
// TODO: Better fallback // TODO: Better fallback
catchError(e => this.errToast.present(e) && of({} as MarketplacePkg)), catchError(e => this.errToast.present(e) && of({} as MarketplacePkg)),
@@ -57,16 +49,4 @@ export class MarketplaceShowPage {
getIcon(icon: string): string { getIcon(icon: string): string {
return `data:image/png;base64,${icon}` return `data:image/png;base64,${icon}`
} }
// async getPkg(version: string): Promise<void> {
// this.loading = true
// try {
// this.pkg = await this.marketplaceService.getPkg(this.pkgId, version)
// } catch (e) {
// this.errToast.present(e)
// } finally {
// await pauseFor(100)
// this.loading = false
// }
// }
} }

View File

@@ -1,5 +1,6 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
import { InstallProgress, packageLoadingProgress } from '@start9labs/shared' import { InstallProgress } from 'src/app/types/install-progress'
import { packageLoadingProgress } from 'src/app/util/package-loading-progress'
@Pipe({ @Pipe({
name: 'installProgress', name: 'installProgress',

View File

@@ -24,5 +24,5 @@
</div> </div>
</ng-container> </ng-container>
<ng-template #none> <ng-template #none>
<div>Not Installed</div> <ion-text style="color: var(--ion-color-step-450)">Not Installed</ion-text>
</ng-template> </ng-template>

View File

@@ -1,6 +1,6 @@
import { Component, Input } from '@angular/core' import { Component, Input } from '@angular/core'
import { LocalPkg } from '@start9labs/marketplace' import { PackageState } from 'src/app/types/package-state'
import { PackageState } from '@start9labs/shared' import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
@Component({ @Component({
selector: 'marketplace-status', selector: 'marketplace-status',
@@ -8,7 +8,7 @@ import { PackageState } from '@start9labs/shared'
}) })
export class MarketplaceStatusComponent { export class MarketplaceStatusComponent {
@Input() @Input()
pkg?: LocalPkg pkg?: PackageDataEntry
PackageState = PackageState PackageState = PackageState

View File

@@ -2,18 +2,13 @@ import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { EmverPipesModule } from '@start9labs/shared' import { EmverPipesModule } from '@start9labs/shared'
import { MarketplacePipesModule } from '@start9labs/marketplace'
import { InstallProgressPipe } from './install-progress.pipe'
import { MarketplaceStatusComponent } from './marketplace-status.component' import { MarketplaceStatusComponent } from './marketplace-status.component'
@NgModule({ @NgModule({
imports: [ imports: [CommonModule, IonicModule, EmverPipesModule],
CommonModule, declarations: [MarketplaceStatusComponent, InstallProgressPipe],
IonicModule,
EmverPipesModule,
MarketplacePipesModule,
],
declarations: [MarketplaceStatusComponent],
exports: [MarketplaceStatusComponent], exports: [MarketplaceStatusComponent],
}) })
export class MarketplaceStatusModule {} export class MarketplaceStatusModule {}

View File

@@ -1,8 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [defaultHref]="href"></ion-back-button>
</ion-buttons>
<ion-title>Release Notes</ion-title>
</ion-toolbar>
</ion-header>

View File

@@ -1,13 +0,0 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
@Component({
selector: 'release-notes-header',
templateUrl: 'release-notes-header.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReleaseNotesHeaderComponent {
readonly href = `/marketplace/${this.route.snapshot.paramMap.get('pkgId')}`
constructor(private readonly route: ActivatedRoute) {}
}

View File

@@ -1,17 +1,9 @@
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router' import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { import { ReleaseNotesModule } from '@start9labs/marketplace'
EmverPipesModule,
MarkdownPipeModule,
TextSpinnerComponentModule,
ElementModule,
} from '@start9labs/shared'
import { MarketplacePipesModule } from '@start9labs/marketplace'
import { ReleaseNotesPage } from './release-notes.page' import { ReleaseNotesPage } from './release-notes.page'
import { ReleaseNotesHeaderComponent } from './release-notes-header/release-notes-header.component'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -21,17 +13,8 @@ const routes: Routes = [
] ]
@NgModule({ @NgModule({
imports: [ imports: [IonicModule, ReleaseNotesModule, RouterModule.forChild(routes)],
CommonModule, declarations: [ReleaseNotesPage],
IonicModule, exports: [ReleaseNotesPage],
RouterModule.forChild(routes),
TextSpinnerComponentModule,
EmverPipesModule,
MarkdownPipeModule,
MarketplacePipesModule,
ElementModule,
],
declarations: [ReleaseNotesPage, ReleaseNotesHeaderComponent],
exports: [ReleaseNotesPage, ReleaseNotesHeaderComponent],
}) })
export class ReleaseNotesPageModule {} export class ReleaseNotesPageModule {}

View File

@@ -1,34 +1,10 @@
<release-notes-header></release-notes-header> <ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [defaultHref]="href"></ion-back-button>
</ion-buttons>
<ion-title>Release Notes</ion-title>
</ion-toolbar>
</ion-header>
<ion-content> <release-notes></release-notes>
<ng-container *ngIf="notes$ | async as notes else loading">
<div *ngFor="let note of notes | keyvalue : asIsOrder">
<ion-button
expand="full"
color="light"
class="version-button"
[class.ion-activated]="isSelected(note.key)"
(click)="setSelected(note.key)"
>
<p class="version">{{ note.key | displayEmver }}</p>
</ion-button>
<ion-card
elementRef
#element="elementRef"
class="panel"
color="light"
[id]="note.key"
[style.maxHeight.px]="getDocSize(note.key, element)"
>
<ion-text
id="release-notes"
[innerHTML]="note.value | markdown"
></ion-text>
</ion-card>
</div>
</ng-container>
<ng-template #loading>
<text-spinner text="Loading Release Notes"></text-spinner>
</ng-template>
</ion-content>

View File

@@ -1,38 +1,12 @@
import { ChangeDetectionStrategy, Component, ElementRef } from '@angular/core' import { ChangeDetectionStrategy, Component } from '@angular/core'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
@Component({ @Component({
selector: 'release-notes',
templateUrl: './release-notes.page.html', templateUrl: './release-notes.page.html',
styleUrls: ['./release-notes.page.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ReleaseNotesPage { export class ReleaseNotesPage {
private readonly pkgId = this.route.snapshot.paramMap.get('pkgId') readonly href = `/marketplace/${this.route.snapshot.paramMap.get('pkgId')}`
private selected: string | null = null constructor(private readonly route: ActivatedRoute) {}
readonly notes$ = this.marketplaceService.getReleaseNotes(this.pkgId)
constructor(
private readonly route: ActivatedRoute,
private readonly marketplaceService: AbstractMarketplaceService,
) {}
isSelected(key: string): boolean {
return this.selected === key
}
setSelected(selected: string) {
this.selected = this.isSelected(selected) ? null : selected
}
getDocSize(key: string, { nativeElement }: ElementRef<HTMLElement>) {
return this.isSelected(key) ? nativeElement.scrollHeight : 0
}
asIsOrder(a: any, b: any) {
return 0
}
} }

View File

@@ -0,0 +1,5 @@
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
export function spreadProgress(pkg: PackageDataEntry) {
pkg['install-progress'] = { ...pkg['install-progress'] }
}

View File

@@ -1,5 +1,5 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
import { PackageState } from '@start9labs/shared' import { PackageState } from 'src/app/types/package-state'
import { import {
InterfaceDef, InterfaceDef,
PackageMainStatus, PackageMainStatus,

View File

@@ -1,4 +1,4 @@
import { PackageState } from '@start9labs/shared' import { PackageState } from 'src/app/types/package-state'
import { ConfigSpec } from 'src/app/pkg-config/config-types' import { ConfigSpec } from 'src/app/pkg-config/config-types'
import { import {
DependencyErrorType, DependencyErrorType,

View File

@@ -1,3 +1,4 @@
import { AbstractApiService } from '@start9labs/shared'
import { Subject, Observable } from 'rxjs' import { Subject, Observable } from 'rxjs'
import { import {
Http, Http,
@@ -13,7 +14,10 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
import { RequestError } from '../http.service' import { RequestError } from '../http.service'
import { map } from 'rxjs/operators' import { map } from 'rxjs/operators'
export abstract class ApiService implements Source<DataModel>, Http<DataModel> { export abstract class ApiService
extends AbstractApiService
implements Source<DataModel>, Http<DataModel>
{
protected readonly sync$ = new Subject<Update<DataModel>>() protected readonly sync$ = new Subject<Update<DataModel>>()
/** PatchDb Source interface. Post/Patch requests provide a source of patches to the db. */ /** PatchDb Source interface. Post/Patch requests provide a source of patches to the db. */
@@ -24,9 +28,6 @@ export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
.pipe(map(result => ({ result, jsonrpc: '2.0' }))) .pipe(map(result => ({ result, jsonrpc: '2.0' })))
} }
// for getting static files: ex icons, instructions, licenses
abstract getStatic(url: string): Promise<string>
// db // db
abstract getRevisions(since: number): Promise<RR.GetRevisionsRes> abstract getRevisions(since: number): Promise<RR.GetRevisionsRes>

View File

@@ -1,8 +1,9 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { InstallProgress, pauseFor } from '@start9labs/shared' import { pauseFor } from '@start9labs/shared'
import { ApiService } from './embassy-api.service' import { ApiService } from './embassy-api.service'
import { PatchOp, Update, Operation, RemoveOperation } from 'patch-db-client' import { PatchOp, Update, Operation, RemoveOperation } from 'patch-db-client'
import { PackageState } from '@start9labs/shared' import { PackageState } from 'src/app/types/package-state'
import { InstallProgress } from 'src/app/types/install-progress'
import { import {
DataModel, DataModel,
DependencyErrorType, DependencyErrorType,
@@ -192,6 +193,8 @@ export class MockApiService extends ApiService {
return Mock.MarketplacePkgsList return Mock.MarketplacePkgsList
} else if (path.startsWith('/package/v0/release-notes')) { } else if (path.startsWith('/package/v0/release-notes')) {
return Mock.ReleaseNotes return Mock.ReleaseNotes
} else if (path.includes('instructions') || path.includes('license')) {
return markdown
} }
} }

View File

@@ -1,4 +1,4 @@
import { PackageState } from '@start9labs/shared' import { PackageState } from 'src/app/types/package-state'
import { import {
DataModel, DataModel,
DependencyErrorType, DependencyErrorType,

Some files were not shown because too many files have changed in this diff Show More