Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import {WrappedBoolean} from "../data-field-template/models/wrapped-boolean";
})
export abstract class AbstractBaseDataFieldComponent<T extends DataField<unknown>> implements OnDestroy {

private static readonly TRUE_VALUES = ['true', '1', 'yes', 'y', 'ano', 'áno', 'pravda'];
private static readonly FALSE_VALUES = ['false', '0', 'no', 'n', 'nie', 'nepravda'];

@Input() public dataField: T;
@Input() public formControlRef: FormControl;
@Input() public showLargeLayout: WrappedBoolean;
Expand All @@ -21,7 +24,7 @@ export abstract class AbstractBaseDataFieldComponent<T extends DataField<unknown
this.showLargeLayout = dataFieldPortalData.showLargeLayout;
if (!this.dataField.initialized) {
this.formControlRef = new FormControl('', {updateOn: this.dataField.getUpdateOnStrategy()});
this.dataField.registerFormControl(this.formControlRef)
this.dataField.registerFormControl(this.formControlRef);
}
}
}
Expand All @@ -35,6 +38,51 @@ export abstract class AbstractBaseDataFieldComponent<T extends DataField<unknown
&& property in this.dataField.component.properties;
}

public getBooleanComponentProperty(property: string, defaultValue = false): boolean {
return AbstractBaseDataFieldComponent.resolveBooleanProperty(
this.dataField?.component?.properties?.[property],
defaultValue
);
}

public getNumberComponentProperty(property: string, defaultValue: number): number {
return AbstractBaseDataFieldComponent.resolveNumberProperty(
this.dataField?.component?.properties?.[property],
defaultValue
);
}

public static resolveBooleanProperty(value: unknown, defaultValue = false): boolean {
if (value === undefined || value === null || value === '') {
return defaultValue;
}

if (typeof value === 'boolean') {
return value;
}

if (typeof value === 'string') {
const normalized = value.trim().toLowerCase();
if (AbstractBaseDataFieldComponent.TRUE_VALUES.includes(normalized)) {
return true;
}
if (AbstractBaseDataFieldComponent.FALSE_VALUES.includes(normalized)) {
return false;
}
return defaultValue;
}
return Boolean(value);
}

public static resolveNumberProperty(value: unknown, defaultValue: number): number {
if (value === undefined || value === null || value === '') {
return defaultValue;
}

const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : defaultValue;
}

public hasTitle(): boolean {
return this.dataField.title !== undefined && this.dataField.title !== '';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,27 @@ export abstract class AbstractDateTimeDefaultFieldComponent extends AbstractTime
@Inject(MAT_DATE_LOCALE) protected _locale: string,
protected _languageService: LanguageService,
@Optional() @Inject(DATA_FIELD_PORTAL_DATA) dataFieldPortalData: DataFieldPortalData<DateTimeField>) {
super(_translate, _adapter, _locale, _languageService, dataFieldPortalData)
super(_translate, _adapter, _locale, _languageService, dataFieldPortalData);
}

public get showSeconds(): boolean {
return this.dataField?.showSeconds ?? false;
}

public get stepHour(): number {
return this.dataField?.stepHour ?? 1;
}

public get stepMinute(): number {
return this.dataField?.stepMinute ?? 5;
}

public get stepSecond(): number {
return this.dataField?.stepSecond ?? 1;
}

public get enableMeridian(): boolean {
return this.dataField?.enableMeridian ?? false;
}

getErrorMessage() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,64 @@ import {AbstractTimeInstanceField} from '../../time-instance-abstract-field/mode
import {Layout} from '../../models/layout';
import {Validation} from '../../models/validation';
import {Component, ComponentPrefixes} from '../../models/component';
import {AbstractBaseDataFieldComponent} from '../../base-component/abstract-base-data-field.component';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Decouple DateTimeField model from AbstractBaseDataFieldComponent.

Importing a UI component class into a model for static parsing helpers couples layers unnecessarily. Move these parsers into a neutral utility (e.g., field-property parser util) and reuse from both model/component layers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@projects/netgrif-components-core/src/lib/data-fields/date-time-field/models/date-time-field.ts`
at line 7, DateTimeField currently imports AbstractBaseDataFieldComponent for
static parsing helpers; remove that dependency by extracting the parsing logic
into a neutral utility (e.g., FieldPropertyParser or date-time specific parser
util) and have DateTimeField use the new util instead; update the static parser
helpers referenced on the DateTimeField model (and any usages in
AbstractBaseDataFieldComponent) to import from the new util so the model no
longer imports the UI component class.


export class DateTimeField extends AbstractTimeInstanceField {

public static readonly SHOW_SECONDS_PROPERTY = 'showSeconds';

public static readonly ENABLE_MERIDIAN_PROPERTY = 'enableMeridian';

public static readonly STEP_HOUR_PROPERTY = 'stepHour';
public static readonly STEP_MINUTE_PROPERTY = 'stepMinute';
public static readonly STEP_SECOND_PROPERTY = 'stepSecond';

constructor(stringId: string, title: string, value: Moment, behavior: Behavior, placeholder?: string,
description?: string, layout?: Layout, validations?: Array<Validation>, component?: Component, parentTaskId?: string) {
super(stringId, title, value, behavior, placeholder, description, layout, validations, component, parentTaskId);
}

public get showSeconds(): boolean {
return AbstractBaseDataFieldComponent.resolveBooleanProperty(
this.component?.properties?.[DateTimeField.SHOW_SECONDS_PROPERTY],
false
);
}

public get stepHour(): number {
return AbstractBaseDataFieldComponent.resolveNumberProperty(
this.component?.properties?.[DateTimeField.STEP_HOUR_PROPERTY],
1
);
}

public get stepMinute(): number {
return AbstractBaseDataFieldComponent.resolveNumberProperty(
this.component?.properties?.[DateTimeField.STEP_MINUTE_PROPERTY],
5
);
}


public get stepSecond(): number {
return AbstractBaseDataFieldComponent.resolveNumberProperty(
this.component?.properties?.[DateTimeField.STEP_SECOND_PROPERTY],
1
);
}
Comment on lines +31 to +51
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Constrain step properties to positive integers before exposing them.

stepHour, stepMinute, and stepSecond currently pass through any finite number (including 0, negative, or decimal). Add a > 0 integer guard and fallback defaults.

Proposed fix
 public get stepHour(): number {
-    return AbstractBaseDataFieldComponent.resolveNumberProperty(
+    const value = AbstractBaseDataFieldComponent.resolveNumberProperty(
         this.component?.properties?.[DateTimeField.STEP_HOUR_PROPERTY],
         1
     );
+    return Number.isInteger(value) && value > 0 ? value : 1;
 }

 public get stepMinute(): number {
-    return AbstractBaseDataFieldComponent.resolveNumberProperty(
+    const value = AbstractBaseDataFieldComponent.resolveNumberProperty(
         this.component?.properties?.[DateTimeField.STEP_MINUTE_PROPERTY],
         5
     );
+    return Number.isInteger(value) && value > 0 ? value : 5;
 }

 public get stepSecond(): number {
-    return AbstractBaseDataFieldComponent.resolveNumberProperty(
+    const value = AbstractBaseDataFieldComponent.resolveNumberProperty(
         this.component?.properties?.[DateTimeField.STEP_SECOND_PROPERTY],
         1
     );
+    return Number.isInteger(value) && value > 0 ? value : 1;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public get stepHour(): number {
return AbstractBaseDataFieldComponent.resolveNumberProperty(
this.component?.properties?.[DateTimeField.STEP_HOUR_PROPERTY],
1
);
}
public get stepMinute(): number {
return AbstractBaseDataFieldComponent.resolveNumberProperty(
this.component?.properties?.[DateTimeField.STEP_MINUTE_PROPERTY],
5
);
}
public get stepSecond(): number {
return AbstractBaseDataFieldComponent.resolveNumberProperty(
this.component?.properties?.[DateTimeField.STEP_SECOND_PROPERTY],
1
);
}
public get stepHour(): number {
const value = AbstractBaseDataFieldComponent.resolveNumberProperty(
this.component?.properties?.[DateTimeField.STEP_HOUR_PROPERTY],
1
);
return Number.isInteger(value) && value > 0 ? value : 1;
}
public get stepMinute(): number {
const value = AbstractBaseDataFieldComponent.resolveNumberProperty(
this.component?.properties?.[DateTimeField.STEP_MINUTE_PROPERTY],
5
);
return Number.isInteger(value) && value > 0 ? value : 5;
}
public get stepSecond(): number {
const value = AbstractBaseDataFieldComponent.resolveNumberProperty(
this.component?.properties?.[DateTimeField.STEP_SECOND_PROPERTY],
1
);
return Number.isInteger(value) && value > 0 ? value : 1;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@projects/netgrif-components-core/src/lib/data-fields/date-time-field/models/date-time-field.ts`
around lines 31 - 51, The getters stepHour, stepMinute, and stepSecond should
validate the resolved number to ensure it is a positive integer (>0) and
otherwise fall back to their defaults (1, 5, 1); change each getter to call
AbstractBaseDataFieldComponent.resolveNumberProperty as now, then
coerce/validate the result from DateTimeField.STEP_HOUR_PROPERTY /
STEP_MINUTE_PROPERTY / STEP_SECOND_PROPERTY into a positive integer (e.g.,
Math.floor or equivalent) and if the value is not a finite integer > 0 return
the respective default.


public get enableMeridian(): boolean {
return AbstractBaseDataFieldComponent.resolveBooleanProperty(
this.component?.properties?.[DateTimeField.ENABLE_MERIDIAN_PROPERTY],
false
);
}

public getTypedComponentType(): string {
return ComponentPrefixes.DATE_TIME + this.getComponentType();
}

protected valueEquality(a: Moment, b: Moment): boolean {
return AbstractTimeInstanceField.isEqual(a, b, 'minute');
return AbstractTimeInstanceField.isEqual(a, b, 'second');
}
}
16 changes: 16 additions & 0 deletions projects/netgrif-components-core/src/lib/moment/time-formats.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const DATE_FORMAT_STRING = 'DD.MM.YYYY';
export const DATE_TIME_FORMAT_STRING = 'DD.MM.YYYY HH:mm';
export const DATE_TIME_SECONDS_FORMAT_STRING = 'DD.MM.YYYY HH:mm:ss';

// https://momentjs.com/docs/#/displaying/format/
export const DATE_FORMAT = {
Expand All @@ -25,3 +26,18 @@ export const DATE_TIME_FORMAT = {
monthYearA11yLabel: 'MMMM YYYY',
},
};

export const DATE_TIME_SECONDS_FORMAT = {
parse: {
dateInput: [
DATE_TIME_SECONDS_FORMAT_STRING,
DATE_TIME_FORMAT_STRING
],
},
display: {
dateInput: DATE_TIME_SECONDS_FORMAT_STRING,
monthYearLabel: 'MMM YYYY',
dateA11yLabel: 'Do MMMM YYYY HH:mm:ss',
monthYearA11yLabel: 'MMMM YYYY',
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import {UserField} from '../../data-fields/user-field/models/user-field';
import {ButtonField} from '../../data-fields/button-field/models/button-field';
import {FileField, FileUploadMIMEType} from '../../data-fields/file-field/models/file-field';
import moment from 'moment';
import moment, {Moment} from 'moment';
import {UserValue} from '../../data-fields/user-field/models/user-value';
import {FieldTypeResource} from '../model/field-type-resource';
import {FileListField} from '../../data-fields/file-list-field/models/file-list-field';
Expand All @@ -23,15 +23,16 @@
import {I18nField} from '../../data-fields/i18n-field/models/i18n-field';
import {UserListField} from '../../data-fields/user-list-field/models/user-list-field';
import {UserListValue} from '../../data-fields/user-list-field/models/user-list-value';
import {decodeBase64, encodeBase64} from "../../utility/base64";
import {decodeBase64, encodeBase64} from '../../utility/base64';
import {CaseRefField} from '../../data-fields/case-ref-field/model/case-ref-field';
import {StringCollectionField} from '../../data-fields/string-collection-field/models/string-collection-field';

@Injectable({
providedIn: 'root'
})
export class FieldConverterService {
private textFieldNames = [ 'richtextarea', 'htmltextarea', 'editor', 'htmlEditor' ]

private textFieldNames = ['richtextarea', 'htmltextarea', 'editor', 'htmlEditor'];

Check warning on line 35 in projects/netgrif-components-core/src/lib/task-content/services/field-converter.service.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'textFieldNames' is never reassigned; mark it as `readonly`.

See more on https://sonarcloud.io/project/issues?id=netgrif_components&issues=AZ343vdcuLVDl_1VWNQ1&open=AZ343vdcuLVDl_1VWNQ1&pullRequest=327

constructor() {
}
Expand All @@ -41,6 +42,7 @@
case FieldTypeResource.BOOLEAN:
return new BooleanField(item.stringId, item.name, item.value as boolean, item.behavior,
item.placeholder, item.description, item.layout, item.validations, item.component, item.parentTaskId);

case FieldTypeResource.TEXT:
if (this.textFieldNames.includes(item.component?.name)) {
return new TextAreaField(item.stringId, item.name, this.resolveTextValue(item, item.value), item.behavior,
Expand Down Expand Up @@ -70,12 +72,7 @@
return new DateField(item.stringId, item.name, date, item.behavior, item.placeholder,
item.description, item.layout, item.validations, item.component, item.parentTaskId);
case FieldTypeResource.DATE_TIME:
let dateTime;
if (item.value) {
dateTime = moment(new Date(item.value[0], item.value[1] - 1, item.value[2], item.value[3], item.value[4]));
}
return new DateTimeField(item.stringId, item.name, dateTime, item.behavior,
item.placeholder, item.description, item.layout, item.validations, item.component, item.parentTaskId);
return new DateTimeField(item.stringId, item.name, this.resolveDateTime(item.value), item.behavior, item.placeholder, item.description, item.layout, item.validations, item.component, item.parentTaskId);
case FieldTypeResource.USER:
let user;
if (item.value) {
Expand Down Expand Up @@ -291,7 +288,7 @@
return new UserValue(value.id, value.name, value.surname, value.email);
}
if (this.resolveType(field) === FieldTypeResource.DATE_TIME) {
return moment(new Date(value[0], value[1] - 1, value[2], value[3], value[4]));
return this.resolveDateTime(value);
}
if (this.resolveType(field) === FieldTypeResource.MULTICHOICE) {
const array = [];
Expand All @@ -317,8 +314,33 @@
return value;
}

protected resolveDateTime(value: any): Moment | undefined {
if (!value) {
return undefined;
}
if (moment.isMoment(value)) {
return value;
}
if (value instanceof Date) {
return moment(value);
}
if (Array.isArray(value)) {
const [year, month, day, hour = 0, minute = 0, second = 0, millisecond = 0] = value;
return moment({
year,
month: month - 1,
date: day,
hour,
minute,
second,
millisecond
});
}
return moment(value);
}
Comment on lines +317 to +340
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle falsy-but-valid input and invalid parse output in resolveDateTime.

Line 318 currently treats 0 as empty, and invalid parsed values are returned unguarded. Use a nullish/empty-string check and return undefined for invalid parse results.

Proposed fix
 protected resolveDateTime(value: any): Moment | undefined {
-    if (!value) {
+    if (value === undefined || value === null || value === '') {
         return undefined;
     }
+    let parsed: Moment;
     if (moment.isMoment(value)) {
-        return value;
+        parsed = value;
+    } else if (value instanceof Date) {
+        parsed = moment(value);
+    } else if (Array.isArray(value)) {
+        const [year, month, day, hour = 0, minute = 0, second = 0, millisecond = 0] = value;
+        parsed = moment({
+            year,
+            month: month - 1,
+            date: day,
+            hour,
+            minute,
+            second,
+            millisecond
+        });
+    } else {
+        parsed = moment(value);
     }
-    if (value instanceof Date) {
-        return moment(value);
-    }
-    if (Array.isArray(value)) {
-        const [year, month, day, hour = 0, minute = 0, second = 0, millisecond = 0] = value;
-        return moment({
-            year,
-            month: month - 1,
-            date: day,
-            hour,
-            minute,
-            second,
-            millisecond
-        });
-    }
-    return moment(value);
+    return parsed.isValid() ? parsed : undefined;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@projects/netgrif-components-core/src/lib/task-content/services/field-converter.service.ts`
around lines 317 - 340, In resolveDateTime, avoid treating falsy-but-valid
inputs like 0 as empty and guard against invalid Moment results: replace the
initial if (!value) with a nullish/empty-string check (e.g. if (value == null ||
value === '') return undefined), keep the existing branches for moment, Date and
Array, but after creating a Moment from Array or generic moment(value) verify
the result is valid (use moment.isMoment(...) || result.isValid()) and return
undefined if invalid; reference the resolveDateTime function, the Array
destructuring branch, and the final return moment(value) conversion when adding
these validity checks.


protected resolveAllowedTypes(allowTypes: string[]) {
return allowTypes?.length > 0 ? (allowTypes.length > 1 ? allowTypes as FileUploadMIMEType[] : allowTypes[0]) : null
return allowTypes?.length > 0 ? (allowTypes.length > 1 ? allowTypes as FileUploadMIMEType[] : allowTypes[0]) : null;

Check warning on line 343 in projects/netgrif-components-core/src/lib/task-content/services/field-converter.service.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=netgrif_components&issues=AZ343vdcuLVDl_1VWNQ2&open=AZ343vdcuLVDl_1VWNQ2&pullRequest=327
}

protected resolveByteSize(bytesSize) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
<mat-datepicker-toggle matPrefix [for]="picker"></mat-datepicker-toggle>
<ngx-mat-datetime-picker #picker
[showSpinners]="true"
[showSeconds]="false"
[stepHour]="1"
[stepMinute]="5"
[showSeconds]="showSeconds"
[stepHour]="stepHour"
[stepMinute]="stepMinute"
[stepSecond]="stepSecond"
[color]="'primary'"
[enableMeridian]="false">
[enableMeridian]="enableMeridian">
</ngx-mat-datetime-picker>
<mat-hint [ngClass]="{'mat-hint-disabled': formControlRef.disabled}">{{dataField.description}}</mat-hint>
<mat-error *ngIf="dataField.isInvalid(formControlRef)">{{getErrorMessage()}}</mat-error>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,38 @@ import {
DataFieldPortalData,
DateTimeField,
DATE_TIME_FORMAT,
DATE_TIME_SECONDS_FORMAT,
LanguageService
} from '@netgrif/components-core'
import {TranslateService} from "@ngx-translate/core";
import {NGX_MAT_DATE_FORMATS, NgxMatDateAdapter} from "@angular-material-components/datetime-picker";
import {MAT_DATE_LOCALE} from "@angular/material/core";
} from '@netgrif/components-core';
import {TranslateService} from '@ngx-translate/core';
import {NGX_MAT_DATE_FORMATS, NgxMatDateAdapter} from '@angular-material-components/datetime-picker';
import {MAT_DATE_LOCALE} from '@angular/material/core';

export function dateTimeDefaultFormatsFactory(dataFieldPortalData?: DataFieldPortalData<DateTimeField>) {
return dataFieldPortalData?.dataField?.showSeconds
? DATE_TIME_SECONDS_FORMAT
: DATE_TIME_FORMAT;
}

@Component({
selector: 'nc-date-time-default-field',
templateUrl: './date-time-default-field.component.html',
styleUrls: ['./date-time-default-field.component.scss'],
selector: 'nc-date-time-default-field',
templateUrl: './date-time-default-field.component.html',
styleUrls: ['./date-time-default-field.component.scss'],
providers: [
{provide: NGX_MAT_DATE_FORMATS, useValue: DATE_TIME_FORMAT}
{
provide: NGX_MAT_DATE_FORMATS,
useFactory: dateTimeDefaultFormatsFactory,
deps: [[new Optional(), DATA_FIELD_PORTAL_DATA]]
}
]
})
export class DateTimeDefaultFieldComponent extends AbstractDateTimeDefaultFieldComponent {

constructor(_translate: TranslateService,
_adapter: NgxMatDateAdapter<any>,
@Inject(MAT_DATE_LOCALE) protected _locale: string,
_languageService: LanguageService,
@Optional() @Inject(DATA_FIELD_PORTAL_DATA) dataFieldPortalData: DataFieldPortalData<DateTimeField>) {
super(_translate, _adapter, _locale, _languageService, dataFieldPortalData);
}

constructor(_translate: TranslateService,
_adapter: NgxMatDateAdapter<any>,
@Inject(MAT_DATE_LOCALE) protected _locale: string,
_languageService: LanguageService,
@Optional() @Inject(DATA_FIELD_PORTAL_DATA) dataFieldPortalData: DataFieldPortalData<DateTimeField>) {
super(_translate, _adapter, _locale, _languageService, dataFieldPortalData);
}
}
Loading