Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions projects/start-os/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ file tracks notable changes since the move to the monorepo.
any browser pointed at this server. Narrow windows and phones always show the
grid.

- **An action can return a multi-line value** — a diagnostic report, a
generated config file, an exported key block. It appears as a read-only
monospace box that keeps its line breaks, and, where the service asks for it,
can be copied, shown as a QR code, or saved to a file.

### Changed

- **Your server's name is now its `.local` address, without the `.local` on the
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,48 @@
import { Component, Input } from '@angular/core'
import { TuiAccordion, TuiFade } from '@taiga-ui/kit'
import { ActionSuccessMemberComponent } from './action-success-member.component'
import { ActionSuccessMultilineComponent } from './action-success-multiline.component'
import { GroupResult } from './types'

@Component({
selector: 'app-action-success-group',
template: `
@for (member of group.value; track $index) {
<p>
@if (member.type === 'single') {
<app-action-success-member [member]="member" />
}
@if (member.type === 'group') {
<tui-accordion>
<button tuiAccordion>
<span tuiFade>{{ member.name }}</span>
</button>
<tui-expand>
<app-action-success-group [group]="member" />
</tui-expand>
</tui-accordion>
}
</p>
@if (member.type === 'single') {
<app-action-success-member [member]="member" />
}
@if (member.type === 'multiline') {
<app-action-success-multiline
[multiline]="member"
[name]="member.name"
[description]="member.description || ''"
/>
}
@if (member.type === 'group') {
<tui-accordion>
<button tuiAccordion>
<span tuiFade>{{ member.name }}</span>
</button>
<tui-expand>
<app-action-success-group [group]="member" />
</tui-expand>
</tui-accordion>
}
}
`,
styles: `
p:first-child {
margin-top: 0;
}

p:last-child {
margin-bottom: 0;
:host {
display: flex;
flex-direction: column;
gap: 1rem;
}
`,
imports: [ActionSuccessMemberComponent, TuiAccordion, TuiFade],
imports: [
ActionSuccessMemberComponent,
ActionSuccessMultilineComponent,
TuiAccordion,
TuiFade,
],
})
export class ActionSuccessGroupComponent {
@Input()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import {
Component,
computed,
inject,
input,
linkedSignal,
TemplateRef,
} from '@angular/core'
import { CopyService, DialogService, i18nPipe } from '@start9labs/shared'
import { TuiButton, TuiTitle } from '@taiga-ui/core'
import { TuiTextarea } from '@taiga-ui/kit'
import { QRComponent } from 'src/app/routes/portal/components/qr.component'
import { MultilineResult } from './types'

@Component({
selector: 'app-action-success-multiline',
template: `
@if (name()) {
<span tuiTitle>
<strong>{{ name() }}</strong>
@if (description()) {
<span tuiSubtitle>{{ description() }}</span>
}
</span>
}
<tui-textfield>
<textarea
tuiTextarea
[max]="16"
[min]="3"
[readOnly]="true"
[style.filter]="masked() ? 'blur(0.5rem)' : null"
[value]="multiline().value"
></textarea>
@if (multiline().masked) {
<button
tuiIconButton
appearance="icon"
size="s"
type="button"
tabindex="-1"
[iconStart]="masked() ? '@tui.eye' : '@tui.eye-off'"
(pointerdown.stop)="(0)"
(click)="masked.set(!masked())"
>
{{ 'Reveal/Hide' | i18n }}
</button>
}
@if (multiline().copyable) {
<button
tuiIconButton
appearance="icon"
size="s"
type="button"
tabindex="-1"
iconStart="@tui.copy"
(pointerdown.stop)="(0)"
(click)="copy.copy(multiline().value)"
>
{{ 'Copy' | i18n }}
</button>
}
@if (multiline().qr) {
<button
tuiIconButton
appearance="icon"
size="s"
type="button"
tabindex="-1"
iconStart="@tui.qr-code"
(pointerdown.stop)="(0)"
(click)="show(qr)"
>
{{ 'Show QR' | i18n }}
</button>
}
@if (multiline().filename; as filename) {
<a
tuiIconButton
appearance="icon"
size="s"
tabindex="-1"
iconStart="@tui.download"
[download]="filename"
[href]="href()"
(pointerdown.stop)="(0)"
>
{{ 'Download' | i18n }}
</a>
}
</tui-textfield>
<ng-template #qr>
<app-qr
[value]="multiline().value"
[style.filter]="masked() ? 'blur(0.5rem)' : null"
/>
@if (masked()) {
<button
tuiIconButton
class="reveal"
iconStart="@tui.eye"
[style.border-radius.%]="100"
(click)="masked.set(false)"
>
{{ 'Reveal' | i18n }}
</button>
}
</ng-template>
`,
styles: `
@use '@taiga-ui/styles/utils' as taiga;

:host {
display: flex;
flex-direction: column;
gap: 0.5rem;
}

tui-textfield {
font-family: var(--tui-typography-family-code);
}

.reveal {
@include taiga.center-all();
}
`,
imports: [TuiButton, TuiTextarea, TuiTitle, QRComponent, i18nPipe],
})
export class ActionSuccessMultilineComponent {
private readonly dialog = inject(DialogService)
readonly copy = inject(CopyService)

readonly multiline = input.required<MultilineResult>()
readonly name = input('')
readonly description = input('')

protected readonly masked = linkedSignal(() => this.multiline().masked)
protected readonly href = computed(() =>
URL.createObjectURL(
new Blob([this.multiline().value], { type: 'application/octet-stream' }),
),
)

protected show(template: TemplateRef<any>) {
const masked = this.masked()

this.masked.set(this.multiline().masked)
this.dialog
.openComponent(template, { label: 'Scan this QR', size: 's' })
.subscribe({ complete: () => this.masked.set(masked) })
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { TuiDialogContext } from '@taiga-ui/core'
import { NgDompurifyPipe } from '@taiga-ui/dompurify'
import { injectContext } from '@taiga-ui/polymorpheus'
import { ActionSuccessGroupComponent } from './action-success-group.component'
import { ActionSuccessMultilineComponent } from './action-success-multiline.component'
import { ActionSuccessSingleComponent } from './action-success-single.component'
import { ActionResponse } from './types'

Expand All @@ -24,12 +25,23 @@ import { ActionResponse } from './types'
@if (single) {
<app-action-success-single [single]="single" />
}
@if (multiline) {
<app-action-success-multiline [multiline]="multiline" />
}
@if (group) {
<app-action-success-group [group]="group" />
}
`,
styles: `
:host {
display: flex;
flex-direction: column;
gap: 1rem;
}
`,
imports: [
ActionSuccessGroupComponent,
ActionSuccessMultilineComponent,
ActionSuccessSingleComponent,
NgDompurifyPipe,
MarkdownPipe,
Expand All @@ -43,6 +55,8 @@ export class ActionSuccessPage {
readonly message = this.data.message as i18nKey | null
readonly single =
this.data.result?.type === 'single' ? this.data.result : null
readonly multiline =
this.data.result?.type === 'multiline' ? this.data.result : null
readonly group = this.data.result?.type === 'group' ? this.data.result : null

readonly options = { breaks: true }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import { ActionRes } from 'src/app/services/api/api.types'
export type ActionResponse = NonNullable<ActionRes>
type ActionResult = NonNullable<ActionResponse['result']>
export type SingleResult = ActionResult & { type: 'single' }
export type MultilineResult = ActionResult & { type: 'multiline' }
export type GroupResult = ActionResult & { type: 'group' }
8 changes: 8 additions & 0 deletions projects/start-os/web/ui/src/app/services/action.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { inject, Injectable } from '@angular/core'
import { DialogService, getErrorMessage, i18nKey } from '@start9labs/shared'
import { T } from '@start9labs/start-core'
import { TuiNotificationMiddleService } from '@taiga-ui/kit'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { filter } from 'rxjs'
Expand Down Expand Up @@ -68,6 +69,7 @@ export class ActionService {
.openComponent(new PolymorpheusComponent(ActionSuccessPage), {
label: res.title as i18nKey,
data: res,
size: res.result && hasMultiline(res.result) ? 'l' : 'm',
})
.subscribe()
}
Expand All @@ -80,3 +82,9 @@ export class ActionService {
}
}
}

function hasMultiline(value: T.ActionResultValue): boolean {
return value.type === 'group'
? value.value.some(hasMultiline)
: value.type === 'multiline'
}
Loading
Loading