mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-02 05:23:14 +00:00
feat(portal): implement adding/removing to desktop (#2374)
* feat(portal): implement adding/removing to desktop, reordering desktop items, baseline for system utils * chore: fix comments --------- Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
<tui-data-list>
|
||||
<h3 class="title"><ng-content></ng-content></h3>
|
||||
<tui-opt-group
|
||||
*ngFor="let group of actions | keyvalue : asIsOrder"
|
||||
[label]="group.key.toUpperCase()"
|
||||
>
|
||||
<button
|
||||
*ngFor="let action of group.value"
|
||||
tuiOption
|
||||
class="item"
|
||||
(click)="action.action()"
|
||||
>
|
||||
<tui-svg class="icon" [src]="action.icon"></tui-svg>
|
||||
{{ action.label }}
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
</tui-data-list>
|
||||
@@ -0,0 +1,16 @@
|
||||
.title {
|
||||
margin: 0;
|
||||
padding: 0 0.5rem 0.25rem;
|
||||
white-space: nowrap;
|
||||
font: var(--tui-font-text-l);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.item {
|
||||
justify-content: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.icon {
|
||||
opacity: var(--tui-disabled-opacity);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { TuiDataListModule, TuiSvgModule } from '@taiga-ui/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
|
||||
export interface Action {
|
||||
icon: string
|
||||
label: string
|
||||
action: () => void
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-actions',
|
||||
templateUrl: './actions.component.html',
|
||||
styleUrls: ['./actions.component.scss'],
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiDataListModule, TuiSvgModule, CommonModule],
|
||||
})
|
||||
export class ActionsComponent {
|
||||
@Input()
|
||||
actions: Record<string, readonly Action[]> = {}
|
||||
|
||||
asIsOrder(a: any, b: any) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
<span class="link">
|
||||
<img alt="" class="icon" [src]="appCard.icon" />
|
||||
<label ticker class="title">{{ appCard.title }}</label>
|
||||
<img alt="" class="icon" [src]="icon" />
|
||||
<label ticker class="title">{{ title }}</label>
|
||||
</span>
|
||||
<span class="side">
|
||||
<tui-hosted-dropdown [content]="content" (click.stop.prevent)="(0)">
|
||||
<tui-hosted-dropdown
|
||||
#dropdown
|
||||
[content]="content"
|
||||
(click.stop.prevent)="(0)"
|
||||
(pointerdown.stop)="(0)"
|
||||
>
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="outline"
|
||||
@@ -14,26 +19,12 @@
|
||||
Actions
|
||||
</button>
|
||||
<ng-template #content>
|
||||
<!-- TODO: Move menu to a separate component -->
|
||||
<tui-data-list>
|
||||
<h3 class="menu-title">{{ appCard.title }}</h3>
|
||||
<tui-opt-group label="LAUNCH">
|
||||
<button tuiOption class="menu-item">
|
||||
<tui-svg src="tuiIconLogOut" class="menu-icon"></tui-svg>
|
||||
Tor
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
<tui-opt-group label="MANAGE">
|
||||
<button tuiOption class="menu-item">
|
||||
<tui-svg src="tuiIconSliders" class="menu-icon"></tui-svg>
|
||||
Console
|
||||
</button>
|
||||
<button tuiOption class="menu-item">
|
||||
<tui-svg src="tuiIconX" class="menu-icon"></tui-svg>
|
||||
Remove from desktop
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
</tui-data-list>
|
||||
<app-actions
|
||||
[actions]="(actions | toDesktopActions : id | async) || {}"
|
||||
(click)="dropdown.openChange.next(false)"
|
||||
>
|
||||
{{ title }}
|
||||
</app-actions>
|
||||
</ng-template>
|
||||
</tui-hosted-dropdown>
|
||||
</span>
|
||||
|
||||
@@ -43,20 +43,3 @@
|
||||
// TODO: Theme
|
||||
background: #4b4a4a;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
margin: 0;
|
||||
padding: 0 0.5rem 0.25rem;
|
||||
white-space: nowrap;
|
||||
font: var(--tui-font-text-l);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
justify-content: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
opacity: var(--tui-disabled-opacity);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
@@ -13,10 +14,10 @@ import {
|
||||
TuiHostedDropdownModule,
|
||||
TuiSvgModule,
|
||||
} from '@taiga-ui/core'
|
||||
import {
|
||||
NavigationItem,
|
||||
NavigationService,
|
||||
} from '../navigation/navigation.service'
|
||||
import { NavigationService } from '../navigation/navigation.service'
|
||||
import { Action, ActionsComponent } from '../actions/actions.component'
|
||||
import { ToDesktopActionsPipe } from '../../pipes/to-desktop-actions'
|
||||
import { toRouterLink } from '../../utils/to-router-link'
|
||||
|
||||
@Component({
|
||||
selector: '[appCard]',
|
||||
@@ -25,22 +26,37 @@ import {
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterLink,
|
||||
TuiButtonModule,
|
||||
TuiHostedDropdownModule,
|
||||
TuiDataListModule,
|
||||
TuiSvgModule,
|
||||
TickerModule,
|
||||
ActionsComponent,
|
||||
ToDesktopActionsPipe,
|
||||
],
|
||||
})
|
||||
export class CardComponent {
|
||||
private readonly navigation = inject(NavigationService)
|
||||
|
||||
@Input({ required: true })
|
||||
appCard!: NavigationItem
|
||||
@Input()
|
||||
id = ''
|
||||
|
||||
@Input()
|
||||
icon = ''
|
||||
|
||||
@Input()
|
||||
title = ''
|
||||
|
||||
@Input()
|
||||
actions: Record<string, readonly Action[]> = {}
|
||||
|
||||
@HostListener('click')
|
||||
onClick() {
|
||||
this.navigation.addTab(this.appCard)
|
||||
const { id, icon, title } = this
|
||||
const routerLink = toRouterLink(id)
|
||||
|
||||
this.navigation.addTab({ icon, title, routerLink })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,26 +13,37 @@
|
||||
>
|
||||
Enter service name
|
||||
</tui-input>
|
||||
<h2 class="title">System Utilities</h2>
|
||||
<div class="items">
|
||||
<a
|
||||
*ngFor="let item of system | tuiFilter : bySearch : search; empty: empty"
|
||||
[appCard]="item"
|
||||
[routerLink]="item.routerLink"
|
||||
(click)="open = false"
|
||||
></a>
|
||||
</div>
|
||||
<h2 class="title">Installed services</h2>
|
||||
<div class="items">
|
||||
<a
|
||||
*ngFor="
|
||||
let item of (services$ | async) || [] | tuiFilter : bySearch : search;
|
||||
empty: empty
|
||||
"
|
||||
[appCard]="item"
|
||||
[routerLink]="item.routerLink"
|
||||
(click)="open = false"
|
||||
></a>
|
||||
</div>
|
||||
<ng-template #empty>Nothing found</ng-template>
|
||||
<tui-scrollbar class="scrollbar">
|
||||
<h2 class="title">System Utilities</h2>
|
||||
<div class="items">
|
||||
<a
|
||||
*ngFor="
|
||||
let item of system | keyvalue | tuiFilter : bySearch : search;
|
||||
empty: empty
|
||||
"
|
||||
appCard
|
||||
[id]="item.key"
|
||||
[title]="item.value.title"
|
||||
[icon]="item.value.icon"
|
||||
[routerLink]="item.key"
|
||||
(click)="open = false"
|
||||
></a>
|
||||
</div>
|
||||
<h2 class="title">Installed services</h2>
|
||||
<div class="items">
|
||||
<a
|
||||
*ngFor="
|
||||
let item of (services$ | async) || [] | tuiFilter : bySearch : search;
|
||||
empty: empty
|
||||
"
|
||||
appCard
|
||||
[id]="item.manifest.id"
|
||||
[icon]="item.icon"
|
||||
[title]="item.manifest.title"
|
||||
[routerLink]="getLink(item.manifest.id)"
|
||||
(click)="open = false"
|
||||
></a>
|
||||
</div>
|
||||
<ng-template #empty>Nothing found</ng-template>
|
||||
</tui-scrollbar>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
min-height: calc(100% - 10.25rem);
|
||||
height: calc(100% - 10.25rem);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// TODO: Theme
|
||||
@@ -21,6 +21,9 @@
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
@@ -48,13 +51,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.search {
|
||||
max-width: 41rem;
|
||||
width: 25rem;
|
||||
margin: 6rem auto 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 5rem 0 1.25rem;
|
||||
margin: 4rem 0 1.25rem;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
font: var(--tui-font-text-xl);
|
||||
|
||||
@@ -13,14 +13,16 @@ import {
|
||||
TuiFilterPipeModule,
|
||||
TuiForModule,
|
||||
} from '@taiga-ui/cdk'
|
||||
import { TuiSvgModule, TuiTextfieldControllerModule } from '@taiga-ui/core'
|
||||
import {
|
||||
TuiScrollbarModule,
|
||||
TuiSvgModule,
|
||||
TuiTextfieldControllerModule,
|
||||
} from '@taiga-ui/core'
|
||||
import { TuiInputModule } from '@taiga-ui/kit'
|
||||
import { map } from 'rxjs'
|
||||
import { CardComponent } from '../card/card.component'
|
||||
import { NavigationItem } from '../navigation/navigation.service'
|
||||
import { ServicesService } from '../../services/services.service'
|
||||
import { SYSTEM_UTILITIES } from './drawer.const'
|
||||
import { toNavigationItem } from '../../utils/to-navigation-item'
|
||||
import { toRouterLink } from '../../utils/to-router-link'
|
||||
|
||||
@Component({
|
||||
selector: 'app-drawer',
|
||||
@@ -32,6 +34,7 @@ import { toNavigationItem } from '../../utils/to-navigation-item'
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
TuiSvgModule,
|
||||
TuiScrollbarModule,
|
||||
TuiActiveZoneModule,
|
||||
TuiInputModule,
|
||||
TuiTextfieldControllerModule,
|
||||
@@ -48,10 +51,13 @@ export class DrawerComponent {
|
||||
search = ''
|
||||
|
||||
readonly system = SYSTEM_UTILITIES
|
||||
readonly services$ = inject(ServicesService).pipe(
|
||||
map(services => services.map(toNavigationItem)),
|
||||
)
|
||||
readonly services$ = inject(ServicesService)
|
||||
|
||||
readonly bySearch = (item: NavigationItem, search: string): boolean =>
|
||||
search.length < 2 || TUI_DEFAULT_MATCHER(item.title, search)
|
||||
readonly bySearch = (item: any, search: string): boolean =>
|
||||
search.length < 2 ||
|
||||
TUI_DEFAULT_MATCHER(item.manifest?.title || item.value?.title || '', search)
|
||||
|
||||
getLink(id: string): string {
|
||||
return toRouterLink(id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
import { NavigationItem } from '../navigation/navigation.service'
|
||||
|
||||
export const SYSTEM_UTILITIES: readonly NavigationItem[] = [
|
||||
export const SYSTEM_UTILITIES: Record<string, { icon: string; title: string }> =
|
||||
{
|
||||
title: 'Devices',
|
||||
routerLink: 'devices',
|
||||
icon: 'assets/img/icon_transparent.png',
|
||||
},
|
||||
{
|
||||
title: 'Metrics',
|
||||
routerLink: 'metrics',
|
||||
icon: 'assets/img/icon_transparent.png',
|
||||
},
|
||||
{
|
||||
title: 'User manual',
|
||||
routerLink: 'manual',
|
||||
icon: 'assets/img/icon_transparent.png',
|
||||
},
|
||||
{
|
||||
title: 'Snek',
|
||||
routerLink: 'snek',
|
||||
icon: 'assets/img/icon_transparent.png',
|
||||
},
|
||||
]
|
||||
'/portal/system/devices': {
|
||||
icon: 'assets/img/icon_transparent.png',
|
||||
title: 'Devices',
|
||||
},
|
||||
'/portal/system/metrics': {
|
||||
icon: 'assets/img/icon_transparent.png',
|
||||
title: 'Metrics',
|
||||
},
|
||||
'/portal/system/manual': {
|
||||
icon: 'assets/img/icon_transparent.png',
|
||||
title: 'Manual',
|
||||
},
|
||||
'/portal/system/snek': {
|
||||
icon: 'assets/img/icon_transparent.png',
|
||||
title: 'Snek',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { inject, Pipe, PipeTransform } from '@angular/core'
|
||||
import { Action } from '../components/actions/actions.component'
|
||||
import { filter, map, Observable } from 'rxjs'
|
||||
import { DesktopService } from '../routes/desktop/desktop.service'
|
||||
|
||||
@Pipe({
|
||||
name: 'toDesktopActions',
|
||||
standalone: true,
|
||||
})
|
||||
export class ToDesktopActionsPipe implements PipeTransform {
|
||||
private readonly desktop = inject(DesktopService)
|
||||
|
||||
transform(
|
||||
value: Record<string, readonly Action[]>,
|
||||
id: string,
|
||||
): Observable<Record<string, readonly Action[]>> {
|
||||
return this.desktop.desktop$.pipe(
|
||||
filter(Boolean),
|
||||
map(desktop => {
|
||||
const action = desktop.includes(id)
|
||||
? {
|
||||
icon: 'tuiIconMinus',
|
||||
label: 'Remove from Desktop',
|
||||
action: () => this.desktop.remove(id),
|
||||
}
|
||||
: {
|
||||
icon: 'tuiIconPlus',
|
||||
label: 'Add to Desktop',
|
||||
action: () => this.desktop.add(id),
|
||||
}
|
||||
|
||||
return {
|
||||
manage: [action],
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { SYSTEM_UTILITIES } from '../components/drawer/drawer.const'
|
||||
import { NavigationItem } from '../components/navigation/navigation.service'
|
||||
import { toRouterLink } from '../utils/to-router-link'
|
||||
|
||||
@Pipe({
|
||||
name: 'toDesktopItem',
|
||||
standalone: true,
|
||||
})
|
||||
export class ToDesktopItemPipe implements PipeTransform {
|
||||
private readonly system = SYSTEM_UTILITIES
|
||||
|
||||
transform(
|
||||
packages: Record<string, PackageDataEntry>,
|
||||
id: string,
|
||||
): NavigationItem {
|
||||
const item = SYSTEM_UTILITIES[id]
|
||||
const routerLink = toRouterLink(id)
|
||||
|
||||
if (SYSTEM_UTILITIES[id]) {
|
||||
return {
|
||||
icon: item.icon,
|
||||
title: item.title,
|
||||
routerLink,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
icon: packages[id].icon,
|
||||
title: packages[id].manifest.title,
|
||||
routerLink,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { NavigationItem } from '../components/navigation/navigation.service'
|
||||
import { toNavigationItem } from '../utils/to-navigation-item'
|
||||
|
||||
@Pipe({
|
||||
name: 'toNavigationItem',
|
||||
standalone: true,
|
||||
})
|
||||
export class ToNavigationItemPipe implements PipeTransform {
|
||||
transform(service: PackageDataEntry): NavigationItem {
|
||||
return toNavigationItem(service)
|
||||
}
|
||||
}
|
||||
@@ -6,4 +6,5 @@
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,11 @@ const ROUTES: Routes = [
|
||||
m => m.ServicesModule,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'system',
|
||||
loadChildren: () =>
|
||||
import('./routes/system/system.module').then(m => m.SystemModule),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
<a
|
||||
*ngFor="let service of services$ | async"
|
||||
[appCard]="service | toNavigationItem"
|
||||
[routerLink]="(service | toNavigationItem).routerLink"
|
||||
></a>
|
||||
<ng-container *ngIf="desktop$ | async as desktop">
|
||||
<tui-tiles
|
||||
*ngIf="packages$ | async as packages"
|
||||
class="tiles"
|
||||
[debounce]="500"
|
||||
[order]="order"
|
||||
(orderChange)="onReorder($event, desktop)"
|
||||
>
|
||||
<tui-tile
|
||||
*ngFor="let service of desktop; let index = index"
|
||||
class="item"
|
||||
[style.order]="order.get(index)"
|
||||
>
|
||||
<a
|
||||
*ngIf="packages | toDesktopItem : service as item"
|
||||
tuiTileHandle
|
||||
appCard
|
||||
[id]="service"
|
||||
[title]="item.title"
|
||||
[icon]="item.icon"
|
||||
[routerLink]="item.routerLink"
|
||||
></a>
|
||||
</tui-tile>
|
||||
</tui-tiles>
|
||||
</ng-container>
|
||||
|
||||
@@ -3,9 +3,16 @@
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
height: 100%;
|
||||
max-width: 56rem;
|
||||
margin: 0 auto;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.tiles {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
grid-template-columns: repeat(auto-fit, 12.5rem);
|
||||
grid-auto-rows: 5.5rem;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { ServicesService } from '../../services/services.service'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { tap } from 'rxjs'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { DesktopService } from './desktop.service'
|
||||
|
||||
@Component({
|
||||
templateUrl: 'desktop.component.html',
|
||||
@@ -7,6 +10,26 @@ import { ServicesService } from '../../services/services.service'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DesktopComponent {
|
||||
// TODO: Only show services added to desktop
|
||||
readonly services$ = inject(ServicesService)
|
||||
private readonly desktop = inject(DesktopService)
|
||||
|
||||
readonly desktop$ = this.desktop.desktop$.pipe(
|
||||
tap(() => (this.order = new Map())),
|
||||
)
|
||||
|
||||
readonly packages$ =
|
||||
inject<PatchDB<DataModel>>(PatchDB).watch$('package-data')
|
||||
|
||||
order = new Map()
|
||||
|
||||
onReorder(order: Map<number, number>, desktop: readonly string[]) {
|
||||
this.order = order
|
||||
|
||||
const items: string[] = []
|
||||
|
||||
Array.from(this.order.entries()).forEach(([index, order]) => {
|
||||
items[order] = desktop[index]
|
||||
})
|
||||
|
||||
this.desktop.save(items)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { TuiTilesModule } from '@taiga-ui/kit'
|
||||
import { DesktopComponent } from './desktop.component'
|
||||
import { CardComponent } from '../../components/card/card.component'
|
||||
import { ToNavigationItemPipe } from '../../pipes/to-navigation-item'
|
||||
import { ToDesktopActionsPipe } from '../../pipes/to-desktop-actions'
|
||||
import { ToDesktopItemPipe } from '../../pipes/to-desktop-item'
|
||||
|
||||
const ROUTES: Routes = [
|
||||
{
|
||||
@@ -16,7 +18,9 @@ const ROUTES: Routes = [
|
||||
imports: [
|
||||
CommonModule,
|
||||
CardComponent,
|
||||
ToNavigationItemPipe,
|
||||
TuiTilesModule,
|
||||
ToDesktopActionsPipe,
|
||||
ToDesktopItemPipe,
|
||||
RouterModule.forChild(ROUTES),
|
||||
],
|
||||
declarations: [DesktopComponent],
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { TuiAlertService } from '@taiga-ui/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { BehaviorSubject, first } from 'rxjs'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class DesktopService {
|
||||
private readonly alerts = inject(TuiAlertService)
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
readonly desktop$ = new BehaviorSubject<readonly string[] | undefined>(
|
||||
undefined,
|
||||
)
|
||||
|
||||
constructor() {
|
||||
inject<PatchDB<DataModel>>(PatchDB)
|
||||
.watch$('ui', 'desktop')
|
||||
.pipe(first())
|
||||
.subscribe(desktop => {
|
||||
if (!this.desktop$.value) {
|
||||
this.desktop$.next(desktop)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
add(id: string) {
|
||||
this.desktop$.next(this.desktop$.value?.concat(id))
|
||||
this.save(this.desktop$.value)
|
||||
}
|
||||
|
||||
remove(id: string) {
|
||||
this.desktop$.next(this.desktop$.value?.filter(x => x !== id))
|
||||
this.save(this.desktop$.value)
|
||||
}
|
||||
|
||||
save(ids: readonly string[] = []) {
|
||||
this.api
|
||||
.setDbValue(['desktop'], ids)
|
||||
.catch(() =>
|
||||
this.alerts
|
||||
.open(
|
||||
'Desktop might be out of sync. Please refresh the page to fix it.',
|
||||
{ status: 'warning' },
|
||||
)
|
||||
.subscribe(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { PatchDB } from 'patch-db-client'
|
||||
import { tap } from 'rxjs'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { NavigationService } from '../../components/navigation/navigation.service'
|
||||
import { toRouterLink } from '../../utils/to-router-link'
|
||||
|
||||
@Component({
|
||||
templateUrl: 'service.component.html',
|
||||
@@ -26,9 +27,9 @@ export class ServiceComponent {
|
||||
this.router.navigate(['..'], { relativeTo: this.route })
|
||||
} else {
|
||||
this.navigation.addTab({
|
||||
title: pkg.manifest.title,
|
||||
routerLink: `/portal/services/${pkg.manifest.id}`,
|
||||
icon: pkg.icon,
|
||||
title: pkg.manifest.title,
|
||||
routerLink: toRouterLink(pkg.manifest.id),
|
||||
})
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
template: 'Here be snek',
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SnekComponent {}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
|
||||
const ROUTES: Routes = [
|
||||
{
|
||||
path: 'snek',
|
||||
loadComponent: () =>
|
||||
import('./snek/snek.component').then(m => m.SnekComponent),
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(ROUTES)],
|
||||
declarations: [],
|
||||
exports: [],
|
||||
})
|
||||
export class SystemModule {}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { NavigationItem } from '../components/navigation/navigation.service'
|
||||
|
||||
export function toNavigationItem({
|
||||
manifest,
|
||||
icon,
|
||||
}: PackageDataEntry): NavigationItem {
|
||||
return {
|
||||
title: manifest.title,
|
||||
routerLink: `/portal/services/${manifest.id}`,
|
||||
icon,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function toRouterLink(id: string): string {
|
||||
return id.includes('/') ? id : `/portal/services/${id}`
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { TuiAlertModule } from '@start9labs/shared'
|
||||
import { TuiAutoFocusModule } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TuiAlertModule,
|
||||
TuiButtonModule,
|
||||
TuiDialogModule,
|
||||
} from '@taiga-ui/core'
|
||||
|
||||
import { ToastContainerComponent } from './toast-container.component'
|
||||
import { NotificationsToastComponent } from './notifications-toast/notifications-toast.component'
|
||||
import { RefreshAlertComponent } from './refresh-alert/refresh-alert.component'
|
||||
import { UpdateToastComponent } from './update-toast/update-toast.component'
|
||||
import { TuiButtonModule, TuiDialogModule } from '@taiga-ui/core'
|
||||
import { TuiAutoFocusModule } from '@taiga-ui/cdk'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
||||
@@ -7,6 +7,7 @@ export const mockPatchData: DataModel = {
|
||||
name: `Matt's Server`,
|
||||
'ack-welcome': '1.0.0',
|
||||
theme: 'Dark',
|
||||
desktop: ['lnd'],
|
||||
widgets: BUILT_IN_WIDGETS.filter(
|
||||
({ id }) =>
|
||||
id === 'favorites' ||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Url } from '@start9labs/shared'
|
||||
import { Manifest } from '@start9labs/marketplace'
|
||||
import { BackupJob } from '../api/api.types'
|
||||
import { customSmtp } from '@start9labs/start-sdk/lib/config/configConstants'
|
||||
import { CustomSpec } from 'src/app/apps/ui/pages/system/domains/domain.const'
|
||||
|
||||
export interface DataModel {
|
||||
'server-info': ServerInfo
|
||||
@@ -23,6 +22,7 @@ export interface UIData {
|
||||
'ack-instructions': Record<string, boolean>
|
||||
theme: string
|
||||
widgets: readonly Widget[]
|
||||
desktop: readonly string[]
|
||||
}
|
||||
|
||||
export interface Widget {
|
||||
|
||||
Reference in New Issue
Block a user