diff --git a/src/app/modules/forms/ix-forms/components/ix-button-group/ix-button-group.component.ts b/src/app/modules/forms/ix-forms/components/ix-button-group/ix-button-group.component.ts index 3e77c76ea07..fa825ba3ae5 100644 --- a/src/app/modules/forms/ix-forms/components/ix-button-group/ix-button-group.component.ts +++ b/src/app/modules/forms/ix-forms/components/ix-button-group/ix-button-group.component.ts @@ -1,6 +1,6 @@ import { AsyncPipe } from '@angular/common'; import { - ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, + ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, input, Input, } from '@angular/core'; import { ControlValueAccessor, NgControl } from '@angular/forms'; import { MatButtonToggleChange, MatButtonToggleGroup, MatButtonToggle } from '@angular/material/button-toggle'; @@ -41,6 +41,8 @@ export class IxButtonGroupComponent implements ControlValueAccessor { @HostBinding('class.inlineFields') @Input() inlineFields = false; + formControlName = input(); + isDisabled = false; value: string; @@ -51,6 +53,10 @@ export class IxButtonGroupComponent implements ControlValueAccessor { this.controlDirective.valueAccessor = this; } + @HostBinding('attr.id') get id(): string { + return this.formControlName() || ''; + } + onChange: (value: string) => void = (): void => {}; onTouch: () => void = (): void => {}; diff --git a/src/app/modules/forms/ix-forms/components/ix-checkbox-list/ix-checkbox-list.component.ts b/src/app/modules/forms/ix-forms/components/ix-checkbox-list/ix-checkbox-list.component.ts index d458e0c3244..07a0f8888db 100644 --- a/src/app/modules/forms/ix-forms/components/ix-checkbox-list/ix-checkbox-list.component.ts +++ b/src/app/modules/forms/ix-forms/components/ix-checkbox-list/ix-checkbox-list.component.ts @@ -1,6 +1,6 @@ import { AsyncPipe } from '@angular/common'; import { - ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, + ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, } from '@angular/core'; import { ControlValueAccessor, NgControl } from '@angular/forms'; import { MatCheckbox } from '@angular/material/checkbox'; @@ -36,6 +36,7 @@ export class IxCheckboxListComponent implements ControlValueAccessor { @Input() options: Observable; @Input() inlineFields: boolean; @Input() inlineFieldFlex: string; + @Input() formControlName: string; isDisabled = false; value: (string | number)[]; @@ -62,6 +63,10 @@ export class IxCheckboxListComponent implements ControlValueAccessor { onChange: (value: (string | number)[]) => void = (): void => {}; onTouch: () => void = (): void => {}; + @HostBinding('attr.id') get id(): string { + return this.formControlName || ''; + } + writeValue(value: (string | number)[]): void { this.value = value; this.cdr.markForCheck(); diff --git a/src/app/modules/forms/ix-forms/components/ix-checkbox/ix-checkbox.component.ts b/src/app/modules/forms/ix-forms/components/ix-checkbox/ix-checkbox.component.ts index 8c492514dc4..fc5efda24ec 100644 --- a/src/app/modules/forms/ix-forms/components/ix-checkbox/ix-checkbox.component.ts +++ b/src/app/modules/forms/ix-forms/components/ix-checkbox/ix-checkbox.component.ts @@ -1,5 +1,5 @@ import { - ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, + ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, } from '@angular/core'; import { ControlValueAccessor, NgControl, @@ -34,6 +34,7 @@ export class IxCheckboxComponent implements ControlValueAccessor { @Input() tooltip: string; @Input() warning: string; @Input() required: boolean; + @Input() formControlName: string; isDisabled = false; value: boolean; @@ -45,6 +46,10 @@ export class IxCheckboxComponent implements ControlValueAccessor { this.controlDirective.valueAccessor = this; } + @HostBinding('attr.id') get id(): string { + return this.formControlName || ''; + } + onChange: (value: boolean) => void = (): void => {}; onTouch: () => void = (): void => {}; diff --git a/src/app/modules/forms/ix-forms/components/ix-chips/ix-chips.component.ts b/src/app/modules/forms/ix-forms/components/ix-chips/ix-chips.component.ts index 1d561f37c83..06d0b52b2fd 100644 --- a/src/app/modules/forms/ix-forms/components/ix-chips/ix-chips.component.ts +++ b/src/app/modules/forms/ix-forms/components/ix-chips/ix-chips.component.ts @@ -5,6 +5,7 @@ import { ChangeDetectorRef, Component, ElementRef, + HostBinding, Input, OnChanges, ViewChild, @@ -62,6 +63,7 @@ export class IxChipsComponent implements OnChanges, ControlValueAccessor { @Input() tooltip: string; @Input() required: boolean; @Input() allowNewEntries = true; + @Input() formControlName: string; /** * A function that provides the options for the autocomplete dropdown. * This function is called when the user types into the input field, @@ -108,6 +110,10 @@ export class IxChipsComponent implements OnChanges, ControlValueAccessor { inputReset$ = new Subject(); + @HostBinding('attr.id') get id(): string { + return this.formControlName || ''; + } + onChange: (value: string[]) => void = (): void => {}; onTouch: () => void = (): void => {}; diff --git a/src/app/modules/forms/ix-forms/components/ix-code-editor/ix-code-editor.component.ts b/src/app/modules/forms/ix-forms/components/ix-code-editor/ix-code-editor.component.ts index f23d532fe8d..23a53ec4f6d 100644 --- a/src/app/modules/forms/ix-forms/components/ix-code-editor/ix-code-editor.component.ts +++ b/src/app/modules/forms/ix-forms/components/ix-code-editor/ix-code-editor.component.ts @@ -1,6 +1,15 @@ import { AsyncPipe } from '@angular/common'; import { - AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnChanges, OnInit, ViewChild, + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + HostBinding, + Input, + OnChanges, + OnInit, + ViewChild, } from '@angular/core'; import { ControlValueAccessor, NgControl } from '@angular/forms'; import { MatHint } from '@angular/material/form-field'; @@ -42,6 +51,7 @@ export class IxCodeEditorComponent implements OnChanges, OnInit, AfterViewInit, @Input() tooltip: string; @Input() language: CodeEditorLanguage; @Input() placeholder: string; + @Input() formControlName: string; afterViewInit$ = new BehaviorSubject(false); @@ -68,6 +78,10 @@ export class IxCodeEditorComponent implements OnChanges, OnInit, AfterViewInit, this.controlDirective.valueAccessor = this; } + @HostBinding('attr.id') get id(): string { + return this.formControlName || ''; + } + ngOnChanges(changes: IxSimpleChanges): void { if (changes.language?.currentValue) { this.afterViewInit$.pipe( diff --git a/src/app/modules/forms/ix-forms/components/ix-combobox/ix-combobox.component.ts b/src/app/modules/forms/ix-forms/components/ix-combobox/ix-combobox.component.ts index a66a4d580ce..8d01af435e0 100644 --- a/src/app/modules/forms/ix-forms/components/ix-combobox/ix-combobox.component.ts +++ b/src/app/modules/forms/ix-forms/components/ix-combobox/ix-combobox.component.ts @@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, + HostBinding, Input, OnInit, ViewChild, @@ -66,6 +67,8 @@ export class IxComboboxComponent implements ControlValueAccessor, OnInit { this.cdr.markForCheck(); } + @Input() formControlName: string; + private comboboxProviderHandler: IxComboboxProviderManager; @ViewChild('ixInput') inputElementRef: ElementRef; @@ -95,6 +98,10 @@ export class IxComboboxComponent implements ControlValueAccessor, OnInit { this.controlDirective.valueAccessor = this; } + @HostBinding('attr.id') get id(): string { + return this.formControlName || ''; + } + writeValue(value: string | number): void { this.value = value; if (this.value && this.options?.length) { diff --git a/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.ts b/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.ts index 37685e23d8f..a624edd3ddf 100644 --- a/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.ts +++ b/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.ts @@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + HostBinding, Input, OnChanges, OnInit, @@ -69,6 +70,7 @@ export class IxExplorerComponent implements OnInit, OnChanges, ControlValueAcces @Input() nodeProvider: TreeNodeProvider; @Input() canCreateDataset = false; @Input() createDatasetProps: Omit = {}; + @Input() formControlName: string; @ViewChild('tree', { static: true }) tree: TreeComponent; @@ -91,6 +93,10 @@ export class IxExplorerComponent implements OnInit, OnChanges, ControlValueAcces || this.isDisabled; } + @HostBinding('attr.id') get id(): string { + return this.formControlName || ''; + } + private readonly actionMapping: IActionMapping = { mouse: { dblClick: (tree, node, $event) => { diff --git a/src/app/modules/forms/ix-forms/components/ix-file-input/ix-file-input.component.ts b/src/app/modules/forms/ix-forms/components/ix-file-input/ix-file-input.component.ts index d2a810bd2a0..cc170c93ceb 100644 --- a/src/app/modules/forms/ix-forms/components/ix-file-input/ix-file-input.component.ts +++ b/src/app/modules/forms/ix-forms/components/ix-file-input/ix-file-input.component.ts @@ -1,5 +1,5 @@ import { - ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, + ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, } from '@angular/core'; import { ControlValueAccessor, NgControl, @@ -39,6 +39,7 @@ export class IxFileInputComponent implements ControlValueAccessor { @Input() acceptedFiles = '*.*'; @Input() multiple: boolean; @Input() required: boolean; + @Input() formControlName: string; value: FileList; isDisabled = false; @@ -54,6 +55,10 @@ export class IxFileInputComponent implements ControlValueAccessor { this.controlDirective.valueAccessor = this; } + @HostBinding('attr.id') get id(): string { + return this.formControlName || ''; + } + onChanged(value: FileList): void { this.value = value; this.onChange([...value]); diff --git a/src/app/modules/forms/ix-forms/components/ix-form-with-glossary/ix-form-with-glossary.component.html b/src/app/modules/forms/ix-forms/components/ix-form-with-glossary/ix-form-with-glossary.component.html new file mode 100644 index 00000000000..6e0e98ea1d3 --- /dev/null +++ b/src/app/modules/forms/ix-forms/components/ix-form-with-glossary/ix-form-with-glossary.component.html @@ -0,0 +1,35 @@ +
+
+
+ +
+ +
+ +
+
+ @if (searchMap()) { + + } + + @for (section of sections(); track section) { +
+ {{ section.label }} + @if (!section.valid()) { + + } +
+ } +
+
+
diff --git a/src/app/modules/forms/ix-forms/components/ix-form-with-glossary/ix-form-with-glossary.component.scss b/src/app/modules/forms/ix-forms/components/ix-form-with-glossary/ix-form-with-glossary.component.scss new file mode 100644 index 00000000000..d538e58889b --- /dev/null +++ b/src/app/modules/forms/ix-forms/components/ix-form-with-glossary/ix-form-with-glossary.component.scss @@ -0,0 +1,79 @@ +@import 'scss-imports/variables'; + +.wrapper { + display: flex; +} + +.wizard-container { + width: 80%; + + @media (max-width: $breakpoint-sm) { + width: 100%; + } + + .actions { + padding: 0 10px; + + @media (max-width: $breakpoint-xs) { + margin: 0 10px; + padding: 0; + + button { + width: 100%; + } + } + } +} + +.search-container { + margin-left: 16px; + min-width: 250px; + width: 20%; + + @media (max-width: $breakpoint-xs) { + display: none; + } + + .search-card { + background-color: var(--bg2); + display: flex; + flex-direction: column; + gap: 5px; + padding: 0 14px 14px; + position: sticky; + top: 10px; + + .section { + color: var(--fg1); + display: flex; + font-family: var(--font-family-body); + font-size: 15px; + gap: 5px; + padding: 10px; + + ix-icon { + color: var(--red); + } + } + + .section:hover { + background-color: var(--contrast-lighter); + cursor: pointer; + } + } +} + +.mini-search-card { + background-color: var(--bg2); + display: none; + padding: 2px 14px; + position: sticky; + top: -16px; + z-index: 99; + + @media (max-width: $breakpoint-xs) { + display: block; + margin: 0 9px; + top: 0; + } +} diff --git a/src/app/modules/forms/ix-forms/components/ix-form-with-glossary/ix-form-with-glossary.component.ts b/src/app/modules/forms/ix-forms/components/ix-form-with-glossary/ix-form-with-glossary.component.ts new file mode 100644 index 00000000000..7b36db3c45c --- /dev/null +++ b/src/app/modules/forms/ix-forms/components/ix-form-with-glossary/ix-form-with-glossary.component.ts @@ -0,0 +1,109 @@ +import { AsyncPipe } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + contentChildren, + OnInit, + signal, + ViewChild, + ViewContainerRef, +} from '@angular/core'; +import { + FormBuilder, ReactiveFormsModule, +} from '@angular/forms'; +import { MatButton } from '@angular/material/button'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { TranslateModule } from '@ngx-translate/core'; +import { timer } from 'rxjs'; +import { + debounceTime, + distinctUntilChanged, +} from 'rxjs/operators'; +import { RequiresRolesDirective } from 'app/directives/requires-roles/requires-roles.directive'; +import { Option } from 'app/interfaces/option.interface'; +import { IxFullPageFormSectionComponent } from 'app/modules/forms/ix-forms/components/ix-full-page-form/ix-full-page-form-section/ix-full-page-form-section.component'; +import { IxInputComponent } from 'app/modules/forms/ix-forms/components/ix-input/ix-input.component'; +import { ReadOnlyComponent } from 'app/modules/forms/ix-forms/components/readonly-badge/readonly-badge.component'; +import { IxFormService } from 'app/modules/forms/ix-forms/services/ix-form.service'; +import { iconMarker } from 'app/modules/ix-icon/icon-marker.util'; +import { IxIconComponent } from 'app/modules/ix-icon/ix-icon.component'; +import { PageHeaderComponent } from 'app/modules/page-header/page-title-header/page-header.component'; +import { TestDirective } from 'app/modules/test-id/test.directive'; +import { AppMetadataCardComponent } from 'app/pages/apps/components/installed-apps/app-metadata-card/app-metadata-card.component'; + +@UntilDestroy() +@Component({ + selector: 'ix-form-with-glossary', + templateUrl: './ix-form-with-glossary.component.html', + styleUrls: ['./ix-form-with-glossary.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + PageHeaderComponent, + ReadOnlyComponent, + IxInputComponent, + AppMetadataCardComponent, + MatButton, + RequiresRolesDirective, + AsyncPipe, + TranslateModule, + TestDirective, + ReactiveFormsModule, + IxIconComponent, + ], +}) +export class IxFormWithGlossaryComponent implements OnInit { + @ViewChild('contentContainer', { read: ViewContainerRef, static: true }) + viewContainerRef!: ViewContainerRef; + + protected sections = contentChildren(IxFullPageFormSectionComponent); + protected searchControl = this.formBuilder.control(''); + protected searchOptions = signal([]); + + readonly iconMarker = iconMarker; + + constructor( + private formBuilder: FormBuilder, + private formService: IxFormService, + ) {} + + ngOnInit(): void { + this.handleSearchControl(); + this.updateSearchOption(''); + } + + protected onSectionClick(id: string, label: string = null): void { + const nextElement = this.formService.getElementByControlName(id) + || this.formService.getElementByLabel(label); + if (!nextElement) { + return; + } + + nextElement?.scrollIntoView({ block: 'center' }); + nextElement.classList.add('highlighted'); + + timer(999) + .pipe(untilDestroyed(this)) + .subscribe(() => nextElement.classList.remove('highlighted')); + } + + private updateSearchOption(searchQuery: string): void { + const options: Option[] = this.formService.getControlsOptions(searchQuery); + this.searchOptions.set(options); + } + + private handleSearchControl(): void { + this.searchControl.valueChanges.pipe( + debounceTime(100), + distinctUntilChanged(), + untilDestroyed(this), + ).subscribe((value: string) => { + const option = this.searchOptions().find((opt) => opt.value === value) + || this.searchOptions().find((opt) => opt.label.toLocaleLowerCase() === value.toLocaleLowerCase()); + + if (option) { + this.onSectionClick(option.value.toString(), option.label); + } + }); + } +} diff --git a/src/app/modules/forms/ix-forms/components/ix-full-page-form/ix-full-page-form-section/ix-full-page-form-section.component.html b/src/app/modules/forms/ix-forms/components/ix-full-page-form/ix-full-page-form-section/ix-full-page-form-section.component.html new file mode 100644 index 00000000000..87c2bc2d551 --- /dev/null +++ b/src/app/modules/forms/ix-forms/components/ix-full-page-form/ix-full-page-form-section/ix-full-page-form-section.component.html @@ -0,0 +1,15 @@ + + +
+
+ +
+
+
{{ 'Section Help' | translate }}
+
{{ help }}
+
+
+
diff --git a/src/app/modules/forms/ix-forms/components/ix-full-page-form/ix-full-page-form-section/ix-full-page-form-section.component.scss b/src/app/modules/forms/ix-forms/components/ix-full-page-form/ix-full-page-form-section/ix-full-page-form-section.component.scss new file mode 100644 index 00000000000..a7c4400450d --- /dev/null +++ b/src/app/modules/forms/ix-forms/components/ix-full-page-form/ix-full-page-form-section/ix-full-page-form-section.component.scss @@ -0,0 +1,69 @@ +@import 'scss-imports/variables'; + +:host { + color: var(--fg1); + margin: 15px; +} + +.divider { + margin: -12px 0 12px !important; +} + +.section { + display: flex; + justify-content: space-between; + + @media (max-width: $breakpoint-sm) { + flex-direction: column; + + .fieldset { + order: 2; + padding: 0; + } + + .help { + order: 1; + padding: 0 !important; + } + } + + .fieldset { + max-width: 425px; + width: 37%; + + @media (max-width: $breakpoint-md) { + width: 50%; + } + + @media (max-width: $breakpoint-sm) { + width: 100%; + } + } + + .help { + padding: 0 20px; + width: 62%; + + @media (max-width: $breakpoint-md) { + width: 50%; + } + + @media (max-width: $breakpoint-sm) { + background-color: var(--contrast-darker); + margin-bottom: 16px; + padding: 16px; + width: 100%; + } + + .title { + font-family: var(--font-family-body); + font-size: 15px; + font-weight: 600; + } + + .value { + font-size: 1rem; + white-space: pre-wrap; + } + } +} diff --git a/src/app/modules/forms/ix-forms/components/ix-full-page-form/ix-full-page-form-section/ix-full-page-form-section.component.ts b/src/app/modules/forms/ix-forms/components/ix-full-page-form/ix-full-page-form-section/ix-full-page-form-section.component.ts new file mode 100644 index 00000000000..fdbbc314c59 --- /dev/null +++ b/src/app/modules/forms/ix-forms/components/ix-full-page-form/ix-full-page-form-section/ix-full-page-form-section.component.ts @@ -0,0 +1,26 @@ +import { + ChangeDetectionStrategy, Component, input, Input, +} from '@angular/core'; +import { MatDivider } from '@angular/material/divider'; +import { UntilDestroy } from '@ngneat/until-destroy'; +import { TranslateModule } from '@ngx-translate/core'; +import { IxFieldsetComponent } from 'app/modules/forms/ix-forms/components/ix-fieldset/ix-fieldset.component'; + +@UntilDestroy() +@Component({ + selector: 'ix-full-page-form-section', + styleUrls: ['./ix-full-page-form-section.component.scss'], + templateUrl: './ix-full-page-form-section.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + IxFieldsetComponent, + MatDivider, + TranslateModule, + ], +}) +export class IxFullPageFormSectionComponent { + @Input() help: string; + @Input() label: string; + valid = input.required(); +} diff --git a/src/app/modules/forms/ix-forms/components/ix-full-page-form/ix-full-page-form.component.html b/src/app/modules/forms/ix-forms/components/ix-full-page-form/ix-full-page-form.component.html new file mode 100644 index 00000000000..4564e06c530 --- /dev/null +++ b/src/app/modules/forms/ix-forms/components/ix-full-page-form/ix-full-page-form.component.html @@ -0,0 +1,56 @@ + + @if (!(hasRequiredRoles$ | async)) { + + } + + +
+
+
+ +
+
+ + +
+ +
+
+
+ +
+
+ @if (searchMap()) { + + } + + @for (section of sections(); track section) { +
+ {{ section.label }} + @if (!section.valid()) { + + } +
+ } +
+
+
diff --git a/src/app/modules/forms/ix-forms/components/ix-full-page-form/ix-full-page-form.component.scss b/src/app/modules/forms/ix-forms/components/ix-full-page-form/ix-full-page-form.component.scss new file mode 100644 index 00000000000..8548ff576fb --- /dev/null +++ b/src/app/modules/forms/ix-forms/components/ix-full-page-form/ix-full-page-form.component.scss @@ -0,0 +1,105 @@ +@import 'scss-imports/variables'; + +.wrapper { + display: flex; +} + +.wizard-container { + width: 80%; + + @media (max-width: $breakpoint-sm) { + width: 100%; + } + + .actions { + padding: 0 10px; + + @media (max-width: $breakpoint-xs) { + margin: 0 10px; + padding: 0; + + button { + width: 100%; + } + } + } +} + +.search-container { + margin-left: 16px; + min-width: 250px; + width: 20%; + + @media (max-width: $breakpoint-xs) { + display: none; + } + + .search-card { + background-color: var(--bg2); + display: flex; + flex-direction: column; + gap: 5px; + padding: 0 14px 14px; + position: sticky; + top: 10px; + + .section { + color: var(--fg1); + display: flex; + font-family: var(--font-family-body); + font-size: 15px; + gap: 5px; + padding: 10px; + + ix-icon { + color: var(--red); + } + } + + .section:hover { + background-color: var(--contrast-lighter); + cursor: pointer; + } + } +} + +.mini-search-card { + background-color: var(--bg2); + display: none; + padding: 2px 14px; + position: sticky; + top: -16px; + z-index: 99; + + @media (max-width: $breakpoint-xs) { + display: block; + margin: 0 9px; + top: 0; + } +} + +ix-app-metadata-card ::ng-deep { + display: block; + margin-bottom: 16px; + margin-top: -32px; + + mat-card.card { + background: transparent; + } + + mat-card-header { + display: none; + opacity: 0; + visibility: hidden; + } + + .gradient { + background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--bg1)); + } +} + +:host ::ng-deep { + .header-container .title-container .actions-container { + justify-content: flex-start; + } +} diff --git a/src/app/modules/forms/ix-forms/components/ix-full-page-form/ix-full-page-form.component.ts b/src/app/modules/forms/ix-forms/components/ix-full-page-form/ix-full-page-form.component.ts new file mode 100644 index 00000000000..264b9b1405e --- /dev/null +++ b/src/app/modules/forms/ix-forms/components/ix-full-page-form/ix-full-page-form.component.ts @@ -0,0 +1,130 @@ +import { AsyncPipe } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + contentChildren, + input, + OnInit, + output, + signal, +} from '@angular/core'; +import { + FormBuilder, FormGroup, ReactiveFormsModule, +} from '@angular/forms'; +import { MatButton } from '@angular/material/button'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { TranslateModule } from '@ngx-translate/core'; +import { + Observable, timer, +} from 'rxjs'; +import { + debounceTime, + distinctUntilChanged, +} from 'rxjs/operators'; +import { RequiresRolesDirective } from 'app/directives/requires-roles/requires-roles.directive'; +import { Role } from 'app/enums/role.enum'; +import { Option } from 'app/interfaces/option.interface'; +import { IxFullPageFormSectionComponent } from 'app/modules/forms/ix-forms/components/ix-full-page-form/ix-full-page-form-section/ix-full-page-form-section.component'; +import { IxInputComponent } from 'app/modules/forms/ix-forms/components/ix-input/ix-input.component'; +import { ReadOnlyComponent } from 'app/modules/forms/ix-forms/components/readonly-badge/readonly-badge.component'; +import { iconMarker } from 'app/modules/ix-icon/icon-marker.util'; +import { IxIconComponent } from 'app/modules/ix-icon/ix-icon.component'; +import { PageHeaderComponent } from 'app/modules/page-header/page-title-header/page-header.component'; +import { TestDirective } from 'app/modules/test-id/test.directive'; +import { AppMetadataCardComponent } from 'app/pages/apps/components/installed-apps/app-metadata-card/app-metadata-card.component'; +import { AuthService } from 'app/services/auth/auth.service'; + +@UntilDestroy() +@Component({ + selector: 'ix-full-page-form', + templateUrl: './ix-full-page-form.component.html', + styleUrls: ['./ix-full-page-form.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + PageHeaderComponent, + ReadOnlyComponent, + IxInputComponent, + AppMetadataCardComponent, + MatButton, + RequiresRolesDirective, + AsyncPipe, + TranslateModule, + TestDirective, + ReactiveFormsModule, + IxIconComponent, + ], +}) +export class IxFullPageFormComponent implements OnInit { + formGroup = input.required(); + requiredRoles = input(); + searchMap = input>(); + pageTitle = input.required(); + isLoading = input.required(); + buttonText = input.required(); + onSubmit = output(); + + protected sections = contentChildren(IxFullPageFormSectionComponent); + protected searchControl = this.formBuilder.control(''); + protected searchOptions = signal([]); + + readonly iconMarker = iconMarker; + + get hasRequiredRoles$(): Observable { + return this.authService.hasRole(this.requiredRoles()); + } + + constructor( + private formBuilder: FormBuilder, + private authService: AuthService, + ) {} + + ngOnInit(): void { + this.handleSearchControl(); + this.updateSearchOption(''); + } + + protected onSectionClick(id: string, label: string = null): void { + const nextElement = document.getElementById(id) + || document.getElementById(label); + if (!nextElement) { + return; + } + + nextElement?.scrollIntoView({ block: 'center' }); + nextElement.classList.add('highlighted'); + + timer(999) + .pipe(untilDestroyed(this)) + .subscribe(() => nextElement.classList.remove('highlighted')); + } + + private updateSearchOption(searchQuery: string): void { + const options: Option[] = []; + const query = searchQuery.toLowerCase().trim(); + for (const [key, label] of this.searchMap().entries()) { + if ( + key.toLowerCase().trim().includes(query) + || label.toLowerCase().includes(query) + ) { + options.push({ label, value: key }); + } + } + this.searchOptions.set(options); + } + + private handleSearchControl(): void { + this.searchControl.valueChanges.pipe( + debounceTime(100), + distinctUntilChanged(), + untilDestroyed(this), + ).subscribe((value: string) => { + const option = this.searchOptions().find((opt) => opt.value === value) + || this.searchOptions().find((opt) => opt.label.toLocaleLowerCase() === value.toLocaleLowerCase()); + + if (option) { + this.onSectionClick(option.value.toString(), option.label); + } + }); + } +} diff --git a/src/app/modules/forms/ix-forms/components/ix-input/ix-input.component.ts b/src/app/modules/forms/ix-forms/components/ix-input/ix-input.component.ts index d38031e6cbe..721ea3e8c4a 100644 --- a/src/app/modules/forms/ix-forms/components/ix-input/ix-input.component.ts +++ b/src/app/modules/forms/ix-forms/components/ix-input/ix-input.component.ts @@ -3,6 +3,7 @@ import { ChangeDetectorRef, Component, ElementRef, + HostBinding, Input, OnChanges, OnInit, @@ -68,6 +69,7 @@ export class IxInputComponent implements ControlValueAccessor, OnInit, OnChanges @Input() autocomplete = 'off'; @Input() autocompleteOptions: Option[]; @Input() maxLength = 524288; + @Input() formControlName: string | number; /** If formatted value returned by parseAndFormatInput has non-numeric letters * and input 'type' is a number, the input will stay empty on the form */ @@ -102,6 +104,10 @@ export class IxInputComponent implements ControlValueAccessor, OnInit, OnChanges } } + @HostBinding('attr.id') get id(): string { + return this.formControlName?.toString() || ''; + } + ngOnInit(): void { if (this.autocompleteOptions) { this.handleAutocompleteOptionsOnInit(); diff --git a/src/app/modules/forms/ix-forms/components/ix-ip-input-with-netmask/ix-ip-input-with-netmask.component.ts b/src/app/modules/forms/ix-forms/components/ix-ip-input-with-netmask/ix-ip-input-with-netmask.component.ts index 3934aa32e3e..21bb32c6426 100644 --- a/src/app/modules/forms/ix-forms/components/ix-ip-input-with-netmask/ix-ip-input-with-netmask.component.ts +++ b/src/app/modules/forms/ix-forms/components/ix-ip-input-with-netmask/ix-ip-input-with-netmask.component.ts @@ -1,5 +1,5 @@ import { - ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, + ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, } from '@angular/core'; import { ControlValueAccessor, NgControl } from '@angular/forms'; import { MatOption } from '@angular/material/core'; @@ -34,6 +34,7 @@ export class IxIpInputWithNetmaskComponent implements ControlValueAccessor { @Input() tooltip: string; @Input() hint: string; @Input() required: boolean; + @Input() formControlName: string | number; onChange: (value: string) => void = (): void => {}; onTouched: () => void = (): void => {}; @@ -52,6 +53,10 @@ export class IxIpInputWithNetmaskComponent implements ControlValueAccessor { this.controlDirective.valueAccessor = this; } + @HostBinding('attr.id') get id(): string { + return this.formControlName?.toString() || ''; + } + onAddressInput(input: HTMLInputElement): void { this.address = input.value; this.onValueChanged(); diff --git a/src/app/modules/forms/ix-forms/components/ix-list/ix-list-item/ix-list-item.component.ts b/src/app/modules/forms/ix-forms/components/ix-list/ix-list-item/ix-list-item.component.ts index 6175f4a62c0..e13cd43b8ee 100644 --- a/src/app/modules/forms/ix-forms/components/ix-list/ix-list-item/ix-list-item.component.ts +++ b/src/app/modules/forms/ix-forms/components/ix-list/ix-list-item/ix-list-item.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, - Component, input, output, + Component, HostBinding, input, output, } from '@angular/core'; import { MatIconButton } from '@angular/material/button'; import { TranslateModule } from '@ngx-translate/core'; @@ -23,10 +23,15 @@ import { TestDirective } from 'app/modules/test-id/test.directive'; export class IxListItemComponent { readonly canDelete = input(true); readonly label = input(); + formControlName = input(); readonly delete = output(); deleteItem(): void { this.delete.emit(); } + + @HostBinding('attr.id') get id(): string { + return this.formControlName() || ''; + } } diff --git a/src/app/modules/forms/ix-forms/components/ix-list/ix-list.component.ts b/src/app/modules/forms/ix-forms/components/ix-list/ix-list.component.ts index e82e147dbf5..7df63d7044b 100644 --- a/src/app/modules/forms/ix-forms/components/ix-list/ix-list.component.ts +++ b/src/app/modules/forms/ix-forms/components/ix-list/ix-list.component.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + HostBinding, Input, output, } from '@angular/core'; import { AbstractControl } from '@angular/forms'; @@ -38,6 +39,7 @@ export class IxListComponent implements AfterViewInit { // TODO: Does not belong to the scope of this component. @Input() itemsSchema: ChartSchemaNode[]; @Input() isEditMode: boolean; + @Input() formArrayName: string; readonly add = output(); @@ -53,6 +55,10 @@ export class IxListComponent implements AfterViewInit { } } + @HostBinding('attr.id') get id(): string { + return this.formArrayName || ''; + } + addItem(schema?: ChartSchemaNode[]): void { this.add.emit(schema); } diff --git a/src/app/modules/forms/ix-forms/components/ix-permissions/ix-permissions.component.ts b/src/app/modules/forms/ix-forms/components/ix-permissions/ix-permissions.component.ts index 48a05dbfae5..72ec64746f9 100644 --- a/src/app/modules/forms/ix-forms/components/ix-permissions/ix-permissions.component.ts +++ b/src/app/modules/forms/ix-forms/components/ix-permissions/ix-permissions.component.ts @@ -1,5 +1,5 @@ import { - ChangeDetectionStrategy, ChangeDetectorRef, Component, input, + ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, input, } from '@angular/core'; import { ControlValueAccessor, NgControl } from '@angular/forms'; import { MatCheckbox } from '@angular/material/checkbox'; @@ -33,6 +33,7 @@ export class IxPermissionsComponent implements ControlValueAccessor { readonly tooltip = input(); readonly required = input(false); readonly hideOthersPermissions = input(false); + formControlName = input(); isDisabled = false; @@ -65,6 +66,10 @@ export class IxPermissionsComponent implements ControlValueAccessor { this.setPermissionsAndUpdateValue(value); } + @HostBinding('attr.id') get id(): string { + return this.formControlName() || ''; + } + setPermissionsAndUpdateValue(value = '000'): void { if (value && this.formatRe.test(value)) { this.value = value; diff --git a/src/app/modules/forms/ix-forms/components/ix-radio-group/ix-radio-group.component.ts b/src/app/modules/forms/ix-forms/components/ix-radio-group/ix-radio-group.component.ts index e6f1fc84a4e..e8cdeae96ab 100644 --- a/src/app/modules/forms/ix-forms/components/ix-radio-group/ix-radio-group.component.ts +++ b/src/app/modules/forms/ix-forms/components/ix-radio-group/ix-radio-group.component.ts @@ -1,6 +1,6 @@ import { AsyncPipe } from '@angular/common'; import { - ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, + ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, } from '@angular/core'; import { ControlValueAccessor, NgControl, @@ -43,6 +43,7 @@ export class IxRadioGroupComponent implements ControlValueAccessor { @Input() options: Observable; @Input() inlineFields: boolean; @Input() inlineFieldFlex: string; + @Input() formControlName: string; isDisabled = false; value: string; @@ -74,6 +75,10 @@ export class IxRadioGroupComponent implements ControlValueAccessor { this.cdr.markForCheck(); } + @HostBinding('attr.id') get id(): string { + return this.formControlName || ''; + } + registerOnChange(onChange: (value: string) => void): void { this.onChange = onChange; } diff --git a/src/app/modules/forms/ix-forms/components/ix-select/ix-select.component.ts b/src/app/modules/forms/ix-forms/components/ix-select/ix-select.component.ts index 36db4682d9a..c161496d576 100644 --- a/src/app/modules/forms/ix-forms/components/ix-select/ix-select.component.ts +++ b/src/app/modules/forms/ix-forms/components/ix-select/ix-select.component.ts @@ -1,7 +1,7 @@ import { AsyncPipe } from '@angular/common'; import { ChangeDetectionStrategy, ChangeDetectorRef, - Component, Input, OnChanges, OnInit, + Component, HostBinding, Input, OnChanges, OnInit, } from '@angular/core'; import { ControlValueAccessor, NgControl, FormsModule } from '@angular/forms'; import { MatOption } from '@angular/material/core'; @@ -58,6 +58,7 @@ export class IxSelectComponent implements ControlValueAccessor, OnInit, OnChange @Input() emptyValue: string = null; @Input() hideEmpty = false; @Input() showSelectAll = false; + @Input() formControlName: string | number; @Input() compareWith: (val1: unknown, val2: unknown) => boolean = (val1: unknown, val2: unknown) => val1 === val2; protected value: IxSelectValue; @@ -140,6 +141,10 @@ export class IxSelectComponent implements ControlValueAccessor, OnInit, OnChange } } + @HostBinding('attr.id') get id(): string { + return this.formControlName?.toString() || ''; + } + onChange: (value: IxSelectValue) => void = (): void => {}; onTouch: () => void = (): void => {}; diff --git a/src/app/modules/forms/ix-forms/components/ix-slide-toggle/ix-slide-toggle.component.ts b/src/app/modules/forms/ix-forms/components/ix-slide-toggle/ix-slide-toggle.component.ts index 57ca9188502..8ab8542686c 100644 --- a/src/app/modules/forms/ix-forms/components/ix-slide-toggle/ix-slide-toggle.component.ts +++ b/src/app/modules/forms/ix-forms/components/ix-slide-toggle/ix-slide-toggle.component.ts @@ -1,5 +1,5 @@ import { - ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, + ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, } from '@angular/core'; import { ControlValueAccessor, NgControl, @@ -31,6 +31,7 @@ export class IxSlideToggleComponent implements ControlValueAccessor { @Input() hint: string; @Input() tooltip: string; @Input() required: boolean; + @Input() formControlName: string; isDisabled = false; value: boolean; @@ -50,6 +51,10 @@ export class IxSlideToggleComponent implements ControlValueAccessor { this.cdr.markForCheck(); } + @HostBinding('attr.id') get id(): string { + return this.formControlName || ''; + } + registerOnChange(onChange: (value: boolean) => void): void { this.onChange = onChange; } diff --git a/src/app/modules/forms/ix-forms/components/ix-star-rating/ix-star-rating.component.ts b/src/app/modules/forms/ix-forms/components/ix-star-rating/ix-star-rating.component.ts index ed3d5dcacc8..49782fde18c 100644 --- a/src/app/modules/forms/ix-forms/components/ix-star-rating/ix-star-rating.component.ts +++ b/src/app/modules/forms/ix-forms/components/ix-star-rating/ix-star-rating.component.ts @@ -1,5 +1,5 @@ import { - ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, input, + ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, HostBinding, input, } from '@angular/core'; import { ControlValueAccessor, NgControl } from '@angular/forms'; import { MatIconButton } from '@angular/material/button'; @@ -33,6 +33,7 @@ export class IxStarRatingComponent implements ControlValueAccessor { readonly tooltip = input(''); readonly required = input(false); readonly maxRating = input(5); + readonly formControlName = input(); isDisabled = false; value: number; @@ -51,6 +52,10 @@ export class IxStarRatingComponent implements ControlValueAccessor { onChange: (value: number) => void = (): void => {}; onTouch: () => void = (): void => {}; + @HostBinding('attr.id') get id(): string { + return this.formControlName() || ''; + } + writeValue(value: number): void { this.value = value > this.maxRating() ? this.maxRating() : value; this.cdr.markForCheck(); diff --git a/src/app/modules/forms/ix-forms/components/ix-textarea/ix-textarea.component.ts b/src/app/modules/forms/ix-forms/components/ix-textarea/ix-textarea.component.ts index bb3ebd75c1c..067aad95510 100644 --- a/src/app/modules/forms/ix-forms/components/ix-textarea/ix-textarea.component.ts +++ b/src/app/modules/forms/ix-forms/components/ix-textarea/ix-textarea.component.ts @@ -1,5 +1,5 @@ import { - ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, + ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, input, Input, } from '@angular/core'; import { ControlValueAccessor, NgControl, FormsModule } from '@angular/forms'; import { MatHint } from '@angular/material/form-field'; @@ -35,6 +35,7 @@ export class IxTextareaComponent implements ControlValueAccessor { @Input() required: boolean; @Input() rows = 4; @Input() readonly: boolean; + formControlName = input(); value = ''; isDisabled = false; @@ -49,6 +50,10 @@ export class IxTextareaComponent implements ControlValueAccessor { this.controlDirective.valueAccessor = this; } + @HostBinding('attr.id') get id(): string { + return this.formControlName() || ''; + } + writeValue(value: string): void { this.value = value; this.cdr.markForCheck(); diff --git a/src/app/modules/forms/ix-forms/directives/registered-control.directive.ts b/src/app/modules/forms/ix-forms/directives/registered-control.directive.ts index 8bd134e2d47..4f279588698 100644 --- a/src/app/modules/forms/ix-forms/directives/registered-control.directive.ts +++ b/src/app/modules/forms/ix-forms/directives/registered-control.directive.ts @@ -1,7 +1,11 @@ -import { AfterViewInit, Directive, ElementRef } from '@angular/core'; +import { + AfterViewInit, Directive, ElementRef, input, +} from '@angular/core'; import { NgControl } from '@angular/forms'; import { IxFormService } from 'app/modules/forms/ix-forms/services/ix-form.service'; +export const ixControlLabelTag = 'ix-label'; + /** * This directive is used to the be able to locate the template of the control programmatically via IxFormService. */ @@ -10,6 +14,7 @@ import { IxFormService } from 'app/modules/forms/ix-forms/services/ix-form.servi selector: '[ixRegisteredControl]', }) export class RegisteredControlDirective implements AfterViewInit { + label = input(); constructor( private elementRef: ElementRef, private formService: IxFormService, @@ -17,6 +22,7 @@ export class RegisteredControlDirective implements AfterViewInit { ) {} ngAfterViewInit(): void { + this.elementRef.nativeElement.setAttribute(ixControlLabelTag, this.label() || this.control.name?.toString() || ''); this.formService.registerControl(this.control, this.elementRef); } } diff --git a/src/app/modules/forms/ix-forms/directives/with-glossary.directive.ts b/src/app/modules/forms/ix-forms/directives/with-glossary.directive.ts new file mode 100644 index 00000000000..3f60171fe8c --- /dev/null +++ b/src/app/modules/forms/ix-forms/directives/with-glossary.directive.ts @@ -0,0 +1,29 @@ +import { + ComponentRef, Directive, Input, TemplateRef, ViewContainerRef, ViewRef, AfterContentInit, +} from '@angular/core'; +import { FormGroupDirective } from '@angular/forms'; +import { IxFormWithGlossaryComponent } from 'app/modules/forms/ix-forms/components/ix-form-with-glossary/ix-form-with-glossary.component'; + +@Directive({ + selector: '[withGlossary]', + standalone: true, +}) +export class WithGlossaryDirective implements AfterContentInit { + @Input('withGlossary') formGroup!: FormGroupDirective; + constructor( + private templateRef: TemplateRef, + private viewContainer: ViewContainerRef, + ) {} + + ngAfterContentInit(): void { + this.viewContainer.clear(); + + const componentRef: ComponentRef = this.viewContainer.createComponent( + IxFormWithGlossaryComponent, + ); + + const embeddedView = this.viewContainer.createEmbeddedView(this.templateRef); + const contentContainer = componentRef.instance.viewContainerRef; + embeddedView.rootNodes.forEach((node: ViewRef) => contentContainer.insert(node)); + } +} diff --git a/src/app/modules/forms/ix-forms/services/ix-form.service.ts b/src/app/modules/forms/ix-forms/services/ix-form.service.ts index 1094992aea8..f8f38032de0 100644 --- a/src/app/modules/forms/ix-forms/services/ix-form.service.ts +++ b/src/app/modules/forms/ix-forms/services/ix-form.service.ts @@ -1,5 +1,7 @@ import { ElementRef, Injectable } from '@angular/core'; import { NgControl } from '@angular/forms'; +import { Option } from 'app/interfaces/option.interface'; +import { ixControlLabelTag } from 'app/modules/forms/ix-forms/directives/registered-control.directive'; @Injectable({ providedIn: 'root' }) export class IxFormService { @@ -13,6 +15,28 @@ export class IxFormService { return this.getControls().map((ctrl) => ctrl.name); } + getControlsOptions(query = ''): Option[] { + const options: Option[] = []; + const cleanedQuery = query.toLowerCase().trim(); + for (const [control, element] of this.controls.entries()) { + const name = control.name?.toString(); + if (!name) { + continue; + } + const label = element.getAttribute(ixControlLabelTag)?.toString() || name; + if (!query) { + options.push({ label, value: name }); + } else { + const cleanedLabel = label.trim().toLowerCase(); + const cleanedName = name.trim().toLowerCase(); + if (cleanedName.includes(cleanedQuery) || cleanedLabel.includes(cleanedQuery)) { + options.push({ label, value: name }); + } + } + } + return options; + } + getControlByName(controlName: string): NgControl | undefined { return this.getControls().find((control) => control.name === controlName); } @@ -22,6 +46,15 @@ export class IxFormService { return control ? this.controls.get(control) : undefined; } + getElementByLabel(label: string): HTMLElement | undefined { + for (const htmlElement of this.controls.values()) { + if (htmlElement.getAttribute(ixControlLabelTag) === label) { + return htmlElement; + } + } + return undefined; + } + registerControl(control: NgControl, elementRef: ElementRef): void { this.controls.set(control, elementRef.nativeElement); } diff --git a/src/assets/i18n/es-ar.json b/src/assets/i18n/es-ar.json index 5708212ce21..cda602ce378 100644 --- a/src/assets/i18n/es-ar.json +++ b/src/assets/i18n/es-ar.json @@ -239,6 +239,7 @@ "Create Exporter": "", "Create Idmap": "", "Create Init/Shutdown Script": "", + "Create Instance": "", "Create Kerberos Keytab": "", "Create Kerberos Realm": "", "Create Periodic S.M.A.R.T. Test": "", @@ -562,6 +563,7 @@ "If {vmName} is still running, the Guest OS did not respond as expected. It is possible to use Power Off or the Force Stop After Timeout option to stop the VM.": "", "Ignore Builtin": "", "Ignore List": "", + "Image": "", "Improvement": "", "In order for dRAID to overweight its benefits over RaidZ the minimum recommended number of disks per dRAID vdev is 10.": "", "In some cases it's possible that the provided key/passphrase is valid but the path where the dataset is supposed to be mounted after being unlocked already exists and is not empty. In this case, unlock operation would fail. This can be overridden by Force flag. When it is set, system will rename the existing directory/file path where the dataset should be mounted resulting in successful unlock of the dataset.": "", @@ -2530,7 +2532,6 @@ "Create Disk Test": "Crear prueba de disco", "Create Group": "Crear grupo", "Create Home Directory": "Crear directorio Home", - "Create Instance": "Crear instancia", "Create Interface": "Crear interfaz", "Create Kernel Parameters": "Crear parámetros del núcleo", "Create Key": "Crear clave", @@ -3403,7 +3404,6 @@ "If the Hex key type is chosen, an encryption key will be auto-generated.": "Si se elige el tipo clave hexadecimal, se generará automáticamente una clave de cifrado.", "If the IPMI out-of-band management interface is on a different VLAN from the management network, enter the IPMI VLAN.": "Si la interfaz de administración fuera de banda de IPMI está en una VLAN diferente de la red de administración, ingrese la VLAN de IPMI.", "If the destination system has snapshots but they do not have any data in common with the source snapshots, destroy all destination snapshots and do a full replication. Warning: enabling this option can cause data loss or excessive data transfer if the replication is misconfigured.": "Si el sistema de destino tiene instantáneas pero no tiene ningún dato en común con las instantáneas de origen, destruya todas las instantáneas de destino y realice una replicación completa. Advertencia: habilitar esta opción puede causar pérdida de datos o transferencia excesiva de datos si la replicación está mal configurada.", - "Image": "Imagen", "Image ID": "ID de imagen", "Image Name": "Nombre de la imagen", "Image Size": "Tamaño de la imagen", diff --git a/src/assets/i18n/fr.json b/src/assets/i18n/fr.json index f4818221005..d91399d9871 100644 --- a/src/assets/i18n/fr.json +++ b/src/assets/i18n/fr.json @@ -146,6 +146,7 @@ "Cooling": "", "Copies": "", "Crashed": "", + "Create Instance": "", "Create Kerberos Keytab": "", "Create Kerberos Realm": "", "Create Kernel Parameters": "", @@ -1888,7 +1889,6 @@ "Create Home Directory": "Créer un répertoire personnel", "Create Idmap": "Créer un Idmap", "Create Init/Shutdown Script": "Créer un script Init/Shutdown", - "Create Instance": "Créer une instance", "Create Interface": "Créer une interface", "Create NFS Share": "Créer un partage NFS", "Create NTP Server": "Créer un serveur NTP", diff --git a/src/assets/i18n/nl.json b/src/assets/i18n/nl.json index 8273b1ce046..9538598e07f 100644 --- a/src/assets/i18n/nl.json +++ b/src/assets/i18n/nl.json @@ -21,6 +21,7 @@ "Browse Catalog": "", "Convert": "", "Convert to custom app": "", + "Create Instance": "", "Created Date": "", "Creating Instance": "", "Current status: {status}": "", @@ -49,6 +50,7 @@ "Host Port": "", "Host Protocol": "", "Host ports are listed on the left and associated container ports are on the right. 0.0.0.0 on the host side represents binding to any IP address on the host.": "", + "Image": "", "Increase logging verbosity related to the active directory service in /var/log/middlewared.log": "", "Initiator Group": "", "Instance": "", @@ -1156,7 +1158,6 @@ "Create Home Directory": "Home-map aanmaken", "Create Idmap": "Idmap aanmaken", "Create Init/Shutdown Script": "Init/Shutdownscript aanmaken", - "Create Instance": "Instance aanmaken", "Create Interface": "Interface aanmaken", "Create Kerberos Keytab": "Kerberos Keytab aanmaken", "Create Kerberos Realm": "Kerberos Realm aanmaken", @@ -2324,7 +2325,6 @@ "If {vmName} is still running, the Guest OS did not respond as expected. It is possible to use Power Off or the Force Stop After Timeout option to stop the VM.": "Als {vmName} nog steeds actief is, heeft het gast-besturingssysteem niet gereageerd zoals verwacht. Het is mogelijk om Power Off of de optie Force Stop After Timeout te gebruiken om de VM te stoppen.", "Ignore Builtin": "Ingebouwd negeren", "Ignore List": "Te negeren lijst", - "Image": "Image", "Image ID": "Image ID", "Image Name": "Imagenaam", "Image Size": "Imagegrootte",