Compare commits

...
17 changed files with 357 additions and 43 deletions
+3 -1
View File
@@ -23,4 +23,6 @@ runs:
echo "TAG_NPM not set, aborting"
exit 1
fi
./scripts/github/build/npm-check-bundles.sh
ADF_VERSION=$(npm view @alfresco/adf-core@${TAG_NPM} version)
echo "check bundle on TAG_NPM='${TAG_NPM}' and ADF_VERSION='${ADF_VERSION}'"
./scripts/github/build/npm-check-bundles.sh -v ${ADF_VERSION}
@@ -146,7 +146,7 @@ describe('FormFieldModel', () => {
});
expect(field.options).toEqual([{ id: 'id_one', name: 'One' }]);
expect(field.value).toEqual('id_one');
expect(field.value).toEqual({ id: 'id_one', name: 'One' });
});
it('should add value (selected options) to field options if NOT present (multiple selection)', () => {
@@ -176,7 +176,7 @@ describe('FormFieldModel', () => {
expect(field.hasEmptyValue).toBe(true);
expect(field.emptyOption).toEqual({ id: 'empty', name: 'Chose one...' });
expect(field.value).toEqual('empty');
expect(field.value).toEqual({ id: 'empty', name: 'Chose one...' });
});
it('should set hasEmptyValue to true if "empty" option is present in options', () => {
@@ -238,7 +238,8 @@ describe('FormFieldModel', () => {
options: [],
value: { id: 'delayed-option-id', name: 'Delayed option' }
});
expect(field.value).toBe('delayed-option-id');
expect(field.value).toEqual({ id: 'delayed-option-id', name: 'Delayed option' });
});
});
});
@@ -736,7 +737,7 @@ describe('FormFieldModel', () => {
];
});
it('should update form with selected option and options from which we chose', () => {
it('should update form with selected option and options from which we chose when is a string', () => {
field.value = 'restOpt2';
field.updateForm();
@@ -974,7 +975,7 @@ describe('FormFieldModel', () => {
expect(field.options).toEqual(staticOptions);
});
it('should selected option appear in form values', () => {
it('should selected option appear in form values string', () => {
const field = getFieldConfig('manual', staticOptions, 'opt2');
field.updateForm();
@@ -982,6 +983,15 @@ describe('FormFieldModel', () => {
expect(field.value).toEqual('opt2');
expect(field.form.values['dropdown_field']).toEqual({ id: 'opt2', name: 'Option 2' });
});
it('should selected option appear in form values obj', () => {
const field = getFieldConfig('manual', staticOptions, { id: 'opt3', name: 'opt3' });
field.updateForm();
expect(field.value).toEqual({ id: 'opt3', name: 'opt3' });
expect(field.form.values['dropdown_field']).toEqual({ id: 'opt3', name: 'opt3' });
});
});
describe('radio buttons field', () => {
@@ -327,13 +327,13 @@ export class FormFieldModel extends FormWidgetModel {
const isEmptyValue = !value || [this.emptyOption.id, this.emptyOption.name].includes(value);
if (isEmptyValue) {
return this.emptyOption.id;
return this.emptyOption;
}
}
if (this.isValidOption(value)) {
this.addOption({ id: value.id, name: value.name });
return value.id;
return value;
}
if (this.hasMultipleValues) {
@@ -432,6 +432,17 @@ export class FormFieldModel extends FormWidgetModel {
this.form.values[this.id] = matchingOption || null;
}
if (typeof this.value === 'object') {
if (this.value.id === 'empty' || this.value.id === '') {
this.form.values[this.id] = null;
break;
}
const matchingOption: FormFieldOption = this.options.find((opt) => opt.id === this.value.id);
this.form.values[this.id] = matchingOption;
}
break;
}
case FormFieldTypes.RADIO_BUTTONS: {
@@ -18,7 +18,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { of, throwError } from 'rxjs';
import { DropdownCloudWidgetComponent } from './dropdown-cloud.widget';
import { DEFAULT_OPTION, DropdownCloudWidgetComponent } from './dropdown-cloud.widget';
import { FormFieldModel, FormModel, FormService, FormFieldEvent, FormFieldTypes } from '@alfresco/adf-core';
import { FormCloudService } from '../../../services/form-cloud.service';
import { ProcessServiceCloudTestingModule } from '../../../../testing/process-service-cloud.testing.module';
@@ -375,22 +375,52 @@ describe('DropdownCloudWidgetComponent', () => {
expect(element.querySelector('.adf-invalid')).toBeFalsy();
});
it('should be valid when field is hidden with empty value', () => {
widget.field.isVisible = false;
fixture.detectChanges();
describe('and visible', () => {
beforeEach(() => {
widget.field.isVisible = true;
});
expect(widget.field.isValid).toBeTrue();
expect(widget.dropdownControl.valid).toBeTrue();
expect(widget.field.validationSummary.message).toBe('');
it('should be invalid with no option selected', () => {
fixture.detectChanges();
expect(widget.field.isValid).toBeFalse();
expect(widget.dropdownControl.valid).toBeFalse();
expect(widget.field.validationSummary.message).toBe('FORM.FIELD.REQUIRED');
});
it('should be invalid with default option selected', () => {
widget.field.hasEmptyValue = true;
widget.field.value = DEFAULT_OPTION;
fixture.detectChanges();
expect(widget.field.isValid).toBeFalse();
expect(widget.dropdownControl.valid).toBeFalse();
expect(widget.field.validationSummary.message).toBe('FORM.FIELD.REQUIRED');
});
});
it('should be invalid when field is hidden with empty value', () => {
widget.field.isVisible = true;
fixture.detectChanges();
describe('and NOT visible', () => {
beforeEach(() => {
widget.field.isVisible = false;
});
expect(widget.field.isValid).toBeFalse();
expect(widget.dropdownControl.valid).toBeFalse();
expect(widget.field.validationSummary.message).toBe('FORM.FIELD.REQUIRED');
it('should be valid with no option selected', () => {
fixture.detectChanges();
expect(widget.field.isValid).toBeTrue();
expect(widget.dropdownControl.valid).toBeTrue();
expect(widget.field.validationSummary.message).toBe('');
});
it('should be valid with default option selected', () => {
widget.field.hasEmptyValue = true;
widget.field.value = DEFAULT_OPTION;
fixture.detectChanges();
expect(widget.field.isValid).toBeTrue();
expect(widget.dropdownControl.valid).toBeTrue();
expect(widget.field.validationSummary.message).toBe('');
});
});
});
@@ -953,6 +983,34 @@ describe('DropdownCloudWidgetComponent', () => {
expect(widget.field.options.length).toEqual(0);
};
it('should set dropdownControl value without emitting events if the mapping is a string', () => {
widget.field = {
value: 'testValue',
options: [],
isVisible: true
} as any; // Mock field
spyOn(widget.dropdownControl, 'setValue').and.callThrough();
widget['setFormControlValue']();
expect(widget.dropdownControl.setValue).toHaveBeenCalledWith({ id: 'testValue', name: '' }, { emitEvent: false });
expect(widget.dropdownControl.value).toEqual({ id: 'testValue', name: '' });
});
it('should set dropdownControl value without emitting events if is an object', () => {
widget.field = {
value: { id: 'testValueObj', name: 'testValueObjName' },
options: [],
isVisible: true
} as any; // Mock field
spyOn(widget.dropdownControl, 'setValue').and.callThrough();
widget['setFormControlValue']();
expect(widget.dropdownControl.setValue).toHaveBeenCalledWith({ id: 'testValueObj', name: 'testValueObjName' }, { emitEvent: false });
expect(widget.dropdownControl.value).toEqual({ id: 'testValueObj', name: 'testValueObjName' });
});
it('should display options persisted from process variable', async () => {
widget.field = getVariableDropdownWidget(
'variables.json-variable',
@@ -195,24 +195,44 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
}
private setFormControlValue(): void {
this.dropdownControl.setValue(this.field?.value, { emitEvent: false });
if (Array.isArray(this.field.value)) {
this.dropdownControl.setValue(this.field?.value, { emitEvent: false });
} else if (this.field?.value && typeof this.field?.value === 'object') {
this.dropdownControl.setValue({ id: this.field?.value.id, name: this.field?.value.name }, { emitEvent: false });
} else if (this.field.value === null) {
this.dropdownControl.setValue(this.field?.value, { emitEvent: false });
} else {
this.dropdownControl.setValue({ id: this.field?.value, name: '' }, { emitEvent: false });
}
}
private updateFormControlState(): void {
const isFieldRequired = this.isRequired();
this.updateDropdownValidationRules();
this.updateDropdownReadonlyRules();
this.dropdownControl.updateValueAndValidity({ emitEvent: false });
}
this.dropdownControl.setValidators(isFieldRequired && this.field?.isVisible ? [Validators.required] : []);
private updateDropdownValidationRules() {
this.dropdownControl.setValidators([]);
const addSelectDefaultOptionValidator = isFieldRequired && this.field.hasEmptyValue;
if (addSelectDefaultOptionValidator) {
this.dropdownControl.addValidators([defaultValueValidator(this.field)]);
if (!this.field?.isVisible) {
return;
}
this.field?.readOnly || this.readOnly
? this.dropdownControl.disable({ emitEvent: false })
: this.dropdownControl.enable({ emitEvent: false });
if (this.isRequired()) {
this.dropdownControl.addValidators([Validators.required]);
if (this.field.hasEmptyValue) {
this.dropdownControl.addValidators([defaultValueValidator(this.field)]);
}
}
}
this.dropdownControl.updateValueAndValidity({ emitEvent: false });
private updateDropdownReadonlyRules() {
if (this.field?.readOnly || this.readOnly) {
this.dropdownControl.disable({ emitEvent: false });
} else {
this.dropdownControl.enable({ emitEvent: false });
}
}
private handleErrors(): void {
@@ -457,7 +477,11 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
const fieldValueIds = this.field.value.map((valueOption) => valueOption.id);
return fieldValueIds.every((valueOptionId) => optionIdList.includes(valueOptionId));
} else {
return [...this.field.options].map((option) => option.id).includes(this.field.value);
if (this.field?.value && typeof this.field?.value === 'object') {
return [...this.field.options].map((option) => option.id).includes(this.field.value.id);
} else {
return [...this.field.options].map((option) => option.id).includes(this.field.value);
}
}
}
@@ -0,0 +1,62 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { FormFieldModel } from '@alfresco/adf-core';
import { FormControl } from '@angular/forms';
import { defaultValueValidator } from './validators';
import { DEFAULT_OPTION } from './dropdown-cloud.widget';
describe('defaultValueValidator', () => {
let mockField: FormFieldModel;
beforeEach(() => {
mockField = new FormFieldModel(null, {
options: [
{ id: DEFAULT_OPTION.id, name: DEFAULT_OPTION.name },
{ id: 'opt_1', name: 'Option 1' },
{ id: 'opt_2', name: 'Option 2' }
]
});
});
it('should return null when a valid option is selected', () => {
const validator = defaultValueValidator(mockField);
const control = new FormControl({ id: 'opt_1' });
const result = validator(control);
expect(result).toBeNull();
});
it('should return a required error when no valid option is selected', () => {
const validator = defaultValueValidator(mockField);
const control = new FormControl(null);
const result = validator(control);
expect(result).toEqual({ required: true });
});
it('should return a required error when the default "choose one" option is selected', () => {
const validator = defaultValueValidator(mockField);
const control = new FormControl(DEFAULT_OPTION.id);
const result = validator(control);
expect(result).toEqual({ required: true });
});
});
@@ -23,13 +23,14 @@ export const defaultValueValidator =
(filed: FormFieldModel): ValidatorFn =>
(control: AbstractControl): ValidationErrors | null => {
const optionsWithNoDefaultValue = filed.options.filter((dropdownOption) => {
const isDefaultValue = dropdownOption.id === DEFAULT_OPTION.id && dropdownOption.name === DEFAULT_OPTION.name;
const isDefaultValue = dropdownOption.id === DEFAULT_OPTION.id;
return !isDefaultValue;
});
const isSomeOptionSelected = optionsWithNoDefaultValue.some((dropdownOption) => {
const isOptionSelected = dropdownOption.id === control.value?.id;
return isOptionSelected;
});
@@ -52,7 +52,7 @@ describe('ProcessFiltersCloudComponent', () => {
provide: ProcessListCloudService,
useValue: {
getProcessCounter: () => of(10),
getProcessListCounter: () => of(10)
getProcessListCount: () => of(10)
}
},
{ provide: ProcessFilterCloudService, useValue: ProcessFilterCloudServiceMock },
@@ -320,7 +320,7 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
private fetchProcessFilterCounter(filter: ProcessFilterCloudModel): Observable<number> {
return this.searchApiMethod === 'POST'
? this.processListCloudService.getProcessListCounter(new ProcessFilterCloudAdapter(filter))
? this.processListCloudService.getProcessListCount(new ProcessFilterCloudAdapter(filter))
: this.processListCloudService.getProcessCounter(filter.appName, filter.status);
}
}
@@ -267,4 +267,40 @@ describe('ProcessListCloudService', () => {
expect(requestBodyParams.variableKeys[1]).toBe('test-two');
});
});
describe('getProcessListCount', () => {
it('should concat the app name to the request url', async () => {
const taskRequest = {
appName: 'fakeName'
} as ProcessListRequestModel;
requestSpy.and.callFake(returnCallUrl);
const res = await firstValueFrom(service.getProcessListCount(taskRequest));
expect(res).toBeDefined();
expect(res).not.toBeNull();
expect(res).toContain('fakeName/query/v1/process-instances/count');
});
it('should return 0 if response is falsy for getProcessListCount', async () => {
const taskRequest = {
appName: 'fakeName',
pagination: { skipCount: 0, maxItems: 20 }
} as ProcessListRequestModel;
requestSpy.and.callFake(() => Promise.resolve(null));
const res = await firstValueFrom(service.getProcessListCount(taskRequest));
expect(res).toBe(0);
});
it('should throw error if appName is not configured in getProcessListCount', async () => {
const taskRequest = { appName: null } as ProcessListRequestModel;
requestSpy.and.callFake(returnCallUrl);
const res = await firstValueFrom(service.getProcessListCount(taskRequest).pipe(catchError((error) => of(error.message))));
expect(res).toBe('Appname not configured');
});
});
});
@@ -229,6 +229,17 @@ export class ProcessListCloudService extends BaseCloudService {
return this.getProcess(callback, defaultQueryUrl, requestNode, queryUrl);
}
getProcessListCount(requestNode: ProcessListRequestModel): Observable<number> {
if (!requestNode?.appName) {
return throwError(() => new Error('Appname not configured'));
}
const queryUrl = `${this.getBasePath(requestNode.appName)}/query/v1/process-instances/count`;
const queryData = this.buildQueryData(requestNode);
return this.post<object, number>(queryUrl, queryData).pipe(map((response) => response || 0));
}
private getVariableKeysFromQueryParams(queryParams: any): string[] {
if (!queryParams['variableKeys'] || queryParams['variableKeys'].length <= 0) {
return [];
@@ -42,7 +42,7 @@ describe('TaskFiltersCloudComponent', () => {
let fixture: ComponentFixture<TaskFiltersCloudComponent>;
let getTaskFilterCounterSpy: jasmine.Spy;
let getTaskListFiltersSpy: jasmine.Spy;
let getTaskListCounterSpy: jasmine.Spy;
let getTaskListCountSpy: jasmine.Spy;
const configureTestingModule = (searchApiMethod: 'GET' | 'POST') => {
TestBed.configureTestingModule({
@@ -52,7 +52,7 @@ describe('TaskFiltersCloudComponent', () => {
taskFilterService = TestBed.inject(TaskFilterCloudService);
taskListService = TestBed.inject(TaskListCloudService);
getTaskFilterCounterSpy = spyOn(taskFilterService, 'getTaskFilterCounter').and.returnValue(of(11));
getTaskListCounterSpy = spyOn(taskListService, 'getTaskListCounter').and.returnValue(of(11));
getTaskListCountSpy = spyOn(taskListService, 'getTaskListCount').and.returnValue(of(11));
spyOn(taskFilterService, 'getTaskNotificationSubscription').and.returnValue(of(taskNotifications));
getTaskListFiltersSpy = spyOn(taskFilterService, 'getTaskListFilters').and.returnValue(of(fakeGlobalFilter));
@@ -355,7 +355,7 @@ describe('TaskFiltersCloudComponent', () => {
);
await filterButton.click();
expect(getTaskListCounterSpy).toHaveBeenCalledWith(new TaskFilterCloudAdapter(fakeGlobalFilter[0]));
expect(getTaskListCountSpy).toHaveBeenCalledWith(new TaskFilterCloudAdapter(fakeGlobalFilter[0]));
});
});
@@ -151,7 +151,7 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
private fetchTaskFilterCounter(filter: TaskFilterCloudModel): Observable<number> {
return this.searchApiMethod === 'POST'
? this.taskListCloudService.getTaskListCounter(new TaskFilterCloudAdapter(filter))
? this.taskListCloudService.getTaskListCount(new TaskFilterCloudAdapter(filter))
: this.taskFilterCloudService.getTaskFilterCounter(filter);
}
@@ -155,6 +155,41 @@ describe('TaskListCloudService', () => {
const res = await firstValueFrom(service.fetchTaskList(taskRequest).pipe(catchError((error) => of(error.message))));
expect(res).toBe('Appname not configured');
});
});
describe('getTaskListCount', () => {
it('should concat the app name to the request url', async () => {
const taskRequest = {
appName: 'fakeName'
} as TaskListRequestModel;
requestSpy.and.callFake(returnCallUrl);
const res = await firstValueFrom(service.getTaskListCount(taskRequest));
expect(res).toBeDefined();
expect(res).not.toBeNull();
expect(res).toContain('fakeName/query/v1/tasks/count');
});
it('should return 0 if response is falsy for getTaskListCount', async () => {
const taskRequest = {
appName: 'fakeName',
pagination: { skipCount: 0, maxItems: 20 }
} as TaskListRequestModel;
requestSpy.and.callFake(() => Promise.resolve(null));
const res = await firstValueFrom(service.getTaskListCount(taskRequest));
expect(res).toBe(0);
});
it('should throw error if appName is not configured in getTaskListCount', async () => {
const taskRequest = { appName: null } as TaskListRequestModel;
requestSpy.and.callFake(returnCallUrl);
const res = await firstValueFrom(service.getTaskListCount(taskRequest).pipe(catchError((error) => of(error.message))));
expect(res).toBe('Appname not configured');
});
});
@@ -97,6 +97,17 @@ export class TaskListCloudService extends BaseCloudService implements TaskListCl
return this.fetchTaskList(requestNode).pipe(map((tasks) => tasks.list.pagination.totalItems));
}
getTaskListCount(requestNode: TaskListRequestModel): Observable<number> {
if (!requestNode?.appName) {
return throwError(() => new Error('Appname not configured'));
}
const queryUrl = `${this.getBasePath(requestNode.appName)}/query/v1/tasks/count`;
const queryData = this.buildQueryData(requestNode);
return this.post<object, number>(queryUrl, queryData).pipe(map((response) => response || 0));
}
protected buildQueryData(requestNode: TaskListRequestModel) {
const queryData: any = {
id: requestNode.id,
@@ -941,7 +941,7 @@ describe('FormComponent', () => {
let dropdownField = formFields.find((field) => field.id === 'dropdownId');
let radioField = formFields.find((field) => field.id === 'radio');
expect(dropdownField.value).toBe('empty');
expect(dropdownField.value).toEqual({ id: 'empty', name: 'Choose one...' });
expect(radioField.value).toBeNull();
const formValues: any = {};
@@ -961,7 +961,10 @@ describe('FormComponent', () => {
dropdownField = formFields.find((field) => field.id === 'dropdownId');
radioField = formFields.find((field) => field.id === 'radio');
expect(dropdownField.value).toBe('dropdown_option_2');
expect(dropdownField.value).toEqual({
id: 'dropdown_option_2',
name: 'Dropdown option 2'
});
expect(radioField.value).toBe('radio_option_3');
});
+52 -2
View File
@@ -2,6 +2,20 @@
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
VERSION="alpha"
while getopts "v:" opt; do
case $opt in
v)
VERSION="$OPTARG"
;;
\?)
echo "Usage: $0 [-v version]" >&2
echo " -v Specify package version (default: alpha)" >&2
exit 1
;;
esac
done
eval projects=( "js-api"
"adf-core"
"adf-insights"
@@ -29,10 +43,46 @@ do
mkdir $PACKAGE
cd $PACKAGE
PKG_VERSION=$(npm view @alfresco/$PACKAGE@alpha version)
# Handle js-api differently - increase major version by 1
if [ $PACKAGE == 'js-api' ]; then
if [ $VERSION == 'alpha' ] || [ $VERSION == 'beta' ] || [ $VERSION == 'latest' ]; then
# For tag versions, we need to get the current version and increment
CURRENT_VERSION=$(npm view @alfresco/$PACKAGE@$VERSION version)
MAJOR_VERSION=$(echo $CURRENT_VERSION | cut -d'.' -f1)
NEXT_MAJOR=$((MAJOR_VERSION + 1))
# Keep the rest of the version string
REST_VERSION=$(echo $CURRENT_VERSION | cut -d'.' -f2-)
PACKAGE_VERSION="${NEXT_MAJOR}.${REST_VERSION}"
else
# For specific versions, just increment the major number
MAJOR_VERSION=$(echo $VERSION | cut -d'.' -f1)
NEXT_MAJOR=$((MAJOR_VERSION + 1))
REST_VERSION=$(echo $VERSION | cut -d'.' -f2-)
PACKAGE_VERSION="${NEXT_MAJOR}.${REST_VERSION}"
fi
else
PACKAGE_VERSION=$VERSION
fi
# Try the calculated package version first
PKG_VERSION=$(npm view @alfresco/$PACKAGE@$PACKAGE_VERSION version 2>/dev/null)
# If that fails for js-api, try the original version
if [ -z "$PKG_VERSION" ] && [ $PACKAGE == 'js-api' ]; then
echo "Warning: js-api@$PACKAGE_VERSION not found, trying @$VERSION"
PACKAGE_VERSION=$VERSION
PKG_VERSION=$(npm view @alfresco/$PACKAGE@$PACKAGE_VERSION version 2>/dev/null)
fi
# If still no version found, exit with error
if [ -z "$PKG_VERSION" ]; then
error_out '31;1' "Package @alfresco/$PACKAGE@$PACKAGE_VERSION not found!" >&2
exit 1
fi
echo "Inspecting: $PACKAGE@$PKG_VERSION"
npm pack '@alfresco/'$PACKAGE@$PKG_VERSION
npm pack '@alfresco/'$PACKAGE@$PACKAGE_VERSION
tar zxf 'alfresco-'$PACKAGE-$PKG_VERSION.tgz
if [ $PACKAGE == 'js-api' ]; then