mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
feat: implement disabled, immutable and generate (#2280)
* feat: implement `disabled`, `immutable` and `generate` * chore: remove unnecessary code * chore: add generate to textarea and implement immutable * no generate for textarea --------- Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
This commit is contained in:
committed by
Aiden McClelland
parent
4d1c7a3884
commit
8aa19e6420
@@ -62,6 +62,7 @@ export * from './util/base-64'
|
||||
export * from './util/copy-to-clipboard'
|
||||
export * from './util/get-new-entries'
|
||||
export * from './util/get-pkg-id'
|
||||
export * from './util/invert'
|
||||
export * from './util/misc.util'
|
||||
export * from './util/rpc.util'
|
||||
export * from './util/to-local-iso-string'
|
||||
|
||||
12
frontend/projects/shared/src/util/invert.ts
Normal file
12
frontend/projects/shared/src/util/invert.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export function invert<
|
||||
T extends string | number | symbol,
|
||||
D extends string | number | symbol,
|
||||
>(obj: Record<T, D>): Record<D, T> {
|
||||
const result = {} as Record<D, T>
|
||||
|
||||
for (const key in obj) {
|
||||
result[obj[key]] = key
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { TUI_DATE_FORMAT, TUI_DATE_SEPARATOR } from '@taiga-ui/cdk'
|
||||
import {
|
||||
tuiButtonOptionsProvider,
|
||||
tuiNumberFormatProvider,
|
||||
tuiTextfieldOptionsProvider,
|
||||
} from '@taiga-ui/core'
|
||||
import {
|
||||
TUI_DATE_TIME_VALUE_TRANSFORMER,
|
||||
@@ -33,6 +34,7 @@ export const APP_PROVIDERS: Provider[] = [
|
||||
IonNav,
|
||||
tuiNumberFormatProvider({ decimalSeparator: '.', thousandSeparator: '' }),
|
||||
tuiButtonOptionsProvider({ size: 'm' }),
|
||||
tuiTextfieldOptionsProvider({ hintOnDisabled: true }),
|
||||
{
|
||||
provide: TUI_DATE_FORMAT,
|
||||
useValue: 'MDY',
|
||||
|
||||
@@ -14,6 +14,13 @@ export abstract class Control<Spec extends ValueSpec, Value> {
|
||||
return this.control.spec
|
||||
}
|
||||
|
||||
// TODO: Properly handle already set immutable value
|
||||
get readOnly(): boolean {
|
||||
return (
|
||||
!!this.value && !!this.control.control?.pristine && this.control.immutable
|
||||
)
|
||||
}
|
||||
|
||||
get value(): Value | null {
|
||||
return this.control.value
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<div class="label">
|
||||
{{ spec.name }}
|
||||
<tui-tooltip
|
||||
*ngIf="spec.description"
|
||||
[content]="spec.description"
|
||||
*ngIf="spec.description || spec.disabled"
|
||||
[content]="spec | hint"
|
||||
></tui-tooltip>
|
||||
<button
|
||||
tuiLink
|
||||
|
||||
@@ -37,8 +37,9 @@ export class FormArrayComponent {
|
||||
|
||||
get canAdd(): boolean {
|
||||
return (
|
||||
!this.spec.maxLength ||
|
||||
this.spec.maxLength >= this.array.control.controls.length
|
||||
!this.spec.disabled &&
|
||||
(!this.spec.maxLength ||
|
||||
this.spec.maxLength >= this.array.control.controls.length)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
[maskito]="mask"
|
||||
[tuiTextfieldCustomContent]="color"
|
||||
[tuiTextfieldCleaner]="false"
|
||||
[tuiHintContent]="spec.description"
|
||||
[tuiHintContent]="spec | hint"
|
||||
[readOnly]="readOnly"
|
||||
[disabled]="!!spec.disabled"
|
||||
[pseudoInvalid]="invalid"
|
||||
[(ngModel)]="value"
|
||||
(focusedChange)="onFocus($event)"
|
||||
@@ -13,6 +15,7 @@
|
||||
<ng-template #color>
|
||||
<div class="wrapper" [style.color]="value">
|
||||
<input
|
||||
*ngIf="!readOnly && !spec.disabled"
|
||||
type="color"
|
||||
class="color"
|
||||
tabindex="-1"
|
||||
|
||||
@@ -10,8 +10,13 @@
|
||||
<form-toggle *ngSwitchCase="'toggle'"></form-toggle>
|
||||
</ng-container>
|
||||
<tui-error [error]="order | tuiFieldError | async"></tui-error>
|
||||
<ng-template *ngIf="spec.warning" #warning let-completeWith="completeWith">
|
||||
<ng-template
|
||||
*ngIf="spec.warning || immutable"
|
||||
#warning
|
||||
let-completeWith="completeWith"
|
||||
>
|
||||
{{ spec.warning }}
|
||||
<p *ngIf="immutable">This value cannot be changed once set!</p>
|
||||
<div class="buttons">
|
||||
<button
|
||||
tuiButton
|
||||
|
||||
@@ -39,6 +39,10 @@ export class FormControlComponent<
|
||||
readonly order = ERRORS
|
||||
private readonly alerts = inject(TuiAlertService)
|
||||
|
||||
get immutable(): boolean {
|
||||
return 'immutable' in this.spec && this.spec.immutable
|
||||
}
|
||||
|
||||
onFocus(focused: boolean) {
|
||||
this.focused = focused
|
||||
this.updateFocused(focused)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<ng-container [ngSwitch]="spec.inputmode" [tuiHintContent]="spec.description">
|
||||
<tui-input-time
|
||||
*ngSwitchCase="'time'"
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[pseudoInvalid]="invalid"
|
||||
[ngModel]="getTime(value)"
|
||||
(ngModelChange)="value = $event?.toString() || null"
|
||||
@@ -11,6 +14,9 @@
|
||||
</tui-input-time>
|
||||
<tui-input-date
|
||||
*ngSwitchCase="'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"
|
||||
@@ -22,6 +28,9 @@
|
||||
</tui-input-date>
|
||||
<tui-input-date-time
|
||||
*ngSwitchCase="'datetime-local'"
|
||||
[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"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<tui-multi-select
|
||||
[tuiHintContent]="spec.description"
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="disabled"
|
||||
[readOnly]="readOnly"
|
||||
[pseudoInvalid]="invalid"
|
||||
[editable]="false"
|
||||
[disabledItemHandler]="disabledItemHandler"
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Component } from '@angular/core'
|
||||
import { ValueSpecMultiselect } from '@start9labs/start-sdk/lib/config/configTypes'
|
||||
import { Control } from '../control'
|
||||
import { tuiPure } from '@taiga-ui/cdk'
|
||||
import { invert } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'form-multiselect',
|
||||
@@ -11,13 +12,26 @@ export class FormMultiselectComponent extends Control<
|
||||
ValueSpecMultiselect,
|
||||
readonly string[]
|
||||
> {
|
||||
readonly items = Object.values(this.spec.values)
|
||||
private readonly inverted = invert(this.spec.values)
|
||||
|
||||
readonly disabledItemHandler = (item: string): boolean =>
|
||||
private readonly isDisabled = (item: string) =>
|
||||
Array.isArray(this.spec.disabled) &&
|
||||
this.spec.disabled.includes(this.inverted[item])
|
||||
|
||||
private readonly isExceedingLimit = (item: string) =>
|
||||
!!this.spec.maxLength &&
|
||||
this.selected.length >= this.spec.maxLength &&
|
||||
!this.selected.includes(item)
|
||||
|
||||
readonly disabledItemHandler = (item: string): boolean =>
|
||||
this.isDisabled(item) || this.isExceedingLimit(item)
|
||||
|
||||
readonly items = Object.values(this.spec.values)
|
||||
|
||||
get disabled(): boolean {
|
||||
return typeof this.spec.disabled === 'string'
|
||||
}
|
||||
|
||||
get selected(): string[] {
|
||||
return this.memoize(this.value)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<tui-input-number
|
||||
[tuiHintContent]="spec.description"
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[tuiTextfieldPostfix]="spec.units || ''"
|
||||
[pseudoInvalid]="invalid"
|
||||
[precision]="Infinity"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<tui-select
|
||||
[tuiHintContent]="spec.description"
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="disabled"
|
||||
[readOnly]="readOnly"
|
||||
[tuiTextfieldCleaner]="!spec.required"
|
||||
[pseudoInvalid]="invalid"
|
||||
[(ngModel)]="selected"
|
||||
@@ -9,7 +11,7 @@
|
||||
<span *ngIf="spec.required">*</span>
|
||||
<select
|
||||
tuiSelect
|
||||
[labels]="[spec.warning ? '⚠️ ' + spec.warning : '']"
|
||||
[items]="[items]"
|
||||
[items]="items"
|
||||
[disabledItemHandler]="disabledItemHandler"
|
||||
></select>
|
||||
</tui-select>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ValueSpecSelect } from '@start9labs/start-sdk/lib/config/configTypes'
|
||||
import { invert } from '@start9labs/shared'
|
||||
import { Control } from '../control'
|
||||
|
||||
@Component({
|
||||
@@ -7,15 +8,23 @@ import { Control } from '../control'
|
||||
templateUrl: './form-select.component.html',
|
||||
})
|
||||
export class FormSelectComponent extends Control<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.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]
|
||||
}
|
||||
|
||||
set selected(value: string | null) {
|
||||
this.value =
|
||||
Object.entries(this.spec.values).find(([_, v]) => value === v)?.[0] ??
|
||||
null
|
||||
this.value = (value && this.inverted[value]) || null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<tui-input
|
||||
[tuiTextfieldCustomContent]="spec.masked ? toggle : ''"
|
||||
[tuiHintContent]="spec.description"
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[pseudoInvalid]="invalid"
|
||||
[(ngModel)]="value"
|
||||
(focusedChange)="onFocus($event)"
|
||||
@@ -17,13 +19,24 @@
|
||||
/>
|
||||
</tui-input>
|
||||
<ng-template #toggle>
|
||||
<button
|
||||
*ngIf="spec.generate"
|
||||
tuiIconButton
|
||||
type="button"
|
||||
appearance="icon"
|
||||
title="Generate"
|
||||
size="xs"
|
||||
class="button"
|
||||
icon="tuiIconRefreshCcw"
|
||||
(click)="generate()"
|
||||
></button>
|
||||
<button
|
||||
tuiIconButton
|
||||
type="button"
|
||||
appearance="icon"
|
||||
title="Toggle masking"
|
||||
size="xs"
|
||||
class="toggle"
|
||||
class="button"
|
||||
[icon]="masked ? 'tuiIconEye' : 'tuiIconEyeOff'"
|
||||
(click)="masked = !masked"
|
||||
></button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.toggle {
|
||||
.button {
|
||||
pointer-events: auto;
|
||||
margin-left: auto;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.masked {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ValueSpecText } from '@start9labs/start-sdk/lib/config/configTypes'
|
||||
import { Control } from '../control'
|
||||
import { getDefaultString } from '../../../util/config-utilities'
|
||||
|
||||
@Component({
|
||||
selector: 'form-text',
|
||||
@@ -9,4 +10,8 @@ import { Control } from '../control'
|
||||
})
|
||||
export class FormTextComponent extends Control<ValueSpecText, string> {
|
||||
masked = true
|
||||
|
||||
generate() {
|
||||
this.value = getDefaultString(this.spec.generate || '')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<tui-text-area
|
||||
[tuiHintContent]="spec.description"
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[pseudoInvalid]="invalid"
|
||||
[expandable]="true"
|
||||
[rows]="6"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{{ spec.name }}
|
||||
<tui-tooltip
|
||||
*ngIf="spec.description"
|
||||
[content]="spec.description"
|
||||
*ngIf="spec.description || spec.disabled"
|
||||
[tuiHintContent]="spec | hint"
|
||||
></tui-tooltip>
|
||||
<tui-toggle
|
||||
size="l"
|
||||
[disabled]="!!spec.disabled || readOnly"
|
||||
[(ngModel)]="value"
|
||||
(focusedChange)="onFocus($event)"
|
||||
></tui-toggle>
|
||||
|
||||
@@ -48,6 +48,7 @@ import { MustachePipe } from './mustache.pipe'
|
||||
import { ControlDirective } from './control.directive'
|
||||
import { FormColorComponent } from './form-color/form-color.component'
|
||||
import { FormDatetimeComponent } from './form-datetime/form-datetime.component'
|
||||
import { HintPipe } from './hint.pipe'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -98,6 +99,7 @@ import { FormDatetimeComponent } from './form-datetime/form-datetime.component'
|
||||
FormObjectComponent,
|
||||
FormArrayComponent,
|
||||
MustachePipe,
|
||||
HintPipe,
|
||||
ControlDirective,
|
||||
],
|
||||
exports: [FormGroupComponent],
|
||||
|
||||
21
frontend/projects/ui/src/app/components/form/hint.pipe.ts
Normal file
21
frontend/projects/ui/src/app/components/form/hint.pipe.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { ValueSpec } from '@start9labs/start-sdk/lib/config/configTypes'
|
||||
|
||||
@Pipe({
|
||||
name: 'hint',
|
||||
})
|
||||
export class HintPipe implements PipeTransform {
|
||||
transform(spec: ValueSpec): string {
|
||||
const hint = []
|
||||
|
||||
if (spec.description) {
|
||||
hint.push(spec.description)
|
||||
}
|
||||
|
||||
if ('disabled' in spec && typeof spec.disabled === 'string') {
|
||||
hint.push(`Disabled: ${spec.disabled}`)
|
||||
}
|
||||
|
||||
return hint.join('\n\n')
|
||||
}
|
||||
}
|
||||
@@ -717,6 +717,18 @@ export module Mock {
|
||||
),
|
||||
}),
|
||||
),
|
||||
users: Value.multiselect({
|
||||
name: 'Users',
|
||||
default: [],
|
||||
maxLength: 2,
|
||||
disabled: ['matt'],
|
||||
values: {
|
||||
matt: 'Matt Hill',
|
||||
alex: 'Alex Inkin',
|
||||
blue: 'Blue J',
|
||||
lucy: 'Lucy',
|
||||
},
|
||||
}),
|
||||
advanced: Value.object(
|
||||
{
|
||||
name: 'Advanced',
|
||||
@@ -900,19 +912,19 @@ export module Mock {
|
||||
},
|
||||
),
|
||||
),
|
||||
'random-enum': Value.select({
|
||||
name: 'Random Enum',
|
||||
'random-select': Value.select({
|
||||
name: 'Random select',
|
||||
description: 'This is not even real.',
|
||||
warning: 'Be careful changing this!',
|
||||
required: {
|
||||
default: 'null',
|
||||
default: null,
|
||||
},
|
||||
values: {
|
||||
null: 'null',
|
||||
option1: 'option1',
|
||||
option2: 'option2',
|
||||
option3: 'option3',
|
||||
},
|
||||
disabled: ['option2'],
|
||||
}),
|
||||
'favorite-number':
|
||||
/* TODO: Convert range for this value ((-100,100])*/ Value.number({
|
||||
@@ -1037,8 +1049,13 @@ export module Mock {
|
||||
description: 'Options<ul><li>Item 1</li><li>Item 2</li></ul>',
|
||||
warning: 'Careful changing this',
|
||||
required: { default: 'internal' },
|
||||
disabled: ['fake'],
|
||||
},
|
||||
Variants.of({
|
||||
fake: {
|
||||
name: 'Fake',
|
||||
spec: Config.of({}),
|
||||
},
|
||||
internal: {
|
||||
name: 'Internal',
|
||||
spec: Config.of({}),
|
||||
@@ -1113,6 +1130,10 @@ export module Mock {
|
||||
}),
|
||||
'favorite-slogan': Value.text({
|
||||
name: 'Favorite Slogan',
|
||||
generate: {
|
||||
charset: 'a-z,A-Z,2-9',
|
||||
len: 20,
|
||||
},
|
||||
required: false,
|
||||
description:
|
||||
'You most favorite slogan in the whole world, used for paying you.',
|
||||
|
||||
@@ -52,8 +52,6 @@ export class FormService {
|
||||
): ValueSpecSelect {
|
||||
return {
|
||||
...spec,
|
||||
// TODO: implement disabled
|
||||
disabled: false,
|
||||
type: 'select',
|
||||
default: selection,
|
||||
values: Object.fromEntries(
|
||||
|
||||
Reference in New Issue
Block a user