diff --git a/web/projects/ui/src/app/routes/portal/components/form/containers/control.component.ts b/web/projects/ui/src/app/routes/portal/components/form/containers/control.component.ts index ec99b45b4..2e97ec829 100644 --- a/web/projects/ui/src/app/routes/portal/components/form/containers/control.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/form/containers/control.component.ts @@ -2,29 +2,19 @@ import { AsyncPipe } from '@angular/common' import { ChangeDetectionStrategy, Component, - DestroyRef, inject, Input, - TemplateRef, - ViewChild, } from '@angular/core' -import { takeUntilDestroyed } from '@angular/core/rxjs-interop' -import { i18nPipe } from '@start9labs/shared' +import { DialogService, i18nPipe } from '@start9labs/shared' import { IST } from '@start9labs/start-sdk' import { tuiAsControl, TuiControl } from '@taiga-ui/cdk' -import { - TuiAlertService, - TuiButton, - TuiDialogContext, - TuiError, -} from '@taiga-ui/core' +import { TuiError } from '@taiga-ui/core' import { TUI_FORMAT_ERROR, TUI_VALIDATION_ERRORS, TuiFieldErrorPipe, } from '@taiga-ui/kit' import { PolymorpheusOutlet } from '@taiga-ui/polymorpheus' -import { filter } from 'rxjs' import { ControlSpec } from '../controls/control' import { CONTROLS } from '../controls/controls' @@ -46,35 +36,6 @@ export const ERRORS = [ template: ` - @if (spec.warning || immutable) { - - {{ spec.warning }} - @if (immutable) { - {{ 'This value cannot be changed once set' | i18n }}! - } - - - {{ 'Continue' | i18n }} - - - {{ 'Cancel' | i18n }} - - - - } `, changeDetection: ChangeDetectionStrategy.OnPush, providers: [ @@ -92,21 +53,13 @@ export const ERRORS = [ }, ], hostDirectives: [ControlDirective], - imports: [ - AsyncPipe, - i18nPipe, - PolymorpheusOutlet, - TuiError, - TuiFieldErrorPipe, - TuiButton, - ], + imports: [AsyncPipe, PolymorpheusOutlet, TuiError, TuiFieldErrorPipe], }) export class FormControlComponent< T extends ControlSpec, V, > extends TuiControl { - private readonly destroyRef = inject(DestroyRef) - private readonly alerts = inject(TuiAlertService) + private readonly dialogs = inject(DialogService) private readonly i18n = inject(i18nPipe) protected readonly controls = CONTROLS @@ -114,30 +67,29 @@ export class FormControlComponent< @Input({ required: true }) spec!: T - @ViewChild('warning') - warning?: TemplateRef> - warned = false readonly order = ERRORS - get immutable(): boolean { - return 'immutable' in this.spec && this.spec.immutable - } - onInput(value: V | null) { const previous = this.value() + const immutable = + 'immutable' in this.spec && this.spec.immutable + ? `${this.i18n.transform('This value cannot be changed once set')}` + : '' + const warning = this.spec.warning + immutable - if (!this.warned && this.warning) { - this.alerts - .open(this.warning, { - label: this.i18n.transform('Warning'), - appearance: 'warning', + if (!this.warned && warning) { + this.dialogs + .openConfirm({ + label: 'Warning', + data: { content: warning as any, yes: 'Confirm', no: 'Cancel' }, closeable: false, - autoClose: 0, + dismissible: false, }) - .pipe(filter(Boolean), takeUntilDestroyed(this.destroyRef)) - .subscribe(() => { - this.onChange(previous) + .subscribe(confirm => { + if (!confirm) { + this.onChange(previous) + } }) } diff --git a/web/projects/ui/src/app/routes/portal/components/form/controls/number.component.ts b/web/projects/ui/src/app/routes/portal/components/form/controls/number.component.ts index bc7abf54b..4f4828e45 100644 --- a/web/projects/ui/src/app/routes/portal/components/form/controls/number.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/form/controls/number.component.ts @@ -21,14 +21,14 @@ import { HintPipe } from '../pipes/hint.pipe' } @@ -51,4 +51,16 @@ export class FormNumberComponent extends Control { get precision(): number { return this.spec.integer ? 0 : Infinity } + + get postfix(): string { + return this.spec.units && (this.value !== null || !this.spec.placeholder) + ? ` ${this.spec.units}` + : '' + } + + get placeholder(): string { + const units = this.spec.units ? ` (${this.spec.units})` : '' + + return this.spec.placeholder ? this.spec.placeholder + units : '' + } } diff --git a/web/projects/ui/src/app/routes/portal/components/form/controls/select.component.ts b/web/projects/ui/src/app/routes/portal/components/form/controls/select.component.ts index 068c4d830..fcbd350f4 100644 --- a/web/projects/ui/src/app/routes/portal/components/form/controls/select.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/form/controls/select.component.ts @@ -3,8 +3,14 @@ import { FormsModule } from '@angular/forms' import { invert } from '@start9labs/shared' import { IST } from '@start9labs/start-sdk' import { TUI_IS_MOBILE } from '@taiga-ui/cdk' -import { TuiIcon, TuiTextfield } from '@taiga-ui/core' -import { TuiDataListWrapper, TuiSelect, TuiTooltip } from '@taiga-ui/kit' +import { TuiDataList, TuiIcon, TuiTextfield } from '@taiga-ui/core' +import { + TuiDataListWrapper, + TuiFluidTypography, + tuiFluidTypographyOptionsProvider, + TuiSelect, + TuiTooltip, +} from '@taiga-ui/kit' import { Control } from './control' import { HintPipe } from '../pipes/hint.pipe' @@ -41,18 +47,32 @@ import { HintPipe } from '../pipes/hint.pipe' /> } @if (!mobile) { - + + @for (item of items; track $index) { + + {{ item }} + + } + } @if (spec | hint; as hint) { } `, + providers: [tuiFluidTypographyOptionsProvider({ max: 1 })], imports: [ FormsModule, TuiTextfield, TuiSelect, - TuiDataListWrapper, + TuiDataList, + TuiFluidTypography, TuiIcon, TuiTooltip, HintPipe, diff --git a/web/projects/ui/src/app/routes/portal/routes/notifications/item.component.ts b/web/projects/ui/src/app/routes/portal/routes/notifications/item.component.ts index 445750ce7..29d27b7f0 100644 --- a/web/projects/ui/src/app/routes/portal/routes/notifications/item.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/notifications/item.component.ts @@ -1,104 +1,131 @@ -import { TuiLineClamp } from '@taiga-ui/kit' import { CommonModule } from '@angular/common' import { ChangeDetectionStrategy, Component, + computed, inject, - Input, + input, } from '@angular/core' +import { toObservable, toSignal } from '@angular/core/rxjs-interop' import { RouterLink } from '@angular/router' -import { T } from '@start9labs/start-sdk' -import { tuiPure } from '@taiga-ui/cdk' +import { i18nPipe } from '@start9labs/shared' import { TuiIcon, TuiLink } from '@taiga-ui/core' +import { TuiAvatar, TuiLineClamp } from '@taiga-ui/kit' import { PatchDB } from 'patch-db-client' -import { first, Observable } from 'rxjs' +import { EMPTY, first, switchMap } from 'rxjs' import { ServerNotification } from 'src/app/services/api/api.types' import { NotificationService } from 'src/app/services/notification.service' import { DataModel } from 'src/app/services/patch-db/data-model' -import { toRouterLink } from 'src/app/utils/to-router-link' -import { i18nPipe } from '@start9labs/shared' @Component({ selector: '[notificationItem]', template: ` - - - {{ notificationItem.createdAt | date: 'medium' }} - - - - {{ notificationItem.title }} - - - @if (manifest$ | async; as manifest) { - - {{ manifest.title }} - - } @else if (notificationItem.packageId) { - {{ notificationItem.packageId }} - } @else { - - - } - - - - @if (overflow) { - - {{ 'View full' | i18n }} - - } - @if ([1, 2].includes(notificationItem.code)) { - - {{ - notificationItem.code === 1 - ? ('View report' | i18n) - : ('View details' | i18n) - }} - - } - + @if (notificationItem(); as item) { + + + {{ item.createdAt | date: 'MMM d, y, h:mm a' }} + + + + {{ item.title }} + + + @if (pkg(); as pkg) { + @if (pkg.stateInfo.manifest; as manifest) { + + + + } @else { + {{ item.packageId || '-' }} + } + } @else { + - + } + + + + @if (overflow) { + + {{ 'View full' | i18n }} + + } + @if ([1, 2].includes(item.code)) { + + {{ + item?.code === 1 + ? ('View report' | i18n) + : ('View details' | i18n) + }} + + } + + } `, changeDetection: ChangeDetectionStrategy.OnPush, host: { - '[class._new]': '!notificationItem.seen', + '[class._new]': '!notificationItem()?.seen', }, styles: ` - :host { - &._new td { - font-weight: bold; - color: var(--tui-text-primary); - } + :host._new td { + font-weight: bold; + color: var(--tui-text-primary); } - button { - position: relative; + .title { + width: 13rem; + } + + .service { + width: 4.25rem; + text-align: center; + grid-column: 2; + grid-row: 1 / 3; + place-content: center; + } + + tui-icon { + font-size: 1rem; + vertical-align: sub; } td { color: var(--tui-text-secondary); - } + grid-column: 1; - :host-context(tui-root._mobile) { - td { - width: 100%; + &:first-child { + width: 12rem; + padding-inline-start: 2.5rem; + white-space: nowrap; + } - &:first-child { - padding: 0 !important; - } + &:last-child { + grid-column: 1 / 3; } + } - gap: 0.5rem; - padding: 0.75rem 1rem !important; + :host-context(tui-root._mobile) { + :host { + grid-template-columns: 1fr 2rem; + user-select: none; + gap: 0.5rem; + } - .date { - order: 1; + td:first-child { + padding: 0; + font: var(--tui-font-text-s); color: var(--tui-text-secondary); + margin-block-end: -0.25rem; } .title { @@ -106,55 +133,65 @@ import { i18nPipe } from '@start9labs/shared' font-size: 1.2em; display: flex; align-items: center; - gap: 0.5rem; + gap: 0.375rem; } - .service:not(:has(a)) { - display: none; + .service { + width: auto; + + &:not(:has(a)) { + display: none; + } + } + + :host-context(table:has(:checked)) tui-icon { + opacity: 0; } } `, - imports: [CommonModule, RouterLink, TuiLineClamp, TuiLink, TuiIcon, i18nPipe], + imports: [ + CommonModule, + RouterLink, + TuiLineClamp, + TuiLink, + TuiIcon, + i18nPipe, + TuiAvatar, + ], }) export class NotificationItemComponent { private readonly patch = inject>(PatchDB) readonly service = inject(NotificationService) - @Input({ required: true }) notificationItem!: ServerNotification - - overflow = false + readonly notificationItem = input>() - @tuiPure - get manifest$(): Observable { - return this.patch - .watch$( - 'packageData', - this.notificationItem.packageId || '', - 'stateInfo', - 'manifest', - ) - .pipe(first()) - } + readonly color = computed((item = this.notificationItem()) => + item ? this.service.getColor(item) : '', + ) - get color(): string { - return this.service.getColor(this.notificationItem) - } + readonly icon = computed((item = this.notificationItem()) => + item ? this.service.getIcon(item) : '', + ) - get icon(): string { - return this.service.getIcon(this.notificationItem) - } + readonly pkg = toSignal( + toObservable(this.notificationItem).pipe( + switchMap(item => + item + ? this.patch.watch$('packageData', item.packageId || '').pipe(first()) + : EMPTY, + ), + ), + ) - getLink(id: string) { - return toRouterLink(id) - } + overflow = false - onClick() { + onClick(item: ServerNotification) { if (this.overflow) { - this.service.viewModal(this.notificationItem, true) - this.notificationItem.seen = true - } else if ([1, 2].includes(this.notificationItem.code)) { - this.service.viewModal(this.notificationItem) - this.notificationItem.seen = true + this.service.viewModal(item, true) + item.seen = true + } else if ([1, 2].includes(item.code)) { + this.service.viewModal(item) + item.seen = true } } } diff --git a/web/projects/ui/src/app/routes/portal/routes/notifications/notifications.component.ts b/web/projects/ui/src/app/routes/portal/routes/notifications/notifications.component.ts index cc81d14a3..d3f2ee98a 100644 --- a/web/projects/ui/src/app/routes/portal/routes/notifications/notifications.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/notifications/notifications.component.ts @@ -26,9 +26,7 @@ import { NotificationsTableComponent } from './table.component' @Component({ template: ` - - {{ 'Notifications' | i18n }} - + {{ 'Notifications' | i18n }} {{ 'Notifications' | i18n }} diff --git a/web/projects/ui/src/app/routes/portal/routes/notifications/table.component.ts b/web/projects/ui/src/app/routes/portal/routes/notifications/table.component.ts index 822640c0c..87fb1f4c5 100644 --- a/web/projects/ui/src/app/routes/portal/routes/notifications/table.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/notifications/table.component.ts @@ -28,7 +28,7 @@ import { i18nPipe } from '@start9labs/shared' /> {{ 'Date' | i18n }} - @for (not of notifications(); track $index) { + @for (not of notifications(); track not) { - {{ 'No notifications' | i18n }} + {{ 'No notifications' | i18n }} } @else { @for (i of ['', '']; track $index) { - + {{ 'Loading' | i18n }} @@ -71,16 +71,15 @@ import { i18nPipe } from '@start9labs/shared' transform: translateY(-50%); } - :host-context(tui-root._mobile) { - tr { - grid-template-columns: 1fr 5rem; - user-select: none; - } + td:only-child { + text-align: center; + } + :host-context(tui-root._mobile) { input { position: absolute; - top: 0.875rem; - left: 1rem; + top: 2.875rem; + left: 0; z-index: 1; pointer-events: none; } diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index 2020f31bc..70baeee2c 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -1598,6 +1598,8 @@ export namespace Mock { option1: 'option1', option2: 'option2', option3: 'option3', + option4: + 'https://qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnm12345.onion', }, disabled: ['option2'], })), @@ -1610,6 +1612,7 @@ export namespace Mock { default: 7, integer: false, units: 'BTC', + placeholder: 'Is it 237?', min: -100, max: 100, }),
{{ 'This value cannot be changed once set' | i18n }}!
${this.i18n.transform('This value cannot be changed once set')}