mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
Gateways, domains, and new service interface (#3001)
* add support for inbound proxies * backend changes * fix file type * proxy -> tunnel, implement backend apis * wip start-tunneld * add domains and gateways, remove routers, fix docs links * dont show hidden actions * show and test dns * edit instead of chnage acme and change gateway * refactor: domains page * refactor: gateways page * domains and acme refactor * certificate authorities * refactor public/private gateways * fix fe types * domains mostly finished * refactor: add file control to form service * add ip util to sdk * domains api + migration * start service interface page, WIP * different options for clearnet domains * refactor: styles for interfaces page * minor * better placeholder for no addresses * start sorting addresses * best address logic * comments * fix unnecessary export * MVP of service interface page * domains preferred * fix: address comments * only translations left * wip: start-tunnel & fix build * forms for adding domain, rework things based on new ideas * fix: dns testing * public domain, max width, descriptions for dns * nix StartOS domains, implement public and private domains at interface scope * restart tor instead of reset * better icon for restart tor * dns * fix sort functions for public and private domains * with todos * update types * clean up tech debt, bump dependencies * revert to ts-rs v9 * fix all types * fix dns form * add missing translations * it builds * fix: comments (#3009) * fix: comments * undo default --------- Co-authored-by: Matt Hill <mattnine@protonmail.com> * fix: refactor legacy components (#3010) * fix: comments * fix: refactor legacy components * remove default again --------- Co-authored-by: Matt Hill <mattnine@protonmail.com> * more translations * wip * fix deadlock * coukd work * simple renaming * placeholder for empty service interfaces table * honor hidden form values * remove logs * reason instead of description * fix dns * misc fixes * implement toggling gateways for service interface * fix showing dns records * move status column in service list * remove unnecessary truthy check * refactor: refactor forms components and remove legacy Taiga UI package (#3012) * handle wh file uploads * wip: debugging tor * socks5 proxy working * refactor: fix multiple comments (#3013) * refactor: fix multiple comments * styling changes, add documentation to sidebar * translations for dns page * refactor: subtle colors * rearrange service page --------- Co-authored-by: Matt Hill <mattnine@protonmail.com> * fix file_stream and remove non-terminating test * clean up logs * support for sccache * fix gha sccache * more marketplace translations * install wizard clarity * stub hostnameInfo in migration * fix address info after setup, fix styling on SI page, new 040 release notes * remove tor logs from os * misc fixes * reset tor still not functioning... * update ts * minor styling and wording * chore: some fixes (#3015) * fix gateway renames * different handling for public domains * styling fixes * whole navbar should not be clickable on service show page * timeout getState request * remove links from changelog * misc fixes from pairing * use custom name for gateway in more places * fix dns parsing * closes #3003 * closes #2999 * chore: some fixes (#3017) * small copy change * revert hardcoded error for testing * dont require port forward if gateway is public * use old wan ip when not available * fix .const hanging on undefined * fix test * fix doc test * fix renames * update deps * allow specifying dependency metadata directly * temporarily make dependencies not cliackable in marketplace listings * fix socks bind * fix test --------- Co-authored-by: Aiden McClelland <me@drbonez.dev> Co-authored-by: waterplea <alexander@inkin.ru>
This commit is contained in:
5937
web/package-lock.json
generated
5937
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "startos-ui",
|
||||
"version": "0.4.0-alpha.9",
|
||||
"version": "0.4.0-alpha.10",
|
||||
"author": "Start9 Labs, Inc",
|
||||
"homepage": "https://start9.com/",
|
||||
"license": "MIT",
|
||||
@@ -46,20 +46,19 @@
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"@start9labs/argon2": "^0.3.0",
|
||||
"@start9labs/start-sdk": "file:../sdk/baseDist",
|
||||
"@taiga-ui/addon-charts": "4.44.0",
|
||||
"@taiga-ui/addon-commerce": "4.44.0",
|
||||
"@taiga-ui/addon-mobile": "4.44.0",
|
||||
"@taiga-ui/addon-table": "4.44.0",
|
||||
"@taiga-ui/cdk": "4.44.0",
|
||||
"@taiga-ui/core": "4.44.0",
|
||||
"@taiga-ui/event-plugins": "4.6.0",
|
||||
"@taiga-ui/experimental": "4.44.0",
|
||||
"@taiga-ui/icons": "4.44.0",
|
||||
"@taiga-ui/kit": "4.44.0",
|
||||
"@taiga-ui/layout": "4.44.0",
|
||||
"@taiga-ui/legacy": "4.44.0",
|
||||
"@taiga-ui/polymorpheus": "4.9.0",
|
||||
"@taiga-ui/addon-charts": "4.52.0",
|
||||
"@taiga-ui/addon-commerce": "4.52.0",
|
||||
"@taiga-ui/addon-mobile": "4.52.0",
|
||||
"@taiga-ui/addon-table": "4.52.0",
|
||||
"@taiga-ui/cdk": "4.52.0",
|
||||
"@taiga-ui/core": "4.52.0",
|
||||
"@taiga-ui/dompurify": "4.1.11",
|
||||
"@taiga-ui/event-plugins": "4.7.0",
|
||||
"@taiga-ui/experimental": "4.52.0",
|
||||
"@taiga-ui/icons": "4.52.0",
|
||||
"@taiga-ui/kit": "4.52.0",
|
||||
"@taiga-ui/layout": "4.52.0",
|
||||
"@taiga-ui/polymorpheus": "4.9.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"base64-js": "^1.5.1",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -68,8 +67,8 @@
|
||||
"core-js": "^3.42.0",
|
||||
"cron": "^2.2.0",
|
||||
"cronstrue": "^2.21.0",
|
||||
"dompurify": "^3.1.7",
|
||||
"deep-equality-data-structures": "1.5.1",
|
||||
"dompurify": "^3.1.7",
|
||||
"fast-json-patch": "^3.1.1",
|
||||
"fuse.js": "^6.4.6",
|
||||
"jose": "^4.9.0",
|
||||
@@ -83,17 +82,18 @@
|
||||
"patch-db-client": "file:../patch-db/client",
|
||||
"pbkdf2": "^3.1.2",
|
||||
"rxjs": "^7.8.2",
|
||||
"tldts": "^7.0.11",
|
||||
"ts-matches": "^6.3.2",
|
||||
"tslib": "^2.8.1",
|
||||
"uuid": "^8.3.2",
|
||||
"zone.js": "^0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-experts/hawkeye": "^1.7.2",
|
||||
"@angular/build": "^20.1.0",
|
||||
"@angular/cli": "^20.1.0",
|
||||
"@angular/compiler-cli": "^20.1.0",
|
||||
"@angular/language-service": "^20.1.0",
|
||||
"@angular-experts/hawkeye": "^1.7.2",
|
||||
"@types/dompurify": "3.0.5",
|
||||
"@types/estree": "^0.0.51",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
Back
|
||||
</button>
|
||||
}
|
||||
<h1>{{ selected ? 'Install Type' : 'Select Disk' }}</h1>
|
||||
<h1>{{ selected ? 'Install Type' : 'StartOS Install' }}</h1>
|
||||
@if (!selected) {
|
||||
<h2>Select Disk</h2>
|
||||
}
|
||||
<div [style.color]="'var(--tui-text-negative)'">{{ error }}</div>
|
||||
</header>
|
||||
<div class="pages">
|
||||
|
||||
@@ -34,6 +34,9 @@ main {
|
||||
text-align: center;
|
||||
padding-top: 0.25rem;
|
||||
margin-bottom: -2rem;
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.back {
|
||||
|
||||
@@ -50,10 +50,7 @@ export class AppComponent {
|
||||
|
||||
private async reboot() {
|
||||
this.dialogs
|
||||
.open(
|
||||
'Remove the USB stick and reboot your device to begin using your new Start9 server',
|
||||
SUCCESS,
|
||||
)
|
||||
.open('1. Remove the USB stick<br />2. Click "Reboot" below', SUCCESS)
|
||||
.subscribe({
|
||||
complete: async () => {
|
||||
const loader = this.loader.open().subscribe()
|
||||
@@ -62,7 +59,7 @@ export class AppComponent {
|
||||
await this.api.reboot()
|
||||
this.dialogs
|
||||
.open(
|
||||
'Please wait for StartOS to restart, then refresh this page',
|
||||
'Please wait 1-2 minutes, then refresh this page to access the StartOS setup wizard.',
|
||||
{
|
||||
label: 'Rebooting',
|
||||
size: 's',
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import {
|
||||
provideHttpClient,
|
||||
withFetch,
|
||||
withInterceptorsFromDi,
|
||||
} from '@angular/common/http'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||
import {
|
||||
@@ -50,7 +54,7 @@ const {
|
||||
provide: RELATIVE_URL,
|
||||
useValue: `/${api.url}/${api.version}`,
|
||||
},
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClient(withInterceptorsFromDi(), withFetch()),
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { TuiDialogOptions } from '@taiga-ui/core'
|
||||
import { TuiConfirmData } from '@taiga-ui/kit'
|
||||
|
||||
export const SUCCESS: Partial<TuiDialogOptions<any>> = {
|
||||
label: 'Install Success',
|
||||
label: 'Install Success!',
|
||||
closeable: false,
|
||||
size: 's',
|
||||
data: { button: 'Reboot' },
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
[url]="registry?.url || ''"
|
||||
/>
|
||||
<h1 [tuiSkeleton]="!registry">
|
||||
{{ registry?.info?.name || 'Unnamed Registry' }}
|
||||
{{ registry?.info?.name || 'Unnamed registry' }}
|
||||
</h1>
|
||||
<!-- change registry modal -->
|
||||
<ng-content select="[slot=desktop]"></ng-content>
|
||||
@@ -62,12 +62,8 @@
|
||||
<div>
|
||||
<!-- link to store for brochure -->
|
||||
<ng-content select="[slot=store-mobile]" />
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://docs.start9.com/latest/packaging-guide/"
|
||||
>
|
||||
<span>Package a service</span>
|
||||
<a docsLink path="/packaging-guide">
|
||||
<span>{{ 'Package a service' | i18n }}</span>
|
||||
<tui-icon tuiAppearance="icon" icon="@tui.external-link" />
|
||||
</a>
|
||||
</div>
|
||||
@@ -90,12 +86,8 @@
|
||||
<div>
|
||||
<!-- link to store for brochure -->
|
||||
<ng-content select="[slot=store]" />
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://docs.start9.com/latest/packaging-guide/"
|
||||
>
|
||||
<span>Package a service</span>
|
||||
<a docsLink path="/packaging-guide">
|
||||
<span>{{ 'Package a service' | i18n }}</span>
|
||||
<tui-icon tuiAppearance="icon" icon="@tui.external-link" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { SharedPipesModule } from '@start9labs/shared'
|
||||
import {
|
||||
DocsLinkDirective,
|
||||
i18nPipe,
|
||||
SharedPipesModule,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiLet } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TuiAppearance,
|
||||
@@ -31,6 +35,8 @@ import { MenuComponent } from './menu.component'
|
||||
TuiSkeleton,
|
||||
TuiDrawer,
|
||||
TuiPopup,
|
||||
i18nPipe,
|
||||
DocsLinkDirective,
|
||||
],
|
||||
declarations: [MenuComponent],
|
||||
exports: [MenuComponent],
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
output,
|
||||
} from '@angular/core'
|
||||
import { MarketplacePkgBase } from '../../types'
|
||||
import { CopyService } from '@start9labs/shared'
|
||||
import { CopyService, i18nPipe } from '@start9labs/shared'
|
||||
import { DatePipe } from '@angular/common'
|
||||
import { MarketplaceItemComponent } from './item.component'
|
||||
|
||||
@@ -36,7 +36,7 @@ import { MarketplaceItemComponent } from './item.component'
|
||||
<marketplace-item
|
||||
[style.pointer-events]="'none'"
|
||||
[data]="pkg().sdkVersion || 'Unknown'"
|
||||
label="SDK Version"
|
||||
label="SDK version"
|
||||
icon=""
|
||||
/>
|
||||
<!-- git hash -->
|
||||
@@ -44,15 +44,15 @@ import { MarketplaceItemComponent } from './item.component'
|
||||
<marketplace-item
|
||||
(click)="copyService.copy(gitHash)"
|
||||
[data]="gitHash"
|
||||
label="Git Hash"
|
||||
label="Git hash"
|
||||
icon="@tui.copy"
|
||||
class="item-copy"
|
||||
/>
|
||||
} @else {
|
||||
<div class="item-padding">
|
||||
<label tuiTitle>
|
||||
<span tuiSubtitle>Git Hash</span>
|
||||
Unknown
|
||||
<span tuiSubtitle>{{ 'Git hash' | i18n }}</span>
|
||||
{{ 'Unknown' | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
@@ -128,7 +128,7 @@ import { MarketplaceItemComponent } from './item.component'
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [MarketplaceItemComponent, DatePipe],
|
||||
imports: [MarketplaceItemComponent, DatePipe, i18nPipe],
|
||||
})
|
||||
export class MarketplaceAboutComponent {
|
||||
readonly copyService = inject(CopyService)
|
||||
|
||||
@@ -8,13 +8,14 @@ import {
|
||||
} from '@angular/core'
|
||||
import { MarketplacePkgBase } from '../../../types'
|
||||
import { MarketplaceDepItemComponent } from './dependency-item.component'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-dependencies',
|
||||
template: `
|
||||
<div class="background-border shadow-color-light box-shadow-lg">
|
||||
<div class="dependencies-container">
|
||||
<h2 class="additional-detail-title">Dependencies</h2>
|
||||
<h2 class="additional-detail-title">{{ 'Dependencies' | i18n }}</h2>
|
||||
<div class="dependencies-list">
|
||||
@for (dep of pkg.dependencyMetadata | keyvalue; track $index) {
|
||||
<marketplace-dep-item
|
||||
@@ -48,7 +49,7 @@ import { MarketplaceDepItemComponent } from './dependency-item.component'
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule, MarketplaceDepItemComponent],
|
||||
imports: [CommonModule, MarketplaceDepItemComponent, i18nPipe],
|
||||
})
|
||||
export class MarketplaceDependenciesComponent {
|
||||
@Input({ required: true })
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { KeyValue } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { ExverPipesModule } from '@start9labs/shared'
|
||||
import { ExverPipesModule, i18nPipe } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiAvatar, TuiLineClamp } from '@taiga-ui/kit'
|
||||
import { MarketplacePkgBase } from '../../../types'
|
||||
@@ -20,9 +20,9 @@ import { MarketplacePkgBase } from '../../../types'
|
||||
</span>
|
||||
<p>
|
||||
@if (dep.value.optional) {
|
||||
<span>(optional)</span>
|
||||
<span>({{ 'Optional' | i18n }})</span>
|
||||
} @else {
|
||||
<span>(required)</span>
|
||||
<span>({{ 'Required' | i18n }})</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
@@ -49,10 +49,11 @@ import { MarketplacePkgBase } from '../../../types'
|
||||
filter: drop-shadow(0 10px 8px rgb(0 0 0 / 0.04))
|
||||
drop-shadow(0 4px 3px rgb(0 0 0 / 0.1));
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(63 63 70 / 0.7);
|
||||
cursor: pointer;
|
||||
}
|
||||
// @TODO re-engage when button can link to root with search QP
|
||||
// &:hover {
|
||||
// background-color: rgb(63 63 70 / 0.7);
|
||||
// cursor: pointer;
|
||||
// }
|
||||
}
|
||||
|
||||
.title {
|
||||
@@ -88,7 +89,7 @@ import { MarketplacePkgBase } from '../../../types'
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [RouterModule, TuiAvatar, ExverPipesModule, TuiLineClamp],
|
||||
imports: [RouterModule, TuiAvatar, ExverPipesModule, TuiLineClamp, i18nPipe],
|
||||
})
|
||||
export class MarketplaceDepItemComponent {
|
||||
@Input({ required: true })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { SharedPipesModule } from '@start9labs/shared'
|
||||
import { i18nPipe, SharedPipesModule } from '@start9labs/shared'
|
||||
import { TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiAvatar } from '@taiga-ui/kit'
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
@@ -11,7 +11,9 @@ import { MarketplacePkg } from '../../types'
|
||||
template: `
|
||||
<div class="background-border box-shadow-lg shadow-color-light">
|
||||
<div class="box-container">
|
||||
<h2 class="additional-detail-title">Alternative Implementations</h2>
|
||||
<h2 class="additional-detail-title">
|
||||
{{ 'Alternative Implementations' | i18n }}
|
||||
</h2>
|
||||
@for (pkg of pkgs; track $index) {
|
||||
<a
|
||||
tuiCell
|
||||
@@ -42,7 +44,14 @@ import { MarketplacePkg } from '../../types'
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [RouterLink, TuiCell, TuiTitle, SharedPipesModule, TuiAvatar],
|
||||
imports: [
|
||||
RouterLink,
|
||||
TuiCell,
|
||||
TuiTitle,
|
||||
SharedPipesModule,
|
||||
TuiAvatar,
|
||||
i18nPipe,
|
||||
],
|
||||
})
|
||||
export class MarketplaceFlavorsComponent {
|
||||
@Input()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { i18nKey } from '@start9labs/shared'
|
||||
import { TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiFade } from '@taiga-ui/kit'
|
||||
|
||||
@@ -6,7 +7,7 @@ import { TuiFade } from '@taiga-ui/kit'
|
||||
selector: 'marketplace-item',
|
||||
template: `
|
||||
<label tuiTitle>
|
||||
<span tuiSubtitle>{{ label }}</span>
|
||||
<span tuiSubtitle>{{ label || '' }}</span>
|
||||
<span tuiFade>{{ data }}</span>
|
||||
</label>
|
||||
<tui-icon [icon]="icon" />
|
||||
@@ -38,7 +39,7 @@ import { TuiFade } from '@taiga-ui/kit'
|
||||
})
|
||||
export class MarketplaceItemComponent {
|
||||
@Input({ required: true })
|
||||
label!: string
|
||||
label!: i18nKey | null
|
||||
|
||||
@Input({ required: true })
|
||||
icon!: string
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { MarketplaceItemComponent } from './item.component'
|
||||
import { i18nKey } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-link',
|
||||
@@ -13,7 +14,7 @@ import { MarketplaceItemComponent } from './item.component'
|
||||
})
|
||||
export class MarketplaceLinkComponent {
|
||||
@Input({ required: true })
|
||||
label!: string
|
||||
label!: i18nKey
|
||||
|
||||
@Input({ required: true })
|
||||
icon!: string
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { CopyService } from '@start9labs/shared'
|
||||
import { CopyService, i18nPipe } from '@start9labs/shared'
|
||||
import { MarketplacePkgBase } from '../../types'
|
||||
import { MarketplaceLinkComponent } from './link.component'
|
||||
|
||||
@@ -18,13 +18,13 @@ import { MarketplaceLinkComponent } from './link.component'
|
||||
<div class="detail-container">
|
||||
<marketplace-link
|
||||
[url]="pkg().upstreamRepo"
|
||||
label="Upstream Service"
|
||||
label="Upstream service"
|
||||
icon="@tui.external-link"
|
||||
class="item-pointer"
|
||||
/>
|
||||
<marketplace-link
|
||||
[url]="pkg().wrapperRepo"
|
||||
label="StartOS Package"
|
||||
label="StartOS package"
|
||||
icon="@tui.external-link"
|
||||
class="item-pointer"
|
||||
/>
|
||||
@@ -34,7 +34,7 @@ import { MarketplaceLinkComponent } from './link.component'
|
||||
|
||||
<div class="background-border shadow-color-light box-shadow-lg">
|
||||
<div class="box-container">
|
||||
<h2 class="additional-detail-title">Links</h2>
|
||||
<h2 class="additional-detail-title">{{ 'Links' | i18n }}</h2>
|
||||
<div class="detail-container">
|
||||
<marketplace-link
|
||||
[url]="pkg().marketingSite"
|
||||
@@ -117,7 +117,7 @@ import { MarketplaceLinkComponent } from './link.component'
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [MarketplaceLinkComponent],
|
||||
imports: [MarketplaceLinkComponent, i18nPipe],
|
||||
})
|
||||
export class MarketplaceLinksComponent {
|
||||
readonly copyService = inject(CopyService)
|
||||
|
||||
@@ -18,12 +18,12 @@ import { MarketplaceItemComponent } from './item.component'
|
||||
template: `
|
||||
<div class="background-border shadow-color-light box-shadow-lg">
|
||||
<div class="box-container">
|
||||
<h2 class="additional-detail-title">Versions</h2>
|
||||
<h2 class="additional-detail-title">{{ 'Versions' | i18n }}</h2>
|
||||
<marketplace-item
|
||||
(click)="promptSelectVersion(versionSelect)"
|
||||
data="Select another version"
|
||||
[data]="'Select another version' | i18n"
|
||||
icon="@tui.chevron-right"
|
||||
label=""
|
||||
[label]="null"
|
||||
class="select"
|
||||
/>
|
||||
<ng-template
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import {
|
||||
provideHttpClient,
|
||||
withFetch,
|
||||
withInterceptorsFromDi,
|
||||
} from '@angular/common/http'
|
||||
import { inject, NgModule, provideAppInitializer } from '@angular/core'
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||
import { PreloadAllModules, RouterModule } from '@angular/router'
|
||||
@@ -51,7 +55,7 @@ const version = require('../../../../package.json').version
|
||||
provide: VERSION,
|
||||
useValue: version,
|
||||
},
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClient(withInterceptorsFromDi(), withFetch()),
|
||||
provideAppInitializer(() => {
|
||||
const origin = inject(WA_LOCATION).origin
|
||||
const module_or_path = new URL('/assets/argon2_bg.wasm', origin)
|
||||
|
||||
@@ -14,9 +14,13 @@ import {
|
||||
TuiDialogContext,
|
||||
TuiDialogService,
|
||||
TuiError,
|
||||
TuiTextfield,
|
||||
} from '@taiga-ui/core'
|
||||
import { TUI_VALIDATION_ERRORS, TuiFieldErrorPipe } from '@taiga-ui/kit'
|
||||
import { TuiInputModule, TuiInputPasswordModule } from '@taiga-ui/legacy'
|
||||
import {
|
||||
TUI_VALIDATION_ERRORS,
|
||||
TuiFieldErrorPipe,
|
||||
TuiPassword,
|
||||
} from '@taiga-ui/kit'
|
||||
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { SERVERS, ServersResponse } from 'src/app/components/servers.component'
|
||||
import { ApiService } from 'src/app/services/api.service'
|
||||
@@ -30,39 +34,47 @@ export interface CifsResponse {
|
||||
@Component({
|
||||
template: `
|
||||
<form [formGroup]="form" (ngSubmit)="submit()">
|
||||
<tui-input formControlName="hostname">
|
||||
Hostname *
|
||||
<tui-textfield>
|
||||
<label tuiLabel>Hostname *</label>
|
||||
<input
|
||||
tuiTextfieldLegacy
|
||||
tuiTextfield
|
||||
formControlName="hostname"
|
||||
placeholder="e.g. 'My Computer' OR 'my-computer.local'"
|
||||
/>
|
||||
</tui-input>
|
||||
</tui-textfield>
|
||||
<tui-error
|
||||
formControlName="hostname"
|
||||
[error]="['required'] | tuiFieldError | async"
|
||||
></tui-error>
|
||||
/>
|
||||
|
||||
<tui-input formControlName="path" class="input">
|
||||
Path *
|
||||
<input tuiTextfieldLegacy placeholder="/Desktop/my-folder'" />
|
||||
</tui-input>
|
||||
<tui-error
|
||||
formControlName="path"
|
||||
[error]="[] | tuiFieldError | async"
|
||||
></tui-error>
|
||||
<tui-textfield class="input">
|
||||
<label tuiLabel>Path *</label>
|
||||
<input
|
||||
tuiTextfield
|
||||
formControlName="path"
|
||||
placeholder="/Desktop/my-folder'"
|
||||
/>
|
||||
</tui-textfield>
|
||||
<tui-error formControlName="path" [error]="[] | tuiFieldError | async" />
|
||||
|
||||
<tui-input formControlName="username" class="input">
|
||||
Username *
|
||||
<input tuiTextfieldLegacy placeholder="Enter username" />
|
||||
</tui-input>
|
||||
<tui-textfield class="input">
|
||||
<label tuiLabel>Username *</label>
|
||||
<input
|
||||
tuiTextfield
|
||||
formControlName="username"
|
||||
placeholder="Enter username"
|
||||
/>
|
||||
</tui-textfield>
|
||||
<tui-error
|
||||
formControlName="username"
|
||||
[error]="[] | tuiFieldError | async"
|
||||
></tui-error>
|
||||
/>
|
||||
|
||||
<tui-input-password formControlName="password" class="input">
|
||||
Password
|
||||
</tui-input-password>
|
||||
<tui-textfield class="input">
|
||||
<label tuiLabel>Password</label>
|
||||
<input tuiTextfield type="password" formControlName="password" />
|
||||
<tui-icon tuiPassword />
|
||||
</tui-textfield>
|
||||
|
||||
<footer>
|
||||
<button
|
||||
@@ -93,8 +105,8 @@ export interface CifsResponse {
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
TuiButton,
|
||||
TuiInputModule,
|
||||
TuiInputPasswordModule,
|
||||
TuiTextfield,
|
||||
TuiPassword,
|
||||
TuiError,
|
||||
TuiFieldErrorPipe,
|
||||
],
|
||||
|
||||
@@ -44,7 +44,7 @@ import { DocsLinkDirective } from '@start9labs/shared'
|
||||
Download your server's Root CA and
|
||||
<a
|
||||
docsLink
|
||||
href="/user-manual/trust-ca.html"
|
||||
path="/user-manual/trust-ca.html"
|
||||
style="color: #6866cc; font-weight: bold; text-decoration: none"
|
||||
>
|
||||
follow the instructions
|
||||
@@ -110,7 +110,7 @@ import { DocsLinkDirective } from '@start9labs/shared'
|
||||
This address will only work from a Tor-enabled browser.
|
||||
<a
|
||||
docsLink
|
||||
href="/user-manual/connecting-remotely/tor.html"
|
||||
path="/user-manual/connecting-remotely/tor.html"
|
||||
style="color: #6866cc; font-weight: bold; text-decoration: none"
|
||||
>
|
||||
Follow the instructions
|
||||
|
||||
@@ -7,11 +7,15 @@ import {
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { Router } from '@angular/router'
|
||||
import {
|
||||
DialogService,
|
||||
formatProgress,
|
||||
getErrorMessage,
|
||||
i18nKey,
|
||||
InitializingComponent,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import {
|
||||
catchError,
|
||||
filter,
|
||||
@@ -26,19 +30,43 @@ import { ApiService } from 'src/app/services/api.service'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
|
||||
@Component({
|
||||
template:
|
||||
'<app-initializing [setupType]="type" [progress]="progress()" [error]="error()" />',
|
||||
template: `
|
||||
@if (error(); as err) {
|
||||
<section>
|
||||
<h1>{{ 'Error initializing server' }}</h1>
|
||||
<p>{{ err }}</p>
|
||||
<button tuiButton (click)="restart()">
|
||||
{{ 'Restart server' }}
|
||||
</button>
|
||||
</section>
|
||||
} @else {
|
||||
<app-initializing [initialSetup]="true" [progress]="progress()" />
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
max-width: unset;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
section {
|
||||
border-radius: 0.25rem;
|
||||
padding: 1rem;
|
||||
margin: 1.5rem;
|
||||
text-align: center;
|
||||
// @TODO Theme
|
||||
background: #e0e0e0;
|
||||
color: #333;
|
||||
--tui-background-neutral-1: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
`,
|
||||
imports: [InitializingComponent],
|
||||
imports: [InitializingComponent, TuiButton],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export default class LoadingPage {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly dialog = inject(DialogService)
|
||||
|
||||
readonly type = inject(StateService).setupType
|
||||
readonly router = inject(Router)
|
||||
@@ -84,4 +112,21 @@ export default class LoadingPage {
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
const loader = this.loader.open(undefined).subscribe()
|
||||
|
||||
try {
|
||||
await this.api.restart()
|
||||
this.dialog
|
||||
.openAlert('Wait 1-2 minutes and refresh the page' as i18nKey, {
|
||||
label: 'Server is restarting',
|
||||
})
|
||||
.subscribe()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,12 +127,11 @@ export default class SuccessPage implements AfterViewInit {
|
||||
}
|
||||
|
||||
download() {
|
||||
const torAddress = this.document.getElementById('tor-addr')
|
||||
const lanAddress = this.document.getElementById('lan-addr')
|
||||
const html = this.documentation?.nativeElement.innerHTML || ''
|
||||
const torElem = this.document.getElementById('tor-addr')
|
||||
const lanElem = this.document.getElementById('lan-addr')
|
||||
|
||||
if (torAddress) torAddress.innerHTML = this.torAddresses?.join('\n') || ''
|
||||
if (lanAddress) lanAddress.innerHTML = this.lanAddress || ''
|
||||
if (torElem) torElem.innerHTML = this.torAddresses?.join('\n') || ''
|
||||
if (lanElem) lanElem.innerHTML = this.lanAddress || ''
|
||||
|
||||
this.document
|
||||
.getElementById('cert')
|
||||
@@ -140,6 +139,9 @@ export default class SuccessPage implements AfterViewInit {
|
||||
'href',
|
||||
`data:application/x-x509-ca-cert;base64,${encodeURIComponent(this.cert!)}`,
|
||||
)
|
||||
|
||||
const html = this.documentation?.nativeElement.innerHTML || ''
|
||||
|
||||
this.downloadHtml.download('StartOS-info.html', html).then(_ => {
|
||||
this.disableLogin = false
|
||||
})
|
||||
|
||||
@@ -24,6 +24,7 @@ export abstract class ApiService {
|
||||
abstract complete(): Promise<T.SetupResult> // setup.complete
|
||||
abstract exit(): Promise<void> // setup.exit
|
||||
abstract initFollowLogs(): Promise<FollowLogsRes> // setup.logs.follow
|
||||
abstract restart(): Promise<void> // setup.restart
|
||||
abstract openWebsocket$<T>(guid: string): Observable<T>
|
||||
|
||||
async encrypt(toEncrypt: string): Promise<T.EncryptedWire> {
|
||||
|
||||
@@ -120,6 +120,13 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
await this.rpcRequest<void>({
|
||||
method: 'setup.restart',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
private async rpcRequest<T>(opts: RPCOptions): Promise<T> {
|
||||
const res = await this.http.rpcRequest<T>(opts)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import * as jose from 'node-jose'
|
||||
import { interval, map, Observable } from 'rxjs'
|
||||
import { first, interval, map, Observable } from 'rxjs'
|
||||
import { ApiService } from './api.service'
|
||||
|
||||
@Injectable({
|
||||
@@ -119,6 +119,7 @@ export class MockApiService extends ApiService {
|
||||
} else if (guid === 'progress-guid') {
|
||||
// @TODO Matt mock progress
|
||||
return interval(1000).pipe(
|
||||
first(),
|
||||
map(() => ({
|
||||
overall: true,
|
||||
phases: [
|
||||
@@ -323,6 +324,10 @@ export class MockApiService extends ApiService {
|
||||
async exit(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
}
|
||||
}
|
||||
|
||||
const rootCA = `-----BEGIN CERTIFICATE-----
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000" width="128" height="128" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><title style="" fill="#fff">Bitcoin icon</title><path d="M23.638 14.904c-1.602 6.43-8.113 10.34-14.542 8.736C2.67 22.05-1.244 15.525.362 9.105 1.962 2.67 8.475-1.243 14.9.358c6.43 1.605 10.342 8.115 8.738 14.548v-.002zm-6.35-4.613c.24-1.59-.974-2.45-2.64-3.03l.54-2.153-1.315-.33-.525 2.107c-.345-.087-.705-.167-1.064-.25l.526-2.127-1.32-.33-.54 2.165c-.285-.067-.565-.132-.84-.2l-1.815-.45-.35 1.407s.975.225.955.236c.535.136.63.486.615.766l-1.477 5.92c-.075.166-.24.406-.614.314.015.02-.96-.24-.96-.24l-.66 1.51 1.71.426.93.242-.54 2.19 1.32.327.54-2.17c.36.1.705.19 1.05.273l-.51 2.154 1.32.33.545-2.19c2.24.427 3.93.257 4.64-1.774.57-1.637-.03-2.58-1.217-3.196.854-.193 1.5-.76 1.68-1.93h.01zm-3.01 4.22c-.404 1.64-3.157.75-4.05.53l.72-2.9c.896.23 3.757.67 3.33 2.37zm.41-4.24c-.37 1.49-2.662.735-3.405.55l.654-2.64c.744.18 3.137.524 2.75 2.084v.006z" style="" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 18 KiB |
@@ -4,7 +4,6 @@ import {
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { TuiProgress } from '@taiga-ui/kit'
|
||||
import { LogsWindowComponent } from './logs-window.component'
|
||||
@@ -13,32 +12,25 @@ import { i18nPipe } from '../../i18n/i18n.pipe'
|
||||
@Component({
|
||||
selector: 'app-initializing',
|
||||
template: `
|
||||
@if (error(); as err) {
|
||||
<section>
|
||||
<h1>{{ 'Error initializing server' | i18n }}</h1>
|
||||
<p>{{ err }}</p>
|
||||
</section>
|
||||
} @else {
|
||||
<section>
|
||||
<h1 [style.font-size.rem]="2" [style.margin-bottom.rem]="2">
|
||||
{{
|
||||
setupType()
|
||||
? ('Setting up your server' | i18n)
|
||||
: ('Booting StartOS' | i18n)
|
||||
}}
|
||||
</h1>
|
||||
<div>
|
||||
{{ 'Progress' | i18n }}: {{ (progress().total * 100).toFixed(0) }}%
|
||||
</div>
|
||||
<progress
|
||||
tuiProgressBar
|
||||
[style.max-width.rem]="40"
|
||||
[style.margin]="'1rem auto'"
|
||||
[attr.value]="progress().total"
|
||||
></progress>
|
||||
<p [innerHTML]="message()"></p>
|
||||
</section>
|
||||
}
|
||||
<section>
|
||||
<h1 [style.font-size.rem]="2" [style.margin-bottom.rem]="2">
|
||||
{{
|
||||
initialSetup()
|
||||
? ('Setting up your server' | i18n)
|
||||
: ('Booting StartOS' | i18n)
|
||||
}}
|
||||
</h1>
|
||||
<div>
|
||||
{{ 'Progress' | i18n }}: {{ (progress().total * 100).toFixed(0) }}%
|
||||
</div>
|
||||
<progress
|
||||
tuiProgressBar
|
||||
[style.max-width.rem]="40"
|
||||
[style.margin]="'1rem auto'"
|
||||
[attr.value]="progress().total"
|
||||
></progress>
|
||||
<p [innerHTML]="message()"></p>
|
||||
</section>
|
||||
<logs-window />
|
||||
`,
|
||||
styles: `
|
||||
@@ -76,10 +68,7 @@ export class InitializingComponent {
|
||||
total: 0,
|
||||
message: '',
|
||||
})
|
||||
readonly setupType = input<
|
||||
'fresh' | 'restore' | 'attach' | 'transfer' | undefined
|
||||
>()
|
||||
readonly error = input<string>()
|
||||
readonly initialSetup = input(false)
|
||||
|
||||
readonly message = computed(() => {
|
||||
return (
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { TuiAutoFocus } from '@taiga-ui/cdk'
|
||||
import { TuiButton, TuiDialogContext } from '@taiga-ui/core'
|
||||
import { TuiInputModule, TuiTextfieldControllerModule } from '@taiga-ui/legacy'
|
||||
import {
|
||||
POLYMORPHEUS_CONTEXT,
|
||||
PolymorpheusComponent,
|
||||
} from '@taiga-ui/polymorpheus'
|
||||
import { TuiButton, TuiDialogContext, TuiTextfield } from '@taiga-ui/core'
|
||||
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { i18nPipe } from '../i18n/i18n.pipe'
|
||||
import { i18nKey } from '../i18n/i18n.providers'
|
||||
|
||||
@@ -17,23 +13,36 @@ import { i18nKey } from '../i18n/i18n.providers'
|
||||
<p class="warning">{{ options.warning }}</p>
|
||||
}
|
||||
<form (ngSubmit)="submit(value.trim())">
|
||||
<tui-input
|
||||
tuiAutoFocus
|
||||
[tuiTextfieldLabelOutside]="!options.label"
|
||||
[tuiTextfieldCustomContent]="options.useMask ? toggle : ''"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[(ngModel)]="value"
|
||||
>
|
||||
{{ options.label }}
|
||||
@if (options.required !== false && options.label) {
|
||||
<span>*</span>
|
||||
<tui-textfield>
|
||||
@if (options.label) {
|
||||
<label tuiLabel>
|
||||
{{ options.label }}
|
||||
@if (options.required !== false && options.label) {
|
||||
<span>*</span>
|
||||
}
|
||||
</label>
|
||||
}
|
||||
<input
|
||||
tuiTextfieldLegacy
|
||||
tuiTextfield
|
||||
tuiAutoFocus
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[(ngModel)]="value"
|
||||
[class.masked]="options.useMask && masked && value"
|
||||
[placeholder]="options.placeholder || ''"
|
||||
/>
|
||||
</tui-input>
|
||||
@if (options.useMask) {
|
||||
<button
|
||||
tuiIconButton
|
||||
type="button"
|
||||
appearance="icon"
|
||||
title="Toggle masking"
|
||||
size="xs"
|
||||
class="button"
|
||||
[iconStart]="masked ? '@tui.eye' : '@tui.eye-off'"
|
||||
(click)="masked = !masked"
|
||||
></button>
|
||||
}
|
||||
</tui-textfield>
|
||||
<footer class="g-buttons">
|
||||
<button
|
||||
tuiButton
|
||||
@@ -48,19 +57,6 @@ import { i18nKey } from '../i18n/i18n.providers'
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
|
||||
<ng-template #toggle>
|
||||
<button
|
||||
tuiIconButton
|
||||
type="button"
|
||||
appearance="icon"
|
||||
title="Toggle masking"
|
||||
size="xs"
|
||||
class="button"
|
||||
[iconStart]="masked ? '@tui.eye' : '@tui.eye-off'"
|
||||
(click)="masked = !masked"
|
||||
></button>
|
||||
</ng-template>
|
||||
`,
|
||||
styles: `
|
||||
.warning {
|
||||
@@ -76,25 +72,16 @@ import { i18nKey } from '../i18n/i18n.providers'
|
||||
-webkit-text-security: disc;
|
||||
}
|
||||
`,
|
||||
imports: [
|
||||
FormsModule,
|
||||
TuiInputModule,
|
||||
TuiButton,
|
||||
TuiTextfieldControllerModule,
|
||||
TuiAutoFocus,
|
||||
i18nPipe,
|
||||
],
|
||||
imports: [FormsModule, TuiButton, TuiTextfield, TuiAutoFocus, i18nPipe],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PromptModal {
|
||||
private readonly context =
|
||||
injectContext<TuiDialogContext<string, PromptOptions>>()
|
||||
|
||||
masked = this.options.useMask
|
||||
value = this.options.initialValue || ''
|
||||
|
||||
constructor(
|
||||
@Inject(POLYMORPHEUS_CONTEXT)
|
||||
private readonly context: TuiDialogContext<string, PromptOptions>,
|
||||
) {}
|
||||
|
||||
get options(): PromptOptions {
|
||||
return this.context.data
|
||||
}
|
||||
|
||||
@@ -19,11 +19,13 @@ export const VERSION = new InjectionToken<string>('VERSION')
|
||||
export class DocsLinkDirective {
|
||||
private readonly version = inject(VERSION)
|
||||
|
||||
readonly href = input.required<string>()
|
||||
readonly path = input.required<string>()
|
||||
|
||||
readonly fragment = input<string>('')
|
||||
|
||||
protected readonly url = computed(() => {
|
||||
const path = this.href()
|
||||
const path = this.path()
|
||||
const relative = path.startsWith('/') ? path : `/${path}`
|
||||
return `https://docs.start9.com${relative}?os=${this.version}`
|
||||
return `https://docs.start9.com${relative}?os=${this.version}${this.fragment()}`
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { i18n } from '../i18n.providers'
|
||||
export default {
|
||||
1: 'Ändern',
|
||||
2: 'Aktualisieren',
|
||||
3: 'Zurücksetzen',
|
||||
4: 'System',
|
||||
5: 'Allgemein',
|
||||
6: 'E-Mail',
|
||||
@@ -15,7 +14,6 @@ export default {
|
||||
12: 'Aktive Sitzungen',
|
||||
13: 'Passwort ändern',
|
||||
14: 'Allgemeine Einstellungen',
|
||||
15: 'Verwalten Sie Ihre Gesamteinrichtung und Einstellungen',
|
||||
16: 'Browser-Tab Titel',
|
||||
17: 'Sprache',
|
||||
18: 'Festplattenreparatur',
|
||||
@@ -24,7 +22,7 @@ export default {
|
||||
21: 'Stammzertifizierungsstelle (Root-CA)',
|
||||
22: 'Laden Sie Ihre Root-CA herunter',
|
||||
23: 'Herunterladen',
|
||||
24: 'Tor zurücksetzen',
|
||||
24: 'Tor neu starten',
|
||||
25: 'Tor-Daemon auf Ihrem Server neu starten',
|
||||
26: 'Software-Aktualisierung',
|
||||
27: 'Neustart erforderlich',
|
||||
@@ -39,8 +37,8 @@ export default {
|
||||
36: 'Abbrechen',
|
||||
37: 'Diese Aktion sollte nur auf Anweisung eines Start9-Supportmitarbeiters ausgeführt werden. Wir empfehlen, vor dem Fortfahren eine Sicherung Ihres Geräts zu erstellen. Wenn während des Neustarts etwas schiefgeht, z. B. Stromausfall oder das Trennen des Laufwerks, kann das Dateisystem irreparabel beschädigt werden. Bitte fahren Sie mit Vorsicht fort.',
|
||||
38: 'Löschen',
|
||||
39: 'Tor-Reset läuft',
|
||||
40: 'Tor wird zurückgesetzt',
|
||||
39: 'Tor-Neustart läuft',
|
||||
40: 'Tor wird neu gestartet',
|
||||
41: 'Suche nach Updates',
|
||||
42: 'Neustart wird eingeleitet',
|
||||
43: 'Sie verwenden die neueste Version von StartOS.',
|
||||
@@ -48,8 +46,7 @@ export default {
|
||||
45: 'Versionshinweise',
|
||||
46: 'Update starten',
|
||||
47: 'Update wird gestartet',
|
||||
48: 'Sie sind derzeit über Tor verbunden. Wenn Sie den Tor-Daemon zurücksetzen, verlieren Sie die Verbindung, bis dieser wieder online ist.',
|
||||
49: 'Tor zurücksetzen?',
|
||||
48: 'Sie sind derzeit über Tor verbunden. Wenn Sie den Tor-Dienst neu starten, verlieren Sie die Verbindung, bis er wieder online ist.',
|
||||
50: 'Optional Zustand löschen, um neue Guard-Nodes zu erzwingen. Es wird empfohlen, zuerst ohne das Löschen zu versuchen.',
|
||||
51: 'Zustand löschen',
|
||||
52: 'Bestenstand wird gespeichert',
|
||||
@@ -91,30 +88,18 @@ export default {
|
||||
88: 'Aktionen',
|
||||
89: 'nicht empfohlen',
|
||||
90: 'Root-CA ist vertrauenswürdig!',
|
||||
91: 'Fügen Sie eine Clearnet-Adresse hinzu, um diese Oberfläche im Internet verfügbar zu machen. Clearnet-Adressen sind vollständig öffentlich und nicht anonym.',
|
||||
92: 'Mehr erfahren',
|
||||
93: 'Öffentlich machen',
|
||||
94: 'Privat machen',
|
||||
95: 'Keine öffentlichen Adressen',
|
||||
96: 'Domain hinzufügen',
|
||||
96: 'Öffentliche Domain hinzufügen',
|
||||
97: 'Wird entfernt',
|
||||
98: 'Wird öffentlich gemacht',
|
||||
99: 'Wird privat gemacht',
|
||||
100: 'Nicht gespeicherte Änderungen',
|
||||
101: 'Sie haben nicht gespeicherte Änderungen. Möchten Sie die Seite wirklich verlassen?',
|
||||
102: 'Verlassen',
|
||||
103: 'Sind Sie sicher?',
|
||||
104: 'Domain auswählen',
|
||||
105: 'Lokal',
|
||||
106: 'Lokale Adressen sind nur von Geräten erreichbar, die direkt oder über VPN mit demselben LAN wie Ihr Server verbunden sind.',
|
||||
107: 'Mehr erfahren',
|
||||
108: 'Öffentlich',
|
||||
109: 'Privat',
|
||||
110: 'Fügen Sie eine Onion-Adresse hinzu, um dieses Interface anonym im Darknet verfügbar zu machen. Onion-Adressen sind nur über das Tor-Netzwerk erreichbar.',
|
||||
111: 'Keine Onion-Adressen',
|
||||
112: 'Neue Onion-Adresse',
|
||||
109: 'privat',
|
||||
111: 'Keine Onion-Domains',
|
||||
112: 'Neue Onion-Domain',
|
||||
113: 'Privater Schlüssel (optional)',
|
||||
114: 'Optional können Sie einen base64-codierten ed25519-Schlüssel angeben, um die Tor V3 (.onion)-Adresse zu generieren. Wenn nicht angegeben, wird ein zufälliger Schlüssel erstellt.',
|
||||
114: 'Optional können Sie einen base64-codierten ed25519-Privatschlüssel angeben, um die Tor V3 (.onion)-Domain zu erzeugen. Wenn nicht angegeben, wird ein zufälliger Schlüssel generiert.',
|
||||
115: 'Verarbeite 10.000 Logs',
|
||||
116: 'Ältere Logs werden geladen',
|
||||
117: 'Warten auf Netzwerkverbindung',
|
||||
@@ -140,7 +125,6 @@ export default {
|
||||
137: 'Tor-Logs',
|
||||
138: 'Rohdatenprotokolle des Betriebssystems ohne Filter',
|
||||
139: 'Diagnose für Treiber und andere Kernel-Prozesse',
|
||||
140: 'Diagnose-Logs des Tor-Daemons unter StartOS',
|
||||
141: 'Downgrade',
|
||||
142: 'Neu installieren',
|
||||
143: 'Installierte',
|
||||
@@ -179,7 +163,6 @@ export default {
|
||||
177: 'Kernelspeicher',
|
||||
178: 'Leerlauf',
|
||||
179: 'I/O-Wartezeit',
|
||||
180: 'ACME',
|
||||
181: 'Gesamt',
|
||||
182: 'Verwendet',
|
||||
183: 'Verfügbar',
|
||||
@@ -242,7 +225,7 @@ export default {
|
||||
240: 'Name',
|
||||
241: 'Status',
|
||||
242: 'Öffnen',
|
||||
243: 'Schnittstellen',
|
||||
243: 'Service-Schnittstellen',
|
||||
244: 'Hosting',
|
||||
245: 'Installation läuft',
|
||||
246: 'Siehe unten',
|
||||
@@ -290,23 +273,20 @@ export default {
|
||||
292: 'Upload wird gestartet',
|
||||
293: 'Erneut versuchen',
|
||||
294: '.s9pk-Paketdatei hochladen',
|
||||
295: 'Warnung: Der Upload über Tor ist langsam. Wechseln Sie für bessere Leistung ins lokale Netzwerk.',
|
||||
296: 'Hochladen',
|
||||
295: 'Warnung: Der Upload über Tor ist langsam.',
|
||||
296: 'Auswählen',
|
||||
297: 'Version 1 s9pk erkannt. Dieses Format ist veraltet. Falls nötig, kann ein V1 s9pk über start-cli installiert werden.',
|
||||
298: 'Ungültige Paketdatei',
|
||||
299: 'Fügen Sie ACME-Anbieter hinzu, um SSL-(https)-Zertifikate für den Clearnet-Zugriff zu generieren.',
|
||||
300: 'Anleitung anzeigen',
|
||||
301: 'Gespeicherte Anbieter',
|
||||
302: 'Anbieter hinzufügen',
|
||||
303: 'Kontakt',
|
||||
304: 'Bearbeiten',
|
||||
305: 'ACME-Anbieter hinzufügen',
|
||||
306: 'ACME-Anbieter bearbeiten',
|
||||
305: 'Zertifizierungsstelle hinzufügen',
|
||||
306: 'Kontaktinformationen bearbeiten',
|
||||
307: 'Kontakt-E-Mails',
|
||||
308: 'Erforderlich, um ein Zertifikat von einer Zertifizierungsstelle zu erhalten',
|
||||
309: 'Alle umschalten',
|
||||
310: 'Fertig',
|
||||
311: 'Master-Passwort erforderlich',
|
||||
311: 'Master-passwort erforderlich',
|
||||
312: 'Geben Sie Ihr Master-Passwort ein, um diese Sicherung zu verschlüsseln.',
|
||||
313: 'Master-Passwort',
|
||||
314: 'Master-Passwort eingeben',
|
||||
@@ -327,8 +307,6 @@ export default {
|
||||
329: 'Hostname',
|
||||
330: 'Pfad',
|
||||
331: 'URL',
|
||||
332: 'Netzwerkschnittstelle',
|
||||
333: 'Protokoll',
|
||||
334: 'Modell',
|
||||
335: 'User-Agent',
|
||||
336: 'Plattform',
|
||||
@@ -375,7 +353,6 @@ export default {
|
||||
377: 'StartOS-Sicherungen erkannt',
|
||||
378: 'Keine StartOS-Sicherungen erkannt',
|
||||
379: 'StartOS-Version',
|
||||
380: 'Die Verbindung zu einem externen SMTP-Server ermöglicht es StartOS und seinen Diensten, E-Mails zu senden.',
|
||||
381: 'SMTP-Zugangsdaten',
|
||||
382: 'Test-E-Mail senden',
|
||||
383: 'Senden',
|
||||
@@ -383,7 +360,6 @@ export default {
|
||||
385: 'Eine Test-E-Mail wurde gesendet an',
|
||||
386: 'Prüfen Sie Ihren Spam-Ordner und markieren Sie die Nachricht als „kein Spam“.',
|
||||
387: 'Die Web-Benutzeroberfläche Ihres StartOS-Servers, zugänglich über jeden Browser.',
|
||||
388: 'Ändern Sie Ihr Master-Passwort für StartOS.',
|
||||
389: 'Sie benötigen weiterhin Ihr aktuelles Passwort, um bestehende Sicherungen zu entschlüsseln!',
|
||||
390: 'Neue Passwörter stimmen nicht überein',
|
||||
391: 'Neues Passwort muss mindestens 12 Zeichen lang sein',
|
||||
@@ -393,7 +369,6 @@ export default {
|
||||
395: 'Aktuelles Passwort',
|
||||
396: 'Neues Passwort',
|
||||
397: 'Neues Passwort erneut eingeben',
|
||||
398: 'Eine Sitzung ist ein Gerät, das aktuell bei StartOS angemeldet ist. Beenden Sie Sitzungen, die Sie nicht kennen oder nicht mehr verwenden.',
|
||||
399: 'Aktuelle Sitzung',
|
||||
400: 'Weitere Sitzungen',
|
||||
401: 'Ausgewählte beenden',
|
||||
@@ -500,7 +475,6 @@ export default {
|
||||
502: 'souveränes computing',
|
||||
503: 'Passen Sie den Namen an, der in Ihrem Browser-Tab erscheint',
|
||||
504: 'Verwalten',
|
||||
505: 'Möchten Sie diese Adresse wirklich löschen?',
|
||||
506: '"Weiches Deinstallieren" entfernt den Dienst aus StartOS, behält jedoch die Daten bei.',
|
||||
507: 'Keine gespeicherten Anbieter',
|
||||
508: 'Kiosk-Modus',
|
||||
@@ -514,18 +488,105 @@ export default {
|
||||
516: 'Empfohlen',
|
||||
517: 'Möchten Sie diese Aufgabe wirklich verwerfen?',
|
||||
518: 'Verwerfen',
|
||||
519: 'Um Clearnet-Domains zu veröffentlichen, musst du oben auf „Öffentlich machen“ klicken.',
|
||||
520: 'Update verfügbar',
|
||||
521: 'Um das Problem zu beheben, siehe',
|
||||
522: 'SDK Version',
|
||||
522: 'SDK version',
|
||||
523: 'Sicherungsbericht',
|
||||
524: 'Ausgewählte löschen',
|
||||
525: 'Keine schlüssel',
|
||||
526: 'Öffentlichen SSH-Schlüssel hinzufügen',
|
||||
527: 'Standardmäßig kannst du dich per SSH von jedem Gerät aus mit deinem Server verbinden, indem du dein Master-Passwort verwendest. Optional kannst du SSH-öffentliche Schlüssel hinzufügen, um bestimmten Geräten den Zugriff ohne Passworteingabe zu ermöglichen.',
|
||||
525: 'Keine SSH-Schlüssel',
|
||||
526: 'SSH-Schlüssel hinzufügen',
|
||||
527: 'SSH-Schlüssel',
|
||||
528: 'Quellcode',
|
||||
529: 'Upstream-Dienst',
|
||||
530: 'StartOS-Paket',
|
||||
531: 'Fehler beim Initialisieren des Servers',
|
||||
532: 'Abgeschlossen',
|
||||
533: 'Gateways',
|
||||
535: 'Gateway hinzufügen',
|
||||
536: 'Umbenennen',
|
||||
537: 'Zugriff',
|
||||
538: 'Öffentliche Domains',
|
||||
539: 'Zertifizierungsstellen',
|
||||
540: 'Domain',
|
||||
541: 'Gateway',
|
||||
543: 'Zertifizierungsstelle',
|
||||
544: 'Domain bearbeiten',
|
||||
545: 'Keine öffentlichen Domains',
|
||||
546: 'Anbieter',
|
||||
547: 'DNS anzeigen',
|
||||
548: 'Neue öffentliche Domain',
|
||||
550: 'Adressen',
|
||||
553: 'Keine Adressen',
|
||||
554: 'CA ändern',
|
||||
555: 'Adressdetails',
|
||||
556: 'Private Domains',
|
||||
557: 'Keine privaten Domains',
|
||||
558: 'Neue private Domain',
|
||||
559: 'DNS-Server',
|
||||
560: 'Geben Sie einen vollständig qualifizierten Domainnamen ein. Da die Domain für private Zwecke verwendet wird, kann es jede gewünschte Domain sein, auch eine, die Sie nicht kontrollieren.',
|
||||
561: 'Geben Sie einen vollständig qualifizierten Domainnamen ein. Wenn Sie beispielsweise domain.com kontrollieren, könnten Sie domain.com oder subdomain.domain.com oder another.subdomain.domain.com eingeben.',
|
||||
562: 'DNS-Einträge',
|
||||
563: 'Erstellen Sie einen der unten aufgeführten DNS-Einträge.',
|
||||
564: 'Kein DNS-Eintrag erkannt für',
|
||||
565: 'Ungültiger DNS-Eintrag',
|
||||
566: 'löst auf in',
|
||||
567: 'DNS-Eintrag erkannt!',
|
||||
568: 'Wählen Sie ein Gateway für diese Domain aus.',
|
||||
569: 'Wählen Sie eine Zertifizierungsstelle aus, um SSL/TLS-Zertifikate für diese Domain auszustellen.',
|
||||
570: 'Andere',
|
||||
571: 'Ein Name zur einfachen Identifizierung des Gateways',
|
||||
572: 'Wählen Sie diese Option, wenn das Gateway für den privaten Zugriff nur für autorisierte Clients konfiguriert ist. StartTunnel ist ein privates Gateway.',
|
||||
573: 'Wählen Sie diese Option, wenn das Gateway für uneingeschränkten öffentlichen Zugriff konfiguriert ist.',
|
||||
574: 'Datei',
|
||||
575: 'Wireguard-Konfigurationsdatei',
|
||||
576: 'Kopieren/Einfügen',
|
||||
577: 'Dateiinhalt',
|
||||
578: 'Öffentlicher Schlüssel',
|
||||
579: 'muss ein gültiger SSH-Öffentlicher Schlüssel sein',
|
||||
580: 'Aktualisierung erforderlich',
|
||||
581: 'Ihre Benutzeroberfläche ist zwischengespeichert und veraltet. Versuchen Sie, die PWA mit der Schaltfläche unten neu zu laden. Wenn Sie diese Nachricht weiterhin sehen, deinstallieren und installieren Sie die PWA erneut.',
|
||||
582: 'Ihre Benutzeroberfläche ist zwischengespeichert und veraltet. Führen Sie einen Hard-Refresh der Seite durch, um die neueste Benutzeroberfläche zu erhalten.',
|
||||
583: 'Erfordert Vertrauen in die Root-CA Ihres Servers',
|
||||
584: 'Verbindungen können manchmal langsam oder unzuverlässig sein',
|
||||
585: 'Öffentlich, wenn Sie die Adresse öffentlich teilen, andernfalls privat',
|
||||
586: 'Erfordert ein Tor-fähiges Gerät oder einen Browser',
|
||||
587: 'Nur nützlich für Clients, die HTTPS erzwingen',
|
||||
588: 'Ideal für anonyme, zensurresistente Bereitstellung und Fernzugriff',
|
||||
589: 'Ideal für lokalen Zugriff',
|
||||
590: 'Erfordert die Verbindung mit demselben lokalen Netzwerk (LAN) wie Ihr Server, entweder physisch oder über VPN',
|
||||
591: 'Erfordert die Einstellung einer statischen IP-Adresse für',
|
||||
592: 'Ideal für VPN-Zugriff über',
|
||||
593: 'in Ihrem Gateway',
|
||||
594: 'der Wireguard-Server Ihres Routers',
|
||||
595: 'Erfordert Portweiterleitung im Gateway',
|
||||
596: 'Erfordert einen DNS-Eintrag für',
|
||||
597: 'der sich auflöst zu',
|
||||
598: 'Nicht empfohlen für VPN-Zugriff. VPNs unterstützen keine „.local“-Domains ohne erweiterte Konfiguration',
|
||||
599: 'Kann für Clearnet-Zugriff verwendet werden',
|
||||
600: 'In den meisten Fällen nicht empfohlen. Öffentliche Domains werden bevorzugt',
|
||||
601: 'Lokal',
|
||||
602: 'Kann für lokalen Zugriff verwendet werden',
|
||||
603: 'Ideal für öffentlichen Zugriff über das Internet',
|
||||
604: 'Kann für persönlichen Zugriff über das öffentliche Internet verwendet werden. VPN ist privater und sicherer',
|
||||
605: 'wenn die Verwendung von IP-Adressen und Ports unerwünscht ist',
|
||||
606: 'Host',
|
||||
607: 'Wert',
|
||||
608: 'Zweck',
|
||||
609: 'alle Subdomains von',
|
||||
610: 'Dynamisches DNS',
|
||||
611: 'Keine Service-Schnittstellen',
|
||||
612: 'Grund',
|
||||
613: 'Private Gateways für die StartOS-Benutzeroberfläche können nicht deaktiviert werden',
|
||||
614: 'CA-Fingerabdruck',
|
||||
615: 'DHCP-Server',
|
||||
616: 'DHCP-Server können nicht bearbeitet werden',
|
||||
617: 'Statisch',
|
||||
618: 'Statische Server',
|
||||
619: 'Warnung. StartOS verwendet derzeit das folgende Gateway für DNS',
|
||||
620: 'Wenn Sie dieses Gateway für die Auflösung privater Domains verwenden möchten, legen Sie alternative statische DNS-Server mit dem obigen Formular fest.',
|
||||
621: 'Einen Dienst paketieren',
|
||||
622: 'Veröffentlicht',
|
||||
623: 'Alternative Implementierungen',
|
||||
624: 'Versionen',
|
||||
625: 'Eine andere Version auswählen',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
export const ENGLISH = {
|
||||
'Change': 1, // verb
|
||||
'Update': 2, // verb
|
||||
'Reset': 3, // verb
|
||||
'System': 4, // as in, system preferences
|
||||
'General': 5, // as in, general settings
|
||||
'Email': 6,
|
||||
@@ -14,8 +13,7 @@ export const ENGLISH = {
|
||||
'Active Sessions': 12,
|
||||
'Change Password': 13,
|
||||
'General Settings': 14,
|
||||
'Manage your overall setup and preferences': 15,
|
||||
'Browser Tab Title': 16,
|
||||
'Browser tab title': 16,
|
||||
'Language': 17,
|
||||
'Disk Repair': 18,
|
||||
'Attempt automatic repair': 19,
|
||||
@@ -23,7 +21,7 @@ export const ENGLISH = {
|
||||
'Root Certificate Authority': 21,
|
||||
'Download your Root CA': 22,
|
||||
'Download': 23,
|
||||
'Reset Tor': 24,
|
||||
'Restart Tor': 24,
|
||||
'Restart the Tor daemon on your server': 25,
|
||||
'Software Update': 26,
|
||||
'Restart to apply': 27,
|
||||
@@ -38,8 +36,8 @@ export const ENGLISH = {
|
||||
'Cancel': 36,
|
||||
'This action should only be executed if directed by a Start9 support specialist. We recommend backing up your device before preforming this action. If anything happens to the device during the reboot, such as losing power or unplugging the drive, the filesystem will be in an unrecoverable state. Please proceed with caution.': 37,
|
||||
'Delete': 38,
|
||||
'Tor reset in progress': 39,
|
||||
'Resetting Tor': 40,
|
||||
'Tor restart in progress': 39,
|
||||
'Restarting Tor': 40,
|
||||
'Checking for updates': 41,
|
||||
'Beginning restart': 42,
|
||||
'You are on the latest version of StartOS.': 43,
|
||||
@@ -47,8 +45,7 @@ export const ENGLISH = {
|
||||
'Release notes': 45,
|
||||
'Begin Update': 46,
|
||||
'Beginning update': 47,
|
||||
'You are currently connected over Tor. If you reset the Tor daemon, you will lose connectivity until it comes back online.': 48,
|
||||
'Reset Tor?': 49,
|
||||
'You are currently connected over Tor. If you restart the Tor daemon, you will lose connectivity until it comes back online.': 48,
|
||||
'Optionally wipe state to forcibly acquire new guard nodes. It is recommended to try without wiping state first.': 50,
|
||||
'Wipe state': 51,
|
||||
'Saving high score': 52,
|
||||
@@ -90,30 +87,18 @@ export const ENGLISH = {
|
||||
'Actions': 88, // as in, actions available to the user
|
||||
'not recommended': 89,
|
||||
'Root CA Trusted!': 90,
|
||||
'Add a clearnet address to expose this interface on the Internet. Clearnet addresses are fully public and not anonymous.': 91,
|
||||
'Learn more': 92,
|
||||
'Make public': 93,
|
||||
'Make private': 94,
|
||||
'No public addresses': 95,
|
||||
'Add domain': 96,
|
||||
'Add public domain': 96,
|
||||
'Removing': 97,
|
||||
'Making public': 98,
|
||||
'Making private': 99,
|
||||
'Unsaved changes': 100,
|
||||
'You have unsaved changes. Are you sure you want to leave?': 101,
|
||||
'Leave': 102,
|
||||
'Are you sure?': 103,
|
||||
'Select Domain': 104,
|
||||
'Local': 105,
|
||||
'Local addresses can only be accessed by devices connected to the same LAN as your server, either directly or using a VPN.': 106,
|
||||
'Learn More': 107,
|
||||
'Public': 108,
|
||||
'Private': 109,
|
||||
'Add an onion address to anonymously expose this interface on the darknet. Onion addresses can only be reached over the Tor network.': 110,
|
||||
'No onion addresses': 111,
|
||||
'New Onion Address': 112,
|
||||
'public': 108,
|
||||
'private': 109,
|
||||
'No Tor domains': 111,
|
||||
'New Tor domain': 112,
|
||||
'Private Key (optional)': 113,
|
||||
'Optionally provide a base64-encoded ed25519 private key for generating the Tor V3 (.onion) address. If not provided, a random key will be generated and used.': 114,
|
||||
'Optionally provide a base64-encoded ed25519 private key for generating the Tor V3 (.onion) domain. If not provided, a random key will be generated.': 114,
|
||||
'Processing 10,000 logs': 115,
|
||||
'Loading older logs': 116,
|
||||
'Waiting for network connectivity': 117,
|
||||
@@ -139,7 +124,6 @@ export const ENGLISH = {
|
||||
'Tor Logs': 137,
|
||||
'Raw, unfiltered operating system logs': 138,
|
||||
'Diagnostics for drivers and other kernel processes': 139,
|
||||
'Diagnostic logs for the Tor daemon on StartOS': 140,
|
||||
'Downgrade': 141,
|
||||
'Reinstall': 142,
|
||||
'Installed': 143,
|
||||
@@ -178,7 +162,6 @@ export const ENGLISH = {
|
||||
'Kernel space': 177,
|
||||
'Idle': 178, // a CPU metric
|
||||
'I/O wait': 179,
|
||||
'ACME': 180,
|
||||
'Total': 181,
|
||||
'Used': 182,
|
||||
'Available': 183,
|
||||
@@ -241,7 +224,7 @@ export const ENGLISH = {
|
||||
'Name': 240,
|
||||
'Status': 241,
|
||||
'Open': 242, // verb
|
||||
'Interfaces': 243, // as in user interface or application program interface
|
||||
'Service Interfaces': 243, // as in, a UI or API for an application
|
||||
'Hosting': 244,
|
||||
'Installing': 245,
|
||||
'See below': 246,
|
||||
@@ -270,9 +253,9 @@ export const ENGLISH = {
|
||||
'unknown %': 270,
|
||||
'Not provided': 271,
|
||||
'Links': 272,
|
||||
'Git Hash': 273,
|
||||
'Git hash': 273,
|
||||
'License': 274,
|
||||
'Installed From': 275,
|
||||
'Installed from': 275,
|
||||
'Marketing': 278,
|
||||
'Support': 279,
|
||||
'Donations': 280,
|
||||
@@ -289,27 +272,24 @@ export const ENGLISH = {
|
||||
'Starting upload': 292,
|
||||
'Try again': 293,
|
||||
'Upload .s9pk package file': 294,
|
||||
'Warning: package upload will be slow over Tor. Switch to local for a better experience.': 295,
|
||||
'Upload': 296,
|
||||
'Warning: package upload will be slow over Tor.': 295,
|
||||
'Select': 296,
|
||||
'Version 1 s9pk detected. This package format is deprecated. You can sideload a V1 s9pk via start-cli if necessary.': 297,
|
||||
'Invalid package file': 298,
|
||||
'Add ACME providers in order to generate SSL (https) certificates for clearnet access.': 299,
|
||||
'View instructions': 300,
|
||||
'Saved Providers': 301, // as in, ACME service provider, such as Let's Encrypt
|
||||
'Add Provider': 302,
|
||||
'Contact': 303, // as in, "contact us"
|
||||
'Edit': 304,
|
||||
'Add ACME Provider': 305,
|
||||
'Edit ACME Provider': 306,
|
||||
'Add Certificate Authority': 305,
|
||||
'Edit contact info': 306,
|
||||
'Contact Emails': 307,
|
||||
'Needed to obtain a certificate from a Certificate Authority': 308,
|
||||
'Toggle all': 309,
|
||||
'Done': 310,
|
||||
'Master Password Needed': 311,
|
||||
'Master password needed': 311,
|
||||
'Enter your master password to encrypt this backup.': 312,
|
||||
'Master Password': 313,
|
||||
'Enter master password': 314,
|
||||
'Original Password Needed': 315,
|
||||
'Original password needed': 315,
|
||||
'This backup was created with a different password. Enter the original password that was used to encrypt this backup.': 316,
|
||||
'Original Password': 317,
|
||||
'Enter original password': 318,
|
||||
@@ -326,8 +306,6 @@ export const ENGLISH = {
|
||||
'Hostname': 329,
|
||||
'Path': 330, // as in, a URL path
|
||||
'URL': 331,
|
||||
'Network Interface': 332,
|
||||
'Protocol': 333, // as in, http protocol
|
||||
'Model': 334, // as in, a product model
|
||||
'User Agent': 335,
|
||||
'Platform': 336, // as in, OS platform, such as iOS, Android, Linux, etc
|
||||
@@ -366,7 +344,7 @@ export const ENGLISH = {
|
||||
'Ready to restore': 369,
|
||||
'Local Hostname': 370,
|
||||
'Created': 371,
|
||||
'Password Required': 372,
|
||||
'Password required': 372,
|
||||
'Enter the master password that was used to encrypt this backup. On the next screen, you will select the individual services you want to restore.': 373,
|
||||
'Decrypting drive': 374,
|
||||
'Select services to restore': 375,
|
||||
@@ -374,7 +352,6 @@ export const ENGLISH = {
|
||||
'StartOS backups detected': 377,
|
||||
'No StartOS backups detected': 378,
|
||||
'StartOS Version': 379,
|
||||
'Connecting an external SMTP server allows StartOS and your installed services to send you emails.': 380,
|
||||
'SMTP Credentials': 381,
|
||||
'Send test email': 382,
|
||||
'Send': 383,
|
||||
@@ -382,7 +359,6 @@ export const ENGLISH = {
|
||||
'A test email has been sent to': 385,
|
||||
'Check your spam folder and mark as not spam.': 386,
|
||||
'The web user interface for your StartOS server, accessible from any browser.': 387,
|
||||
'Change your StartOS master password.': 388,
|
||||
'You will still need your current password to decrypt existing backups!': 389,
|
||||
'New passwords do not match': 390,
|
||||
'New password must be 12 characters or greater': 391,
|
||||
@@ -392,13 +368,12 @@ export const ENGLISH = {
|
||||
'Current Password': 395,
|
||||
'New Password': 396,
|
||||
'Retype New Password': 397,
|
||||
'A session is a device that is currently logged into StartOS. For best security, terminate sessions you do not recognize or no longer use.': 398,
|
||||
'Current session': 399,
|
||||
'Other sessions': 400,
|
||||
'Terminate selected': 401,
|
||||
'Terminating sessions': 402,
|
||||
'No sessions': 403,
|
||||
'Password Needed': 404,
|
||||
'Password needed': 404,
|
||||
'Connected': 405,
|
||||
'Forget': 406, // as in, delete or remove
|
||||
'WiFi Credentials': 407,
|
||||
@@ -499,7 +474,6 @@ export const ENGLISH = {
|
||||
'sovereign computing': 502,
|
||||
'Customize the name appearing in your browser tab': 503,
|
||||
'Manage': 504, // as in, administer
|
||||
'Are you sure you want to delete this address?': 505, // this address referes to a domain or URL
|
||||
'"Soft uninstall" will remove the service from StartOS but preserve its data.': 506,
|
||||
'No saved providers': 507,
|
||||
'Kiosk Mode': 508, // an OS mode that permits attaching a monitor to the computer
|
||||
@@ -513,18 +487,105 @@ export const ENGLISH = {
|
||||
'Recommended': 516, // as in, we recommend this
|
||||
'Are you sure you want to dismiss this task?': 517,
|
||||
'Dismiss': 518, // as in, dismiss or delete a task
|
||||
'To publish clearnet domains, you must click "Make Public", above.': 519,
|
||||
'Update available': 520,
|
||||
'To resolve the issue, refer to': 521,
|
||||
'SDK Version': 522,
|
||||
'SDK version': 522,
|
||||
'Backup Report': 523,
|
||||
'Delete selected': 524,
|
||||
'No keys': 525,
|
||||
'Add SSH Public Key': 526,
|
||||
'By default, you can SSH into your server from any device using your master password. Optionally add SSH public keys to grant specific devices access without needing to enter a password.': 527,
|
||||
'No SSH keys': 525,
|
||||
'Add SSH key': 526,
|
||||
'SSH Keys': 527,
|
||||
'Source Code': 528,
|
||||
'Upstream service': 529, // as in, the URL of the source code for the original software
|
||||
'StartOS package': 530, // as in, the URL of the source code for the StartOS package
|
||||
'Error initializing server': 531,
|
||||
'Finished': 532, // an in, complete
|
||||
'Gateways': 533, // as in, a device or software that connects two different networks
|
||||
'Add gateway': 535, // as in, add a new network gateway to StartOS
|
||||
'Rename': 536,
|
||||
'Access': 537, // as in, public or private access, almost "permission"
|
||||
'Public Domains': 538, // as in, internet domains
|
||||
'Certificate Authorities': 539,
|
||||
'Domain': 540, // as in, an internat domain name
|
||||
'Gateway': 541, // as in, a device or software that connects two different networks
|
||||
'Certificate Authority': 543,
|
||||
'Edit public domain': 544,
|
||||
'No public domains': 545,
|
||||
'Provider': 546,
|
||||
'View DNS': 547,
|
||||
'New public domain': 548,
|
||||
'Addresses': 550,
|
||||
'No addresses': 553,
|
||||
'Change CA': 554,
|
||||
'Address details': 555,
|
||||
'Private Domains': 556,
|
||||
'No private domains': 557,
|
||||
'New private domain': 558,
|
||||
'DNS Servers': 559,
|
||||
'Enter a fully qualified domain name. Since the domain is for private use, it can be any domain you want, even one you do not control.': 560,
|
||||
'Enter a fully qualified domain name. For example, if you control domain.com, you could enter domain.com or subdomain.domain.com or another.subdomain.domain.com.': 561,
|
||||
'DNS Records': 562,
|
||||
'Create one of the DNS records below.': 563,
|
||||
'No DNS record detected for': 564, // this is a partial sentence. A domain name will be added after "for" to complete the sentence.
|
||||
'Invalid DNS record': 565,
|
||||
'resolves to': 566, // as in "domain.com 'resolves to' [IP address]"
|
||||
'DNS record detected!': 567,
|
||||
'Select a gateway to use for this domain.': 568,
|
||||
'Select a Certificate Authority to issue SSL/TLS certificates for this domain': 569,
|
||||
'Other': 570, // as in, a list option to indicate none of the options listed
|
||||
'A name to easily identify the gateway': 571,
|
||||
'select this option if the gateway is configured for private access to authorized clients only. StartTunnel is a private gateway.': 572,
|
||||
'select this option if the gateway is configured for unfettered public access.': 573,
|
||||
'File': 574, // as in, a computer file
|
||||
'Wireguard Config File': 575,
|
||||
'Copy/Paste': 576,
|
||||
'File Contents': 577,
|
||||
'Public Key': 578, // as in, a cryptographic public key
|
||||
'must be a valid SSH public key': 579,
|
||||
'Refresh Needed': 580,
|
||||
'Your user interface is cached and out of date. Attempt to reload the PWA using the button below. If you continue to see this message, uninstall and reinstall the PWA.': 581,
|
||||
'Your user interface is cached and out of date. Hard refresh the page to get the latest UI.': 582,
|
||||
"Requires trusting your server's Root CA": 583,
|
||||
'Connections can be slow or unreliable at times': 584,
|
||||
'Public if you share the address publicly, otherwise private': 585,
|
||||
'Requires using a Tor-enabled device or browser': 586,
|
||||
'Only useful for clients that enforce HTTPS': 587,
|
||||
'Ideal for anonymous, censorship-resistant hosting and remote access': 588,
|
||||
'Ideal for local access': 589,
|
||||
'Requires being connected to the same Local Area Network (LAN) as your server, either physically or via VPN': 590,
|
||||
'Requires setting a static IP address for': 591, // this is a partial sentence. An IP address will be added after "for" to complete the sentence.
|
||||
'Ideal for VPN access via': 592, // this is a partial sentence. A connection medium will be added after "via" to complete the sentence.
|
||||
'in your gateway': 593, // this is a partial sentence. It is preceded by an instruction: e.g. "do something" in your gateway. Gateway refers to a router or VPN server.
|
||||
"your router's Wireguard server": 594, // this is a partial sentence. It is preceded by "ideal for access via"
|
||||
'Requires port forwarding in gateway': 595,
|
||||
'Requires a DNS record for': 596, // this is a partial sentence. A domain name will be added after "for" to complete the sentence.
|
||||
'that resolves to': 597, // this is a partial sentence. It is preceded by "requires a DNS record for [domain] "
|
||||
'Not recommended for VPN access. VPNs do not support ".local" domains without advanced configuration': 598,
|
||||
'Can be used for clearnet access': 599,
|
||||
'Not recommended in most cases. Public domains are preferred': 600,
|
||||
'Local': 601, // as in, not remote
|
||||
'Can be used for local access': 602,
|
||||
'Ideal for public access via the Internet': 603,
|
||||
'Can be used for personal access via the public Internet. VPN is more private and secure': 604,
|
||||
'when using IP addresses and ports is undesirable': 605, // this is a partial sentence. It is preceded by "Good for connections "
|
||||
'Host': 606, // as in, a network host
|
||||
'Value': 607, // as in, the value in a column of a table
|
||||
'Purpose': 608, // as in, the reason for a thing to exist
|
||||
'all subdomains of': 609, // this is a partial sentence. A domain name will be added after "of" to complete the sentence.
|
||||
'Dynamic DNS': 610,
|
||||
'No service interfaces': 611, // as in, there are no available interfaces (API, UI, etc) for this software application
|
||||
'Reason': 612, // as in, an explanation for something
|
||||
'Cannot disable private gateways for StartOS UI': 613,
|
||||
'CA fingerprint': 614, // as in, the unique, fixed-length digital identifier generated from a certificate's data using a cryptographic hash function
|
||||
'DHCP Servers': 615,
|
||||
'Cannot edit DHCP servers': 616,
|
||||
'Static': 617, // as in, unchanging
|
||||
'Static Servers': 618, // as in, servers that do not change
|
||||
'Warning. StartOS is currently using the following gateway for DNS': 619,
|
||||
'If you intend to use this gateway for private domain resolution, set alternative static DNS servers using the form above.': 620,
|
||||
'Package a service': 621, // as in, package a software application for an operating system
|
||||
'Released': 622, // as in, the date something became available
|
||||
'Alternative Implementations': 623,
|
||||
'Versions': 624,
|
||||
'Select another version': 625,
|
||||
} as const
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { i18n } from '../i18n.providers'
|
||||
export default {
|
||||
1: 'Cambiar',
|
||||
2: 'Actualizar',
|
||||
3: 'Restablecer',
|
||||
4: 'Sistema',
|
||||
5: 'General',
|
||||
6: 'Correo electrónico',
|
||||
@@ -15,7 +14,6 @@ export default {
|
||||
12: 'Sesiones activas',
|
||||
13: 'Cambiar contraseña',
|
||||
14: 'Configuración general',
|
||||
15: 'Administra tu configuración y preferencias generales',
|
||||
16: 'Título de la pestaña del navegador',
|
||||
17: 'Idioma',
|
||||
18: 'Reparación de disco',
|
||||
@@ -24,7 +22,7 @@ export default {
|
||||
21: 'Autoridad certificadora raíz',
|
||||
22: 'Descargar tu CA raíz',
|
||||
23: 'Descargar',
|
||||
24: 'Restablecer Tor',
|
||||
24: 'Reiniciar Tor',
|
||||
25: 'Reiniciar el servicio Tor en tu servidor',
|
||||
26: 'Actualización de software',
|
||||
27: 'Reiniciar para aplicar',
|
||||
@@ -39,8 +37,8 @@ export default {
|
||||
36: 'Cancelar',
|
||||
37: 'Esta acción solo debe realizarse si es indicada por un especialista de soporte de Start9. Recomendamos hacer una copia de seguridad del dispositivo antes de ejecutar esta acción. Si ocurre algo durante el reinicio, como un corte de energía o desconectar el disco, el sistema de archivos quedará en un estado irrecuperable. Procede con precaución.',
|
||||
38: 'Eliminar',
|
||||
39: 'Restablecimiento de Tor en curso',
|
||||
40: 'Restableciendo Tor',
|
||||
39: 'Reinicio de Tor en progreso',
|
||||
40: 'Reiniciando Tor',
|
||||
41: 'Buscando actualizaciones',
|
||||
42: 'Iniciando reinicio',
|
||||
43: 'Estás usando la última versión de StartOS.',
|
||||
@@ -48,8 +46,7 @@ export default {
|
||||
45: 'notas de la versión',
|
||||
46: 'Iniciar actualización',
|
||||
47: 'Iniciando actualización',
|
||||
48: 'Actualmente estás conectado a través de Tor. Si restableces el servicio Tor, perderás la conexión hasta que vuelva a estar en línea.',
|
||||
49: '¿Restablecer Tor?',
|
||||
48: 'Actualmente estás conectado a través de Tor. Si reinicias el demonio de Tor, perderás la conectividad hasta que vuelva a estar en línea.',
|
||||
50: 'Opcionalmente borra el estado para forzar la adquisición de nuevos nodos de entrada. Se recomienda intentar sin borrar el estado primero.',
|
||||
51: 'Borrar estado',
|
||||
52: 'Guardando puntuación máxima',
|
||||
@@ -91,30 +88,18 @@ export default {
|
||||
88: 'Acciones',
|
||||
89: 'no recomendado',
|
||||
90: '¡CA raíz confiable!',
|
||||
91: 'Agrega una dirección clearnet para exponer esta interfaz en Internet. Las direcciones clearnet son totalmente públicas y no anónimas.',
|
||||
92: 'Saber más',
|
||||
93: 'Hacer público',
|
||||
94: 'Hacer privado',
|
||||
95: 'Sin direcciones públicas',
|
||||
96: 'Agregar dominio',
|
||||
96: 'Agregar dominio público',
|
||||
97: 'Eliminando',
|
||||
98: 'Haciendo público',
|
||||
99: 'Haciendo privado',
|
||||
100: 'Cambios no guardados',
|
||||
101: 'Tienes cambios no guardados. ¿Estás seguro de que deseas salir?',
|
||||
102: 'Salir',
|
||||
103: '¿Estás seguro?',
|
||||
104: 'Seleccionar dominio',
|
||||
105: 'Local',
|
||||
106: 'Las direcciones locales solo pueden ser accedidas por dispositivos conectados a la misma red local que tu servidor, ya sea directamente o mediante una VPN.',
|
||||
107: 'Más información',
|
||||
108: 'Público',
|
||||
109: 'Privado',
|
||||
110: 'Agrega una dirección onion para exponer esta interfaz de forma anónima en la darknet. Las direcciones onion solo se pueden acceder a través de la red Tor.',
|
||||
111: 'Sin direcciones onion',
|
||||
112: 'Nueva dirección Onion',
|
||||
108: 'público',
|
||||
109: 'privado',
|
||||
111: 'Sin dominios onion',
|
||||
112: 'Nueva dominio onion',
|
||||
113: 'Clave privada (opcional)',
|
||||
114: 'Opcionalmente proporciona una clave privada ed25519 codificada en base64 para generar la dirección Tor V3 (.onion). Si no se proporciona, se generará una clave aleatoria.',
|
||||
114: 'Opcionalmente proporciona una clave privada ed25519 codificada en base64 para generar el dominio Tor V3 (.onion). Si no se proporciona, se generará una clave aleatoria.',
|
||||
115: 'Procesando 10,000 registros',
|
||||
116: 'Cargando registros anteriores',
|
||||
117: 'Esperando conectividad de red',
|
||||
@@ -140,7 +125,6 @@ export default {
|
||||
137: 'Registros de Tor',
|
||||
138: 'Registros sin filtrar del sistema operativo',
|
||||
139: 'Diagnóstico de controladores y otros procesos del kernel',
|
||||
140: 'Registros de diagnóstico del servicio Tor en StartOS',
|
||||
141: 'Retroceder versión',
|
||||
142: 'Reinstalar',
|
||||
143: 'Instalados',
|
||||
@@ -179,7 +163,6 @@ export default {
|
||||
177: 'Espacio del kernel',
|
||||
178: 'Inactivo',
|
||||
179: 'Espera de E/S',
|
||||
180: 'ACME',
|
||||
181: 'Total',
|
||||
182: 'Usado',
|
||||
183: 'Disponible',
|
||||
@@ -242,7 +225,7 @@ export default {
|
||||
240: 'Nombre',
|
||||
241: 'Estado',
|
||||
242: 'Abrir',
|
||||
243: 'Interfaces',
|
||||
243: 'Interfaces de servicio',
|
||||
244: 'Alojamiento',
|
||||
245: 'Instalando',
|
||||
246: 'Ver abajo',
|
||||
@@ -290,18 +273,15 @@ export default {
|
||||
292: 'Iniciando carga',
|
||||
293: 'Intentar de nuevo',
|
||||
294: 'Subir archivo de paquete .s9pk',
|
||||
295: 'Advertencia: la carga del paquete será lenta a través de Tor. Cambia a conexión local para una mejor experiencia.',
|
||||
296: 'Subir',
|
||||
295: 'Advertencia: la carga del paquete será lenta a través de Tor.',
|
||||
296: 'Seleccionar',
|
||||
297: 'Se detectó un paquete s9pk de versión 1. Este formato está obsoleto. Puedes instalarlo manualmente con start-cli si es necesario.',
|
||||
298: 'Archivo de paquete inválido',
|
||||
299: 'Agrega proveedores ACME para generar certificados SSL (https) para el acceso desde clearnet.',
|
||||
300: 'Ver instrucciones',
|
||||
301: 'Proveedores guardados',
|
||||
302: 'Agregar proveedor',
|
||||
303: 'Contacto',
|
||||
304: 'Editar',
|
||||
305: 'Agregar proveedor ACME',
|
||||
306: 'Editar proveedor ACME',
|
||||
305: 'Agregar autoridad certificadora',
|
||||
306: 'Editar información de contacto',
|
||||
307: 'Correos de contacto',
|
||||
308: 'Necesarios para obtener un certificado de una Autoridad Certificadora',
|
||||
309: 'Alternar todo',
|
||||
@@ -327,8 +307,6 @@ export default {
|
||||
329: 'Nombre del host',
|
||||
330: 'Ruta',
|
||||
331: 'URL',
|
||||
332: 'Interfaz de red',
|
||||
333: 'Protocolo',
|
||||
334: 'Modelo',
|
||||
335: 'Agente de usuario',
|
||||
336: 'Plataforma',
|
||||
@@ -375,7 +353,6 @@ export default {
|
||||
377: 'Copias de seguridad de StartOS detectadas',
|
||||
378: 'No se detectaron copias de seguridad de StartOS',
|
||||
379: 'Versión de StartOS',
|
||||
380: 'Conectar un servidor SMTP externo permite que StartOS y tus servicios instalados te envíen correos electrónicos.',
|
||||
381: 'Credenciales SMTP',
|
||||
382: 'Enviar correo de prueba',
|
||||
383: 'Enviar',
|
||||
@@ -383,7 +360,6 @@ export default {
|
||||
385: 'Se ha enviado un correo de prueba a',
|
||||
386: 'Revisa tu carpeta de spam y márcalo como no spam.',
|
||||
387: 'La interfaz web de tu servidor StartOS, accesible desde cualquier navegador.',
|
||||
388: 'Cambia tu contraseña maestra de StartOS.',
|
||||
389: '¡Aún necesitarás tu contraseña actual para descifrar copias de seguridad existentes!',
|
||||
390: 'Las nuevas contraseñas no coinciden',
|
||||
391: 'La nueva contraseña debe tener al menos 12 caracteres',
|
||||
@@ -393,7 +369,6 @@ export default {
|
||||
395: 'Contraseña actual',
|
||||
396: 'Nueva contraseña',
|
||||
397: 'Reingresa nueva contraseña',
|
||||
398: 'Una sesión es un dispositivo que actualmente ha iniciado sesión en StartOS. Para mayor seguridad, cierra las sesiones que no reconozcas o que ya no uses.',
|
||||
399: 'Sesión actual',
|
||||
400: 'Otras sesiones',
|
||||
401: 'Terminar seleccionados',
|
||||
@@ -500,7 +475,6 @@ export default {
|
||||
502: 'computación soberana',
|
||||
503: 'Personaliza el nombre que aparece en la pestaña de tu navegador',
|
||||
504: 'Administrar',
|
||||
505: '¿Estás seguro de que deseas eliminar esta dirección?',
|
||||
506: '"Desinstalación suave" eliminará el servicio de StartOS pero conservará sus datos.',
|
||||
507: 'No hay proveedores guardados',
|
||||
508: 'Modo quiosco',
|
||||
@@ -514,18 +488,105 @@ export default {
|
||||
516: 'Recomendado',
|
||||
517: '¿Estás seguro de que deseas descartar esta tarea?',
|
||||
518: 'Descartar',
|
||||
519: 'Para publicar dominios en clearnet, debes hacer clic en "Hacer público" arriba.',
|
||||
520: 'Actualización disponible',
|
||||
521: 'Para resolver el problema, consulta',
|
||||
522: 'Versión de SDK',
|
||||
523: 'Informe de respaldo',
|
||||
524: 'Eliminar seleccionado',
|
||||
525: 'Sin llaves',
|
||||
526: 'Agregar clave pública SSH',
|
||||
527: 'De forma predeterminada, puedes conectarte por SSH a tu servidor desde cualquier dispositivo usando tu contraseña maestra. Opcionalmente, añade claves públicas SSH para otorgar acceso a dispositivos específicos sin necesidad de ingresar una contraseña.',
|
||||
525: 'Sin claves SSH',
|
||||
526: 'Agregar clave SSH',
|
||||
527: 'Claves SSH',
|
||||
528: 'Código fuente',
|
||||
529: 'Servicio original',
|
||||
530: 'Paquete StartOS',
|
||||
531: 'Error al inicializar el servidor',
|
||||
532: 'Finalizado',
|
||||
533: 'Puertas de enlace',
|
||||
535: 'Agregar puerta de enlace',
|
||||
536: 'Renombrar',
|
||||
537: 'Acceso',
|
||||
538: 'Dominios públicos',
|
||||
539: 'Autoridades certificadoras',
|
||||
540: 'Dominio',
|
||||
541: 'Puerta de enlace',
|
||||
543: 'Autoridad certificadora',
|
||||
544: 'Editar dominio',
|
||||
545: 'Sin dominios públicos',
|
||||
546: 'Proveedor',
|
||||
547: 'Ver DNS',
|
||||
548: 'Nuevo dominio público',
|
||||
550: 'Direcciones',
|
||||
553: 'Sin direcciones',
|
||||
554: 'Cambiar CA',
|
||||
555: 'Detalles de dirección',
|
||||
556: 'Dominios privados',
|
||||
557: 'Sin dominios privados',
|
||||
558: 'Nuevo dominio privado',
|
||||
559: 'Servidores DNS',
|
||||
560: 'Introduce un nombre de dominio completo. Dado que el dominio es para uso privado, puede ser cualquier dominio que desees, incluso uno que no controles.',
|
||||
561: 'Introduce un nombre de dominio completo. Por ejemplo, si controlas domain.com, podrías introducir domain.com o subdomain.domain.com o another.subdomain.domain.com.',
|
||||
562: 'Registros DNS',
|
||||
563: 'Crea uno de los registros DNS a continuación.',
|
||||
564: 'No se detectó ningún registro DNS para',
|
||||
565: 'Registro DNS inválido',
|
||||
566: 'se resuelve en',
|
||||
567: '¡Registro DNS detectado!',
|
||||
568: 'Selecciona una puerta de enlace para usar con este dominio.',
|
||||
569: 'Selecciona una Autoridad Certificadora para emitir certificados SSL/TLS para este dominio.',
|
||||
570: 'Otro',
|
||||
571: 'Un nombre para identificar fácilmente la puerta de enlace',
|
||||
572: 'Selecciona esta opción si la puerta de enlace está configurada para acceso privado solo a clientes autorizados. StartTunnel es una puerta de enlace privada.',
|
||||
573: 'Selecciona esta opción si la puerta de enlace está configurada para acceso público sin restricciones.',
|
||||
574: 'Archivo',
|
||||
575: 'Archivo de configuración de Wireguard',
|
||||
576: 'Copiar/Pegar',
|
||||
577: 'Contenido del archivo',
|
||||
578: 'Clave pública',
|
||||
579: 'debe ser una clave pública SSH válida',
|
||||
580: 'Actualización necesaria',
|
||||
581: 'Tu interfaz de usuario está en caché y desactualizada. Intenta recargar la PWA usando el botón de abajo. Si sigues viendo este mensaje, desinstala y vuelve a instalar la PWA.',
|
||||
582: 'Tu interfaz de usuario está en caché y desactualizada. Haz un hard refresh de la página para obtener la última interfaz.',
|
||||
583: 'Requiere confiar en la CA raíz de tu servidor',
|
||||
584: 'Las conexiones pueden ser lentas o poco confiables a veces',
|
||||
585: 'Público si compartes la dirección públicamente, de lo contrario privado',
|
||||
586: 'Requiere un dispositivo o navegador habilitado para Tor',
|
||||
587: 'Solo útil para clientes que imponen HTTPS',
|
||||
588: 'Ideal para alojamiento y acceso remoto anónimo y resistente a la censura',
|
||||
589: 'Ideal para acceso local',
|
||||
590: 'Requiere estar conectado a la misma red de área local (LAN) que tu servidor, ya sea físicamente o mediante VPN',
|
||||
591: 'Requiere configurar una dirección IP estática para',
|
||||
592: 'Ideal para acceso VPN a través de',
|
||||
593: 'en tu gateway',
|
||||
594: 'el servidor Wireguard de tu router',
|
||||
595: 'Requiere reenvío de puertos en el gateway',
|
||||
596: 'Requiere un registro DNS para',
|
||||
597: 'que se resuelva en',
|
||||
598: 'No recomendado para acceso VPN. Las VPN no admiten dominios “.local” sin configuración avanzada',
|
||||
599: 'Se puede usar para acceso a clearnet',
|
||||
600: 'No recomendado en la mayoría de los casos. Se prefieren los dominios de públicos',
|
||||
601: 'Local',
|
||||
602: 'Se puede usar para acceso local',
|
||||
603: 'Ideal para acceso público a través de Internet',
|
||||
604: 'Se puede usar para acceso personal a través de Internet público. VPN es más privado y seguro',
|
||||
605: 'cuando el uso de direcciones IP y puertos no es deseable',
|
||||
606: 'Host',
|
||||
607: 'Valor',
|
||||
608: 'Propósito',
|
||||
609: 'todos los subdominios de',
|
||||
610: 'DNS dinámico',
|
||||
611: 'Sin interfaces de servicio',
|
||||
612: 'Razón',
|
||||
613: 'No se pueden deshabilitar las puertas de enlace privadas para la interfaz de usuario de StartOS',
|
||||
614: 'Huella digital de la CA',
|
||||
615: 'Servidores DHCP',
|
||||
616: 'No se pueden editar los servidores DHCP',
|
||||
617: 'Estático',
|
||||
618: 'Servidores estáticos',
|
||||
619: 'Advertencia. StartOS está utilizando actualmente la siguiente puerta de enlace para DNS',
|
||||
620: 'Si deseas usar esta puerta de enlace para la resolución de dominios privados, configura servidores DNS estáticos alternativos usando el formulario anterior.',
|
||||
621: 'Empaquetar un servicio',
|
||||
622: 'Publicado',
|
||||
623: 'Implementaciones alternativas',
|
||||
624: 'Versiones',
|
||||
625: 'Seleccionar otra versión',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { i18n } from '../i18n.providers'
|
||||
export default {
|
||||
1: 'Modifier',
|
||||
2: 'Mettre à jour',
|
||||
3: 'Réinitialiser',
|
||||
4: 'Système',
|
||||
5: 'Général',
|
||||
6: 'Email',
|
||||
@@ -15,7 +14,6 @@ export default {
|
||||
12: 'Sessions actives',
|
||||
13: 'Changer le mot de passe',
|
||||
14: 'Paramètres généraux',
|
||||
15: 'Gérez votre configuration et vos préférences globales',
|
||||
16: 'Titre de l’onglet du navigateur',
|
||||
17: 'Langue',
|
||||
18: 'Réparation du disque',
|
||||
@@ -24,7 +22,7 @@ export default {
|
||||
21: 'Autorité de certification racine',
|
||||
22: 'Télécharger votre certificat racine',
|
||||
23: 'Télécharger',
|
||||
24: 'Réinitialiser Tor',
|
||||
24: 'Redémarrer Tor',
|
||||
25: 'Redémarrer le service Tor sur votre serveur',
|
||||
26: 'Mise à jour logicielle',
|
||||
27: 'Redémarrer pour appliquer',
|
||||
@@ -39,8 +37,8 @@ export default {
|
||||
36: 'Annuler',
|
||||
37: 'Cette action ne doit être effectuée que sur instruction d’un spécialiste du support Start9. Nous vous recommandons de sauvegarder votre appareil avant d’effectuer cette opération. Si un incident survient pendant le redémarrage, comme une coupure de courant ou le débranchement du disque, le système de fichiers pourrait devenir irrécupérable. Veuillez procéder avec prudence.',
|
||||
38: 'Supprimer',
|
||||
39: 'Réinitialisation de Tor en cours',
|
||||
40: 'Réinitialisation de Tor',
|
||||
39: 'Redémarrage de Tor en cours',
|
||||
40: 'Redémarrage de Tor',
|
||||
41: 'Recherche de mises à jour',
|
||||
42: 'Redémarrage initié',
|
||||
43: 'Vous utilisez la dernière version de StartOS.',
|
||||
@@ -48,8 +46,7 @@ export default {
|
||||
45: 'Notes de version',
|
||||
46: 'Commencer la mise à jour',
|
||||
47: 'Lancement de la mise à jour',
|
||||
48: 'Vous êtes actuellement connecté via Tor. Si vous réinitialisez le service Tor, vous perdrez la connexion jusqu’à ce que le service soit de nouveau en ligne.',
|
||||
49: 'Réinitialiser Tor ?',
|
||||
48: 'Vous êtes actuellement connecté via Tor. Si vous redémarrez le service Tor, vous perdrez la connectivité jusqu’à ce qu’il soit de nouveau en ligne.',
|
||||
50: 'Vous pouvez effacer l’état pour obtenir de nouveaux nœuds de garde. Il est recommandé d’essayer sans effacer l’état d’abord.',
|
||||
51: 'Effacer l’état',
|
||||
52: 'Enregistrement du meilleur score',
|
||||
@@ -91,30 +88,18 @@ export default {
|
||||
88: 'Actions',
|
||||
89: 'non recommandé',
|
||||
90: 'Certificat racine approuvé !',
|
||||
91: 'Ajoutez une addresse clearnet pour exposer cette interface sur Internet. Les adresses clearnet sont entièrement publiques et non anonymes.',
|
||||
92: 'En savoir plus',
|
||||
93: 'Rendre public',
|
||||
94: 'Rendre privé',
|
||||
95: 'Aucune adresse publique',
|
||||
96: 'Ajouter un domaine',
|
||||
96: 'Ajouter un domaine public',
|
||||
97: 'Suppression',
|
||||
98: 'Mise en public',
|
||||
99: 'Mise en privé',
|
||||
100: 'Modifications non enregistrées',
|
||||
101: 'Vous avez des modifications non enregistrées. Voulez-vous vraiment quitter ?',
|
||||
102: 'Quitter',
|
||||
103: 'Êtes-vous sûr ?',
|
||||
104: 'Sélectionner un domaine',
|
||||
105: 'Local',
|
||||
106: 'Les adresses locales ne sont accessibles qu’aux appareils connectés au même réseau local (LAN) que votre serveur, directement ou via un VPN.',
|
||||
107: 'En savoir plus',
|
||||
108: 'Public',
|
||||
109: 'Privé',
|
||||
110: 'Ajoutez une adresse onion (tor) pour exposer cette interface anonymement sur le darknet. Les adresses onion sont accessibles uniquement via le réseau Tor.',
|
||||
111: 'Aucune adresse onion',
|
||||
112: 'Nouvelle adresse onion',
|
||||
108: 'public',
|
||||
109: 'privé',
|
||||
111: 'Aucune domaine onion',
|
||||
112: 'Nouvelle domaine onion',
|
||||
113: 'Clé privée (optionnel)',
|
||||
114: 'Vous pouvez fournir une clé privée ed25519 encodée en base64 pour générer l’adresse Tor V3 (.onion). Sinon, une clé aléatoire sera générée et utilisée.',
|
||||
114: 'Vous pouvez fournir une clé privée ed25519 encodée en base64 pour générer le domaine Tor V3 (.onion). Si aucune n’est fournie, une clé aléatoire sera générée.',
|
||||
115: 'Traitement de 10 000 journaux',
|
||||
116: 'Chargement des journaux plus anciens',
|
||||
117: 'En attente d’une connexion réseau',
|
||||
@@ -140,7 +125,6 @@ export default {
|
||||
137: 'Journaux Tor',
|
||||
138: 'Journaux système bruts et non filtrés',
|
||||
139: 'Diagnostics des pilotes et autres processus du noyau',
|
||||
140: 'Journaux de diagnostic pour le service Tor sur StartOS',
|
||||
141: 'Rétrograder',
|
||||
142: 'Réinstaller',
|
||||
143: 'Installé',
|
||||
@@ -179,7 +163,6 @@ export default {
|
||||
177: 'Espace noyau',
|
||||
178: 'Inactif',
|
||||
179: 'Attente E/S',
|
||||
180: 'ACME',
|
||||
181: 'Total',
|
||||
182: 'Utilisé',
|
||||
183: 'Disponible',
|
||||
@@ -242,7 +225,7 @@ export default {
|
||||
240: 'Nom',
|
||||
241: 'Statut',
|
||||
242: 'Ouvrir',
|
||||
243: 'Interfaces',
|
||||
243: 'Interfaces de service',
|
||||
244: 'Hébergement',
|
||||
245: 'Installation',
|
||||
246: 'Voir ci-dessous',
|
||||
@@ -290,18 +273,15 @@ export default {
|
||||
292: 'Début du téléversement',
|
||||
293: 'Réessayer',
|
||||
294: 'Téléverser un fichier .s9pk',
|
||||
295: 'Attention : le téléversement du paquet sera lent via Tor. Passez en local pour une meilleure expérience.',
|
||||
296: 'Téléverser',
|
||||
295: 'Attention : le téléversement du paquet sera lent via Tor.',
|
||||
296: 'Sélectionner',
|
||||
297: 'Version 1 de s9pk détectée. Ce format de paquet est obsolète. Vous pouvez installer manuellement un s9pk V1 via start-cli si nécessaire.',
|
||||
298: 'Fichier paquet invalide',
|
||||
299: 'Ajoutez des fournisseurs ACME pour générer des certificats SSL (https) pour l’accès clearnet.',
|
||||
300: 'Voir les instructions',
|
||||
301: 'Fournisseurs enregistrés',
|
||||
302: 'Ajouter un fournisseur',
|
||||
303: 'Contact',
|
||||
304: 'Modifier',
|
||||
305: 'Ajouter un fournisseur ACME',
|
||||
306: 'Modifier le fournisseur ACME',
|
||||
305: 'Ajouter une autorité de certification',
|
||||
306: 'Modifier les informations de contact',
|
||||
307: 'Emails de contact',
|
||||
308: 'Nécessaire pour obtenir un certificat d’une autorité de certification',
|
||||
309: 'Tout cocher',
|
||||
@@ -327,8 +307,6 @@ export default {
|
||||
329: 'Nom d’hôte',
|
||||
330: 'Chemin',
|
||||
331: 'URL',
|
||||
332: 'Interface réseau',
|
||||
333: 'Protocole',
|
||||
334: 'Modèle',
|
||||
335: 'Agent utilisateur',
|
||||
336: 'Plateforme',
|
||||
@@ -375,7 +353,6 @@ export default {
|
||||
377: 'Sauvegardes StartOS détectées',
|
||||
378: 'Aucune sauvegarde StartOS détectée',
|
||||
379: 'Version de StartOS',
|
||||
380: 'Connecter un serveur SMTP externe permet à StartOS et à vos services installés de vous envoyer des emails.',
|
||||
381: 'Identifiants SMTP',
|
||||
382: 'Envoyer un email de test',
|
||||
383: 'Envoyer',
|
||||
@@ -383,7 +360,6 @@ export default {
|
||||
385: 'Un email de test a été envoyé à',
|
||||
386: 'Vérifiez votre dossier spam et marquez-le comme non spam.',
|
||||
387: 'L’interface web de votre serveur StartOS, accessible depuis n’importe quel navigateur.',
|
||||
388: 'Changez le mot de passe maître de StartOS.',
|
||||
389: 'Vous aurez toujours besoin de votre mot de passe actuel pour déchiffrer les sauvegardes existantes !',
|
||||
390: 'Les nouveaux mots de passe ne correspondent pas',
|
||||
391: 'Le nouveau mot de passe doit comporter au moins 12 caractères',
|
||||
@@ -393,7 +369,6 @@ export default {
|
||||
395: 'Mot de passe actuel',
|
||||
396: 'Nouveau mot de passe',
|
||||
397: 'Retapez le nouveau mot de passe',
|
||||
398: 'Une session correspond à un appareil actuellement connecté à StartOS. Pour plus de sécurité, terminez les sessions que vous ne reconnaissez pas ou que vous n’utilisez plus.',
|
||||
399: 'Session en cours',
|
||||
400: 'Autres sessions',
|
||||
401: 'Terminer les sessions séléctionnées',
|
||||
@@ -500,7 +475,6 @@ export default {
|
||||
502: 'informatique souveraine',
|
||||
503: 'Personnalisez le nom qui apparaît dans l’onglet de votre navigateur',
|
||||
504: 'Gérer',
|
||||
505: 'Êtes-vous sûr de vouloir supprimer cette adresse ?',
|
||||
506: '« Désinstallation douce » supprimera le service de StartOS tout en conservant ses données.',
|
||||
507: 'Aucun fournisseur enregistré',
|
||||
508: 'Mode kiosque',
|
||||
@@ -514,18 +488,105 @@ export default {
|
||||
516: 'Recommandé',
|
||||
517: 'Êtes-vous sûr de vouloir ignorer cette tâche ?',
|
||||
518: 'Ignorer',
|
||||
519: 'Pour publier des domaines clearnet, vous devez cliquer sur « Rendre public » ci-dessus.',
|
||||
520: 'Mise à jour disponible',
|
||||
521: 'Pour résoudre le problème, consultez',
|
||||
522: 'Version de SDK',
|
||||
523: 'Rapport de sauvegarde',
|
||||
524: 'Supprimer la sélection',
|
||||
525: 'Pas de clés',
|
||||
526: 'Ajouter une clé publique SSH',
|
||||
527: 'Par défaut, vous pouvez accéder à votre serveur en SSH depuis n’importe quel appareil en utilisant votre mot de passe maître. Vous pouvez également ajouter des clés publiques SSH pour accorder l’accès à certains appareils sans avoir à saisir de mot de passe.',
|
||||
525: 'Aucune clé SSH',
|
||||
526: 'Ajouter une clé SSH',
|
||||
527: 'Clés SSH',
|
||||
528: 'Code source',
|
||||
529: 'Service en amont',
|
||||
530: 'Paquet StartOS',
|
||||
531: "Erreur lors de l'initialisation du serveur",
|
||||
532: 'Terminé',
|
||||
533: 'Passerelles',
|
||||
535: 'Ajouter une passerelle',
|
||||
536: 'Renommer',
|
||||
537: 'Accès',
|
||||
538: 'Domaines publics',
|
||||
539: 'Autorités de certification',
|
||||
540: 'Domaine',
|
||||
541: 'Passerelle',
|
||||
543: 'Autorité de certification',
|
||||
544: 'Modifier le domaine',
|
||||
545: 'Aucun domaine public',
|
||||
546: 'Fournisseur',
|
||||
547: 'Voir DNS',
|
||||
548: 'Nouveau domaine public',
|
||||
550: 'Adresses',
|
||||
553: 'Aucune adresse',
|
||||
554: 'Changer l’AC',
|
||||
555: 'Détails de l’adresse',
|
||||
556: 'Domaines privés',
|
||||
557: 'Aucun domaine privé',
|
||||
558: 'Nouveau domaine privé',
|
||||
559: 'Serveurs DNS',
|
||||
560: 'Entrez un nom de domaine complet. Comme le domaine est destiné à un usage privé, il peut s’agir de n’importe quel domaine, même d’un domaine que vous ne contrôlez pas.',
|
||||
561: 'Entrez un nom de domaine complet. Par exemple, si vous contrôlez domain.com, vous pourriez entrer domain.com, subdomain.domain.com ou another.subdomain.domain.com.',
|
||||
562: 'Enregistrements DNS',
|
||||
563: 'Créez l’un des enregistrements DNS ci-dessous.',
|
||||
564: 'Aucun enregistrement DNS détecté pour',
|
||||
565: 'Enregistrement DNS invalide',
|
||||
566: 'se résout en',
|
||||
567: 'Enregistrement DNS détecté !',
|
||||
568: 'Sélectionnez une passerelle à utiliser pour ce domaine.',
|
||||
569: 'Sélectionnez une Autorité de Certification pour émettre des certificats SSL/TLS pour ce domaine.',
|
||||
570: 'Autre',
|
||||
571: 'Un nom pour identifier facilement la passerelle',
|
||||
572: 'Sélectionnez cette option si la passerelle est configurée pour un accès privé uniquement aux clients autorisés. StartTunnel est une passerelle privée.',
|
||||
573: 'Sélectionnez cette option si la passerelle est configurée pour un accès public illimité.',
|
||||
574: 'Fichier',
|
||||
575: 'Fichier de configuration Wireguard',
|
||||
576: 'Copier/Coller',
|
||||
577: 'Contenu du fichier',
|
||||
578: 'Clé publique',
|
||||
579: 'doit être une clé publique SSH valide',
|
||||
580: 'Actualisation nécessaire',
|
||||
581: 'Votre interface utilisateur est mise en cache et obsolète. Essayez de recharger le PWA à l’aide du bouton ci-dessous. Si vous continuez à voir ce message, désinstallez puis réinstallez le PWA.',
|
||||
582: 'Votre interface utilisateur est mise en cache et obsolète. Faites un rafraîchissement forcé de la page pour obtenir la dernière interface.',
|
||||
583: 'Nécessite de faire confiance à l’autorité de certification racine de votre serveur',
|
||||
584: 'Les connexions peuvent parfois être lentes ou peu fiables',
|
||||
585: 'Public si vous partagez l’adresse publiquement, sinon privé',
|
||||
586: 'Nécessite un appareil ou un navigateur compatible Tor',
|
||||
587: 'Utile uniquement pour les clients qui imposent HTTPS',
|
||||
588: 'Idéal pour l’hébergement et l’accès à distance anonymes et résistants à la censure',
|
||||
589: 'Idéal pour un accès local',
|
||||
590: 'Nécessite d’être connecté au même réseau local (LAN) que votre serveur, soit physiquement, soit via VPN',
|
||||
591: 'Nécessite de définir une adresse IP statique pour',
|
||||
592: 'Idéal pour un accès VPN via',
|
||||
593: 'dans votre passerelle',
|
||||
594: 'le serveur Wireguard de votre routeur',
|
||||
595: 'Nécessite un transfert de port dans la passerelle',
|
||||
596: 'Nécessite un enregistrement DNS pour',
|
||||
597: 'qui se résout en',
|
||||
598: 'Non recommandé pour l’accès VPN. Les VPN ne prennent pas en charge les domaines « .local » sans configuration avancée',
|
||||
599: 'Peut être utilisé pour un accès clearnet',
|
||||
600: 'Non recommandé dans la plupart des cas. Les domaines publics sont préférés',
|
||||
601: 'Local',
|
||||
602: 'Peut être utilisé pour un accès local',
|
||||
603: 'Idéal pour un accès public via Internet',
|
||||
604: 'Peut être utilisé pour un accès personnel via Internet public. Le VPN est plus privé et sécurisé',
|
||||
605: 'lorsque l’utilisation des adresses IP et des ports est indésirable',
|
||||
606: 'Hôte',
|
||||
607: 'Valeur',
|
||||
608: 'But',
|
||||
609: 'tous les sous-domaines de',
|
||||
610: 'DNS dynamique',
|
||||
611: 'Aucune interface de service',
|
||||
612: 'Raison',
|
||||
613: "Impossible de désactiver les passerelles privées pour l'interface utilisateur StartOS",
|
||||
614: 'Empreinte de l’AC',
|
||||
615: 'Serveurs DHCP',
|
||||
616: 'Impossible de modifier les serveurs DHCP',
|
||||
617: 'Statique',
|
||||
618: 'Serveurs statiques',
|
||||
619: 'Avertissement. StartOS utilise actuellement la passerelle suivante pour le DNS',
|
||||
620: 'Si vous souhaitez utiliser cette passerelle pour la résolution de domaines privés, définissez des serveurs DNS statiques alternatifs à l’aide du formulaire ci-dessus.',
|
||||
621: 'Emballer un service',
|
||||
622: 'Publié',
|
||||
623: 'Implémentations alternatives',
|
||||
624: 'Versions',
|
||||
625: 'Sélectionner une autre version',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { i18n } from '../i18n.providers'
|
||||
export default {
|
||||
1: 'Zmień',
|
||||
2: 'Aktualizuj',
|
||||
3: 'Resetuj',
|
||||
4: 'Ustawienia',
|
||||
5: 'Ogólne',
|
||||
6: 'E-mail',
|
||||
@@ -15,7 +14,6 @@ export default {
|
||||
12: 'Aktywne sesje',
|
||||
13: 'Zmień hasło',
|
||||
14: 'Ustawienia ogólne',
|
||||
15: 'Zarządzaj ustawieniami i preferencjami systemu',
|
||||
16: 'Tytuł karty przeglądarki',
|
||||
17: 'Język',
|
||||
18: 'Naprawa dysku',
|
||||
@@ -24,7 +22,7 @@ export default {
|
||||
21: 'Główny urząd certyfikacji (Root CA)',
|
||||
22: 'Pobierz swój główny certyfikat CA',
|
||||
23: 'Pobierz',
|
||||
24: 'Resetuj Tor',
|
||||
24: 'Uruchom ponownie Tor',
|
||||
25: 'Uruchom ponownie usługę Tor na serwerze',
|
||||
26: 'Aktualizacja systemu',
|
||||
27: 'Uruchom ponownie, aby zastosować',
|
||||
@@ -39,8 +37,8 @@ export default {
|
||||
36: 'Anuluj',
|
||||
37: 'Ta akcja powinna być wykonana tylko na polecenie specjalisty wsparcia Start9. Zalecamy wykonanie kopii zapasowej urządzenia przed wykonaniem tej akcji. Jeśli podczas ponownego uruchamiania urządzenia wystąpią problemy, takie jak utrata zasilania lub odłączenie dysku, system plików może znaleźć się w stanie niemożliwym do odzyskania. Zachowaj ostrożność.',
|
||||
38: 'Usuń',
|
||||
39: 'Resetowanie Tora w toku',
|
||||
40: 'Resetowanie Tora',
|
||||
39: 'Ponowne uruchamianie Tor w toku',
|
||||
40: 'Trwa ponowne uruchamianie Tor',
|
||||
41: 'Sprawdzanie aktualizacji',
|
||||
42: 'Rozpoczynanie ponownego uruchamiania',
|
||||
43: 'Korzystasz z najnowszej wersji StartOS.',
|
||||
@@ -48,8 +46,7 @@ export default {
|
||||
45: 'Informacje o wydaniu',
|
||||
46: 'Rozpocznij aktualizację',
|
||||
47: 'Rozpoczynanie aktualizacji',
|
||||
48: 'Obecnie jesteś połączony przez sieć Tor. Jeśli zresetujesz usługę Tor, utracisz połączenie do czasu jej ponownego uruchomienia.',
|
||||
49: 'Zresetować Tora?',
|
||||
48: 'Jesteś obecnie połączony przez Tor. Jeśli zrestartujesz demona Tor, stracisz łączność do momentu, aż ponownie będzie online.',
|
||||
50: 'Opcjonalnie wyczyść stan, aby wymusić pozyskanie nowych węzłów strażniczych. Zaleca się najpierw spróbować bez czyszczenia stanu.',
|
||||
51: 'Wyczyść stan',
|
||||
52: 'Zapisywanie najlepszego wyniku',
|
||||
@@ -91,30 +88,18 @@ export default {
|
||||
88: 'Akcje',
|
||||
89: 'niezalecane',
|
||||
90: 'Główny certyfikat CA zaufany!',
|
||||
91: 'Dodaj adres clearnet, aby udostępnić ten interfejs w Internecie. Adresy clearnet są w pełni publiczne i nie zapewniają anonimowości.',
|
||||
92: 'Dowiedz się więcej',
|
||||
93: 'Upublicznij',
|
||||
94: 'Ukryj',
|
||||
95: 'Brak publicznych adresów',
|
||||
96: 'Dodaj domenę',
|
||||
96: 'Dodaj domenę publiczną',
|
||||
97: 'Usuwanie',
|
||||
98: 'Upublicznianie',
|
||||
99: 'Ukrywanie',
|
||||
100: 'Niezapisane zmiany',
|
||||
101: 'Masz niezapisane zmiany. Czy na pewno chcesz opuścić tę stronę?',
|
||||
102: 'Opuść',
|
||||
103: 'Czy jesteś pewien?',
|
||||
104: 'Wybierz domenę',
|
||||
105: 'Lokalne',
|
||||
106: 'Adresy lokalne są dostępne tylko dla urządzeń podłączonych do tej samej sieci LAN co twój serwer, bezpośrednio lub przez VPN.',
|
||||
107: 'Dowiedz się więcej',
|
||||
108: 'Publiczny',
|
||||
109: 'Prywatny',
|
||||
110: 'Dodaj adres onion, aby anonimowo udostępnić ten interfejs w sieci Tor. Adresy onion są dostępne tylko przez sieć Tor.',
|
||||
111: 'Brak adresów onion',
|
||||
112: 'Nowy adres Onion',
|
||||
108: 'publiczny',
|
||||
109: 'prywatny',
|
||||
111: 'Brak domeny onion',
|
||||
112: 'Nowy domenę onion',
|
||||
113: 'Klucz prywatny (opcjonalnie)',
|
||||
114: 'Opcjonalnie podaj klucz prywatny ed25519 zakodowany w base64, aby wygenerować adres Tor V3 (.onion). Jeśli nie zostanie podany, zostanie wygenerowany i użyty losowy klucz.',
|
||||
114: 'Opcjonalnie podaj klucz prywatny ed25519 zakodowany w base64, aby wygenerować domenę Tor V3 (.onion). Jeśli nie zostanie podany, zostanie wygenerowany losowy klucz.',
|
||||
115: 'Przetwarzanie 10 000 logów',
|
||||
116: 'Ładowanie starszych logów',
|
||||
117: 'Oczekiwanie na połączenie sieciowe',
|
||||
@@ -140,7 +125,6 @@ export default {
|
||||
137: 'Logi Tor',
|
||||
138: 'Surowe, nieprzefiltrowane logi systemu operacyjnego',
|
||||
139: 'Diagnostyka sterowników i innych procesów jądra',
|
||||
140: 'Logi diagnostyczne usługi Tor w StartOS',
|
||||
141: 'Przywróć starszą wersję',
|
||||
142: 'Zainstaluj ponownie',
|
||||
143: 'Zainstalowane',
|
||||
@@ -179,7 +163,6 @@ export default {
|
||||
177: 'Przestrzeń jądra',
|
||||
178: 'Bezczynność',
|
||||
179: 'Oczekiwanie na I/O',
|
||||
180: 'ACME',
|
||||
181: 'Łącznie',
|
||||
182: 'Wykorzystane',
|
||||
183: 'Dostępne',
|
||||
@@ -242,7 +225,7 @@ export default {
|
||||
240: 'Nazwa',
|
||||
241: 'Stan',
|
||||
242: 'Otwórz',
|
||||
243: 'Przyłącza',
|
||||
243: 'Interfejsy usług',
|
||||
244: 'Hosting',
|
||||
245: 'Instalowanie',
|
||||
246: 'Zobacz poniżej',
|
||||
@@ -290,18 +273,15 @@ export default {
|
||||
292: 'Rozpoczynanie przesyłania',
|
||||
293: 'Spróbuj ponownie',
|
||||
294: 'Prześlij plik pakietu .s9pk',
|
||||
295: 'Uwaga: przesyłanie pakietu przez Tor będzie powolne. Przełącz się na sieć lokalną, aby uzyskać lepszą wydajność.',
|
||||
296: 'Prześlij',
|
||||
295: 'Uwaga: przesyłanie pakietu przez Tor będzie powolne.',
|
||||
296: 'Wybierz',
|
||||
297: 'Wykryto pakiet s9pk w wersji 1. Ten format pakietu jest przestarzały. Możesz zainstalować pakiet s9pk V1 przez start-cli, jeśli to konieczne.',
|
||||
298: 'Nieprawidłowy plik pakietu',
|
||||
299: 'Dodaj dostawców ACME, aby wygenerować certyfikaty SSL (https) dla dostępu przez clearnet.',
|
||||
300: 'Zobacz instrukcje',
|
||||
301: 'Zapisani dostawcy',
|
||||
302: 'Dodaj dostawcę',
|
||||
303: 'Kontakt',
|
||||
304: 'Edytuj',
|
||||
305: 'Dodaj dostawcę ACME',
|
||||
306: 'Edytuj dostawcę ACME',
|
||||
305: 'Dodaj urząd certyfikacji',
|
||||
306: 'Edytuj dane kontaktowe',
|
||||
307: 'Adresy e-mail kontaktowe',
|
||||
308: 'Wymagane do uzyskania certyfikatu od urzędu certyfikacji',
|
||||
309: 'Zaznacz wszystkie',
|
||||
@@ -327,8 +307,6 @@ export default {
|
||||
329: 'Nazwa hosta',
|
||||
330: 'Ścieżka',
|
||||
331: 'URL',
|
||||
332: 'Interfejs sieciowy',
|
||||
333: 'Protokół',
|
||||
334: 'Model',
|
||||
335: 'Agent użytkownika',
|
||||
336: 'Platforma',
|
||||
@@ -375,7 +353,6 @@ export default {
|
||||
377: 'Wykryto kopie zapasowe StartOS',
|
||||
378: 'Nie wykryto kopii zapasowych StartOS',
|
||||
379: 'Wersja StartOS',
|
||||
380: 'Podłączenie zewnętrznego serwera SMTP umożliwia StartOS i zainstalowanym serwisom wysyłanie wiadomości e-mail.',
|
||||
381: 'Dane logowania SMTP',
|
||||
382: 'Wyślij e-mail testowy',
|
||||
383: 'Wyślij',
|
||||
@@ -383,7 +360,6 @@ export default {
|
||||
385: 'Wiadomość testowa została wysłana na adres',
|
||||
386: 'Sprawdź folder spam i oznacz wiadomość jako "nie spam".',
|
||||
387: 'Przyłącze użytkownika twojego serwera StartOS, dostępne z dowolnej przeglądarki.',
|
||||
388: 'Zmień swoje hasło główne StartOS.',
|
||||
389: 'Nadal będziesz potrzebować aktualnego hasła, aby odszyfrować istniejące kopie zapasowe!',
|
||||
390: 'Nowe hasła nie są zgodne',
|
||||
391: 'Nowe hasło musi mieć co najmniej 12 znaków',
|
||||
@@ -393,7 +369,6 @@ export default {
|
||||
395: 'Bieżące hasło',
|
||||
396: 'Nowe hasło',
|
||||
397: 'Powtórz nowe hasło',
|
||||
398: 'Sesja to urządzenie, które jest obecnie zalogowane do StartOS. Dla najlepszego bezpieczeństwa zakończ sesje, których nie rozpoznajesz lub już nie używasz.',
|
||||
399: 'Bieżąca sesja',
|
||||
400: 'Inne sesje',
|
||||
401: 'Zakończ wybrane',
|
||||
@@ -500,7 +475,6 @@ export default {
|
||||
502: 'suwerenne przetwarzanie',
|
||||
503: 'Dostosuj nazwę wyświetlaną na karcie przeglądarki',
|
||||
504: 'Zarządzać',
|
||||
505: 'Czy na pewno chcesz usunąć ten adres?',
|
||||
506: '„Miękkie odinstalowanie” usunie usługę z StartOS, ale zachowa jej dane.',
|
||||
507: 'Brak zapisanych dostawców',
|
||||
508: 'Tryb kiosku',
|
||||
@@ -514,18 +488,105 @@ export default {
|
||||
516: 'Zalecane',
|
||||
517: 'Czy na pewno chcesz odrzucić to zadanie?',
|
||||
518: 'Odrzuć',
|
||||
519: 'Aby opublikować domeny w clearnet, kliknij „Upublicznij” powyżej.',
|
||||
520: 'Aktualizacja dostępna',
|
||||
521: 'Aby rozwiązać problem, zapoznaj się z',
|
||||
522: 'Wersja SDK',
|
||||
523: 'Raport kopii zapasowej',
|
||||
524: 'Usuń wybrane',
|
||||
525: 'Brak kluczy',
|
||||
526: 'Dodaj klucz publiczny SSH',
|
||||
527: 'Domyślnie możesz połączyć się z serwerem przez SSH z dowolnego urządzenia, używając hasła głównego. Opcjonalnie dodaj klucze publiczne SSH, aby przyznać dostęp określonym urządzeniom bez potrzeby wpisywania hasła.',
|
||||
525: 'Brak kluczy SSH',
|
||||
526: 'Dodaj klucz SSH',
|
||||
527: 'Klucze SSH',
|
||||
528: 'Kod źródłowy',
|
||||
529: 'Usługa źródłowa',
|
||||
530: 'Pakiet StartOS',
|
||||
531: 'Błąd inicjalizacji serwera',
|
||||
532: 'Zakończono',
|
||||
533: 'Bramy sieciowe',
|
||||
535: 'Dodaj bramę',
|
||||
536: 'Zmień nazwę',
|
||||
537: 'Dostęp',
|
||||
538: 'Domeny publiczne',
|
||||
539: 'Urzędy certyfikacji',
|
||||
540: 'Domena',
|
||||
541: 'Brama',
|
||||
543: 'Urząd certyfikacji',
|
||||
544: 'Edytuj domenę',
|
||||
545: 'Brak domen publicznych',
|
||||
546: 'Dostawca',
|
||||
547: 'Pokaż DNS',
|
||||
548: 'Nowa domena publiczna',
|
||||
550: 'Adresy',
|
||||
553: 'Brak adresów',
|
||||
554: 'Zmień CA',
|
||||
555: 'Szczegóły adresu',
|
||||
556: 'Domeny prywatne',
|
||||
557: 'Brak domen prywatnych',
|
||||
558: 'Nowa domena prywatna',
|
||||
559: 'Serwery DNS',
|
||||
560: 'Wprowadź w pełni kwalifikowaną nazwę domeny. Ponieważ domena jest przeznaczona do użytku prywatnego, może to być dowolna domena, nawet taka, której nie kontrolujesz.',
|
||||
561: 'Wprowadź w pełni kwalifikowaną nazwę domeny. Na przykład, jeśli kontrolujesz domain.com, możesz wprowadzić domain.com, subdomain.domain.com lub another.subdomain.domain.com.',
|
||||
562: 'Rekordy DNS',
|
||||
563: 'Utwórz jeden z poniższych rekordów DNS.',
|
||||
564: 'Nie wykryto rekordu DNS dla',
|
||||
565: 'Nieprawidłowy rekord DNS',
|
||||
566: 'rozwiązuje się na',
|
||||
567: 'Wykryto rekord DNS!',
|
||||
568: 'Wybierz bramę do użycia dla tej domeny.',
|
||||
569: 'Wybierz Urząd Certyfikacji, aby wystawić certyfikaty SSL/TLS dla tej domeny.',
|
||||
570: 'Inne',
|
||||
571: 'Nazwa ułatwiająca identyfikację bramy',
|
||||
572: 'Wybierz tę opcję, jeśli brama jest skonfigurowana do prywatnego dostępu tylko dla autoryzowanych klientów. StartTunnel to prywatna brama.',
|
||||
573: 'Wybierz tę opcję, jeśli brama jest skonfigurowana do nieograniczonego publicznego dostępu.',
|
||||
574: 'Plik',
|
||||
575: 'Plik konfiguracyjny Wireguard',
|
||||
576: 'Kopiuj/Wklej',
|
||||
577: 'Zawartość pliku',
|
||||
578: 'Klucz publiczny',
|
||||
579: 'musi być prawidłowym kluczem publicznym SSH',
|
||||
580: 'Wymagane odświeżenie',
|
||||
581: 'Twój interfejs użytkownika jest w pamięci podręcznej i jest nieaktualny. Spróbuj ponownie załadować PWA za pomocą przycisku poniżej. Jeśli nadal widzisz ten komunikat, odinstaluj i ponownie zainstaluj PWA.',
|
||||
582: 'Twój interfejs użytkownika jest w pamięci podręcznej i jest nieaktualny. Wykonaj twarde odświeżenie strony, aby uzyskać najnowszy interfejs.',
|
||||
583: 'Wymaga zaufania do głównego CA twojego serwera',
|
||||
584: 'Połączenia mogą być czasami wolne lub niestabilne',
|
||||
585: 'Publiczne, jeśli udostępniasz adres publicznie, w przeciwnym razie prywatne',
|
||||
586: 'Wymaga urządzenia lub przeglądarki obsługującej Tor',
|
||||
587: 'Przydatne tylko dla klientów wymuszających HTTPS',
|
||||
588: 'Idealne do anonimowego, odpornego na cenzurę hostingu i zdalnego dostępu',
|
||||
589: 'Idealne do dostępu lokalnego',
|
||||
590: 'Wymaga połączenia z tą samą siecią lokalną (LAN) co serwer, fizycznie lub przez VPN',
|
||||
591: 'Wymaga ustawienia statycznego adresu IP dla',
|
||||
592: 'Idealne do dostępu VPN przez',
|
||||
593: 'w twojej bramie',
|
||||
594: 'serwer Wireguard twojego routera',
|
||||
595: 'Wymaga przekierowania portów w bramie',
|
||||
596: 'Wymaga rekordu DNS dla',
|
||||
597: 'który rozwiązuje się na',
|
||||
598: 'Niezalecane do dostępu VPN. VPN-y nie obsługują domen „.local” bez zaawansowanej konfiguracji',
|
||||
599: 'Może być używane do dostępu do clearnet',
|
||||
600: 'Niezalecane w większości przypadków. Preferowane są domeny publiczne',
|
||||
601: 'Lokalne',
|
||||
602: 'Może być używane do dostępu lokalnego',
|
||||
603: 'Idealne do publicznego dostępu przez Internet',
|
||||
604: 'Może być używane do osobistego dostępu przez publiczny Internet. VPN jest bardziej prywatny i bezpieczny',
|
||||
605: 'gdy używanie adresów IP i portów jest niepożądane',
|
||||
606: 'Host',
|
||||
607: 'Wartość',
|
||||
608: 'Cel',
|
||||
609: 'wszystkie subdomeny',
|
||||
610: 'Dynamiczny DNS',
|
||||
611: 'Brak interfejsów usług',
|
||||
612: 'Powód',
|
||||
613: 'Nie można wyłączyć prywatnych bram dla interfejsu użytkownika StartOS',
|
||||
614: 'Odcisk palca CA',
|
||||
615: 'Serwery DHCP',
|
||||
616: 'Nie można edytować serwerów DHCP',
|
||||
617: 'Statyczny',
|
||||
618: 'Serwery statyczne',
|
||||
619: 'Ostrzeżenie. StartOS obecnie używa następującej bramy do DNS',
|
||||
620: 'Jeśli zamierzasz używać tej bramy do rozwiązywania domen prywatnych, ustaw alternatywne statyczne serwery DNS za pomocą powyższego formularza.',
|
||||
621: 'Spakietuj usługę',
|
||||
622: 'Wydano',
|
||||
623: 'Alternatywne implementacje',
|
||||
624: 'Wersje',
|
||||
625: 'Wybierz inną wersję',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -10,9 +10,9 @@ import { I18N, i18nKey } from './i18n.providers'
|
||||
export class i18nPipe implements PipeTransform {
|
||||
private readonly i18n = inject(I18N)
|
||||
|
||||
transform(englishKey: i18nKey | null | undefined): string | undefined {
|
||||
return englishKey
|
||||
? this.i18n()?.[ENGLISH[englishKey as i18nKey]] || englishKey
|
||||
: undefined
|
||||
transform(englishKey: i18nKey | null | undefined): string {
|
||||
englishKey = englishKey || ('' as i18nKey)
|
||||
|
||||
return this.i18n()?.[ENGLISH[englishKey]] || englishKey
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export class DialogService {
|
||||
})
|
||||
}
|
||||
|
||||
openConfirm<T = void>(
|
||||
openConfirm(
|
||||
options: Partial<TuiResponsiveDialogOptions<TuiConfirmData>> & {
|
||||
label: i18nKey
|
||||
data?: TuiConfirmData & {
|
||||
@@ -49,13 +49,13 @@ export class DialogService {
|
||||
}
|
||||
},
|
||||
) {
|
||||
options.data = options.data || {}
|
||||
const { content, yes, no } = options.data
|
||||
const { content, yes, no } = options.data || {}
|
||||
|
||||
return this.dialogs.open<T>(TUI_CONFIRM, {
|
||||
return this.dialogs.open<boolean>(TUI_CONFIRM, {
|
||||
...options,
|
||||
label: this.i18n.transform(options.label),
|
||||
data: {
|
||||
...options.data,
|
||||
...(options.data || {}),
|
||||
content: isI18n(content) ? this.i18n.transform(content) : content,
|
||||
yes: this.i18n.transform(yes),
|
||||
no: this.i18n.transform(no),
|
||||
|
||||
@@ -88,13 +88,6 @@
|
||||
--start9-base-5: rgba(60, 62, 64, 1);
|
||||
}
|
||||
|
||||
[tuiAppearance][data-appearance^='primary'] {
|
||||
@include taiga.appearance-disabled {
|
||||
background: var(--tui-status-neutral);
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
[tuiAppearance][data-appearance='primary-success'] {
|
||||
color: var(--tui-text-primary-on-accent-1);
|
||||
background: var(--tui-status-positive);
|
||||
@@ -108,8 +101,7 @@
|
||||
}
|
||||
|
||||
@include taiga.appearance-disabled {
|
||||
background: var(--tui-status-neutral);
|
||||
color: #333;
|
||||
opacity: var(--tui-disabled-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,11 +118,8 @@ tui-dropdown[data-appearance='start-os'][data-appearance='start-os'] {
|
||||
var(--tui-background-elevation-3) 75%,
|
||||
transparent
|
||||
);
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
rgba(255, 255, 255, 0.15),
|
||||
transparent
|
||||
),
|
||||
background-image:
|
||||
linear-gradient(to bottom, rgba(255, 255, 255, 0.15), transparent),
|
||||
linear-gradient(to bottom, rgba(255, 255, 255, 0.15), transparent);
|
||||
background-size: 1px 100%;
|
||||
background-repeat: no-repeat;
|
||||
@@ -162,6 +151,10 @@ tui-badge-notification {
|
||||
background: var(--tui-status-negative);
|
||||
}
|
||||
|
||||
tui-textfield [tuiTooltip] {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
[tuiCell] {
|
||||
&[data-height='spacious'] {
|
||||
padding-block: 0.75rem;
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import {
|
||||
provideHttpClient,
|
||||
withFetch,
|
||||
withInterceptorsFromDi,
|
||||
} from '@angular/common/http'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
import { ServiceWorkerModule } from '@angular/service-worker'
|
||||
import { TuiRoot } from '@taiga-ui/core'
|
||||
import { ToastContainerComponent } from 'src/app/components/toast-container.component'
|
||||
@@ -12,7 +16,7 @@ import { RoutingModule } from './routing.module'
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [
|
||||
BrowserAnimationsModule,
|
||||
BrowserModule,
|
||||
RoutingModule,
|
||||
ToastContainerComponent,
|
||||
TuiRoot,
|
||||
@@ -23,7 +27,10 @@ import { RoutingModule } from './routing.module'
|
||||
registrationStrategy: 'registerWhenStable:30000',
|
||||
}),
|
||||
],
|
||||
providers: [APP_PROVIDERS, provideHttpClient(withInterceptorsFromDi())],
|
||||
providers: [
|
||||
APP_PROVIDERS,
|
||||
provideHttpClient(withInterceptorsFromDi(), withFetch()),
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { inject, provideAppInitializer } from '@angular/core'
|
||||
import { UntypedFormBuilder } from '@angular/forms'
|
||||
import { provideAnimations } from '@angular/platform-browser/animations'
|
||||
import { Router } from '@angular/router'
|
||||
import { WA_LOCATION } from '@ng-web-apis/common'
|
||||
import initArgon from '@start9labs/argon2'
|
||||
@@ -28,7 +29,6 @@ import {
|
||||
TUI_DATE_TIME_VALUE_TRANSFORMER,
|
||||
TUI_DATE_VALUE_TRANSFORMER,
|
||||
} from '@taiga-ui/kit'
|
||||
import { tuiTextfieldOptionsProvider } from '@taiga-ui/legacy'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { filter, of, pairwise } from 'rxjs'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
PatchDbSource,
|
||||
} from 'src/app/services/patch-db/patch-db-source'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
import { FilterUpdatesPipe } from './routes/portal/routes/updates/filter-updates.pipe'
|
||||
import { ApiService } from './services/api/embassy-api.service'
|
||||
import { LiveApiService } from './services/api/embassy-live-api.service'
|
||||
import { MockApiService } from './services/api/embassy-mock-api.service'
|
||||
@@ -46,7 +47,6 @@ import { ClientStorageService } from './services/client-storage.service'
|
||||
import { DateTransformerService } from './services/date-transformer.service'
|
||||
import { DatetimeTransformerService } from './services/datetime-transformer.service'
|
||||
import { StorageService } from './services/storage.service'
|
||||
import { FilterUpdatesPipe } from './routes/portal/routes/updates/filter-updates.pipe'
|
||||
|
||||
const {
|
||||
useMocks,
|
||||
@@ -54,6 +54,7 @@ const {
|
||||
} = require('../../../../config.json') as WorkspaceConfig
|
||||
|
||||
export const APP_PROVIDERS = [
|
||||
provideAnimations(),
|
||||
provideEventPlugins(),
|
||||
I18N_PROVIDERS,
|
||||
FilterPackagesPipe,
|
||||
@@ -61,7 +62,6 @@ export const APP_PROVIDERS = [
|
||||
UntypedFormBuilder,
|
||||
tuiNumberFormatProvider({ decimalSeparator: '.', thousandSeparator: '' }),
|
||||
tuiButtonOptionsProvider({ size: 'm' }),
|
||||
tuiTextfieldOptionsProvider({ hintOnDisabled: true }),
|
||||
tuiDropdownOptionsProvider({ appearance: 'start-os' }),
|
||||
tuiAlertOptionsProvider({
|
||||
autoClose: appearance => (appearance === 'negative' ? 0 : 3000),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { SwUpdate } from '@angular/service-worker'
|
||||
import { WA_WINDOW } from '@ng-web-apis/common'
|
||||
import { LoadingService } from '@start9labs/shared'
|
||||
import { i18nPipe, LoadingService } from '@start9labs/shared'
|
||||
import { Version } from '@start9labs/start-sdk'
|
||||
import { TuiResponsiveDialog } from '@taiga-ui/addon-mobile'
|
||||
import { TuiAutoFocus } from '@taiga-ui/cdk'
|
||||
@@ -17,14 +17,18 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
template: `
|
||||
<ng-template
|
||||
[tuiResponsiveDialog]="show()"
|
||||
[tuiResponsiveDialogOptions]="{ label: 'Refresh Needed', size: 's' }"
|
||||
[tuiResponsiveDialogOptions]="{
|
||||
label: i18n.transform('Refresh Needed'),
|
||||
size: 's',
|
||||
}"
|
||||
(tuiResponsiveDialogChange)="dismiss$.next()"
|
||||
>
|
||||
@if (isPwa) {
|
||||
<p>
|
||||
Your user interface is cached and out of date. Attempt to reload the
|
||||
PWA using the button below. If you continue to see this message,
|
||||
uninstall and reinstall the PWA.
|
||||
{{
|
||||
'Your user interface is cached and out of date. Attempt to reload the PWA using the button below. If you continue to see this message, uninstall and reinstall the PWA.'
|
||||
| i18n
|
||||
}}
|
||||
</p>
|
||||
<button
|
||||
tuiButton
|
||||
@@ -34,11 +38,13 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
[tuiAppearanceFocus]="false"
|
||||
(click)="pwaReload()"
|
||||
>
|
||||
Reload
|
||||
{{ 'Refresh' | i18n }}
|
||||
</button>
|
||||
} @else {
|
||||
Your user interface is cached and out of date. Hard refresh the page to
|
||||
get the latest UI.
|
||||
{{
|
||||
'Your user interface is cached and out of date. Hard refresh the page to get the latest UI.'
|
||||
| i18n
|
||||
}}
|
||||
<ul>
|
||||
<li>
|
||||
<b>On Mac</b>
|
||||
@@ -57,13 +63,13 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
[tuiAppearanceFocus]="false"
|
||||
(click)="dismiss$.next()"
|
||||
>
|
||||
Ok
|
||||
{{ 'Ok' | i18n }}
|
||||
</button>
|
||||
}
|
||||
</ng-template>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiResponsiveDialog, TuiButton, TuiAutoFocus],
|
||||
imports: [TuiResponsiveDialog, TuiButton, TuiAutoFocus, i18nPipe],
|
||||
})
|
||||
export class RefreshAlertComponent {
|
||||
private readonly win = inject(WA_WINDOW)
|
||||
@@ -71,6 +77,8 @@ export class RefreshAlertComponent {
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly version = Version.parse(inject(ConfigService).version)
|
||||
|
||||
readonly i18n = inject(i18nPipe)
|
||||
|
||||
readonly dismiss$ = new Subject<void>()
|
||||
readonly isPwa = this.win.matchMedia('(display-mode: standalone)').matches
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
max-height: 100vh;
|
||||
max-height: 100dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
tuiButton
|
||||
docsLink
|
||||
size="s"
|
||||
href="/user-manual/trust-ca.html"
|
||||
path="/user-manual/trust-ca.html"
|
||||
iconEnd="@tui.external-link"
|
||||
>
|
||||
{{ 'View instructions' | i18n }}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
}
|
||||
|
||||
[tuiButton] {
|
||||
|
||||
@@ -37,7 +37,7 @@ export class CAWizardComponent {
|
||||
}
|
||||
|
||||
launchHttps() {
|
||||
this.document.defaultView?.open(`https://${this.config.getHost()}`, '_self')
|
||||
this.document.defaultView?.open(`https://${this.config.host}`, '_self')
|
||||
}
|
||||
|
||||
private async testHttps() {
|
||||
|
||||
@@ -7,14 +7,18 @@
|
||||
<img alt="StartOS Icon" class="logo" src="assets/img/icon.png" />
|
||||
<h1 class="header">{{ 'Login to StartOS' | i18n }}</h1>
|
||||
<form (submit)="submit()">
|
||||
<tui-input-password
|
||||
tuiTextfieldIconLeft="@tui.key"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[(ngModel)]="password"
|
||||
(ngModelChange)="error = null"
|
||||
>
|
||||
{{ 'Password' | i18n }}
|
||||
</tui-input-password>
|
||||
<tui-textfield iconStart="@tui.key">
|
||||
<label tuiLabel>{{ 'Password' | i18n }}</label>
|
||||
<input
|
||||
tuiAutoFocus
|
||||
tuiTextfield
|
||||
type="password"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[(ngModel)]="password"
|
||||
(ngModelChange)="error = null"
|
||||
/>
|
||||
<tui-icon tuiPassword />
|
||||
</tui-textfield>
|
||||
<tui-error class="error" [error]="error || null" />
|
||||
<button tuiButton class="button">{{ 'Login' | i18n }}</button>
|
||||
</form>
|
||||
|
||||
@@ -2,15 +2,13 @@ import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { TuiButton, TuiError } from '@taiga-ui/core'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { TuiAutoFocus } from '@taiga-ui/cdk'
|
||||
import { TuiButton, TuiError, TuiIcon, TuiTextfield } from '@taiga-ui/core'
|
||||
import { TuiPassword } from '@taiga-ui/kit'
|
||||
import { TuiCardLarge } from '@taiga-ui/layout'
|
||||
import {
|
||||
TuiInputPasswordModule,
|
||||
TuiTextfieldControllerModule,
|
||||
} from '@taiga-ui/legacy'
|
||||
import { CAWizardComponent } from './ca-wizard/ca-wizard.component'
|
||||
import { LoginPage } from './login.page'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -26,8 +24,10 @@ const routes: Routes = [
|
||||
CAWizardComponent,
|
||||
TuiButton,
|
||||
TuiCardLarge,
|
||||
TuiInputPasswordModule,
|
||||
TuiTextfieldControllerModule,
|
||||
...TuiTextfield,
|
||||
TuiIcon,
|
||||
TuiPassword,
|
||||
TuiAutoFocus,
|
||||
TuiError,
|
||||
RouterModule.forChild(routes),
|
||||
i18nPipe,
|
||||
|
||||
@@ -31,3 +31,7 @@
|
||||
border-radius: 10rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
[tuiLabel] {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ import { TuiButton, TuiDialogContext } from '@taiga-ui/core'
|
||||
import { TuiConfirmService } from '@taiga-ui/kit'
|
||||
import { POLYMORPHEUS_CONTEXT } from '@taiga-ui/polymorpheus'
|
||||
import { Operation } from 'fast-json-patch'
|
||||
import { FormModule } from 'src/app/routes/portal/components/form/form.module'
|
||||
import { InvalidService } from 'src/app/routes/portal/components/form/invalid.service'
|
||||
import { FormGroupComponent } from 'src/app/routes/portal/components/form/containers/group.component'
|
||||
import { InvalidService } from 'src/app/routes/portal/components/form/containers/control.directive'
|
||||
import { FormService } from 'src/app/services/form.service'
|
||||
|
||||
export interface ActionButton<T> {
|
||||
@@ -88,7 +88,7 @@ export interface FormContext<T> {
|
||||
RouterModule,
|
||||
TuiValueChanges,
|
||||
TuiButton,
|
||||
FormModule,
|
||||
FormGroupComponent,
|
||||
],
|
||||
providers: [InvalidService],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
import { AsyncPipe } from '@angular/common'
|
||||
import {
|
||||
Component,
|
||||
DestroyRef,
|
||||
forwardRef,
|
||||
HostBinding,
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
||||
import {
|
||||
AbstractControl,
|
||||
FormArrayName,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { DialogService, i18nKey, i18nPipe } from '@start9labs/shared'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import {
|
||||
TUI_ANIMATIONS_SPEED,
|
||||
TuiButton,
|
||||
TuiError,
|
||||
tuiFadeIn,
|
||||
tuiHeightCollapse,
|
||||
TuiIcon,
|
||||
TuiLink,
|
||||
tuiParentStop,
|
||||
TuiTextfield,
|
||||
tuiToAnimationOptions,
|
||||
} from '@taiga-ui/core'
|
||||
import { TuiFieldErrorPipe, TuiTooltip } from '@taiga-ui/kit'
|
||||
import { filter } from 'rxjs'
|
||||
import { FormService } from 'src/app/services/form.service'
|
||||
|
||||
import { HintPipe } from '../pipes/hint.pipe'
|
||||
import { MustachePipe } from '../pipes/mustache.pipe'
|
||||
import { ERRORS, FormControlComponent } from './control.component'
|
||||
import { ControlDirective } from './control.directive'
|
||||
import { FormObjectComponent } from './object.component'
|
||||
|
||||
@Component({
|
||||
selector: 'form-array',
|
||||
template: `
|
||||
<div class="label">
|
||||
{{ spec.name }}
|
||||
@if (spec.description || spec.disabled) {
|
||||
<tui-icon [tuiTooltip]="spec | hint" />
|
||||
}
|
||||
<button
|
||||
tuiLink
|
||||
type="button"
|
||||
class="add"
|
||||
[disabled]="!canAdd"
|
||||
(click)="add()"
|
||||
>
|
||||
+ {{ 'Add' | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<tui-error [error]="order | tuiFieldError | async" />
|
||||
@for (item of array.control.controls; track item) {
|
||||
@if (spec.spec.type === 'object') {
|
||||
<form-object
|
||||
class="object"
|
||||
[class.object_open]="!!open.get(item)"
|
||||
[formGroup]="$any(item)"
|
||||
[spec]="$any(spec.spec)"
|
||||
[@tuiHeightCollapse]="animation"
|
||||
[@tuiFadeIn]="animation"
|
||||
[open]="!!open.get(item)"
|
||||
(openChange)="open.set(item, $event)"
|
||||
>
|
||||
{{ item.value | mustache: $any(spec.spec).displayAs }}
|
||||
<button
|
||||
tuiIconButton
|
||||
type="button"
|
||||
class="remove"
|
||||
iconStart="@tui.trash"
|
||||
appearance="icon"
|
||||
size="m"
|
||||
title="Remove"
|
||||
(click.stop)="removeAt($index)"
|
||||
></button>
|
||||
</form-object>
|
||||
} @else {
|
||||
<form-control
|
||||
class="control"
|
||||
tuiTextfieldSize="m"
|
||||
[formControl]="$any(item)"
|
||||
[spec]="$any(spec.spec)"
|
||||
[@tuiHeightCollapse]="animation"
|
||||
[@tuiFadeIn]="animation"
|
||||
(remove)="removeAt($index)"
|
||||
/>
|
||||
}
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
@use '@taiga-ui/core/styles/taiga-ui-local' as taiga;
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.add {
|
||||
font-size: 1rem;
|
||||
padding: 0 1rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.object {
|
||||
display: block;
|
||||
position: relative;
|
||||
|
||||
&_open::after,
|
||||
&:last-child::after {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
@include taiga.transition(opacity);
|
||||
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -0.5rem;
|
||||
height: 1px;
|
||||
left: 3rem;
|
||||
right: 1rem;
|
||||
background: var(--tui-background-neutral-1);
|
||||
}
|
||||
}
|
||||
|
||||
.remove {
|
||||
margin: 0 0.375rem 0 auto;
|
||||
pointer-events: auto;
|
||||
|
||||
&::before {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.control {
|
||||
display: block;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
`,
|
||||
animations: [tuiFadeIn, tuiHeightCollapse, tuiParentStop],
|
||||
hostDirectives: [ControlDirective],
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
ReactiveFormsModule,
|
||||
TuiIcon,
|
||||
TuiTooltip,
|
||||
TuiLink,
|
||||
TuiError,
|
||||
TuiFieldErrorPipe,
|
||||
TuiButton,
|
||||
TuiTextfield,
|
||||
i18nPipe,
|
||||
HintPipe,
|
||||
MustachePipe,
|
||||
FormControlComponent,
|
||||
forwardRef(() => FormObjectComponent),
|
||||
],
|
||||
})
|
||||
export class FormArrayComponent {
|
||||
@Input({ required: true })
|
||||
spec!: IST.ValueSpecList
|
||||
|
||||
@HostBinding('@tuiParentStop')
|
||||
readonly animation = tuiToAnimationOptions(inject(TUI_ANIMATIONS_SPEED))
|
||||
readonly order = ERRORS
|
||||
readonly array = inject(FormArrayName)
|
||||
readonly open = new Map<AbstractControl, boolean>()
|
||||
|
||||
private warned = false
|
||||
private readonly formService = inject(FormService)
|
||||
private readonly destroyRef = inject(DestroyRef)
|
||||
private readonly dialog = inject(DialogService)
|
||||
|
||||
get canAdd(): boolean {
|
||||
return (
|
||||
!this.spec.disabled &&
|
||||
(!this.spec.maxLength ||
|
||||
this.spec.maxLength >= this.array.control.controls.length)
|
||||
)
|
||||
}
|
||||
|
||||
add() {
|
||||
if (!this.warned && this.spec.warning) {
|
||||
this.dialog
|
||||
.openConfirm({
|
||||
label: 'Warning',
|
||||
size: 's',
|
||||
data: {
|
||||
content: this.spec.warning as i18nKey,
|
||||
yes: 'Ok',
|
||||
no: 'Cancel',
|
||||
},
|
||||
})
|
||||
.pipe(filter(Boolean), takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(() => {
|
||||
this.addItem()
|
||||
})
|
||||
} else {
|
||||
this.addItem()
|
||||
}
|
||||
|
||||
this.warned = true
|
||||
}
|
||||
|
||||
removeAt(index: number) {
|
||||
this.removeItem(index)
|
||||
}
|
||||
|
||||
private removeItem(index: number) {
|
||||
this.open.delete(this.array.control.at(index))
|
||||
this.array.control.removeAt(index)
|
||||
}
|
||||
|
||||
private addItem() {
|
||||
this.array.control.insert(0, this.formService.getListItem(this.spec))
|
||||
this.open.set(this.array.control.at(0), true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
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 { IST } from '@start9labs/start-sdk'
|
||||
import { tuiAsControl, TuiControl } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TuiAlertService,
|
||||
TuiButton,
|
||||
TuiDialogContext,
|
||||
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'
|
||||
import { ControlDirective } from './control.directive'
|
||||
|
||||
export const ERRORS = [
|
||||
'required',
|
||||
'pattern',
|
||||
'notNumber',
|
||||
'numberNotInteger',
|
||||
'numberNotInRange',
|
||||
'listNotUnique',
|
||||
'listNotInRange',
|
||||
'listItemIssue',
|
||||
]
|
||||
|
||||
@Component({
|
||||
selector: 'form-control',
|
||||
template: `
|
||||
<ng-container *polymorpheusOutlet="controls[spec.type]" />
|
||||
<tui-error [error]="order | tuiFieldError | async" />
|
||||
@if (spec.warning || immutable) {
|
||||
<ng-template #warning let-completeWith="completeWith">
|
||||
{{ spec.warning }}
|
||||
@if (immutable) {
|
||||
<p>{{ 'This value cannot be changed once set' | i18n }}!</p>
|
||||
}
|
||||
<div [style.margin-top.rem]="0.5">
|
||||
<button
|
||||
tuiButton
|
||||
type="button"
|
||||
appearance="secondary-grayscale"
|
||||
size="s"
|
||||
[style.margin-inline-end.rem]="0.5"
|
||||
(click)="completeWith(false)"
|
||||
>
|
||||
{{ 'Continue' | i18n }}
|
||||
</button>
|
||||
<button
|
||||
tuiButton
|
||||
type="button"
|
||||
appearance="flat-grayscale"
|
||||
size="s"
|
||||
(click)="completeWith(true)"
|
||||
>
|
||||
{{ 'Cancel' | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [
|
||||
tuiAsControl(FormControlComponent),
|
||||
{
|
||||
provide: TUI_VALIDATION_ERRORS,
|
||||
deps: [FormControlComponent],
|
||||
useFactory: (control: FormControlComponent<ControlSpec, string>) => ({
|
||||
[TUI_FORMAT_ERROR]: 'Invalid file format',
|
||||
required: 'Required',
|
||||
pattern: (context: any) =>
|
||||
'patterns' in control.spec &&
|
||||
getText(control.spec, String(context.requiredPattern)),
|
||||
}),
|
||||
},
|
||||
],
|
||||
hostDirectives: [ControlDirective],
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
i18nPipe,
|
||||
PolymorpheusOutlet,
|
||||
TuiError,
|
||||
TuiFieldErrorPipe,
|
||||
TuiButton,
|
||||
],
|
||||
})
|
||||
export class FormControlComponent<
|
||||
T extends ControlSpec,
|
||||
V,
|
||||
> extends TuiControl<V | null> {
|
||||
private readonly destroyRef = inject(DestroyRef)
|
||||
private readonly alerts = inject(TuiAlertService)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
|
||||
protected readonly controls = CONTROLS
|
||||
|
||||
@Input({ required: true })
|
||||
spec!: T
|
||||
|
||||
@ViewChild('warning')
|
||||
warning?: TemplateRef<TuiDialogContext<boolean>>
|
||||
|
||||
warned = false
|
||||
readonly order = ERRORS
|
||||
|
||||
get immutable(): boolean {
|
||||
return 'immutable' in this.spec && this.spec.immutable
|
||||
}
|
||||
|
||||
onInput(value: V | null) {
|
||||
const previous = this.value()
|
||||
|
||||
if (!this.warned && this.warning) {
|
||||
this.alerts
|
||||
.open<boolean>(this.warning, {
|
||||
label: this.i18n.transform('Warning'),
|
||||
appearance: 'warning',
|
||||
closeable: false,
|
||||
autoClose: 0,
|
||||
})
|
||||
.pipe(filter(Boolean), takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(() => {
|
||||
this.onChange(previous)
|
||||
})
|
||||
}
|
||||
|
||||
this.warned = true
|
||||
this.onChange(value === '' ? null : value)
|
||||
}
|
||||
}
|
||||
|
||||
function getText({ patterns }: IST.ValueSpecText, pattern: unknown): string {
|
||||
return (
|
||||
patterns?.find(({ regex }) => String(regex) === pattern)?.description ||
|
||||
'Invalid format'
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Directive, inject, Injectable, OnDestroy, OnInit } from '@angular/core'
|
||||
import { ControlContainer, NgControl } from '@angular/forms'
|
||||
import { tuiInjectElement } from '@taiga-ui/cdk'
|
||||
|
||||
@Injectable()
|
||||
export class InvalidService {
|
||||
private readonly controls: ControlDirective[] = []
|
||||
|
||||
scrollIntoView() {
|
||||
this.controls.find(d => d.invalid)?.scrollIntoView()
|
||||
}
|
||||
|
||||
add(control: ControlDirective) {
|
||||
this.controls.push(control)
|
||||
}
|
||||
|
||||
remove(control: ControlDirective) {
|
||||
this.controls.splice(this.controls.indexOf(control), 1)
|
||||
}
|
||||
}
|
||||
|
||||
@Directive()
|
||||
export class ControlDirective implements OnInit, OnDestroy {
|
||||
private readonly service = inject(InvalidService, { optional: true })
|
||||
private readonly element = tuiInjectElement()
|
||||
private readonly control =
|
||||
inject(NgControl, { optional: true }) ||
|
||||
inject(ControlContainer, { optional: true })
|
||||
|
||||
get invalid(): boolean {
|
||||
return !!this.control?.invalid
|
||||
}
|
||||
|
||||
scrollIntoView() {
|
||||
this.element.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.service?.add(this)
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.service?.remove(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { KeyValuePipe } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
SkipSelf,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core'
|
||||
import { ControlContainer, ReactiveFormsModule } from '@angular/forms'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { TUI_DEFAULT_ERROR_MESSAGE } from '@taiga-ui/core'
|
||||
import { identity, of } from 'rxjs'
|
||||
|
||||
import { FilterHiddenPipe } from '../pipes/filter-hidden.pipe'
|
||||
import { FormArrayComponent } from './array.component'
|
||||
import { FormControlComponent } from './control.component'
|
||||
import { FormObjectComponent } from './object.component'
|
||||
import { FormUnionComponent } from './union.component'
|
||||
|
||||
@Component({
|
||||
selector: 'form-group',
|
||||
template: `
|
||||
@for (entry of spec | keyvalue: asIsOrder | filterHidden; track entry) {
|
||||
@switch (entry.value.type) {
|
||||
@case ('object') {
|
||||
<form-object
|
||||
class="g-form-control"
|
||||
[formGroupName]="entry.key"
|
||||
[spec]="$any(entry.value)"
|
||||
/>
|
||||
}
|
||||
@case ('union') {
|
||||
<form-union
|
||||
class="g-form-control"
|
||||
[formGroupName]="entry.key"
|
||||
[spec]="$any(entry.value)"
|
||||
/>
|
||||
}
|
||||
@case ('list') {
|
||||
<form-array [formArrayName]="entry.key" [spec]="$any(entry.value)" />
|
||||
}
|
||||
@default {
|
||||
<form-control
|
||||
class="g-form-control"
|
||||
[formControlName]="entry.key"
|
||||
[spec]="entry.value"
|
||||
/>
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
form-group .g-form-control:not(:first-child) {
|
||||
display: block;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
form-group .g-form-group {
|
||||
position: relative;
|
||||
padding-left: var(--tui-height-m);
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background: var(--tui-background-neutral-1);
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: 0;
|
||||
left: calc(1rem - 1px);
|
||||
bottom: 0.5rem;
|
||||
width: 2px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: 0.75rem;
|
||||
bottom: 0;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
form-group tui-tooltip {
|
||||
z-index: 1;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
`,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
viewProviders: [
|
||||
{
|
||||
provide: TUI_DEFAULT_ERROR_MESSAGE,
|
||||
useValue: of('Unknown error'),
|
||||
},
|
||||
{
|
||||
provide: ControlContainer,
|
||||
deps: [[new SkipSelf(), ControlContainer]],
|
||||
useFactory: identity,
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
KeyValuePipe,
|
||||
ReactiveFormsModule,
|
||||
FilterHiddenPipe,
|
||||
FormControlComponent,
|
||||
FormObjectComponent,
|
||||
FormArrayComponent,
|
||||
FormUnionComponent,
|
||||
],
|
||||
})
|
||||
export class FormGroupComponent {
|
||||
@Input() spec: IST.InputSpec = {}
|
||||
|
||||
asIsOrder() {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
forwardRef,
|
||||
inject,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core'
|
||||
import { ControlContainer } from '@angular/forms'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { TuiButton, TuiIcon } from '@taiga-ui/core'
|
||||
import { TuiExpand } from '@taiga-ui/experimental'
|
||||
import { TuiTooltip } from '@taiga-ui/kit'
|
||||
|
||||
import { ControlDirective } from './control.directive'
|
||||
import { FormGroupComponent } from './group.component'
|
||||
|
||||
@Component({
|
||||
selector: 'form-object',
|
||||
template: `
|
||||
<h3 class="title" (click)="toggle()">
|
||||
<button
|
||||
tuiIconButton
|
||||
size="s"
|
||||
iconStart="@tui.chevron-down"
|
||||
type="button"
|
||||
class="button"
|
||||
[class.button_open]="open"
|
||||
[style.border-radius.%]="100"
|
||||
[appearance]="invalid ? 'primary-destructive' : 'secondary'"
|
||||
></button>
|
||||
<ng-content />
|
||||
{{ spec.name }}
|
||||
@if (spec.description) {
|
||||
<tui-icon [tuiTooltip]="spec.description" (click.stop)="(0)" />
|
||||
}
|
||||
</h3>
|
||||
<tui-expand class="expand" [expanded]="open">
|
||||
<div class="g-form-group" [class.g-form-group_invalid]="invalid">
|
||||
<form-group [spec]="spec.spec" />
|
||||
</div>
|
||||
</tui-expand>
|
||||
`,
|
||||
styles: `
|
||||
@use '@taiga-ui/core/styles/taiga-ui-local' as taiga;
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.title {
|
||||
position: relative;
|
||||
height: var(--tui-height-l);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font: var(--tui-font-text-l);
|
||||
font-weight: bold;
|
||||
margin: 0 0 -0.75rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
@include taiga.transition(transform);
|
||||
|
||||
margin-right: 1rem;
|
||||
|
||||
&_open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.expand {
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.g-form-group {
|
||||
padding-top: 0.75rem;
|
||||
|
||||
&_invalid::before,
|
||||
&_invalid::after {
|
||||
background: var(--tui-status-negative-pale);
|
||||
}
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
hostDirectives: [ControlDirective],
|
||||
imports: [
|
||||
TuiButton,
|
||||
TuiIcon,
|
||||
TuiTooltip,
|
||||
TuiExpand,
|
||||
forwardRef(() => FormGroupComponent),
|
||||
],
|
||||
})
|
||||
export class FormObjectComponent {
|
||||
@Input({ required: true })
|
||||
spec!: IST.ValueSpecObject
|
||||
|
||||
@Input()
|
||||
open = false
|
||||
|
||||
@Output()
|
||||
readonly openChange = new EventEmitter<boolean>()
|
||||
|
||||
private readonly container = inject(ControlContainer)
|
||||
|
||||
get invalid() {
|
||||
return !this.container.valid && this.container.touched
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.open = !this.open
|
||||
this.openChange.emit(this.open)
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,49 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
forwardRef,
|
||||
inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
} from '@angular/core'
|
||||
import { ControlContainer, FormGroupName } from '@angular/forms'
|
||||
import {
|
||||
ControlContainer,
|
||||
FormGroupName,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { tuiPure, TuiValueChanges } from '@taiga-ui/cdk'
|
||||
import { TuiElasticContainer } from '@taiga-ui/kit'
|
||||
import { FormService } from 'src/app/services/form.service'
|
||||
import { tuiPure } from '@taiga-ui/cdk'
|
||||
|
||||
import { FormControlComponent } from './control.component'
|
||||
import { FormGroupComponent } from './group.component'
|
||||
|
||||
@Component({
|
||||
selector: 'form-union',
|
||||
templateUrl: './form-union.component.html',
|
||||
styleUrls: ['./form-union.component.scss'],
|
||||
template: `
|
||||
<form-control
|
||||
[spec]="selectSpec"
|
||||
formControlName="selection"
|
||||
(tuiValueChanges)="onUnion($event)"
|
||||
/>
|
||||
<tui-elastic-container class="g-form-group" formGroupName="value">
|
||||
<form-group
|
||||
class="group"
|
||||
[spec]="(union && spec.variants[union]?.spec) || {}"
|
||||
/>
|
||||
</tui-elastic-container>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.group {
|
||||
display: block;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
viewProviders: [
|
||||
{
|
||||
@@ -21,7 +51,13 @@ import { tuiPure } from '@taiga-ui/cdk'
|
||||
useExisting: FormGroupName,
|
||||
},
|
||||
],
|
||||
standalone: false,
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
TuiValueChanges,
|
||||
TuiElasticContainer,
|
||||
FormControlComponent,
|
||||
forwardRef(() => FormGroupComponent),
|
||||
],
|
||||
})
|
||||
export class FormUnionComponent implements OnChanges {
|
||||
@Input({ required: true })
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Directive, ElementRef, inject, OnDestroy, OnInit } from '@angular/core'
|
||||
import { ControlContainer, NgControl } from '@angular/forms'
|
||||
import { InvalidService } from './invalid.service'
|
||||
|
||||
@Directive({
|
||||
selector: 'form-control, form-array, form-object',
|
||||
standalone: false,
|
||||
})
|
||||
export class ControlDirective implements OnInit, OnDestroy {
|
||||
private readonly invalidService = inject(InvalidService, { optional: true })
|
||||
private readonly element: ElementRef<HTMLElement> = inject(ElementRef)
|
||||
private readonly control =
|
||||
inject(NgControl, { optional: true }) ||
|
||||
inject(ControlContainer, { optional: true })
|
||||
|
||||
get invalid(): boolean {
|
||||
return !!this.control?.invalid
|
||||
}
|
||||
|
||||
scrollIntoView() {
|
||||
this.element.nativeElement.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.invalidService?.add(this)
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.invalidService?.remove(this)
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { inject } from '@angular/core'
|
||||
import { FormControlComponent } from './form-control/form-control.component'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
|
||||
export abstract class Control<
|
||||
Spec extends Exclude<IST.ValueSpec, IST.ValueSpecHidden>,
|
||||
Value,
|
||||
> {
|
||||
private readonly control: FormControlComponent<Spec, Value> =
|
||||
inject(FormControlComponent)
|
||||
|
||||
get invalid(): boolean {
|
||||
return this.control.touched && this.control.invalid
|
||||
}
|
||||
|
||||
get spec(): Spec {
|
||||
return this.control.spec
|
||||
}
|
||||
|
||||
get readOnly(): boolean {
|
||||
const def =
|
||||
'default' in this.spec &&
|
||||
this.spec.default != null &&
|
||||
this.spec.default !== this.value
|
||||
|
||||
return (
|
||||
!!this.value &&
|
||||
!def &&
|
||||
!!this.control.control?.pristine &&
|
||||
this.control.immutable
|
||||
)
|
||||
}
|
||||
|
||||
get value(): Value | null {
|
||||
return this.control.value
|
||||
}
|
||||
|
||||
set value(value: Value | null) {
|
||||
this.control.onInput(value)
|
||||
}
|
||||
|
||||
onFocus(focused: boolean) {
|
||||
this.control.onFocus(focused)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { TuiIcon, TuiTextfield } from '@taiga-ui/core'
|
||||
import { TuiInputColor, TuiTooltip } from '@taiga-ui/kit'
|
||||
|
||||
import { Control } from './control'
|
||||
import { HintPipe } from '../pipes/hint.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'form-color',
|
||||
template: `
|
||||
<tui-textfield iconStart=" " [tuiTextfieldCleaner]="false">
|
||||
@if (spec.name) {
|
||||
<label tuiLabel>
|
||||
{{ spec.name }}
|
||||
@if (spec.required) {
|
||||
<span>*</span>
|
||||
}
|
||||
</label>
|
||||
}
|
||||
<input
|
||||
placeholder="#000000"
|
||||
tuiInputColor
|
||||
[invalid]="control.invalid()"
|
||||
[readOnly]="readOnly"
|
||||
[disabled]="!!spec.disabled"
|
||||
[(ngModel)]="value"
|
||||
(blur)="control.onTouched()"
|
||||
/>
|
||||
@if (spec | hint; as hint) {
|
||||
<tui-icon [tuiTooltip]="hint" />
|
||||
}
|
||||
</tui-textfield>
|
||||
`,
|
||||
imports: [
|
||||
FormsModule,
|
||||
TuiTextfield,
|
||||
TuiInputColor,
|
||||
TuiIcon,
|
||||
TuiTooltip,
|
||||
HintPipe,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FormColorComponent extends Control<IST.ValueSpecColor, string> {}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { inject } from '@angular/core'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { TuiControl } from '@taiga-ui/cdk'
|
||||
|
||||
export type ControlSpec = Exclude<
|
||||
IST.ValueSpec,
|
||||
| IST.ValueSpecHidden
|
||||
| IST.ValueSpecList
|
||||
| IST.ValueSpecUnion
|
||||
| IST.ValueSpecObject
|
||||
>
|
||||
|
||||
export abstract class Control<Spec extends ControlSpec, Value> {
|
||||
public readonly control: any = inject(TuiControl)
|
||||
|
||||
get spec(): Spec {
|
||||
return this.control.spec
|
||||
}
|
||||
|
||||
get readOnly(): boolean {
|
||||
const def =
|
||||
'default' in this.spec &&
|
||||
this.spec.default != null &&
|
||||
this.spec.default !== this.value
|
||||
|
||||
return (
|
||||
!!this.value &&
|
||||
!def &&
|
||||
!!this.control['control']?.pristine &&
|
||||
this.control.immutable
|
||||
)
|
||||
}
|
||||
|
||||
get value(): Value | null {
|
||||
return this.control.value()
|
||||
}
|
||||
|
||||
set value(value: Value | null) {
|
||||
this.control.onInput(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { FormColorComponent } from './color.component'
|
||||
import { FormDatetimeComponent } from './datetime.component'
|
||||
import { FormFileComponent } from './file.component'
|
||||
import { FormMultiselectComponent } from './multiselect.component'
|
||||
import { FormNumberComponent } from './number.component'
|
||||
import { FormSelectComponent } from './select.component'
|
||||
import { FormTextComponent } from './text.component'
|
||||
import { FormTextareaComponent } from './textarea.component'
|
||||
import { FormToggleComponent } from './toggle.component'
|
||||
|
||||
export const CONTROLS = {
|
||||
color: new PolymorpheusComponent(FormColorComponent),
|
||||
datetime: new PolymorpheusComponent(FormDatetimeComponent),
|
||||
file: new PolymorpheusComponent(FormFileComponent),
|
||||
number: new PolymorpheusComponent(FormNumberComponent),
|
||||
select: new PolymorpheusComponent(FormSelectComponent),
|
||||
multiselect: new PolymorpheusComponent(FormMultiselectComponent),
|
||||
text: new PolymorpheusComponent(FormTextComponent),
|
||||
textarea: new PolymorpheusComponent(FormTextareaComponent),
|
||||
toggle: new PolymorpheusComponent(FormToggleComponent),
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import {
|
||||
TUI_FIRST_DAY,
|
||||
TUI_LAST_DAY,
|
||||
TuiDay,
|
||||
TuiMapperPipe,
|
||||
tuiPure,
|
||||
TuiTime,
|
||||
} from '@taiga-ui/cdk'
|
||||
import { TuiIcon, TuiTextfield } from '@taiga-ui/core'
|
||||
import {
|
||||
TuiInputDate,
|
||||
TuiInputDateTime,
|
||||
TuiInputTime,
|
||||
TuiTooltip,
|
||||
} from '@taiga-ui/kit'
|
||||
|
||||
import { Control } from './control'
|
||||
import { HintPipe } from '../pipes/hint.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'form-datetime',
|
||||
template: `
|
||||
<!--
|
||||
TODO: Move @switch down to only affect <input ... /> after fix:
|
||||
https://github.com/taiga-family/taiga-ui/issues/11780
|
||||
-->
|
||||
@switch (spec.inputmode) {
|
||||
@case ('time') {
|
||||
<tui-textfield (tuiActiveZoneChange)="!$event && control.onTouched()">
|
||||
@if (spec.name) {
|
||||
<label tuiLabel>
|
||||
{{ spec.name }}
|
||||
@if (spec.required) {
|
||||
<span>*</span>
|
||||
}
|
||||
</label>
|
||||
}
|
||||
<input
|
||||
tuiInputTime
|
||||
type="time"
|
||||
[invalid]="control.invalid()"
|
||||
[readOnly]="readOnly"
|
||||
[disabled]="!!spec.disabled"
|
||||
[ngModel]="getTime(value)"
|
||||
(ngModelChange)="value = $event?.toString() || null"
|
||||
(blur)="control.onTouched()"
|
||||
/>
|
||||
@if (spec | hint; as hint) {
|
||||
<tui-icon [tuiTooltip]="hint" />
|
||||
}
|
||||
</tui-textfield>
|
||||
}
|
||||
@case ('date') {
|
||||
<tui-textfield (tuiActiveZoneChange)="!$event && control.onTouched()">
|
||||
@if (spec.name) {
|
||||
<label tuiLabel>
|
||||
{{ spec.name }}
|
||||
@if (spec.required) {
|
||||
<span>*</span>
|
||||
}
|
||||
</label>
|
||||
}
|
||||
<input
|
||||
tuiInputDate
|
||||
type="date"
|
||||
[invalid]="control.invalid()"
|
||||
[readOnly]="readOnly"
|
||||
[disabled]="!!spec.disabled"
|
||||
[min]="spec.min ? (spec.min | tuiMapper: getLimit)[0] : min"
|
||||
[max]="spec.max ? (spec.max | tuiMapper: getLimit)[0] : max"
|
||||
[(ngModel)]="value"
|
||||
(blur)="control.onTouched()"
|
||||
/>
|
||||
@if (spec | hint; as hint) {
|
||||
<tui-icon [tuiTooltip]="hint" />
|
||||
}
|
||||
<tui-calendar *tuiTextfieldDropdown />
|
||||
</tui-textfield>
|
||||
}
|
||||
@case ('datetime-local') {
|
||||
<tui-textfield (tuiActiveZoneChange)="!$event && control.onTouched()">
|
||||
@if (spec.name) {
|
||||
<label tuiLabel>
|
||||
{{ spec.name }}
|
||||
@if (spec.required) {
|
||||
<span>*</span>
|
||||
}
|
||||
</label>
|
||||
}
|
||||
<input
|
||||
tuiInputDateTime
|
||||
type="datetime-local"
|
||||
[invalid]="control.invalid()"
|
||||
[readOnly]="readOnly"
|
||||
[disabled]="!!spec.disabled"
|
||||
[min]="spec.min ? (spec.min | tuiMapper: getLimit)[0] : min"
|
||||
[max]="spec.max ? (spec.max | tuiMapper: getLimit)[0] : max"
|
||||
[(ngModel)]="value"
|
||||
(blur)="control.onTouched()"
|
||||
/>
|
||||
@if (spec | hint; as hint) {
|
||||
<tui-icon [tuiTooltip]="hint" />
|
||||
}
|
||||
<tui-calendar *tuiTextfieldDropdown />
|
||||
</tui-textfield>
|
||||
}
|
||||
}
|
||||
`,
|
||||
imports: [
|
||||
FormsModule,
|
||||
TuiTextfield,
|
||||
TuiIcon,
|
||||
TuiTooltip,
|
||||
TuiInputTime,
|
||||
TuiInputDate,
|
||||
TuiMapperPipe,
|
||||
TuiInputDateTime,
|
||||
HintPipe,
|
||||
],
|
||||
})
|
||||
export class FormDatetimeComponent extends Control<
|
||||
IST.ValueSpecDatetime,
|
||||
string
|
||||
> {
|
||||
readonly min = TUI_FIRST_DAY
|
||||
readonly max = TUI_LAST_DAY
|
||||
|
||||
@tuiPure
|
||||
getTime(value: string | null) {
|
||||
return value ? TuiTime.fromString(value) : null
|
||||
}
|
||||
|
||||
getLimit(limit: string): [TuiDay, TuiTime] {
|
||||
return [
|
||||
TuiDay.jsonParse(limit.slice(0, 10)),
|
||||
limit.length === 10
|
||||
? new TuiTime(0, 0)
|
||||
: TuiTime.fromString(limit.slice(-5)),
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { TuiButton, TuiIcon } from '@taiga-ui/core'
|
||||
import { TuiChip, TuiFileLike, TuiFiles, TuiTooltip } from '@taiga-ui/kit'
|
||||
|
||||
import { Control } from './control'
|
||||
|
||||
@Component({
|
||||
selector: 'form-file',
|
||||
template: `
|
||||
<label tuiInputFiles>
|
||||
<input
|
||||
tuiInputFiles
|
||||
[invalid]="control.invalid()"
|
||||
[accept]="spec.extensions.join(',')"
|
||||
[(ngModel)]="value"
|
||||
(blur)="control.onTouched()"
|
||||
/>
|
||||
<ng-template let-drop>
|
||||
<div class="template" [class.template_hidden]="drop">
|
||||
<div class="label">
|
||||
{{ spec.name }}
|
||||
@if (spec.required) {
|
||||
<span>*</span>
|
||||
}
|
||||
@if (spec.description) {
|
||||
<tui-icon [tuiTooltip]="spec.description" />
|
||||
}
|
||||
</div>
|
||||
@if (value) {
|
||||
<tui-chip>
|
||||
{{ value.name }}
|
||||
<button
|
||||
tuiIconButton
|
||||
type="button"
|
||||
appearance="icon"
|
||||
size="xs"
|
||||
iconStart="@tui.x"
|
||||
(click.stop)="value = null"
|
||||
>
|
||||
{{ 'Delete' | i18n }}
|
||||
</button>
|
||||
</tui-chip>
|
||||
} @else {
|
||||
<small>{{ 'Click or drop file here' | i18n }}</small>
|
||||
}
|
||||
</div>
|
||||
<div class="drop" [class.drop_hidden]="!drop">
|
||||
{{ 'Drop file here' | i18n }}
|
||||
</div>
|
||||
</ng-template>
|
||||
</label>
|
||||
`,
|
||||
styles: `
|
||||
@use '@taiga-ui/core/styles/taiga-ui-local' as taiga;
|
||||
|
||||
.template {
|
||||
@include taiga.transition(opacity);
|
||||
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 0.5rem;
|
||||
font: var(--tui-font-text-m);
|
||||
font-weight: bold;
|
||||
|
||||
&_hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.drop {
|
||||
@include taiga.fullsize();
|
||||
@include taiga.transition(opacity);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
|
||||
&_hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
small {
|
||||
max-width: 50%;
|
||||
font-weight: normal;
|
||||
color: var(--tui-text-secondary);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
tui-chip {
|
||||
z-index: 1;
|
||||
margin: -0.25rem -0.25rem -0.25rem auto;
|
||||
pointer-events: auto;
|
||||
}
|
||||
`,
|
||||
imports: [
|
||||
FormsModule,
|
||||
TuiFiles,
|
||||
TuiIcon,
|
||||
TuiTooltip,
|
||||
TuiChip,
|
||||
TuiButton,
|
||||
i18nPipe,
|
||||
],
|
||||
})
|
||||
export class FormFileComponent extends Control<
|
||||
IST.ValueSpecFile,
|
||||
TuiFileLike
|
||||
> {}
|
||||
@@ -1,13 +1,49 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
import { tuiPure } from '@taiga-ui/cdk'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { invert } from '@start9labs/shared'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { tuiPure } from '@taiga-ui/cdk'
|
||||
import { TuiIcon, TuiTextfield } from '@taiga-ui/core'
|
||||
import { TuiMultiSelect, TuiTooltip } from '@taiga-ui/kit'
|
||||
|
||||
import { Control } from './control'
|
||||
import { HintPipe } from '../pipes/hint.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'form-multiselect',
|
||||
templateUrl: './form-multiselect.component.html',
|
||||
standalone: false,
|
||||
template: `
|
||||
<tui-textfield multi [disabledItemHandler]="disabledItemHandler">
|
||||
@if (spec.name) {
|
||||
<label tuiLabel>{{ spec.name }}</label>
|
||||
}
|
||||
<select
|
||||
tuiMultiSelect
|
||||
[invalid]="control.invalid()"
|
||||
[disabled]="disabled"
|
||||
[readOnly]="readOnly"
|
||||
[items]="items"
|
||||
[(ngModel)]="selected"
|
||||
(blur)="control.onTouched()"
|
||||
></select>
|
||||
@if (spec | hint; as hint) {
|
||||
<tui-icon [tuiTooltip]="hint" />
|
||||
}
|
||||
</tui-textfield>
|
||||
`,
|
||||
styles: `
|
||||
// TODO: Remove after Taiga UI update
|
||||
:host ::ng-deep .t-input {
|
||||
pointer-events: none;
|
||||
}
|
||||
`,
|
||||
imports: [
|
||||
FormsModule,
|
||||
TuiTextfield,
|
||||
TuiMultiSelect,
|
||||
TuiIcon,
|
||||
TuiTooltip,
|
||||
HintPipe,
|
||||
],
|
||||
})
|
||||
export class FormMultiselectComponent extends Control<
|
||||
IST.ValueSpecMultiselect,
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { TuiIcon, TuiNumberFormat, TuiTextfield } from '@taiga-ui/core'
|
||||
import { TuiInputNumber, TuiTooltip } from '@taiga-ui/kit'
|
||||
|
||||
import { Control } from './control'
|
||||
import { HintPipe } from '../pipes/hint.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'form-number',
|
||||
template: `
|
||||
<tui-textfield [tuiNumberFormat]="{ precision, decimalMode: 'not-zero' }">
|
||||
@if (spec.name) {
|
||||
<label tuiLabel>
|
||||
{{ spec.name }}
|
||||
@if (spec.required) {
|
||||
<span>*</span>
|
||||
}
|
||||
</label>
|
||||
}
|
||||
<input
|
||||
tuiInputNumber
|
||||
[postfix]="spec.units ? ' ' + spec.units : ''"
|
||||
[min]="spec.min"
|
||||
[max]="spec.max"
|
||||
[step]="spec.step || 0"
|
||||
[invalid]="control.invalid()"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[placeholder]="spec.placeholder || ''"
|
||||
[(ngModel)]="value"
|
||||
(blur)="control.onTouched()"
|
||||
/>
|
||||
@if (spec | hint; as hint) {
|
||||
<tui-icon [tuiTooltip]="hint" />
|
||||
}
|
||||
</tui-textfield>
|
||||
`,
|
||||
imports: [
|
||||
FormsModule,
|
||||
TuiTextfield,
|
||||
TuiInputNumber,
|
||||
TuiNumberFormat,
|
||||
TuiIcon,
|
||||
TuiTooltip,
|
||||
HintPipe,
|
||||
],
|
||||
})
|
||||
export class FormNumberComponent extends Control<IST.ValueSpecNumber, number> {
|
||||
get precision(): number {
|
||||
return this.spec.integer ? 0 : Infinity
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Component, inject } from '@angular/core'
|
||||
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 { Control } from './control'
|
||||
import { HintPipe } from '../pipes/hint.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'form-select',
|
||||
template: `
|
||||
<tui-textfield
|
||||
[tuiTextfieldCleaner]="false"
|
||||
[disabledItemHandler]="disabledItemHandler"
|
||||
(tuiActiveZoneChange)="!$event && control.onTouched()"
|
||||
>
|
||||
@if (spec.name) {
|
||||
<label tuiLabel>{{ spec.name }} *</label>
|
||||
}
|
||||
@if (mobile) {
|
||||
<select
|
||||
tuiSelect
|
||||
[disabled]="disabled"
|
||||
[readOnly]="readOnly"
|
||||
[invalid]="control.invalid()"
|
||||
[placeholder]="spec.name"
|
||||
[items]="items"
|
||||
[(ngModel)]="selected"
|
||||
></select>
|
||||
} @else {
|
||||
<input
|
||||
tuiSelect
|
||||
[disabled]="disabled"
|
||||
[readOnly]="readOnly"
|
||||
[invalid]="control.invalid()"
|
||||
[placeholder]="spec.name"
|
||||
[(ngModel)]="selected"
|
||||
/>
|
||||
}
|
||||
<tui-data-list-wrapper *tuiTextfieldDropdown new [items]="items" />
|
||||
@if (spec | hint; as hint) {
|
||||
<tui-icon [tuiTooltip]="hint" />
|
||||
}
|
||||
</tui-textfield>
|
||||
`,
|
||||
imports: [
|
||||
FormsModule,
|
||||
TuiTextfield,
|
||||
TuiSelect,
|
||||
TuiDataListWrapper,
|
||||
TuiIcon,
|
||||
TuiTooltip,
|
||||
HintPipe,
|
||||
],
|
||||
})
|
||||
export class FormSelectComponent extends Control<IST.ValueSpecSelect, string> {
|
||||
private readonly inverted = invert(this.spec.values)
|
||||
|
||||
protected readonly mobile = inject(TUI_IS_MOBILE)
|
||||
protected readonly items = Object.values(this.spec.values)
|
||||
protected readonly disabledItemHandler = (item: string) =>
|
||||
Array.isArray(this.spec.disabled) &&
|
||||
!!this.inverted[item] &&
|
||||
this.spec.disabled.includes(this.inverted[item]!)
|
||||
|
||||
get disabled(): boolean {
|
||||
return typeof this.spec.disabled === 'string'
|
||||
}
|
||||
|
||||
get selected(): string | null {
|
||||
return (this.value && this.spec.values[this.value]) || null
|
||||
}
|
||||
|
||||
set selected(value: string | null) {
|
||||
this.value = (value && this.inverted[value]) || null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { IST, utils } from '@start9labs/start-sdk'
|
||||
import { tuiInjectElement } from '@taiga-ui/cdk'
|
||||
import { TuiButton, TuiIcon, TuiTextfield } from '@taiga-ui/core'
|
||||
import { TuiTooltip } from '@taiga-ui/kit'
|
||||
|
||||
import { Control } from './control'
|
||||
import { HintPipe } from '../pipes/hint.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'form-text',
|
||||
template: `
|
||||
<tui-textfield>
|
||||
@if (spec.name) {
|
||||
<label tuiLabel>
|
||||
{{ spec.name }}
|
||||
@if (spec.required) {
|
||||
<span>*</span>
|
||||
}
|
||||
</label>
|
||||
}
|
||||
<input
|
||||
tuiTextfield
|
||||
[attr.inputmode]="spec.inputmode"
|
||||
[attr.minLength]="spec.minLength"
|
||||
[attr.maxLength]="spec.maxLength"
|
||||
[style.-webkit-text-security]="spec.masked && masked ? 'disc' : null"
|
||||
[placeholder]="spec.placeholder || ''"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[invalid]="control.invalid()"
|
||||
[(ngModel)]="value"
|
||||
(blur)="control.onTouched()"
|
||||
/>
|
||||
@if (spec.generate) {
|
||||
<button
|
||||
tuiIconButton
|
||||
type="button"
|
||||
appearance="icon"
|
||||
title="Generate"
|
||||
size="xs"
|
||||
iconStart="@tui.refresh-ccw"
|
||||
(click)="generate()"
|
||||
></button>
|
||||
}
|
||||
@if (spec.masked) {
|
||||
<button
|
||||
tuiIconButton
|
||||
type="button"
|
||||
appearance="icon"
|
||||
title="Toggle masking"
|
||||
size="xs"
|
||||
[iconStart]="masked ? '@tui.eye' : '@tui.eye-off'"
|
||||
(click)="masked = !masked"
|
||||
></button>
|
||||
}
|
||||
<button
|
||||
tuiIconButton
|
||||
type="button"
|
||||
iconStart="@tui.trash"
|
||||
appearance="icon"
|
||||
size="xs"
|
||||
title="Remove"
|
||||
class="remove"
|
||||
(click)="remove()"
|
||||
></button>
|
||||
@if (spec | hint; as hint) {
|
||||
<tui-icon [tuiTooltip]="hint" />
|
||||
}
|
||||
</tui-textfield>
|
||||
`,
|
||||
styles: `
|
||||
.remove {
|
||||
display: none;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
:host-context(form-array > form-control > :host) .remove {
|
||||
display: flex;
|
||||
}
|
||||
`,
|
||||
imports: [
|
||||
FormsModule,
|
||||
TuiTextfield,
|
||||
TuiButton,
|
||||
TuiIcon,
|
||||
TuiTooltip,
|
||||
HintPipe,
|
||||
],
|
||||
})
|
||||
export class FormTextComponent extends Control<IST.ValueSpecText, string> {
|
||||
private readonly el = tuiInjectElement()
|
||||
|
||||
masked = true
|
||||
|
||||
generate() {
|
||||
this.value = utils.getDefaultString(this.spec.generate || '')
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.el.dispatchEvent(new CustomEvent('remove', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { TuiIcon, TuiTextfield } from '@taiga-ui/core'
|
||||
import { TuiTextarea, TuiTooltip } from '@taiga-ui/kit'
|
||||
|
||||
import { Control } from './control'
|
||||
import { HintPipe } from '../pipes/hint.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'form-textarea',
|
||||
template: `
|
||||
<tui-textfield>
|
||||
@if (spec.name) {
|
||||
<label tuiLabel>
|
||||
{{ spec.name }}
|
||||
@if (spec.required) {
|
||||
<span>*</span>
|
||||
}
|
||||
</label>
|
||||
}
|
||||
<textarea
|
||||
placeholder="Placeholder"
|
||||
tuiTextarea
|
||||
[max]="6"
|
||||
[min]="3"
|
||||
[attr.maxLength]="spec.maxLength"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[placeholder]="spec.placeholder || ''"
|
||||
[invalid]="control.invalid()"
|
||||
[(ngModel)]="value"
|
||||
(blur)="control.onTouched()"
|
||||
></textarea>
|
||||
@if (spec | hint; as hint) {
|
||||
<tui-icon [tuiTooltip]="hint" />
|
||||
}
|
||||
</tui-textfield>
|
||||
`,
|
||||
imports: [
|
||||
FormsModule,
|
||||
TuiTextfield,
|
||||
TuiTextarea,
|
||||
TuiIcon,
|
||||
TuiTooltip,
|
||||
HintPipe,
|
||||
],
|
||||
})
|
||||
export class FormTextareaComponent extends Control<
|
||||
IST.ValueSpecTextarea,
|
||||
string
|
||||
> {}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { TuiIcon } from '@taiga-ui/core'
|
||||
import { TuiSwitch, TuiTooltip } from '@taiga-ui/kit'
|
||||
|
||||
import { Control } from './control'
|
||||
import { HintPipe } from '../pipes/hint.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'form-toggle',
|
||||
template: `
|
||||
{{ spec.name }}
|
||||
@if (spec.description || spec.disabled) {
|
||||
<tui-icon [tuiTooltip]="spec | hint" />
|
||||
}
|
||||
<input
|
||||
tuiSwitch
|
||||
type="checkbox"
|
||||
size="m"
|
||||
[disabled]="!!spec.disabled || readOnly"
|
||||
[showIcons]="false"
|
||||
[(ngModel)]="value"
|
||||
(blur)="control.onTouched()"
|
||||
/>
|
||||
`,
|
||||
host: { class: 'g-toggle' },
|
||||
imports: [TuiIcon, TuiTooltip, HintPipe, TuiSwitch, FormsModule],
|
||||
})
|
||||
export class FormToggleComponent extends Control<
|
||||
IST.ValueSpecToggle,
|
||||
boolean
|
||||
> {}
|
||||
@@ -1,57 +0,0 @@
|
||||
<div class="label">
|
||||
{{ spec.name }}
|
||||
@if (spec.description || spec.disabled) {
|
||||
<tui-icon [tuiTooltip]="spec | hint" />
|
||||
}
|
||||
<button
|
||||
tuiLink
|
||||
type="button"
|
||||
class="add"
|
||||
[disabled]="!canAdd"
|
||||
(click)="add()"
|
||||
>
|
||||
+ {{ 'Add' | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<tui-error [error]="order | tuiFieldError | async" />
|
||||
|
||||
@for (item of array.control.controls; track item) {
|
||||
@if (spec.spec.type === 'object') {
|
||||
<form-object
|
||||
class="object"
|
||||
[class.object_open]="!!open.get(item)"
|
||||
[formGroup]="$any(item)"
|
||||
[spec]="$any(spec.spec)"
|
||||
[@tuiHeightCollapse]="animation"
|
||||
[@tuiFadeIn]="animation"
|
||||
[open]="!!open.get(item)"
|
||||
(openChange)="open.set(item, $event)"
|
||||
>
|
||||
{{ item.value | mustache: $any(spec.spec).displayAs }}
|
||||
<ng-container *ngTemplateOutlet="remove" />
|
||||
</form-object>
|
||||
} @else {
|
||||
<form-control
|
||||
class="control"
|
||||
tuiTextfieldSize="m"
|
||||
[tuiTextfieldLabelOutside]="true"
|
||||
[tuiTextfieldIcon]="remove"
|
||||
[formControl]="$any(item)"
|
||||
[spec]="$any(spec.spec)"
|
||||
[@tuiHeightCollapse]="animation"
|
||||
[@tuiFadeIn]="animation"
|
||||
/>
|
||||
}
|
||||
<ng-template #remove>
|
||||
<button
|
||||
tuiIconButton
|
||||
type="button"
|
||||
class="remove"
|
||||
iconStart="@tui.trash"
|
||||
appearance="icon"
|
||||
size="m"
|
||||
title="Remove"
|
||||
(click.stop)="removeAt($index)"
|
||||
></button>
|
||||
</ng-template>
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
@use '@taiga-ui/core/styles/taiga-ui-local' as taiga;
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.add {
|
||||
font-size: 1rem;
|
||||
padding: 0 1rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.object {
|
||||
display: block;
|
||||
position: relative;
|
||||
|
||||
&_open::after,
|
||||
&:last-child::after {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
@include taiga.transition(opacity);
|
||||
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -0.5rem;
|
||||
height: 1px;
|
||||
left: 3rem;
|
||||
right: 1rem;
|
||||
background: var(--tui-background-neutral-1);
|
||||
}
|
||||
}
|
||||
|
||||
.remove {
|
||||
margin-left: auto;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.control {
|
||||
display: block;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import {
|
||||
Component,
|
||||
DestroyRef,
|
||||
HostBinding,
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
||||
import { AbstractControl, FormArrayName } from '@angular/forms'
|
||||
import {
|
||||
TUI_ANIMATIONS_SPEED,
|
||||
tuiFadeIn,
|
||||
tuiHeightCollapse,
|
||||
tuiParentStop,
|
||||
tuiToAnimationOptions,
|
||||
} from '@taiga-ui/core'
|
||||
import { filter } from 'rxjs'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { FormService } from 'src/app/services/form.service'
|
||||
import { ERRORS } from '../form-group/form-group.component'
|
||||
import { DialogService, i18nKey } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'form-array',
|
||||
templateUrl: './form-array.component.html',
|
||||
styleUrls: ['./form-array.component.scss'],
|
||||
animations: [tuiFadeIn, tuiHeightCollapse, tuiParentStop],
|
||||
standalone: false,
|
||||
})
|
||||
export class FormArrayComponent {
|
||||
@Input({ required: true })
|
||||
spec!: IST.ValueSpecList
|
||||
|
||||
@HostBinding('@tuiParentStop')
|
||||
readonly animation = tuiToAnimationOptions(inject(TUI_ANIMATIONS_SPEED))
|
||||
readonly order = ERRORS
|
||||
readonly array = inject(FormArrayName)
|
||||
readonly open = new Map<AbstractControl, boolean>()
|
||||
|
||||
private warned = false
|
||||
private readonly formService = inject(FormService)
|
||||
private readonly destroyRef = inject(DestroyRef)
|
||||
private readonly dialog = inject(DialogService)
|
||||
|
||||
get canAdd(): boolean {
|
||||
return (
|
||||
!this.spec.disabled &&
|
||||
(!this.spec.maxLength ||
|
||||
this.spec.maxLength >= this.array.control.controls.length)
|
||||
)
|
||||
}
|
||||
|
||||
add() {
|
||||
if (!this.warned && this.spec.warning) {
|
||||
this.dialog
|
||||
.openConfirm<boolean>({
|
||||
label: 'Warning',
|
||||
size: 's',
|
||||
data: {
|
||||
content: this.spec.warning as i18nKey,
|
||||
yes: 'Ok',
|
||||
no: 'Cancel',
|
||||
},
|
||||
})
|
||||
.pipe(filter(Boolean), takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(() => {
|
||||
this.addItem()
|
||||
})
|
||||
} else {
|
||||
this.addItem()
|
||||
}
|
||||
|
||||
this.warned = true
|
||||
}
|
||||
|
||||
removeAt(index: number) {
|
||||
this.removeItem(index)
|
||||
}
|
||||
|
||||
private removeItem(index: number) {
|
||||
this.open.delete(this.array.control.at(index))
|
||||
this.array.control.removeAt(index)
|
||||
}
|
||||
|
||||
private addItem() {
|
||||
this.array.control.insert(0, this.formService.getListItem(this.spec))
|
||||
this.open.set(this.array.control.at(0), true)
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<tui-input
|
||||
[maskito]="mask"
|
||||
[tuiTextfieldCustomContent]="color"
|
||||
[tuiTextfieldCleaner]="false"
|
||||
[tuiHintContent]="spec | hint"
|
||||
[readOnly]="readOnly"
|
||||
[disabled]="!!spec.disabled"
|
||||
[pseudoInvalid]="invalid"
|
||||
[(ngModel)]="value"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
@if (spec.required) {
|
||||
<span>*</span>
|
||||
}
|
||||
</tui-input>
|
||||
<ng-template #color>
|
||||
<div class="wrapper" [style.color]="value">
|
||||
@if (!readOnly && !spec.disabled) {
|
||||
<input
|
||||
type="color"
|
||||
class="color"
|
||||
tabindex="-1"
|
||||
[(ngModel)]="value"
|
||||
(click.stop)="(0)"
|
||||
/>
|
||||
}
|
||||
<tui-icon icon="@tui.paint-bucket" tuiAppearance="icon" class="icon" />
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -1,33 +0,0 @@
|
||||
@use '@taiga-ui/core/styles/taiga-ui-local' as taiga;
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
pointer-events: auto;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 0.3rem;
|
||||
width: 1.4rem;
|
||||
bottom: -0.25rem;
|
||||
background: currentColor;
|
||||
border-radius: 0.125rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.color {
|
||||
@include taiga.fullsize();
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@include taiga.fullsize();
|
||||
pointer-events: none;
|
||||
|
||||
input:hover + & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
import { MaskitoOptions } from '@maskito/core'
|
||||
|
||||
@Component({
|
||||
selector: 'form-color',
|
||||
templateUrl: './form-color.component.html',
|
||||
styleUrls: ['./form-color.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class FormColorComponent extends Control<IST.ValueSpecColor, string> {
|
||||
readonly mask: MaskitoOptions = {
|
||||
mask: ['#', ...Array(6).fill(/[0-9a-f]/i)],
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
@switch (spec.type) {
|
||||
@case ('color') {
|
||||
<form-color />
|
||||
}
|
||||
@case ('datetime') {
|
||||
<form-datetime />
|
||||
}
|
||||
@case ('file') {
|
||||
<form-file />
|
||||
}
|
||||
@case ('multiselect') {
|
||||
<form-multiselect />
|
||||
}
|
||||
@case ('number') {
|
||||
<form-number />
|
||||
}
|
||||
@case ('select') {
|
||||
<form-select />
|
||||
}
|
||||
@case ('text') {
|
||||
<form-text />
|
||||
}
|
||||
@case ('textarea') {
|
||||
<form-textarea />
|
||||
}
|
||||
@case ('toggle') {
|
||||
<form-toggle />
|
||||
}
|
||||
}
|
||||
<tui-error [error]="order | tuiFieldError | async" />
|
||||
@if (spec.warning || immutable) {
|
||||
<ng-template #warning let-completeWith="completeWith">
|
||||
{{ spec.warning }}
|
||||
@if (immutable) {
|
||||
<p>{{ 'This value cannot be changed once set' | i18n }}!</p>
|
||||
}
|
||||
<div class="buttons">
|
||||
<button
|
||||
tuiButton
|
||||
type="button"
|
||||
appearance="secondary"
|
||||
size="s"
|
||||
(click)="completeWith(true)"
|
||||
>
|
||||
{{ 'Cancel' | i18n }}
|
||||
</button>
|
||||
<button
|
||||
tuiButton
|
||||
type="button"
|
||||
appearance="flat-grayscale"
|
||||
size="s"
|
||||
(click)="completeWith(false)"
|
||||
>
|
||||
{{ 'Continue' | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 0.5rem;
|
||||
|
||||
:first-child {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
TemplateRef,
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
||||
import { AbstractTuiNullableControl } from '@taiga-ui/legacy'
|
||||
import { filter } from 'rxjs'
|
||||
import { TuiAlertService, TuiDialogContext } from '@taiga-ui/core'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { ERRORS } from '../form-group/form-group.component'
|
||||
import { FORM_CONTROL_PROVIDERS } from './form-control.providers'
|
||||
import { DialogService, i18nKey, i18nPipe } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'form-control',
|
||||
templateUrl: './form-control.component.html',
|
||||
styleUrls: ['./form-control.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: FORM_CONTROL_PROVIDERS,
|
||||
standalone: false,
|
||||
})
|
||||
export class FormControlComponent<
|
||||
T extends Exclude<IST.ValueSpec, IST.ValueSpecHidden>,
|
||||
V,
|
||||
> extends AbstractTuiNullableControl<V> {
|
||||
private readonly alerts = inject(TuiAlertService)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
|
||||
@Input({ required: true })
|
||||
spec!: T
|
||||
|
||||
@ViewChild('warning')
|
||||
warning?: TemplateRef<TuiDialogContext<boolean>>
|
||||
|
||||
warned = false
|
||||
focused = false
|
||||
readonly order = ERRORS
|
||||
|
||||
get immutable(): boolean {
|
||||
return 'immutable' in this.spec && this.spec.immutable
|
||||
}
|
||||
|
||||
onFocus(focused: boolean) {
|
||||
this.focused = focused
|
||||
this.updateFocused(focused)
|
||||
}
|
||||
|
||||
onInput(value: V | null) {
|
||||
const previous = this.value
|
||||
|
||||
if (!this.warned && this.warning) {
|
||||
this.alerts
|
||||
.open<boolean>(this.warning, {
|
||||
label: this.i18n.transform('Warning'),
|
||||
appearance: 'warning',
|
||||
closeable: false,
|
||||
autoClose: 0,
|
||||
})
|
||||
.pipe(filter(Boolean), takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(() => {
|
||||
this.value = previous
|
||||
})
|
||||
}
|
||||
|
||||
this.warned = true
|
||||
this.value = value === '' ? null : value
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { forwardRef, Provider } from '@angular/core'
|
||||
import { TUI_VALIDATION_ERRORS } from '@taiga-ui/kit'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { FormControlComponent } from './form-control.component'
|
||||
|
||||
interface ValidatorsPatternError {
|
||||
actualValue: string
|
||||
requiredPattern: string | RegExp
|
||||
}
|
||||
|
||||
export const FORM_CONTROL_PROVIDERS: Provider[] = [
|
||||
{
|
||||
provide: TUI_VALIDATION_ERRORS,
|
||||
deps: [forwardRef(() => FormControlComponent)],
|
||||
useFactory: (
|
||||
control: FormControlComponent<
|
||||
Exclude<IST.ValueSpec, IST.ValueSpecHidden>,
|
||||
string
|
||||
>,
|
||||
) => ({
|
||||
required: 'Required',
|
||||
pattern: ({ requiredPattern }: ValidatorsPatternError) =>
|
||||
('patterns' in control.spec &&
|
||||
control.spec.patterns.find(
|
||||
({ regex }) => String(regex) === String(requiredPattern),
|
||||
)?.description) ||
|
||||
'Invalid format',
|
||||
}),
|
||||
},
|
||||
]
|
||||
@@ -1,54 +0,0 @@
|
||||
<ng-container [tuiHintContent]="spec.description">
|
||||
@switch (spec.inputmode) {
|
||||
@case ('time') {
|
||||
<tui-input-time
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[pseudoInvalid]="invalid"
|
||||
[ngModel]="getTime(value)"
|
||||
(ngModelChange)="value = $event?.toString() || null"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
@if (spec.required) {
|
||||
<span>*</span>
|
||||
}
|
||||
</tui-input-time>
|
||||
}
|
||||
@case ('date') {
|
||||
<tui-input-date
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[pseudoInvalid]="invalid"
|
||||
[min]="spec.min ? (spec.min | tuiMapper: getLimit)[0] : min"
|
||||
[max]="spec.max ? (spec.max | tuiMapper: getLimit)[0] : max"
|
||||
[(ngModel)]="value"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
@if (spec.required) {
|
||||
<span>*</span>
|
||||
}
|
||||
</tui-input-date>
|
||||
}
|
||||
@case ('datetime-local') {
|
||||
<tui-input-date-time
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[pseudoInvalid]="invalid"
|
||||
[min]="spec.min ? (spec.min | tuiMapper: getLimit) : min"
|
||||
[max]="spec.max ? (spec.max | tuiMapper: getLimit) : max"
|
||||
[(ngModel)]="value"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
@if (spec.required) {
|
||||
<span>*</span>
|
||||
}
|
||||
</tui-input-date-time>
|
||||
}
|
||||
}
|
||||
</ng-container>
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Component } from '@angular/core'
|
||||
import {
|
||||
TUI_FIRST_DAY,
|
||||
TUI_LAST_DAY,
|
||||
TuiDay,
|
||||
tuiPure,
|
||||
TuiTime,
|
||||
} from '@taiga-ui/cdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
|
||||
@Component({
|
||||
selector: 'form-datetime',
|
||||
templateUrl: './form-datetime.component.html',
|
||||
standalone: false,
|
||||
})
|
||||
export class FormDatetimeComponent extends Control<
|
||||
IST.ValueSpecDatetime,
|
||||
string
|
||||
> {
|
||||
readonly min = TUI_FIRST_DAY
|
||||
readonly max = TUI_LAST_DAY
|
||||
|
||||
@tuiPure
|
||||
getTime(value: string | null) {
|
||||
return value ? TuiTime.fromString(value) : null
|
||||
}
|
||||
|
||||
getLimit(limit: string): [TuiDay, TuiTime] {
|
||||
return [
|
||||
TuiDay.jsonParse(limit.slice(0, 10)),
|
||||
limit.length === 10
|
||||
? new TuiTime(0, 0)
|
||||
: TuiTime.fromString(limit.slice(-5)),
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<label tuiInputFiles [(ngModel)]="value">
|
||||
<input
|
||||
tuiInputFiles
|
||||
[invalid]="invalid"
|
||||
[accept]="spec.extensions.join(',')"
|
||||
(blur)="onFocus(false)"
|
||||
/>
|
||||
<ng-template let-drop>
|
||||
<div class="template" [class.template_hidden]="drop">
|
||||
<div class="label">
|
||||
{{ spec.name }}
|
||||
@if (spec.required) {
|
||||
<span>*</span>
|
||||
}
|
||||
@if (spec.description) {
|
||||
<tui-icon [tuiTooltip]="spec.description" />
|
||||
}
|
||||
</div>
|
||||
@if (value) {
|
||||
<tui-tag
|
||||
class="file"
|
||||
size="l"
|
||||
[value]="value.name"
|
||||
[removable]="true"
|
||||
(edited)="value = null"
|
||||
/>
|
||||
} @else {
|
||||
<small>{{ 'Click or drop file here' | i18n }}</small>
|
||||
}
|
||||
</div>
|
||||
<div class="drop" [class.drop_hidden]="!drop">{{ 'Drop file here' | i18n }}</div>
|
||||
</ng-template>
|
||||
</label>
|
||||
@@ -1,46 +0,0 @@
|
||||
@use '@taiga-ui/core/styles/taiga-ui-local' as taiga;
|
||||
|
||||
.template {
|
||||
@include taiga.transition(opacity);
|
||||
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 0.5rem;
|
||||
font: var(--tui-font-text-m);
|
||||
font-weight: bold;
|
||||
|
||||
&_hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.drop {
|
||||
@include taiga.fullsize();
|
||||
@include taiga.transition(opacity);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
|
||||
&_hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
small {
|
||||
max-width: 50%;
|
||||
font-weight: normal;
|
||||
color: var(--tui-text-secondary);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
tui-tag {
|
||||
z-index: 1;
|
||||
margin: -0.25rem -0.25rem -0.25rem auto;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { TuiFileLike } from '@taiga-ui/kit'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
|
||||
@Component({
|
||||
selector: 'form-file',
|
||||
templateUrl: './form-file.component.html',
|
||||
styleUrls: ['./form-file.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class FormFileComponent extends Control<
|
||||
IST.ValueSpecFile,
|
||||
TuiFileLike
|
||||
> {}
|
||||
@@ -1,30 +0,0 @@
|
||||
@for (entry of spec | keyvalue: asIsOrder | filterHidden; track entry) {
|
||||
<ng-container [tuiTextfieldCleaner]="true">
|
||||
@switch (entry.value.type) {
|
||||
@case ('object') {
|
||||
<form-object
|
||||
class="g-form-control"
|
||||
[formGroupName]="entry.key"
|
||||
[spec]="$any(entry.value)"
|
||||
/>
|
||||
}
|
||||
@case ('union') {
|
||||
<form-union
|
||||
class="g-form-control"
|
||||
[formGroupName]="entry.key"
|
||||
[spec]="$any(entry.value)"
|
||||
/>
|
||||
}
|
||||
@case ('list') {
|
||||
<form-array [formArrayName]="entry.key" [spec]="$any(entry.value)" />
|
||||
}
|
||||
@default {
|
||||
<form-control
|
||||
class="g-form-control"
|
||||
[formControlName]="entry.key"
|
||||
[spec]="entry.value"
|
||||
/>
|
||||
}
|
||||
}
|
||||
</ng-container>
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
form-group .g-form-control:not(:first-child) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
form-group .g-form-group {
|
||||
position: relative;
|
||||
padding-left: var(--tui-height-m);
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background: var(--tui-background-neutral-1);
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: 0;
|
||||
left: calc(1rem - 1px);
|
||||
bottom: 0.5rem;
|
||||
width: 2px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: 0.75rem;
|
||||
bottom: 0;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
form-group tui-tooltip {
|
||||
z-index: 1;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { FORM_GROUP_PROVIDERS } from './form-group.providers'
|
||||
|
||||
export const ERRORS = [
|
||||
'required',
|
||||
'pattern',
|
||||
'notNumber',
|
||||
'numberNotInteger',
|
||||
'numberNotInRange',
|
||||
'listNotUnique',
|
||||
'listNotInRange',
|
||||
'listItemIssue',
|
||||
]
|
||||
|
||||
@Component({
|
||||
selector: 'form-group',
|
||||
templateUrl: './form-group.component.html',
|
||||
styleUrls: ['./form-group.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
viewProviders: [FORM_GROUP_PROVIDERS],
|
||||
standalone: false,
|
||||
})
|
||||
export class FormGroupComponent {
|
||||
@Input() spec: IST.InputSpec = {}
|
||||
|
||||
asIsOrder() {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Provider, SkipSelf } from '@angular/core'
|
||||
import { ControlContainer } from '@angular/forms'
|
||||
import { TUI_DEFAULT_ERROR_MESSAGE } from '@taiga-ui/core'
|
||||
import { tuiInputDateOptionsProvider } from '@taiga-ui/kit'
|
||||
import { TUI_ARROW_MODE, tuiInputTimeOptionsProvider } from '@taiga-ui/legacy'
|
||||
import { identity, of } from 'rxjs'
|
||||
|
||||
export const FORM_GROUP_PROVIDERS: Provider[] = [
|
||||
{
|
||||
provide: TUI_DEFAULT_ERROR_MESSAGE,
|
||||
useValue: of('Unknown error'),
|
||||
},
|
||||
{
|
||||
provide: ControlContainer,
|
||||
deps: [[new SkipSelf(), ControlContainer]],
|
||||
useFactory: identity,
|
||||
},
|
||||
{
|
||||
provide: TUI_ARROW_MODE,
|
||||
useValue: {
|
||||
interactive: null,
|
||||
disabled: null,
|
||||
},
|
||||
},
|
||||
tuiInputDateOptionsProvider({
|
||||
nativePicker: true,
|
||||
}),
|
||||
tuiInputTimeOptionsProvider({
|
||||
nativePicker: true,
|
||||
}),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
<tui-multi-select
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="disabled"
|
||||
[readOnly]="readOnly"
|
||||
[pseudoInvalid]="invalid"
|
||||
[editable]="false"
|
||||
[disabledItemHandler]="disabledItemHandler"
|
||||
[(ngModel)]="selected"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
<select
|
||||
tuiSelect
|
||||
multiple
|
||||
[items]="items"
|
||||
[disabledItemHandler]="disabledItemHandler"
|
||||
></select>
|
||||
</tui-multi-select>
|
||||
@@ -1,22 +0,0 @@
|
||||
<tui-input-number
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[tuiTextfieldPostfix]="spec.units || ''"
|
||||
[pseudoInvalid]="invalid"
|
||||
[tuiNumberFormat]="{
|
||||
precision: spec.integer ? 0 : Infinity,
|
||||
decimalMode: 'not-zero',
|
||||
}"
|
||||
[min]="spec.min ?? -Infinity"
|
||||
[max]="spec.max ?? Infinity"
|
||||
[step]="spec.step || 0"
|
||||
[(ngModel)]="value"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
@if (spec.required) {
|
||||
<span>*</span>
|
||||
}
|
||||
<input tuiTextfieldLegacy [placeholder]="spec.placeholder || ''" />
|
||||
</tui-input-number>
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
|
||||
@Component({
|
||||
selector: 'form-number',
|
||||
templateUrl: './form-number.component.html',
|
||||
standalone: false,
|
||||
})
|
||||
export class FormNumberComponent extends Control<IST.ValueSpecNumber, number> {
|
||||
protected readonly Infinity = Infinity
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<h3 class="title" (click)="toggle()">
|
||||
<button
|
||||
tuiIconButton
|
||||
size="s"
|
||||
iconStart="@tui.chevron-down"
|
||||
type="button"
|
||||
class="button"
|
||||
[class.button_open]="open"
|
||||
[style.border-radius.%]="100"
|
||||
[appearance]="invalid ? 'primary-destructive' : 'secondary'"
|
||||
></button>
|
||||
<ng-content />
|
||||
{{ spec.name }}
|
||||
@if (spec.description) {
|
||||
<tui-icon [tuiTooltip]="spec.description" (click.stop)="(0)" />
|
||||
}
|
||||
</h3>
|
||||
|
||||
<tui-expand class="expand" [expanded]="open">
|
||||
<div class="g-form-group" [class.g-form-group_invalid]="invalid">
|
||||
<form-group [spec]="spec.spec" />
|
||||
</div>
|
||||
</tui-expand>
|
||||
@@ -1,41 +0,0 @@
|
||||
@use '@taiga-ui/core/styles/taiga-ui-local' as taiga;
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.title {
|
||||
position: relative;
|
||||
height: var(--tui-height-l);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font: var(--tui-font-text-l);
|
||||
font-weight: bold;
|
||||
margin: 0 0 -0.75rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
@include taiga.transition(transform);
|
||||
|
||||
margin-right: 1rem;
|
||||
|
||||
&_open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.expand {
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.g-form-group {
|
||||
padding-top: 0.75rem;
|
||||
|
||||
&_invalid::before,
|
||||
&_invalid::after {
|
||||
background: var(--tui-status-negative-pale);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core'
|
||||
import { ControlContainer } from '@angular/forms'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
|
||||
@Component({
|
||||
selector: 'form-object',
|
||||
templateUrl: './form-object.component.html',
|
||||
styleUrls: ['./form-object.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class FormObjectComponent {
|
||||
@Input({ required: true })
|
||||
spec!: IST.ValueSpecObject
|
||||
|
||||
@Input()
|
||||
open = false
|
||||
|
||||
@Output()
|
||||
readonly openChange = new EventEmitter<boolean>()
|
||||
|
||||
private readonly container = inject(ControlContainer)
|
||||
|
||||
get invalid() {
|
||||
return !this.container.valid && this.container.touched
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.open = !this.open
|
||||
this.openChange.emit(this.open)
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<tui-select
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="disabled"
|
||||
[readOnly]="readOnly"
|
||||
[tuiTextfieldCleaner]="false"
|
||||
[pseudoInvalid]="invalid"
|
||||
[(ngModel)]="selected"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }} *
|
||||
<select
|
||||
tuiSelect
|
||||
[placeholder]="spec.name"
|
||||
[items]="items"
|
||||
[disabledItemHandler]="disabledItemHandler"
|
||||
></select>
|
||||
</tui-select>
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { invert } from '@start9labs/shared'
|
||||
import { Control } from '../control'
|
||||
|
||||
@Component({
|
||||
selector: 'form-select',
|
||||
templateUrl: './form-select.component.html',
|
||||
standalone: false,
|
||||
})
|
||||
export class FormSelectComponent extends Control<IST.ValueSpecSelect, string> {
|
||||
private readonly inverted = invert(this.spec.values)
|
||||
|
||||
readonly items = Object.values(this.spec.values)
|
||||
|
||||
readonly disabledItemHandler = (item: string) =>
|
||||
Array.isArray(this.spec.disabled) &&
|
||||
!!this.inverted[item] &&
|
||||
this.spec.disabled.includes(this.inverted[item]!)
|
||||
|
||||
get disabled(): boolean {
|
||||
return typeof this.spec.disabled === 'string'
|
||||
}
|
||||
|
||||
get selected(): string | null {
|
||||
return (this.value && this.spec.values[this.value]) || null
|
||||
}
|
||||
|
||||
set selected(value: string | null) {
|
||||
this.value = (value && this.inverted[value]) || null
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<tui-input
|
||||
[tuiTextfieldCustomContent]="spec.masked || spec.generate ? toggle : ''"
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[pseudoInvalid]="invalid"
|
||||
[(ngModel)]="value"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
@if (spec.required) {
|
||||
<span>*</span>
|
||||
}
|
||||
<input
|
||||
tuiTextfieldLegacy
|
||||
[class.masked]="spec.masked && masked"
|
||||
[placeholder]="spec.placeholder || ''"
|
||||
[attr.minLength]="spec.minLength"
|
||||
[attr.maxLength]="spec.maxLength"
|
||||
[attr.inputmode]="spec.inputmode"
|
||||
/>
|
||||
</tui-input>
|
||||
<ng-template #toggle>
|
||||
@if (spec.generate) {
|
||||
<button
|
||||
tuiIconButton
|
||||
type="button"
|
||||
appearance="icon"
|
||||
title="Generate"
|
||||
size="xs"
|
||||
class="button"
|
||||
iconStart="@tui.refresh-ccw"
|
||||
(click)="generate()"
|
||||
></button>
|
||||
}
|
||||
@if (spec.masked) {
|
||||
<button
|
||||
tuiIconButton
|
||||
type="button"
|
||||
appearance="icon"
|
||||
title="Toggle masking"
|
||||
size="xs"
|
||||
class="button"
|
||||
[iconStart]="masked ? '@tui.eye' : '@tui.eye-off'"
|
||||
(click)="masked = !masked"
|
||||
></button>
|
||||
}
|
||||
</ng-template>
|
||||
@@ -1,8 +0,0 @@
|
||||
.button {
|
||||
pointer-events: auto;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.masked {
|
||||
-webkit-text-security: disc;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { IST, utils } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
|
||||
@Component({
|
||||
selector: 'form-text',
|
||||
templateUrl: './form-text.component.html',
|
||||
styleUrls: ['./form-text.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class FormTextComponent extends Control<IST.ValueSpecText, string> {
|
||||
masked = true
|
||||
|
||||
generate() {
|
||||
this.value = utils.getDefaultString(this.spec.generate || '')
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<tui-textarea
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[pseudoInvalid]="invalid"
|
||||
[expandable]="true"
|
||||
[rows]="6"
|
||||
[maxLength]="spec.maxLength"
|
||||
[(ngModel)]="value"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
@if (spec.required) {
|
||||
<span>*</span>
|
||||
}
|
||||
<textarea
|
||||
tuiTextfieldLegacy
|
||||
[placeholder]="spec.placeholder || ''"
|
||||
></textarea>
|
||||
</tui-textarea>
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
|
||||
@Component({
|
||||
selector: 'form-textarea',
|
||||
templateUrl: './form-textarea.component.html',
|
||||
standalone: false,
|
||||
})
|
||||
export class FormTextareaComponent extends Control<
|
||||
IST.ValueSpecTextarea,
|
||||
string
|
||||
> {}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user