feat(portal): refactor marketplace for new portal (#2539)

* feat(portal): refactor marketplace for new portal

* fix background position

* chore: refactor sidebar

* chore: small fix

---------

Co-authored-by: Lucy Cifferello <12953208+elvece@users.noreply.github.com>
This commit is contained in:
Alex Inkin
2023-12-19 01:43:59 +04:00
committed by GitHub
parent ea6f70e3c5
commit e47f126bd5
32 changed files with 1201 additions and 155 deletions

View File

@@ -1,5 +1,5 @@
<header
class="z-50 overflow-hidden w-full fixed sm:w-[34vw] md:w-[28vw] lg:w-[22vw] 2xl:w-[280px] sm:bg-zinc-700/90 sm:backdrop-blur-2xl sm:min-h-screen overflow-y-auto flex flex-col sm:py-6 sm:after:block sm:after:absolute sm:after:top-0 sm:after:bottom-0 sm:after:right-0 sm:after:w-0.5 after:h-[calc(100vh - 20px)] sm:after:bg-gradient-to-b from-zinc-700 to-zinc-400"
class="header overflow-hidden w-full fixed sm:w-[34vw] md:w-[28vw] lg:w-[22vw] 2xl:w-[280px] sm:bg-zinc-700/90 sm:backdrop-blur-2xl sm:min-h-screen overflow-y-auto flex flex-col sm:py-6 sm:after:block sm:after:absolute sm:after:top-0 sm:after:bottom-0 sm:after:right-0 sm:after:w-0.5 after:h-[calc(100vh - 20px)] sm:after:bg-gradient-to-b from-zinc-700 to-zinc-400"
>
<ng-container *tuiLet="store$ | async as store">
<div class="hidden sm:flex flex-col mx-6 pb-3 items-center">

View File

@@ -0,0 +1,13 @@
@import '@taiga-ui/core/styles/taiga-ui-local';
.header {
@include scrollbar-hidden();
max-height: 100%;
// TODO: Theme
background: #373a3f;
@media screen and (min-width: 640px) {
width: 15rem;
}
}

View File

@@ -9,7 +9,6 @@ import { combineLatest, map, Subject, takeUntil } from 'rxjs'
import { StoreIdentity } from '../../types'
import { AbstractMarketplaceService } from '../../services/marketplace.service'
import { AbstractCategoryService } from '../../services/category.service'
import { Router } from '@angular/router'
import { MarketplaceConfig } from '@start9labs/shared'
@Component({
@@ -22,8 +21,6 @@ export class MenuComponent implements OnDestroy {
@Input({ required: true })
iconConfig!: MarketplaceConfig
constructor(private readonly router: Router) {}
private destroy$ = new Subject<void>()
private readonly marketplaceService = inject(AbstractMarketplaceService)
private readonly categoryService = inject(AbstractCategoryService)
@@ -69,13 +66,11 @@ export class MenuComponent implements OnDestroy {
this.query = ''
this.categoryService.resetQuery()
this.categoryService.changeCategory(category)
this.router.navigate(['/marketplace'], { replaceUrl: true })
}
onQueryChange(query: string): void {
this.query = query
this.categoryService.setQuery(query)
this.router.navigate(['/marketplace'], { replaceUrl: true })
}
toggleMenu(open: boolean): void {

View File

@@ -1,15 +1,21 @@
@import '@taiga-ui/core/styles/taiga-ui-local';
:root {
--tui-primary: #3880ff;
--tui-primary-hover: #4c8dff;
--tui-primary-active: #3171e0;
}
/* stylelint-disable order/order */
[tuiAppearance][data-appearance='secondary-warning'] {
background: var(--tui-warning-bg);
color: var(--tui-warning-fill);
@include wrapper-hover {
@include appearance-hover {
background: var(--tui-warning-bg-hover);
}
@include wrapper-active {
@include appearance-active {
background: var(--tui-warning-bg-hover);
}
}
@@ -30,142 +36,96 @@
[tuiAppearance][data-appearance='outline'] {
color: var(--tui-text-01);
}
[tuiWrapper][data-appearance='primary-solid'] {
background: #3880ff;
color: #fff;
.wrapper-hover {
background: #4c8dff;
}
;
.wrapper-active {
background: #3171e0;
}
;
.wrapper-disabled {
[tuiAppearance][data-appearance='primary'] {
@include appearance-disabled {
background: #eaecee;
}
;
}
[tuiWrapper][data-appearance='secondary-solid'] {
[tuiAppearance][data-appearance='secondary-solid'] {
background: #3dc2ff;
color: #fff;
.wrapper-hover {
@include appearance-hover {
background: #50c8ff;
}
;
.wrapper-active {
@include appearance-active {
background: #36abe0;
}
;
.wrapper-disabled {
@include appearance-disabled {
background: #eaecee;
}
;
}
[tuiWrapper][data-appearance='tertiary-solid'] {
[tuiAppearance][data-appearance='tertiary-solid'] {
background: #5260ff;
color: #fff;
.wrapper-hover {
@include appearance-hover {
background: #6370ff;
}
;
.wrapper-active {
@include appearance-active {
background: #4854e0;
}
;
.wrapper-disabled {
@include appearance-disabled {
background: #eaecee;
}
;
}
[tuiWrapper][data-appearance='success-solid'] {
[tuiAppearance][data-appearance='success-solid'] {
background: #2dd36f;
color: #fff;
.wrapper-hover {
@include appearance-hover {
background: #42d77d;
}
;
.wrapper-active {
@include appearance-active {
background: #28ba62;
}
;
.wrapper-disabled {
@include appearance-disabled {
background: #eaecee;
}
;
}
[tuiWrapper][data-appearance='warning-solid'] {
[tuiAppearance][data-appearance='warning-solid'] {
background: #ffc409;
color: #fff;
.wrapper-hover {
@include appearance-hover {
background: #ffca22;
}
;
.wrapper-active {
@include appearance-active {
background: #e0ac08;
}
;
.wrapper-disabled {
@include appearance-disabled {
background: #eaecee;
}
;
}
[tuiWrapper][data-appearance='danger-solid'] {
[tuiAppearance][data-appearance='danger-solid'] {
background: #eb445a;
color: #fff;
.wrapper-hover {
@include appearance-hover {
background: #ed576b;
}
;
.wrapper-active {
@include appearance-active {
background: #cf3c4f;
}
;
.wrapper-disabled {
@include appearance-disabled {
background: #eaecee;
}
;
}
[tuiWrapper][data-appearance='input-file'] {
@@ -196,7 +156,7 @@ tui-hint[data-appearance='onDark'] {
}
}
[tuiWrapper][data-appearance='drawer'] {
[tuiAppearance][data-appearance='drawer'] {
// TODO: Theme
background: rgb(81 80 83 / 86%);
border-radius: 10rem;
@@ -215,7 +175,9 @@ tui-dropdown[data-appearance='start-os'][data-appearance='start-os'] {
tui-opt-group {
&::before {
background: var(--tui-clear);
box-shadow: 1rem 0 var(--tui-clear), -1rem 0 var(--tui-clear);
box-shadow:
1rem 0 var(--tui-clear),
-1rem 0 var(--tui-clear);
padding-top: 0.375rem !important;
padding-bottom: 0 !important;
}

View File

@@ -3,11 +3,11 @@
<tui-badge-notification *ngIf="badge" size="m" tuiSlot="top">
{{ badge }}
</tui-badge-notification>
<tui-svg
<tui-icon
*ngIf="icon?.startsWith('tuiIcon'); else url"
class="icon"
[src]="icon"
></tui-svg>
[icon]="icon"
/>
<ng-template #url>
<img alt="" class="icon" [src]="icon" />
</ng-template>

View File

@@ -10,14 +10,11 @@ import {
TuiBadgedContentModule,
TuiBadgeNotificationModule,
TuiButtonModule,
TuiIconModule,
} from '@taiga-ui/experimental'
import { RouterLink } from '@angular/router'
import { TickerModule } from '@start9labs/shared'
import {
TuiDataListModule,
TuiHostedDropdownModule,
TuiSvgModule,
} from '@taiga-ui/core'
import { TuiDataListModule, TuiHostedDropdownModule } from '@taiga-ui/core'
import { NavigationService } from '../../services/navigation.service'
import { Action, ActionsComponent } from '../actions/actions.component'
import { toRouterLink } from '../../utils/to-router-link'
@@ -34,7 +31,7 @@ import { toRouterLink } from '../../utils/to-router-link'
TuiButtonModule,
TuiHostedDropdownModule,
TuiDataListModule,
TuiSvgModule,
TuiIconModule,
TickerModule,
TuiBadgedContentModule,
TuiBadgeNotificationModule,

View File

@@ -4,7 +4,7 @@
routerLinkActive="tab_active"
[routerLinkActiveOptions]="{ exact: true }"
>
<tui-svg src="tuiIconHomeLarge" class="icon"></tui-svg>
<tui-icon icon="tuiIconHome" class="icon" />
</a>
<a
*ngFor="let tab of tabs$ | async"
@@ -13,11 +13,11 @@
routerLinkActive="tab_active"
[routerLink]="tab.routerLink"
>
<tui-svg
<tui-icon
*ngIf="tab.icon.startsWith('tuiIcon'); else url"
class="icon"
[src]="tab.icon"
></tui-svg>
[icon]="tab.icon"
/>
<ng-template #url>
<img class="icon" [src]="tab.icon" [alt]="tab.title" />
</ng-template>

View File

@@ -1,10 +1,8 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { Router, RouterModule } from '@angular/router'
import { TuiSvgModule } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { TuiButtonModule, TuiIconModule } from '@taiga-ui/experimental'
import { NavigationService } from '../../services/navigation.service'
import { NavigationItem } from '../../types/navigation-item'
@Component({
selector: 'nav[appNavigation]',
@@ -12,7 +10,7 @@ import { NavigationItem } from '../../types/navigation-item'
styleUrls: ['navigation.component.scss'],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, RouterModule, TuiButtonModule, TuiSvgModule],
imports: [CommonModule, RouterModule, TuiButtonModule, TuiIconModule],
})
export class NavigationComponent {
private readonly router = inject(Router)

View File

@@ -1,19 +1,23 @@
export const SYSTEM_UTILITIES: Record<string, { icon: string; title: string }> =
{
'/portal/system/backups': {
icon: 'tuiIconSaveLarge',
icon: 'tuiIconSave',
title: 'Backups',
},
'/portal/system/marketplace': {
icon: 'tuiIconShoppingCart',
title: 'Marketplace',
},
'/portal/system/updates': {
icon: 'tuiIconGlobeLarge',
icon: 'tuiIconGlobe',
title: 'Updates',
},
'/portal/system/sideload': {
icon: 'tuiIconUploadLarge',
icon: 'tuiIconUpload',
title: 'Sideload',
},
'/portal/system/settings': {
icon: 'tuiIconToolLarge',
icon: 'tuiIconTool',
title: 'Settings',
},
'/portal/system/snek': {
@@ -21,7 +25,7 @@ export const SYSTEM_UTILITIES: Record<string, { icon: string; title: string }> =
title: 'Snek',
},
'/portal/system/notifications': {
icon: 'tuiIconBellLarge',
icon: 'tuiIconBell',
title: 'Notifications',
},
}

View File

@@ -1,27 +1,37 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { ToMenuPipe } from '../pipes/to-menu.pipe'
import { ServiceMenuItemComponent } from './menu-item.component'
import { RouterLink } from '@angular/router'
@Component({
selector: 'service-menu',
template: `
<h3 class="g-title">Menu</h3>
<button
*ngFor="let menu of service | toMenu"
class="g-action"
[serviceMenuItem]="menu"
(click)="menu.action()"
>
<div *ngIf="menu.name === 'Outbound Proxy'" [style.color]="color">
{{ proxy }}
</div>
</button>
@for (menu of service | toMenu; track $index) {
@if (menu.routerLink) {
<a
class="g-action"
[serviceMenuItem]="menu"
[routerLink]="menu.routerLink"
[queryParams]="menu.params || {}"
></a>
} @else {
<button
class="g-action"
[serviceMenuItem]="menu"
(click)="menu.action?.()"
>
@if (menu.name === 'Outbound Proxy') {
<div [style.color]="color">{{ proxy }}</div>
}
</button>
}
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CommonModule, ToMenuPipe, ServiceMenuItemComponent],
imports: [ToMenuPipe, ServiceMenuItemComponent, RouterLink],
})
export class ServiceMenuComponent {
@Input({ required: true })

View File

@@ -1,5 +1,5 @@
import { inject, Pipe, PipeTransform, Type } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { ActivatedRoute, Params, Router } from '@angular/router'
import { Manifest } from '@start9labs/marketplace'
import { MarkdownComponent } from '@start9labs/shared'
import { TuiDialogService } from '@taiga-ui/core'
@@ -20,7 +20,9 @@ export interface ServiceMenu {
icon: string
name: string
description: string
action: () => void
action?: () => void
routerLink?: string
params?: Params
}
@Pipe({
@@ -66,10 +68,7 @@ export class ToMenuPipe implements PipeTransform {
icon: 'tuiIconZapLarge',
name: 'Actions',
description: `Uninstall and other commands specific to ${manifest.title}`,
action: () =>
this.router.navigate(['actions'], {
relativeTo: this.route,
}),
routerLink: `actions`,
},
{
icon: 'tuiIconShieldLarge',
@@ -81,27 +80,20 @@ export class ToMenuPipe implements PipeTransform {
icon: 'tuiIconFileTextLarge',
name: 'Logs',
description: `Raw, unfiltered logs`,
action: () =>
this.router.navigate(['logs'], {
relativeTo: this.route,
}),
routerLink: 'logs',
},
url
? {
icon: 'tuiIconShoppingBagLarge',
name: 'Marketplace Listing',
description: `View ${manifest.title} on the Marketplace`,
action: () =>
this.router.navigate(['marketplace'], {
relativeTo: this.route,
queryParams: { url, id: manifest.id },
}),
routerLink: `/portal/system/marketplace`,
params: { url, id: manifest.id },
}
: {
icon: 'tuiIconShoppingBagLarge',
name: 'Marketplace Listing',
description: `This package was not installed from the marketplace`,
action: () => {},
},
]
}

View File

@@ -0,0 +1,189 @@
import { CommonModule } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
inject,
Input,
} from '@angular/core'
import { Router } from '@angular/router'
import {
AbstractMarketplaceService,
MarketplacePkg,
} from '@start9labs/marketplace'
import {
Emver,
ErrorService,
isEmptyObject,
LoadingService,
sameUrl,
EmverPipesModule,
} from '@start9labs/shared'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { PatchDB } from 'patch-db-client'
import { firstValueFrom } from 'rxjs'
import {
DataModel,
PackageDataEntry,
PackageState,
} from 'src/app/services/patch-db/data-model'
import { ClientStorageService } from 'src/app/services/client-storage.service'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import { hasCurrentDeps } from 'src/app/util/has-deps'
import { getAllPackages } from 'src/app/util/get-package-data'
import { dryUpdate } from 'src/app/util/dry-update'
import { SidebarService } from 'src/app/services/sidebar.service'
import { MarketplaceAlertsService } from '../services/alerts.service'
@Component({
selector: 'marketplace-controls',
template: `
@if (localPkg) {
<button
tuiButton
type="button"
appearance="primary"
(click)="showService()"
>
View Installed
</button>
@if (installed) {
@switch (localVersion | compareEmver: pkg.manifest.version) {
@case (1) {
<button
tuiButton
type="button"
appearance="secondary-solid"
(click)="tryInstall()"
>
Downgrade
</button>
}
@case (-1) {
<button
tuiButton
type="button"
appearance="warning-solid"
(click)="tryInstall()"
>
Update
</button>
}
@case (0) {
@if (showDevTools$ | async) {
<button
tuiButton
type="button"
appearance="tertiary-solid"
(click)="tryInstall()"
>
Reinstall
</button>
}
}
}
}
} @else {
<button
tuiButton
type="button"
appearance="primary"
(click)="tryInstall()"
>
Install
</button>
}
`,
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, EmverPipesModule, TuiButtonModule],
})
export class MarketplaceControlsComponent {
private readonly alerts = inject(MarketplaceAlertsService)
private readonly patch = inject(PatchDB<DataModel>)
private readonly errorService = inject(ErrorService)
private readonly loader = inject(LoadingService)
private readonly emver = inject(Emver)
private readonly router = inject(Router)
private readonly marketplace = inject(
AbstractMarketplaceService,
) as MarketplaceService
@Input()
url?: string
@Input({ required: true })
pkg!: MarketplacePkg
@Input()
localPkg!: PackageDataEntry | null
readonly showDevTools$ = inject(ClientStorageService).showDevTools$
get installed(): boolean {
return this.localPkg?.state === PackageState.Installed
}
get localVersion(): string {
return this.localPkg?.manifest.version || ''
}
async tryInstall() {
const current = await firstValueFrom(this.marketplace.getSelectedHost$())
const url = this.url || current.url
const originalUrl = this.localPkg?.installed?.['marketplace-url'] || ''
if (!this.localPkg) {
if (await this.alerts.alertInstall(this.pkg)) this.install(url)
return
}
if (
!sameUrl(url, originalUrl) &&
!(await this.alerts.alertMarketplace(url, originalUrl))
) {
return
}
if (
hasCurrentDeps(this.localPkg) &&
this.emver.compare(this.localVersion, this.pkg.manifest.version) !== 0
) {
this.dryInstall(url)
} else {
this.install(url)
}
}
async showService() {
this.router.navigate(['/portal/service', this.pkg.manifest.id])
}
private async dryInstall(url: string) {
const breakages = dryUpdate(
this.pkg.manifest,
await getAllPackages(this.patch),
this.emver,
)
if (
isEmptyObject(breakages) ||
(await this.alerts.alertBreakages(breakages))
) {
this.install(url)
}
}
private async install(url: string) {
const loader = this.loader.open('Beginning Install...').subscribe()
const { id, version } = this.pkg.manifest
try {
await this.marketplace.installPackage(id, version, url)
} catch (e: any) {
this.errorService.handleError(e)
} finally {
loader.unsubscribe()
}
}
}

View File

@@ -0,0 +1,47 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { MenuModule } from '@start9labs/marketplace'
import { TuiButtonModule, TuiDialogService } from '@taiga-ui/core'
import { ConfigService } from 'src/app/services/config.service'
import { TuiAppearanceModule, TuiIconModule } from '@taiga-ui/experimental'
import { MARKETPLACE_REGISTRY } from '../modals/registry.component'
@Component({
standalone: true,
selector: 'marketplace-menu',
template: `
<menu [iconConfig]="marketplace">
<button
slot="desktop"
tuiIconButton
type="button"
appearance="icon"
icon="tuiIconRepeat"
(click)="changeRegistry()"
>
Change Registry
</button>
<button
slot="mobile"
class="flex gap-2 p-5 text-base"
(click)="changeRegistry()"
>
<tui-icon tuiAppearance="icon" icon="tuiIconRepeat"></tui-icon>
Change Registry
</button>
</menu>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [MenuModule, TuiButtonModule, TuiIconModule, TuiAppearanceModule],
})
export class MarketplaceMenuComponent {
private readonly dialogs = inject(TuiDialogService)
readonly marketplace = inject(ConfigService).marketplace
changeRegistry() {
this.dialogs
.open(MARKETPLACE_REGISTRY, {
label: 'Change Registry',
})
.subscribe()
}
}

View File

@@ -0,0 +1,77 @@
import { Component, inject, Input } from '@angular/core'
import { NgIf } from '@angular/common'
import { TuiNotificationModule } from '@taiga-ui/core'
import { ConfigService } from 'src/app/services/config.service'
@Component({
standalone: true,
selector: 'marketplace-notification',
template: `
<tui-notification [status]="status || 'warning'" icon="" class="m-4">
@switch (status) {
@case ('success') {
Services from this registry are packaged and maintained by the Start9
team. If you experience an issue or have questions related to a
service from this registry, one of our dedicated support staff will be
happy to assist you.
}
@case ('info') {
Services from this registry are packaged and maintained by members of
the Start9 community.
<strong>Install at your own risk</strong>
. If you experience an issue or have a question related to a service
in this marketplace, please reach out to the package developer for
assistance.
}
@case ('warning') {
Services from this registry are undergoing
<strong>beta</strong>
testing and may contain bugs.
<strong>Install at your own risk</strong>
.
}
@case ('error') {
Services from this registry are undergoing
<strong>alpha</strong>
testing. They are expected to contain bugs and could damage your
system.
<strong>Install at your own risk</strong>
.
}
@default {
This is a Custom Registry. Start9 cannot verify the integrity or
functionality of services from this registry, and they could damage
your system.
<strong>Install at your own risk</strong>
.
}
}
</tui-notification>
`,
imports: [TuiNotificationModule],
})
export class MarketplaceNotificationComponent {
private readonly marketplace = inject(ConfigService).marketplace
@Input() url = ''
get status() {
if (this.url === this.marketplace.start9) {
return 'success'
}
if (this.url === this.marketplace.community) {
return 'info'
}
if (this.url.includes('beta')) {
return 'warning'
}
if (this.url.includes('alpha')) {
return 'error'
}
return null
}
}

View File

@@ -0,0 +1,37 @@
import { NgIf } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
inject,
Input,
} from '@angular/core'
import { StoreIconComponentModule } from '@start9labs/marketplace'
import { TuiIconModule, TuiTitleModule } from '@taiga-ui/experimental'
import { ConfigService } from 'src/app/services/config.service'
@Component({
standalone: true,
selector: '[registry]',
template: `
<store-icon [url]="registry.url" [marketplace]="marketplace" size="40px" />
<div tuiTitle>
{{ registry.name }}
<div tuiSubtitle>{{ registry.url }}</div>
</div>
<tui-icon
*ngIf="registry.selected; else content"
icon="tuiIconCheck"
[style.color]="'var(--tui-positive)'"
/>
<ng-template #content><ng-content></ng-content></ng-template>
`,
styles: [':host { border-radius: 0.25rem; width: stretch; }'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgIf, StoreIconComponentModule, TuiIconModule, TuiTitleModule],
})
export class MarketplaceRegistryComponent {
readonly marketplace = inject(ConfigService).marketplace
@Input()
registry!: { url: string; selected: boolean; name?: string }
}

View File

@@ -0,0 +1,30 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import {
AbstractTuiPortalHostComponent,
AbstractTuiPortalService,
} from '@taiga-ui/cdk'
import { MarketplaceSidebarService } from '../services/sidebar.service'
@Component({
standalone: true,
selector: 'marketplace-sidebars',
template: '<ng-container #viewContainer></ng-container>',
styles: [
`
:host {
position: fixed;
inset: 7.5rem 0 0;
pointer-events: none;
transform: translate3d(0, 0, 0);
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: AbstractTuiPortalService,
useExisting: MarketplaceSidebarService,
},
],
})
export class MarketplaceSidebarsComponent extends AbstractTuiPortalHostComponent {}

View File

@@ -0,0 +1,111 @@
import { CommonModule } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
Input,
inject,
} from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { ItemModule, MarketplacePkg } from '@start9labs/marketplace'
import { TuiSidebarModule } from '@taiga-ui/addon-mobile'
import {
TuiActiveZoneModule,
TuiAutoFocusModule,
TuiDropdownPortalService,
} from '@taiga-ui/cdk'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { debounceTime, map } from 'rxjs'
import { ToLocalPipe } from '../pipes/to-local.pipe'
import { MarketplaceControlsComponent } from './controls.component'
import { MarketplacePreviewComponent } from '../modals/preview.component'
import { MarketplaceSidebarService } from '../services/sidebar.service'
@Component({
selector: 'marketplace-tile',
template: `
<marketplace-item [pkg]="pkg" (click)="toggle(true)">
<marketplace-preview
*tuiSidebar="
(id$ | async) === pkg.manifest.id;
direction: 'right';
autoWidth: true
"
class="overflow-y-auto max-w-full md:max-w-[30rem]"
[pkg]="pkg"
(tuiActiveZoneChange)="toggle($event)"
>
<button
tuiAutoFocus
slot="close"
size="xs"
class="place-self-end"
tuiIconButton
type="button"
appearance="icon"
iconLeft="tuiIconClose"
(click)="toggle(false)"
></button>
<marketplace-controls
slot="controls"
class="flex justify-start gap-2"
[pkg]="pkg"
[localPkg]="pkg.manifest.id | toLocal | async"
/>
</marketplace-preview>
</marketplace-item>
`,
styles: [
`
:host {
animation: animateIn 400ms calc(var(--animation-order) * 200ms) both;
}
@keyframes animateIn {
from {
opacity: 0;
transform: scale(0.6) translateY(-20px);
}
to {
opacity: 1;
}
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
providers: [
{
provide: TuiDropdownPortalService,
useExisting: MarketplaceSidebarService,
},
],
imports: [
CommonModule,
ItemModule,
ToLocalPipe,
TuiActiveZoneModule,
TuiSidebarModule,
TuiButtonModule,
MarketplaceControlsComponent,
MarketplacePreviewComponent,
TuiAutoFocusModule,
],
})
export class MarketplaceTileComponent {
private readonly router = inject(Router)
readonly id$ = inject(ActivatedRoute).queryParamMap.pipe(
map(map => map.get('id') || ''),
debounceTime(100),
)
@Input({ required: true })
pkg!: MarketplacePkg
toggle(open: boolean) {
this.router.navigate([], {
queryParams: { id: open ? this.pkg.manifest.id : null },
})
}
}

View File

@@ -0,0 +1,89 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import {
AbstractCategoryService,
AbstractMarketplaceService,
FilterPackagesPipe,
} from '@start9labs/marketplace'
import { combineLatest, map } from 'rxjs'
import { MarketplaceNotificationComponent } from './components/notification.component'
import { MarketplaceMenuComponent } from './components/menu.component'
import { MarketplaceTileComponent } from './components/tile.component'
import { MarketplaceControlsComponent } from './components/controls.component'
import { MarketplacePreviewComponent } from './modals/preview.component'
import { MarketplaceSidebarsComponent } from './components/sidebars.component'
@Component({
standalone: true,
template: `
<marketplace-menu />
<div
class="sm:pl-[34vw] md:pl-[28vw] lg:pl-[22vw] 2xl:pl-[280px] min-h-screen flex justify-between overflow-auto scroll-smooth"
>
<div class="pt-24 sm:pt-3 md:pb-10 md:px-8">
<marketplace-notification [url]="(details$ | async)?.url || ''" />
<div class="mt-8 px-6 mb-10">
<h1 class="text-4xl sm:text-5xl font-bold text-zinc-50/80">
{{ category$ | async | titlecase }}
</h1>
</div>
@if (filtered$ | async; as filtered) {
<section
class="p-6 md:p-8 grid grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 gap-16 list-none"
>
@for (pkg of filtered; track $index) {
<marketplace-tile
[pkg]="pkg"
[style.--animation-order]="$index"
class="block h-full"
/>
}
</section>
} @else {
<h1 class="text-xl pl-6">
Loading
<span class="loading-dots"></span>
</h1>
}
</div>
</div>
<marketplace-sidebars />
`,
styles: [
`
:host {
max-height: 100%;
overflow: auto;
// TODO: Theme
background: #18181b url('/assets/img/background.png') no-repeat top
right;
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [FilterPackagesPipe],
imports: [
CommonModule,
MarketplaceTileComponent,
MarketplaceMenuComponent,
MarketplaceNotificationComponent,
MarketplaceControlsComponent,
MarketplacePreviewComponent,
MarketplaceSidebarsComponent,
],
})
export class MarketplaceComponent {
private readonly pipe = inject(FilterPackagesPipe)
private readonly categoryService = inject(AbstractCategoryService)
private readonly marketplaceService = inject(AbstractMarketplaceService)
readonly details$ = this.marketplaceService.getSelectedHost$()
readonly category$ = this.categoryService.getCategory$()
readonly filtered$ = combineLatest([
this.marketplaceService
.getSelectedStore$()
.pipe(map(({ packages }) => packages)),
this.categoryService.getQuery$(),
this.category$,
]).pipe(map(args => this.pipe.transform(...args)))
}

View File

@@ -0,0 +1,12 @@
import { Routes } from '@angular/router'
const MARKETPLACE_ROUTES: Routes = [
{
path: '',
pathMatch: 'full',
loadComponent: () =>
import('./marketplace.component').then(m => m.MarketplaceComponent),
},
]
export default MARKETPLACE_ROUTES

View File

@@ -0,0 +1,98 @@
import { CommonModule } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
inject,
Input,
} from '@angular/core'
import {
AboutModule,
AbstractMarketplaceService,
AdditionalModule,
DependenciesModule,
MarketplacePackageHeroComponent,
MarketplacePkg,
ReleaseNotesModule,
} from '@start9labs/marketplace'
import { SharedPipesModule } from '@start9labs/shared'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { map } from 'rxjs'
import { Router } from '@angular/router'
@Component({
selector: 'marketplace-preview',
template: `
<div class="grid gap-8 p-7 justify-center">
<ng-content select="[slot=close]" />
<marketplace-package-hero [pkg]="pkg">
<ng-content select="[slot=controls]" />
</marketplace-package-hero>
@if (url$ | async; as url) {
<a
[href]="url + '/marketplace/' + pkg.manifest.id"
tuiButton
appearance="tertiary-solid"
iconRight="tuiIconExternalLink"
target="_blank"
>
View more details
</a>
}
<div class="grid grid-cols-1 gap-x-8">
<marketplace-about [pkg]="pkg" />
@if (!(pkg.manifest.dependencies | empty)) {
<div
class="rounded-xl bg-gradient-to-bl from-zinc-400/75 to-zinc-600 p-px shadow-lg shadow-zinc-400/10 mt-6"
>
<div class="lg:col-span-5 xl:col-span-4 bg-zinc-800 rounded-xl p-7">
<h2 class="text-lg font-bold small-caps my-2 pb-3">
Dependencies
</h2>
<div class="grid grid-row-auto gap-3">
@for (
dep of pkg.manifest.dependencies | keyvalue;
track $index
) {
<marketplace-dependencies
[dep]="dep"
[pkg]="pkg"
(click)="open(dep.key)"
/>
}
</div>
</div>
</div>
}
<release-notes [pkg]="pkg" />
<marketplace-additional class="mt-6" [pkg]="pkg" />
</div>
</div>
`,
styles: [':host { pointer-events: auto }'],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
MarketplacePackageHeroComponent,
TuiButtonModule,
DependenciesModule,
ReleaseNotesModule,
AdditionalModule,
AboutModule,
SharedPipesModule,
],
})
export class MarketplacePreviewComponent {
private readonly router = inject(Router)
@Input({ required: true })
pkg!: MarketplacePkg
readonly url$ = inject(AbstractMarketplaceService)
.getSelectedHost$()
.pipe(map(({ url }) => url))
open(id: string) {
this.router.navigate([], { queryParams: { id } })
}
}

View File

@@ -0,0 +1,221 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import {
ErrorService,
LoadingService,
sameUrl,
toUrl,
} from '@start9labs/shared'
import {
AbstractMarketplaceService,
StoreIconComponentModule,
} from '@start9labs/marketplace'
import { TuiDialogService } from '@taiga-ui/core'
import {
TuiButtonModule,
TuiCellModule,
TuiIconModule,
TuiTitleModule,
} from '@taiga-ui/experimental'
import { TUI_PROMPT } from '@taiga-ui/kit'
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
import { PatchDB } from 'patch-db-client'
import { combineLatest, filter, firstValueFrom, map, Subscription } from 'rxjs'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { DataModel, UIStore } from 'src/app/services/patch-db/data-model'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import { FormDialogService } from 'src/app/services/form-dialog.service'
import { FormPage } from 'src/app/apps/ui/modals/form/form.page'
import { MarketplaceRegistryComponent } from '../components/registry.component'
import { getMarketplaceValueSpec, getPromptOptions } from '../utils/registry'
@Component({
standalone: true,
template: `
@if (stores$ | async; as stores) {
<h3 class="g-title">Default Registries</h3>
@for (registry of stores.standard; track $index) {
<button
tuiCell
[disabled]="registry.selected"
[registry]="registry"
(click)="connect(registry.url)"
></button>
}
<h3 class="g-title">Custom Registries</h3>
<button tuiCell (click)="add()">
<tui-icon icon="tuiIconPlus" [style.margin-inline.rem]="0.5" />
<div tuiTitle>Add custom registry</div>
</button>
@for (registry of stores.alt; track $index) {
<div tuiCell [registry]="registry">
<button
tuiIconButton
appearance="icon"
iconLeft="tuiIconTrash2"
(click)="delete(registry.url, registry.name)"
>
Delete
</button>
<button
tuiIconButton
appearance="icon"
iconLeft="tuiIconLogIn"
(click)="connect(registry.url)"
>
Connect
</button>
</div>
}
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
TuiCellModule,
TuiIconModule,
TuiTitleModule,
TuiButtonModule,
MarketplaceRegistryComponent,
StoreIconComponentModule,
],
})
export class MarketplaceRegistryModal {
private readonly api = inject(ApiService)
private readonly loader = inject(LoadingService)
private readonly errorService = inject(ErrorService)
private readonly formDialog = inject(FormDialogService)
private readonly dialogs = inject(TuiDialogService)
private readonly marketplace = inject(
AbstractMarketplaceService,
) as MarketplaceService
private readonly hosts$ = inject(PatchDB<DataModel>).watch$(
'ui',
'marketplace',
'known-hosts',
)
readonly stores$ = combineLatest([
this.marketplace.getKnownHosts$(),
this.marketplace.getSelectedHost$(),
]).pipe(
map(([stores, selected]) =>
stores.map(s => ({
...s,
selected: sameUrl(s.url, selected.url),
})),
),
// 0 and 1 are prod and community, 2 and beyond are alts
map(stores => ({ standard: stores.slice(0, 2), alt: stores.slice(2) })),
)
add() {
const { name, spec } = getMarketplaceValueSpec()
this.formDialog.open(FormPage, {
label: name,
data: {
spec,
buttons: [
{
text: 'Save for Later',
handler: async ({ url }: { url: string }) => this.save(url),
},
{
text: 'Save and Connect',
handler: async ({ url }: { url: string }) => this.save(url, true),
isSubmit: true,
},
],
},
})
}
delete(url: string, name: string = '') {
this.dialogs
.open(TUI_PROMPT, getPromptOptions(name))
.pipe(filter(Boolean))
.subscribe(async () => {
const loader = this.loader.open('Deleting...').subscribe()
const hosts = await firstValueFrom(this.hosts$)
const filtered: { [url: string]: UIStore } = Object.keys(hosts)
.filter(key => !sameUrl(key, url))
.reduce(
(prev, curr) => ({
...prev,
[curr]: hosts[curr],
}),
{},
)
try {
await this.api.setDbValue(['marketplace', 'known-hosts'], filtered)
} catch (e: any) {
this.errorService.handleError(e)
} finally {
loader.unsubscribe()
}
})
}
async connect(
url: string,
loader: Subscription = new Subscription(),
): Promise<void> {
loader.unsubscribe()
loader.closed = false
loader.add(this.loader.open('Changing Registry...').subscribe())
try {
await this.api.setDbValue<string>(['marketplace', 'selected-url'], url)
} catch (e: any) {
this.errorService.handleError(e)
} finally {
loader.unsubscribe()
}
}
private async save(rawUrl: string, connect = false): Promise<boolean> {
const loader = this.loader.open('Loading').subscribe()
const url = new URL(rawUrl).toString()
try {
await this.validateAndSave(url, loader)
if (connect) await this.connect(url, loader)
return true
} catch (e: any) {
this.errorService.handleError(e)
return false
} finally {
loader.unsubscribe()
}
}
private async validateAndSave(
url: string,
loader: Subscription,
): Promise<void> {
// Error on duplicates
const hosts = await firstValueFrom(this.hosts$)
const currentUrls = Object.keys(hosts).map(toUrl)
if (currentUrls.includes(url)) throw new Error('Marketplace already added')
// Validate
loader.unsubscribe()
loader.closed = false
loader.add(this.loader.open('Validating marketplace...').subscribe())
const { name } = await firstValueFrom(this.marketplace.fetchInfo$(url))
// Save
loader.unsubscribe()
loader.closed = false
loader.add(this.loader.open('Saving...').subscribe())
await this.api.setDbValue(['marketplace', 'known-hosts', url], { name })
}
}
export const MARKETPLACE_REGISTRY = new PolymorpheusComponent(
MarketplaceRegistryModal,
)

View File

@@ -0,0 +1,17 @@
import { inject, Pipe, PipeTransform } from '@angular/core'
import { PatchDB } from 'patch-db-client'
import { filter, Observable } from 'rxjs'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Pipe({
name: 'toLocal',
standalone: true,
})
export class ToLocalPipe implements PipeTransform {
private readonly patch = inject(PatchDB<DataModel>)
transform(id: string): Observable<PackageDataEntry> {
return this.patch.watch$('package-data', id).pipe(filter(Boolean))
}
}

View File

@@ -0,0 +1,84 @@
import { inject, Injectable } from '@angular/core'
import { MarketplacePkg } from '@start9labs/marketplace'
import { TUI_PROMPT } from '@taiga-ui/kit'
import { TuiDialogService } from '@taiga-ui/core'
import { PatchDB } from 'patch-db-client'
import { defaultIfEmpty, firstValueFrom } from 'rxjs'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Injectable({
providedIn: 'root',
})
export class MarketplaceAlertsService {
private readonly dialogs = inject(TuiDialogService)
private readonly marketplace$ = inject(PatchDB<DataModel>).watch$(
'ui',
'marketplace',
)
async alertMarketplace(url: string, originalUrl: string): Promise<boolean> {
const marketplaces = await firstValueFrom(this.marketplace$)
const name = marketplaces['known-hosts'][url]?.name || url
const source = marketplaces['known-hosts'][originalUrl]?.name || originalUrl
const message = source ? `installed from ${source}` : 'side loaded'
return new Promise(async resolve => {
this.dialogs
.open<boolean>(TUI_PROMPT, {
label: 'Warning',
size: 's',
data: {
content: `This service was originally ${message}, but you are currently connected to ${name}. To install from ${name} anyway, click "Continue".`,
yes: 'Continue',
no: 'Cancel',
},
})
.pipe(defaultIfEmpty(false))
.subscribe(response => resolve(response))
})
}
async alertBreakages(breakages: string[]): Promise<boolean> {
let content: string =
'As a result of this update, the following services will no longer work properly and may crash:<ul>'
const bullets = breakages.map(title => `<li><b>${title}</b></li>`)
content = `${content}${bullets.join('')}</ul>`
return new Promise(async resolve => {
this.dialogs
.open<boolean>(TUI_PROMPT, {
label: 'Warning',
size: 's',
data: {
content,
yes: 'Continue',
no: 'Cancel',
},
})
.pipe(defaultIfEmpty(false))
.subscribe(response => resolve(response))
})
}
async alertInstall({ manifest }: MarketplacePkg): Promise<boolean> {
const content = manifest.alerts.install
return (
!!content &&
new Promise(resolve => {
this.dialogs
.open<boolean>(TUI_PROMPT, {
label: 'Alert',
size: 's',
data: {
content,
yes: 'Install',
no: 'Cancel',
},
})
.pipe(defaultIfEmpty(false))
.subscribe(response => resolve(response))
})
)
}
}

View File

@@ -0,0 +1,5 @@
import { Injectable } from '@angular/core'
import { AbstractTuiPortalService } from '@taiga-ui/cdk'
@Injectable({ providedIn: `root` })
export class MarketplaceSidebarService extends AbstractTuiPortalService {}

View File

@@ -0,0 +1,50 @@
import { ValueSpecObject } from '@start9labs/start-sdk/lib/config/configTypes'
import { TuiDialogOptions } from '@taiga-ui/core'
import { TuiPromptData } from '@taiga-ui/kit'
export function getMarketplaceValueSpec(): ValueSpecObject {
return {
type: 'object',
name: 'Add Custom Registry',
description: null,
warning: null,
spec: {
url: {
type: 'text',
name: 'URL',
description: 'A fully-qualified URL of the custom registry',
inputmode: 'url',
required: true,
masked: false,
minLength: null,
maxLength: null,
patterns: [
{
regex: `https?:\/\/[a-zA-Z0-9][a-zA-Z0-9-\.]+[a-zA-Z0-9]\.[^\s]{2,}`,
description: 'Must be a valid URL',
},
],
placeholder: 'e.g. https://example.org',
default: null,
warning: null,
disabled: false,
immutable: false,
generate: null,
},
},
}
}
export function getPromptOptions(
name: string,
): Partial<TuiDialogOptions<TuiPromptData>> {
return {
label: 'Confirm',
size: 's',
data: {
content: `Are you sure you want to delete ${name}?`,
yes: 'Delete',
no: 'Cancel',
},
}
}

View File

@@ -29,7 +29,7 @@ import { NavigationService } from '../../../services/navigation.service'
selector: 'sideload-package',
template: `
<div class="grid gap-8 mb-16 p-4 lg:px-16 lg:pb-8 pt-14 justify-center">
<ng-content></ng-content>
<ng-content />
<marketplace-package-hero
*tuiLet="button$ | async as button"
[pkg]="package"
@@ -48,7 +48,7 @@ import { NavigationService } from '../../../services/navigation.service'
</button>
</div>
</marketplace-package-hero>
<marketplace-about [pkg]="package"></marketplace-about>
<marketplace-about [pkg]="package" />
<div
*ngIf="!(package.manifest.dependencies | empty)"
class="rounded-xl bg-gradient-to-bl from-zinc-400/75 to-zinc-600 p-px shadow-lg shadow-zinc-400/10"
@@ -57,15 +57,12 @@ import { NavigationService } from '../../../services/navigation.service'
<h2 class="text-lg font-bold small-caps my-2 pb-3">Dependencies</h2>
<div class="grid grid-row-auto gap-3">
<div *ngFor="let dep of package.manifest.dependencies | keyvalue">
<marketplace-dependencies
[dep]="dep"
[pkg]="package"
></marketplace-dependencies>
<marketplace-dependencies [dep]="dep" [pkg]="package" />
</div>
</div>
</div>
</div>
<marketplace-additional [pkg]="package"></marketplace-additional>
<marketplace-additional [pkg]="package" />
</div>
`,
standalone: true,

View File

@@ -11,6 +11,12 @@ const ROUTES: Routes = [
import('./backups/backups.component').then(m => m.BackupsComponent),
data: toNavigationItem('/portal/system/backups'),
},
{
title: systemTabResolver,
path: 'marketplace',
loadChildren: () => import('./marketplace/marketplace.routes'),
data: toNavigationItem('/portal/system/marketplace'),
},
{
title: systemTabResolver,
path: 'settings',

View File

@@ -16,9 +16,12 @@ import {
TuiDialogService,
TuiLinkModule,
TuiLoaderModule,
TuiSvgModule,
} from '@taiga-ui/core'
import { TuiAvatarModule, TuiButtonModule } from '@taiga-ui/experimental'
import {
TuiAvatarModule,
TuiButtonModule,
TuiIconModule,
} from '@taiga-ui/experimental'
import {
TUI_PROMPT,
TuiAccordionModule,
@@ -35,27 +38,25 @@ import { InstallProgressPipe } from '../pipes/install-progress.pipe'
template: `
<tui-accordion-item borders="top-bottom">
<div class="g-action">
<tui-avatar size="s" [src]="marketplacePkg | mimeType | trustUrl" />
<tui-avatar size="s" [src]="marketplacePkg | mimeType" />
<div [style.flex]="1" [style.overflow]="'hidden'">
<strong>{{ marketplacePkg.manifest.title }}</strong>
<div>
<!-- @TODO left side should be local['old-manifest'] (or whatever), not manifest. -->
{{ localPkg.manifest.version || '' | displayEmver }}
<tui-svg src="tuiIconArrowRight"></tui-svg>
<tui-icon icon="tuiIconArrowRight" [style.font-size.rem]="1" />
<span [style.color]="'var(--tui-positive)'">
{{ marketplacePkg.manifest.version | displayEmver }}
</span>
</div>
<div [style.color]="'var(--tui-negative)'">
{{ errors }}
</div>
<div [style.color]="'var(--tui-negative)'">{{ errors }}</div>
</div>
<tui-progress-circle
*ngIf="localPkg.state === 'updating'; else button"
style="color: var(--tui-positive)"
[max]="100"
[value]="localPkg['install-progress'] | installProgress"
></tui-progress-circle>
/>
<ng-template #button>
<button
*ngIf="ready; else queued"
@@ -68,7 +69,7 @@ import { InstallProgressPipe } from '../pipes/install-progress.pipe'
</button>
</ng-template>
<ng-template #queued>
<tui-loader [style.width.rem]="2" [inheritColor]="true"></tui-loader>
<tui-loader [style.width.rem]="2" [inheritColor]="true" />
</ng-template>
</div>
<ng-template tuiAccordionItemContent>
@@ -116,7 +117,7 @@ import { InstallProgressPipe } from '../pipes/install-progress.pipe'
TuiProgressModule,
TuiAccordionModule,
TuiAvatarModule,
TuiSvgModule,
TuiIconModule,
TuiButtonModule,
TuiLinkModule,
TuiLoaderModule,

View File

@@ -44,7 +44,7 @@ import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { TuiButtonModule } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
@Component({
selector: 'marketplace-show-controls',
@@ -67,7 +67,7 @@ import { TuiButtonModule } from '@taiga-ui/core'
type="button"
class="mr-2"
appearance="warning-solid"
*ngIf="(localVersion | compareEmver : pkg.manifest.version) === -1"
*ngIf="(localVersion | compareEmver: pkg.manifest.version) === -1"
(click)="tryInstall()"
>
Update
@@ -77,7 +77,7 @@ import { TuiButtonModule } from '@taiga-ui/core'
type="button"
class="mr-2"
appearance="secondary-solid"
*ngIf="(localVersion | compareEmver : pkg.manifest.version) === 1"
*ngIf="(localVersion | compareEmver: pkg.manifest.version) === 1"
(click)="tryInstall()"
>
Downgrade
@@ -88,7 +88,7 @@ import { TuiButtonModule } from '@taiga-ui/core'
type="button"
class="mr-2"
appearance="tertiary-solid"
*ngIf="(localVersion | compareEmver : pkg.manifest.version) === 0"
*ngIf="(localVersion | compareEmver: pkg.manifest.version) === 0"
(click)="tryInstall()"
>
Reinstall
@@ -101,7 +101,7 @@ import { TuiButtonModule } from '@taiga-ui/core'
<button
tuiButton
type="button"
appearance="primary-solid"
appearance="primary"
(click)="tryInstall()"
>
Install

View File

@@ -13,7 +13,7 @@ import {
} from '@start9labs/marketplace'
import { MarketplaceShowPreviewComponent } from './marketplace-show-preview.component'
import { TuiButtonModule } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { RouterModule } from '@angular/router'
@NgModule({

View File

@@ -18,7 +18,7 @@ import {
} from 'src/app/services/patch-db/data-model'
import { BackupTargetType, Metrics, RR } from './api.types'
import { Mock } from './api.fixures'
import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md'
// import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md'
import {
EMPTY,
iif,
@@ -82,7 +82,7 @@ export class MockApiService extends ApiService {
async getStatic(url: string): Promise<string> {
await pauseFor(2000)
return markdown
return '' // markdown
}
async uploadPackage(guid: string, body: Blob): Promise<void> {
@@ -458,7 +458,7 @@ export class MockApiService extends ApiService {
} else if (path.startsWith('/package/v0/release-notes')) {
return Mock.ReleaseNotes
} else if (path.includes('instructions') || path.includes('license')) {
return markdown
return '' // markdown
}
}

View File

@@ -476,6 +476,10 @@ button.g-action {
overflow: auto !important;
}
ng-component {
display: block;
}
svg:not(:root) {
overflow: auto;
}
@@ -486,4 +490,4 @@ svg:not(:root) {
.externalLink {
color: var(--ion-color-secondary);
}
}