[ACS-8065][ADF] Unify components in TagModule (#10226)

* [ACS-8065] tag creator unification

* [ACS-8065] test adjustments and after review fixes

* [ACS-8065] remove unnecessary style

* [ACS-8065] fix style selector

* [ACS-8065] remove transforming pipe and unnecessary styles

* [ACS-8065] fixes

* [ACS-8065] fixes

* [ACS-8065] fixes

* [ACS-8065] fix view more button placement

* [ACS-8065] fix view more button placement

* [ACS-8065] fix position of view more button for pagination

* [ACS-8065] Fix imports

* [ACS-8065] Unit test fixes

---------

Co-authored-by: MichalKinas <michal.kinas@hyland.com>
This commit is contained in:
tamaragruszka
2024-12-11 11:07:12 +01:00
committed by GitHub
parent 7fa92308f0
commit d6151308c9
13 changed files with 131 additions and 159 deletions

View File

@@ -32,6 +32,7 @@ This component shows dynamic list of chips which render depending on free space.
|---------------------|---------------------------------------------------------------------------------|---------------|----------------------------------------------------------------| |---------------------|---------------------------------------------------------------------------------|---------------|----------------------------------------------------------------|
| limitChipsDisplayed | `boolean` | false | Should limit number of chips displayed. | | limitChipsDisplayed | `boolean` | false | Should limit number of chips displayed. |
| showDelete | `boolean` | true | Show delete button. | | showDelete | `boolean` | true | Show delete button. |
| disableDelete | `boolean` | false | Disable delete button. |
| roundUpChips | `boolean` | false | Round up chips increasing the border radius of a chip to 20px. | | roundUpChips | `boolean` | false | Round up chips increasing the border radius of a chip to 20px. |
| pagination | [`Pagination`](../../../lib/js-api/src/api/content-rest-api/docs/Pagination.md) | | Provide if you want to use paginated chips. | | pagination | [`Pagination`](../../../lib/js-api/src/api/content-rest-api/docs/Pagination.md) | | Provide if you want to use paginated chips. |
| chips | [`Chip`](../../../lib/core/src/lib/dynamic-chip-list/chip.ts)`[]` | | List of chips to display. | | chips | [`Chip`](../../../lib/core/src/lib/dynamic-chip-list/chip.ts)`[]` | | List of chips to display. |

View File

@@ -98,10 +98,9 @@
</div> </div>
</adf-content-metadata-header> </adf-content-metadata-header>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<div *ngIf="currentPanel.panelTitle === DefaultPanels.TAGS && !editing" class="adf-metadata-properties-tags"> <div *ngIf="currentPanel.panelTitle === DefaultPanels.TAGS && !editing"
<mat-chip-set> class="adf-metadata-properties-tags">
<mat-chip *ngFor="let tag of tags" [disableRipple]="true" class="metadata-properties-tag-chip" data-automation-id="metadata-properties-tag-chip">{{ tag }}</mat-chip> <adf-dynamic-chip-list [chips]="tagsToDisplay" [showDelete]="false" />
</mat-chip-set>
</div> </div>
<div *ngIf="showEmptyTagMessage" class="adf-metadata-no-item-added"> <div *ngIf="showEmptyTagMessage" class="adf-metadata-no-item-added">
{{ 'METADATA.BASIC.NO_TAGS_ADDED' | translate }} {{ 'METADATA.BASIC.NO_TAGS_ADDED' | translate }}

View File

@@ -84,18 +84,8 @@ $panel-properties-height: 56px !default;
} }
&-tags { &-tags {
adf-tags-creator { .adf-dynamic-chip-list-chip {
.adf-tags-creation { padding: 0;
padding-right: 0;
}
&.adf-creator-with-existing-tags-panel {
background: var(--adf-theme-background-dialog-color);
}
}
[hidden] {
visibility: hidden;
} }
} }
} }

View File

@@ -23,20 +23,20 @@ import { ContentMetadataComponent } from './content-metadata.component';
import { ContentMetadataService } from '../../services/content-metadata.service'; import { ContentMetadataService } from '../../services/content-metadata.service';
import { AppConfigService, CardViewBaseItemModel, CardViewComponent, NotificationService, UpdateNotification } from '@alfresco/adf-core'; import { AppConfigService, CardViewBaseItemModel, CardViewComponent, NotificationService, UpdateNotification } from '@alfresco/adf-core';
import { NodesApiService } from '../../../common/services/nodes-api.service'; import { NodesApiService } from '../../../common/services/nodes-api.service';
import { EMPTY, of, throwError } from 'rxjs';
import { CardViewContentUpdateService } from '../../../common/services/card-view-content-update.service';
import { PropertyGroup } from '../../interfaces/property-group.interface';
import { PropertyDescriptorsService } from '../../services/property-descriptors.service';
import { MatExpansionPanel } from '@angular/material/expansion';
import { MatDialogModule } from '@angular/material/dialog';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatChipHarness } from '@angular/material/chips/testing'; import { MatChipHarness } from '@angular/material/chips/testing';
import { MatDialogModule } from '@angular/material/dialog';
import { MatExpansionPanel } from '@angular/material/expansion';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { EMPTY, of, throwError } from 'rxjs';
import { CategoriesManagementComponent, CategoriesManagementMode } from '../../../category';
import { TagsCreatorComponent, TagsCreatorMode } from '../../../tag';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { PropertyGroup } from '../../interfaces/property-group.interface';
import { PropertyDescriptorsService } from '../../services/property-descriptors.service';
import { TagService } from '../../../tag/services/tag.service'; import { TagService } from '../../../tag/services/tag.service';
import { CategoryService } from '../../../category/services/category.service'; import { CategoryService } from '../../../category/services/category.service';
import { TagsCreatorComponent, TagsCreatorMode } from '../../../tag'; import { CardViewContentUpdateService } from '../../../common/services/card-view-content-update.service';
import { CategoriesManagementComponent, CategoriesManagementMode } from '../../../category';
import { ContentTestingModule } from '../../../testing/content.testing.module';
describe('ContentMetadataComponent', () => { describe('ContentMetadataComponent', () => {
let component: ContentMetadataComponent; let component: ContentMetadataComponent;
@@ -71,11 +71,16 @@ describe('ContentMetadataComponent', () => {
const category1 = new Category({ id: 'test', name: 'testCat' }); const category1 = new Category({ id: 'test', name: 'testCat' });
const category2 = new Category({ id: 'test2', name: 'testCat2' }); const category2 = new Category({ id: 'test2', name: 'testCat2' });
const categoryPagingResponse: CategoryPaging = { list: { pagination: {}, entries: [{ entry: category1 }, { entry: category2 }] } }; const categoryPagingResponse: CategoryPaging = {
list: {
pagination: {},
entries: [{ entry: category1 }, { entry: category2 }]
}
};
const findTagElements = async (): Promise<string[]> => { const findTagElements = async (): Promise<string[]> => {
const matChipHarnessList = await TestbedHarnessEnvironment.loader(fixture).getAllHarnesses( const matChipHarnessList = await TestbedHarnessEnvironment.loader(fixture).getAllHarnesses(
MatChipHarness.with({ selector: '[data-automation-id="metadata-properties-tag-chip"]' }) MatChipHarness.with({ selector: '.adf-dynamic-chip-list-chip' })
); );
const tags = []; const tags = [];
for (const matChip of matChipHarnessList) { for (const matChip of matChipHarnessList) {
@@ -269,7 +274,11 @@ describe('ContentMetadataComponent', () => {
})); }));
it('nodeAspectUpdate', fakeAsync(() => { it('nodeAspectUpdate', fakeAsync(() => {
const fakeNode = { id: 'fake-minimal-node', aspectNames: ['ft:a', 'ft:b', 'ft:c'], name: 'fake-node' } as Node; const fakeNode = {
id: 'fake-minimal-node',
aspectNames: ['ft:a', 'ft:b', 'ft:c'],
name: 'fake-node'
} as Node;
getGroupedPropertiesSpy.and.stub(); getGroupedPropertiesSpy.and.stub();
spyOn(contentMetadataService, 'getBasicProperties').and.stub(); spyOn(contentMetadataService, 'getBasicProperties').and.stub();
updateService.updateNodeAspect(fakeNode); updateService.updateNodeAspect(fakeNode);
@@ -1321,7 +1330,8 @@ describe('ContentMetadataComponent', () => {
toggleEditModeForTags(); toggleEditModeForTags();
fixture.detectChanges(); fixture.detectChanges();
expect(await findTagElements()).toHaveSize(0); const noEditableTagsContainer = fixture.debugElement.query(By.css('div.adf-metadata-properties-tags'));
expect(noEditableTagsContainer).toBeNull();
}); });
}); });

View File

@@ -23,6 +23,8 @@ import {
CardViewBaseItemModel, CardViewBaseItemModel,
CardViewComponent, CardViewComponent,
CardViewItem, CardViewItem,
Chip,
DynamicChipListComponent,
NotificationService, NotificationService,
TranslationService, TranslationService,
UpdateNotification UpdateNotification
@@ -39,17 +41,17 @@ import { CategoriesManagementMode } from '../../../category/categories-managemen
import { AllowableOperationsEnum } from '../../../common/models/allowable-operations.enum'; import { AllowableOperationsEnum } from '../../../common/models/allowable-operations.enum';
import { ContentService } from '../../../common/services/content.service'; import { ContentService } from '../../../common/services/content.service';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { MatExpansionModule } from '@angular/material/expansion';
import { ContentMetadataHeaderComponent } from './content-metadata-header.component';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { TranslateModule } from '@ngx-translate/core';
import { MatIconModule } from '@angular/material/icon';
import { MatChipsModule } from '@angular/material/chips'; import { MatChipsModule } from '@angular/material/chips';
import { CategoriesManagementComponent } from '../../../category'; import { MatExpansionModule } from '@angular/material/expansion';
import { DynamicExtensionComponent } from '@alfresco/adf-extensions'; import { MatIconModule } from '@angular/material/icon';
import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatProgressBarModule } from '@angular/material/progress-bar';
import { TagsCreatorComponent } from '../../../tag'; import { TagsCreatorComponent } from '../../../tag';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { TranslateModule } from '@ngx-translate/core';
import { ContentMetadataHeaderComponent } from './content-metadata-header.component';
import { CategoriesManagementComponent } from '../../../category/categories-management/categories-management.component';
import { DynamicExtensionComponent } from '@alfresco/adf-extensions';
const DEFAULT_SEPARATOR = ', '; const DEFAULT_SEPARATOR = ', ';
@@ -74,7 +76,8 @@ enum DefaultPanels {
DynamicExtensionComponent, DynamicExtensionComponent,
MatProgressBarModule, MatProgressBarModule,
TagsCreatorComponent, TagsCreatorComponent,
CardViewComponent CardViewComponent,
DynamicChipListComponent
], ],
templateUrl: './content-metadata.component.html', templateUrl: './content-metadata.component.html',
styleUrls: ['./content-metadata.component.scss'], styleUrls: ['./content-metadata.component.scss'],
@@ -153,6 +156,7 @@ export class ContentMetadataComponent implements OnChanges, OnInit {
basicProperties$: Observable<CardViewItem[]>; basicProperties$: Observable<CardViewItem[]>;
groupedProperties$: Observable<CardViewGroup[]>; groupedProperties$: Observable<CardViewGroup[]>;
tagsToDisplay: Chip[];
changedProperties = {}; changedProperties = {};
hasMetadataChanged = false; hasMetadataChanged = false;
assignedCategories: Category[] = []; assignedCategories: Category[] = [];
@@ -215,6 +219,11 @@ export class ContentMetadataComponent implements OnChanges, OnInit {
return this._assignedTags; return this._assignedTags;
} }
set tags(tags: string[]) {
this._tags = tags;
this.tagsToDisplay = this.tags.map((tag) => ({ id: tag, name: tag }));
}
get tags(): string[] { get tags(): string[] {
return this._tags; return this._tags;
} }
@@ -309,7 +318,8 @@ export class ContentMetadataComponent implements OnChanges, OnInit {
* @param tags array of tags to register, they are not saved yet until we click save button. * @param tags array of tags to register, they are not saved yet until we click save button.
*/ */
storeTagsToAssign(tags: string[]) { storeTagsToAssign(tags: string[]) {
this._tags = tags; this.tags = tags;
this._assignedTags = tags;
this.hasMetadataChanged = true; this.hasMetadataChanged = true;
} }
@@ -512,8 +522,8 @@ export class ContentMetadataComponent implements OnChanges, OnInit {
private loadTagsForNode(id: string) { private loadTagsForNode(id: string) {
this.tagService.getTagsByNodeId(id).subscribe((tagPaging) => { this.tagService.getTagsByNodeId(id).subscribe((tagPaging) => {
this.assignedTagsEntries = tagPaging.list.entries; this.assignedTagsEntries = tagPaging.list.entries;
this._tags = tagPaging.list.entries.map((tagEntry) => tagEntry.entry.tag); this.tags = tagPaging.list.entries.map((tagEntry) => tagEntry.entry.tag);
this._assignedTags = [...this._tags]; this._assignedTags = [...this.tags];
}); });
} }

View File

@@ -142,7 +142,7 @@
}, },
"TAGS_CREATOR": { "TAGS_CREATOR": {
"EXISTING_TAGS": "Existing tags:", "EXISTING_TAGS": "Existing tags:",
"EXISTING_TAGS_SELECTION": "Select an existing tag:", "EXISTING_TAGS_SELECTION": "Select an existing Tag:",
"NO_TAGS_CREATED": "No Tags Created", "NO_TAGS_CREATED": "No Tags Created",
"NO_EXISTING_TAGS": "No Existing Tags", "NO_EXISTING_TAGS": "No Existing Tags",
"TITLE": "Create Tags", "TITLE": "Create Tags",
@@ -152,7 +152,6 @@
"EXISTING_TAG": "Tag already exists", "EXISTING_TAG": "Tag already exists",
"ALREADY_ADDED_TAG": "Tag is already added", "ALREADY_ADDED_TAG": "Tag is already added",
"EMPTY_TAG": "Tag name can't contain only spaces", "EMPTY_TAG": "Tag name can't contain only spaces",
"REQUIRED": "Tag name is required",
"FETCH_TAGS": "Error while fetching the tags", "FETCH_TAGS": "Error while fetching the tags",
"CREATE_TAGS": "Error while creating the tags", "CREATE_TAGS": "Error while creating the tags",
"SPECIAL_CHARACTERS": "Tag name cannot contain prohibited characters" "SPECIAL_CHARACTERS": "Tag name cannot contain prohibited characters"
@@ -172,7 +171,7 @@
"DELETE_CATEGORY": "Delete Category", "DELETE_CATEGORY": "Delete Category",
"EXISTING_CATEGORIES": "Existing Categories:", "EXISTING_CATEGORIES": "Existing Categories:",
"SELECT_EXISTING_CATEGORY": "Select an existing Category:", "SELECT_EXISTING_CATEGORY": "Select an existing Category:",
"NO_EXISTING_CATEGORIES": "No existing Categories", "NO_EXISTING_CATEGORIES": "No Existing Categories",
"GENERIC_CREATE": "Create: {{name}}", "GENERIC_CREATE": "Create: {{name}}",
"NAME": "Category name", "NAME": "Category name",
"LOADING": "Loading", "LOADING": "Loading",

View File

@@ -10,26 +10,21 @@
adf-auto-focus adf-auto-focus
placeholder="{{ 'TAG.TAGS_CREATOR.TAG_SEARCH_PLACEHOLDER' | translate }}" placeholder="{{ 'TAG.TAGS_CREATOR.TAG_SEARCH_PLACEHOLDER' | translate }}"
/> />
<mat-error *ngIf="tagNameControl.invalid && tagNameControl.touched">{{ tagNameErrorMessageKey | translate }} </mat-error> <mat-error *ngIf="tagNameControl.invalid && tagNameControl.touched">
{{ tagNameErrorMessageKey | translate }}
</mat-error>
</div> </div>
<p class="adf-no-tags-message" *ngIf="showEmptyTagMessage"> <p class="adf-no-tags-message" *ngIf="showEmptyTagMessage">
{{ 'TAG.TAGS_CREATOR.NO_TAGS_CREATED' | translate }} {{ 'TAG.TAGS_CREATOR.NO_TAGS_CREATED' | translate }}
</p> </p>
<div class="adf-tags-list" [class.adf-tags-list-fixed]="!tagNameControlVisible" #tagsList> <div class="adf-tags-list" [class.adf-tags-list-fixed]="!tagNameControlVisible" #tagsList>
<mat-chip-listbox *ngIf="tags.length > 0"> <ng-container *ngIf="tags?.length > 0">
<mat-chip *ngFor="let tag of tags" [disableRipple]="true" [title]="tag" class="adf-tags-chip"> <adf-dynamic-chip-list
{{ tag }} class="adf-tags-chips-container"
<button [chips]="tagsToDisplay"
data-automation-id="remove-tag-button" [disableDelete]="disabledTagsRemoving"
mat-icon-button (removedChip)="removeTag($event)" />
(click)="removeTag(tag)" </ng-container>
[attr.title]="'TAG.TAGS_CREATOR.TOOLTIPS.DELETE_TAG' | translate"
[disabled]="disabledTagsRemoving"
matChipRemove>
<mat-icon>close</mat-icon>
</button>
</mat-chip>
</mat-chip-listbox>
</div> </div>
</div> </div>
<div class="adf-existing-tags-panel" *ngIf="existingTagsPanelVisible"> <div class="adf-existing-tags-panel" *ngIf="existingTagsPanelVisible">
@@ -40,7 +35,7 @@
role="button" role="button"
tabindex="0" tabindex="0"
(keyup.enter)="addTag()" (keyup.enter)="addTag()"
[hidden]="tagNameControl.invalid || typing" [hidden]="!tagNameControl.value || tagNameControl.invalid || typing"
> >
{{ 'TAG.TAGS_CREATOR.CREATE_TAG' | translate : { tag: tagNameControl.value } }} {{ 'TAG.TAGS_CREATOR.CREATE_TAG' | translate : { tag: tagNameControl.value } }}
</span> </span>
@@ -49,7 +44,8 @@
</p> </p>
<div class="adf-tags-list"> <div class="adf-tags-list">
<mat-list *ngIf="!spinnerVisible && existingTags" [disabled]="isOnlyCreateMode()"> <mat-list *ngIf="!spinnerVisible && existingTags" [disabled]="isOnlyCreateMode()">
<mat-list-item *ngFor="let tagRow of existingTags" class="adf-tag" (click)="addExistingTagToTagsToAssign(tagRow)"> <mat-list-item *ngFor="let tagRow of existingTags" class="adf-tag"
(click)="addExistingTagToTagsToAssign(tagRow)">
{{ tagRow.entry.tag }} {{ tagRow.entry.tag }}
</mat-list-item> </mat-list-item>
<p *ngIf="!existingTags?.length">{{ 'TAG.TAGS_CREATOR.NO_EXISTING_TAGS' | translate }}</p> <p *ngIf="!existingTags?.length">{{ 'TAG.TAGS_CREATOR.NO_EXISTING_TAGS' | translate }}</p>

View File

@@ -15,19 +15,19 @@
* limitations under the License. * limitations under the License.
*/ */
import { ComponentFixture, discardPeriodicTasks, fakeAsync, flush, TestBed, tick } from '@angular/core/testing';
import { TagsCreatorComponent } from './tags-creator.component';
import { NoopTranslateModule, NotificationService } from '@alfresco/adf-core';
import { By } from '@angular/platform-browser';
import { MatError } from '@angular/material/form-field';
import { TagsCreatorMode, TagService } from '@alfresco/adf-content-services'; import { TagsCreatorMode, TagService } from '@alfresco/adf-content-services';
import { EMPTY, of, throwError } from 'rxjs'; import { NoopTranslateModule, NotificationService } from '@alfresco/adf-core';
import { DebugElement } from '@angular/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { HarnessLoader } from '@angular/cdk/testing'; import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatProgressSpinnerHarness } from '@angular/material/progress-spinner/testing'; import { DebugElement } from '@angular/core';
import { ComponentFixture, discardPeriodicTasks, fakeAsync, flush, TestBed, tick } from '@angular/core/testing';
import { MatChipHarness } from '@angular/material/chips/testing'; import { MatChipHarness } from '@angular/material/chips/testing';
import { MatError } from '@angular/material/form-field';
import { MatProgressSpinnerHarness } from '@angular/material/progress-spinner/testing';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { EMPTY, of, throwError } from 'rxjs';
import { TagsCreatorComponent } from './tags-creator.component';
describe('TagsCreatorComponent', () => { describe('TagsCreatorComponent', () => {
let fixture: ComponentFixture<TagsCreatorComponent>; let fixture: ComponentFixture<TagsCreatorComponent>;
@@ -94,7 +94,7 @@ describe('TagsCreatorComponent', () => {
* @returns list of native elements * @returns list of native elements
*/ */
function getRemoveTagButtons(): HTMLButtonElement[] { function getRemoveTagButtons(): HTMLButtonElement[] {
const elements = fixture.debugElement.queryAll(By.css(`[data-automation-id="remove-tag-button"]`)); const elements = fixture.debugElement.queryAll(By.css(`.adf-dynamic-chip-list-delete-icon`));
return elements.map((el) => el.nativeElement); return elements.map((el) => el.nativeElement);
} }
@@ -104,7 +104,7 @@ describe('TagsCreatorComponent', () => {
* @returns list of tags * @returns list of tags
*/ */
async function getAddedTags(): Promise<string[]> { async function getAddedTags(): Promise<string[]> {
const matChipHarness = await loader.getAllHarnesses(MatChipHarness.with({ selector: '.adf-tags-chip' })); const matChipHarness = await loader.getAllHarnesses(MatChipHarness.with({ selector: '.adf-dynamic-chip-list-chip' }));
const tagElements = []; const tagElements = [];
for (const matChip of matChipHarness) { for (const matChip of matChipHarness) {
tagElements.push(await matChip.getText()); tagElements.push(await matChip.getText());
@@ -337,22 +337,6 @@ describe('TagsCreatorComponent', () => {
expect(getFirstError()).toBe('TAG.TAGS_CREATOR.ERRORS.EMPTY_TAG'); expect(getFirstError()).toBe('TAG.TAGS_CREATOR.ERRORS.EMPTY_TAG');
})); }));
it('should show error for required', fakeAsync(() => {
typeTag('');
component.tagNameControl.markAsTouched();
fixture.detectChanges();
const error = getFirstError();
expect(error).toBe('TAG.TAGS_CREATOR.ERRORS.REQUIRED');
}));
it('should not show error for required if tags are changed', fakeAsync(() => {
typeTag('');
component.tagNameControl.markAsTouched();
component.tags = ['new tag 1', 'new tag 2'];
fixture.detectChanges();
expect(getFirstError()).toBeUndefined();
}));
it('should show error when duplicated already added tag', fakeAsync(() => { it('should show error when duplicated already added tag', fakeAsync(() => {
const tag = 'Some tag'; const tag = 'Some tag';
@@ -438,17 +422,6 @@ describe('TagsCreatorComponent', () => {
const error = getFirstError(); const error = getFirstError();
expect(error).toBe('TAG.TAGS_CREATOR.ERRORS.EXISTING_TAG'); expect(error).toBe('TAG.TAGS_CREATOR.ERRORS.EXISTING_TAG');
})); }));
it('should error for required when not typed anything and blur input', fakeAsync(() => {
component.tagNameControlVisible = true;
component.tagNameControl.markAsTouched();
fixture.detectChanges();
const error = getFirstError();
expect(error).toBe('TAG.TAGS_CREATOR.ERRORS.REQUIRED');
flush();
}));
}); });
}); });

View File

@@ -15,6 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Chip, DynamicChipListComponent, NotificationService } from '@alfresco/adf-core';
import { TagEntry, TagPaging } from '@alfresco/js-api'; import { TagEntry, TagPaging } from '@alfresco/js-api';
import { import {
Component, Component,
@@ -30,28 +31,26 @@ import {
ViewChild, ViewChild,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { debounce, distinctUntilChanged, finalize, first, map, takeUntil, tap } from 'rxjs/operators';
import { EMPTY, forkJoin, Observable, Subject, timer } from 'rxjs';
import { NotificationService } from '@alfresco/adf-core';
import { TagsCreatorMode } from './tags-creator-mode';
import { TagService } from '../services/tag.service';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { MatInputModule } from '@angular/material/input'; import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { AutoFocusDirective } from '../../directives';
import { TranslateModule } from '@ngx-translate/core';
import { MatChipsModule } from '@angular/material/chips';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list'; import { MatListModule } from '@angular/material/list';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { TranslateModule } from '@ngx-translate/core';
import { EMPTY, forkJoin, Observable, Subject, timer } from 'rxjs';
import { debounce, distinctUntilChanged, finalize, first, map, takeUntil, tap } from 'rxjs/operators';
import { AutoFocusDirective } from '../../directives';
import { TagService } from '../services/tag.service';
import { TagsCreatorMode } from './tags-creator-mode';
interface TagNameControlErrors { interface TagNameControlErrors {
duplicatedExistingTag?: boolean; duplicatedExistingTag?: boolean;
duplicatedAddedTag?: boolean; duplicatedAddedTag?: boolean;
emptyTag?: boolean; emptyTag?: boolean;
required?: boolean;
specialCharacters?: boolean; specialCharacters?: boolean;
} }
@@ -76,7 +75,8 @@ const DEFAULT_TAGS_SORTING = {
MatButtonModule, MatButtonModule,
MatIconModule, MatIconModule,
MatListModule, MatListModule,
MatProgressSpinnerModule MatProgressSpinnerModule,
DynamicChipListComponent
], ],
templateUrl: './tags-creator.component.html', templateUrl: './tags-creator.component.html',
styleUrls: ['./tags-creator.component.scss'], styleUrls: ['./tags-creator.component.scss'],
@@ -105,13 +105,11 @@ export class TagsCreatorComponent implements OnInit, OnDestroy {
@Input() @Input()
set tags(tags: string[]) { set tags(tags: string[]) {
this._tags = [...tags]; this._tags = [...tags];
this.tagsToDisplay = this.tags.map((tag) => ({ id: tag, name: tag }));
this._initialExistingTags = null; this._initialExistingTags = null;
this._existingTags = null; this._existingTags = null;
this.loadTags(this.tagNameControl.value); this.loadTags(this.tagNameControl.value);
this.tagNameControl.updateValueAndValidity(); this.tagNameControl.updateValueAndValidity();
if (this.tagNameControl.errors?.required) {
this.tagNameControl.markAsUntouched();
}
} }
get tags(): string[] { get tags(): string[] {
@@ -130,9 +128,6 @@ export class TagsCreatorComponent implements OnInit, OnDestroy {
this._existingTagsPanelVisible = true; this._existingTagsPanelVisible = true;
setTimeout(() => { setTimeout(() => {
this.tagNameInputElement?.nativeElement?.scrollIntoView(); this.tagNameInputElement?.nativeElement?.scrollIntoView();
if (!this.tags.length) {
this.tagNameInputElement?.nativeElement?.focus();
}
}); });
} else { } else {
this._existingTagsPanelVisible = false; this._existingTagsPanelVisible = false;
@@ -156,11 +151,12 @@ export class TagsCreatorComponent implements OnInit, OnDestroy {
@Output() @Output()
tagsChange = new EventEmitter<string[]>(); tagsChange = new EventEmitter<string[]>();
tagsToDisplay: Chip[] = [];
readonly nameErrorMessagesByErrors = new Map<keyof TagNameControlErrors, string>([ readonly nameErrorMessagesByErrors = new Map<keyof TagNameControlErrors, string>([
['duplicatedExistingTag', 'EXISTING_TAG'], ['duplicatedExistingTag', 'EXISTING_TAG'],
['duplicatedAddedTag', 'ALREADY_ADDED_TAG'], ['duplicatedAddedTag', 'ALREADY_ADDED_TAG'],
['emptyTag', 'EMPTY_TAG'], ['emptyTag', 'EMPTY_TAG'],
['required', 'REQUIRED'],
['specialCharacters', 'SPECIAL_CHARACTERS'] ['specialCharacters', 'SPECIAL_CHARACTERS']
]); ]);
@@ -170,7 +166,7 @@ export class TagsCreatorComponent implements OnInit, OnDestroy {
private _tags: string[] = []; private _tags: string[] = [];
private _tagNameControl = new FormControl<string>( private _tagNameControl = new FormControl<string>(
'', '',
[this.validateIfNotAlreadyAdded.bind(this), Validators.required, this.validateEmptyTag, this.validateSpecialCharacters], [this.validateIfNotAlreadyAdded.bind(this), this.validateEmptyTag, this.validateSpecialCharacters],
this.validateIfNotExistingTag.bind(this) this.validateIfNotExistingTag.bind(this)
); );
private _tagNameControlVisible = false; private _tagNameControlVisible = false;
@@ -276,7 +272,7 @@ export class TagsCreatorComponent implements OnInit, OnDestroy {
*/ */
addTag(): void { addTag(): void {
if (!this._typing && !this.tagNameControl.invalid) { if (!this._typing && !this.tagNameControl.invalid) {
this.tags.push(this.tagNameControl.value.trim()); this.tags = [...this.tags, this.tagNameControl.value.trim()];
this.clearTagNameInput(); this.clearTagNameInput();
this.checkScrollbarVisibility(); this.checkScrollbarVisibility();
this.tagsChange.emit(this.tags); this.tagsChange.emit(this.tags);
@@ -291,7 +287,8 @@ export class TagsCreatorComponent implements OnInit, OnDestroy {
*/ */
removeTag(tag: string): void { removeTag(tag: string): void {
this.removeTagFromArray(this.tags, tag); this.removeTagFromArray(this.tags, tag);
this.tagNameControl.updateValueAndValidity({ emitEvent: false }); this.tags = [...this.tags];
this.tagNameControl.updateValueAndValidity();
this.updateExistingTagsListOnRemoveFromTagsToConfirm(tag); this.updateExistingTagsListOnRemoveFromTagsToConfirm(tag);
this.exactTagSet$.next(); this.exactTagSet$.next();
this.checkScrollbarVisibility(); this.checkScrollbarVisibility();
@@ -308,6 +305,7 @@ export class TagsCreatorComponent implements OnInit, OnDestroy {
this.tags.push(selectedTag.entry.tag); this.tags.push(selectedTag.entry.tag);
this.removeTagFromArray(this.existingTags, selectedTag); this.removeTagFromArray(this.existingTags, selectedTag);
this.tagNameControl.updateValueAndValidity(); this.tagNameControl.updateValueAndValidity();
this.tags = [...this.tags];
this.exactTagSet$.next(); this.exactTagSet$.next();
this.tagsChange.emit(this.tags); this.tagsChange.emit(this.tags);
} }
@@ -337,8 +335,8 @@ export class TagsCreatorComponent implements OnInit, OnDestroy {
takeUntil(this.cancelExistingTagsLoading$), takeUntil(this.cancelExistingTagsLoading$),
finalize(() => (this._typing = false)) finalize(() => (this._typing = false))
) )
.subscribe( .subscribe({
({ exactResult, searchedResult }: { exactResult: TagEntry; searchedResult: TagPaging }) => { next: ({ exactResult, searchedResult }: { exactResult: TagEntry; searchedResult: TagPaging }) => {
if (exactResult) { if (exactResult) {
this.existingExactTag = exactResult; this.existingExactTag = exactResult;
this.removeExactTagFromSearchedResult(searchedResult); this.removeExactTagFromSearchedResult(searchedResult);
@@ -352,11 +350,11 @@ export class TagsCreatorComponent implements OnInit, OnDestroy {
this.exactTagSet$.next(); this.exactTagSet$.next();
this._spinnerVisible = false; this._spinnerVisible = false;
}, },
() => { error: () => {
this.notificationService.showError('TAG.TAGS_CREATOR.ERRORS.FETCH_TAGS'); this.notificationService.showError('TAG.TAGS_CREATOR.ERRORS.FETCH_TAGS');
this._spinnerVisible = false; this._spinnerVisible = false;
} }
); });
} else { } else {
this.existingExactTag = null; this.existingExactTag = null;
this._spinnerVisible = false; this._spinnerVisible = false;

View File

@@ -18,6 +18,7 @@
<mat-icon *ngIf="showDelete" <mat-icon *ngIf="showDelete"
id="adf-dynamic-chip-list-delete-{{ chip.name }}" id="adf-dynamic-chip-list-delete-{{ chip.name }}"
class="adf-dynamic-chip-list-delete-icon" class="adf-dynamic-chip-list-delete-icon"
[disabled]="disableDelete"
matChipRemove> matChipRemove>
close close
</mat-icon> </mat-icon>
@@ -26,9 +27,9 @@
<button <button
data-automation-id="adf-dynamic-chip-list-view-more-button" data-automation-id="adf-dynamic-chip-list-view-more-button"
mat-button mat-button
[hidden]="chipsToDisplay.length === 0 || !limitChipsDisplayed" [hidden]="chipsToDisplay?.length === 0 || !limitChipsDisplayed"
[style.left.px]="viewMoreButtonLeftOffset" [style.left.px]="viewMoreButtonLeftOffset"
[style.top.px]="viewMoreButtonTop" [style.top.px]="!!pagination ? viewMoreButtonTop : ''"
class="adf-dynamic-chip-list-view-more-button" class="adf-dynamic-chip-list-view-more-button"
[class.adf-dynamic-chip-list-hidden-btn]="!calculationsDone" [class.adf-dynamic-chip-list-hidden-btn]="!calculationsDone"
(click)="displayNextChips($event)"> (click)="displayNextChips($event)">

View File

@@ -10,7 +10,6 @@
.adf-dynamic-chip-list-view-more-button { .adf-dynamic-chip-list-view-more-button {
margin-left: 5px; margin-left: 5px;
position: absolute; position: absolute;
width: auto;
padding: 0 16px; padding: 0 16px;
&[hidden] { &[hidden] {
@@ -27,27 +26,14 @@
} }
&.adf-dynamic-chip-list-paginated { &.adf-dynamic-chip-list-paginated {
/* TODO(mdc-migration): The following rule targets internal classes of chips that may no longer apply for the MDC version. */
mat-chip-list {
width: 100%;
& > div {
width: 100%;
}
}
.adf-dynamic-chip-list-view-more-button { .adf-dynamic-chip-list-view-more-button {
margin: -2px 4px 4px 24px; margin-left: 24px;
} }
} }
&.adf-dynamic-chip-list-button-in-next-line { &.adf-dynamic-chip-list-button-in-next-line {
align-items: unset; align-items: unset;
padding-bottom: 54px; padding-bottom: 54px;
.adf-dynamic-chip-list-view-more-button {
margin-top: 4px;
}
} }
&:not(.adf-dynamic-chip-list-paginated) { &:not(.adf-dynamic-chip-list-paginated) {
@@ -55,7 +41,6 @@
&:not(.adf-dynamic-chip-list-flex-column) { &:not(.adf-dynamic-chip-list-flex-column) {
.adf-dynamic-chip-list-view-more-button { .adf-dynamic-chip-list-view-more-button {
margin-top: 18px;
margin-left: 4px; margin-left: 4px;
} }
} }
@@ -70,7 +55,6 @@
} }
.adf-dynamic-chip-list-chip { .adf-dynamic-chip-list-chip {
height: auto;
word-break: break-word; word-break: break-word;
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
@@ -84,14 +68,4 @@
} }
} }
} }
.adf-dynamic-chip-list-delete-icon {
font-size: var(--theme-title-font-size);
background-repeat: no-repeat;
display: inline-block;
fill: currentcolor;
height: 20px;
width: 20px;
color: var(--theme-primary-color-default-contrast);
}
} }

View File

@@ -146,6 +146,19 @@ describe('DynamicChipListComponent', () => {
expect(getComputedStyle(chip.nativeElement).borderRadius).toBe('20px'); expect(getComputedStyle(chip.nativeElement).borderRadius).toBe('20px');
}); });
it('should disable the delete button if disableDelete is true', async () => {
component.disableDelete = true;
component.ngOnChanges({
chips: new SimpleChange(undefined, component.chips, true)
});
fixture.detectChanges();
await fixture.whenStable();
const chip = fixture.debugElement.query(By.css('.adf-dynamic-chip-list-delete-icon'));
expect(Object.keys(chip.attributes)).toContain('disabled');
});
it('should not render view more button by default', async () => { it('should not render view more button by default', async () => {
component.ngOnChanges({ component.ngOnChanges({
chips: new SimpleChange(undefined, component.chips, true) chips: new SimpleChange(undefined, component.chips, true)

View File

@@ -65,6 +65,10 @@ export class DynamicChipListComponent implements OnChanges, OnInit, AfterViewIni
@Input() @Input()
showDelete = true; showDelete = true;
/** Disable delete button. */
@Input()
disableDelete = false;
/** Should limit number of chips displayed. */ /** Should limit number of chips displayed. */
@Input() @Input()
limitChipsDisplayed = false; limitChipsDisplayed = false;
@@ -101,8 +105,10 @@ export class DynamicChipListComponent implements OnChanges, OnInit, AfterViewIni
private viewMoreButtonLeftOffsetBeforeFlexDirection: number; private viewMoreButtonLeftOffsetBeforeFlexDirection: number;
private requestedDisplayingAllChips = false; private requestedDisplayingAllChips = false;
private resizeObserver = new ResizeObserver(() => { private resizeObserver = new ResizeObserver(() => {
this.calculateChipsToDisplay(); if (this.initialLimitChipsDisplayed && this.chipsToDisplay.length) {
this.changeDetectorRef.detectChanges(); this.calculateChipsToDisplay();
this.changeDetectorRef.detectChanges();
}
}); });
constructor(private changeDetectorRef: ChangeDetectorRef) {} constructor(private changeDetectorRef: ChangeDetectorRef) {}
@@ -117,10 +123,8 @@ export class DynamicChipListComponent implements OnChanges, OnInit, AfterViewIni
this.initialChips = this.chips; this.initialChips = this.chips;
this.chipsToDisplay = this.initialChips; this.chipsToDisplay = this.initialChips;
if (this.limitChipsDisplayed && this.chipsToDisplay.length) { if (this.limitChipsDisplayed && this.chipsToDisplay.length) {
setTimeout(() => { this.calculateChipsToDisplay();
this.calculateChipsToDisplay(); this.changeDetectorRef.detectChanges();
this.changeDetectorRef.detectChanges();
});
} }
} }
} }
@@ -230,5 +234,9 @@ export class DynamicChipListComponent implements OnChanges, OnInit, AfterViewIni
} else { } else {
this.viewMoreButtonLeftOffset = this.columnFlexDirection ? 0 : this.viewMoreButtonLeftOffsetBeforeFlexDirection; this.viewMoreButtonLeftOffset = this.columnFlexDirection ? 0 : this.viewMoreButtonLeftOffsetBeforeFlexDirection;
} }
if (!this.pagination) {
this.viewMoreButtonTop = null;
}
} }
} }