mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
chore: break down form-object by type, part 1
This commit is contained in:
committed by
Aiden McClelland
parent
aa950669f6
commit
ddf1f9bcd5
@@ -1,6 +1,6 @@
|
||||
<ion-button
|
||||
*ngIf="data.description"
|
||||
class="slot-start"
|
||||
class="icon"
|
||||
fill="clear"
|
||||
(click.stop)="presentAlertDescription()"
|
||||
>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.slot-start {
|
||||
.icon {
|
||||
--padding-start: 0;
|
||||
--padding-end: 7px;
|
||||
--padding-end: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ import { AlertController } from '@ionic/angular'
|
||||
export class FormLabelComponent {
|
||||
@Input() data!: {
|
||||
name: string
|
||||
new: boolean
|
||||
edited: boolean
|
||||
description: string | null
|
||||
edited?: boolean
|
||||
new?: boolean
|
||||
required?: boolean
|
||||
newOptions?: boolean
|
||||
}
|
||||
@@ -22,7 +22,7 @@ export class FormLabelComponent {
|
||||
async presentAlertDescription() {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: this.data.name,
|
||||
message: this.data.description!,
|
||||
message: this.data.description || '',
|
||||
buttons: [
|
||||
{
|
||||
text: 'OK',
|
||||
|
||||
@@ -16,6 +16,9 @@ import {
|
||||
ToEnumListDisplayPipe,
|
||||
ToRangePipe,
|
||||
} from './form-object.pipes'
|
||||
import { FormFileComponent } from './form-object/controls/form-file/form-file.component'
|
||||
import { FormInputComponent } from './form-object/controls/form-input/form-input.component'
|
||||
import { FormWarningDirective } from './form-warning.directive'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -27,6 +30,9 @@ import {
|
||||
ToEnumListDisplayPipe,
|
||||
ToElementIdPipe,
|
||||
ToRangePipe,
|
||||
FormWarningDirective,
|
||||
FormFileComponent,
|
||||
FormInputComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<ion-item style="--padding-start: 0">
|
||||
<form-label
|
||||
[data]="{
|
||||
name: spec.name,
|
||||
description: spec.description,
|
||||
edited: control.dirty
|
||||
}"
|
||||
></form-label>
|
||||
<div slot="end">
|
||||
<ion-button
|
||||
*ngIf="!control.value; else hasFile"
|
||||
strong
|
||||
color="dark"
|
||||
size="small"
|
||||
(click)="uploadFile.click()"
|
||||
>
|
||||
Browse...
|
||||
</ion-button>
|
||||
<input
|
||||
type="file"
|
||||
[accept]="spec.extensions.join(',')"
|
||||
style="display: none"
|
||||
#uploadFile
|
||||
(change)="handleFileInput($event)"
|
||||
/>
|
||||
<ng-template #hasFile>
|
||||
<div class="inline">
|
||||
<p class="ion-padding-end">{{ control.value.name }}</p>
|
||||
<div style="cursor: pointer" (click)="clearFile()">
|
||||
<ion-icon name="close"></ion-icon>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ion-item>
|
||||
<p class="error-message">
|
||||
<span *ngIf="control.errors as errors">
|
||||
{{ errors | getError }}
|
||||
</span>
|
||||
</p>
|
||||
@@ -0,0 +1,26 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ion-item-divider {
|
||||
text-transform: unset;
|
||||
border-bottom: 1px solid
|
||||
var(
|
||||
--ion-item-border-color,
|
||||
var(--ion-border-color, var(--ion-color-step-150, rgba(0, 0, 0, 0.13)))
|
||||
);
|
||||
|
||||
--padding-top: 18px;
|
||||
--padding-start: 0;
|
||||
|
||||
&.error-border {
|
||||
border-color: var(--ion-color-danger-shade);
|
||||
--border-color: var(--ion-color-danger-shade);
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-top: 2px;
|
||||
font-size: small;
|
||||
color: var(--ion-color-danger);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { AbstractControl } from '@angular/forms'
|
||||
import { ValueSpecOf } from 'start-sdk/types/config-types'
|
||||
|
||||
@Component({
|
||||
selector: 'form-file',
|
||||
templateUrl: './form-file.component.html',
|
||||
styleUrls: ['./form-file.component.scss'],
|
||||
})
|
||||
export class FormFileComponent {
|
||||
@Input() spec!: ValueSpecOf<'file'>
|
||||
@Input() control!: AbstractControl
|
||||
|
||||
handleFileInput(e: any) {
|
||||
this.control.patchValue(e.target.files[0])
|
||||
}
|
||||
|
||||
clearFile() {
|
||||
this.control.patchValue(null)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<form-label
|
||||
class="label"
|
||||
[data]="{
|
||||
name: spec.name,
|
||||
description: spec.description,
|
||||
new: form.original?.[name] === undefined,
|
||||
edited: control.dirty,
|
||||
required: !spec.nullable
|
||||
}"
|
||||
></form-label>
|
||||
<ion-item [color]="(theme$ | async) === 'Light' ? 'light' : 'dark'">
|
||||
<ion-textarea
|
||||
*ngIf="spec.type === 'string' && spec.textarea; else notTextArea"
|
||||
formWarning
|
||||
#warning="formWarning"
|
||||
[placeholder]="spec.placeholder"
|
||||
[formControl]="control"
|
||||
(ionFocus)="warning.onChange(name, spec)"
|
||||
(ionChange)="onInputChange.emit()"
|
||||
></ion-textarea>
|
||||
<ng-template #notTextArea>
|
||||
<ion-input
|
||||
formWarning
|
||||
#warning="formWarning"
|
||||
type="text"
|
||||
class="input"
|
||||
[class.input_redacted]="
|
||||
spec.type === 'string' && control.value && spec.masked && !unmasked
|
||||
"
|
||||
[inputmode]="spec.type === 'number' ? 'tel' : 'text'"
|
||||
[placeholder]="spec.placeholder"
|
||||
[formControl]="control"
|
||||
(ionFocus)="warning.onChange(name, spec)"
|
||||
(ionChange)="onInputChange.emit()"
|
||||
></ion-input>
|
||||
</ng-template>
|
||||
<ion-button
|
||||
*ngIf="spec.type === 'string' && spec.masked"
|
||||
slot="end"
|
||||
fill="clear"
|
||||
color="light"
|
||||
(click)="unmasked = !unmasked"
|
||||
>
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
size="small"
|
||||
[name]="unmasked ? 'eye-off-outline' : 'eye-outline'"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
<ion-note
|
||||
*ngIf="spec.type === 'number' && spec.units"
|
||||
slot="end"
|
||||
color="light"
|
||||
class="units"
|
||||
>
|
||||
{{ spec.units }}
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
<p class="error-message">
|
||||
<span *ngIf="control.errors as errors">
|
||||
{{ errors | getError : $any(spec)['pattern-description'] }}
|
||||
</span>
|
||||
</p>
|
||||
@@ -0,0 +1,24 @@
|
||||
.label {
|
||||
margin: 16px 0 6px;
|
||||
}
|
||||
|
||||
.input {
|
||||
font-family: 'Courier New';
|
||||
font-weight: bold;
|
||||
|
||||
--placeholder-font-weight: 400;
|
||||
|
||||
&_redacted {
|
||||
font-family: 'Redacted';
|
||||
}
|
||||
}
|
||||
|
||||
.units {
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-top: 2px;
|
||||
font-size: small;
|
||||
color: var(--ion-color-danger);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Component, Input, inject, Output, EventEmitter } from '@angular/core'
|
||||
import { FormControl } from '@angular/forms'
|
||||
import { ValueSpecOf } from 'start-sdk/types/config-types'
|
||||
import { THEME } from '@start9labs/shared'
|
||||
import { FormObjectComponent } from '../../form-object.component'
|
||||
|
||||
@Component({
|
||||
selector: 'form-input',
|
||||
templateUrl: './form-input.component.html',
|
||||
styleUrls: ['./form-input.component.scss'],
|
||||
})
|
||||
export class FormInputComponent {
|
||||
@Input() name!: string
|
||||
@Input() spec!: ValueSpecOf<'string' | 'number'>
|
||||
@Input() control!: FormControl
|
||||
|
||||
@Output() onInputChange = new EventEmitter<void>()
|
||||
|
||||
unmasked = false
|
||||
|
||||
readonly theme$ = inject(THEME)
|
||||
|
||||
constructor(readonly form: FormObjectComponent) {}
|
||||
}
|
||||
@@ -1,104 +1,33 @@
|
||||
<ion-item-group [formGroup]="formGroup">
|
||||
<div *ngFor="let entry of formGroup.controls | keyvalue : asIsOrder">
|
||||
<div *ngIf="objectSpec[entry.key] as spec">
|
||||
<!-- file -->
|
||||
<form-file
|
||||
*ngIf="spec.type === 'file'"
|
||||
[spec]="spec"
|
||||
[control]="entry.value"
|
||||
></form-file>
|
||||
<!-- string or number -->
|
||||
<ng-container *ngIf="spec.type === 'string' || spec.type === 'number'">
|
||||
<!-- label -->
|
||||
<p class="input-label">
|
||||
<form-label
|
||||
[data]="{
|
||||
name: spec.name,
|
||||
description: spec.description,
|
||||
new: original?.[entry.key] === undefined,
|
||||
edited: entry.value.dirty,
|
||||
required: !spec.nullable
|
||||
}"
|
||||
></form-label>
|
||||
</p>
|
||||
<ion-item [color]="(theme$ | async) === 'Light' ? 'light' : 'dark'">
|
||||
<ion-textarea
|
||||
*ngIf="spec.type === 'string' && spec.textarea; else notTextArea"
|
||||
[placeholder]="spec.placeholder"
|
||||
[formControlName]="entry.key"
|
||||
(ionFocus)="presentAlertChangeWarning(entry.key, spec)"
|
||||
(ionChange)="handleInputChange()"
|
||||
></ion-textarea>
|
||||
<ng-template #notTextArea>
|
||||
<ion-input
|
||||
type="text"
|
||||
class="input"
|
||||
[class.input_redacted]="
|
||||
spec.type === 'string' &&
|
||||
entry.value.value &&
|
||||
spec.masked &&
|
||||
!unmasked[entry.key]
|
||||
"
|
||||
[inputmode]="spec.type === 'number' ? 'tel' : 'text'"
|
||||
[placeholder]="spec.placeholder"
|
||||
[formControlName]="entry.key"
|
||||
(ionFocus)="presentAlertChangeWarning(entry.key, spec)"
|
||||
(ionChange)="handleInputChange()"
|
||||
></ion-input>
|
||||
</ng-template>
|
||||
<ion-button
|
||||
*ngIf="spec.type === 'string' && spec.masked"
|
||||
slot="end"
|
||||
fill="clear"
|
||||
color="light"
|
||||
(click)="unmasked[entry.key] = !unmasked[entry.key]"
|
||||
>
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
[name]="unmasked[entry.key] ? 'eye-off-outline' : 'eye-outline'"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
<ion-note
|
||||
*ngIf="spec.type === 'number' && spec.units"
|
||||
slot="end"
|
||||
color="light"
|
||||
style="font-size: medium"
|
||||
>
|
||||
{{ spec.units }}
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
<p class="error-message">
|
||||
<span *ngIf="formGroup.get(entry.key)?.errors as errors">
|
||||
{{ errors | getError : $any(spec)['pattern-description'] }}
|
||||
</span>
|
||||
</p>
|
||||
</ng-container>
|
||||
<form-input
|
||||
*ngIf="spec.type === 'string' || spec.type === 'number'"
|
||||
[spec]="spec"
|
||||
[name]="entry.key"
|
||||
[control]="$any(entry.value)"
|
||||
(onInputChange)="handleInputChange()"
|
||||
></form-input>
|
||||
<!-- boolean or enum -->
|
||||
<ion-item
|
||||
*ngIf="spec.type === 'boolean' || spec.type === 'enum'"
|
||||
style="--padding-start: 0"
|
||||
>
|
||||
<ion-button
|
||||
*ngIf="spec.description as desc"
|
||||
fill="clear"
|
||||
(click.stop)="presentAlertDescription(spec.name, desc)"
|
||||
style="--padding-start: 0"
|
||||
>
|
||||
<ion-icon
|
||||
name="help-circle-outline"
|
||||
slot="icon-only"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
<ion-label>
|
||||
<b>
|
||||
{{ spec.name }}
|
||||
<ion-text
|
||||
*ngIf="original?.[entry.key] === undefined"
|
||||
color="success"
|
||||
>
|
||||
(New)
|
||||
</ion-text>
|
||||
<ion-text *ngIf="entry.value.dirty" color="warning">
|
||||
(Edited)
|
||||
</ion-text>
|
||||
</b>
|
||||
</ion-label>
|
||||
<form-label
|
||||
[data]="{
|
||||
name: spec.name,
|
||||
description: spec.description,
|
||||
new: original?.[entry.key] === undefined,
|
||||
edited: entry.value.dirty
|
||||
}"
|
||||
></form-label>
|
||||
<!-- boolean -->
|
||||
<ion-toggle
|
||||
*ngIf="spec.type === 'boolean'"
|
||||
@@ -109,7 +38,7 @@
|
||||
<!-- enum -->
|
||||
<!-- class enter-click disables the enter click on the modal behind the select -->
|
||||
<ion-select
|
||||
*ngIf="spec.type === 'enum' && formGroup.get(entry.key) as control"
|
||||
*ngIf="spec.type === 'enum'"
|
||||
[interfaceOptions]="{
|
||||
message: spec.warning | toWarningText,
|
||||
cssClass: 'enter-click'
|
||||
@@ -117,7 +46,7 @@
|
||||
slot="end"
|
||||
placeholder="Select"
|
||||
[formControlName]="entry.key"
|
||||
[selectedText]="spec['value-names'][control.value]"
|
||||
[selectedText]="spec['value-names'][entry.value.value]"
|
||||
>
|
||||
<ion-select-option
|
||||
*ngFor="let option of spec.values"
|
||||
@@ -127,64 +56,6 @@
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
<!-- file -->
|
||||
<ng-container
|
||||
*ngIf="spec.type === 'file' && formGroup.get(entry.key) as control"
|
||||
>
|
||||
<ion-item style="--padding-start: 0">
|
||||
<ion-button
|
||||
*ngIf="spec.description as desc"
|
||||
fill="clear"
|
||||
(click.stop)="presentAlertDescription(spec.name, desc)"
|
||||
style="--padding-start: 0"
|
||||
>
|
||||
<ion-icon
|
||||
name="help-circle-outline"
|
||||
slot="icon-only"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
<ion-label>
|
||||
<b>
|
||||
{{ spec.name }}
|
||||
<ion-text *ngIf="entry.value.dirty" color="warning">
|
||||
(Edited)
|
||||
</ion-text>
|
||||
</b>
|
||||
</ion-label>
|
||||
<div slot="end">
|
||||
<ion-button
|
||||
*ngIf="!control.value; else hasFile"
|
||||
strong
|
||||
color="dark"
|
||||
size="small"
|
||||
(click)="uploadFile.click()"
|
||||
>
|
||||
Browse...
|
||||
</ion-button>
|
||||
<input
|
||||
type="file"
|
||||
[accept]="spec.extensions.join(',')"
|
||||
style="display: none"
|
||||
#uploadFile
|
||||
(change)="handleFileInput($event, control)"
|
||||
/>
|
||||
<ng-template #hasFile>
|
||||
<div class="inline">
|
||||
<p class="ion-padding-end">{{ control.value.name }}</p>
|
||||
<div style="cursor: pointer" (click)="clearFile(control)">
|
||||
<ion-icon name="close"></ion-icon>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ion-item>
|
||||
<p class="error-message">
|
||||
<span *ngIf="control.errors as errors">
|
||||
{{ errors | getError }}
|
||||
</span>
|
||||
</p>
|
||||
</ng-container>
|
||||
<!-- object -->
|
||||
<ng-container *ngIf="spec.type === 'object'">
|
||||
<!-- label -->
|
||||
|
||||
@@ -7,12 +7,7 @@ import {
|
||||
inject,
|
||||
SimpleChanges,
|
||||
} from '@angular/core'
|
||||
import {
|
||||
AbstractControl,
|
||||
FormArray,
|
||||
UntypedFormArray,
|
||||
UntypedFormGroup,
|
||||
} from '@angular/forms'
|
||||
import { FormArray, UntypedFormArray, UntypedFormGroup } from '@angular/forms'
|
||||
import { AlertButton, AlertController, ModalController } from '@ionic/angular'
|
||||
import {
|
||||
InputSpec,
|
||||
@@ -240,28 +235,6 @@ export class FormObjectComponent {
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
handleFileInput(e: any, control: AbstractControl) {
|
||||
control.patchValue(e.target.files[0])
|
||||
}
|
||||
|
||||
clearFile(control: AbstractControl) {
|
||||
control.patchValue(null)
|
||||
}
|
||||
|
||||
async presentAlertDescription(name: string, description: string) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: name,
|
||||
message: description || '',
|
||||
buttons: [
|
||||
{
|
||||
text: 'OK',
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
private addListItem(key: string): void {
|
||||
const arr = this.formGroup.get(key) as UntypedFormArray
|
||||
const listSpec = this.objectSpec[key] as ValueSpecList
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Directive } from '@angular/core'
|
||||
import { ValueSpec, ValueSpecUnion } from 'start-sdk/types/config-types'
|
||||
import { AlertButton, AlertController } from '@ionic/angular'
|
||||
|
||||
@Directive({
|
||||
selector: '[formWarning]',
|
||||
exportAs: 'formWarning',
|
||||
})
|
||||
export class FormWarningDirective {
|
||||
private warned = false
|
||||
|
||||
constructor(private readonly alertCtrl: AlertController) {}
|
||||
|
||||
async onChange<T extends ValueSpec>(
|
||||
key: string,
|
||||
spec: T extends ValueSpecUnion ? never : T,
|
||||
okFn?: Function,
|
||||
cancelFn?: Function,
|
||||
) {
|
||||
if (!spec.warning || this.warned) return okFn ? okFn() : null
|
||||
|
||||
this.warned = true
|
||||
|
||||
const buttons: AlertButton[] = [
|
||||
{
|
||||
text: 'Ok',
|
||||
handler: () => {
|
||||
if (okFn) okFn()
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
]
|
||||
|
||||
if (okFn || cancelFn) {
|
||||
buttons.unshift({
|
||||
text: 'Cancel',
|
||||
handler: () => {
|
||||
if (cancelFn) cancelFn()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
subHeader: `Editing ${spec.name} has consequences:`,
|
||||
message: spec.warning,
|
||||
buttons,
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user