diff --git a/src/app/pages/admin/admin-create-user/admin-create-user.ts b/src/app/pages/admin/admin-create-user/admin-create-user.ts index d266478..facdc42 100644 --- a/src/app/pages/admin/admin-create-user/admin-create-user.ts +++ b/src/app/pages/admin/admin-create-user/admin-create-user.ts @@ -12,7 +12,7 @@ import { AdminCreateUserRequest, UserRole } from '@models/user'; import { Router } from '@angular/router'; import { AdminService } from '@services/admin/admin'; import { FormUtility } from '@utilities/form-utility'; -import { hasValidPasswordComplexity, evaluatePasswordChecks, initialPasswordChecks } from '@utilities/password-validation'; +import { hasValidPasswordComplexity, evaluatePasswordChecks, initialPasswordChecks, PASSWORD_MIN_LENGTH } from '@utilities/password-validation'; @Component({ selector: 'app-admin-create-user-page', @@ -58,7 +58,7 @@ export class AdminCreateUserPage extends FormUtility { return { kind: 'invalidEmail', message: 'Invalid email address' }; }); required(model.password, { message: 'Password is required' }); - minLength(model.password, 8, { message: 'Password must be at least 8 characters long' }); + minLength(model.password, PASSWORD_MIN_LENGTH, { message: `Password must be at least ${PASSWORD_MIN_LENGTH} characters long` }); validate(model.password, ({ value }) => { const password = value(); if (!password || hasValidPasswordComplexity(password)) return null; diff --git a/src/app/pages/admin/admin-users/admin-users.ts b/src/app/pages/admin/admin-users/admin-users.ts index ae69b3b..cd17f8e 100644 --- a/src/app/pages/admin/admin-users/admin-users.ts +++ b/src/app/pages/admin/admin-users/admin-users.ts @@ -17,7 +17,7 @@ import { AdminService } from '@services/admin/admin'; import { ConfirmDialogService } from '@services/dialogs/confirm-dialog'; import { AdminResetPasswordDialogComponent } from '@shared/admin-reset-password-dialog/admin-reset-password-dialog'; import { FormUtility } from '@utilities/form-utility'; -import { hasValidPasswordComplexity, evaluatePasswordChecks, initialPasswordChecks } from '@utilities/password-validation'; +import { hasValidPasswordComplexity, evaluatePasswordChecks, initialPasswordChecks, PASSWORD_MIN_LENGTH } from '@utilities/password-validation'; @Component({ selector: 'app-admin-users', @@ -77,7 +77,7 @@ export class AdminUsersPage extends FormUtility { }; }); required(model.password, { message: 'Password is required' }); - minLength(model.password, 8, { message: 'Password must be at least 8 characters long' }); + minLength(model.password, PASSWORD_MIN_LENGTH, { message: `Password must be at least ${PASSWORD_MIN_LENGTH} characters long` }); validate(model.password, ({ value }) => { const password = value(); if (!password || hasValidPasswordComplexity(password)) return null; diff --git a/src/app/pages/auth/signup/signup.ts b/src/app/pages/auth/signup/signup.ts index ca67c85..bddd936 100644 --- a/src/app/pages/auth/signup/signup.ts +++ b/src/app/pages/auth/signup/signup.ts @@ -10,7 +10,7 @@ import { environment } from '@environment'; import { UserRegistration } from '@models/user'; import { Authorization } from '@services/authorization/authorization'; import { FormUtility } from '@utilities/form-utility'; -import { hasValidPasswordComplexity } from '@utilities/password-validation'; +import { hasValidPasswordComplexity, PASSWORD_MIN_LENGTH } from '@utilities/password-validation'; declare global { interface Window { @@ -91,7 +91,7 @@ export class Signup extends FormUtility implements AfterViewInit, OnDestroy { maxLength(model.username, 20, { message: 'Username cannot exceed 20 characters' }), email(model.email, { message: 'Invalid email address' }), required(model.email, { message: 'Email is required' }), - minLength(model.password, 8, { message: 'Password must be at least 8 characters long' }), + minLength(model.password, PASSWORD_MIN_LENGTH, { message: `Password must be at least ${PASSWORD_MIN_LENGTH} characters long` }), validate(model.password, ({ value }) => { const password = value(); if (!password || hasValidPasswordComplexity(password)) { diff --git a/src/app/shared/admin-reset-password-dialog/admin-reset-password-dialog.ts b/src/app/shared/admin-reset-password-dialog/admin-reset-password-dialog.ts index 487ac98..ad99143 100644 --- a/src/app/shared/admin-reset-password-dialog/admin-reset-password-dialog.ts +++ b/src/app/shared/admin-reset-password-dialog/admin-reset-password-dialog.ts @@ -6,14 +6,7 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { FormUtility } from '@utilities/form-utility'; - -function hasValidPasswordComplexity(value: string): boolean { - return /^\S+$/.test(value) - && /[a-z]/.test(value) - && /[A-Z]/.test(value) - && /\d/.test(value) - && /[^A-Za-z0-9]/.test(value); -} +import { hasValidPasswordComplexity, evaluatePasswordChecks, PASSWORD_MIN_LENGTH } from '@utilities/password-validation'; @Component({ selector: 'app-admin-reset-password-dialog', @@ -40,7 +33,7 @@ export class AdminResetPasswordDialogComponent extends FormUtility { readonly passwordForm = form(this.model, (model) => { required(model.newPassword, { message: 'New password is required' }); - minLength(model.newPassword, 8, { message: 'Password must be at least 8 characters long' }); + minLength(model.newPassword, PASSWORD_MIN_LENGTH, { message: `Password must be at least ${PASSWORD_MIN_LENGTH} characters long` }); validate(model.newPassword, ({ value }) => { const password = value(); if (!password || hasValidPasswordComplexity(password)) return null; @@ -63,17 +56,7 @@ export class AdminResetPasswordDialogComponent extends FormUtility { }); readonly canSubmit = computed(() => !this.passwordForm().invalid() && !this.saving()); - readonly newPasswordChecks = computed(() => { - const password = this.model().newPassword; - return [ - { label: 'At least 8 characters', satisfied: password.length >= 8 }, - { label: 'At least one lowercase letter', satisfied: /[a-z]/.test(password) }, - { label: 'At least one uppercase letter', satisfied: /[A-Z]/.test(password) }, - { label: 'At least one number', satisfied: /\d/.test(password) }, - { label: 'At least one special character', satisfied: /[^A-Za-z0-9]/.test(password) }, - { label: 'No spaces', satisfied: /^\S*$/.test(password) } - ]; - }); + readonly newPasswordChecks = computed(() => evaluatePasswordChecks(this.model().newPassword)); close(event?: Event) { event?.preventDefault(); diff --git a/src/app/shared/change-password-dialog/change-password-dialog.ts b/src/app/shared/change-password-dialog/change-password-dialog.ts index 05786cb..af84b2c 100644 --- a/src/app/shared/change-password-dialog/change-password-dialog.ts +++ b/src/app/shared/change-password-dialog/change-password-dialog.ts @@ -7,14 +7,7 @@ import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { ChangePasswordRequest } from '@models/user'; import { FormUtility } from '@utilities/form-utility'; - -function hasValidPasswordComplexity(value: string): boolean { - return /^\S+$/.test(value) - && /[a-z]/.test(value) - && /[A-Z]/.test(value) - && /\d/.test(value) - && /[^A-Za-z0-9]/.test(value); -} +import { hasValidPasswordComplexity, evaluatePasswordChecks, PASSWORD_MIN_LENGTH } from '@utilities/password-validation'; @Component({ selector: 'app-change-password-dialog', @@ -45,7 +38,7 @@ export class ChangePasswordDialogComponent extends FormUtility { required(model.oldPassword, { message: 'Current password is required' }); required(model.newPassword, { message: 'New password is required' }); - minLength(model.newPassword, 8, { message: 'Password must be at least 8 characters long' }); + minLength(model.newPassword, PASSWORD_MIN_LENGTH, { message: `Password must be at least ${PASSWORD_MIN_LENGTH} characters long` }); validate(model.newPassword, ({ value }) => { const password = value(); if (!password || hasValidPasswordComplexity(password)) return null; @@ -68,17 +61,7 @@ export class ChangePasswordDialogComponent extends FormUtility { }); readonly canSubmit = computed(() => !this.passwordForm().invalid() && !this.saving()); - readonly newPasswordChecks = computed(() => { - const password = this.model().newPassword; - return [ - { label: 'At least 8 characters', satisfied: password.length >= 8 }, - { label: 'At least one lowercase letter', satisfied: /[a-z]/.test(password) }, - { label: 'At least one uppercase letter', satisfied: /[A-Z]/.test(password) }, - { label: 'At least one number', satisfied: /\d/.test(password) }, - { label: 'At least one special character', satisfied: /[^A-Za-z0-9]/.test(password) }, - { label: 'No spaces', satisfied: /^\S*$/.test(password) } - ]; - }); + readonly newPasswordChecks = computed(() => evaluatePasswordChecks(this.model().newPassword)); close(event?: Event) { event?.preventDefault(); diff --git a/src/app/shared/nodes/generic-node/generic-node.ts b/src/app/shared/nodes/generic-node/generic-node.ts index 6353e2a..bfa9cfe 100644 --- a/src/app/shared/nodes/generic-node/generic-node.ts +++ b/src/app/shared/nodes/generic-node/generic-node.ts @@ -1391,6 +1391,12 @@ export class GenericNodeComponent { const propertySchema = resolveSchemaRef(rawPropertySchema as Record, schemaRoot); if (shouldSkipSchemaField(key, propertySchema)) continue; + const fieldUi = this.toFieldUiMeta(propertySchema); + const visible = fieldUi.visibleWhen.every((rule) => + evaluateUiConditionRule(rule, item, (fieldPath) => resolveSchemaPath(itemSchema, fieldPath)) + ); + if (!visible) continue; + if (this.hasDynamicSchema(propertySchema)) { const dynamicFields = await this.buildDynamicSchemaFields(key, propertySchema, item); fields.push(...dynamicFields.fields); @@ -1415,7 +1421,10 @@ export class GenericNodeComponent { rows: fieldType === 'textarea' ? 8 : undefined, options, placeholder: typeof propertySchema?.['x-ui-placeholder'] === 'string' ? String(propertySchema['x-ui-placeholder']) : undefined, - tip: schemaFieldDescription(propertySchema) ?? undefined + tip: schemaFieldDescription(propertySchema) ?? undefined, + readonly: !fieldUi.enabledWhen.every((rule) => + evaluateUiConditionRule(rule, item, (fieldPath) => resolveSchemaPath(itemSchema, fieldPath)) + ) }); if (fieldType === 'checkbox') { @@ -1464,6 +1473,13 @@ export class GenericNodeComponent { for (const [key, rawPropertySchema] of Object.entries(properties)) { const propertySchema = resolveSchemaRef(rawPropertySchema as Record, schemaRoot); if (shouldSkipSchemaField(key, propertySchema)) continue; + + const fieldUi = this.toFieldUiMeta(propertySchema); + const visible = fieldUi.visibleWhen.every((rule) => + evaluateUiConditionRule(rule, result, (fieldPath) => resolveSchemaPath(itemSchema, fieldPath)) + ); + if (!visible) continue; + if (this.hasDynamicSchema(propertySchema)) { const dynamicValue = this.extractNestedDialogValues(result, key); nextItem[key] = Object.keys(dynamicValue).length ? dynamicValue : (previousItem[key] ?? {}); @@ -1608,7 +1624,8 @@ export class GenericNodeComponent { const walk = async ( node: Record, pathPrefix: string, - titlePrefix: string + titlePrefix: string, + inheritedUi?: { visibleWhen: UiConditionRule[]; enabledWhen: UiConditionRule[]; group: string | null } ) => { const resolved = resolveSchemaRef(node, schema); const properties = resolved?.['properties'] as Record | undefined; @@ -1617,13 +1634,27 @@ export class GenericNodeComponent { for (const [childKey, rawChildSchema] of Object.entries(properties)) { const childSchema = resolveSchemaRef(rawChildSchema as Record, schema); if (shouldSkipSchemaField(childKey, childSchema)) continue; + + const childUi = this.toFieldUiMeta(childSchema, inheritedUi); + const fieldRelativePath = pathPrefix === keyPrefix + ? childKey + : `${pathPrefix.slice(`${keyPrefix}.`.length)}.${childKey}`; + const visible = childUi.visibleWhen.every((rule) => + evaluateUiConditionRule(rule, currentRecord, (fieldPath) => resolveSchemaPath(schema, fieldPath)) + ); + if (!visible) continue; + const nextPath = `${pathPrefix}.${childKey}`; const nextLabel = `${titlePrefix} ${schemaFieldLabel(childKey, childSchema)}`; - const currentNestedValue = getValueByPath(currentRecord, nextPath.slice(`${keyPrefix}.`.length)); + const currentNestedValue = getValueByPath(currentRecord, fieldRelativePath); const hasChildren = !!childSchema?.['properties'] || childSchema?.type === 'object'; if (hasChildren) { - await walk(childSchema as Record, nextPath, nextLabel); + await walk(childSchema as Record, nextPath, nextLabel, { + visibleWhen: childUi.visibleWhen, + enabledWhen: childUi.enabledWhen, + group: childUi.group + }); continue; } @@ -1646,7 +1677,10 @@ export class GenericNodeComponent { rows: fieldType === 'textarea' ? 8 : undefined, options, placeholder: typeof childSchema?.['x-ui-placeholder'] === 'string' ? String(childSchema['x-ui-placeholder']) : undefined, - tip: schemaFieldDescription(childSchema) ?? undefined + tip: schemaFieldDescription(childSchema) ?? undefined, + readonly: !childUi.enabledWhen.every((rule) => + evaluateUiConditionRule(rule, currentRecord, (fieldPath) => resolveSchemaPath(schema, fieldPath)) + ) }); if (fieldType === 'checkbox') { diff --git a/src/app/utilities/password-validation.ts b/src/app/utilities/password-validation.ts index 959bcb3..44e6e66 100644 --- a/src/app/utilities/password-validation.ts +++ b/src/app/utilities/password-validation.ts @@ -1,3 +1,5 @@ +export const PASSWORD_MIN_LENGTH = 10; + export function hasValidPasswordComplexity(value: string): boolean { return /^\S+$/.test(value) && /[a-z]/.test(value) @@ -10,7 +12,7 @@ export type PasswordCheck = { label: string; satisfied: boolean }; export function evaluatePasswordChecks(password: string): PasswordCheck[] { return [ - { label: 'At least 8 characters', satisfied: password.length >= 8 }, + { label: `At least ${PASSWORD_MIN_LENGTH} characters`, satisfied: password.length >= PASSWORD_MIN_LENGTH }, { label: 'At least one lowercase letter', satisfied: /[a-z]/.test(password) }, { label: 'At least one uppercase letter', satisfied: /[A-Z]/.test(password) }, { label: 'At least one number', satisfied: /\d/.test(password) }, @@ -21,7 +23,7 @@ export function evaluatePasswordChecks(password: string): PasswordCheck[] { export function initialPasswordChecks(): PasswordCheck[] { return [ - { label: 'At least 8 characters', satisfied: false }, + { label: `At least ${PASSWORD_MIN_LENGTH} characters`, satisfied: false }, { label: 'At least one lowercase letter', satisfied: false }, { label: 'At least one uppercase letter', satisfied: false }, { label: 'At least one number', satisfied: false },