fix: address comments (#3044)

* fix: address comments

* fix unread notification mocks

* fix row click for notification

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
Alex Inkin
2025-11-07 01:17:57 +04:00
committed by GitHub
parent 515d37147b
commit 66cb9a93b8
19 changed files with 115 additions and 145 deletions

View File

@@ -368,6 +368,7 @@
"styles": [ "styles": [
"node_modules/@taiga-ui/core/styles/taiga-ui-theme.less", "node_modules/@taiga-ui/core/styles/taiga-ui-theme.less",
"node_modules/@taiga-ui/core/styles/taiga-ui-fonts.less", "node_modules/@taiga-ui/core/styles/taiga-ui-fonts.less",
"projects/shared/styles/shared.scss",
"projects/start-tunnel/src/styles.scss" "projects/start-tunnel/src/styles.scss"
], ],
"scripts": [] "scripts": []

View File

@@ -32,7 +32,7 @@ import { MarketplaceItemComponent } from './item.component'
let-completeWith="completeWith" let-completeWith="completeWith"
> >
<tui-radio-list [items]="versions()" [(ngModel)]="data.version" /> <tui-radio-list [items]="versions()" [(ngModel)]="data.version" />
<footer class="buttons"> <footer class="g-buttons">
<button <button
tuiButton tuiButton
appearance="secondary" appearance="secondary"

View File

@@ -12,15 +12,15 @@ import { getErrorMessage } from '../services/error.service'
@Component({ @Component({
template: ` template: `
@if (error()) { @if (error()) {
<tui-notification appearance="negative" safeLinks> <tui-notification appearance="negative" safeLinks [innerHTML]="error()" />
{{ error() }}
</tui-notification>
} }
@if (content(); as result) { @if (content(); as result) {
<div safeLinks [innerHTML]="result | markdown | dompurify"></div> <div safeLinks [innerHTML]="result | markdown | dompurify"></div>
} @else { } @else {
<tui-loader textContent="Loading" [style.height.%]="100" /> @if (!error()) {
<tui-loader textContent="Loading" [style.height.%]="100" />
}
} }
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@@ -34,14 +34,10 @@ import { getErrorMessage } from '../services/error.service'
], ],
}) })
export class MarkdownComponent { export class MarkdownComponent {
private readonly data = protected readonly data = injectContext<{ data: Observable<string> }>().data
injectContext<TuiDialogContext<void, { content: Observable<string> }>>({ protected readonly content = toSignal<string>(this.data)
optional: true, protected readonly error = toSignal(
})?.data || inject(ActivatedRoute).snapshot.data this.data.pipe(
readonly content = toSignal<string>(this.data['content'])
readonly error = toSignal(
this.data['content'].pipe(
ignoreElements(), ignoreElements(),
catchError(e => of(getErrorMessage(e))), catchError(e => of(getErrorMessage(e))),
), ),

View File

@@ -95,6 +95,9 @@ $wide-modal: 900px;
--tw-color-zinc-800: 39 39 42; --tw-color-zinc-800: 39 39 42;
--tw-color-zinc-900: 24 24 27; --tw-color-zinc-900: 24 24 27;
--tw-color-zinc-950: 9 9 11; --tw-color-zinc-950: 9 9 11;
--tui-font-text: 'Proxima Nova', system-ui;
--tui-font-heading: 'Proxima Nova', system-ui;
} }
body { body {
@@ -172,14 +175,6 @@ a {
font-weight: 300; font-weight: 300;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.06rem; letter-spacing: 0.06rem;
margin: 0rem 0 1rem 0; margin: 0 0 1rem 0;
pointer-events: none; pointer-events: none;
} }
.buttons {
margin-top: 1rem;
:first-child {
margin-right: 0.5rem;
}
}

View File

@@ -182,16 +182,14 @@ export class MarketplacePreviewComponent {
} }
onStatic() { onStatic() {
const content = this.pkg$.pipe(
filter(Boolean),
switchMap(pkg => this.marketplaceService.fetchStatic$(pkg)),
)
this.dialog this.dialog
.openComponent(MARKDOWN, { .openComponent(MARKDOWN, {
label: 'License', label: 'License',
size: 'l', size: 'l',
data: { content }, data: this.pkg$.pipe(
filter(Boolean),
switchMap(pkg => this.marketplaceService.fetchStatic$(pkg)),
),
}) })
.subscribe() .subscribe()
} }

View File

@@ -67,7 +67,6 @@ import { i18nPipe } from '@start9labs/shared'
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
host: { host: {
'[class._new]': '!notificationItem.seen', '[class._new]': '!notificationItem.seen',
'(click)': 'onClick()',
}, },
styles: ` styles: `
@use '@taiga-ui/core/styles/taiga-ui-local' as taiga; @use '@taiga-ui/core/styles/taiga-ui-local' as taiga;

View File

@@ -1,7 +1,9 @@
import { TUI_IS_MOBILE } from '@taiga-ui/cdk'
import { TuiCheckbox, TuiSkeleton } from '@taiga-ui/kit' import { TuiCheckbox, TuiSkeleton } from '@taiga-ui/kit'
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
inject,
Input, Input,
OnChanges, OnChanges,
signal, signal,
@@ -43,7 +45,9 @@ import { i18nPipe } from '@start9labs/shared'
[notificationItem]="notification" [notificationItem]="notification"
(longtap)="!selected().length && onToggle(notification)" (longtap)="!selected().length && onToggle(notification)"
(click.capture)=" (click.capture)="
selected().length && onToggle(notification, $event) selected().length &&
$any($event.target).closest('tui-root._mobile') &&
onToggle(notification, $event)
" "
> >
<input <input
@@ -81,6 +85,7 @@ import { i18nPipe } from '@start9labs/shared'
top: 0.875rem; top: 0.875rem;
left: 1rem; left: 1rem;
z-index: 1; z-index: 1;
pointer-events: none;
} }
:host:not(:has(:checked)) input { :host:not(:has(:checked)) input {

View File

@@ -1,9 +1,4 @@
import { import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
ChangeDetectionStrategy,
Component,
inject,
INJECTOR,
} from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop' import { toSignal } from '@angular/core/rxjs-interop'
import { import {
CopyService, CopyService,
@@ -11,12 +6,12 @@ import {
getPkgId, getPkgId,
i18nKey, i18nKey,
i18nPipe, i18nPipe,
MarkdownComponent, MARKDOWN,
} from '@start9labs/shared' } from '@start9labs/shared'
import { TuiCell } from '@taiga-ui/layout' import { TuiCell } from '@taiga-ui/layout'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { map } from 'rxjs' import { from, map } from 'rxjs'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
import { getManifest } from 'src/app/utils/get-package-data' import { getManifest } from 'src/app/utils/get-package-data'
import { import {
@@ -58,15 +53,17 @@ import {
imports: [ServiceAdditionalItemComponent, TuiCell, i18nPipe], imports: [ServiceAdditionalItemComponent, TuiCell, i18nPipe],
}) })
export default class ServiceAboutRoute { export default class ServiceAboutRoute {
private readonly pkgId = getPkgId()
private readonly copyService = inject(CopyService) private readonly copyService = inject(CopyService)
private readonly markdown = inject(DialogService).openComponent( private readonly markdown = inject(DialogService).openComponent(MARKDOWN, {
new PolymorpheusComponent(MarkdownComponent, inject(INJECTOR)), label: 'License',
{ label: 'License', size: 'l' }, size: 'l',
) data: from(inject(ApiService).getStaticInstalled(this.pkgId, 'LICENSE.md')),
})
readonly groups = toSignal<{ header: i18nKey; items: AdditionalItem[] }[]>( readonly groups = toSignal<{ header: i18nKey; items: AdditionalItem[] }[]>(
inject<PatchDB<DataModel>>(PatchDB) inject<PatchDB<DataModel>>(PatchDB)
.watch$('packageData', getPkgId()) .watch$('packageData', this.pkgId)
.pipe( .pipe(
map(pkg => { map(pkg => {
const manifest = getManifest(pkg) const manifest = getManifest(pkg)

View File

@@ -1,8 +1,4 @@
import { inject } from '@angular/core' import { Routes } from '@angular/router'
import { ActivatedRouteSnapshot, ResolveFn, Routes } from '@angular/router'
import { defer, map, Observable, of } from 'rxjs'
import { share } from 'rxjs/operators'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { titleResolver } from 'src/app/utils/title-resolver' import { titleResolver } from 'src/app/utils/title-resolver'
import { ServiceOutletComponent } from './routes/outlet.component' import { ServiceOutletComponent } from './routes/outlet.component'
@@ -33,7 +29,6 @@ export const ROUTES: Routes = [
{ {
path: 'about', path: 'about',
loadComponent: () => import('./routes/about.component'), loadComponent: () => import('./routes/about.component'),
resolve: { content: getStatic() },
}, },
], ],
}, },
@@ -44,15 +39,4 @@ export const ROUTES: Routes = [
}, },
] ]
function getStatic(): ResolveFn<Observable<string>> {
return ({ paramMap }: ActivatedRouteSnapshot) =>
of(inject(ApiService)).pipe(
map(api =>
defer(() =>
api.getStaticInstalled(paramMap.get('pkgId')!, 'LICENSE.md'),
).pipe(share()),
),
)
}
export default ROUTES export default ROUTES

View File

@@ -87,13 +87,11 @@ export class SideloadPackageComponent {
readonly file = input.required<File>() readonly file = input.required<File>()
onStatic() { onStatic() {
const content = of(this.pkg()['license'])
this.dialog this.dialog
.openComponent(MARKDOWN, { .openComponent(MARKDOWN, {
label: 'License', label: 'License',
size: 'l', size: 'l',
data: { content }, data: of(this.pkg()['license']),
}) })
.subscribe() .subscribe()
} }

View File

@@ -41,7 +41,11 @@ import { i18nPipe } from '@start9labs/shared'
@for (session of sessions(); track $index) { @for (session of sessions(); track $index) {
<tr <tr
(longtap)="!selected().length && onToggle(session)" (longtap)="!selected().length && onToggle(session)"
(click)="selected().length && onToggle(session)" (click)="
selected().length &&
$any($event.target).closest('tui-root._mobile') &&
onToggle(session)
"
> >
<td [style.padding-left.rem]="single() ? null : 2.5"> <td [style.padding-left.rem]="single() ? null : 2.5">
@if (!single()) { @if (!single()) {
@@ -123,6 +127,7 @@ import { i18nPipe } from '@start9labs/shared'
input { input {
left: 0.25rem; left: 0.25rem;
pointer-events: none;
} }
td { td {

View File

@@ -31,7 +31,11 @@ import { SSHKey } from 'src/app/services/api/api.types'
@for (key of keys(); track $index) { @for (key of keys(); track $index) {
<tr <tr
(longtap)="!selected().length && onToggle(key)" (longtap)="!selected().length && onToggle(key)"
(click)="selected().length && onToggle(key)" (click)="
selected().length &&
$any($event.target).closest('tui-root._mobile') &&
onToggle(key)
"
> >
<td [style.padding-left.rem]="2.5"> <td [style.padding-left.rem]="2.5">
<input <input
@@ -104,6 +108,7 @@ import { SSHKey } from 'src/app/services/api/api.types'
input { input {
left: 0.25rem; left: 0.25rem;
pointer-events: none;
} }
td { td {

View File

@@ -149,6 +149,11 @@ import UpdatesComponent from './updates.component'
font-size: 1rem; font-size: 1rem;
} }
tui-progress-circle {
display: inline-block;
vertical-align: middle;
}
div { div {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -172,12 +177,7 @@ import UpdatesComponent from './updates.component'
} }
&:last-child { &:last-child {
text-align: right;
white-space: nowrap; white-space: nowrap;
div {
justify-content: flex-start;
}
} }
&[colspan]:only-child { &[colspan]:only-child {

View File

@@ -19,19 +19,17 @@ import {
TuiSkeleton, TuiSkeleton,
} from '@taiga-ui/kit' } from '@taiga-ui/kit'
import { TuiCell } from '@taiga-ui/layout' import { TuiCell } from '@taiga-ui/layout'
import { PatchDB } from 'patch-db-client' import { combineLatest, tap } from 'rxjs'
import { combineLatest, map, tap } from 'rxjs'
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component' import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
import { TableComponent } from 'src/app/routes/portal/components/table.component' import { TableComponent } from 'src/app/routes/portal/components/table.component'
import { LocalPackagesService } from 'src/app/services/local-packages.service'
import { MarketplaceService } from 'src/app/services/marketplace.service' import { MarketplaceService } from 'src/app/services/marketplace.service'
import { import {
DataModel,
InstalledState, InstalledState,
PackageDataEntry, PackageDataEntry,
UpdatingState, UpdatingState,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { TitleDirective } from 'src/app/services/title.service' import { TitleDirective } from 'src/app/services/title.service'
import { isInstalled, isUpdating } from 'src/app/utils/get-package-data'
import { FilterUpdatesPipe } from './filter-updates.pipe' import { FilterUpdatesPipe } from './filter-updates.pipe'
import { UpdatesItemComponent } from './item.component' import { UpdatesItemComponent } from './item.component'
import { i18nPipe } from '@start9labs/shared' import { i18nPipe } from '@start9labs/shared'
@@ -256,21 +254,7 @@ export default class UpdatesComponent {
), ),
), ),
marketplace: this.marketplaceService.marketplace$, marketplace: this.marketplaceService.marketplace$,
localPkgs: inject<PatchDB<DataModel>>(PatchDB) localPkgs: inject(LocalPackagesService),
.watch$('packageData')
.pipe(
map(pkgs =>
Object.entries(pkgs).reduce<
Record<string, PackageDataEntry<InstalledState | UpdatingState>>
>(
(acc, [id, val]) =>
isInstalled(val) || isUpdating(val)
? { ...acc, [id]: val }
: acc,
{},
),
),
),
errors: this.marketplaceService.requestErrors$, errors: this.marketplaceService.requestErrors$,
}), }),
) )

View File

@@ -194,7 +194,7 @@ export const mockPatchData: DataModel = {
staticServers: null, staticServers: null,
}, },
}, },
unreadNotificationCount: 4, unreadNotificationCount: 5,
// password is asdfasdf // password is asdfasdf
passwordHash: passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',

View File

@@ -1,23 +1,11 @@
import { inject, Injectable } from '@angular/core' import { inject, Injectable } from '@angular/core'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { import { combineLatest, EMPTY, map, Observable, shareReplay } from 'rxjs'
combineLatest, import { LocalPackagesService } from 'src/app/services/local-packages.service'
EMPTY,
filter,
first,
map,
Observable,
pairwise,
shareReplay,
startWith,
switchMap,
} from 'rxjs'
import { ConnectionService } from 'src/app/services/connection.service'
import { OSService } from 'src/app/services/os.service'
import { MarketplaceService } from 'src/app/services/marketplace.service' import { MarketplaceService } from 'src/app/services/marketplace.service'
import { NotificationService } from 'src/app/services/notification.service' import { NotificationService } from 'src/app/services/notification.service'
import { OSService } from 'src/app/services/os.service'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
import { getManifest } from 'src/app/utils/get-package-data'
import { FilterUpdatesPipe } from '../routes/portal/routes/updates/filter-updates.pipe' import { FilterUpdatesPipe } from '../routes/portal/routes/updates/filter-updates.pipe'
@Injectable({ @Injectable({
@@ -35,32 +23,9 @@ export class BadgeService {
private readonly marketplaceService = inject(MarketplaceService) private readonly marketplaceService = inject(MarketplaceService)
private readonly filterUpdatesPipe = inject(FilterUpdatesPipe) private readonly filterUpdatesPipe = inject(FilterUpdatesPipe)
private readonly local$ = inject(ConnectionService).pipe(
filter(Boolean),
switchMap(() => this.patch.watch$('packageData').pipe(first())),
switchMap(outer =>
this.patch.watch$('packageData').pipe(
pairwise(),
filter(([prev, curr]) =>
Object.values(prev).some(p => {
const { id } = getManifest(p)
return (
!curr[id] ||
(p.stateInfo.installingInfo &&
!curr[id]?.stateInfo.installingInfo)
)
}),
),
map(([_, curr]) => curr),
startWith(outer),
),
),
)
private readonly updates$ = combineLatest([ private readonly updates$ = combineLatest([
this.marketplaceService.marketplace$, this.marketplaceService.marketplace$,
this.local$, inject(LocalPackagesService),
]).pipe( ]).pipe(
map( map(
([marketplace, local]) => ([marketplace, local]) =>

View File

@@ -0,0 +1,34 @@
import { inject, Injectable } from '@angular/core'
import { PatchDB } from 'patch-db-client'
import { map, Observable, shareReplay } from 'rxjs'
import {
DataModel,
InstalledState,
PackageDataEntry,
UpdatingState,
} from 'src/app/services/patch-db/data-model'
import { isInstalled, isUpdating } from 'src/app/utils/get-package-data'
@Injectable({
providedIn: 'root',
})
export class LocalPackagesService extends Observable<
Record<string, PackageDataEntry<InstalledState | UpdatingState>>
> {
private readonly stream$ = inject<PatchDB<DataModel>>(PatchDB)
.watch$('packageData')
.pipe(
map(pkgs =>
Object.entries(pkgs).reduce(
(acc, [id, val]) =>
isInstalled(val) || isUpdating(val) ? { ...acc, [id]: val } : acc,
{},
),
),
shareReplay({ bufferSize: 1, refCount: true }),
)
constructor() {
super(subscriber => this.stream$.subscribe(subscriber))
}
}

View File

@@ -95,21 +95,26 @@ export class NotificationService {
viewModal(notification: ServerNotification<number>, full = false) { viewModal(notification: ServerNotification<number>, full = false) {
const { data, createdAt, code, title, message } = notification const { data, createdAt, code, title, message } = notification
const label = code === 1 ? 'Backup Report' : (title as i18nKey)
const component = code === 1 ? REPORT : MARKDOWN
const content = code === 1 ? data : of(data)
this.markSeen([notification]) this.markSeen([notification])
this.dialogs
.openComponent(full ? message : component, { if (code === 1) {
label, // Backup Report
data: { this.dialogs
content, .openComponent(full ? message : REPORT, {
timestamp: createdAt, label: 'Backup Report',
}, data: { content: data, createdAt },
size: code === 1 ? 'm' : 'l', })
}) .subscribe()
.subscribe() } else {
// Markdown viewer
this.dialogs
.openComponent(full ? message : MARKDOWN, {
label: title as i18nKey,
data: of(data),
size: 'l',
})
.subscribe()
}
} }
private async updateCount(toAdjust: number) { private async updateCount(toAdjust: number) {

View File

@@ -25,7 +25,6 @@ hr {
:root { :root {
--bumper: 0.375rem; --bumper: 0.375rem;
--tui-font-text: 'Proxima Nova', system-ui;
} }
.g-page { .g-page {