[ACS-5483] group details view general info and list of assigned users (#9329)

* ACS-5483 Added possibility to load and update group

* ACS-5483 Implemented unsaved changes dialog

* ACS-5483 Removed console log

* ACS-5483 Made dynamic chip list reusable

* ACS-5483 Fix for more than one row chips

* ACS-5483 Fix for pagination

* ACS-5483 Added some fixes

* ACS-5483 Fixed displaying tags for node

* ACS-5483 Renamed css classes

* ACS-5483 Fixed resizing when chips have pagination

* ACS-5483 Clearing code

* ACS-5483 Documentation for dynamic chip list component

* ACS-5483 Documentation for unsaved changes dialog and guard

* ACS-5483 Documentation for group service

* ACS-5483 Unit tests for GroupService

* ACS-5483 Unit tests for dynamic chip list component

* ACS-5483 Changed fdescribe to describe

* ACS-5483 Unit tests for tag node list component

* ACS-5483 Unit tests for unsaved changes dialog component

* ACS-5483 Unit tests for unsaved changes guard

* ACS-5483 Added description field to group models

* ACS-5483 Correction for updating with description

* ACS-5483 Fixed lint issues

* ACS-5483 Addressed PR comments

* ACS-5483 Reduced complexity

* ACS-5483 Reduced complexity

* ACS-5483 Addressed PR comments
This commit is contained in:
AleksanderSklorz 2024-02-15 13:02:24 +01:00 committed by GitHub
parent 8363d09e79
commit 213a73fc36
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 1587 additions and 426 deletions

View File

@ -93,6 +93,7 @@ for more information about installing and using the source code.
| [Comments Component](core/components/comments.component.md) | Displays comments from users involved in a specified environment and allows an involved user to add a comment to a environment. | [Source](../lib/core/src/lib/comments/comments.component.ts) |
| [Data Column Component](core/components/data-column.component.md) | Defines column properties for DataTable, Tasklist, Document List and other components. | [Source](../lib/core/src/lib/datatable/data-column/data-column.component.ts) |
| [DataTable component](core/components/datatable.component.md) | Displays data as a table with customizable columns and presentation. | [Source](../lib/core/src/lib/datatable/components/datatable/datatable.component.ts) |
| [Dynamic Chip List component](core/components/dynamic-chip-list.component.md) | This component shows dynamic list of chips which render depending on free space. | [Source](../lib/core/src/lib/dynamic-chip-list/dynamic-chip-list.component.ts) |
| [Empty Content Component](core/components/empty-content.component.md) | Provides a generic "Empty Content" placeholder for components. | [Source](../lib/core/src/lib/templates/empty-content/empty-content.component.ts) |
| [Empty list component](core/components/empty-list.component.md) | Displays a message indicating that a list is empty. | [Source](../lib/core/src/lib/datatable/components/empty-list/empty-list.component.ts) |
| [Error Content Component](core/components/error-content.component.md) | Displays info about a specific error. | [Source](../lib/core/src/lib/templates/error-content/error-content.component.ts) |
@ -144,6 +145,7 @@ for more information about installing and using the source code.
| Name | Description | Source link |
| ---- | ----------- | ----------- |
| [Edit JSON Dialog](core/dialogs/edit-json.dialog.md) | Allows a user to preview or edit a JSON content in a dialog. | [Source](../lib/testing/src/lib/core/dialog/edit-json-dialog.ts) |
| [Unsaved Changes Dialog](core/dialogs/unsaved-changes-dialog.component.md) | Dialog which informs about unsaved changes. Allows discard them and proceed or close dialog and stop proceeding. | [Source](../lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.ts) |
### Interfaces
@ -238,6 +240,7 @@ The model to update the DataTable require the ID of the row you want change and
| [Storage service](core/services/storage.service.md) | Stores items in the form of key-value pairs. | [Source](../lib/core/src/lib/common/services/storage.service.ts) |
| [Thumbnail service](core/services/thumbnail.service.md) | Retrieves an SVG thumbnail image to represent a document type. | [Source](../lib/core/src/lib/common/services/thumbnail.service.ts) |
| [Translation service](core/services/translation.service.md) | Supports localisation. | [Source](../lib/core/src/lib/translation/translation.service.ts) |
| [Unsaved Changes guard](core/services/unsaved-changes.guard.md) | This guard prevents deactivating route if page has any unsaved changes. User needs intentionally discard changes through displayed modal to leave actual route. | [Source](../lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes.guard.ts) |
| [Upload Service](core/services/upload.service.md) | Provides access to various APIs related to file upload features. | [Source](../lib/content-services/src/lib/common/services/upload.service.ts) |
| [User access service](core/services/user-access.service.md) | Checks the global and application access of a user | [Source](../lib/core/src/lib/auth/services/user-access.service.ts) |
| [User Preferences Service](core/services/user-preferences.service.md) | Stores preferences for the app and for individual components. | [Source](../lib/core/src/lib/common/services/user-preferences.service.ts) |
@ -387,6 +390,7 @@ for more information about installing and using the source code.
| [Document Actions service](content-services/services/document-actions.service.md) | Implements the document menu actions for the Document List component. | [Source](../lib/content-services/src/lib/document-list/services/document-actions.service.ts) |
| [Document List service](content-services/services/document-list.service.md) | Implements node operations used by the Document List component. | [Source](../lib/content-services/src/lib/document-list/services/document-list.service.ts) |
| [Folder Actions service](content-services/services/folder-actions.service.md) | Implements the folder menu actions for the Document List component. | [Source](../lib/content-services/src/lib/document-list/services/folder-actions.service.ts) |
| [Group Service](content-services/services/group.service.md) | Manages groups. | [Source](../lib/content-services/src/lib/group/services/group.service.ts) |
| [New Version Uploader service](content-services/services/new-version-uploader.dialog.service.md) | Display a dialog that allows to upload new file version or to manage the current node versions. | [Source](../lib/content-services/src/lib/new-version-uploader/new-version-uploader.service.ts) |
| [Node Comments Service](content-services/services/node-comments.service.md) | Adds and retrieves comments for nodes in Content Services. | [Source](../lib/content-services/src/lib/node-comments/services/node-comments.service.ts) |
| [Node permission dialog service](content-services/services/node-permission-dialog.service.md) | Displays dialogs to let the user set node permissions. | [Source](../lib/content-services/src/lib/permission-manager/services/node-permission-dialog.service.ts) |

View File

@ -33,7 +33,7 @@ Shows tags for a node.
| Name | Type | Description |
| ---- | ---- | ----------- |
| results | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<any>` | Emitted when a tag is selected. |
| results | [`EventEmitter`](https://angular.io/api/core/EventEmitter)[`<TagEntry>`](../../../lib/js-api/src/api/content-rest-api/model/tagEntry.ts)`[]` | Emitted when a tag is selected. |
## Details
@ -49,3 +49,7 @@ To limit number of tags initially displayed set `limitTagsDisplayed` to `true`.
```
Now when tag chips will exceed the size of the container number of displayed chips will be limited to as much as fits together with view more button. At least one tag will always be displayed, when one tag and view more button won't fit into one line the button will be displayed under the tag.
## See Also
- [Dynamic Chip List component](../../core/components/dynamic-chip-list.component.md)

View File

@ -0,0 +1,25 @@
---
Title: Group Service
Added: v6.6.0
Status: Active
Last reviewed: 2024-02-07
---
# [Group Service](../../../lib/content-services/src/lib/group/services/group.service.ts "Defined in group.service.ts")
Manages groups.
## Class members
### Methods
- **getGroup**(id: `string`, opts?: [`ContentIncludeQuery`](../../../lib/js-api/src/api/content-rest-api/api/types.ts)): [`Observable`](https://rxjs.dev/guide/observable)`<`[`GroupEntry`](../../../lib/js-api/src/api/content-rest-api/model/groupEntry.ts)`>`<br/>
Returns group for specified id.
- _id:_ `string` - id of group to return.
- _opts:_ [`ContentIncludeQuery`](../../../lib/js-api/src/api/content-rest-api/api/types.ts) - additional query parameters.
- **Returns** [`Observable`](https://rxjs.dev/guide/observable)`<`[`GroupEntry`](../../../lib/js-api/src/api/content-rest-api/model/groupEntry.ts)`>` - group for specified id.
- **updateGroup**(group: [`Group`](../../../lib/js-api/src/api/content-rest-api/docs/Group.md), opts?: [`ContentIncludeQuery`](../../../lib/js-api/src/api/content-rest-api/api/types.ts)): [`Observable`](https://rxjs.dev/guide/observable)`<`[`GroupEntry`](../../../lib/js-api/src/api/content-rest-api/model/groupEntry.ts)`>`<br/>
Updates specified group.
- _group:_ [`Group`](../../../lib/js-api/src/api/content-rest-api/docs/Group.md) - group to update.
- _opts:_ [`ContentIncludeQuery`](../../../lib/js-api/src/api/content-rest-api/api/types.ts) - additional query parameters.
- **Returns** [`Observable`](https://rxjs.dev/guide/observable)`<`[`GroupEntry`](../../../lib/js-api/src/api/content-rest-api/model/groupEntry.ts)`>` - updated group.

View File

@ -0,0 +1,57 @@
---
Title: Dynamic Chip List component
Added: v6.6.0
Status: Active
Last reviewed: 2024-02-06
---
# [Dynamic Chip List component](../../../lib/core/src/lib/dynamic-chip-list/dynamic-chip-list.component.ts "Defined in dynamic-chip-list.component.ts")
This component shows dynamic list of chips which render depending on free space.
![List of chips](../../docassets/images/dynamic-chip-list.png)
## Basic Usage
```html
<adf-dynamic-chip-list
[chips]="chips"
[limitChipsDisplayed]="true"
[showDelete]="true"
(displayNext)="onDisplayNext()"
(removedChip)="onRemovedChip($event)">
</adf-dynamic-chip-list>
```
## Class members
### Properties
| Name | Type | Default value | Description |
|---------------------|---------------------------------------------------------------------------------|---------------|---------------------------------------------|
| limitChipsDisplayed | `boolean` | false | Should limit number of chips displayed. |
| showDelete | `boolean` | true | Show delete button. |
| 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. |
### Events
| Name | Type | Description |
|-------------|----------------------------------------------------------------------|-----------------------------------------------|
| displayNext | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<void>` | Emitted when button for view more is clicked. |
| removedChip | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<string>` | Emitted when any chip is removed. |
## Details
### Limit number of chips displayed initially
To limit number of chips initially displayed set `limitChipsDisplayed` to `true`.
```html
<adf-dynamic-chip-list
[chips]="chips"
[limitChipsDisplayed]="true">
</adf-dynamic-chip-list>
```
Now when chips will exceed the size of the container number of displayed chips will be limited to as much as fits together with view more button. At least one chip will always be displayed, when one chip and view more button won't fit into one line the button will be displayed under the chip.

View File

@ -0,0 +1,27 @@
---
Title: Unsaved Changes Dialog component
Added: v6.6.0
Status: Active
Last reviewed: 2024-02-06
---
# [Unsaved Changes Dialog component](../../../lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.ts "Defined in unsaved-changes-dialog.component.ts")
Dialog which informs about unsaved changes. Allows discard them and proceed or close dialog and stop proceeding.
![Unsaved Changes Dialog](../../docassets/images/unsaved-changes-dialog.png)
## Basic Usage
```ts
this.dialog.open(UnsavedChangesDialogComponent);
```
## Details
### Detecting if changes should be discarded.
To detect if changes should be discarded you need to check value passed on dialog's close. If value is true then changes should be discard, false otherwise.
## See also
- [Unsaved Changes guard](../services/unsaved-changes.guard.md)

View File

@ -0,0 +1,22 @@
---
Title: Unsaved Changes guard
Added: v6.6.0
Status: Active
Last reviewed: 2024-02-06
---
# [Unsaved Changes guard](../../../lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes.guard.ts "Defined in unsaved-changes.guard.ts")
This guard prevents deactivating route if page has any unsaved changes. User needs intentionally discard changes through displayed modal to leave actual route.
## Class members
### Methods
- **canDeactivate**(): `boolean` | [`Observable`](https://rxjs.dev/guide/observable)`<boolean>`<br/>
Allows to deactivate route when there is no unsaved changes, otherwise displays dialog to confirm discarding changes.
- **Returns** `boolean` | [`Observable`](https://rxjs.dev/guide/observable)`<boolean>` - true when there is no unsaved changes or changes can be discarded, false otherwise.
## See also
- [Unsaved Changes Dialog component](../dialogs/unsaved-changes-dialog.component.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -12,6 +12,7 @@ backend services have been tested with each released version of ADF.
## Versions
- [v6.7.0](#v670)
- [v6.4.0](#v640)
- [v6.2.0](#v620)
- [v6.1.0](#v610)
@ -44,6 +45,17 @@ backend services have been tested with each released version of ADF.
- [v2.1.0](#v210)
- [v2.0.0](#v200)
## v6.7.0
<!--670 start-->
- [Dynamic Chip List component](core/components/dynamic-chip-list.component.md)
- [Unsaved Changes Dialog component](core/dialogs/unsaved-changes-dialog.component.md)
- [Unsaved Changes guard](core/services/unsaved-changes.guard.md)
- [Group Service](content-services/services/group.service.md)
<!--670 end-->
## v6.4.0
<!--v640 start-->

View File

@ -0,0 +1,134 @@
/*!
* @license
* Copyright © 2005-2023 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 { TestBed } from '@angular/core/testing';
import { ContentTestingModule } from '../../testing/content.testing.module';
import { GroupService } from '@alfresco/adf-content-services';
import { ContentIncludeQuery, GroupEntry } from '@alfresco/js-api';
describe('GroupService', () => {
let service: GroupService;
let group: GroupEntry;
let returnedGroup: GroupEntry;
let opts: ContentIncludeQuery;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ContentTestingModule]
});
service = TestBed.inject(GroupService);
group = {
entry: {
id: 'some id',
displayName: 'some name',
description: 'some description'
}
};
returnedGroup = JSON.parse(JSON.stringify(group));
opts = {
include: ['description']
};
});
describe('getGroup', () => {
it('should return group returned by GroupsApi', (done) => {
spyOn(service.groupsApi, 'getGroup').and.returnValue(Promise.resolve(returnedGroup));
service.getGroup(group.entry.id, opts).subscribe((groupEntry) => {
expect(groupEntry).toBe(returnedGroup);
expect(service.groupsApi.getGroup).toHaveBeenCalledWith(group.entry.id, {
include: ['description']
});
done();
});
});
it('should return group returned by GroupsApi when description is not supplied', (done) => {
returnedGroup.entry.description = undefined;
spyOn(service.groupsApi, 'getGroup').and.returnValue(Promise.resolve(returnedGroup));
service.getGroup(group.entry.id, opts).subscribe((groupEntry) => {
expect(groupEntry).toEqual({
entry: {
id: returnedGroup.entry.id,
displayName: returnedGroup.entry.displayName,
description: ''
}
});
expect(service.groupsApi.getGroup).toHaveBeenCalledWith(group.entry.id, {
include: ['description']
});
done();
});
});
});
describe('updateGroup', () => {
it('should return updated Group', (done) => {
spyOn(service.groupsApi, 'updateGroup').and.returnValue(Promise.resolve(returnedGroup));
service.updateGroup(group.entry, opts).subscribe((groupEntry) => {
expect(groupEntry).toEqual(returnedGroup);
expect(service.groupsApi.updateGroup).toHaveBeenCalledWith(group.entry.id, {
displayName: group.entry.displayName,
description: group.entry.description
}, {
include: ['description']
});
done();
});
});
it('should return updated Group when description is not supplied', (done) => {
returnedGroup.entry.description = undefined;
spyOn(service.groupsApi, 'updateGroup').and.returnValue(Promise.resolve(returnedGroup));
service.updateGroup(group.entry, opts).subscribe((groupEntry) => {
expect(groupEntry).toEqual({
entry: {
id: returnedGroup.entry.id,
displayName: returnedGroup.entry.displayName,
description: ''
}
});
expect(service.groupsApi.updateGroup).toHaveBeenCalledWith(group.entry.id, {
displayName: group.entry.displayName,
description: group.entry.description
}, {
include: ['description']
});
done();
});
});
it('should allow to update only description', (done) => {
spyOn(service.groupsApi, 'updateGroup').and.returnValue(Promise.resolve(returnedGroup));
group.entry.displayName = undefined;
service.updateGroup(group.entry, opts).subscribe((groupEntry) => {
expect(groupEntry).toEqual(returnedGroup);
expect(service.groupsApi.updateGroup).toHaveBeenCalledWith(group.entry.id, {
displayName: group.entry.displayName,
description: group.entry.description
}, {
include: ['description']
});
done();
});
});
});
});

View File

@ -16,8 +16,10 @@
*/
import { Injectable } from '@angular/core';
import { GroupEntry, GroupsApi } from '@alfresco/js-api';
import { ContentIncludeQuery, Group, GroupEntry, GroupsApi } from '@alfresco/js-api';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { from, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
@ -48,4 +50,35 @@ export class GroupService {
return accumulator;
}
}
/**
* Returns group for specified id.
*
* @param id id of group to return.
* @param opts additional query parameters
* @returns Observable<GroupEntry> group for specified id.
*/
getGroup(id: string, opts?: ContentIncludeQuery): Observable<GroupEntry> {
return from(this.groupsApi.getGroup(id, opts)).pipe(map((group) => {
group.entry.description ||= '';
return group;
}));
}
/**
* Updates specified group.
*
* @param group group to update.
* @param opts additional query parameters
* @returns Observable<GroupEntry> updated group.
*/
updateGroup(group: Group, opts?: ContentIncludeQuery): Observable<GroupEntry> {
return from(this.groupsApi.updateGroup(group.id, {
displayName: group.displayName,
description: group.description
}, opts)).pipe(map((updatedGroup) => {
updatedGroup.entry.description ||= '';
return updatedGroup;
}));
}
}

View File

@ -1,20 +1,7 @@
<div class="adf-tag-node-list" [class.adf-flex-column]="limitTagsDisplayed && (!calculationsDone || columnFlexDirection)" #nodeListContainer>
<mat-chip-list [class.adf-full-width]="limitTagsDisplayed && !calculationsDone" role="listbox" [attr.aria-label]="'METADATA.BASIC.TAGS' | translate">
<mat-chip class="adf-tag-chips"
*ngFor="let currentEntry of tagsEntries; let idx = index"
(removed)="removeTag(currentEntry.entry.id)">
<span id="tag_name_{{idx}}">{{currentEntry.entry.tag}}</span>
<mat-icon *ngIf="showDelete" id="tag_chips_delete_{{currentEntry.entry.tag}}"
class="adf-tag-chips-delete-icon" matChipRemove>cancel
</mat-icon>
</mat-chip>
</mat-chip-list>
<button mat-button
[hidden]="!limitTagsDisplayed"
[style.left.px]="viewMoreButtonLeftOffset"
class="adf-view-more-button"
[class.adf-hidden-btn]="!calculationsDone"
(click)="displayAllTags($event)"
>{{ 'TAG_NODE_LIST.VIEW_MORE' | translate: { count: undisplayedTagsCount} }}
</button>
</div>
<adf-dynamic-chip-list
[chips]="tagChips"
[limitChipsDisplayed]="limitTagsDisplayed"
[showDelete]="showDelete"
(displayNext)="refreshTag()"
(removedChip)="removeTag($event)">
</adf-dynamic-chip-list>

View File

@ -1,61 +0,0 @@
.adf-tag-node-list {
display: flex;
align-items: center;
flex-direction: row;
width: inherit;
.adf-view-more-button {
margin: 4px;
color: var(--adf-theme-foreground-text-color-054);
position: absolute;
&[hidden] {
visibility: hidden;
}
}
&.adf-flex-column {
flex-direction: column;
.adf-view-more-button {
position: relative;
}
}
.adf-full-width {
width: 100%;
}
.adf-hidden-btn {
visibility: hidden;
}
.adf-tag-chips {
color: var(--theme-primary-color-default-contrast);
background-color: var(--theme-primary-color);
height: auto;
word-break: break-word;
}
.adf-tag-chips-delete {
overflow: visible;
cursor: pointer;
height: 17px;
width: 20px;
float: right;
border: 0;
background: none;
padding: 0;
margin: -1px 0 0 10px;
}
.adf-tag-chips-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) !important;
}
}

View File

@ -18,62 +18,16 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TagNodeListComponent } from './tag-node-list.component';
import { TagService } from './services/tag.service';
import { of } from 'rxjs';
import { Observable, of, Subject } from 'rxjs';
import { ContentTestingModule } from '../testing/content.testing.module';
import { Tag, TagEntry, TagPaging } from '@alfresco/js-api';
import { DynamicChipListComponent } from '@alfresco/adf-core';
import { By } from '@angular/platform-browser';
import { TranslateModule } from '@ngx-translate/core';
import { TagEntry } from '@alfresco/js-api';
describe('TagNodeList', () => {
const dataTag = {
list: {
pagination: {
count: 3,
hasMoreItems: false,
totalItems: 3,
skipCount: 0,
maxItems: 100
},
entries: [
{
entry: {tag: 'test1', id: '0ee933fa-57fc-4587-8a77-b787e814f1d2'}
},
{
entry: {tag: 'test2', id: 'fcb92659-1f10-41b4-9b17-851b72a3b597'}
},
{
entry: {tag: 'test3', id: 'fb4213c0-729d-466c-9a6c-ee2e937273bf'}
},
{
entry: {tag: 'test4', id: 'as4213c0-729d-466c-9a6c-ee2e937273as'}
}
]
}
};
let component: TagNodeListComponent;
let fixture: ComponentFixture<TagNodeListComponent>;
let element: HTMLElement;
let tagService: TagService;
let resizeCallback: ResizeObserverCallback;
/**
* Find 'More' button
*
* @returns native element
*/
function findViewMoreButton(): HTMLButtonElement {
return element.querySelector('.adf-view-more-button');
}
/**
* Get the tag chips
*
* @returns native element list
*/
function findTagChips(): NodeListOf<Element> {
return element.querySelectorAll('.adf-tag-chips');
}
beforeEach(() => {
TestBed.configureTestingModule({
@ -82,218 +36,176 @@ describe('TagNodeList', () => {
ContentTestingModule
]
});
const resizeObserverSpy = spyOn(window, 'ResizeObserver').and.callThrough();
fixture = TestBed.createComponent(TagNodeListComponent);
tagService = TestBed.inject(TagService);
spyOn(tagService, 'getTagsByNodeId').and.returnValue(of(dataTag));
element = fixture.nativeElement;
component = fixture.componentInstance;
component.nodeId = 'fake-node-id';
fixture.detectChanges();
resizeCallback = resizeObserverSpy.calls.mostRecent().args[0];
});
describe('Rendering tests', () => {
it('Tag list relative a single node should be rendered', async () => {
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('#tag_name_0').innerHTML).toBe('test1');
expect(element.querySelector('#tag_name_1').innerHTML).toBe('test2');
expect(element.querySelector('#tag_name_2').innerHTML).toBe('test3');
expect(element.querySelector('#tag_chips_delete_test1')).not.toBe(null);
expect(element.querySelector('#tag_chips_delete_test2')).not.toBe(null);
expect(element.querySelector('#tag_chips_delete_test3')).not.toBe(null);
});
it('Tag list click on delete button should delete the tag', async () => {
spyOn(tagService, 'removeTag').and.returnValue(of(undefined));
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
const deleteButton: any = element.querySelector('#tag_chips_delete_test1');
deleteButton.click();
expect(tagService.removeTag).toHaveBeenCalledWith('fake-node-id', '0ee933fa-57fc-4587-8a77-b787e814f1d2');
});
it('Should not show the delete tag button if showDelete is false', async () => {
component.showDelete = false;
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
const deleteButton: any = element.querySelector('#tag_chips_delete_test1');
expect(deleteButton).toBeNull();
});
it('Should show the delete tag button if showDelete is true', async () => {
component.showDelete = true;
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
const deleteButton: any = element.querySelector('#tag_chips_delete_test1');
expect(deleteButton).not.toBeNull();
});
it('should not render view more button by default', async () => {
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
expect(findViewMoreButton().hidden).toBeTrue();
expect(findTagChips()).toHaveSize(4);
});
});
describe('Limit tags display', () => {
let initialEntries: TagEntry[];
/**
* Render tags
*
* @param entries tags to render
*/
async function renderTags(entries?: TagEntry[]) {
dataTag.list.entries = entries || initialEntries;
component.tagsEntries = dataTag.list.entries;
fixture.detectChanges();
await fixture.whenStable();
}
beforeAll(() => {
initialEntries = dataTag.list.entries;
});
describe('DynamicChipListComponent', () => {
let dynamicChipListComponent: DynamicChipListComponent;
let tagService: TagService;
let getTagsByNodeIdSpy: jasmine.Spy<(nodeId: string) => Observable<TagPaging>>;
beforeEach(() => {
fixture.detectChanges();
dynamicChipListComponent = fixture.debugElement.query(By.directive(DynamicChipListComponent)).componentInstance;
tagService = TestBed.inject(TagService);
getTagsByNodeIdSpy = spyOn(tagService, 'getTagsByNodeId').and.returnValue(new Subject<TagPaging>());
});
it('should have assigned limitChipsDisplayed to true if true is passed to limitTagsDisplayed', () => {
component.limitTagsDisplayed = true;
component.ngOnInit();
element.style.maxWidth = '309px';
fixture.detectChanges();
expect(dynamicChipListComponent.limitChipsDisplayed).toBeTrue();
});
it('should render view more button when limiting is enabled', async () => {
await renderTags();
component.ngOnChanges();
it('should have assigned limitChipsDisplayed to false if false is passed to limitTagsDisplayed', () => {
component.limitTagsDisplayed = true;
fixture.detectChanges();
await fixture.whenStable();
expect(findViewMoreButton().hidden).toBeFalse();
expect(findTagChips()).toHaveSize(component.tagsEntries.length);
component.limitTagsDisplayed = false;
fixture.detectChanges();
expect(dynamicChipListComponent.limitChipsDisplayed).toBeFalse();
});
it('should not render view more button when limiting is enabled and all tags fits into container', async () => {
await renderTags();
element.style.maxWidth = '800px';
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
expect(findViewMoreButton().hidden).toBeTrue();
expect(findTagChips()).toHaveSize(4);
it('should have assigned limitChipsDisplayed to false by default if limitTagsDisplayed is not delivered', () => {
expect(dynamicChipListComponent.limitChipsDisplayed).toBeFalse();
});
it('should display all tags when view more button is clicked', async () => {
await renderTags();
component.ngOnChanges();
it('should have assigned showDelete to true if true is passed to showDelete of tag node list', () => {
component.showDelete = false;
fixture.detectChanges();
await fixture.whenStable();
component.showDelete = true;
let viewMoreButton = findViewMoreButton();
viewMoreButton.click();
fixture.detectChanges();
await fixture.whenStable();
viewMoreButton = findViewMoreButton();
expect(viewMoreButton.hidden).toBeTrue();
expect(findTagChips()).toHaveSize(4);
expect(dynamicChipListComponent.showDelete).toBeTrue();
});
it('should not render view more button when tag takes more than one line and there are no more tags', async () => {
await renderTags([{
entry: {
tag: 'VeryLongTag VeryLongTag VeryLongTag VeryLongTag VeryLongTag VeryLongTag VeryLongTag VeryLongTag',
id: '0ee933fa-57fc-4587-8a77-b787e814f1d2'
}
}]);
component.ngOnChanges();
it('should have assigned showDelete to false if false is passed to showDelete of tag node list', () => {
component.showDelete = false;
fixture.detectChanges();
await fixture.whenStable();
expect(findViewMoreButton().hidden).toBeTrue();
expect(findTagChips()).toHaveSize(component.tagsEntries.length);
expect(dynamicChipListComponent.showDelete).toBeFalse();
});
it('should render view more button when tag takes more than one line and there are more tags', async () => {
await renderTags([{
entry: {
tag: 'VeryLongTag VeryLongTag VeryLongTag VeryLongTag VeryLongTag VeryLongTag VeryLongTag VeryLongTag',
id: '0ee933fa-57fc-4587-8a77-b787e814f1d2'
}
}, {
entry: {
tag: 'Some other tag',
id: '0ee933fa-57fc-4587-8a77-b787e814f1d3'
}
}]);
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
const viewMoreButton = findViewMoreButton();
expect(viewMoreButton.hidden).toBeFalse();
expect(viewMoreButton.style.left).toBe('0px');
expect(findTagChips()).toHaveSize(component.tagsEntries.length);
it('should have assigned showDelete to true by default if showDelete of tag node list is not delivered', () => {
expect(dynamicChipListComponent.showDelete).toBeTrue();
});
it('should not render view more button when there is enough space after resizing', async () => {
await renderTags();
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
element.style.maxWidth = '800px';
resizeCallback([], null);
fixture.detectChanges();
const viewMoreButton = findViewMoreButton();
expect(viewMoreButton.hidden).toBeTrue();
expect(findTagChips()).toHaveSize(4);
});
describe('Assigning chips', () => {
let tagEntry: Tag;
let tagEntries: TagEntry[];
it('should render view more button when there is not enough space after resizing', async () => {
await renderTags();
element.style.maxWidth = '800px';
beforeEach(() => {
tagEntry = {
id: 'some id',
tag: 'some tag'
};
tagEntries = [{
entry: tagEntry
}];
getTagsByNodeIdSpy.and.returnValue(of({
list: {
entries: tagEntries,
pagination: undefined
}
}));
spyOn(component.results, 'emit');
});
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
element.style.maxWidth = '309px';
resizeCallback([], null);
fixture.detectChanges();
it('should have assigned correct chips initially', () => {
component.nodeId = 'some node id';
tagService.refresh.emit();
expect(findViewMoreButton().hidden).toBeFalse();
expect(findTagChips()).toHaveSize(component.tagsEntries.length);
});
fixture.detectChanges();
expect(dynamicChipListComponent.chips).toEqual([{
id: tagEntry.id,
name: tagEntry.tag
}]);
expect(tagService.getTagsByNodeId).toHaveBeenCalledWith(component.nodeId);
expect(component.results.emit).toHaveBeenCalledWith(tagEntries);
});
it('should not render view more button again resizing when there is not enough space if user requested to see all tags', async () => {
await renderTags();
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
const viewMoreButton = findViewMoreButton();
viewMoreButton.click();
fixture.detectChanges();
element.style.maxWidth = '309px';
resizeCallback([], null);
fixture.detectChanges();
expect(viewMoreButton.hidden).toBeTrue();
expect(findTagChips()).toHaveSize(4);
it('should not have assigned chips initially if nodeId is not specified', () => {
tagService.refresh.emit();
fixture.detectChanges();
expect(dynamicChipListComponent.chips).toEqual([]);
expect(tagService.getTagsByNodeId).not.toHaveBeenCalled();
expect(component.results.emit).not.toHaveBeenCalled();
});
it('should have assigned correct chips when ngOnChanges is called', () => {
component.nodeId = 'some node id';
component.ngOnChanges();
fixture.detectChanges();
expect(dynamicChipListComponent.chips).toEqual([{
id: tagEntry.id,
name: tagEntry.tag
}]);
expect(tagService.getTagsByNodeId).toHaveBeenCalledWith(component.nodeId);
expect(component.results.emit).toHaveBeenCalledWith(tagEntries);
});
it('should not have assigned chips when ngOnChanges is called and nodeId is not specified', () => {
component.ngOnChanges();
fixture.detectChanges();
expect(dynamicChipListComponent.chips).toEqual([]);
expect(tagService.getTagsByNodeId).not.toHaveBeenCalled();
expect(component.results.emit).not.toHaveBeenCalled();
});
it('should have assigned correct chips when displayNext event from DynamicChipList is triggered', () => {
component.nodeId = 'some node id';
dynamicChipListComponent.displayNext.emit();
fixture.detectChanges();
expect(dynamicChipListComponent.chips).toEqual([{
id: tagEntry.id,
name: tagEntry.tag
}]);
expect(tagService.getTagsByNodeId).toHaveBeenCalledWith(component.nodeId);
expect(component.results.emit).toHaveBeenCalledWith(tagEntries);
});
it('should not have assigned chips when displayNext event from DynamicChipList is triggered and nodeId is not specified', () => {
dynamicChipListComponent.displayNext.emit();
fixture.detectChanges();
expect(dynamicChipListComponent.chips).toEqual([]);
expect(tagService.getTagsByNodeId).not.toHaveBeenCalled();
expect(component.results.emit).not.toHaveBeenCalled();
});
it('should have assigned correct chips when removeTag event from DynamicChipList is triggered', () => {
component.nodeId = 'some node id';
spyOn(tagService, 'removeTag').and.returnValue(of(undefined));
const tag = 'some tag';
dynamicChipListComponent.removedChip.emit(tag);
fixture.detectChanges();
expect(dynamicChipListComponent.chips).toEqual([{
id: tagEntry.id,
name: tagEntry.tag
}]);
expect(tagService.removeTag).toHaveBeenCalledWith(component.nodeId, tag);
expect(tagService.getTagsByNodeId).toHaveBeenCalledWith(component.nodeId);
expect(component.results.emit).toHaveBeenCalledWith(tagEntries);
});
it('should not have assigned chips when removeTag event from DynamicChipList is triggered and nodeId is not specified', () => {
spyOn(tagService, 'removeTag').and.returnValue(of(undefined));
const tag = 'some tag';
dynamicChipListComponent.removedChip.emit(tag);
fixture.detectChanges();
expect(dynamicChipListComponent.chips).toEqual([]);
expect(tagService.removeTag).toHaveBeenCalledWith(component.nodeId, tag);
expect(tagService.getTagsByNodeId).not.toHaveBeenCalled();
expect(component.results.emit).not.toHaveBeenCalled();
});
});
});
});

View File

@ -15,27 +15,12 @@
* limitations under the License.
*/
import {
Component,
EventEmitter,
Input,
OnChanges,
Output,
ViewEncapsulation,
OnDestroy,
OnInit,
ViewChild,
ElementRef,
ViewChildren,
QueryList,
ChangeDetectorRef,
AfterViewInit
} from '@angular/core';
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { TagService } from './services/tag.service';
import { TagEntry } from '@alfresco/js-api';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { MatChip } from '@angular/material/chips';
import { Chip } from '@alfresco/adf-core';
/**
*
@ -45,11 +30,9 @@ import { MatChip } from '@angular/material/chips';
@Component({
selector: 'adf-tag-node-list',
templateUrl: './tag-node-list.component.html',
styleUrls: ['./tag-node-list.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class TagNodeListComponent implements OnChanges, OnDestroy, OnInit, AfterViewInit {
/* eslint no-underscore-dangle: ["error", { "allow": ["_elementRef"] }]*/
export class TagNodeListComponent implements OnChanges, OnDestroy, OnInit {
/** The identifier of a node. */
@Input()
nodeId: string;
@ -62,121 +45,49 @@ export class TagNodeListComponent implements OnChanges, OnDestroy, OnInit, After
@Input()
limitTagsDisplayed = false;
@ViewChild('nodeListContainer')
containerView: ElementRef;
@ViewChildren(MatChip)
tagChips: QueryList<MatChip>;
tagsEntries: TagEntry[] = [];
calculationsDone = false;
columnFlexDirection = false;
undisplayedTagsCount = 0;
viewMoreButtonLeftOffset: number;
/** Emitted when a tag is selected. */
@Output()
results = new EventEmitter();
results = new EventEmitter<TagEntry[]>();
private onDestroy$ = new Subject<boolean>();
private initialLimitTagsDisplayed: boolean;
private initialTagsEntries: TagEntry[] = [];
private viewMoreButtonLeftOffsetBeforeFlexDirection: number;
private requestedDisplayingAllTags = false;
private resizeObserver = new ResizeObserver(() => {
this.calculateTagsToDisplay();
this.changeDetectorRef.detectChanges();
});
private _tagChips: Chip[] = [];
constructor(private tagService: TagService, private changeDetectorRef: ChangeDetectorRef) {
get tagChips(): Chip[] {
return this._tagChips;
}
ngOnChanges() {
constructor(private tagService: TagService) {}
ngOnChanges(): void {
this.refreshTag();
}
ngOnInit() {
this.initialLimitTagsDisplayed = this.limitTagsDisplayed;
ngOnInit(): void {
this.tagService.refresh
.pipe(takeUntil(this.onDestroy$))
.subscribe(() => this.refreshTag());
this.results
.pipe(takeUntil(this.onDestroy$))
.subscribe(() => {
if (this.limitTagsDisplayed && this.tagsEntries.length > 0) {
this.calculateTagsToDisplay();
}
});
}
ngAfterViewInit() {
this.resizeObserver.observe(this.containerView.nativeElement);
}
ngOnDestroy() {
ngOnDestroy(): void {
this.onDestroy$.next(true);
this.onDestroy$.complete();
this.resizeObserver.unobserve(this.containerView.nativeElement);
}
refreshTag() {
refreshTag(): void {
if (this.nodeId) {
this.tagService.getTagsByNodeId(this.nodeId).subscribe((tagPaging) => {
this.tagsEntries = tagPaging.list.entries;
this.initialTagsEntries = tagPaging.list.entries;
this.results.emit(this.tagsEntries);
this._tagChips = tagPaging.list.entries.map((tag) => ({
id: tag.entry.id,
name: tag.entry.tag
}));
this.results.emit(tagPaging.list.entries);
});
}
}
removeTag(tag: string) {
removeTag(tag: string): void {
this.tagService.removeTag(this.nodeId, tag).subscribe(() => {
this.refreshTag();
});
}
displayAllTags(event: Event): void {
event.preventDefault();
event.stopPropagation();
this.limitTagsDisplayed = false;
this.requestedDisplayingAllTags = true;
this.resizeObserver.unobserve(this.containerView.nativeElement);
this.refreshTag();
}
private calculateTagsToDisplay() {
if (!this.requestedDisplayingAllTags) {
this.tagsEntries = this.initialTagsEntries;
this.changeDetectorRef.detectChanges();
this.undisplayedTagsCount = 0;
let tagsToDisplay = 1;
const containerWidth: number = this.containerView.nativeElement.clientWidth;
const viewMoreBtnWidth: number = this.containerView.nativeElement.children[1].offsetWidth;
const firstTag = this.tagChips.get(0);
const tagChipMargin = firstTag ? this.getTagChipMargin(this.tagChips.get(0)) : 0;
const tagChipsWidth: number = this.tagChips.reduce((width, val, index) => {
width += val._elementRef.nativeElement.offsetWidth + tagChipMargin;
if (containerWidth - viewMoreBtnWidth > width) {
tagsToDisplay = index + 1;
this.viewMoreButtonLeftOffset = width;
this.viewMoreButtonLeftOffsetBeforeFlexDirection = width;
}
return width;
}, 0);
if ((containerWidth - tagChipsWidth) <= 0) {
this.columnFlexDirection = tagsToDisplay === 1 && (containerWidth < (this.tagChips.get(0)._elementRef.nativeElement.offsetWidth + viewMoreBtnWidth));
this.undisplayedTagsCount = this.tagsEntries.length - tagsToDisplay;
this.tagsEntries = this.tagsEntries.slice(0, tagsToDisplay);
}
this.limitTagsDisplayed = this.undisplayedTagsCount ? this.initialLimitTagsDisplayed : false;
this.viewMoreButtonLeftOffset = this.columnFlexDirection ? 0 : this.viewMoreButtonLeftOffsetBeforeFlexDirection;
this.calculationsDone = true;
}
}
private getTagChipMargin(chip: MatChip): number {
const tagChipStyles = window.getComputedStyle(chip._elementRef.nativeElement);
return parseInt(tagChipStyles.marginLeft, 10) + parseInt(tagChipStyles.marginRight, 10);
}
}

View File

@ -67,6 +67,8 @@ import { AdfDateFnsAdapter } from './common/utils/date-fns-adapter';
import { MomentDateAdapter } from './common/utils/moment-date-adapter';
import { AdfDateTimeFnsAdapter } from './common/utils/datetime-fns-adapter';
import { StoragePrefixFactory } from './app-config';
import { UnsavedChangesDialogModule } from './dialogs';
import { DynamicChipListModule } from './dynamic-chip-list';
@NgModule({
imports: [
@ -101,6 +103,8 @@ import { StoragePrefixFactory } from './app-config';
NotificationHistoryModule,
SearchTextModule,
BlankPageModule,
UnsavedChangesDialogModule,
DynamicChipListModule,
HttpClientModule,
HttpClientXsrfModule.withOptions({
cookieName: 'CSRF-TOKEN',
@ -138,7 +142,9 @@ import { StoragePrefixFactory } from './app-config';
IconModule,
NotificationHistoryModule,
SearchTextModule,
BlankPageModule
BlankPageModule,
UnsavedChangesDialogModule,
DynamicChipListModule
]
})
export class CoreModule {

View File

@ -17,3 +17,6 @@
export * from './edit-json/edit-json.dialog';
export * from './edit-json/edit-json.dialog.module';
export * from './unsaved-changes-dialog/unsaved-changes-dialog.component';
export * from './unsaved-changes-dialog/unsaved-changes-dialog.module';
export * from './unsaved-changes-dialog/unsaved-changes.guard';

View File

@ -0,0 +1,29 @@
<h1 mat-dialog-title class="adf-unsaved-changes-dialog-title">
{{ 'CORE.DIALOG.UNSAVED_CHANGES.TITLE' | translate }}
<button
data-automation-id="adf-unsaved-changes-dialog-close-button"
mat-icon-button
[title]="'CLOSE' | translate"
[mat-dialog-close]="false">
<mat-icon>close</mat-icon>
</button>
</h1>
<mat-dialog-content>
{{ 'CORE.DIALOG.UNSAVED_CHANGES.DESCRIPTION' | translate }}
</mat-dialog-content>
<mat-dialog-actions align="end">
<button
data-automation-id="adf-unsaved-changes-dialog-cancel-button"
mat-button
[mat-dialog-close]="false"
class="adf-unsaved-changes-dialog-cancel-button">
{{ 'CANCEL' | translate | titlecase }}
</button>
<button
data-automation-id="adf-unsaved-changes-dialog-discard-changes-button"
mat-button
[mat-dialog-close]="true"
class="adf-unsaved-changes-dialog-discard-changes-button">
{{ 'CORE.DIALOG.UNSAVED_CHANGES.DISCARD_CHANGES_BUTTON' | translate }}
</button>
</mat-dialog-actions>

View File

@ -0,0 +1,32 @@
adf-unsaved-changes-dialog {
margin-top: -4px;
display: block;
.adf-unsaved-changes-dialog {
&-title {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 16px;
font-weight: bold;
}
&-cancel-button {
background-color: var(--adf-secondary-button-background);
margin-right: 4px;
}
&-discard-changes-button {
color: var(--theme-warn-color-default-contrast);
background-color: var(--adf-danger-button-background);
min-width: 143px;
}
&-cancel-button, &-discard-changes-button {
padding: 4px 14px;
height: 32px;
display: flex;
align-items: center;
}
}
}

View File

@ -0,0 +1,68 @@
/*!
* @license
* Copyright © 2005-2023 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 { ComponentFixture, TestBed } from '@angular/core/testing';
import { CoreTestingModule, UnsavedChangesDialogComponent } from '@alfresco/adf-core';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { MatDialogClose } from '@angular/material/dialog';
describe('UnsavedChangesDialog', () => {
let fixture: ComponentFixture<UnsavedChangesDialogComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CoreTestingModule]
});
fixture = TestBed.createComponent(UnsavedChangesDialogComponent);
fixture.detectChanges();
});
describe('Close icon button', () => {
let closeIconButton: DebugElement;
beforeEach(() => {
closeIconButton = fixture.debugElement.query(By.css(
'[data-automation-id="adf-unsaved-changes-dialog-close-button"]'
));
});
it('should have assigned mat-dialog-close with false as result', () => {
expect(closeIconButton.injector.get(MatDialogClose).dialogResult).toBeFalse();
});
it('should have displayed correct icon', () => {
expect(closeIconButton.nativeElement.textContent).toBe('close');
});
});
describe('Cancel button', () => {
it('should have assigned mat-dialog-close with false as result', () => {
expect(fixture.debugElement.query(By.css(
'[data-automation-id="adf-unsaved-changes-dialog-cancel-button"]'
)).injector.get(MatDialogClose).dialogResult).toBeFalse();
});
});
describe('Discard changes button', () => {
it('should have assigned mat-dialog-close with true as result', () => {
expect(fixture.debugElement.query(By.css(
'[data-automation-id="adf-unsaved-changes-dialog-discard-changes-button"]'
)).injector.get(MatDialogClose).dialogResult).toBeTrue();
});
});
});

View File

@ -0,0 +1,29 @@
/*!
* @license
* Copyright © 2005-2023 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 { Component, ViewEncapsulation } from '@angular/core';
/**
* Dialog which informs about unsaved changes. Allows discard them and proceed or close dialog and stop proceeding.
*/
@Component({
selector: 'adf-unsaved-changes-dialog',
encapsulation: ViewEncapsulation.None,
templateUrl: './unsaved-changes-dialog.component.html',
styleUrls: ['./unsaved-changes-dialog.component.scss']
})
export class UnsavedChangesDialogComponent {}

View File

@ -0,0 +1,37 @@
/*!
* @license
* Copyright © 2005-2023 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 { NgModule } from '@angular/core';
import { UnsavedChangesDialogComponent } from './unsaved-changes-dialog.component';
import { MatDialogModule } from '@angular/material/dialog';
import { TranslateModule } from '@ngx-translate/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { CommonModule } from '@angular/common';
@NgModule({
declarations: [UnsavedChangesDialogComponent],
imports: [
MatDialogModule,
TranslateModule,
MatButtonModule,
MatIconModule,
CommonModule
],
exports: [UnsavedChangesDialogComponent]
})
export class UnsavedChangesDialogModule {}

View File

@ -0,0 +1,94 @@
/*!
* @license
* Copyright © 2005-2023 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 { TestBed } from '@angular/core/testing';
import { CoreTestingModule, UnsavedChangesDialogComponent, UnsavedChangesGuard } from '@alfresco/adf-core';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { Observable, Subject } from 'rxjs';
describe('UnsavedChangesGuard', () => {
let guard: UnsavedChangesGuard;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CoreTestingModule]
});
guard = TestBed.inject(UnsavedChangesGuard);
});
describe('canDeactivate', () => {
let dialog: MatDialog;
let afterClosed$: Subject<boolean>;
beforeEach(() => {
afterClosed$ = new Subject<boolean>();
dialog = TestBed.inject(MatDialog);
spyOn(dialog, 'open').and.returnValue({
afterClosed: () => afterClosed$ as Observable<boolean>
} as MatDialogRef<UnsavedChangesDialogComponent>);
});
it('should return true if unsaved is set to false', () => {
guard.unsaved = false;
expect(guard.canDeactivate()).toBeTrue();
});
it('should return true if unsaved was not set', () => {
expect(guard.canDeactivate()).toBeTrue();
});
it('should return true when unsaved is set to true and result of dialog is true', (done) => {
guard.unsaved = true;
(guard.canDeactivate() as Observable<boolean>).subscribe((allowed) => {
expect(allowed).toBeTrue();
done();
});
afterClosed$.next(true);
});
it('should return false when unsaved is set to true and result of dialog is false', (done) => {
guard.unsaved = true;
(guard.canDeactivate() as Observable<boolean>).subscribe((allowed) => {
expect(allowed).toBeFalse();
done();
});
afterClosed$.next(false);
});
it('should keep unsaved set to true when unsaved was to true and result of dialog is false', (done) => {
guard.unsaved = true;
(guard.canDeactivate() as Observable<boolean>).subscribe(() => {
expect(guard.unsaved).toBeTrue();
done();
});
afterClosed$.next(false);
});
it('should set unsaved to false when unsaved is set to true and result of dialog is true', (done) => {
guard.unsaved = true;
(guard.canDeactivate() as Observable<boolean>).subscribe(() => {
expect(guard.unsaved).toBeFalse();
done();
});
afterClosed$.next(true);
});
});
});

View File

@ -0,0 +1,47 @@
/*!
* @license
* Copyright © 2005-2023 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 { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { Observable } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import { UnsavedChangesDialogComponent } from './unsaved-changes-dialog.component';
import { tap } from 'rxjs/operators';
/**
* Guard responsible for protecting leaving page with unsaved changes.
*/
@Injectable({
providedIn: 'root'
})
export class UnsavedChangesGuard implements CanDeactivate<any> {
unsaved = false;
constructor(private dialog: MatDialog) {}
/**
* Allows to deactivate route when there is no unsaved changes, otherwise displays dialog to confirm discarding changes.
*
* @returns boolean | Observable<boolean> true when there is no unsaved changes or changes can be discarded, false otherwise.
*/
canDeactivate(): boolean | Observable<boolean> {
return this.unsaved ?
this.dialog.open<UnsavedChangesDialogComponent, undefined, boolean>(UnsavedChangesDialogComponent, {
maxWidth: 346
}).afterClosed().pipe(tap((confirmed) => this.unsaved = !confirmed)) : true;
}
}

View File

@ -0,0 +1,21 @@
/*!
* @license
* Copyright © 2005-2023 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.
*/
export interface Chip {
name: string;
id: string;
}

View File

@ -0,0 +1,39 @@
<div
class="adf-dynamic-chip-list-container"
[class.adf-dynamic-chip-list-flex-column]="limitChipsDisplayed && (!calculationsDone || columnFlexDirection)"
[class.adf-dynamic-chip-list-button-in-next-line]="moveLoadMoreButtonToNextRow"
[class.adf-dynamic-chip-list-paginated]="paginationData"
#nodeListContainer>
<mat-chip-list
[class.adf-dynamic-chip-list-full-width]="limitChipsDisplayed && !calculationsDone"
role="listbox"
[attr.aria-label]="'METADATA.BASIC.TAGS' | translate">
<mat-chip
class="adf-dynamic-chip-list-chip"
*ngFor="let chip of chipsToDisplay; let idx = index"
(removed)="removedChip.emit(chip.id)">
<span id="adf-dynamic-chip-list-chip-name-{{ idx }}">{{ chip.name }}</span>
<mat-icon
*ngIf="showDelete"
id="adf-dynamic-chip-list-delete-{{ chip.name }}"
class="adf-dynamic-chip-list-delete-icon"
matChipRemove>
cancel
</mat-icon>
</mat-chip>
</mat-chip-list>
<button
data-automation-id="adf-dynamic-chip-list-view-more-button"
mat-button
[hidden]="!limitChipsDisplayed"
[style.left.px]="viewMoreButtonLeftOffset"
[style.top.px]="viewMoreButtonTop"
class="adf-dynamic-chip-list-view-more-button"
[class.adf-dynamic-chip-list-hidden-btn]="!calculationsDone"
(click)="displayNextChips($event)">
{{
paginationData ? ('DYNAMIC_CHIP_LIST.LOAD_MORE' | translate) :
('TAG_NODE_LIST.VIEW_MORE' | translate: { count: undisplayedChipsCount})
}}
</button>
</div>

View File

@ -0,0 +1,82 @@
.adf-dynamic-chip-list-container {
display: flex;
flex-direction: row;
width: inherit;
padding-top: 12px;
padding-bottom: 12px;
.adf-dynamic-chip-list-view-more-button {
color: var(--adf-theme-foreground-text-color-054);
position: absolute;
&[hidden] {
visibility: hidden;
}
}
&.adf-dynamic-chip-list-flex-column {
flex-direction: column;
.adf-dynamic-chip-list-view-more-button {
position: relative;
}
}
&.adf-dynamic-chip-list-paginated {
mat-chip-list {
width: 100%;
& > div {
width: 100%;
}
}
.adf-dynamic-chip-list-view-more-button {
margin: -2px 4px 4px 24px;
}
}
&.adf-dynamic-chip-list-button-in-next-line {
align-items: unset;
padding-bottom: 54px;
.adf-dynamic-chip-list-view-more-button {
margin-top: 4px;
}
}
&:not(.adf-dynamic-chip-list-paginated) {
align-items: center;
&:not(.adf-dynamic-chip-list-flex-column) {
.adf-dynamic-chip-list-view-more-button {
margin-top: 10px;
}
}
}
.adf-dynamic-chip-list-full-width {
width: 100%;
}
.adf-dynamic-chip-list-hidden-btn {
visibility: hidden;
}
.adf-dynamic-chip-list-chip {
color: var(--theme-primary-color-default-contrast);
background-color: var(--theme-primary-color);
height: auto;
word-break: break-word;
}
.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

@ -0,0 +1,297 @@
/*!
* @license
* Copyright © 2005-2023 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 { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { Chip, CoreTestingModule, DynamicChipListComponent } from '@alfresco/adf-core';
import { SimpleChange } from '@angular/core';
describe('DynamicChipListComponent', () => {
let chips: Chip[] = [{
name: 'test1',
id: '0ee933fa-57fc-4587-8a77-b787e814f1d2'
}, {
name: 'test2',
id: 'fcb92659-1f10-41b4-9b17-851b72a3b597'
}, {
name: 'test3',
id: 'fb4213c0-729d-466c-9a6c-ee2e937273bf'
}, {
name: 'test4',
id: 'as4213c0-729d-466c-9a6c-ee2e937273as'
}];
let component: DynamicChipListComponent;
let fixture: ComponentFixture<DynamicChipListComponent>;
let element: HTMLElement;
let resizeCallback: ResizeObserverCallback;
/**
* Find 'More' button
*
* @returns native element
*/
function findViewMoreButton(): HTMLButtonElement {
return element.querySelector('[data-automation-id="adf-dynamic-chip-list-view-more-button"]');
}
/**
* Get the chips
*
* @returns native element list
*/
function findChips(): NodeListOf<Element> {
return element.querySelectorAll('.adf-dynamic-chip-list-chip');
}
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
const resizeObserverSpy = spyOn(window, 'ResizeObserver').and.callThrough();
fixture = TestBed.createComponent(DynamicChipListComponent);
element = fixture.nativeElement;
component = fixture.componentInstance;
component.chips = chips;
fixture.detectChanges();
resizeCallback = resizeObserverSpy.calls.mostRecent().args[0];
});
describe('Rendering tests', () => {
it('should render every chip', async () => {
component.ngOnChanges({
chips: new SimpleChange(undefined, component.chips, true)
});
fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('#adf-dynamic-chip-list-chip-name-0').innerHTML).toBe('test1');
expect(element.querySelector('#adf-dynamic-chip-list-chip-name-1').innerHTML).toBe('test2');
expect(element.querySelector('#adf-dynamic-chip-list-chip-name-2').innerHTML).toBe('test3');
expect(element.querySelector('#adf-dynamic-chip-list-delete-test1')).not.toBe(null);
expect(element.querySelector('#adf-dynamic-chip-list-delete-test2')).not.toBe(null);
expect(element.querySelector('#adf-dynamic-chip-list-delete-test3')).not.toBe(null);
});
it('should emit removedChip event when clicked on delete icon', async () => {
spyOn(component.removedChip, 'emit');
component.ngOnChanges({
chips: new SimpleChange(undefined, component.chips, true)
});
fixture.detectChanges();
await fixture.whenStable();
const deleteButton: any = element.querySelector('#adf-dynamic-chip-list-delete-test1');
deleteButton.click();
expect(component.removedChip.emit).toHaveBeenCalledWith('0ee933fa-57fc-4587-8a77-b787e814f1d2');
});
it('should not show the delete button if showDelete is false', async () => {
component.showDelete = false;
component.ngOnChanges({
chips: new SimpleChange(undefined, component.chips, true)
});
fixture.detectChanges();
await fixture.whenStable();
const deleteButton: any = element.querySelector('#adf-dynamic-chip-list-delete-test1');
expect(deleteButton).toBeNull();
});
it('should show the delete button if showDelete is true', async () => {
component.showDelete = true;
component.ngOnChanges({
chips: new SimpleChange(undefined, component.chips, true)
});
fixture.detectChanges();
await fixture.whenStable();
const deleteButton: any = element.querySelector('#adf-dynamic-chip-list-delete-test1');
expect(deleteButton).not.toBeNull();
});
it('should not render view more button by default', async () => {
component.ngOnChanges({
chips: new SimpleChange(undefined, component.chips, true)
});
fixture.detectChanges();
await fixture.whenStable();
expect(findViewMoreButton().hidden).toBeTrue();
expect(findChips()).toHaveSize(4);
});
});
describe('Limit chips display', () => {
let initialChips: Chip[];
/**
* Render chips
*
* @param chipsToRender chips to render
*/
async function renderChips(chipsToRender?: Chip[]) {
chips = chipsToRender || initialChips;
component.chips = chips;
fixture.detectChanges();
}
beforeAll(() => {
initialChips = chips;
});
beforeEach(() => {
component.limitChipsDisplayed = true;
component.ngOnInit();
element.style.maxWidth = '309px';
});
it('should render view more button when limiting is enabled', fakeAsync(() => {
renderChips();
component.ngOnChanges({
chips: new SimpleChange(undefined, component.chips, true)
});
tick();
fixture.detectChanges();
expect(findViewMoreButton().hidden).toBeFalse();
expect(findChips()).toHaveSize(1);
}));
it('should not render view more button when limiting is enabled and all chips fits into container', fakeAsync(() => {
renderChips();
element.style.maxWidth = '800px';
component.ngOnChanges({
chips: new SimpleChange(undefined, component.chips, true)
});
tick();
fixture.detectChanges();
expect(findViewMoreButton().hidden).toBeTrue();
expect(findChips()).toHaveSize(4);
}));
it('should emit displayNext event when view more button is clicked', fakeAsync(() => {
renderChips();
component.ngOnChanges({
chips: new SimpleChange(undefined, component.chips, true)
});
tick();
fixture.detectChanges();
spyOn(component.displayNext, 'emit');
const viewMoreButton = findViewMoreButton();
viewMoreButton.click();
fixture.detectChanges();
expect(viewMoreButton.hidden).toBeTrue();
expect(findChips()).toHaveSize(1);
expect(component.displayNext.emit).toHaveBeenCalled();
}));
it('should not render view more button when chip takes more than one line and there are no more chips', fakeAsync(() => {
renderChips([{
name: 'VeryLongTag VeryLongTag VeryLongTag VeryLongTag VeryLongTag VeryLongTag VeryLongTag VeryLongTag',
id: '0ee933fa-57fc-4587-8a77-b787e814f1d2'
}]);
component.ngOnChanges({
chips: new SimpleChange(undefined, component.chips, true)
});
tick();
fixture.detectChanges();
expect(findViewMoreButton().hidden).toBeTrue();
expect(findChips()).toHaveSize(component.chips.length);
}));
it('should render view more button when chip takes more than one line and there are more chips', fakeAsync(() => {
renderChips([{
name: 'VeryLongTag VeryLongTag VeryLongTag VeryLongTag VeryLongTag VeryLongTag VeryLongTag VeryLongTag',
id: '0ee933fa-57fc-4587-8a77-b787e814f1d2'
}, {
name: 'Some other tag',
id: '0ee933fa-57fc-4587-8a77-b787e814f1d3'
}]);
component.ngOnChanges({
chips: new SimpleChange(undefined, component.chips, true)
});
tick();
fixture.detectChanges();
const viewMoreButton = findViewMoreButton();
expect(viewMoreButton.hidden).toBeFalse();
expect(viewMoreButton.style.left).toBe('0px');
expect(findChips()).toHaveSize(1);
}));
it('should not render view more button when there is enough space after resizing', fakeAsync(() => {
renderChips();
component.ngOnChanges({
chips: new SimpleChange(undefined, component.chips, true)
});
tick();
fixture.detectChanges();
element.style.maxWidth = '800px';
resizeCallback([], null);
fixture.detectChanges();
const viewMoreButton = findViewMoreButton();
expect(viewMoreButton.hidden).toBeTrue();
expect(findChips()).toHaveSize(4);
}));
it('should render view more button when there is not enough space after resizing', fakeAsync(() => {
renderChips();
element.style.maxWidth = '800px';
component.ngOnChanges({
chips: new SimpleChange(undefined, component.chips, true)
});
tick();
fixture.detectChanges();
element.style.maxWidth = '309px';
resizeCallback([], null);
fixture.detectChanges();
expect(findViewMoreButton().hidden).toBeFalse();
expect(findChips()).toHaveSize(1);
}));
it('should not render view more button again after resizing when there is not enough space if user requested to see all chips', fakeAsync(() => {
renderChips();
component.ngOnChanges({
chips: new SimpleChange(undefined, component.chips, true)
});
tick();
fixture.detectChanges();
const viewMoreButton = findViewMoreButton();
viewMoreButton.click();
fixture.detectChanges();
element.style.maxWidth = '309px';
resizeCallback([], null);
fixture.detectChanges();
expect(viewMoreButton.hidden).toBeTrue();
}));
});
});

View File

@ -0,0 +1,218 @@
/*!
* @license
* Copyright © 2005-2023 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 {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
QueryList,
SimpleChanges,
ViewChild,
ViewChildren,
ViewEncapsulation
} from '@angular/core';
import { Subject } from 'rxjs';
import { MatChip } from '@angular/material/chips';
import { Chip } from './chip';
import { Pagination } from '@alfresco/js-api';
/**
* This component shows dynamic list of chips which render depending on free space.
*/
@Component({
selector: 'adf-dynamic-chip-list',
templateUrl: './dynamic-chip-list.component.html',
styleUrls: ['./dynamic-chip-list.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class DynamicChipListComponent implements OnChanges, OnInit, AfterViewInit, OnDestroy {
/* eslint no-underscore-dangle: ["error", { "allow": ["_elementRef"] }]*/
/** Provide if you want to use paginated chips. */
@Input()
pagination: Pagination;
/** List of chips to display. */
@Input()
chips: Chip[];
/** Show delete button. */
@Input()
showDelete = true;
/** Should limit number of chips displayed. */
@Input()
limitChipsDisplayed = false;
/** Emitted when button for view more is clicked. */
@Output()
displayNext = new EventEmitter<void>();
/** Emitted when any chip is removed. */
@Output()
removedChip = new EventEmitter<string>();
@ViewChild('nodeListContainer')
containerView: ElementRef;
@ViewChildren(MatChip)
matChips: QueryList<MatChip>;
chipsToDisplay: Chip[] = [];
calculationsDone = false;
columnFlexDirection = false;
moveLoadMoreButtonToNextRow = false;
undisplayedChipsCount = 0;
viewMoreButtonLeftOffset: number;
viewMoreButtonTop = 0;
paginationData: Pagination;
private initialChips: Chip[] = [];
private onDestroy$ = new Subject<boolean>();
private initialLimitChipsDisplayed: boolean;
private viewMoreButtonLeftOffsetBeforeFlexDirection: number;
private requestedDisplayingAllChips = false;
private resizeObserver = new ResizeObserver(() => {
this.calculateChipsToDisplay();
this.changeDetectorRef.detectChanges();
});
constructor(private changeDetectorRef: ChangeDetectorRef) {}
ngOnChanges(changes: SimpleChanges): void {
if (changes.pagination) {
this.limitChipsDisplayed = this.pagination?.hasMoreItems;
this.paginationData = this.pagination;
this.initialLimitChipsDisplayed = this.limitChipsDisplayed;
}
if (changes.chips) {
this.initialChips = this.chips;
this.chipsToDisplay = this.initialChips;
if (this.limitChipsDisplayed && this.chipsToDisplay.length) {
setTimeout(() => {
this.calculateChipsToDisplay();
this.changeDetectorRef.detectChanges();
});
}
}
}
ngOnInit(): void {
if (this.paginationData) {
this.limitChipsDisplayed = this.paginationData.hasMoreItems;
}
this.initialLimitChipsDisplayed = this.limitChipsDisplayed;
}
ngAfterViewInit(): void {
this.resizeObserver.observe(this.containerView.nativeElement);
}
ngOnDestroy(): void {
this.onDestroy$.next(true);
this.onDestroy$.complete();
this.resizeObserver.unobserve(this.containerView.nativeElement);
}
displayNextChips(event: Event): void {
event.preventDefault();
event.stopPropagation();
if (this.paginationData) {
this.requestedDisplayingAllChips = !this.paginationData.hasMoreItems;
} else {
this.limitChipsDisplayed = false;
this.requestedDisplayingAllChips = true;
}
if (this.requestedDisplayingAllChips) {
this.resizeObserver.unobserve(this.containerView.nativeElement);
}
this.displayNext.emit();
}
private calculateChipsToDisplay(): void {
if (this.requestedDisplayingAllChips || !this.chips.length) {
return;
}
this.chipsToDisplay = this.initialChips;
this.changeDetectorRef.detectChanges();
this.undisplayedChipsCount = 0;
let chipsToDisplay = 1;
const containerWidth: number = this.containerView.nativeElement.clientWidth;
const viewMoreButton: HTMLButtonElement = this.containerView.nativeElement.children[1];
const viewMoreBtnWidth: number = viewMoreButton.getBoundingClientRect().width;
const firstChip = this.matChips.get(0);
const chipMargin = firstChip ? this.getChipMargin(firstChip) : 0;
let chipsWidth = 0;
const chips = this.matChips.toArray();
let lastIndex = 0;
do {
chipsWidth = Math.max(chips.reduce((width, val, index) => {
width += val._elementRef.nativeElement.getBoundingClientRect().width + chipMargin;
const availableSpace = index && index === chips.length - 1 || !this.paginationData ? containerWidth - viewMoreBtnWidth : containerWidth;
if (availableSpace >= width) {
chipsToDisplay = (this.paginationData ? chipsToDisplay : index) + 1;
lastIndex++;
this.viewMoreButtonLeftOffset = width;
this.viewMoreButtonLeftOffsetBeforeFlexDirection = width;
}
return width;
}, 0), chipsWidth);
chips.splice(0, lastIndex);
lastIndex = 0;
} while ((chips.length || chipsToDisplay < this.matChips.length && this.matChips.length) && this.paginationData);
this.arrangeElements(containerWidth, chipsWidth, viewMoreBtnWidth, chipsToDisplay, viewMoreButton);
this.calculationsDone = true;
}
private getChipMargin(chip: MatChip): number {
const chipStyles = window.getComputedStyle(chip._elementRef.nativeElement);
return parseInt(chipStyles.marginLeft, 10) + parseInt(chipStyles.marginRight, 10);
}
private arrangeElements(containerWidth: number, chipsWidth: number, viewMoreBtnWidth: number, chipsToDisplay: number,
viewMoreButton: HTMLButtonElement): void {
if ((containerWidth - chipsWidth - viewMoreBtnWidth) <= 0) {
const chip = this.paginationData ? this.matChips.last : this.matChips.first;
const hasNotEnoughSpaceForMoreButton = (containerWidth < (chip?._elementRef.nativeElement.offsetWidth + chip?._elementRef.nativeElement.offsetLeft + viewMoreBtnWidth));
this.columnFlexDirection = chipsToDisplay === 1 && !this.paginationData && hasNotEnoughSpaceForMoreButton;
this.moveLoadMoreButtonToNextRow = this.paginationData && hasNotEnoughSpaceForMoreButton;
this.undisplayedChipsCount = this.chipsToDisplay.length - chipsToDisplay;
this.chipsToDisplay = this.chipsToDisplay.slice(0, chipsToDisplay);
} else {
this.moveLoadMoreButtonToNextRow = false;
}
this.limitChipsDisplayed = this.undisplayedChipsCount ? this.initialLimitChipsDisplayed : this.paginationData?.hasMoreItems;
if (this.paginationData?.hasMoreItems) {
const lastChipTop = this.matChips.last._elementRef.nativeElement.offsetTop;
if (this.moveLoadMoreButtonToNextRow) {
this.viewMoreButtonLeftOffset = 0;
this.viewMoreButtonTop = lastChipTop + viewMoreButton.offsetHeight;
} else {
this.viewMoreButtonLeftOffset = this.viewMoreButtonLeftOffsetBeforeFlexDirection;
this.viewMoreButtonTop = lastChipTop;
}
} else {
this.viewMoreButtonLeftOffset = this.columnFlexDirection ? 0 : this.viewMoreButtonLeftOffsetBeforeFlexDirection;
}
}
}

View File

@ -0,0 +1,37 @@
/*!
* @license
* Copyright © 2005-2023 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 { NgModule } from '@angular/core';
import { DynamicChipListComponent } from './dynamic-chip-list.component';
import { MatChipsModule } from '@angular/material/chips';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { TranslateModule } from '@ngx-translate/core';
import { CommonModule } from '@angular/common';
@NgModule({
declarations: [DynamicChipListComponent],
imports: [
MatChipsModule,
MatIconModule,
MatButtonModule,
TranslateModule,
CommonModule
],
exports: [DynamicChipListComponent]
})
export class DynamicChipListModule {}

View File

@ -0,0 +1,18 @@
/*!
* @license
* Copyright © 2005-2023 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.
*/
export * from './public-api';

View File

@ -0,0 +1,20 @@
/*!
* @license
* Copyright © 2005-2023 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.
*/
export * from './dynamic-chip-list.component';
export * from './dynamic-chip-list.module';
export * from './chip';

View File

@ -106,6 +106,11 @@
"EDIT_JSON": {
"CLOSE": "Close",
"UPDATE": "Update"
},
"UNSAVED_CHANGES": {
"TITLE": "Unsaved changes",
"DESCRIPTION": "You have unsaved changes on this screen. Are you sure want to leave without saving changes?",
"DISCARD_CHANGES_BUTTON": "Discard Changes"
}
},
"BREADCRUMBS": {
@ -573,5 +578,8 @@
},
"INFO_DRAWER": {
"ICON": "Node Icon"
},
"DYNAMIC_CHIP_LIST": {
"LOAD_MORE": "Load more"
}
}

View File

@ -96,6 +96,8 @@
--adf-header-icon-button-hover-color: $adf-ref-header-icon-color,
--adf-header-icon-button-pressed-color: $adf-ref-header-icon-color,
--adf-header-icon-button-disabled-color: $adf-ref-header-icon-color,
--adf-danger-button-background: $adf-danger-button-background,
--adf-secondary-button-background: $adf-secondary-button-background
);
// propagates SCSS variables into the CSS variables scope

View File

@ -25,3 +25,5 @@ $adf-ref-metadata-property-panel-label-color: rgba(33, 33, 33, 0.24);
$adf-ref-metadata-property-panel-title-color: rgb(33, 33, 33);
$adf-ref-header-icon-color: inherit;
$adf-ref-header-icon-border-radius: 50%;
$adf-danger-button-background: #ba1b1b;
$adf-secondary-button-background: #2121210d;

View File

@ -37,6 +37,7 @@ export * from './lib/templates/index';
export * from './lib/pipes/index';
export * from './lib/services/index';
export * from './lib/directives/index';
export * from './lib/dynamic-chip-list/index';
export * from './lib/clipboard/index';
export * from './lib/dialogs/index';
export * from './lib/icon/index';

View File

@ -5,6 +5,7 @@ Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**id** | **string** | | [default to null]
**displayName** | **string** | | [default to null]
**description** | **string** | | [default to null]
**isRoot** | **boolean** | | [default to null]
**parentIds** | **string[]** | | [optional] [default to null]
**zones** | **string[]** | | [optional] [default to null]

View File

@ -483,6 +483,7 @@ groupsApi.updateGroup(`<groupId>`, groupBodyUpdate, opts).then((data) => {
| Name | Type |
|-------------|--------|
| displayName | string |
| description | string |
## GroupPaging

View File

@ -18,6 +18,7 @@
export interface Group {
id?: string;
displayName?: string;
description?: string;
isRoot?: boolean;
parentIds?: string[];
zones?: string[];

View File

@ -17,4 +17,5 @@
export interface GroupBodyUpdate {
displayName: string;
description?: string;
}