From e000e5ae520a241fe22b6d461df8b5c78ee93980 Mon Sep 17 00:00:00 2001 From: Demerson Carvalho Date: Fri, 20 Feb 2026 17:58:29 -0300 Subject: [PATCH 1/6] DECAF-26 - update --- .prettierrc | 52 ++++++-- .vscode/settings.json | 29 +++-- eslint.config.js => eslint.config.mjs | 69 +++++++--- package.json | 1 + src/app/app.component.ts | 32 ++--- src/app/app.config.ts | 113 +++++++---------- src/app/utils/FakerRepository.ts | 88 +++++++------ src/environments/environment.ts | 27 ++++ src/environments/types.ts | 11 ++ .../file-upload/file-upload.component.ts | 45 +++---- src/lib/engine/NgxComponentDirective.ts | 119 ++++++++---------- src/lib/engine/NgxFormFieldDirective.ts | 65 ++++------ src/lib/engine/NgxModelPageDirective.ts | 3 + src/lib/engine/NgxRepositoryDirective.ts | 2 +- 14 files changed, 355 insertions(+), 301 deletions(-) rename eslint.config.js => eslint.config.mjs (50%) create mode 100644 src/environments/environment.ts create mode 100644 src/environments/types.ts diff --git a/.prettierrc b/.prettierrc index edf00531..c204171f 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,13 +1,49 @@ { - "semi": true, - "singleQuote": true, - "trailingComma": "all", - "printWidth": 100, + "$schema": "https://json.schemastore.org/prettierrc", "tabWidth": 2, - "useTabs": false, + "printWidth": 100, + "singleQuote": true, + "semi": true, + "trailingComma": "es5", "bracketSpacing": true, - "bracketSameLine": false, "arrowParens": "always", - "htmlWhitespaceSensitivity": "ignore", - "proseWrap": "preserve" + "endOfLine": "lf", + + "overrides": [ + { + "files": ["*.ts", "*.tsx"], + "options": { + "printWidth": 120 + } + }, + { + "files": ["*.js", "*.jsx"], + "options": { + "printWidth": 100 + } + }, + { + "files": ["*.html", "*.htm"], + "options": { + "printWidth": 80, + "tabWidth": 2, + "htmlWhitespaceSensitivity": "ignore", + "singleAttributePerLine": true + } + }, + { + "files": ["*.json"], + "options": { + "printWidth": 100 + } + }, + { + "files": ["*.css", "*.scss", "*.less"], + "options": { + "printWidth": 100, + "tabWidth": 2, + "singleQuote": false + } + } + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 06a7a370..e1accaf5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,18 @@ { "cSpell.enabled": false, + "editor.renderWhitespace": "none", + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": false, + "prettier.htmlWhitespaceSensitivity": "ignore", + "prettier.proseWrap": "preserve", + "prettier.bracketSameLine": false, + "prettier.requireConfig": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.fixAll.eslint": "explicit" + }, + "jest.jestCommandLine": "npm run test --", "jest.runMode": "on-demand", "jest.coverageColors": { @@ -14,10 +26,6 @@ "testing.automaticallyOpenTestResults": "neverOpen", "jest.coverageFormatter": "GutterFormatter", - "editor.renderWhitespace": "none", - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": false, - "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": false, @@ -31,6 +39,7 @@ "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": false, "editor.codeActionsOnSave": { + "source.organizeImports": "explicit", // remove unused imports for angular "source.fixAll": "explicit", "source.fixAll.eslint": "explicit", "source.fixAll.stylelint": "never" @@ -84,16 +93,15 @@ }, "[html]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.tabSize": 2, "editor.insertSpaces": true, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": false, + "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll": "explicit", "source.fixAll.eslint": "explicit" } }, - "eslint.validate": [ "javascript", "javascriptreact", @@ -103,10 +111,5 @@ "sass", "css", "html" - ], - - "prettier.htmlWhitespaceSensitivity": "ignore", - "prettier.proseWrap": "preserve", - "prettier.bracketSameLine": false, - "prettier.requireConfig": true + ] } diff --git a/eslint.config.js b/eslint.config.mjs similarity index 50% rename from eslint.config.js rename to eslint.config.mjs index 804a94f2..f0b21a2f 100644 --- a/eslint.config.js +++ b/eslint.config.mjs @@ -1,11 +1,12 @@ -const eslint = require('@eslint/js'); -const tseslint = require('typescript-eslint'); -const angular = require('angular-eslint'); +import eslint from '@eslint/js'; +import angular from 'angular-eslint'; +import unusedImports from 'eslint-plugin-unused-imports'; +import tseslint from 'typescript-eslint'; -module.exports = tseslint.config( +export default tseslint.config( { - files: ['**/*.ts'], ignores: [ + '.vscode/*', 'tests/**', '**/*.spec.*', '**/cli-module.*', @@ -15,6 +16,15 @@ module.exports = tseslint.config( 'src/polyfills.ts', 'src/zone-flags.ts', ], + }, + { + files: ['**/*.ts'], + languageOptions: { + parserOptions: { + project: './tsconfig.json', + }, + }, + extends: [ eslint.configs.recommended, ...tseslint.configs.recommended, @@ -22,43 +32,68 @@ module.exports = tseslint.config( ...angular.configs.tsRecommended, ], processor: angular.processInlineTemplates, + plugins: { + 'unused-imports': unusedImports, + }, rules: { 'prefer-const': 'warn', '@typescript-eslint/no-explicit-any': 'warn', - 'no-useless-constructor': 'off', // Rule disabled but warning kept for TypeScript rule compatibility (line below) + 'no-useless-constructor': 'off', '@typescript-eslint/no-useless-constructor': 'warn', - '@typescript-eslint/no-inferrable-types': 'off', // disabled for compatibility with JSDoc - '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/no-inferrable-types': 'off', '@typescript-eslint/no-empty-function': 'warn', + '@angular-eslint/no-empty-lifecycle-method': 'warn', '@angular-eslint/prefer-inject': 'warn', - '@angular-eslint/component-class-suffix': [ - 'error', - { - suffixes: ['Page', 'Component'], - }, - ], + '@angular-eslint/component-class-suffix': ['error', { suffixes: ['Page', 'Component'] }], '@angular-eslint/directive-selector': [ 'error', { type: 'attribute', prefix: ['ngx-decaf', ''], - style: 'kebab-case', + style: 'camelCase', }, ], '@angular-eslint/component-selector': [ 'error', { type: 'element', - prefix: ['app', 'for-angular', 'decaf', 'ngx-decaf'], // changed to accept for-angular prefix, default "app" + prefix: ['app', 'for-angular', 'decaf', 'ngx-decaf'], style: 'kebab-case', }, ], + + 'unused-imports/no-unused-imports': 'warn', + 'unused-imports/no-unused-vars': [ + 'warn', + { + vars: 'all', + varsIgnorePattern: '^_', + args: 'after-used', + argsIgnorePattern: '^_', + }, + ], + + '@typescript-eslint/no-unused-vars': 'warn', }, }, + + // regras para tipos + { + files: ['**/*.ts', '**/*.spec.ts'], + rules: { + 'unused-imports/no-unused-imports': 'off', + 'unused-imports/no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': 'off', + }, + // Só ativa este override se o arquivo estiver na mesma pasta que types.ts + // ou se o caminho contiver "types" + ignores: ['!**/types.ts', '!**/types/*'], + }, + { files: ['**/*.html'], extends: [...angular.configs.templateRecommended, ...angular.configs.templateAccessibility], rules: {}, - }, + } ); diff --git a/package.json b/package.json index bfa8d3dc..821490b7 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "angular-eslint": "^20.2.0", "eslint": "^9.29.0", "eslint-plugin-storybook": "9.1.5", + "eslint-plugin-unused-imports": "^4.4.1", "ffmpeg-static": "5.2.0", "jest": "^29.7.0", "jest-html-reporters": "^3.1.7", diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 89b9e41f..3ea6a100 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -88,22 +88,22 @@ export class AppComponent extends NgxPageDirective implements OnInit { const menu = []; const models = AppModels; const dbAdapterFlavour = getDbAdapterFlavour(); - for (let model of models) { - uses(dbAdapterFlavour)(model); - if (model instanceof Function) - model = new (model as unknown as ModelConstructor)(); - const name = model.constructor.name.replace(/[0-9]/g, ''); - if (isDevelopment && dbAdapterFlavour.includes(RamFlavour)) { - if (populate.includes(name)) await new FakerRepository(model, 36).initialize(); - } - const label = name.toLowerCase().replace(ModelKeys.MODEL, ''); - if (!menu.length) menu.push({ label: 'models' }); - menu.push({ - label: `${label}`, - url: `/model/${Model.tableName(model)}`, - icon: 'cube-outline', - }); - } + // for (let model of models) { + // uses(dbAdapterFlavour)(model); + // if (model instanceof Function) + // model = new (model as unknown as ModelConstructor)(); + // const name = model.constructor.name.replace(/[0-9]/g, ''); + // if (isDevelopment && dbAdapterFlavour.includes(RamFlavour)) { + // if (populate.includes(name)) await new FakerRepository(model, 36).initialize(); + // } + // const label = name.toLowerCase().replace(ModelKeys.MODEL, ''); + // if (!menu.length) menu.push({ label: 'models' }); + // menu.push({ + // label: `${label}`, + // url: `/model/${Model.tableName(model)}`, + // icon: 'cube-outline', + // }); + // } this.initialized = true; this.menu = [ { diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 2565e6a4..e54953d1 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,92 +1,73 @@ -import { ApplicationConfig } from '@angular/core'; +import { ApplicationConfig, provideAppInitializer } from '@angular/core'; import { + PreloadAllModules, provideRouter, - withComponentInputBinding, RouteReuseStrategy, + withComponentInputBinding, withPreloading, - PreloadAllModules, } from '@angular/router'; -import { RamAdapter } from '@decaf-ts/core/ram'; -import { RootTranslateServiceConfig } from '@ngx-translate/core'; +import { RamAdapter, RamFlavour } from '@decaf-ts/core/ram'; +import { Model } from '@decaf-ts/decorator-validation'; +import { AxiosFlavour } from '@decaf-ts/for-http'; import { IonicRouteStrategy, provideIonicAngular } from '@ionic/angular/standalone'; -import { provideDecafI18nConfig } from 'src/lib/i18n/Loader'; -import { routes } from './app.routes'; +import { RootTranslateServiceConfig } from '@ngx-translate/core'; +import { I18nResourceConfigType } from 'src/lib/engine'; import { + getLogger, provideDecafDbAdapter, - provideDecafPageTransition, provideDecafDynamicComponents, + provideDecafPageTransition, } from 'src/lib/engine/helpers'; -import { AIModel, AIVendorModel } from './models/AIVendorModel'; -import { I18nResourceConfigType } from 'src/lib/engine'; -import { CategoryModel } from './models/CategoryModel'; -import { EmployeeModel } from './models/EmployeeModel'; -import { User } from './forms/FieldsetForm'; -import { Product } from './ew/fabric/Product'; -import { Leaflet } from './ew/fabric/Leaflet'; -import { ProductStrength } from './ew/fabric/ProductStrength'; -import { Batch } from './ew/fabric/Batch'; -import { AppSelectFieldComponent } from './components/select-field/select-field.component'; +import { DecafAxiosHttpAdapter } from 'src/lib/engine/overrides'; +import { provideDecafI18nConfig } from 'src/lib/i18n/Loader'; +import { isDevelopmentMode } from 'src/lib/utils/helpers'; +import { routes } from './app.routes'; import { AppExpiryDateFieldComponent } from './components/expiry-date/expiry-date-field.component'; +import { AppSelectFieldComponent } from './components/select-field/select-field.component'; import { AppSwitcherComponent } from './components/switcher/switcher.component'; -import { ProductImage } from './ew/fabric/ProductImage'; -import { Audit } from './ew/fabric/Audit'; -import { DecafAxiosHttpAdapter } from 'src/lib/engine/overrides'; -// import { AxiosHttpAdapter, HttpAdapter, HttpStatement } from '@decaf-ts/for-http'; +import { Batch } from './ew/fabric/Batch'; +import { Leaflet } from './ew/fabric/Leaflet'; +import { Product } from './ew/fabric/Product'; +import { ProductStrength } from './ew/fabric/ProductStrength'; +import { populateSampleData } from './utils/FakerRepository'; +export const isLocalDevelopmentMode = isDevelopmentMode('local'); export const AppName = 'For Angular'; -// Removed unused Adapter variable and fixed HttpAdapter instantiation issues -// const adapter = new HttpAdapter({ -// protocol: "http", -// host: 'localhost:3000', -// }); - -// export const AppModels = [new CategoryModel(), new EmployeeModel(), new AIModel(), new AIVendorModel()]; - -export const AppModels = [ - // new User(), - new CategoryModel(), - new Product(), - new Batch(), - new ProductImage(), - new Leaflet(), - new Audit(), - // new EmployeeModel(), - // new AIModel(), - // new AIVendorModel(), - new ProductStrength(), -]; +export const DbAdapterFlavour = !isLocalDevelopmentMode ? AxiosFlavour : RamFlavour; +export const AppModels = [] as Model[]; export const AppConfig: ApplicationConfig = { providers: [ + provideAppInitializer(async () => { + const logger = getLogger(provideAppInitializer); + const isDevMode = isLocalDevelopmentMode && DbAdapterFlavour.includes(RamFlavour); + if (isDevMode) { + try { + AppModels.push(new Product(), new ProductStrength(), new Batch(), new Leaflet()); + logger.debug(`AppConfig: Loaded ${AppModels.length} models. Initializing sample data...`); + await populateSampleData(AppModels, ['Product', 'Batch', 'Leaflet'], 6); + } catch (error: unknown) { + logger.error((error as Error)?.message); + } + } + }), + isLocalDevelopmentMode + ? provideDecafDbAdapter(RamAdapter, { user: 'user' }, DbAdapterFlavour) + : provideDecafDbAdapter(DecafAxiosHttpAdapter, { + protocol: 'https', + host: 'ew-backend.ptp.internal', + }), // Providers from ionic angular provideIonicAngular(), + // provideZoneChangeDetection({ eventCoalescing: true }), + { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, + provideRouter(routes, withPreloading(PreloadAllModules), withComponentInputBinding()), // provide dark theme // provideDecafDarkMode(), // change the default page transition provideDecafPageTransition(), - // Providing Local components for dynamic rendering - provideDecafDynamicComponents( - AppExpiryDateFieldComponent, - AppSwitcherComponent, - AppSelectFieldComponent, - ), - // Providing RamAdapter as the database adapter for Decaf - // provideDecafDbAdapter(AxiosHttpAdapter, { - // protocol: "http", - // host: 'localhost:3000', - // responseParser: new NestJSResponseParser() - // }), - - provideDecafDbAdapter(RamAdapter, { user: 'user' }), - // provideDecafDbAdapter(DecafAxiosHttpAdapter, { - // protocol: 'https', - // host: 'ew-backend.ptp.internal', - // }), - // provideZoneChangeDetection({ eventCoalescing: true }), - { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, - - provideRouter(routes, withPreloading(PreloadAllModules), withComponentInputBinding()), + provideDecafDynamicComponents(AppExpiryDateFieldComponent, AppSwitcherComponent, AppSelectFieldComponent), provideDecafI18nConfig( { fallbackLang: 'en', @@ -101,7 +82,7 @@ export const AppConfig: ApplicationConfig = { prefix: './assets/i18n/ew/', suffix: '.json', }, - ] as I18nResourceConfigType, + ] as I18nResourceConfigType ), ], }; diff --git a/src/app/utils/FakerRepository.ts b/src/app/utils/FakerRepository.ts index 3153bf16..a486f8b5 100644 --- a/src/app/utils/FakerRepository.ts +++ b/src/app/utils/FakerRepository.ts @@ -1,20 +1,19 @@ -import { Model } from '@decaf-ts/decorator-validation'; +import { RamFlavour } from '@decaf-ts/core/ram'; +import { uses } from '@decaf-ts/decoration'; +import { Model, ModelConstructor } from '@decaf-ts/decorator-validation'; +import axios from 'axios'; +import { getModelAndRepository } from 'src/lib/engine/helpers'; import { DecafRepository } from 'src/lib/engine/types'; -import { AIModel } from '../models/AIVendorModel'; -import { AIFeatures } from './contants'; import { DecafFakerRepository } from 'src/lib/utils/DecafFakerRepository'; -import { Product } from '../ew/fabric/Product'; -import { Batch } from '../ew/fabric/Batch'; -import { faker } from '@faker-js/faker'; -import { Leaflet, LeafletType } from '../ew/fabric/Leaflet'; +import { DbAdapterFlavour } from '../app.config'; import { Audit } from '../ew/fabric/Audit'; -import { AuditOperations, UserGroup } from '../ew/fabric/constants'; -import { ProductStrength } from '../ew/fabric/ProductStrength'; -import { getModelAndRepository } from 'src/lib/engine/helpers'; -import { generate } from 'rxjs'; +import { Batch } from '../ew/fabric/Batch'; import { generateGtin } from '../ew/fabric/gtin'; -import axios from 'axios'; -import { ProductImage } from '../ew/fabric/ProductImage'; +import { Leaflet } from '../ew/fabric/Leaflet'; +import { Product } from '../ew/fabric/Product'; +import { AIModel } from '../models/AIVendorModel'; +import { AIFeatures } from './contants'; + enum ProductNames { aspirin = 'Aspirin', ibuprofen = 'Ibuprofen', @@ -42,25 +41,29 @@ enum ProductNames { // furosemide = "Furosemide" } -async function getProducts(): Promise { - const repo = getModelAndRepository('Product'); - if (repo) { - const { repository } = repo; - const query = await repository.select().execute(); - if (query.length) { - return query as Product[]; +export async function populateSampleData(models: Model[], populate: string[], limit: number = 12): Promise { + if (DbAdapterFlavour === RamFlavour) { + for (let model of models) { + if (model instanceof Function) { + model = new (model as unknown as ModelConstructor)(); + } + const name = model.constructor.name.replace(/[0-9]/g, ''); + if (populate.includes(name)) { + uses(RamFlavour)(model); + const repository = new FakerRepository(model, limit); + await repository.initialize(); + } } } - return []; } -async function getBatchs(): Promise { - const repo = getModelAndRepository('Batch'); +async function getQueryResults(modelName: string): Promise { + const repo = getModelAndRepository(modelName); if (repo) { const { repository } = repo; const query = await repository.select().execute(); if (query.length) { - return query as Batch[]; + return query as M[]; } } return []; @@ -108,24 +111,22 @@ export class FakerRepository extends DecafFakerRepository { this.propFnMapper = { productCode: () => generateGtin(), }; - data = (await this.generateData(ProductNames, 'inventedName', 'string')).map( - (item: Partial) => { - const productCode = item.productCode; - // const imageData = { - // productCode, - // content: image, - // } as ProductImage; - // item.imageData = imageData; - delete item.imageData; - item.markets = []; - item.strengths = []; - return Model.build(item, Product.name) as T; - }, - ); + data = (await this.generateData(ProductNames, 'inventedName', 'string')).map((item: Partial) => { + const productCode = item.productCode; + // const imageData = { + // productCode, + // content: image, + // } as ProductImage; + // item.imageData = imageData; + delete item.imageData; + item.markets = []; + item.strengths = []; + return Model.build(item, Product.name) as T; + }); break; } case Batch.name: { - const products = await getProducts(); + const products = await getQueryResults('Product'); this.limit = 2; data = await this.generateData(); data = [ @@ -139,13 +140,10 @@ export class FakerRepository extends DecafFakerRepository { `bt_${productCode}_${index % 2 === 0 ? 'aspirin' : this.pickRandomValue(Object.values(ProductNames))}`.trim(); // item.batchNumber = `bt_${productCode}_${item['nameMedicinalProduct']}`.trim(); - item.expiryDate = - index % 2 === 0 - ? new Date('2026-12-30T00:00:00') - : new Date('2020-12-30T23:59:59'); + item.expiryDate = index % 2 === 0 ? new Date('2026-12-30T00:00:00') : new Date('2020-12-30T23:59:59'); return item as T; - }), + }) )), ]; break; @@ -198,7 +196,7 @@ export class FakerRepository extends DecafFakerRepository { this.log .for(this.initialize) .error( - `Error on populate ${this.model?.constructor.name}: ${(error as Error)?.message || (error as string)}`, + `Error on populate ${this.model?.constructor.name}: ${(error as Error)?.message || (error as string)}` ); } } diff --git a/src/environments/environment.ts b/src/environments/environment.ts new file mode 100644 index 00000000..b2ae68a3 --- /dev/null +++ b/src/environments/environment.ts @@ -0,0 +1,27 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. +import { LoggedEnvironment, LogLevel } from '@decaf-ts/logging'; +import { EnvConfig } from './types'; +import { getOnWindow } from 'src/lib/utils'; +const env = (getOnWindow('ENV') || {}) as EnvConfig; +const config: EnvConfig = { + app: env.app || 'EW Frontend', + env: env.env || 'development', + api: { + host: env.api.host || 'localhost:3000', + protocol: env.api.protocol || 'http', + }, + level: LogLevel.debug, +} as EnvConfig; + +export const Environment = LoggedEnvironment.accumulate(config); + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/src/environments/types.ts b/src/environments/types.ts new file mode 100644 index 00000000..6b3272ad --- /dev/null +++ b/src/environments/types.ts @@ -0,0 +1,11 @@ +import { LoggingConfig, LogLevel } from '@decaf-ts/logging'; + +export type EnvConfig = LoggingConfig & { + app: string; + env: string; + api: { + host: string; + protocol: string; + }; + level?: LogLevel; +}; diff --git a/src/lib/components/file-upload/file-upload.component.ts b/src/lib/components/file-upload/file-upload.component.ts index 7e00aecc..c636be43 100644 --- a/src/lib/components/file-upload/file-upload.component.ts +++ b/src/lib/components/file-upload/file-upload.component.ts @@ -1,28 +1,19 @@ import { CommonModule } from '@angular/common'; -import { - Component, - ElementRef, - EventEmitter, - Input, - Output, - ViewChild, - OnInit, - OnDestroy, -} from '@angular/core'; +import { Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; -import { IonItem, IonLabel, IonList, IonButton, IonText } from '@ionic/angular/standalone'; -import { TranslatePipe } from '@ngx-translate/core'; -import { ComponentEventNames, ElementSizes, HTML5InputTypes } from '@decaf-ts/ui-decorators'; import { Constructor } from '@decaf-ts/decoration'; import { Primitives } from '@decaf-ts/decorator-validation'; +import { ComponentEventNames, ElementSizes, HTML5InputTypes } from '@decaf-ts/ui-decorators'; +import { IonButton, IonItem, IonLabel, IonList, IonText } from '@ionic/angular/standalone'; +import { TranslatePipe } from '@ngx-translate/core'; import { Dynamic } from '../../engine/decorators'; +import { IBaseCustomEvent, IFileUploadError } from '../../engine/interfaces'; +import { NgxEventHandler } from '../../engine/NgxEventHandler'; import { NgxFormFieldDirective } from '../../engine/NgxFormFieldDirective'; import { ElementSize, FlexPosition, KeyValue, PossibleInputTypes } from '../../engine/types'; -import { IBaseCustomEvent, IFileUploadError } from '../../engine/interfaces'; -import { presentNgxInlineModal, presentNgxLightBoxModal } from '../modal/modal.component'; import { CardComponent } from '../card/card.component'; import { IconComponent } from '../icon/icon.component'; -import { NgxEventHandler } from '../../engine/NgxEventHandler'; +import { presentNgxInlineModal, presentNgxLightBoxModal } from '../modal/modal.component'; const FileErrors = { notAllowed: 'not_allowed', @@ -134,6 +125,9 @@ export class FileUploadComponent extends NgxFormFieldDirective implements OnInit @Input() override type: PossibleInputTypes = HTML5InputTypes.FILE; + @Input() + subType: 'string' | 'file' = 'string'; + /** * @description Label for the upload button. * @summary Specifies the text displayed on the file upload button. @@ -305,9 +299,7 @@ export class FileUploadComponent extends NgxFormFieldDirective implements OnInit }); this.getPreview(); } catch (error: unknown) { - this.log - .for(this.initialize) - .error(`Error parsing file list: ${(error as Error).message || error}`); + this.log.for(this.initialize).error(`Error parsing file list: ${(error as Error).message || error}`); } } } @@ -446,7 +438,7 @@ export class FileUploadComponent extends NgxFormFieldDirective implements OnInit } if (this.files.length) { const dataValues = await this.getDataURLs(this.files as File[]); - this.setValue(JSON.stringify(dataValues)); + this.setValue(this.subType === 'string' ? JSON.stringify(dataValues) : this.files); } await this.getPreview(); @@ -471,11 +463,7 @@ export class FileUploadComponent extends NgxFormFieldDirective implements OnInit if (ext === '*') return true; if (ext.endsWith('/*')) return file.type.startsWith(ext.replace(/\/\*$/, '')); const fileExtension = file.type.split('/').pop() || ''; - return ( - file.type === ext || - fileExtension === ext || - file.name.toLowerCase().endsWith(ext.replace('.', '')) - ); + return file.type === ext || fileExtension === ext || file.name.toLowerCase().endsWith(ext.replace('.', '')); }); if (!accept) return FileErrors.notAllowed; } @@ -498,8 +486,7 @@ export class FileUploadComponent extends NgxFormFieldDirective implements OnInit const dataUrl = (await this.getDataURLs(file)) as string[]; if (dataUrl && dataUrl.length) file = dataUrl[0]; } - if (fileExtension.includes('image')) - content = ''; + if (fileExtension.includes('image')) content = ''; if (fileExtension.includes('xml')) { const parseXml = (xmlString: string): string | undefined => { @@ -680,8 +667,8 @@ export class FileUploadComponent extends NgxFormFieldDirective implements OnInit reader.onerror = () => reject(reader.error); reader.onload = () => resolve(String(reader.result || '')); reader.readAsDataURL(file); - }), - ), + }) + ) ); } } diff --git a/src/lib/engine/NgxComponentDirective.ts b/src/lib/engine/NgxComponentDirective.ts index 33ef560c..3aea26b3 100644 --- a/src/lib/engine/NgxComponentDirective.ts +++ b/src/lib/engine/NgxComponentDirective.ts @@ -6,51 +6,43 @@ * It centralizes shared behavior for child components and simplifies integration with the rendering engine. * @link {@link NgxComponentDirective} */ +import { Location } from '@angular/common'; import { + ChangeDetectorRef, Directive, ElementRef, + EnvironmentInjector, EventEmitter, Inject, inject, Input, - Output, - SimpleChanges, - ViewChild, OnChanges, - ChangeDetectorRef, - Renderer2, OnDestroy, + Output, + Renderer2, signal, + SimpleChanges, + ViewChild, WritableSignal, - EnvironmentInjector, } from '@angular/core'; import { NavigationStart, Router } from '@angular/router'; -import { Location } from '@angular/common'; -import { shareReplay, Subject, takeUntil } from 'rxjs'; -import { Model, ModelConstructor, ModelKeys, Primitives } from '@decaf-ts/decorator-validation'; import { CrudOperations, InternalError, OperationKeys } from '@decaf-ts/db-decorators'; -import { ComponentEventNames, DecafEventHandler } from '@decaf-ts/ui-decorators'; -import { - DecafRepository, - FormParent, - FunctionLike, - KeyValue, - PropsMapperFn, - WindowColorScheme, -} from './types'; -import { IBaseCustomEvent, ICrudFormEvent } from './interfaces'; -import {} from './NgxEventHandler'; +import { Model, ModelConstructor, ModelKeys, Primitives } from '@decaf-ts/decorator-validation'; +import { ComponentEventNames, DecafEventHandler, UIFunctionLike, UIKeys } from '@decaf-ts/ui-decorators'; +import { OverlayBaseController } from '@ionic/angular/common'; +import { LoadingController, LoadingOptions } from '@ionic/angular/standalone'; +import { shareReplay, Subject, takeUntil } from 'rxjs'; import { getLocaleContext } from '../i18n/Loader'; -import { NgxRenderingEngine } from './NgxRenderingEngine'; -import { AngularEngineKeys, BaseComponentProps, CPTKN, WindowColorSchemes } from './constants'; -import { generateRandomValue, getWindow, setOnWindow } from '../utils'; import { NgxMediaService } from '../services/NgxMediaService'; -import { UIFunctionLike, UIKeys } from '@decaf-ts/ui-decorators'; -import { LoadingController, LoadingOptions } from '@ionic/angular/standalone'; -import { OverlayBaseController } from '@ionic/angular/common'; +import { NgxTranslateService } from '../services/NgxTranslateService'; +import { generateRandomValue, getWindow, setOnWindow } from '../utils'; +import { AngularEngineKeys, BaseComponentProps, CPTKN, WindowColorSchemes } from './constants'; import { getModelAndRepository } from './helpers'; +import { IBaseCustomEvent, ICrudFormEvent } from './interfaces'; +import { } from './NgxEventHandler'; +import { NgxRenderingEngine } from './NgxRenderingEngine'; import { NgxRepositoryDirective } from './NgxRepositoryDirective'; -import { NgxTranslateService } from '../services/NgxTranslateService'; +import { DecafRepository, FormParent, FunctionLike, KeyValue, PropsMapperFn, WindowColorScheme } from './types'; try { const win = getWindow(); @@ -74,10 +66,7 @@ try { * @memberOf module:lib/engine/NgxComponentDirective */ @Directive({ host: { '[attr.id]': 'uid' } }) -export abstract class NgxComponentDirective - extends NgxRepositoryDirective - implements OnChanges, OnDestroy -{ +export abstract class NgxComponentDirective extends NgxRepositoryDirective implements OnChanges, OnDestroy { /** * @description Reference to the component's native DOM element. * @summary Provides direct access to the native DOM element of the component through Angular's @@ -97,9 +86,7 @@ export abstract class NgxComponentDirective * the instance. Signals ensure Angular change detection reacts to prop updates. * @type {WritableSignal} */ - _props: WritableSignal = signal( - {} as NgxComponentDirective, - ); + _props: WritableSignal = signal({} as NgxComponentDirective); /** * @description Flag to enable or disable dark mode support for the component. @@ -369,9 +356,7 @@ export abstract class NgxComponentDirective * @memberOf module:lib/engine/NgxComponentDirective */ @Output() - listenEvent: EventEmitter = new EventEmitter< - IBaseCustomEvent | ICrudFormEvent - >(); + listenEvent: EventEmitter = new EventEmitter(); /** * @description Event emitter for custom component events. @@ -383,9 +368,7 @@ export abstract class NgxComponentDirective * @memberOf module:lib/engine/NgxComponentDirective */ @Output() - refreshEvent: EventEmitter = new EventEmitter< - IBaseCustomEvent | boolean - >(); + refreshEvent: EventEmitter = new EventEmitter(); /** * @description Angular Router instance for programmatic navigation. @@ -485,8 +468,7 @@ export abstract class NgxComponentDirective * @returns {OverlayBaseController} * @memberOf module:lib/engine/NgxComponentDirective */ - protected loadingController: OverlayBaseController = - inject(LoadingController); + protected loadingController: OverlayBaseController = inject(LoadingController); /** * @description Flag indicating if the component is rendered as a child of a modal dialog. @@ -567,7 +549,6 @@ export abstract class NgxComponentDirective this.mediaService.darkModeEnabled(); // connect component to media service for color scheme toggling this.mediaService.colorSchemeObserver(this.component); - if (!this.initialized && Object.keys(this.props || {}).length) { this.parseProps(this); } @@ -575,15 +556,13 @@ export abstract class NgxComponentDirective this.refreshing = false; } - this.router.events - .pipe(shareReplay(1), takeUntil(this.destroySubscriptions$)) - .subscribe(async (event) => { - if (event instanceof NavigationStart) { - if (this.value) { - await this.ngOnDestroy(); - } + this.router.events.pipe(shareReplay(1), takeUntil(this.destroySubscriptions$)).subscribe(async (event) => { + if (event instanceof NavigationStart) { + if (this.value) { + await this.ngOnDestroy(); } - }); + } + }); this.route = this.router.url.replace('/', ''); @@ -766,11 +745,7 @@ export abstract class NgxComponentDirective protected checkDarkMode(): void { this.mediaService.isDarkMode().subscribe((isDark) => { this.isDarkMode = isDark; - this.mediaService.toggleClass( - [this.component], - AngularEngineKeys.DARK_PALETTE_CLASS, - this.isDarkMode, - ); + this.mediaService.toggleClass([this.component], AngularEngineKeys.DARK_PALETTE_CLASS, this.isDarkMode); }); } @@ -857,8 +832,7 @@ export abstract class NgxComponentDirective * @memberOf module:lib/engine/NgxComponentDirective */ getRoute(): string { - if (!this.route && this.model instanceof Model) - this.route = `/model/${this.model?.constructor.name}`; + if (!this.route && this.model instanceof Model) this.route = `/model/${this.model?.constructor.name}`; return this.route || ''; } @@ -1016,8 +990,7 @@ export abstract class NgxComponentDirective async handleEvent(event: IBaseCustomEvent & ICrudFormEvent & CustomEvent): Promise { let name = ''; if (event instanceof CustomEvent) { - if (!event.detail) - return this.log.for(this.handleEvent).debug(`No details for custom event ${name}`); + if (!event.detail) return this.log.for(this.handleEvent).debug(`No details for custom event ${name}`); name = event.detail?.name; event = event.detail; } @@ -1110,21 +1083,31 @@ export abstract class NgxComponentDirective } async initProps( - props: T | KeyValue, - map: (keyof T)[] | KeyValue = [], - instance?: NgxComponentDirective & T, + props: T & KeyValue, + map: (keyof T)[] | Record = [], + instance?: NgxComponentDirective & T ): Promise { - this._props = signal(props as T); - if (!instance) instance = this as KeyValue as T; + if (!instance) { + instance = this as T & KeyValue; + } if (Array.isArray(map)) { map.forEach((key: keyof T) => { - if (key in instance && instance[key]) (props as T)[key as keyof T] = instance[key]; + if (key in instance && instance[key]) { + props[key] = instance[key]; + } }); + } else { + props = { ...props, ...map }; + map = Object.keys(map) as (keyof T)[]; } + this._props = signal(props as T); + Object.entries(props).filter(([key, value]) => { - if (key in instance || map.includes(key as keyof T)) instance[key as keyof T] = value; + const prop = key as keyof T; + if (prop in instance || map.includes(prop)) { + instance[prop] = value; + } }); - await this.initialize(); } } diff --git a/src/lib/engine/NgxFormFieldDirective.ts b/src/lib/engine/NgxFormFieldDirective.ts index 70022b6d..88fd6e3f 100644 --- a/src/lib/engine/NgxFormFieldDirective.ts +++ b/src/lib/engine/NgxFormFieldDirective.ts @@ -6,28 +6,16 @@ * This directive handles form control lifecycle, validation, multi-entry forms, and CRUD operations. */ import { Directive, Inject, Input, OnChanges, SimpleChanges } from '@angular/core'; -import { - AbstractControl, - ControlValueAccessor, - FormArray, - FormControl, - FormGroup, -} from '@angular/forms'; -import { sf } from '@decaf-ts/logging'; -import { Primitives } from '@decaf-ts/decorator-validation'; +import { AbstractControl, ControlValueAccessor, FormArray, FormControl, FormGroup } from '@angular/forms'; import { CrudOperations, InternalError, OperationKeys } from '@decaf-ts/db-decorators'; -import { ComponentEventNames, UIValidator } from '@decaf-ts/ui-decorators'; -import { - FieldProperties, - HTML5InputTypes, - RenderingError, - UIEventProperty, -} from '@decaf-ts/ui-decorators'; -import { NgxComponentDirective } from './NgxComponentDirective'; +import { Primitives } from '@decaf-ts/decorator-validation'; +import { sf } from '@decaf-ts/logging'; +import { ComponentEventNames, FieldProperties, HTML5InputTypes, RenderingError, UIEventProperty, UIValidator } from '@decaf-ts/ui-decorators'; +import { SelectCustomEvent } from '@ionic/angular/standalone'; import { NgxFormService } from '../services/NgxFormService'; +import { AngularEngineKeys, CPTKN } from './constants'; +import { NgxComponentDirective } from './NgxComponentDirective'; import { FormParent, FunctionLike, KeyValue, PossibleInputTypes } from './types'; -import { CPTKN, AngularEngineKeys } from './constants'; -import { SelectCustomEvent } from '@ionic/angular/standalone'; /** * @description Abstract base directive for CRUD form fields in Angular applications. @@ -327,8 +315,7 @@ export abstract class NgxFormFieldDirective if (!this.formGroup) return this.formControl.parent as FormGroup; if (this.multiple) { - if (this.formGroup instanceof FormArray) - return this.formGroup.at(this.activeFormGroupIndex) as FormGroup; + if (this.formGroup instanceof FormArray) return this.formGroup.at(this.activeFormGroupIndex) as FormGroup; return this.formGroup; } @@ -438,7 +425,7 @@ export abstract class NgxFormFieldDirective parent = NgxFormService.getParentEl(this.component.nativeElement, 'div'); } catch (e: unknown) { throw new RenderingError( - `Unable to retrieve parent form element for the ${this.operation}: ${e instanceof Error ? e.message : e}`, + `Unable to retrieve parent form element for the ${this.operation}: ${e instanceof Error ? e.message : e}` ); } // NgxFormService.register(parent.id, this.formGroup, this as AngularFieldDefinition); @@ -492,6 +479,14 @@ export abstract class NgxFormFieldDirective if (this.formGroup) NgxFormService.unregister(this.formGroup); } + getValue(): unknown { + const value = this.formControl?.value || this.formGroup?.get(this.name)?.value; + if (value) { + this.value = value; + } + return value; + } + /** * @description Sets the value of the form control. * @summary Updates the form control's value and triggers validation. This is used @@ -574,10 +569,8 @@ export abstract class NgxFormFieldDirective // Check if string already be translated by validator const translate = (message: string, args: string[]): string => { return this.translateService.instant( - !message.includes(AngularEngineKeys.ERRORS) - ? `${AngularEngineKeys.ERRORS}.${message}` - : message, - args, + !message.includes(AngularEngineKeys.ERRORS) ? `${AngularEngineKeys.ERRORS}.${message}` : message, + args ); }; return /\s/.test(message) && message !== key ? message : translate(message, args); @@ -595,20 +588,16 @@ export abstract class NgxFormFieldDirective getErrors(parent: HTMLElement): string | void { const formControl = this.formControl; if (formControl) { - const accordionComponent = parent - .closest('ngx-decaf-fieldset') - ?.querySelector('ion-accordion-group'); + const accordionComponent = parent.closest('ngx-decaf-fieldset')?.querySelector('ion-accordion-group'); const invalid = this.validateControl(formControl); if (invalid) { - const errors: Record[] = Object.entries(formControl.errors ?? {}).map( - ([key, value]) => { - const message = typeof value === 'boolean' ? key : value; - return { - key: key, - message, - }; - }, - ); + const errors: Record[] = Object.entries(formControl.errors ?? {}).map(([key, value]) => { + const message = typeof value === 'boolean' ? key : value; + return { + key: key, + message, + }; + }); if (errors.length) { if (accordionComponent && !this.validationErrorEventDispatched) { const validationErrorEvent = new CustomEvent(ComponentEventNames.ValidationError, { diff --git a/src/lib/engine/NgxModelPageDirective.ts b/src/lib/engine/NgxModelPageDirective.ts index ca75ee2d..9076bb52 100644 --- a/src/lib/engine/NgxModelPageDirective.ts +++ b/src/lib/engine/NgxModelPageDirective.ts @@ -342,6 +342,9 @@ export abstract class NgxModelPageDirective extends NgxPageDirective implements Model.build(data, model.constructor.name as string); this.name = prop; this.model = Model.build({ [prop]: _model }, modelName); + if (!Object.keys(this._data || {}).length) { + this._data = _model as Model; + } } else { // model[parent] = { // ...model[parent], diff --git a/src/lib/engine/NgxRepositoryDirective.ts b/src/lib/engine/NgxRepositoryDirective.ts index b3608f95..28537457 100644 --- a/src/lib/engine/NgxRepositoryDirective.ts +++ b/src/lib/engine/NgxRepositoryDirective.ts @@ -30,7 +30,7 @@ export class NgxRepositoryDirective extends DecafComponent { _query: M[] = []; /** - * @description Backing model data supplied to the component. + * @description Backing main model data supplied to the component. * @summary Holds the raw `Model` instance or a generic key-value payload that child * components may bind to. When provided, it represents the contextual data the * component should render or mutate. From b5ade8f2ec3d2741c2d2a0b8db1a7e1ce75fb37b Mon Sep 17 00:00:00 2001 From: Demerson Carvalho Date: Fri, 20 Feb 2026 18:07:47 -0300 Subject: [PATCH 2/6] DECAF-26 - update deps --- package-lock.json | 72 +++++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index 173b337b..3c31f269 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "angular-eslint": "^20.2.0", "eslint": "^9.29.0", "eslint-plugin-storybook": "9.1.5", + "eslint-plugin-unused-imports": "^4.4.1", "ffmpeg-static": "5.2.0", "jest": "^29.7.0", "jest-html-reporters": "^3.1.7", @@ -2750,9 +2751,9 @@ } }, "node_modules/@decaf-ts/core": { - "version": "0.8.41", - "resolved": "https://registry.npmjs.org/@decaf-ts/core/-/core-0.8.41.tgz", - "integrity": "sha512-QiUUeN/ouyl0eM0VF5WwOUVTlQ7HsC7M7lq6x/QmkO5g5G1bGUDKN3si3zfpLSJT5Dp77IjG57pnvxwIZ9Q2EA==", + "version": "0.8.51", + "resolved": "https://registry.npmjs.org/@decaf-ts/core/-/core-0.8.51.tgz", + "integrity": "sha512-FSWomHsBolpT9vLF8vTbA86Qpq1irgtBU0IGnJvllWRB8VVjRzQ2aopAyoqeQjSF6UsDWPVmkBt5CQ4HGmCyAA==", "license": "MPL-2.0", "peer": true, "dependencies": { @@ -2768,9 +2769,9 @@ } }, "node_modules/@decaf-ts/db-decorators": { - "version": "0.8.19", - "resolved": "https://registry.npmjs.org/@decaf-ts/db-decorators/-/db-decorators-0.8.19.tgz", - "integrity": "sha512-j9xPvAvmHsjcVZClB+FV8ndFTOH4dzx9PJ6TMDiIF9IkCRFO+AgllzOS/4I+oQvN1hKFYq1MbHI1/05RgQa5Gw==", + "version": "0.8.26", + "resolved": "https://registry.npmjs.org/@decaf-ts/db-decorators/-/db-decorators-0.8.26.tgz", + "integrity": "sha512-foG2wKwjjNFUFZ5FQ71Zdm/AtyZ7SuDgfgl9GtbvTtjfufsLfLb7kUlZKaDKDHLqvZPpzZJ0h/LR9S/VUn9PhQ==", "license": "MIT", "peer": true, "dependencies": { @@ -2785,9 +2786,9 @@ } }, "node_modules/@decaf-ts/decoration": { - "version": "0.8.7", - "resolved": "https://registry.npmjs.org/@decaf-ts/decoration/-/decoration-0.8.7.tgz", - "integrity": "sha512-zZ7P2raTx+XYpVMSA9bucQ5rrjP3Pmyfn90M3qZvgdO5iOgvF7M1kwunHulUAxpKPvNHKTD6RghxDB9EwKlSXA==", + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@decaf-ts/decoration/-/decoration-0.8.8.tgz", + "integrity": "sha512-ls5zew+muhYmpLhdZ6eW3dDcDCBbDlsKELSsZ4f60Omrw5+Cc8zDZE4jxTzmnIiwJW0cs5Y1FK2oZ8FhE6ispA==", "license": "MIT", "peer": true, "dependencies": { @@ -2799,9 +2800,9 @@ } }, "node_modules/@decaf-ts/decorator-validation": { - "version": "1.11.18", - "resolved": "https://registry.npmjs.org/@decaf-ts/decorator-validation/-/decorator-validation-1.11.18.tgz", - "integrity": "sha512-9yD52I5uMcY2/P/A6y2NoPh5D7qFhnQKX0X5Dek03qy1J/oy51GIEhYjZ9V8tmkONpUncRmFDiJ7TzFG1O8VmA==", + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/@decaf-ts/decorator-validation/-/decorator-validation-1.11.19.tgz", + "integrity": "sha512-RHAoIM7yaOaUtl/Y/OCu+t033DBNtNJ3C2QZ9mz60tMJKFnh6Xfo8qupDBr34VOsiE0qsYpzeOIME0EVLM1JHg==", "license": "MIT", "peer": true, "dependencies": { @@ -2896,9 +2897,9 @@ } }, "node_modules/@decaf-ts/ui-decorators": { - "version": "0.6.15", - "resolved": "https://registry.npmjs.org/@decaf-ts/ui-decorators/-/ui-decorators-0.6.15.tgz", - "integrity": "sha512-Asy4lFO0hjoE5l9LIOq5Xmd1mvo0zFomGPlfMqftgxqBC7BK6hpot0LzRgvUF2ODOZFDux5CNaG6BmusS6jL5w==", + "version": "0.6.16", + "resolved": "https://registry.npmjs.org/@decaf-ts/ui-decorators/-/ui-decorators-0.6.16.tgz", + "integrity": "sha512-xLMW4UKAwpNdriTTixuKLkKW8ekt0eNfjQ5Gow83y1Zd2oKPKKbQjshvyr0PYqG6tvOeyY6/DVmuLR+o8Mc4pA==", "license": "MPL-2.0 OR AGPL-3.0", "peer": true, "dependencies": { @@ -2912,9 +2913,9 @@ } }, "node_modules/@decaf-ts/utils": { - "version": "0.11.9", - "resolved": "https://registry.npmjs.org/@decaf-ts/utils/-/utils-0.11.9.tgz", - "integrity": "sha512-teIw6DzaBmR0CMRECtIyMw6V8Ftwh7/a2Nq4Ql+gQ/TDoO0sknJ0lkM49aN+KeR8Ycfy+FJKK5dT++Z+bu51qg==", + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@decaf-ts/utils/-/utils-0.11.10.tgz", + "integrity": "sha512-LsQQfUmuxCFQU09SO9ZOChpzXdRCoJlFHCGNY1Nf2DXn86Xz4sIbsBWEOtrqt8vFfwr2wbRuwy0SrvnHuugRmQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2947,7 +2948,7 @@ "prettier": "3.5.3", "prompts": "^2.4.2", "rimraf": "^6.0.1", - "rollup": "^2.79.2", + "rollup": "^4.0.0", "shell-quote": "^1.8.3", "styled-string-builder": "latest", "ts-jest": "^29.3.2", @@ -3100,20 +3101,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@decaf-ts/utils/node_modules/rollup": { - "version": "2.79.2", - "dev": true, - "license": "MIT", - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=10.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, "node_modules/@derhuerst/http-basic": { "version": "8.2.4", "dev": true, @@ -8684,6 +8671,7 @@ "version": "8.34.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.34.1", @@ -12340,6 +12328,22 @@ "storybook": "^9.1.5" } }, + "node_modules/eslint-plugin-unused-imports": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.4.1.tgz", + "integrity": "sha512-oZGYUz1X3sRMGUB+0cZyK2VcvRX5lm/vB56PgNNcU+7ficUCKm66oZWKUubXWnOuPjQ8PvmXtCViXBMONPe7tQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", + "eslint": "^10.0.0 || ^9.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, "node_modules/eslint-scope": { "version": "9.0.0", "dev": true, @@ -20133,7 +20137,6 @@ "version": "1.90.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -23040,6 +23043,7 @@ "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", From 711f413a7ee31bae75ee381efc0682818082e6d2 Mon Sep 17 00:00:00 2001 From: Demerson Carvalho Date: Fri, 20 Feb 2026 18:15:51 -0300 Subject: [PATCH 3/6] DECAF-26 - fix lint --- eslint.config.mjs | 2 +- src/lib/engine/NgxFormFieldDirective.ts | 12 ++++++-- src/lib/engine/overrides.ts | 40 +++++++++---------------- 3 files changed, 25 insertions(+), 29 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index f0b21a2f..57065efd 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -51,7 +51,7 @@ export default tseslint.config( { type: 'attribute', prefix: ['ngx-decaf', ''], - style: 'camelCase', + style: 'kebab-case', }, ], '@angular-eslint/component-selector': [ diff --git a/src/lib/engine/NgxFormFieldDirective.ts b/src/lib/engine/NgxFormFieldDirective.ts index 88fd6e3f..131fd466 100644 --- a/src/lib/engine/NgxFormFieldDirective.ts +++ b/src/lib/engine/NgxFormFieldDirective.ts @@ -10,7 +10,14 @@ import { AbstractControl, ControlValueAccessor, FormArray, FormControl, FormGrou import { CrudOperations, InternalError, OperationKeys } from '@decaf-ts/db-decorators'; import { Primitives } from '@decaf-ts/decorator-validation'; import { sf } from '@decaf-ts/logging'; -import { ComponentEventNames, FieldProperties, HTML5InputTypes, RenderingError, UIEventProperty, UIValidator } from '@decaf-ts/ui-decorators'; +import { + ComponentEventNames, + FieldProperties, + HTML5InputTypes, + RenderingError, + UIEventProperty, + UIValidator, +} from '@decaf-ts/ui-decorators'; import { SelectCustomEvent } from '@ionic/angular/standalone'; import { NgxFormService } from '../services/NgxFormService'; import { AngularEngineKeys, CPTKN } from './constants'; @@ -546,7 +553,8 @@ export abstract class NgxFormFieldDirective private getErrorMessage(error: Record): string { const instance = this as KeyValue; - let { message, key } = error; + let { message } = error; + const { key } = error; const prop = instance[key]; let args = [] as string[]; if (typeof message === Primitives.STRING) { diff --git a/src/lib/engine/overrides.ts b/src/lib/engine/overrides.ts index 5fe325ee..7125b965 100644 --- a/src/lib/engine/overrides.ts +++ b/src/lib/engine/overrides.ts @@ -1,6 +1,4 @@ -import { AxiosFlags, AxiosFlavour, AxiosHttpAdapter, HttpConfig } from '@decaf-ts/for-http'; -import { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'; -import { ContextualArgs, Context, PersistenceKeys, PreparedStatementKeys } from '@decaf-ts/core'; +import { Context, ContextualArgs, PersistenceKeys, PreparedStatementKeys } from '@decaf-ts/core'; import { BaseError, BulkCrudOperationKeys, @@ -10,22 +8,21 @@ import { } from '@decaf-ts/db-decorators'; import { Constructor } from '@decaf-ts/decoration'; import { Model, ModelKeys } from '@decaf-ts/decorator-validation'; +import { AxiosFlags, AxiosFlavour, AxiosHttpAdapter, HttpConfig } from '@decaf-ts/for-http'; +import { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'; export class DecafAxiosHttpAdapter extends AxiosHttpAdapter { constructor( config: HttpConfig, alias: string = AxiosFlavour, - protected enableCredentials: boolean = true, + protected enableCredentials: boolean = true ) { super(config, alias); } parseStatementURL(url: string): string { const urlArray = url.split('/'); - if ( - urlArray.includes(PersistenceKeys.STATEMENT) && - !urlArray.includes(PreparedStatementKeys.PAGE_BY) - ) { + if (urlArray.includes(PersistenceKeys.STATEMENT) && !urlArray.includes(PreparedStatementKeys.PAGE_BY)) { return urlArray.filter((part) => part !== PersistenceKeys.STATEMENT).join('/'); } return url; @@ -33,17 +30,13 @@ export class DecafAxiosHttpAdapter extends AxiosHttpAdapter { token?: string; - override async request( - details: AxiosRequestConfig, - ...args: ContextualArgs> - ): Promise { + override async request(details: AxiosRequestConfig, ...args: ContextualArgs>): Promise { let overrides = {}; try { const { ctx } = this.logCtx(args, this.request); overrides = this.toRequest(ctx); - // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (err: unknown) { - // do nothing + this.log.debug(`Error generating request overrides: ${(err as Error).message}.`); } if (this.token) { overrides = { @@ -72,7 +65,7 @@ export class DecafAxiosHttpAdapter extends AxiosHttpAdapter { } return await this.client.request( - Object.assign({}, details, { url: this.parseStatementURL(details.url || '') }, overrides), + Object.assign({}, details, { url: this.parseStatementURL(details.url || '') }, overrides) ); } @@ -86,9 +79,7 @@ export class DecafAxiosHttpAdapter extends AxiosHttpAdapter { try { const url = this.url(tableName); const cfg = this.toRequest(ctx); - log.debug( - `POSTing to ${url} with ${JSON.stringify(model)} and cfg ${JSON.stringify(cfg)} and primary key ${id}`, - ); + log.debug(`POSTing to ${url} with ${JSON.stringify(model)} and cfg ${JSON.stringify(cfg)} and primary key ${id}`); const result = await this.request>( { url, @@ -96,11 +87,11 @@ export class DecafAxiosHttpAdapter extends AxiosHttpAdapter { data: JSON.stringify( Object.assign({}, model, { [ModelKeys.ANCHOR]: tableName.name, - }), + }) ), ...cfg, }, - ctx, + ctx ); return result; } catch (error: unknown) { @@ -111,16 +102,13 @@ export class DecafAxiosHttpAdapter extends AxiosHttpAdapter { override async parseResponse( clazz: Constructor, method: OperationKeys | string, - res: AxiosResponse & { body: unknown; error: Error | AxiosError }, + res: AxiosResponse & { body: unknown; error: Error | AxiosError } ) { - if (!res.status && method !== PersistenceKeys.STATEMENT) - throw new InternalError('this should be impossible'); + if (!res.status && method !== PersistenceKeys.STATEMENT) throw new InternalError('this should be impossible'); if (res.status >= 400) { console.log(res instanceof AxiosError); throw this.parseError( - res?.request?.response - ? JSON.parse(res.request.response)?.error - : res.error || `${res.status}`, + res?.request?.response ? JSON.parse(res.request.response)?.error : res.error || `${res.status}` ); } if (!res.body && res.data) { From b0f09e63f444fde80a328c53b3ddc26402d381ae Mon Sep 17 00:00:00 2001 From: Demerson Carvalho Date: Fri, 20 Feb 2026 18:25:46 -0300 Subject: [PATCH 4/6] DECAF-26 - fix lint --- src/lib/components/file-upload/file-upload.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/components/file-upload/file-upload.component.ts b/src/lib/components/file-upload/file-upload.component.ts index c636be43..49d6971c 100644 --- a/src/lib/components/file-upload/file-upload.component.ts +++ b/src/lib/components/file-upload/file-upload.component.ts @@ -126,7 +126,7 @@ export class FileUploadComponent extends NgxFormFieldDirective implements OnInit override type: PossibleInputTypes = HTML5InputTypes.FILE; @Input() - subType: 'string' | 'file' = 'string'; + subType: 'text' | 'file' = 'text'; /** * @description Label for the upload button. @@ -438,7 +438,7 @@ export class FileUploadComponent extends NgxFormFieldDirective implements OnInit } if (this.files.length) { const dataValues = await this.getDataURLs(this.files as File[]); - this.setValue(this.subType === 'string' ? JSON.stringify(dataValues) : this.files); + this.setValue(this.subType === 'text' ? JSON.stringify(dataValues) : this.files); } await this.getPreview(); From e86221bcbffaaec7c079c2ff51675ad110ecf46b Mon Sep 17 00:00:00 2001 From: Demerson Carvalho Date: Fri, 20 Feb 2026 19:04:30 -0300 Subject: [PATCH 5/6] DECAF-26 - update validatior factory --- src/app/ew/fabric/Leaflet.ts | 53 ++++++--------------- src/lib/engine/ValidatorFactory.ts | 74 +++++++----------------------- 2 files changed, 31 insertions(+), 96 deletions(-) diff --git a/src/app/ew/fabric/Leaflet.ts b/src/app/ew/fabric/Leaflet.ts index c56b4ff9..030ead24 100644 --- a/src/app/ew/fabric/Leaflet.ts +++ b/src/app/ew/fabric/Leaflet.ts @@ -1,24 +1,16 @@ -import { Comparison, Model, type ModelArg } from '@decaf-ts/decorator-validation'; -import { model, required } from '@decaf-ts/decorator-validation'; +import { Comparison, Model, model, required, type ModelArg } from '@decaf-ts/decorator-validation'; import { LeafletFile } from './LeafletFile'; // import { gtin } from "@pharmaledgerassoc/ptp-toolkit/shared"; // import { BatchPattern, TableNames } from "@pharmaledgerassoc/ptp-toolkit/shared"; -import { - Cascade, - column, - index, - oneToMany, - OrderDirection, - pk, - Repository, - table, -} from '@decaf-ts/core'; +import { Cascade, column, index, oneToMany, OrderDirection, pk, table } from '@decaf-ts/core'; import { composed, InternalError, OperationKeys, readonly } from '@decaf-ts/db-decorators'; import { description } from '@decaf-ts/decoration'; -import { Cacheable } from './Cacheable'; import { + ComponentEventNames, DecafComponent, DecafEventHandler, + ElementPositions, + ElementSizes, HTML5InputTypes, uielement, uihandlers, @@ -26,15 +18,13 @@ import { uimodel, uionrender, uitablecol, - ComponentEventNames, - ElementPositions, - ElementSizes, } from '@decaf-ts/ui-decorators'; -import { Product } from './Product'; import { getDocumentTypes, getLeafletLanguages, getMarkets } from 'src/app/ew/utils/helpers'; import { Batch } from './Batch'; +import { Cacheable } from './Cacheable'; import { TableNames } from './constants'; import { LeafletHandler } from './handlers/LeafletHandler'; +import { Product } from './Product'; import { audit } from './utils'; export enum LeafletType { @@ -58,10 +48,7 @@ export enum LeafletType { export class Leaflet extends Cacheable { @pk() @audit() - @composed(['productCode', 'batchNumber', 'leafletType', 'lang', 'epiMarket'], ':', [ - 'batchNumber', - 'epiMarket', - ]) + @composed(['productCode', 'batchNumber', 'leafletType', 'lang', 'epiMarket'], ':', ['batchNumber', 'epiMarket']) @description('Unique identifier composed of product code, batch number, and language.') id!: string; @@ -142,11 +129,9 @@ export class Leaflet extends Cacheable { operation: OperationKeys[]; }; if (instance.headers) - instance.headers = instance.headers.map((header) => - header === 'batchNumber' ? 'batchCol' : header, - ); + instance.headers = instance.headers.map((header) => (header === 'batchNumber' ? 'batchCol' : header)); } - }, + } ) @uitablecol(1, async (instance: DecafComponent & { type: string }, value: string) => { if (instance.operation && ['paginated', 'infinite'].includes(instance.type)) value = 'subinfo'; // fallback mapper to list item position @@ -232,11 +217,12 @@ export class Leaflet extends Cacheable { size: ElementSizes.small, position: ElementPositions.left, required: true, + subType: 'text', maxFileSize: 10, // previewHandler: XmlPreviewHandler, accept: ['image/*', '.xml'], }) - xmlFileContent!: string; + xmlFileContent!: string | LeafletFile; //@cache() @oneToMany(() => LeafletFile, { update: Cascade.CASCADE, delete: Cascade.CASCADE }, false) @@ -247,21 +233,10 @@ export class Leaflet extends Cacheable { super(model); } - override compare( - other: M, - ...exceptions: (keyof M)[] - ): Comparison | undefined { + override compare(other: M, ...exceptions: (keyof M)[]): Comparison | undefined { return super.compare( other as any, - ...([ - ...new Set([ - exceptions, - 'updatedAt', - 'updatedBy', - 'otherFilesContent', - 'xmlFileContent', - ]).values(), - ] as any[]), + ...([...new Set([exceptions, 'updatedAt', 'updatedBy', 'otherFilesContent', 'xmlFileContent']).values()] as any[]) ); } diff --git a/src/lib/engine/ValidatorFactory.ts b/src/lib/engine/ValidatorFactory.ts index 75a29dfe..b1a90feb 100644 --- a/src/lib/engine/ValidatorFactory.ts +++ b/src/lib/engine/ValidatorFactory.ts @@ -7,13 +7,7 @@ * * @link {@link ValidatorFactory} */ -import { - AbstractControl, - FormControl, - FormGroup, - ValidationErrors, - ValidatorFn, -} from '@angular/forms'; +import { AbstractControl, FormControl, FormGroup, ValidationErrors, ValidatorFn } from '@angular/forms'; import { ComparisonValidationKeys, PathProxy, @@ -23,16 +17,11 @@ import { ValidationKeys, Validator, } from '@decaf-ts/decorator-validation'; -import { - FieldProperties, - HTML5InputTypes, - parseValueByType, - UIKeys, -} from '@decaf-ts/ui-decorators'; +import { FieldProperties, HTML5InputTypes, parseValueByType, UIKeys } from '@decaf-ts/ui-decorators'; +import { patternValidators } from './constants'; +import { getLogger } from './helpers'; import { NgxRenderingEngine } from './NgxRenderingEngine'; import { ComparisonValidationKey, KeyValue } from './types'; -import { getLogger } from './helpers'; -import { patternValidators } from './constants'; export class ValidatorFactory { /** @@ -45,14 +34,8 @@ export class ValidatorFactory { * @param {FieldProperties} fieldProps - The field properties containing type conversion metadata. * @returns {unknown} The parsed value or undefined if the control value is undefined. */ - static getFieldValue( - control: AbstractControl, - fieldType: string, - fieldProps: FieldProperties, - ): unknown { - return typeof control.value !== 'undefined' - ? parseValueByType(fieldType, control.value, fieldProps) - : undefined; + static getFieldValue(control: AbstractControl, fieldType: string, fieldProps: FieldProperties): unknown { + return typeof control.value !== 'undefined' ? parseValueByType(fieldType, control.value, fieldProps) : undefined; } /** * @summary Resolves the effective field type from multiple possible type sources. @@ -65,17 +48,9 @@ export class ValidatorFactory { * @param {string} [subType] - Secondary type definition. * @returns {string} The resolved field type. */ - static getFieldType( - type?: string, - customTypes?: string | string[], - options?: unknown[], - subType?: string, - ): string { + static getFieldType(type?: string, customTypes?: string | string[], options?: unknown[], subType?: string): string { const fieldType = (customTypes || subType || type) as string; - if ( - (fieldType === HTML5InputTypes.CHECKBOX || fieldType === Array.name) && - Array.isArray(options) - ) { + if ((fieldType === HTML5InputTypes.CHECKBOX || fieldType === Array.name) && Array.isArray(options)) { return Primitives.STRING; } return fieldType; @@ -104,7 +79,7 @@ export class ValidatorFactory { key, fieldType, fieldProps, - customValidator ? (subType as string) : undefined, + customValidator ? (subType as string) : undefined ); const validator = Validation.get(validatorKey) as Validator; // parseValueByType does not support undefined values @@ -142,9 +117,7 @@ export class ValidatorFactory { * @returns The validator value to use. */ static getValidatorValue(key: string, type: string, fieldProps: FieldProperties): unknown { - return key === ValidationKeys.TYPE && - HTML5InputTypes.CHECKBOX && - fieldProps[key as keyof FieldProperties] !== type + return key === ValidationKeys.TYPE && HTML5InputTypes.CHECKBOX && fieldProps[key as keyof FieldProperties] !== type ? type : fieldProps[key as keyof FieldProperties]; } @@ -158,14 +131,11 @@ export class ValidatorFactory { * @param customTypes - Optional custom type definition. * @returns True if validation should use type-based resolution. */ - static isTypeBasedValidation( - key: string, - type: string, - customTypes: string | undefined, - ): boolean { + static isTypeBasedValidation(key: string, type: string, customTypes: string | undefined): boolean { return ( key === ValidationKeys.TYPE && - (typeof customTypes === Primitives.STRING || Object.keys(patternValidators).includes(type)) + ((typeof customTypes === Primitives.STRING && HTML5InputTypes.TEXT !== customTypes) || + Object.keys(patternValidators).includes(type)) ); } @@ -179,15 +149,10 @@ export class ValidatorFactory { * @param {ComparisonValidationKey} key - The validation key determining proxy scope. * @returns {PathProxy} A proxy object for form value access. */ - static getValidatorProxy( - control: AbstractControl | FormGroup, - key: ComparisonValidationKey, - ): PathProxy { + static getValidatorProxy(control: AbstractControl | FormGroup, key: ComparisonValidationKey): PathProxy { const proxy = ValidatorFactory.createProxy({} as AbstractControl); if (Object.values(ComparisonValidationKeys).includes(key)) { - return ValidatorFactory.createProxy( - (control instanceof FormGroup ? control : control.parent) as FormGroup, - ); + return ValidatorFactory.createProxy((control instanceof FormGroup ? control : control.parent) as FormGroup); } return proxy; } @@ -247,7 +212,7 @@ export class ValidatorFactory { key: string, type: string, fieldProps: FieldProperties, - customTypes: string | undefined = undefined, + customTypes: string | undefined = undefined ): { validatorKey: string; props: KeyValue } => { const isTypeBased = this.isTypeBasedValidation(key, type, customTypes); const validatorKey = isTypeBased ? type : key; @@ -266,12 +231,7 @@ export class ValidatorFactory { * @param value - The value to validate. * @returns An object containing validator properties. */ - static getValidatorProps( - validatorKey: string, - type: string, - isTypeBased: boolean, - value: unknown, - ): KeyValue { + static getValidatorProps(validatorKey: string, type: string, isTypeBased: boolean, value: unknown): KeyValue { return { [validatorKey]: !isTypeBased && validatorKey === ValidationKeys.TYPE From e3d06b5065e88bcad405940c8637968c5e5d734a Mon Sep 17 00:00:00 2001 From: tvenceslau Date: Fri, 20 Feb 2026 22:13:07 +0000 Subject: [PATCH 6/6] DECAF-212 - adds spec kit --- .storybook/preview.ts | 8 +++++++- package-lock.json | 9 +++++++++ package.json | 7 +++++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 21fc06cf..e4810909 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,4 +1,10 @@ import type { Preview } from '@storybook/angular' +// .storybook/preview.ts +if (!(globalThis as any).process) { + (globalThis as any).process = { env: {} }; +} else if (!(globalThis as any).process.env) { + (globalThis as any).process.env = {}; +} const preview: Preview = { parameters: { @@ -11,4 +17,4 @@ const preview: Preview = { }, }; -export default preview; \ No newline at end of file +export default preview; diff --git a/package-lock.json b/package-lock.json index 3c31f269..31afdf76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "@decaf-ts/for-angular", "version": "0.0.93", "license": "MPL-2.0 OR AGPL-3.0", + "dependencies": { + "xtermjs": "*" + }, "devDependencies": { "@angular-devkit/schematics": "^20.2.0", "@angular-devkit/schematics-cli": "^20.2.0", @@ -46,6 +49,9 @@ "node": ">=20.0.0", "npm": ">=10.0.0" }, + "optionalDependencies": { + "xtermjs": "npm:null@*" + }, "peerDependencies": { "@angular/animations": "^20.3.16", "@angular/common": "^20.3.16", @@ -23955,6 +23961,9 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/xtermjs": { + "optional": true + }, "node_modules/y18n": { "version": "5.0.8", "dev": true, diff --git a/package.json b/package.json index 821490b7..cee4ff52 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "uml": "cd workdocs/uml && for FILE in ./*.puml; do docker run --rm -v $(pwd):/work -w /work miy4/plantuml -DPLANTUML_LIMIT_SIZE=8192 -tpng $FILE; done && cd ../.. && cp -fr workdocs/uml/*.png workdocs/resources/", "docs": "npx rimraf ./docs && mkdir docs && npx build-scripts --docs", "publish-docs": "docker run -it --rm --user $(id -u):$(id -g) -v \"$(pwd)/workdocs/confluence:/content\" -e ATLASSIAN_API_TOKEN=$(cat .confluence-token) ghcr.io/markdown-confluence/publish:latest", - "storybook": "ng run for-angular:storybook", + "storybook": "ng run for-angular:storybook --debug-webpack", "storybook:build": "ng run for-angular:build-storybook", "playwright": "npx playwright test --config=./playwright.config.ts", "playwright-ui": "npx playwright test --config=./playwright.config.ts --ui", @@ -106,6 +106,7 @@ "@angular/platform-browser": "^20.3.16", "@angular/platform-browser-dynamic": "^20.3.16", "@angular/router": "^20.3.16", + "@bwip-js/browser": "^4.8.0", "@decaf-ts/core": "latest", "@decaf-ts/db-decorators": "latest", "@decaf-ts/decoration": "latest", @@ -123,7 +124,6 @@ "ionicons": "^7.4.0", "reflect-metadata": "^0.2.1", "rxjs": "~7.8.0", - "@bwip-js/browser": "^4.8.0", "zone.js": "^0.15.1" }, "schematics": "./schematics/collection.json", @@ -131,5 +131,8 @@ "extends": [ "plugin:storybook/recommended" ] + }, + "optionalDependencies": { + "xtermjs": "npm:null@*" } }