diff --git a/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.html b/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.html index ee75a8fb76..04f79db0ab 100644 --- a/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.html +++ b/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.html @@ -38,7 +38,7 @@ (click)="saveChanges($event)" color="primary" data-automation-id="save-general-info-metadata" - [disabled]="!hasMetadataChanged"> + [disabled]="!hasMetadataChanged || invalidProperties.size > 0"> check @@ -235,7 +235,7 @@ (click)="saveChanges($event)" color="primary" data-automation-id="save-metadata" - [disabled]="!hasMetadataChanged"> + [disabled]="!hasMetadataChanged || invalidProperties.size > 0"> check diff --git a/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.spec.ts b/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.spec.ts index fe504f2257..32343871dc 100644 --- a/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.spec.ts +++ b/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.spec.ts @@ -114,7 +114,8 @@ describe('ContentMetadataComponent', () => { fixture.detectChanges(); }; - const clickOnGroupSave = () => fixture.debugElement.query(By.css('[data-automation-id="save-metadata"]')).nativeElement.click(); + const getGroupSaveButton = () => fixture.debugElement.query(By.css('[data-automation-id="save-metadata"]')).nativeElement; + const clickOnGroupSaveButton = () => getGroupSaveButton().click(); const findTagsCreator = (): TagsCreatorComponent => fixture.debugElement.query(By.directive(TagsCreatorComponent))?.componentInstance; const getToggleEditButton = () => fixture.debugElement.query(By.css('[data-automation-id="meta-data-general-info-edit"]')); @@ -288,6 +289,135 @@ describe('ContentMetadataComponent', () => { expect(contentMetadataService.getGroupedProperties).toHaveBeenCalled(); })); + describe('Save button - Grouped Properties', () => { + beforeEach(() => { + getGroupedPropertiesSpy.and.returnValue( + of([ + { + editable: true, + title: 'test', + properties: [] + } + ]) + ); + + component.ngOnInit(); + component.readOnly = false; + }); + + it('should disable the save button if metadata has not changed', () => { + fixture.detectChanges(); + toggleEditModeForGroup(); + expect(getGroupSaveButton().disabled).toBeTrue(); + }); + + it('should disable the save button if there are invalid properties', fakeAsync(() => { + updateService.update( + { + key: 'properties.property-key', + isValidValue: false + } as CardViewBaseItemModel, + 'updated-value' + ); + tick(500); + fixture.detectChanges(); + toggleEditModeForGroup(); + expect(getGroupSaveButton().disabled).toBeTrue(); + })); + + it('should enable the save button if metadata has changed and there are no invalid properties', fakeAsync(() => { + updateService.update( + { + key: 'properties.property-key', + isValidValue: true + } as CardViewBaseItemModel, + 'updated-value' + ); + tick(500); + fixture.detectChanges(); + toggleEditModeForGroup(); + expect(getGroupSaveButton().disabled).toBeFalse(); + })); + }); + + describe('Save button - Basic Properties', () => { + beforeEach(fakeAsync(() => { + spyOn(contentMetadataService, 'getBasicProperties').and.returnValue(of([])); + component.ngOnInit(); + component.readOnly = false; + })); + + it('should disable the save button if metadata has not changed', fakeAsync(() => { + fixture.detectChanges(); + toggleEditModeForGeneralInfo(); + expect(findSaveGeneralInfoButton().disabled).toBeTrue(); + })); + + it('should enable the save button if metadata has changed and there are no invalid properties', fakeAsync(() => { + updateService.update( + { + key: 'properties.property-key', + isValidValue: true + } as CardViewBaseItemModel, + 'updated-value' + ); + tick(500); + fixture.detectChanges(); + toggleEditModeForGeneralInfo(); + expect(findSaveGeneralInfoButton().disabled).toBeFalse(); + })); + + it('should disable the save button if there are invalid properties', fakeAsync(() => { + updateService.update( + { + key: 'properties.property-key', + isValidValue: false + } as CardViewBaseItemModel, + 'updated-value' + ); + tick(500); + fixture.detectChanges(); + toggleEditModeForGeneralInfo(); + expect(findSaveGeneralInfoButton().disabled).toBeTrue(); + })); + }); + + describe('updateInvalidProperties', () => { + it('should add the property key to invalidProperties if isValidValue is false and key is not present', fakeAsync(() => { + const property = { key: 'properties.property-key', isValidValue: false } as CardViewBaseItemModel; + expect(component.invalidProperties.size).toBe(0); + updateService.update(property, 'updated-value'); + tick(500); + expect(component.invalidProperties.has(property.key)).toBeTrue(); + })); + + it('should not add the property key to invalidProperties if isValidValue is false and key is already present', fakeAsync(() => { + const property = { key: 'properties.property-key', isValidValue: false } as CardViewBaseItemModel; + updateService.update(property, 'updated-value-1'); + tick(500); + updateService.update(property, 'updated-value-2'); + tick(500); + expect(component.invalidProperties.size).toBe(1); + expect(component.invalidProperties.has(property.key)).toBeTrue(); + })); + + it('should remove the property key from invalidProperties if isValidValue is true and key is present', fakeAsync(() => { + updateService.update({ key: 'properties.property-key', isValidValue: false } as CardViewBaseItemModel, 'updated-value'); + tick(500); + expect(component.invalidProperties).toContain('properties.property-key'); + updateService.update({ key: 'properties.property-key', isValidValue: true } as CardViewBaseItemModel, 'updated-value'); + tick(500); + expect(component.invalidProperties.has('properties.property-key')).toBeFalse(); + })); + + it('should not change invalidProperties if isValidValue is true and key is not present', fakeAsync(() => { + expect(component.invalidProperties.size).toBe(0); + updateService.update({ key: 'properties.property-key', isValidValue: true } as CardViewBaseItemModel, 'updated-value'); + tick(500); + expect(component.invalidProperties.size).toBe(0); + })); + }); + it('should save changedProperties on save click', fakeAsync(() => { getGroupedPropertiesSpy.and.returnValue( of([ @@ -309,7 +439,7 @@ describe('ContentMetadataComponent', () => { spyOn(nodesApiService, 'updateNode').and.returnValue(of(expectedNode)); toggleEditModeForGroup(); - clickOnGroupSave(); + clickOnGroupSaveButton(); expect(nodesApiService.updateNode).toHaveBeenCalled(); expect(component.node).toEqual(expectedNode); })); diff --git a/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.ts b/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.ts index d94963ba66..f68098ec99 100644 --- a/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.ts +++ b/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.ts @@ -164,6 +164,7 @@ export class ContentMetadataComponent implements OnChanges, OnInit { categoriesManagementMode = CategoriesManagementMode.ASSIGN; classifiableChanged = this.classifiableChangedSubject.asObservable(); editing = false; + invalidProperties = new Set(); editedPanelTitle = ''; currentPanel: ContentMetadataPanel = { expanded: false, @@ -194,6 +195,7 @@ export class ContentMetadataComponent implements OnChanges, OnInit { .subscribe((updatedNode: UpdateNotification) => { this.hasMetadataChanged = true; this.targetProperty = updatedNode.target; + this.updateInvalidProperties(); this.updateChanges(updatedNode.changed); }); @@ -549,4 +551,12 @@ export class ContentMetadataComponent implements OnChanges, OnInit { } return observables; } + + private updateInvalidProperties() { + if (this.targetProperty?.isValidValue === false) { + this.invalidProperties.add(this.targetProperty.key); + } else if (this.targetProperty?.isValidValue === true) { + this.invalidProperties.delete(this.targetProperty.key); + } + } } diff --git a/lib/content-services/src/lib/content-metadata/services/basic-properties.service.ts b/lib/content-services/src/lib/content-metadata/services/basic-properties.service.ts index 74fd9686a2..5130a4fb48 100644 --- a/lib/content-services/src/lib/content-metadata/services/basic-properties.service.ts +++ b/lib/content-services/src/lib/content-metadata/services/basic-properties.service.ts @@ -17,13 +17,7 @@ import { inject, Injectable } from '@angular/core'; import { Node } from '@alfresco/js-api'; -import { - CardViewDateItemModel, - CardViewItemMatchValidator, - CardViewTextItemModel, - FileSizePipe, - TranslationService -} from '@alfresco/adf-core'; +import { CardViewDateItemModel, CardViewItemMatchValidator, CardViewTextItemModel, FileSizePipe, TranslationService } from '@alfresco/adf-core'; @Injectable({ providedIn: 'root' @@ -44,9 +38,7 @@ export class BasicPropertiesService { value: node.name, key: 'properties.cm:name', editable: true, - validators: [ - new CardViewItemMatchValidator('[\\/\\*\\\\"\\\\:]') - ] + validators: [new CardViewItemMatchValidator('[\\/\\*\\\\"\\\\:|?<>]')] }), new CardViewTextItemModel({ label: 'CORE.METADATA.BASIC.TITLE', diff --git a/lib/core/src/lib/card-view/components/card-view-textitem/card-view-textitem.component.spec.ts b/lib/core/src/lib/card-view/components/card-view-textitem/card-view-textitem.component.spec.ts index a009799be6..70b9a8b718 100644 --- a/lib/core/src/lib/card-view/components/card-view-textitem/card-view-textitem.component.spec.ts +++ b/lib/core/src/lib/card-view/components/card-view-textitem/card-view-textitem.component.spec.ts @@ -505,7 +505,7 @@ describe('CardViewTextItemComponent', () => { await fixture.whenStable(); expect(itemUpdatedSpy).toHaveBeenCalledWith({ - target: { ...component.property }, + target: { ...component.property, isValidValue: true }, changed: { textkey: expectedText } @@ -580,11 +580,11 @@ describe('CardViewTextItemComponent', () => { await updateTextField(component.property.key, 'updated-value'); await fixture.whenStable(); - const property = { ...component.property }; + const property = { ...component.property, isValidValue: true }; expect(cardViewUpdateService.update).toHaveBeenCalledWith(property, 'updated-value'); }); - it('should NOT trigger the update event if the editedValue is invalid', async () => { + it('should trigger the update event if the editedValue is NOT valid', async () => { const cardViewUpdateService = TestBed.inject(CardViewUpdateService); spyOn(cardViewUpdateService, 'update'); component.property.isValid = () => false; @@ -592,7 +592,8 @@ describe('CardViewTextItemComponent', () => { await updateTextField(component.property.key, '@invalid-value'); await fixture.whenStable(); - expect(cardViewUpdateService.update).not.toHaveBeenCalled(); + const property = { ...component.property, isValidValue: false }; + expect(cardViewUpdateService.update).toHaveBeenCalledWith(property, '@invalid-value'); }); it('should trigger the update event if the editedValue is valid', async () => { @@ -666,7 +667,7 @@ describe('CardViewTextItemComponent', () => { await fixture.whenStable(); expect(itemUpdatedSpy).toHaveBeenCalledWith({ - target: { ...component.property }, + target: { ...component.property, isValidValue: true }, changed: { textkey: expectedText } @@ -711,7 +712,7 @@ describe('CardViewTextItemComponent', () => { await fixture.whenStable(); expect(itemUpdatedSpy).toHaveBeenCalledWith({ - target: { ...component.property }, + target: { ...component.property, isValidValue: true }, changed: { textkey: expectedText } @@ -826,7 +827,7 @@ describe('CardViewTextItemComponent', () => { await fixture.whenStable(); expect(itemUpdatedSpy).toHaveBeenCalledWith({ - target: { ...component.property }, + target: { ...component.property, isValidValue: true }, changed: { textkey: expectedNumber.toString() } @@ -882,7 +883,7 @@ describe('CardViewTextItemComponent', () => { await fixture.whenStable(); expect(itemUpdatedSpy).toHaveBeenCalledWith({ - target: { ...component.property }, + target: { ...component.property, isValidValue: true }, changed: { textkey: expectedNumber.toString() } diff --git a/lib/core/src/lib/card-view/components/card-view-textitem/card-view-textitem.component.ts b/lib/core/src/lib/card-view/components/card-view-textitem/card-view-textitem.component.ts index c79e6ee51b..3ceacb99b2 100644 --- a/lib/core/src/lib/card-view/components/card-view-textitem/card-view-textitem.component.ts +++ b/lib/core/src/lib/card-view/components/card-view-textitem/card-view-textitem.component.ts @@ -15,16 +15,7 @@ * limitations under the License. */ -import { - ChangeDetectorRef, - Component, - DestroyRef, - inject, - Input, - OnChanges, - SimpleChanges, - ViewEncapsulation -} from '@angular/core'; +import { ChangeDetectorRef, Component, DestroyRef, inject, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core'; import { CardViewTextItemModel } from '../../models/card-view-textitem.model'; import { BaseCardView } from '../base-card-view'; import { MatChipInputEvent, MatChipsModule } from '@angular/material/chips'; @@ -91,7 +82,7 @@ export class CardViewTextItemComponent extends BaseCardView { data?: any; type?: string; multivalued?: boolean; + isValidValue?: boolean; constructor(props: CardViewItemProperties) { this.label = props.label || '';