* fix alerts i18n, fix status display, better, remove usb media, hide shutdown for install complete

* trigger chnage detection for localize pipe and round out implementing localize pipe for consistency even though not needed
This commit is contained in:
Matt Hill
2026-02-09 12:41:29 -07:00
committed by GitHub
parent f2142f0bb3
commit ba740a9ee2
16 changed files with 219 additions and 60 deletions

View File

@@ -12,7 +12,7 @@
{{ pkg.title }} {{ pkg.title }}
</span> </span>
<span class="detail-description"> <span class="detail-description">
{{ pkg.description.short }} {{ pkg.description.short | localize }}
</span> </span>
</div> </div>
</div> </div>

View File

@@ -1,12 +1,12 @@
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { RouterModule } from '@angular/router' import { RouterModule } from '@angular/router'
import { SharedPipesModule, TickerComponent } from '@start9labs/shared' import { LocalizePipe, SharedPipesModule, TickerComponent } from '@start9labs/shared'
import { ItemComponent } from './item.component' import { ItemComponent } from './item.component'
@NgModule({ @NgModule({
declarations: [ItemComponent], declarations: [ItemComponent],
exports: [ItemComponent], exports: [ItemComponent],
imports: [CommonModule, RouterModule, SharedPipesModule, TickerComponent], imports: [CommonModule, RouterModule, SharedPipesModule, TickerComponent, LocalizePipe],
}) })
export class ItemModule {} export class ItemModule {}

View File

@@ -6,7 +6,7 @@ import {
output, output,
} from '@angular/core' } from '@angular/core'
import { MarketplacePkgBase } from '../../types' import { MarketplacePkgBase } from '../../types'
import { CopyService, i18nPipe } from '@start9labs/shared' import { CopyService, i18nPipe, LocalizePipe } from '@start9labs/shared'
import { DatePipe } from '@angular/common' import { DatePipe } from '@angular/common'
import { MarketplaceItemComponent } from './item.component' import { MarketplaceItemComponent } from './item.component'
@@ -71,7 +71,7 @@ import { MarketplaceItemComponent } from './item.component'
<div class="background-border box-shadow-lg shadow-color-light"> <div class="background-border box-shadow-lg shadow-color-light">
<div class="box-container"> <div class="box-container">
<h2 class="additional-detail-title">{{ 'Description' | i18n }}</h2> <h2 class="additional-detail-title">{{ 'Description' | i18n }}</h2>
<p [innerHTML]="pkg().description.long"></p> <p [innerHTML]="pkg().description.long | localize"></p>
</div> </div>
</div> </div>
`, `,
@@ -129,7 +129,7 @@ import { MarketplaceItemComponent } from './item.component'
} }
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [MarketplaceItemComponent, DatePipe, i18nPipe], imports: [MarketplaceItemComponent, DatePipe, i18nPipe, LocalizePipe],
}) })
export class MarketplaceAboutComponent { export class MarketplaceAboutComponent {
readonly copyService = inject(CopyService) readonly copyService = inject(CopyService)

View File

@@ -0,0 +1,121 @@
import { Component } from '@angular/core'
import { i18nPipe } from '@start9labs/shared'
import { TuiButton, TuiDialogContext } from '@taiga-ui/core'
import { injectContext } from '@taiga-ui/polymorpheus'
@Component({
standalone: true,
imports: [TuiButton, i18nPipe],
template: `
<div class="animation-container">
<div class="port">
<div class="port-inner"></div>
</div>
<div class="usb-stick">
<div class="usb-connector"></div>
<div class="usb-body"></div>
</div>
</div>
<p>
{{
'Remove USB stick or other installation media from your server' | i18n
}}
</p>
<footer>
<button tuiButton (click)="context.completeWith(true)">
{{ 'Done' | i18n }}
</button>
</footer>
`,
styles: `
:host {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.animation-container {
position: relative;
width: 160px;
height: 69px;
}
.port {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
width: 28px;
height: 18px;
background: var(--tui-background-neutral-1);
border: 2px solid var(--tui-border-normal);
border-radius: 2px;
}
.port-inner {
position: absolute;
top: 3px;
left: 3px;
right: 3px;
bottom: 3px;
background: var(--tui-background-neutral-2);
border-radius: 1px;
}
.usb-stick {
position: absolute;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
animation: slide-out 2s ease-in-out 0.5s infinite;
left: 32px;
}
.usb-connector {
width: 20px;
height: 12px;
background: var(--tui-text-secondary);
border-radius: 1px;
}
.usb-body {
width: 40px;
height: 20px;
background: var(--tui-status-info);
border-radius: 2px 4px 4px 2px;
}
@keyframes slide-out {
0% {
left: 32px;
opacity: 0;
}
5% {
left: 32px;
opacity: 1;
}
80% {
left: 130px;
opacity: 0;
}
100% {
left: 130px;
opacity: 0;
}
}
p {
margin: 0 0 2rem;
}
footer {
display: flex;
justify-content: center;
}
`,
})
export class RemoveMediaDialog {
protected readonly context = injectContext<TuiDialogContext<boolean>>()
}

View File

@@ -1,4 +1,9 @@
import { ChangeDetectorRef, Component, inject } from '@angular/core' import {
ChangeDetectorRef,
Component,
HostListener,
inject,
} from '@angular/core'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { import {
@@ -21,13 +26,14 @@ import {
import { TuiDataListWrapper, TuiSelect, TuiTooltip } from '@taiga-ui/kit' import { TuiDataListWrapper, TuiSelect, TuiTooltip } from '@taiga-ui/kit'
import { TuiCardLarge, TuiHeader } from '@taiga-ui/layout' import { TuiCardLarge, TuiHeader } from '@taiga-ui/layout'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { filter } from 'rxjs' import { filter, Subscription } from 'rxjs'
import { ApiService } from '../services/api.service' import { ApiService } from '../services/api.service'
import { StateService } from '../services/state.service' import { StateService } from '../services/state.service'
import { PreserveOverwriteDialog } from '../components/preserve-overwrite.dialog' import { PreserveOverwriteDialog } from '../components/preserve-overwrite.dialog'
@Component({ @Component({
template: ` template: `
@if (!shuttingDown) {
<section tuiCardLarge="compact"> <section tuiCardLarge="compact">
<header tuiHeader> <header tuiHeader>
<h2 tuiTitle>{{ 'Select Drives' | i18n }}</h2> <h2 tuiTitle>{{ 'Select Drives' | i18n }}</h2>
@@ -132,6 +138,7 @@ import { PreserveOverwriteDialog } from '../components/preserve-overwrite.dialog
} }
</footer> </footer>
</section> </section>
}
`, `,
styles: ` styles: `
.no-drives { .no-drives {
@@ -176,6 +183,14 @@ export default class DrivesPage {
protected readonly mobile = inject(TUI_IS_MOBILE) protected readonly mobile = inject(TUI_IS_MOBILE)
@HostListener('document:keydown', ['$event'])
onKeydown(event: KeyboardEvent) {
if (event.ctrlKey && event.shiftKey && event.key === 'X') {
event.preventDefault()
this.shutdownServer()
}
}
readonly osDriveTooltip = this.i18n.transform( readonly osDriveTooltip = this.i18n.transform(
'The drive where the StartOS operating system will be installed.', 'The drive where the StartOS operating system will be installed.',
) )
@@ -185,6 +200,8 @@ export default class DrivesPage {
drives: DiskInfo[] = [] drives: DiskInfo[] = []
loading = true loading = true
shuttingDown = false
private dialogSub?: Subscription
selectedOsDrive: DiskInfo | null = null selectedOsDrive: DiskInfo | null = null
selectedDataDrive: DiskInfo | null = null selectedDataDrive: DiskInfo | null = null
preserveData: boolean | null = null preserveData: boolean | null = null
@@ -339,22 +356,18 @@ export default class DrivesPage {
loader.unsubscribe() loader.unsubscribe()
// Show success dialog // Show success dialog
this.dialogs this.dialogSub = this.dialogs
.openConfirm({ .openAlert('StartOS has been installed successfully.', {
label: 'Installation Complete!', label: 'Installation Complete!',
size: 's', size: 's',
data: { dismissible: false,
content: 'StartOS has been installed successfully.', closeable: true,
yes: 'Continue to Setup', data: { button: this.i18n.transform('Continue to Setup') },
no: 'Shutdown',
},
}) })
.subscribe(continueSetup => { .subscribe({
if (continueSetup) { complete: () => {
this.navigateToNextStep(result.attach) this.navigateToNextStep(result.attach)
} else { },
this.shutdownServer()
}
}) })
} catch (e: any) { } catch (e: any) {
loader.unsubscribe() loader.unsubscribe()
@@ -372,10 +385,12 @@ export default class DrivesPage {
} }
private async shutdownServer() { private async shutdownServer() {
this.dialogSub?.unsubscribe()
const loader = this.loader.open('Beginning shutdown').subscribe() const loader = this.loader.open('Beginning shutdown').subscribe()
try { try {
await this.api.shutdown() await this.api.shutdown()
this.shuttingDown = true
} catch (e: any) { } catch (e: any) {
this.errorService.handleError(e) this.errorService.handleError(e)
} finally { } finally {

View File

@@ -6,7 +6,12 @@ import {
ViewChild, ViewChild,
DOCUMENT, DOCUMENT,
} from '@angular/core' } from '@angular/core'
import { DownloadHTMLService, ErrorService, i18nPipe } from '@start9labs/shared' import {
DialogService,
DownloadHTMLService,
ErrorService,
i18nPipe,
} from '@start9labs/shared'
import { TuiIcon, TuiLoader, TuiTitle } from '@taiga-ui/core' import { TuiIcon, TuiLoader, TuiTitle } from '@taiga-ui/core'
import { TuiAvatar } from '@taiga-ui/kit' import { TuiAvatar } from '@taiga-ui/kit'
import { TuiCardLarge, TuiCell, TuiHeader } from '@taiga-ui/layout' import { TuiCardLarge, TuiCell, TuiHeader } from '@taiga-ui/layout'
@@ -14,7 +19,9 @@ import { ApiService } from '../services/api.service'
import { StateService } from '../services/state.service' import { StateService } from '../services/state.service'
import { DocumentationComponent } from '../components/documentation.component' import { DocumentationComponent } from '../components/documentation.component'
import { MatrixComponent } from '../components/matrix.component' import { MatrixComponent } from '../components/matrix.component'
import { RemoveMediaDialog } from '../components/remove-media.dialog'
import { SetupCompleteRes } from '../types' import { SetupCompleteRes } from '../types'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
@Component({ @Component({
template: ` template: `
@@ -29,12 +36,8 @@ import { SetupCompleteRes } from '../types'
@if (!stateService.kiosk) { @if (!stateService.kiosk) {
<span tuiSubtitle> <span tuiSubtitle>
{{ {{
stateService.setupType === 'restore' 'http://start.local was for setup only. It will no longer work.'
? ('You can unplug your backup drive' | i18n) | i18n
: stateService.setupType === 'transfer'
? ('You can unplug your transfer drive' | i18n)
: ('http://start.local was for setup only. It will no longer work.'
| i18n)
}} }}
</span> </span>
} }
@@ -69,14 +72,15 @@ import { SetupCompleteRes } from '../types'
tuiCell="l" tuiCell="l"
[class.disabled]="!stateService.kiosk && !downloaded" [class.disabled]="!stateService.kiosk && !downloaded"
[disabled]="(!stateService.kiosk && !downloaded) || usbRemoved" [disabled]="(!stateService.kiosk && !downloaded) || usbRemoved"
(click)="usbRemoved = true" (click)="removeMedia()"
> >
<tui-avatar appearance="secondary" src="@tui.usb" /> <tui-avatar appearance="secondary" src="@tui.usb" />
<div tuiTitle> <div tuiTitle>
{{ 'USB Removed' | i18n }} {{ 'Remove Installation Media' | i18n }}
<div tuiSubtitle> <div tuiSubtitle>
{{ {{
'Remove the USB installation media from your server' | i18n 'Remove USB stick or other installation media from your server'
| i18n
}} }}
</div> </div>
</div> </div>
@@ -184,6 +188,7 @@ export default class SuccessPage implements AfterViewInit {
private readonly errorService = inject(ErrorService) private readonly errorService = inject(ErrorService)
private readonly api = inject(ApiService) private readonly api = inject(ApiService)
private readonly downloadHtml = inject(DownloadHTMLService) private readonly downloadHtml = inject(DownloadHTMLService)
private readonly dialogs = inject(DialogService)
private readonly i18n = inject(i18nPipe) private readonly i18n = inject(i18nPipe)
readonly stateService = inject(StateService) readonly stateService = inject(StateService)
@@ -225,6 +230,21 @@ export default class SuccessPage implements AfterViewInit {
}) })
} }
removeMedia() {
this.dialogs
.openComponent<boolean>(
new PolymorpheusComponent(RemoveMediaDialog),
{
size: 's',
dismissible: false,
closeable: false,
},
)
.subscribe(() => {
this.usbRemoved = true
})
}
exitKiosk() { exitKiosk() {
this.api.exit() this.api.exit()
} }

View File

@@ -100,6 +100,7 @@ export default {
102: 'Verlassen', 102: 'Verlassen',
103: 'Sind Sie sicher?', 103: 'Sind Sie sicher?',
104: 'Neues Netzwerk-Gateway', 104: 'Neues Netzwerk-Gateway',
107: 'Onion-Domains',
108: 'Öffentlich', 108: 'Öffentlich',
109: 'privat', 109: 'privat',
111: 'Keine Onion-Domains', 111: 'Keine Onion-Domains',
@@ -639,13 +640,11 @@ export default {
667: 'Einrichtung wird gestartet', 667: 'Einrichtung wird gestartet',
670: 'Warten Sie 12 Minuten und aktualisieren Sie die Seite', 670: 'Warten Sie 12 Minuten und aktualisieren Sie die Seite',
672: 'Einrichtung abgeschlossen!', 672: 'Einrichtung abgeschlossen!',
673: 'Sie können Ihr Backup-Laufwerk entfernen',
674: 'Sie können Ihr Übertragungs-Laufwerk entfernen',
675: 'http://start.local war nur für die Einrichtung gedacht. Es funktioniert nicht mehr.', 675: 'http://start.local war nur für die Einrichtung gedacht. Es funktioniert nicht mehr.',
676: 'Adressinformationen herunterladen', 676: 'Adressinformationen herunterladen',
677: 'Enthält die permanente lokale Adresse Ihres Servers und die Root-CA', 677: 'Enthält die permanente lokale Adresse Ihres Servers und die Root-CA',
678: 'USB entfernt', 678: 'Installationsmedium entfernen',
679: 'Entfernen Sie das USB-Installationsmedium aus Ihrem Server', 679: 'Entfernen Sie den USB-Stick oder andere Installationsmedien von Ihrem Server',
680: 'Server neu starten', 680: 'Server neu starten',
681: 'Warten, bis der Server wieder online ist', 681: 'Warten, bis der Server wieder online ist',
682: 'Server ist wieder online', 682: 'Server ist wieder online',

View File

@@ -99,6 +99,7 @@ export const ENGLISH: Record<string, number> = {
'Leave': 102, 'Leave': 102,
'Are you sure?': 103, 'Are you sure?': 103,
'New gateway': 104, // as in, a network gateway 'New gateway': 104, // as in, a network gateway
'Tor Domains': 107,
'public': 108, 'public': 108,
'private': 109, 'private': 109,
'No Tor domains': 111, 'No Tor domains': 111,
@@ -639,13 +640,11 @@ export const ENGLISH: Record<string, number> = {
'Starting setup': 667, 'Starting setup': 667,
'Wait 1-2 minutes and refresh the page': 670, 'Wait 1-2 minutes and refresh the page': 670,
'Setup Complete!': 672, 'Setup Complete!': 672,
'You can unplug your backup drive': 673,
'You can unplug your transfer drive': 674,
'http://start.local was for setup only. It will no longer work.': 675, 'http://start.local was for setup only. It will no longer work.': 675,
'Download Address Info': 676, 'Download Address Info': 676,
"Contains your server's permanent local address and Root CA": 677, "Contains your server's permanent local address and Root CA": 677,
'USB Removed': 678, 'Remove Installation Media': 678,
'Remove the USB installation media from your server': 679, 'Remove USB stick or other installation media from your server': 679,
'Restart Server': 680, 'Restart Server': 680,
'Waiting for server to come back online': 681, 'Waiting for server to come back online': 681,
'Server is back online': 682, 'Server is back online': 682,

View File

@@ -100,6 +100,7 @@ export default {
102: 'Salir', 102: 'Salir',
103: '¿Estás seguro?', 103: '¿Estás seguro?',
104: 'Nueva puerta de enlace de red', 104: 'Nueva puerta de enlace de red',
107: 'dominios onion',
108: 'público', 108: 'público',
109: 'privado', 109: 'privado',
111: 'Sin dominios onion', 111: 'Sin dominios onion',
@@ -639,13 +640,11 @@ export default {
667: 'Iniciando configuración', 667: 'Iniciando configuración',
670: 'Espere 12 minutos y actualice la página', 670: 'Espere 12 minutos y actualice la página',
672: '¡Configuración completa!', 672: '¡Configuración completa!',
673: 'Puede desconectar su unidad de copia de seguridad',
674: 'Puede desconectar su unidad de transferencia',
675: 'http://start.local era solo para la configuración. Ya no funcionará.', 675: 'http://start.local era solo para la configuración. Ya no funcionará.',
676: 'Descargar información de direcciones', 676: 'Descargar información de direcciones',
677: 'Contiene la dirección local permanente de su servidor y la CA raíz', 677: 'Contiene la dirección local permanente de su servidor y la CA raíz',
678: 'USB retirado', 678: 'Retirar medio de instalación',
679: 'Retire el medio de instalación USB de su servidor', 679: 'Retire la memoria USB u otro medio de instalación de su servidor',
680: 'Reiniciar servidor', 680: 'Reiniciar servidor',
681: 'Esperando a que el servidor vuelva a estar en línea', 681: 'Esperando a que el servidor vuelva a estar en línea',
682: 'El servidor ha vuelto a estar en línea', 682: 'El servidor ha vuelto a estar en línea',

View File

@@ -100,6 +100,7 @@ export default {
102: 'Quitter', 102: 'Quitter',
103: 'Êtes-vous sûr ?', 103: 'Êtes-vous sûr ?',
104: 'Nouvelle passerelle réseau', 104: 'Nouvelle passerelle réseau',
107: 'domaine onion',
108: 'public', 108: 'public',
109: 'privé', 109: 'privé',
111: 'Aucune domaine onion', 111: 'Aucune domaine onion',
@@ -639,13 +640,11 @@ export default {
667: 'Démarrage de la configuration', 667: 'Démarrage de la configuration',
670: 'Attendez 1 à 2 minutes puis actualisez la page', 670: 'Attendez 1 à 2 minutes puis actualisez la page',
672: 'Configuration terminée !', 672: 'Configuration terminée !',
673: 'Vous pouvez débrancher votre disque de sauvegarde',
674: 'Vous pouvez débrancher votre disque de transfert',
675: 'http://start.local était réservé à la configuration. Il ne fonctionnera plus.', 675: 'http://start.local était réservé à la configuration. Il ne fonctionnera plus.',
676: 'Télécharger les informations dadresse', 676: 'Télécharger les informations dadresse',
677: 'Contient ladresse locale permanente de votre serveur et la CA racine', 677: 'Contient l\u2019adresse locale permanente de votre serveur et la CA racine',
678: 'USB retiré', 678: 'Retirer le support d\u2019installation',
679: 'Retirez le support dinstallation USB de votre serveur', 679: 'Retirez la clé USB ou tout autre support d\u2019installation de votre serveur',
680: 'Redémarrer le serveur', 680: 'Redémarrer le serveur',
681: 'En attente du retour en ligne du serveur', 681: 'En attente du retour en ligne du serveur',
682: 'Le serveur est de nouveau en ligne', 682: 'Le serveur est de nouveau en ligne',

View File

@@ -100,6 +100,7 @@ export default {
102: 'Opuść', 102: 'Opuść',
103: 'Czy jesteś pewien?', 103: 'Czy jesteś pewien?',
104: 'Nowa brama sieciowa', 104: 'Nowa brama sieciowa',
107: 'domeny onion',
108: 'publiczny', 108: 'publiczny',
109: 'prywatny', 109: 'prywatny',
111: 'Brak domeny onion', 111: 'Brak domeny onion',
@@ -639,13 +640,11 @@ export default {
667: 'Rozpoczynanie konfiguracji', 667: 'Rozpoczynanie konfiguracji',
670: 'Poczekaj 12 minuty i odśwież stronę', 670: 'Poczekaj 12 minuty i odśwież stronę',
672: 'Konfiguracja zakończona!', 672: 'Konfiguracja zakończona!',
673: 'Możesz odłączyć dysk kopii zapasowej',
674: 'Możesz odłączyć dysk transferowy',
675: 'http://start.local służył tylko do konfiguracji. Nie będzie już działać.', 675: 'http://start.local służył tylko do konfiguracji. Nie będzie już działać.',
676: 'Pobierz informacje adresowe', 676: 'Pobierz informacje adresowe',
677: 'Zawiera stały lokalny adres serwera oraz główny urząd certyfikacji (Root CA)', 677: 'Zawiera stały lokalny adres serwera oraz główny urząd certyfikacji (Root CA)',
678: 'USB usunięty', 678: 'Usuń nośnik instalacyjny',
679: 'Usuń instalacyjny nośnik USB z serwera', 679: 'Usuń pamięć USB lub inny nośnik instalacyjny z serwera',
680: 'Uruchom ponownie serwer', 680: 'Uruchom ponownie serwer',
681: 'Oczekiwanie na ponowne połączenie serwera', 681: 'Oczekiwanie na ponowne połączenie serwera',
682: 'Serwer jest ponownie online', 682: 'Serwer jest ponownie online',

View File

@@ -1,5 +1,6 @@
import { inject, Injectable, Pipe, PipeTransform } from '@angular/core' import { inject, Injectable, Pipe, PipeTransform } from '@angular/core'
import { i18nService } from './i18n.service' import { i18nService } from './i18n.service'
import { I18N } from './i18n.providers'
import { T } from '@start9labs/start-sdk' import { T } from '@start9labs/start-sdk'
@Pipe({ @Pipe({
@@ -9,8 +10,10 @@ import { T } from '@start9labs/start-sdk'
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class LocalizePipe implements PipeTransform { export class LocalizePipe implements PipeTransform {
private readonly i18nService = inject(i18nService) private readonly i18nService = inject(i18nService)
private readonly i18n = inject(I18N)
transform(string: T.LocaleString): string { transform(string: T.LocaleString): string {
this.i18n() // read signal to trigger change detection on language switch
return this.i18nService.localize(string) return this.i18nService.localize(string)
} }
} }

View File

@@ -35,7 +35,7 @@ type OnionForm = {
selector: 'section[torDomains]', selector: 'section[torDomains]',
template: ` template: `
<header> <header>
Tor Domains {{ 'Tor Domains' | i18n }}
<a <a
tuiIconButton tuiIconButton
docsLink docsLink

View File

@@ -4,6 +4,7 @@ import {
ErrorService, ErrorService,
i18nKey, i18nKey,
i18nPipe, i18nPipe,
i18nService,
LoadingService, LoadingService,
} from '@start9labs/shared' } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk' import { T } from '@start9labs/start-sdk'
@@ -24,6 +25,7 @@ export class ControlsService {
private readonly api = inject(ApiService) private readonly api = inject(ApiService)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB) private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
private readonly i18n = inject(i18nPipe) private readonly i18n = inject(i18nPipe)
private readonly i18nService = inject(i18nService)
async start({ title, alerts, id }: T.Manifest, unmet: boolean) { async start({ title, alerts, id }: T.Manifest, unmet: boolean) {
const deps = const deps =
@@ -31,7 +33,7 @@ export class ControlsService {
if ( if (
(unmet && !(await this.alert(deps))) || (unmet && !(await this.alert(deps))) ||
(alerts.start && !(await this.alert(alerts.start as i18nKey))) (alerts.start && !(await this.alert(alerts.start)))
) { ) {
return return
} }
@@ -49,7 +51,7 @@ export class ControlsService {
async stop({ id, title, alerts }: T.Manifest) { async stop({ id, title, alerts }: T.Manifest) {
const depMessage = `${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('will no longer work properly and may crash.')}` const depMessage = `${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('will no longer work properly and may crash.')}`
let content = alerts.stop || '' let content = alerts.stop ? this.i18nService.localize(alerts.stop) : ''
if (hasCurrentDeps(id, await getAllPackages(this.patch))) { if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
content = content ? `${content}.\n\n${depMessage}` : depMessage content = content ? `${content}.\n\n${depMessage}` : depMessage
@@ -113,14 +115,14 @@ export class ControlsService {
}) })
} }
private alert(content: i18nKey): Promise<boolean> { private alert(content: T.LocaleString): Promise<boolean> {
return firstValueFrom( return firstValueFrom(
this.dialog this.dialog
.openConfirm({ .openConfirm({
label: 'Warning', label: 'Warning',
size: 's', size: 's',
data: { data: {
content, content: this.i18nService.localize(content),
yes: 'Continue', yes: 'Continue',
no: 'Cancel', no: 'Cancel',
}, },

View File

@@ -31,7 +31,7 @@ export function getInstalledBaseStatus(statusInfo: T.StatusInfo): BaseStatus {
(!statusInfo.started || (!statusInfo.started ||
Object.values(statusInfo.health) Object.values(statusInfo.health)
.filter(h => !!h) .filter(h => !!h)
.some(h => h.result === 'starting')) .some(h => h.result === 'starting' || h.result === 'waiting'))
) { ) {
return 'starting' return 'starting'
} }

View File

@@ -5,6 +5,7 @@ import {
ErrorService, ErrorService,
i18nKey, i18nKey,
i18nPipe, i18nPipe,
i18nService,
LoadingService, LoadingService,
} from '@start9labs/shared' } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk' import { T } from '@start9labs/start-sdk'
@@ -27,6 +28,7 @@ export class StandardActionsService {
private readonly loader = inject(LoadingService) private readonly loader = inject(LoadingService)
private readonly router = inject(Router) private readonly router = inject(Router)
private readonly i18n = inject(i18nPipe) private readonly i18n = inject(i18nPipe)
private readonly i18nService = inject(i18nService)
async rebuild(id: string) { async rebuild(id: string) {
const loader = this.loader.open('Rebuilding container').subscribe() const loader = this.loader.open('Rebuilding container').subscribe()
@@ -50,11 +52,12 @@ export class StandardActionsService {
): Promise<void> { ): Promise<void> {
let content = soft let content = soft
? '' ? ''
: alerts.uninstall || : alerts.uninstall
`${this.i18n.transform('Uninstalling')} ${title} ${this.i18n.transform('will permanently delete its data.')}` ? this.i18nService.localize(alerts.uninstall)
: `${this.i18n.transform('Uninstalling')} ${title} ${this.i18n.transform('will permanently delete its data.')}`
if (hasCurrentDeps(id, await getAllPackages(this.patch))) { if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
content = `${content}${content ? ' ' : ''}${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('will no longer work properly and may crash.')}` content = `${content ? `${content} ` : ''}${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('will no longer work properly and may crash.')}`
} }
if (!content) { if (!content) {