[ACS-5645] Property Panel Feature (#8995)

* [ACS-5645]Added edit functionality for each panel and updated test cases

* metadata e2e fix

* [ACS-5725]fixed failing e2es

* added unit test cases for new functionality

* minor fixes

* minor fixes

* minor fixes

* [ACS-5645]code modification

* [ACS-5645]removed unwanted code

* [ACS-5645]modified the changes

* [ACS-5645]removed unwanted space

* [ACS-5645]removed unwanted code

* [ACS-5645]Implemented changes as per the review comments

* linting fixes

* [ACS-5645]minor fixes

* [ACS-5645] removed unwanted code

* [ACS-5645]modified the change

* [ACS-5645]aligned input

* [ACS-5645]modified changes

* [ACS-5645]Implemented the changes as per the review comments

* [ACS-5645]linting fixes

* [ACS-5645]fixed sonarcloud issue

* [ACS-5645]fixed errors

* [ACS-5645]rename the function

* [ACS-5645]fixes linting

* [ACS-5540]lint fixes

* [ACS-5645]Implemented the changes as per review comments

* [ACS-5645] Removed unused code

* [ACS-5645]linting fixes

* [ACS-5645]fixes for lint

* [ACS-5645] e2e fixes

* [ACS-5645]Added translation

* [ACS-5645]fixes for e2e

* [ACS-5645]fixes for e2e

* [ACS-5645]e2e fixes

* [ACS-5645] Renamed the theme

* [ACS-5645]modified changes

* [ACS-5645] fixed lock-file bug

* [ACS-5645] added tooltips for save and cancel icons

* [ACS-5645] Modified the changes

* [ACS-5645]Modified the changes

* [ACS-5645] Implemented the changes as per the review comments

* [ACS-5645] Implemented the changes as per the review comments

* [ACS-5645]Modified the changes

* [ACS-5645] added group panel lock changes

* [ACS-5645] Resolved sonarcloud issue

* [ACS-5645] added test cases for tags component

* [ACS-5645] updated the documentation

* [ACS-5645] updated the documentation

* [ACS-5645] updated the documentation

* [ACS-5645] Implemented changes as per review comments

* [ACS-5645] lint fixes

* [ACS-5645] Implemented the review comments

* [ACS-5645] added focus

* [ACS-5645] modified the changes

* [ACS-5645] Lint fixes

* [ACS-5645] Lint fixes

* [ACS-5645] Lint fixes

* [ACS-5645] Removed unwanted code

* [ACS-5645] fixed sonarcloud issue

* [ACS-5645] Added missing translation key

* [ACS-5645] renamed the methods

* [ACS-5645]Added edit functionality for each panel and updated test cases

* [ACS-5645]code modification

* [ACS-5645]removed unwanted code

* [ACS-5645]Implemented changes as per the review comments

* [ACS-5645]Implemented the changes as per review comments

* [ACS-5645]linting fixes

* [ACS-5645] fixed lock-file bug

* [ACS-5645] Modified the changes

* [ACS-5645] added group panel lock changes

* [ACS-5645]Added edit functionality for each panel and updated test cases

* minor fixes

* [ACS-5645] Modified the changes

* [ACS-5645] added group panel lock changes

* [ACS-5645]Added edit functionality for each panel and updated test cases

* metadata e2e fix

* [ACS-5725]fixed failing e2es

* minor fixes

* [ACS-5645]removed unwanted code

* [ACS-5645]Implemented changes as per the review comments

* [ACS-5551] property panel design

* [ACS-5551] minor changes

* [ACS-5551]minor change

* [ACS-5551] updated checks for non -editable field

* [ACS-5551] modified the changes

* [ACS-5551] modified changes

* [ACS-5551] content-metadata updated

* [ACS-5551] code updated

* [ACS-5551] remove extra space

* fixed scrollbar issue

* [ACS-5551] margin adjusted

* Fixed  ACS-6110

* [ACS-5551] design updated

* [ACCS-5551] unit test added

* [ACS-5551] margin issue fixed

* scroll issue fixed

* [ACS-5551] color updated

* [ACS-5551] design modify

* [ACS-5551] add missing methods

* [ACS-5654] translation added

* [ACS-5645] style updated

* [ACS-5654] hide toggle button for aspects

* [ACS-5645] theme updated

* [ACS-5645] tags and category tyle update

* [ACS-5645] unit test update

* [ACS-5645] code updated as per comments

* [ACS-5645] linting issue fix

* [ACS-5645] fixed the failed unit test cases

* [ACS-5645] e2e fixes

* [ACS-5645] e2e modify

* [ACS-5645] aspect issue resolved

* [ACS-5645] Address the comments

* [ACS-5645] Address the comments

* [ACS-5645] tags list design modify

* [ACS-5645] design modify for chips

* [ACS-5645] Removed unused property

* [ACS-5645] Stop reload on panel cancel changes

* [ACS-5645] Linting issue fixed

* revert file change

* [ACS-5645] update aspect issue fix

* Revert "[ACS-5645] update aspect issue fix"

This reverts commit 5212112f2293ad4c29afdd7c7faaf897cd3d00f6.

* reduce layout duplicates, header panel component

* code improvements

* remove useless logging

* cleanup css, remove mat-divider, fix tests

* remove useless styles

* cleanup e2e

* cleanup useless events

* rename nodeIcon to just icon

* disable transition animation for tabs

* remove "editable" hacks

* improved naming for state properties

* bug fixes for process cloud

* css stylelint fixes

* rework component, cleanup useless code

* fix allowable operations and readonly state

* wait for button

* cleanup css, disable e2e

* remove demo-shell only content, fix metadata

* restore reset date functionality

* fix incorrect styling

* fix clear date button styles

* cleanup text item styles

* remove useless classes

* text item rework, code cleanup

* style bug fixes

* cleanup useless tests

* fix styles and tests

* bug fixes for select item styles, revert PR changes

* rework categories styles

* rework tags creator styles

* rollback divider module

* fix css variable naming

* fix issue with hidden properties

* fix key value pairs layout and styles

* fix tag creator validation

* remove incorrect styles, raise proper errors

* fix unit tests

* fix theme vars naming

* remove css hacks for date items

* fix error borders

* fix css bugs

* reduce code

* cleanup e2e and en.json

* fix css linting

* cleanup unused template refs

* remove useless div for metadata container

* cleanup expanders api

* cleanup and remove useless tests

* cleanup i18n

* cleanup tests

* cleanup css

* cleanup css

* [ACS-5654] added the missing theme variables

* review comments resolved

* fixed  css issue

* [ACS-5654] removesd extra div

* [ACS-5654] save and cancel button bug fix

* [ACS-5654] unit test fix for expand the panel

* [ACS-5645] design issues fix

* [ACS-5654] cards design fixed

* [ACS-5654] node icon added to thumbnail service

* [ACS-5645] linting issue fixed

* [ACS-5645] thumbnail unit test updated

* [ACS-5645] linting updated

* [ACS-5645] removed extra div

* [ACS-5645] important removed

* [ACS-5645] tags text issue fix

* [ACS-5645] add missed class

* [ACS-5645] removed unused classes

* [ACS-5645]  removed unused code

* revert flags to original state

* fix missing semicolon

* fix linting issues

* reduce code duplication

* code cleanup

* [ACS-5645] unit test fix

* [ACS-5645] e2e fix for edit button

* fix linting issue for e2e

* Replaced getNodeIcon from thumbnail to content service

* fix indentation

* refactor css variable

* use rgba color value

---------

Co-authored-by: Yasa-Nataliya <yasa.nataliya@globallogic.com>
Co-authored-by: pkundu <priyanka.kundu@hyland.com>
Co-authored-by: rbahirsheth <raviraj.bahirsheth@globallogic.com>
Co-authored-by: Denys Vuika <denys.vuika@gmail.com>
This commit is contained in:
Anukriti Singh 2023-12-21 16:37:13 +05:30 committed by GitHub
parent a7d18cbfe5
commit a900dd2551
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 1814 additions and 1652 deletions

View File

@ -29,20 +29,17 @@
</adf-info-drawer-tab>
<adf-info-drawer-tab label="Properties">
<adf-content-metadata-card *ngIf="isPreset" [node]="node"
[multi]="multi"
[preset]="customPreset"
[readOnly]="isReadOnly"
[displayAspect]="showAspect"
[displayDefaultProperties]="displayDefaultProperties"
[displayEmpty]="displayEmptyMetadata"></adf-content-metadata-card>
<adf-content-metadata-card *ngIf="!isPreset" [node]="node"
[multi]="multi"
[readOnly]="isReadOnly"
[displayAspect]="showAspect"
[displayDefaultProperties]="displayDefaultProperties"
[displayEmpty]="displayEmptyMetadata"></adf-content-metadata-card>
<adf-content-metadata
[node]="node"
[multi]="multi"
[preset]="isPreset ? customPreset : null"
[readOnly]="isReadOnly"
[displayAspect]="showAspect"
[displayDefaultProperties]="displayDefaultProperties"
[displayTags]="false"
[displayCategories]="false"
[displayEmpty]="displayEmptyMetadata">
</adf-content-metadata>
<p class="toggle">
<mat-slide-toggle

View File

@ -241,10 +241,12 @@
<adf-info-drawer [title]="'Details'" *ngIf="documentList.selection[0]">
<adf-info-drawer-tab label="Properties">
<adf-content-metadata-card
<adf-content-metadata
[node]="documentList.selection[0].entry"
[displayEmpty]="displayEmptyMetadata">
</adf-content-metadata-card>
[displayEmpty]="displayEmptyMetadata"
[displayTags]="true"
[displayCategories]="true">
</adf-content-metadata>
</adf-info-drawer-tab>
<adf-info-drawer-tab label="Versions">
<ng-container *ngIf="hasOneFileSelected();else choose_document_template">

View File

@ -56,12 +56,6 @@ Displays and edits metadata related to a node.
| editable | `boolean` | | (optional) This flag toggles editable of content. |
| customPanels | [`ContentMetadataCustomPanel`](../interfaces/content-metadata-custom-panel.interface.md)`[]` | | (optional) List of custom metadata panels to be displayed as [`Dynamic components`](../../extensions/components/dynamic.component.md). |
### Events
| Name | Type | Description |
|----------------|-----------------------------------------------------------------------|---------------------------------------------------|
| editableChange | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<boolean>` | Emitted when content's editable state is changed. |
## Details
The component shows metadata related to a given node. It uses the
@ -444,3 +438,4 @@ When the list of values is too long, the options selection panel will be enhance
## Custom metadata panels
If there is a need to display some custom node properties that require additional UI or data to be fetched you can define custom metadata panels for the metadata component. After creating custom component to be displayed it has to be registered in [Extension Service](../../extensions/services/extension.service.md). Both `panelTitle` and `component` properties have to be defined.
![Chips for multi value properties](../../docassets/images/adf-search-select.png)

View File

@ -13,7 +13,7 @@ Allows to create multiple tags. That component contains input and two lists. Top
```html
<adf-tags-creator
[(tagNameControlVisible)]="tagNameControlVisible"
[tagNameControlVisible]="tagNameControlVisible"
(tagsChange)="tags = $event"
[mode]="tagsCreatorMode">
</adf-tags-creator>
@ -35,5 +35,4 @@ Allows to create multiple tags. That component contains input and two lists. Top
| Name | Type | Description |
| ---- | ---- | ----------- |
| existingTagsPanelVisibilityChange | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<boolean>` | Emitted when bottom list is showing or hiding. |
| tagNameControlVisibleChange | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<boolean>` | Emitted when input is showing or hiding. |
| tagsChange | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<string[]>` | Emitted when tags in top list are changed. |

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File

@ -98,12 +98,18 @@ Now that the `my-preset` configuration is defined, let's use it in a view of the
</adf-content-metadata-card>
```
### Properties
| Name | Type | Default value | Description |
| ---- | ---- | ------------- | ----------- |
| readOnly | `boolean` | false | (optional) This flag sets the metadata in read only mode preventing changes. |
### Viewing the result
After saving the html file, open the ADF app in a browser and dive into the `Personal Files > Sites > swsdp` folder of the Alfresco's repository. Once there, select the `documentLibrary` folder (one click only) and click on the view details icon (the `i` on the top right). Scrolling down the metadata tab on the right, click on the `More information` item at the bottom. Once clicked, you will see two different groups: `Properties` (already there by default) and `This is my preset`. Click on `This is my preset` to show the properties related.
After saving the html file, open the ADF app in a browser and dive into the `Personal Files > Sites > swsdp` folder of the Alfresco's repository. Once there, select the `documentLibrary` folder (one click only) and click on the view details icon (the `menu_open icon` on the top right). Scrolling down the metadata tab on the right. You will see different panels: `Properties` (already there by default), `Tags`, `Categories` and `grouped properties`.
In the following screenshot you can see how the result should look:
![content_metadata_preset](../docassets/images/content_metadata_preset.png)
![UI_Property_Panel](../docassets/images/UI_property_panel.png)
To check it out, double click on the `documentLibrary` folder and select (with one click) the `Presentations` folder. You should see the `This is my preset` group disappear from the metadata panel, because the node doesn't have the `st:siteContainer` aspect.

View File

@ -426,9 +426,9 @@ describe('Content Services Viewer', () => {
await viewerPage.checkInfoSideBarIsDisplayed();
await viewerPage.clickOnTab('Properties');
await viewerPage.checkTabIsActive('Properties');
await metadataViewPage.editIconClick();
await metadataViewPage.clickEditIconGeneral();
await metadataViewPage.enterPropertyText('properties.cm:name', newName);
await metadataViewPage.clickSaveMetadata();
await metadataViewPage.clickSaveGeneralMetadata();
await viewerPage.clickCloseButton();
}
});

View File

@ -114,16 +114,16 @@ describe('content type', () => {
await viewerPage.clickInfoButton();
await viewerPage.checkInfoSideBarIsDisplayed();
await metadataViewPage.clickOnPropertiesTab();
await metadataViewPage.editIconIsDisplayed();
await metadataViewPage.isEditGeneralIconDisplayed();
await expect(await viewerPage.getActiveTab()).toEqual('PROPERTIES');
await expect(await viewerPage.getActiveTab()).toEqual('Properties');
const defaultType = (await metadataViewPage.hasContentType('Content')) || (await metadataViewPage.hasContentType('cm:content'));
await expect(defaultType).toBe(true, 'Content type not found');
await metadataViewPage.editIconClick();
await metadataViewPage.clickEditIconGeneral();
await expect(await metadataViewPage.changeContentType(type.title)).toBe(true, 'Failed to update node type.');
await metadataViewPage.clickSaveMetadata();
await metadataViewPage.clickSaveGeneralMetadata();
await metadataViewPage.checkConfirmDialogDisplayed();
await metadataViewPage.applyNodeProperties();
@ -139,9 +139,9 @@ describe('content type', () => {
await viewerPage.clickInfoButton();
await viewerPage.checkInfoSideBarIsDisplayed();
await metadataViewPage.clickOnPropertiesTab();
await metadataViewPage.editIconIsDisplayed();
await metadataViewPage.isEditGeneralIconDisplayed();
await expect(await viewerPage.getActiveTab()).toEqual('PROPERTIES');
await expect(await viewerPage.getActiveTab()).toEqual('Properties');
const customType = (await metadataViewPage.hasContentType(type.title)) || (await metadataViewPage.hasContentType(`${model.namespacePrefix}:${type.name}`));
await expect(customType).toBe(true, 'Custom type not found');
await expect(await metadataViewPage.getPropertyText(`properties.${model.namespacePrefix}:${property.name}`)).toContain(property.defaultValue);
@ -155,16 +155,16 @@ describe('content type', () => {
await viewerPage.clickInfoButton();
await viewerPage.checkInfoSideBarIsDisplayed();
await metadataViewPage.clickOnPropertiesTab();
await metadataViewPage.editIconIsDisplayed();
await metadataViewPage.isEditGeneralIconDisplayed();
await expect(await viewerPage.getActiveTab()).toEqual('PROPERTIES');
await expect(await viewerPage.getActiveTab()).toEqual('Properties');
let defaultType = (await metadataViewPage.hasContentType('Content')) || (await metadataViewPage.hasContentType('cm:content'));
await expect(defaultType).toBe(true, 'Content type not found');
await metadataViewPage.editIconClick();
await metadataViewPage.clickEditIconGeneral();
await expect(await metadataViewPage.changeContentType(type.title)).toBe(true, 'Failed to update node type.');
await metadataViewPage.clickSaveMetadata();
await metadataViewPage.clickSaveGeneralMetadata();
await metadataViewPage.checkConfirmDialogDisplayed();
await metadataViewPage.cancelNodeProperties();
@ -178,9 +178,9 @@ describe('content type', () => {
await viewerPage.clickInfoButton();
await viewerPage.checkInfoSideBarIsDisplayed();
await metadataViewPage.clickOnPropertiesTab();
await metadataViewPage.editIconIsDisplayed();
await metadataViewPage.isEditGeneralIconDisplayed();
await expect(await viewerPage.getActiveTab()).toEqual('PROPERTIES');
await expect(await viewerPage.getActiveTab()).toEqual('Properties');
defaultType = (await metadataViewPage.hasContentType('Content')) || (await metadataViewPage.hasContentType('cm:content'));
await expect(defaultType).toBe(true, 'Content type not found');
await viewerPage.clickCloseButton();

View File

@ -15,14 +15,7 @@
* limitations under the License.
*/
import { createApiService,
LoginPage,
StringUtil,
UploadActions,
UserModel,
UsersActions,
ViewerPage
} from '@alfresco/adf-testing';
import { createApiService, LoginPage, StringUtil, UploadActions, UserModel, UsersActions, ViewerPage } from '@alfresco/adf-testing';
import { MetadataViewPage } from '../../core/pages/metadata-view.page';
import { NavigationBarPage } from '../../core/pages/navigation-bar.page';
import { FileModel } from '../../models/ACS/file.model';
@ -31,20 +24,6 @@ import CONSTANTS = require('../../util/constants');
import { SitesApi } from '@alfresco/js-api';
describe('permissions', () => {
const METADATA = {
DATA_FORMAT: 'mmm dd yyyy',
TITLE: 'Details',
COMMENTS_TAB: 'COMMENTS',
PROPERTY_TAB: 'PROPERTIES',
DEFAULT_ASPECT: 'Properties',
MORE_INFO_BUTTON: 'More information',
LESS_INFO_BUTTON: 'Less information',
ARROW_DOWN: 'keyboard_arrow_down',
ARROW_UP: 'keyboard_arrow_up',
EDIT_BUTTON_TOOLTIP: 'Edit'
};
const loginPage = new LoginPage();
const viewerPage = new ViewerPage();
const metadataViewPage = new MetadataViewPage();
@ -130,9 +109,7 @@ describe('permissions', () => {
await metadataViewPage.clickOnPropertiesTab();
await metadataViewPage.editIconIsDisplayed();
await expect(await viewerPage.getActiveTab()).toEqual(METADATA.PROPERTY_TAB);
await metadataViewPage.clickOnInformationButton();
await expect(await viewerPage.getActiveTab()).toEqual('Properties');
await metadataViewPage.clickMetadataGroup('EXIF');
@ -150,9 +127,7 @@ describe('permissions', () => {
await metadataViewPage.clickOnPropertiesTab();
await metadataViewPage.editIconIsDisplayed();
await expect(await viewerPage.getActiveTab()).toEqual(METADATA.PROPERTY_TAB);
await metadataViewPage.clickOnInformationButton();
await expect(await viewerPage.getActiveTab()).toEqual('Properties');
await metadataViewPage.clickMetadataGroup('EXIF');

View File

@ -34,13 +34,9 @@ describe('CardView Component - properties', () => {
const METADATA = {
DATA_FORMAT: 'mmm dd yyyy',
TITLE: 'Details',
COMMENTS_TAB: 'COMMENTS',
PROPERTY_TAB: 'PROPERTIES',
COMMENTS_TAB: 'Comments',
PROPERTY_TAB: 'Properties',
DEFAULT_ASPECT: 'Properties',
MORE_INFO_BUTTON: 'More information',
LESS_INFO_BUTTON: 'Less information',
ARROW_DOWN: 'keyboard_arrow_down',
ARROW_UP: 'keyboard_arrow_up',
EDIT_BUTTON_TOOLTIP: 'Edit'
};
@ -90,7 +86,6 @@ describe('CardView Component - properties', () => {
await expect(await viewerPage.getActiveTab()).toEqual(METADATA.PROPERTY_TAB);
await metadataViewPage.clickOnInformationButton();
await metadataViewPage.clickMetadataGroup('EXIF');
@ -108,11 +103,9 @@ describe('CardView Component - properties', () => {
await viewerPage.clickInfoButton();
await viewerPage.checkInfoSideBarIsDisplayed();
await metadataViewPage.clickOnPropertiesTab();
await metadataViewPage.editIconIsDisplayed();
await metadataViewPage.isEditGeneralIconDisplayed();
await CheckboxPage.check(metadataViewPage.readonlySwitch);
await metadataViewPage.editIconIsNotDisplayed();
});
it('[C268965] Should multi property allow expand multi accordion at the same time when set', async () => {
@ -121,15 +114,9 @@ describe('CardView Component - properties', () => {
await viewerPage.checkInfoSideBarIsDisplayed();
await metadataViewPage.clickOnPropertiesTab();
await metadataViewPage.clickOnInformationButton();
await metadataViewPage.checkMetadataGroupIsNotExpand('EXIF');
await metadataViewPage.checkMetadataGroupIsNotExpand('properties');
await metadataViewPage.clickMetadataGroup('properties');
await metadataViewPage.checkMetadataGroupIsNotExpand('EXIF');
await metadataViewPage.checkMetadataGroupIsExpand('properties');
await metadataViewPage.checkMetadataGroupIsNotExpand('EXIF');
await metadataViewPage.clickMetadataGroup('EXIF');
@ -168,11 +155,7 @@ describe('CardView Component - properties', () => {
await viewerPage.checkInfoSideBarIsDisplayed();
await metadataViewPage.clickOnPropertiesTab();
await metadataViewPage.informationButtonIsDisplayed();
await CheckboxPage.uncheck(metadataViewPage.defaultPropertiesSwitch);
await metadataViewPage.informationButtonIsNotDisplayed();
});
it('[C307975] Should be able to choose which aspect to show expanded in the info-drawer', async () => {

View File

@ -36,14 +36,9 @@ describe('Metadata component', () => {
const METADATA = {
DATA_FORMAT: 'PP',
TITLE: 'Details',
COMMENTS_TAB: 'COMMENTS',
PROPERTY_TAB: 'PROPERTIES',
DEFAULT_ASPECT: 'Properties',
MORE_INFO_BUTTON: 'More information',
LESS_INFO_BUTTON: 'Less information',
ARROW_DOWN: 'keyboard_arrow_down',
ARROW_UP: 'keyboard_arrow_up',
EDIT_BUTTON_TOOLTIP: 'Edit'
COMMENTS_TAB: 'Comments',
PROPERTY_TAB: 'Properties',
DEFAULT_ASPECT: 'General info'
};
const loginPage = new LoginPage();
@ -103,7 +98,7 @@ describe('Metadata component', () => {
await contentServicesPage.waitForTableBody();
});
it("[C245652] Should be possible to display a file's properties", async () => {
it('[C245652] Should be possible to display a file properties', async () => {
await viewerPage.clickInfoButton();
await viewerPage.checkInfoSideBarIsDisplayed();
await metadataViewPage.clickOnPropertiesTab();
@ -129,36 +124,12 @@ describe('Metadata component', () => {
expect(modifiedDate).toEqual(format(new Date(pngFileModel.createdAt), METADATA.DATA_FORMAT), pngFileModel.createdAt);
expect(mimeTypeName).toEqual(pngFileModel.getContent().mimeTypeName);
expect(size).toEqual(pngFileModel.getContent().getSizeInBytes());
await metadataViewPage.editIconIsDisplayed();
await metadataViewPage.informationButtonIsDisplayed();
const informationButtonText = await metadataViewPage.getInformationButtonText();
const informationIconText = await metadataViewPage.getInformationIconText();
expect(informationButtonText).toEqual(METADATA.MORE_INFO_BUTTON);
expect(informationIconText).toEqual(METADATA.ARROW_DOWN);
});
it('[C272769] Should be possible to display more details when clicking on More Information button', async () => {
await viewerPage.clickInfoButton();
await viewerPage.checkInfoSideBarIsDisplayed();
await metadataViewPage.clickOnPropertiesTab();
await metadataViewPage.informationButtonIsDisplayed();
await metadataViewPage.clickOnInformationButton();
const informationButtonText = await metadataViewPage.getInformationButtonText();
const informationIconText = await metadataViewPage.getInformationIconText();
expect(informationButtonText).toEqual(METADATA.LESS_INFO_BUTTON);
expect(informationIconText).toEqual(METADATA.ARROW_UP);
});
it('[C270952] Should be possible to open/close properties using info icon', async () => {
await viewerPage.clickInfoButton();
await viewerPage.checkInfoSideBarIsDisplayed();
await metadataViewPage.clickOnPropertiesTab();
await metadataViewPage.informationButtonIsDisplayed();
await viewerPage.clickInfoButton();
await viewerPage.checkInfoSideBarIsNotDisplayed();
await viewerPage.clickInfoButton();
@ -166,25 +137,17 @@ describe('Metadata component', () => {
await expect(await viewerPage.getActiveTab()).toEqual(METADATA.COMMENTS_TAB);
await metadataViewPage.clickOnPropertiesTab();
await expect(await viewerPage.getActiveTab()).toEqual(METADATA.PROPERTY_TAB);
await expect(await metadataViewPage.getEditIconTooltip()).toEqual(METADATA.EDIT_BUTTON_TOOLTIP);
});
it('[C245654] Should be possible edit the basic Metadata Info of a Document', async () => {
await viewerPage.clickInfoButton();
await viewerPage.checkInfoSideBarIsDisplayed();
await metadataViewPage.clickOnPropertiesTab();
await metadataViewPage.editIconIsDisplayed();
await metadataViewPage.isEditGeneralIconDisplayed();
await expect(await viewerPage.getActiveTab()).toEqual(METADATA.PROPERTY_TAB);
await metadataViewPage.editIconClick();
await metadataViewPage.editPropertyIconIsDisplayed('properties.cm:name');
await metadataViewPage.editPropertyIconIsDisplayed('properties.cm:title');
await metadataViewPage.editPropertyIconIsDisplayed('properties.cm:description');
await expect(await metadataViewPage.getPropertyIconTooltip('properties.cm:name')).toEqual('Edit');
await expect(await metadataViewPage.getPropertyIconTooltip('properties.cm:title')).toEqual('Edit');
await expect(await metadataViewPage.getPropertyIconTooltip('properties.cm:description')).toEqual('Edit');
await metadataViewPage.clickEditIconGeneral();
await metadataViewPage.enterPropertyText('properties.cm:name', 'exampleText');
await metadataViewPage.clickResetMetadata();
@ -192,14 +155,15 @@ describe('Metadata component', () => {
browser.params.resources.Files.ADF_DOCUMENTS.PNG.file_name
);
await metadataViewPage.clickEditIconGeneral();
await metadataViewPage.enterPropertyText('properties.cm:name', 'exampleText.png');
await metadataViewPage.enterPropertyText('properties.cm:title', 'example title');
await metadataViewPage.enterDescriptionText('example description');
await metadataViewPage.clickSaveGeneralMetadata();
await expect(await metadataViewPage.getPropertyText('properties.cm:name')).toEqual('exampleText.png');
await expect(await metadataViewPage.getPropertyText('properties.cm:title')).toEqual('example title');
await expect(await metadataViewPage.getPropertyText('properties.cm:description')).toEqual('example description');
await metadataViewPage.clickSaveMetadata();
await viewerPage.clickCloseButton();
await contentServicesPage.waitForTableBody();
@ -208,32 +172,26 @@ describe('Metadata component', () => {
await viewerPage.clickInfoButton();
await viewerPage.checkInfoSideBarIsDisplayed();
await metadataViewPage.clickOnPropertiesTab();
await metadataViewPage.editIconIsDisplayed();
await metadataViewPage.isEditGeneralIconDisplayed();
await expect(await metadataViewPage.getPropertyText('properties.cm:name')).toEqual('exampleText.png');
await expect(await metadataViewPage.getPropertyText('properties.cm:title')).toEqual('example title');
await expect(await metadataViewPage.getPropertyText('properties.cm:description')).toEqual('example description');
await metadataViewPage.editIconClick();
await metadataViewPage.clickEditIconGeneral();
await metadataViewPage.enterPropertyText('properties.cm:name', browser.params.resources.Files.ADF_DOCUMENTS.PNG.file_name);
await expect(await metadataViewPage.getPropertyText('properties.cm:name')).toEqual(
browser.params.resources.Files.ADF_DOCUMENTS.PNG.file_name
);
await metadataViewPage.clickSaveMetadata();
await metadataViewPage.clickSaveGeneralMetadata();
});
it('[C260181] Should be possible edit all the metadata aspect', async () => {
await viewerPage.clickInfoButton();
await viewerPage.checkInfoSideBarIsDisplayed();
await metadataViewPage.clickOnPropertiesTab();
await metadataViewPage.editIconIsDisplayed();
await expect(await viewerPage.getActiveTab()).toEqual(METADATA.PROPERTY_TAB);
await metadataViewPage.clickOnInformationButton();
await metadataViewPage.clickMetadataGroup('EXIF');
await metadataViewPage.editIconClick();
await metadataViewPage.enterPropertyText('properties.exif:software', 'test custom text software');
@ -255,14 +213,14 @@ describe('Metadata component', () => {
await viewerPage.clickInfoButton();
await viewerPage.checkInfoSideBarIsDisplayed();
await metadataViewPage.clickOnPropertiesTab();
await metadataViewPage.editIconIsDisplayed();
await metadataViewPage.isEditGeneralIconDisplayed();
await expect(await viewerPage.getActiveTab()).toEqual(METADATA.PROPERTY_TAB);
await metadataViewPage.editIconClick();
await metadataViewPage.clickEditIconGeneral();
await metadataViewPage.enterDescriptionText('check author example description');
await metadataViewPage.clickSaveMetadata();
await metadataViewPage.clickSaveGeneralMetadata();
await expect(await metadataViewPage.getPropertyText('properties.cm:description')).toEqual('check author example description');
await navigationBarPage.clickLogoutButton();

View File

@ -32,9 +32,7 @@ export class MetadataViewPage {
description = $(`span[data-automation-id='card-textitem-value-properties.cm:description']`);
author = $(`[data-automation-id='card-textitem-value-properties.cm:author']`);
editIcon = $(`button[data-automation-id='meta-data-card-toggle-edit']`);
informationButton = $(`button[data-automation-id='meta-data-card-toggle-expand']`);
informationSpan = $(`span[data-automation-id='meta-data-card-toggle-expand-label']`);
informationIcon = $(`span[data-automation-id='meta-data-card-toggle-expand-label'] ~ mat-icon`);
editIconGeneral = $(`button[data-automation-id='meta-data-general-info-edit']`);
displayEmptySwitch = $(`#adf-metadata-empty`);
readonlySwitch = $(`#adf-metadata-readonly`);
multiSwitch = $(`#adf-metadata-multi`);
@ -43,6 +41,7 @@ export class MetadataViewPage {
displayAspect = $(`input[data-placeholder='Display Aspect']`);
applyAspect = element(by.cssContainingText(`button span.mat-button-wrapper`, 'Apply Aspect'));
saveMetadataButton = $(`[data-automation-id='save-metadata']`);
saveGeneralMetadataButton = $(`[data-automation-id='save-general-info-metadata']`);
resetMetadataButton = $(`[data-automation-id='reset-metadata']`);
private getMetadataGroupLocator = async (groupName: string): Promise<ElementFinder> =>
@ -102,28 +101,17 @@ export class MetadataViewPage {
await BrowserVisibility.waitUntilElementIsNotVisible(this.editIcon);
}
async isEditGeneralIconDisplayed(): Promise<void> {
await BrowserVisibility.waitUntilElementIsVisible(this.editIconGeneral);
}
async editIconClick(): Promise<void> {
await BrowserActions.clickExecuteScript('button[data-automation-id="meta-data-card-toggle-edit"]');
}
async informationButtonIsDisplayed(): Promise<void> {
await BrowserVisibility.waitUntilElementIsClickable(this.informationButton);
}
async informationButtonIsNotDisplayed(): Promise<void> {
await BrowserVisibility.waitUntilElementIsNotVisible(this.informationButton);
}
async clickOnInformationButton(): Promise<void> {
await BrowserActions.click(this.informationButton);
}
async getInformationButtonText(): Promise<string> {
return BrowserActions.getText(this.informationSpan);
}
async getInformationIconText(): Promise<string> {
return BrowserActions.getText(this.informationIcon);
async clickEditIconGeneral(): Promise<void> {
await BrowserVisibility.waitUntilElementIsVisible(this.editIconGeneral);
await BrowserActions.click(this.editIconGeneral);
}
async clickOnPropertiesTab(): Promise<void> {
@ -137,11 +125,6 @@ export class MetadataViewPage {
return BrowserActions.getAttribute(this.editIcon, 'title');
}
async editPropertyIconIsDisplayed(propertyName: string) {
const editPropertyIcon = $('[data-automation-id="header-' + propertyName + '"] .adf-textitem-edit-icon');
await BrowserVisibility.waitUntilElementIsPresent(editPropertyIcon);
}
async enterPropertyText(propertyName: string, text: string | number): Promise<void> {
const textField = $('input[data-automation-id="card-textitem-value-' + propertyName + '"]');
await BrowserActions.clearSendKeys(textField, text.toString());
@ -161,11 +144,6 @@ export class MetadataViewPage {
return BrowserActions.getInputValue(textField);
}
async getPropertyIconTooltip(propertyName: string): Promise<string> {
const editPropertyIcon = $('[data-automation-id="header-' + propertyName + '"] .adf-textitem-edit-icon');
return BrowserActions.getAttribute(editPropertyIcon, 'title');
}
async clickMetadataGroup(groupName: string): Promise<void> {
const group = await this.getMetadataGroupLocator(groupName);
await BrowserActions.click(group);
@ -286,4 +264,8 @@ export class MetadataViewPage {
async clickResetMetadata(): Promise<void> {
await BrowserActions.click(this.resetMetadataButton);
}
async clickSaveGeneralMetadata(): Promise<void> {
await BrowserActions.click(this.saveGeneralMetadataButton);
}
}

View File

@ -1,10 +1,16 @@
<div class="adf-categories-management">
<p *ngIf="!categories.length && !categoryNameControlVisible"
class="adf-no-categories-message">
{{ noCategoriesMsg | translate }}
</p>
<div class="adf-categories-list"
[class.adf-categories-list-fixed]="!categoryNameControlVisible">
<div *ngIf="categoryNameControlVisible" class="adf-category-name-field">
<input #categoryNameInput
matInput
autocomplete="off"
[formControl]="categoryNameControl"
(keyup.enter)="addCategory()"
placeholder="{{'CATEGORIES_MANAGEMENT.CATEGORIES_SEARCH_PLACEHOLDER' | translate }}"
adf-auto-focus
/>
<mat-error *ngIf="categoryNameControl.invalid">{{ categoryNameErrorMessageKey | translate }}</mat-error>
</div>
<div class="adf-categories-list" [class.adf-categories-list-fixed]="!categoryNameControlVisible">
<span
*ngFor="let category of categories"
[class.adf-categories-padded]="!isCRUDMode"
@ -13,7 +19,6 @@
<button
data-automation-id="categories-remove-category-button"
mat-icon-button
[class.adf-btn-padded]="!isCRUDMode"
(click)="removeCategory(category)"
[attr.title]="removeCategoryTitle | translate"
[disabled]="disableRemoval">
@ -21,33 +26,9 @@
</button>
</span>
</div>
<div *ngIf="((!categoryNameControlVisible && categories.length)) || categoryNameControlVisible"
[hidden]="!categoryNameControlVisible"
class="adf-category-name-field">
<mat-form-field>
<mat-icon matPrefix>search</mat-icon>
<mat-label id="adf-category-name-input-label">
{{ 'CATEGORIES_MANAGEMENT.NAME' | translate }}
</mat-label>
<input
#categoryNameInput
matInput
autocomplete="off"
[formControl]="categoryNameControl"
(keyup.enter)="addCategory()"
aria-labelledby="adf-category-name-input-label"
adf-auto-focus
/>
<mat-error [hidden]="!categoryNameControl.invalid">{{ categoryNameErrorMessageKey | translate }}</mat-error>
</mat-form-field>
<button
mat-icon-button
[class.adf-btn-padded]="!isCRUDMode"
(click)="hideNameInput()"
[attr.title]="'CATEGORIES_MANAGEMENT.HIDE_INPUT' | translate">
<mat-icon>remove</mat-icon>
</button>
</div>
<p *ngIf="showEmptyCategoryMessage" class="adf-no-categories-message">
{{ noCategoriesMsg | translate }}
</p>
</div>
<div class="adf-existing-categories-panel" *ngIf="existingCategoriesPanelVisible">
<ng-container *ngIf="isCRUDMode && (!existingCategoriesLoading || existingCategories)">

View File

@ -1,15 +1,17 @@
.adf-categories-management {
padding-top: 12px;
.adf-category-name-field {
display: flex;
justify-content: space-between;
width: 100%;
color: var(--adf-metadata-property-panel-text-color);
background: var(--adf-metadata-buttons-background-color);
height: 32px;
border-radius: 12px;
align-items: center;
mat-form-field {
width: 100%;
}
.adf-btn-padded {
margin-right: -14px;
input {
padding: 7px 8px;
}
}
@ -18,10 +20,6 @@
justify-content: space-between;
align-items: center;
word-break: break-word;
.adf-btn-padded {
margin-right: -14px;
}
}
.adf-categories-padded {
@ -31,11 +29,14 @@
[hidden] {
visibility: hidden;
}
.adf-no-categories-message {
margin-bottom: 0;
height: 30px;
}
}
.adf-categories-list {
padding-bottom: 10px;
.mat-list-base .mat-list-item,
.mat-list-base .mat-list-option {
display: flex;

View File

@ -223,44 +223,19 @@ describe('CategoriesManagementComponent', () => {
component.categoryNameControlVisible = true;
fixture.detectChanges();
});
it('should be hidden initially', () => {
component.categoryNameControlVisible = false;
fixture.detectChanges();
const categoryControl: HTMLDivElement = fixture.debugElement.query(By.css('.adf-category-name-field')).nativeElement;
expect(categoryControl.hidden).toBeTrue();
});
it('should be visible when categoryNameControlVisible is true', () => {
const categoryControl = fixture.debugElement.query(By.css('.adf-category-name-field'));
expect(categoryControl).toBeTruthy();
});
});
it('should have correct label and hide button', () => {
const categoryControlLabel = fixture.debugElement.query(By.css('#adf-category-name-input-label')).nativeElement;
const categoryControlHideBtn: HTMLButtonElement = fixture.debugElement.query(By.css('.adf-category-name-field button')).nativeElement;
expect(categoryControlHideBtn).toBeTruthy();
expect(categoryControlHideBtn.attributes.getNamedItem('title').textContent.trim()).toBe('CATEGORIES_MANAGEMENT.HIDE_INPUT');
expect(categoryControlLabel.textContent.trim()).toBe('CATEGORIES_MANAGEMENT.NAME');
describe('showEmptyCategoryMessage', () => {
it('should return true when categories empty and category in non editable state', () => {
component.categories = [];
component.categoryNameControlVisible = false;
expect(component.showEmptyCategoryMessage).toBeTrue();
});
it('should hide and clear category control and existing categories panel on clicking hide button', fakeAsync(() => {
typeCategory('test');
const categoryControlHideBtn: HTMLButtonElement = fixture.debugElement.query(By.css('.adf-category-name-field button')).nativeElement;
const controlVisibilityChangeSpy = spyOn(component.categoryNameControlVisibleChange, 'emit').and.callThrough();
categoryControlHideBtn.click();
fixture.detectChanges();
const categoryControl: HTMLDivElement = fixture.debugElement.query(By.css('.adf-category-name-field')).nativeElement;
expect(categoryControl.hidden).toBeTrue();
expect(component.categoryNameControlVisible).toBeFalse();
expect(component.existingCategoriesPanelVisible).toBeFalse();
expect(controlVisibilityChangeSpy).toHaveBeenCalledOnceWith(false);
component.categoryNameControlVisible = true;
fixture.detectChanges();
tick(100);
expect(getCategoryControlInput().value).toBe('');
}));
});
describe('Spinner', () => {
@ -472,13 +447,8 @@ describe('CategoriesManagementComponent', () => {
expect(categoriesChangeSpy).toHaveBeenCalledOnceWith(component.categories);
}));
it('should clear and hide input after category is created', fakeAsync(() => {
const controlVisibilityChangeSpy = spyOn(component.categoryNameControlVisibleChange, 'emit');
it('should clear input after category is created', fakeAsync(() => {
createCategory('test');
const categoryControl: HTMLDivElement = fixture.debugElement.query(By.css('.adf-category-name-field')).nativeElement;
expect(categoryControl.hidden).toBeTrue();
expect(controlVisibilityChangeSpy).toHaveBeenCalledOnceWith(false);
expect(getExistingCategoriesList()).toEqual([]);
expect(component.categoryNameControl.value).toBe('');
expect(component.categoryNameControl.untouched).toBeTrue();

View File

@ -181,6 +181,13 @@ export class CategoriesManagementComponent implements OnInit, OnDestroy {
return this._categoryNameControl;
}
/*
* Returns `true` if categories empty and category panel non editable state, otherwise `false`
*/
get showEmptyCategoryMessage(): boolean {
return this.categories.length === 0 && !this.categoryNameControlVisible;
}
get existingCategories(): Category[] {
return this._existingCategories;
}
@ -205,16 +212,6 @@ export class CategoriesManagementComponent implements OnInit, OnDestroy {
return this.managementMode === CategoriesManagementMode.CRUD;
}
/**
* Hides and emits categoryNameControl and hides existing categories panel.
*/
hideNameInput() {
this.categoryNameControlVisible = false;
this.categoryNameControlVisibleChange.emit(false);
this._existingCategoriesPanelVisible = false;
this.clearCategoryNameInput();
}
/**
* Adds category that has been typed to a categoryNameControl and hides it afterwards.
*/
@ -223,7 +220,6 @@ export class CategoriesManagementComponent implements OnInit, OnDestroy {
const newCatName = this.categoryNameControl.value.trim();
const newCat = new Category({ id: newCatName, name: newCatName });
this.categories.push(newCat);
this.hideNameInput();
this.clearCategoryNameInput();
this._existingCategories = null;
this.categoriesChange.emit(this.categories);

View File

@ -129,4 +129,52 @@ describe('ContentService', () => {
expect(contentService.hasPermissions(permissionNode, 'manager')).toBeTruthy();
});
});
describe('Node Icons', () => {
let node: Node;
node = {
isFolder: true,
isFile: false,
createdByUser: { id: 'admin', displayName: 'Administrator' },
modifiedAt: new Date('2017-05-24T15:08:55.640Z'),
nodeType: 'cm:content',
content: {
mimeType: 'application/rtf',
mimeTypeName: 'Rich Text Format',
sizeInBytes: 14530
},
createdAt: new Date('2017-05-24T15:08:55.640Z'),
modifiedByUser: { id: 'admin', displayName: 'Administrator' },
name: 'b_txt_file.rtf',
id: 'test node 1',
aspectNames: ['']
} as Node;
it('should resolve folder icon', () => {
expect(contentService.getNodeIcon(node)).toContain('assets/images/ft_ic_folder.svg');
});
it('should resolve link folder icon', () => {
node.nodeType = 'app:folderlink';
expect(contentService.getNodeIcon(node)).toContain('assets/images/ft_ic_folder_shortcut_link.svg');
});
it('should resolve smart folder icon', () => {
node.aspectNames = ['smf:customConfigSmartFolder'];
expect(contentService.getNodeIcon(node)).toContain('assets/images/ft_ic_smart_folder.svg');
});
it('should resolve file icon for content type', () => {
node.isFolder = false;
node.isFile = true;
expect(contentService.getNodeIcon(node)).toContain('assets/images/ft_ic_ms_word.svg');
});
it('should resolve fallback file icon for unknown node', () => {
node.isFolder = false;
node.isFile = false;
expect(contentService.getNodeIcon(node)).toContain('assets/images/ft_ic_miscellaneous');
});
});
});

View File

@ -18,7 +18,7 @@
import { Injectable } from '@angular/core';
import { ContentApi, Node, NodeEntry } from '@alfresco/js-api';
import { Subject } from 'rxjs';
import { AlfrescoApiService, AuthenticationService } from '@alfresco/adf-core';
import { AlfrescoApiService, AuthenticationService, ThumbnailService } from '@alfresco/adf-core';
import { PermissionsEnum } from '../models/permissions.enum';
import { AllowableOperationsEnum } from '../models/allowable-operations.enum';
@ -43,7 +43,7 @@ export class ContentService {
return this._contentApi;
}
constructor(public authService: AuthenticationService, public apiService: AlfrescoApiService) {}
constructor(public authService: AuthenticationService, public apiService: AlfrescoApiService, private thumbnailService?: ThumbnailService) {}
/**
* Gets a content URL for the given node.
@ -145,4 +145,48 @@ export class ContentService {
return hasAllowableOperations;
}
getNodeIcon(node: Node): string {
if (node?.isFolder) {
return this.getFolderIcon(node);
}
if (node?.isFile) {
return this.thumbnailService.getMimeTypeIcon(node?.content?.mimeType);
}
return this.thumbnailService.getDefaultMimeTypeIcon();
}
private getFolderIcon(node: Node): string {
if (this.isSmartFolder(node)) {
return this.thumbnailService.getMimeTypeIcon('smartFolder');
} else if (this.isRuleFolder(node)) {
return this.thumbnailService.getMimeTypeIcon('ruleFolder');
} else if (this.isLinkFolder(node)) {
return this.thumbnailService.getMimeTypeIcon('linkFolder');
} else {
return this.thumbnailService.getMimeTypeIcon('folder');
}
}
isSmartFolder(node: Node): boolean {
if (node) {
return this.hasAspect(node, 'smf:customConfigSmartFolder') || this.hasAspect(node, 'smf:systemConfigSmartFolder');
}
return false;
}
isRuleFolder(node: Node): boolean {
if (node) {
return this.hasAspect(node, 'rule:rules');
}
return false;
}
isLinkFolder(node: Node): boolean {
return node?.nodeType === 'app:folderlink';
}
private hasAspect(node: Node, aspectName: string): boolean {
return node?.aspectNames?.includes(aspectName);
}
}

View File

@ -5,7 +5,7 @@
[expanded]="expanded"
[node]="node"
[displayEmpty]="displayEmpty"
[editable]="editable"
[readOnly]="!editable"
[multi]="multi"
[displayAspect]="displayAspect"
[preset]="preset"
@ -24,24 +24,6 @@
data-automation-id="meta-data-card-edit-aspect">
<mat-icon>menu</mat-icon>
</button>
<button *ngIf="!readOnly && hasAllowableOperations()"
mat-icon-button
(click)="toggleEdit()"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.aria-label]="'CORE.METADATA.ACCESSIBILITY.EDIT' | translate"
data-automation-id="meta-data-card-toggle-edit">
<mat-icon>mode_edit</mat-icon>
</button>
</div>
<button *ngIf="displayDefaultProperties" mat-button (click)="toggleExpanded()" data-automation-id="meta-data-card-toggle-expand">
<ng-container *ngIf="!expanded">
<span data-automation-id="meta-data-card-toggle-expand-label">{{ 'ADF_VIEWER.SIDEBAR.METADATA.MORE_INFORMATION' | translate }}</span>
<mat-icon>keyboard_arrow_down</mat-icon>
</ng-container>
<ng-container *ngIf="expanded">
<span data-automation-id="meta-data-card-toggle-expand-label">{{ 'ADF_VIEWER.SIDEBAR.METADATA.LESS_INFORMATION' | translate }}</span>
<mat-icon>keyboard_arrow_up</mat-icon>
</ng-container>
</button>
</mat-card-footer>
</mat-card>

View File

@ -114,14 +114,6 @@ describe('ContentMetadataCardComponent', () => {
expect(contentMetadataComponent.displayEmpty).toBe(true);
});
it('should pass through the editable to the underlying component', () => {
component.editable = true;
fixture.detectChanges();
const contentMetadataComponent = fixture.debugElement.query(By.directive(ContentMetadataComponent)).componentInstance;
expect(contentMetadataComponent.editable).toBe(true);
});
it('should pass through the multi to the underlying component', () => {
component.multi = true;
fixture.detectChanges();
@ -147,55 +139,6 @@ describe('ContentMetadataCardComponent', () => {
expect(contentMetadataComponent).toBeNull();
});
it('should toggle editable by clicking on the button', () => {
component.editable = true;
component.node.allowableOperations = [AllowableOperationsEnum.UPDATE];
fixture.detectChanges();
getToggleEditButton().triggerEventHandler('click', {});
fixture.detectChanges();
expect(component.editable).toBe(false);
});
it('should emit editableChange by clicking on toggle edit button', () => {
component.node.allowableOperations = [AllowableOperationsEnum.UPDATE];
fixture.detectChanges();
spyOn(component.editableChange, 'emit');
getToggleEditButton().nativeElement.click();
expect(component.editableChange.emit).toHaveBeenCalledWith(true);
});
it('should toggle expanded by clicking on the button', () => {
component.expanded = true;
fixture.detectChanges();
const button = fixture.debugElement.query(By.css('[data-automation-id="meta-data-card-toggle-expand"]'));
button.triggerEventHandler('click', {});
fixture.detectChanges();
expect(component.expanded).toBe(false);
});
it('should have the proper text on button while collapsed', () => {
component.expanded = false;
fixture.detectChanges();
const buttonLabel = fixture.debugElement.query(By.css('[data-automation-id="meta-data-card-toggle-expand-label"]'));
expect(buttonLabel.nativeElement.innerText.trim()).toBe('ADF_VIEWER.SIDEBAR.METADATA.MORE_INFORMATION');
});
it('should have the proper text on button while collapsed', () => {
component.expanded = true;
fixture.detectChanges();
const buttonLabel = fixture.debugElement.query(By.css('[data-automation-id="meta-data-card-toggle-expand-label"]'));
expect(buttonLabel.nativeElement.innerText.trim()).toBe('ADF_VIEWER.SIDEBAR.METADATA.LESS_INFORMATION');
});
it('should hide the edit button in readOnly is true', () => {
component.readOnly = true;
fixture.detectChanges();
@ -211,14 +154,6 @@ describe('ContentMetadataCardComponent', () => {
expect(getToggleEditButton()).toBeNull();
});
it('should show the edit button if node does has `update` permissions', () => {
component.readOnly = false;
component.node.allowableOperations = [AllowableOperationsEnum.UPDATE];
fixture.detectChanges();
expect(getToggleEditButton()).not.toBeNull();
});
it('should expand the card when custom display aspect is valid', () => {
expect(component.expanded).toBeFalsy();

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewEncapsulation } from '@angular/core';
import { Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core';
import { Node } from '@alfresco/js-api';
import { NodeAspectService } from '../../../aspect-list/services/node-aspect.service';
import { ContentMetadataCustomPanel, PresetConfig } from '../../interfaces/content-metadata.interfaces';
@ -84,10 +84,6 @@ export class ContentMetadataCardComponent implements OnChanges {
@Input()
customPanels: ContentMetadataCustomPanel[];
/** Emitted when content's editable state is changed. */
@Output()
editableChange = new EventEmitter<boolean>();
private _displayDefaultProperties: boolean = true;
/**
@ -125,15 +121,6 @@ export class ContentMetadataCardComponent implements OnChanges {
this.expanded = !this._displayDefaultProperties;
}
toggleEdit(): void {
this.editable = !this.editable;
this.editableChange.emit(this.editable);
}
toggleExpanded(): void {
this.expanded = !this.expanded;
}
hasAllowableOperations() {
return this.contentService.hasAllowableOperations(this.node, AllowableOperationsEnum.UPDATE);
}

View File

@ -0,0 +1,55 @@
/*!
* @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 { CommonModule } from '@angular/common';
import { Component, Input, ViewEncapsulation } from '@angular/core';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon';
import { TranslateModule } from '@ngx-translate/core';
@Component({
standalone: true,
imports: [CommonModule, MatIconModule, MatExpansionModule, TranslateModule],
selector: 'adf-content-metadata-header',
encapsulation: ViewEncapsulation.None,
styles: [
`
adf-content-metadata-header {
display: flex;
align-items: center;
flex: 1;
}
.adf-metadata-properties-title {
font-weight: 700;
font-size: 15px;
padding-left: 12px;
}
`
],
template: `
<ng-container>
<mat-icon>{{ expanded ? 'expand_more' : 'chevron_right' }}</mat-icon>
<mat-panel-title *ngIf="title" class="adf-metadata-properties-title">{{ title | translate }}</mat-panel-title>
<ng-content></ng-content>
</ng-container>
`
})
export class ContentMetadataHeaderComponent {
@Input() title: string = null;
@Input() expanded = true;
}

View File

@ -1,138 +1,225 @@
<div class="adf-metadata-properties">
<mat-accordion displayMode="flat"
[multi]="multi">
<mat-expansion-panel *ngIf="displayDefaultProperties"
[expanded]="canExpandProperties()"
[attr.data-automation-id]="'adf-metadata-group-properties'">
<mat-expansion-panel-header>
<mat-panel-title class="adf-metadata-properties-title">
{{ 'CORE.METADATA.BASIC.HEADER' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<adf-card-view
(keydown)="keyDown($event)"
[properties]="basicProperties$ | async"
[editable]="editable"
[displayEmpty]="displayEmpty"
[copyToClipboardAction]="copyToClipboardAction"
[useChipsForMultiValueProperty]="useChipsForMultiValueProperty"
[multiValueSeparator]="multiValueSeparator">
</adf-card-view>
</mat-expansion-panel>
<ng-container *ngIf="displayTags">
<mat-expansion-panel *ngIf="!editable">
<mat-expansion-panel-header>
<mat-panel-title>{{ 'METADATA.BASIC.TAGS' | translate }}</mat-panel-title>
</mat-expansion-panel-header>
<p *ngFor="let tag of tags" class="adf-metadata-properties-tag">{{ tag }}</p>
</mat-expansion-panel>
<div
*ngIf="editable"
class="adf-metadata-properties-tags">
<div class="adf-metadata-properties-tags-title">
<p>{{ 'METADATA.BASIC.TAGS' | translate }}</p>
<button
data-automation-id="showing-tag-input-button"
<mat-accordion displayMode="flat" [multi]="multi" class="adf-metadata-properties">
<mat-expansion-panel
*ngIf="displayDefaultProperties"
class="adf-content-metadata-panel"
[(expanded)]="isGeneralPanelExpanded"
[attr.data-automation-id]="'adf-metadata-group-properties'"
hideToggle>
<mat-expansion-panel-header>
<adf-content-metadata-header [title]="'CORE.METADATA.BASIC.HEADER'" [expanded]="isGeneralPanelExpanded">
<button *ngIf="canEditGeneralInfo"
mat-icon-button
[attr.title]="'METADATA.BASIC.ADD_TAG_TOOLTIP' | translate"
(click)="tagNameControlVisible = true"
[hidden]="tagNameControlVisible || saving">
<mat-icon>add</mat-icon>
(click)="onToggleGeneralInfoEdit($event)"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.aria-label]="'CORE.METADATA.ACCESSIBILITY.EDIT' | translate"
data-automation-id="meta-data-general-info-edit"
class="adf-edit-icon-buttons">
<mat-icon>mode_edit</mat-icon>
</button>
<div *ngIf="isEditingGeneralInfo" class="adf-metadata-action-buttons">
<button mat-icon-button
[attr.title]="'CORE.METADATA.ACTIONS.CANCEL' | translate"
(click)="onCancelGeneralInfoEdit($event)"
data-automation-id="reset-metadata"
class="adf-metadata-action-buttons-clear">
<mat-icon>clear</mat-icon>
</button>
<button mat-icon-button
[attr.title]="'CORE.METADATA.ACTIONS.SAVE' | translate"
(click)="onSaveGeneralInfoChanges($event)"
color="primary"
data-automation-id="save-general-info-metadata"
[disabled]="!hasMetadataChanged">
<mat-icon>check</mat-icon>
</button>
</div>
<adf-tags-creator
[(tagNameControlVisible)]="tagNameControlVisible"
(tagsChange)="storeTagsToAssign($event)"
[mode]="tagsCreatorMode"
[tags]="assignedTags"
[disabledTagsRemoving]="saving">
</adf-tags-creator>
</adf-content-metadata-header>
</mat-expansion-panel-header>
<adf-card-view
(keydown)="keyDown($event)"
[properties]="basicProperties$ | async"
[editable]="isEditingModeGeneralInfo"
[displayEmpty]="displayEmpty"
[copyToClipboardAction]="copyToClipboardAction"
[useChipsForMultiValueProperty]="useChipsForMultiValueProperty"
[multiValueSeparator]="multiValueSeparator">
</adf-card-view>
</mat-expansion-panel>
<ng-container *ngIf="displayTags">
<mat-expansion-panel hideToggle [(expanded)]="isTagPanelExpanded" class="adf-content-metadata-panel">
<mat-expansion-panel-header>
<adf-content-metadata-header [title]="'METADATA.BASIC.TAGS'" [expanded]="isTagPanelExpanded">
<button *ngIf="canEditTags"
mat-icon-button
(click)="onToggleTagsEdit($event)"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.aria-label]="'CORE.METADATA.ACCESSIBILITY.EDIT' | translate"
data-automation-id="showing-tag-input-button"
class="adf-edit-icon-buttons">
<mat-icon>mode_edit</mat-icon>
</button>
<div *ngIf="isEditingTags" class="adf-metadata-action-buttons">
<button mat-icon-button
[attr.title]="'CORE.METADATA.ACTIONS.CANCEL' | translate"
(click)="onCancelTagsEdit($event)"
data-automation-id="reset-tags-metadata"
class="adf-metadata-action-buttons-clear">
<mat-icon>clear</mat-icon>
</button>
<button mat-icon-button
[attr.title]="'CORE.METADATA.ACTIONS.SAVE' | translate"
(click)="onSaveTagsChanges($event)"
color="primary"
data-automation-id="save-tags-metadata"
[disabled]="!hasMetadataChanged">
<mat-icon>check</mat-icon>
</button>
</div>
</adf-content-metadata-header>
</mat-expansion-panel-header>
<div *ngIf="!isEditingModeTags" class="adf-metadata-properties-tags">
<span *ngFor="let tag of tags" class="adf-metadata-properties-tag">{{ tag }}</span>
</div>
</ng-container>
<ng-container *ngIf="displayCategories">
<mat-expansion-panel *ngIf="!editable">
<mat-expansion-panel-header>
<mat-panel-title>{{ 'CATEGORIES_MANAGEMENT.CATEGORIES_TITLE' | translate }}</mat-panel-title>
</mat-expansion-panel-header>
<div *ngIf="showEmptyTagMessage" class="adf-metadata-no-item-added">
{{ 'METADATA.BASIC.NO_TAGS_ADDED' | translate }}
</div>
<adf-tags-creator
*ngIf="isEditingModeTags"
class="adf-metadata-properties-tags"
[tagNameControlVisible]="tagNameControlVisible"
(tagsChange)="storeTagsToAssign($event)"
[mode]="tagsCreatorMode"
[tags]="assignedTags"
[disabledTagsRemoving]="saving">
</adf-tags-creator>
</mat-expansion-panel>
</ng-container>
<ng-container *ngIf="displayCategories">
<mat-expansion-panel hideToggle [(expanded)]="isCategoriesPanelExpanded" class="adf-content-metadata-panel">
<mat-expansion-panel-header>
<adf-content-metadata-header [title]="'CATEGORIES_MANAGEMENT.CATEGORIES_TITLE'" [expanded]="isCategoriesPanelExpanded">
<button *ngIf="canEditCategories"
mat-icon-button
(click)="onToggleCategoriesEdit($event)"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.aria-label]="'CORE.METADATA.ACCESSIBILITY.EDIT' | translate"
data-automation-id="meta-data-categories-edit"
class="adf-categories-button adf-edit-icon-buttons">
<mat-icon>mode_edit</mat-icon>
</button>
<div *ngIf="isEditingCategories" class="adf-metadata-action-buttons">
<button mat-icon-button
[attr.title]="'CORE.METADATA.ACTIONS.CANCEL' | translate"
(click)="onCancelCategoriesEdit($event)"
data-automation-id="reset-metadata"
class="adf-metadata-action-buttons-clear">
<mat-icon>clear</mat-icon>
</button>
<button mat-icon-button
[attr.title]="'CORE.METADATA.ACTIONS.SAVE' | translate"
(click)="onSaveCategoriesChanges($event)"
color="primary"
data-automation-id="save-categories-metadata"
[disabled]="!hasMetadataChanged">
<mat-icon>check</mat-icon>
</button>
</div>
</adf-content-metadata-header>
</mat-expansion-panel-header>
<div *ngIf="!isEditingModeCategories">
<p *ngFor="let category of categories" class="adf-metadata-categories">{{ category.name }}</p>
</mat-expansion-panel>
<div *ngIf="editable"
class="adf-metadata-categories-header">
<div class="adf-metadata-categories-title">
<p>{{ 'CATEGORIES_MANAGEMENT.CATEGORIES_TITLE' | translate }}</p>
<button
mat-icon-button
[attr.title]="'CATEGORIES_MANAGEMENT.ASSIGN_CATEGORIES' | translate"
[hidden]="categoryControlVisible || saving"
(click)="categoryControlVisible = true">
<mat-icon>add</mat-icon>
</button>
</div>
<adf-categories-management
[(categoryNameControlVisible)]="categoryControlVisible"
[disableRemoval]="saving"
[categories]="categories"
[managementMode]="categoriesManagementMode"
[classifiableChanged]="classifiableChanged"
(categoriesChange)="storeCategoriesToAssign($event)">
</adf-categories-management>
</div>
</ng-container>
<mat-expansion-panel *ngFor="let customPanel of customPanels" [expanded]="canExpandTheCard(customPanel.panelTitle)">
<mat-expansion-panel-header>
<mat-panel-title>{{ customPanel.panelTitle | translate }}</mat-panel-title>
</mat-expansion-panel-header>
<adf-dynamic-component [id]="customPanel.component" [data]="{ node }"></adf-dynamic-component>
<div *ngIf="showEmptyCategoryMessage" class="adf-metadata-no-item-added">
{{ 'CATEGORIES_MANAGEMENT.NO_CATEGORIES_ADDED' | translate }}
</div>
<adf-categories-management
*ngIf="isEditingModeCategories"
class="adf-metadata-categories-header"
[(categoryNameControlVisible)]="categoryControlVisible"
[disableRemoval]="saving"
[categories]="categories"
[managementMode]="categoriesManagementMode"
[classifiableChanged]="classifiableChanged"
(categoriesChange)="storeCategoriesToAssign($event)">
</adf-categories-management>
</mat-expansion-panel>
<ng-container *ngIf="expanded">
<ng-container *ngIf="groupedProperties$ | async; else loading; let groupedProperties">
<div *ngFor="let group of groupedProperties; let first = first;"
class="adf-metadata-grouped-properties-container">
<mat-expansion-panel *ngIf="showGroup(group) || editable"
[attr.data-automation-id]="'adf-metadata-group-' + group.title"
[expanded]="canExpandTheCard(group.title) || !displayDefaultProperties && first">
<mat-expansion-panel-header>
<mat-panel-title>
{{ group.title | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
</ng-container>
<adf-card-view
(keydown)="keyDown($event)"
[properties]="group.properties"
[editable]="editable"
[displayEmpty]="displayEmpty"
[copyToClipboardAction]="copyToClipboardAction"
[useChipsForMultiValueProperty]="useChipsForMultiValueProperty"
[multiValueSeparator]="multiValueSeparator"
[displayLabelForChips]="true">
</adf-card-view>
</mat-expansion-panel>
<mat-expansion-panel
*ngFor="let customPanel of customPanels"
[expanded]="canExpandTheCard(customPanel.panelTitle)"
(opened)="customPanel.expanded = true"
(closed)="customPanel.expanded = false"
class="adf-content-metadata-panel"
hideToggle>
<mat-expansion-panel-header>
<adf-content-metadata-header class="adf-metadata-custom-panel-title" [title]="customPanel.panelTitle" [expanded]="customPanel.expanded">
</adf-content-metadata-header>
</mat-expansion-panel-header>
<adf-dynamic-component [id]="customPanel.component" [data]="{ node }"></adf-dynamic-component>
</mat-expansion-panel>
<ng-container *ngIf="groupedProperties$ | async; else loading; let groupedProperties">
<div *ngFor="let group of groupedProperties; let first = first;"
class="adf-metadata-grouped-properties-container">
<mat-expansion-panel
[attr.data-automation-id]="'adf-metadata-group-' + group.title"
[expanded]="canExpandTheCard(group.title) || !displayDefaultProperties && first || group.expanded"
(opened)="group.expanded = true"
(closed)="group.expanded = false"
class="adf-content-metadata-panel"
hideToggle>
<mat-expansion-panel-header>
<adf-content-metadata-header [title]="group.title" [expanded]="group.expanded">
<button *ngIf="hasGroupToggleEdit(group)"
mat-icon-button
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.aria-label]="'CORE.METADATA.ACCESSIBILITY.EDIT' | translate"
data-automation-id="meta-data-card-toggle-edit"
class="adf-edit-icon-buttons"
(click)="onToggleGroupEdit(group, $event)">
<mat-icon>mode_edit</mat-icon>
</button>
<div class="adf-metadata-action-buttons" *ngIf="isGroupToggleEditing(group)">
<button mat-icon-button
[attr.title]="'CORE.METADATA.ACTIONS.CANCEL' | translate"
(click)="onCancelGroupEdit(group, $event)"
data-automation-id="reset-metadata"
class="adf-metadata-action-buttons-clear">
<mat-icon>clear</mat-icon>
</button>
<button mat-icon-button
[attr.title]="'CORE.METADATA.ACTIONS.SAVE' | translate"
(click)="onSaveGroupChanges(group, $event)"
color="primary"
data-automation-id="save-metadata"
[disabled]="!hasMetadataChanged">
<mat-icon>check</mat-icon>
</button>
</div>
</adf-content-metadata-header>
</mat-expansion-panel-header>
<div *ngIf="!showGroup(group) && !group.editable" class="adf-metadata-no-item-added">
{{ 'METADATA.BASIC.NO_ITEMS_MESSAGE' | translate: { groupTitle: group.title | translate } }}
</div>
</ng-container>
<ng-template #loading>
<mat-progress-bar mode="indeterminate" [attr.aria-label]="'DATA_LOADING' | translate">
</mat-progress-bar>
</ng-template>
</ng-container>
</mat-accordion>
<adf-card-view
(keydown)="keyDown($event)"
[properties]="group.properties"
[editable]="group.editable"
[displayEmpty]="displayEmpty"
[copyToClipboardAction]="copyToClipboardAction"
[useChipsForMultiValueProperty]="useChipsForMultiValueProperty"
[multiValueSeparator]="multiValueSeparator"
[displayLabelForChips]="true">
</adf-card-view>
</mat-expansion-panel>
</div>
</ng-container>
<div class="adf-metadata-action-buttons"
*ngIf="editable">
<button mat-button
(click)="cancelChanges()"
data-automation-id="reset-metadata"
[disabled]="!hasMetadataChanged">
{{ 'CORE.METADATA.ACTIONS.CANCEL' | translate }}
</button>
<button mat-raised-button
(click)="saveChanges()"
color="primary"
data-automation-id="save-metadata"
[disabled]="!hasMetadataChanged">
{{ 'CORE.METADATA.ACTIONS.SAVE' | translate }}
</button>
</div>
</div>
<ng-template #loading>
<mat-progress-bar mode="indeterminate" [attr.aria-label]="'DATA_LOADING' | translate">
</mat-progress-bar>
</ng-template>
</mat-accordion>

View File

@ -1,56 +1,64 @@
$panel-properties-height: 56px !default;
.adf {
&-metadata-properties {
.mat-expansion-panel-header.mat-expanded:hover,
.mat-expansion-panel-header.mat-expanded:focus {
background: var(--adf-theme-background-hover-color);
.adf-content-metadata-panel {
box-shadow: none;
border: 1px solid var(--adf-metadata-property-panel-border-color);
border-radius: 12px;
margin: 12px;
}
mat-expansion-panel-header {
height: 64px;
height: $panel-properties-height;
padding: 0 12px;
border-radius: 12px 12px 0 0;
.adf-metadata-properties-title {
font-weight: normal;
font-size: 15px;
&.mat-expanded {
height: $panel-properties-height;
border-bottom: 1px solid var(--adf-metadata-property-panel-border-color);
}
.mat-content {
display: contents;
margin-right: 0;
}
}
.mat-expansion-panel:not([class*='mat-elevation-z']) {
box-shadow: none;
.mat-expansion-panel-content {
.mat-expansion-panel-body {
padding: 0 12px 12px;
}
}
.adf-edit-icon-buttons {
color: var(--adf-theme-foreground-text-color-054);
}
.adf-metadata-properties-tag {
height: 40px;
display: flex;
min-height: 32px;
display: inline-flex;
align-items: center;
margin-top: -14px;
margin-bottom: 1em;
border-radius: 16px;
width: fit-content;
background: var(--adf-metadata-buttons-background-color);
margin-top: 12px;
padding: 6px 12px;
justify-content: center;
margin-left: 8px;
overflow-wrap: anywhere;
}
&:first-of-type {
margin-top: 5px;
}
.adf-metadata-no-item-added {
word-break: break-all;
font-size: 15px;
padding: 16px 0 0 12px;
}
&-tags {
padding: 0 10px 0 26px;
&-title {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 15px;
height: 68px;
}
adf-tags-creator {
margin-top: 19px;
.adf-tags-creation {
padding-right: 0;
padding-left: 12px;
.adf-tag {
margin-top: -14px;
}
}
&.adf-creator-with-existing-tags-panel {
@ -67,24 +75,19 @@
&-metadata-action-buttons {
display: flex;
justify-content: space-evenly;
margin: 10px;
&-clear {
color: var(--adf-metadata-action-button-clear-color);
}
}
&-metadata-categories-header {
display: flex;
flex-direction: column;
padding: 0 24px;
.adf-metadata-categories-title {
.adf-categories-button {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 15px;
height: 64px;
button {
margin-right: -14px;
}
height: $panel-properties-height;
[hidden] {
visibility: hidden;

View File

@ -21,16 +21,16 @@ import { By } from '@angular/platform-browser';
import { Category, CategoryPaging, ClassesApi, Node, Tag, TagBody, TagEntry, TagPaging, TagPagingList } from '@alfresco/js-api';
import { ContentMetadataComponent } from './content-metadata.component';
import { ContentMetadataService } from '../../services/content-metadata.service';
import { AppConfigService, CardViewBaseItemModel, CardViewComponent, LogService, UpdateNotification } from '@alfresco/adf-core';
import { AppConfigService, CardViewBaseItemModel, CardViewComponent, NotificationService, UpdateNotification } from '@alfresco/adf-core';
import { NodesApiService } from '../../../common/services/nodes-api.service';
import { EMPTY, of, throwError } from 'rxjs';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { mockGroupProperties } from './mock-data';
import { TranslateModule } from '@ngx-translate/core';
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 {
CardViewGroup,
CategoriesManagementComponent,
CategoriesManagementMode,
CategoryService,
@ -49,6 +49,8 @@ describe('ContentMetadataComponent', () => {
let folderNode: Node;
let tagService: TagService;
let categoryService: CategoryService;
let getClassSpy: jasmine.Spy;
let notificationService: NotificationService;
const preset = 'custom-preset';
@ -71,26 +73,38 @@ describe('ContentMetadataComponent', () => {
const category2 = new Category({ id: 'test2', name: 'testCat2' });
const categoryPagingResponse: CategoryPaging = { list: { pagination: {}, entries: [{ entry: category1 }, { entry: category2 }] } };
const findTagElements = (): DebugElement[] => fixture.debugElement.queryAll(By.css('.adf-metadata-properties-tag'));
const findTagElements = (): DebugElement[] => fixture.debugElement.queryAll(By.css('.adf-metadata-properties .adf-metadata-properties-tag'));
const findCancelButton = (): HTMLButtonElement => fixture.debugElement.query(By.css('[data-automation-id=reset-metadata]')).nativeElement;
const findCancelTagsButton = (): HTMLButtonElement =>
fixture.debugElement.query(By.css('[data-automation-id=reset-tags-metadata]')).nativeElement;
const clickOnCancel = () => {
findCancelButton().click();
fixture.detectChanges();
};
const findSaveButton = (): HTMLButtonElement => fixture.debugElement.query(By.css('[data-automation-id=save-metadata]')).nativeElement;
const findSaveGeneralInfoButton = (): HTMLButtonElement =>
fixture.debugElement.query(By.css('[data-automation-id=save-general-info-metadata]')).nativeElement;
const findSaveTagsButton = (): HTMLButtonElement => fixture.debugElement.query(By.css('[data-automation-id=save-tags-metadata]')).nativeElement;
const findSaveCategoriesButton = (): HTMLButtonElement =>
fixture.debugElement.query(By.css('[data-automation-id=save-categories-metadata]')).nativeElement;
const clickOnSave = () => {
findSaveButton().click();
const clickOnGeneralInfoSave = () => {
findSaveGeneralInfoButton().click();
fixture.detectChanges();
};
const clickOnTagsSave = () => {
findSaveTagsButton().click();
fixture.detectChanges();
};
const findTagsCreator = (): TagsCreatorComponent => fixture.debugElement.query(By.directive(TagsCreatorComponent))?.componentInstance;
const findShowingTagInputButton = (): HTMLButtonElement =>
fixture.debugElement.query(By.css('[data-automation-id=showing-tag-input-button]')).nativeElement;
const getToggleEditButton = () => fixture.debugElement.query(By.css('[data-automation-id="meta-data-general-info-edit"]'));
const getTagsToggleEditButton = () => fixture.debugElement.query(By.css('[data-automation-id="showing-tag-input-button"]'));
const getCategoriesToggleEditButton = () => fixture.debugElement.query(By.css('[data-automation-id="meta-data-categories-edit"]'));
const getGroupToggleEditButton = () => fixture.debugElement.query(By.css('[data-automation-id="meta-data-card-toggle-edit"]'));
/**
* Get metadata categories
@ -110,22 +124,12 @@ describe('ContentMetadataComponent', () => {
return fixture.debugElement.query(By.directive(CategoriesManagementComponent))?.componentInstance;
}
/**
* Get categories title button
*
* @returns native element
*/
function getAssignCategoriesBtn(): HTMLButtonElement {
return fixture.debugElement.query(By.css('.adf-metadata-categories-title button')).nativeElement;
}
/**
* Update aspect property
*
* @param newValue value to set
*/
async function updateAspectProperty(newValue: string): Promise<void> {
component.editable = true;
const property = { key: 'properties.property-key', value: 'original-value' } as CardViewBaseItemModel;
const expectedNode = { ...node, name: 'some-modified-value' };
spyOn(nodesApiService, 'updateNode').and.returnValue(of(expectedNode));
@ -135,8 +139,7 @@ describe('ContentMetadataComponent', () => {
fixture.detectChanges();
await fixture.whenStable();
clickOnSave();
component.onSaveGroupChanges({} as any);
await fixture.whenStable();
}
@ -144,12 +147,6 @@ describe('ContentMetadataComponent', () => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), ContentTestingModule],
providers: [
{
provide: LogService,
useValue: {
error: jasmine.createSpy('error')
}
},
{
provide: TagService,
useValue: {
@ -175,6 +172,9 @@ describe('ContentMetadataComponent', () => {
nodesApiService = TestBed.inject(NodesApiService);
tagService = TestBed.inject(TagService);
categoryService = TestBed.inject(CategoryService);
notificationService = TestBed.inject(NotificationService);
const propertyDescriptorsService = TestBed.inject(PropertyDescriptorsService);
const classesApi = propertyDescriptorsService['classesApi'];
node = {
id: 'node-id',
@ -197,6 +197,7 @@ describe('ContentMetadataComponent', () => {
component.node = node;
component.preset = preset;
spyOn(contentMetadataService, 'getContentTypeProperty').and.returnValue(of([]));
getClassSpy = spyOn(classesApi, 'getClass');
fixture.detectChanges();
});
@ -206,7 +207,7 @@ describe('ContentMetadataComponent', () => {
describe('Default input param values', () => {
it('should have editable input param as false by default', () => {
expect(component.editable).toBe(false);
expect(component.isEditingModeGeneralInfo).toBe(false);
});
it('should have displayEmpty input param as false by default', () => {
@ -272,7 +273,7 @@ describe('ContentMetadataComponent', () => {
}));
it('should call removeTag and assignTagsToNode on TagService on save click', fakeAsync(() => {
component.editable = true;
component.isEditingModeTags = true;
component.displayTags = true;
const property = { key: 'properties.property-key', value: 'original-value' } as CardViewBaseItemModel;
const expectedNode = { ...node, name: 'some-modified-value' };
@ -284,24 +285,26 @@ describe('ContentMetadataComponent', () => {
spyOn(tagService, 'assignTagsToNode').and.returnValue(EMPTY);
const tagName1 = tagPaging.list.entries[0].entry.tag;
const tagName2 = 'New tag 3';
component.isEditingModeTags = true;
component.readOnly = false;
updateService.update(property, 'updated-value');
tick(600);
fixture.detectChanges();
findTagsCreator().tagsChange.emit([tagName1, tagName2]);
clickOnSave();
const tag1 = new TagBody();
tag1.tag = tagName1;
const tag2 = new TagBody();
tag2.tag = tagName2;
fixture.detectChanges();
tick(600);
clickOnTagsSave();
tick(100);
const tag1 = new TagBody({ tag: tagName1 });
const tag2 = new TagBody({ tag: tagName2 });
expect(tagService.removeTag).toHaveBeenCalledWith(node.id, tagPaging.list.entries[1].entry.id);
expect(tagService.assignTagsToNode).toHaveBeenCalledWith(node.id, [tag1, tag2]);
discardPeriodicTasks();
flush();
}));
it('should call getTagsByNodeId on TagService on save click', fakeAsync(() => {
component.editable = true;
it('should call getTagsByNodeId on TagService on save click', () => {
component.isEditingModeTags = true;
component.displayTags = true;
const property = { key: 'properties.property-key', value: 'original-value' } as CardViewBaseItemModel;
const expectedNode = { ...node, name: 'some-modified-value' };
@ -313,25 +316,23 @@ describe('ContentMetadataComponent', () => {
spyOn(tagService, 'assignTagsToNode').and.returnValue(of({}));
updateService.update(property, 'updated-value');
tick(600);
fixture.detectChanges();
findTagsCreator().tagsChange.emit([tagPaging.list.entries[0].entry.tag, 'New tag 3']);
getTagsByNodeIdSpy.calls.reset();
clickOnSave();
component.onSaveTagsChanges();
expect(tagService.getTagsByNodeId).toHaveBeenCalledWith(node.id);
}));
});
it('should throw error on unsuccessful save', fakeAsync(() => {
const logService: LogService = TestBed.inject(LogService);
component.editable = true;
component.isEditingModeGeneralInfo = true;
component.readOnly = false;
const property = { key: 'properties.property-key', value: 'original-value' } as CardViewBaseItemModel;
updateService.update(property, 'updated-value');
tick(600);
const sub = contentMetadataService.error.subscribe((err) => {
expect(logService.error).toHaveBeenCalledWith(new Error('My bad'));
expect(err.statusCode).toBe(0);
expect(err.message).toBe('METADATA.ERRORS.GENERIC');
sub.unsubscribe();
@ -340,13 +341,14 @@ describe('ContentMetadataComponent', () => {
spyOn(nodesApiService, 'updateNode').and.returnValue(throwError(new Error('My bad')));
fixture.detectChanges();
fixture.whenStable().then(() => clickOnSave());
fixture.whenStable().then(() => clickOnGeneralInfoSave());
discardPeriodicTasks();
flush();
}));
it('should open the confirm dialog when content type is changed', fakeAsync(() => {
component.editable = true;
component.isEditingModeGeneralInfo = true;
component.readOnly = false;
const property = { key: 'nodeType', value: 'ft:sbiruli' } as CardViewBaseItemModel;
const expectedNode = { ...node, nodeType: 'ft:sbiruli' };
spyOn(contentMetadataService, 'openConfirmDialog').and.returnValue(of(true));
@ -357,17 +359,17 @@ describe('ContentMetadataComponent', () => {
fixture.detectChanges();
tick(100);
clickOnSave();
clickOnGeneralInfoSave();
tick(100);
expect(component.node).toEqual(expectedNode);
expect(contentMetadataService.openConfirmDialog).toHaveBeenCalledWith({ nodeType: 'ft:poppoli' });
expect(nodesApiService.updateNode).toHaveBeenCalled();
discardPeriodicTasks();
flush();
}));
it('should call removeTag and assignTagsToNode on TagService after confirming confirmation dialog when content type is changed', fakeAsync(() => {
component.editable = true;
component.displayTags = true;
const property = { key: 'nodeType', value: 'ft:sbiruli' } as CardViewBaseItemModel;
const expectedNode = { ...node, nodeType: 'ft:sbiruli' };
@ -380,7 +382,8 @@ describe('ContentMetadataComponent', () => {
spyOn(tagService, 'assignTagsToNode').and.returnValue(EMPTY);
const tagName1 = tagPaging.list.entries[0].entry.tag;
const tagName2 = 'New tag 3';
component.isEditingModeTags = true;
component.readOnly = false;
updateService.update(property, 'ft:poppoli');
tick(600);
@ -388,19 +391,22 @@ describe('ContentMetadataComponent', () => {
findTagsCreator().tagsChange.emit([tagName1, tagName2]);
tick(100);
fixture.detectChanges();
clickOnSave();
clickOnTagsSave();
tick(100);
const tag1 = new TagBody();
tag1.tag = tagName1;
const tag2 = new TagBody();
tag2.tag = tagName2;
const tag1 = new TagBody({ tag: tagName1 });
const tag2 = new TagBody({ tag: tagName2 });
expect(tagService.removeTag).toHaveBeenCalledWith(node.id, tagPaging.list.entries[1].entry.id);
expect(tagService.assignTagsToNode).toHaveBeenCalledWith(node.id, [tag1, tag2]);
discardPeriodicTasks();
flush();
}));
it('should retrigger the load of the properties when the content type has changed', fakeAsync(() => {
component.editable = true;
component.isEditingModeGeneralInfo = true;
component.readOnly = false;
const property = { key: 'nodeType', value: 'ft:sbiruli' } as CardViewBaseItemModel;
const expectedNode = Object.assign({}, node, { nodeType: 'ft:sbiruli' });
spyOn(contentMetadataService, 'openConfirmDialog').and.returnValue(of(true));
@ -412,21 +418,227 @@ describe('ContentMetadataComponent', () => {
fixture.detectChanges();
tick(100);
clickOnSave();
clickOnGeneralInfoSave();
tick(100);
expect(component.node).toEqual(expectedNode);
expect(updateService.updateNodeAspect).toHaveBeenCalledWith(expectedNode);
discardPeriodicTasks();
flush();
}));
});
describe('toggleEdit', () => {
let showErrorSpy: jasmine.Spy;
const mockGroup: CardViewGroup = {
editable: false,
expanded: false,
title: '',
properties: []
};
beforeEach(() => {
component.currentGroup = mockGroup;
showErrorSpy = spyOn(notificationService, 'showError').and.stub();
});
it('should toggle General Info editing mode', () => {
component.isEditingModeGeneralInfo = false;
component.onToggleGeneralInfoEdit();
expect(component.isEditingModeTags).toBe(false);
expect(component.isEditingModeCategories).toBe(false);
expect(component.currentGroup.editable).toBe(false);
});
it('should toggle Tags editing mode', () => {
component.isEditingModeTags = false;
component.onToggleTagsEdit();
expect(component.isTagPanelExpanded).toBe(component.isEditingModeTags);
expect(component.tagNameControlVisible).toBe(true);
expect(component.isEditingModeCategories).toBe(false);
expect(component.currentGroup.editable).toBe(false);
});
it('should toggle Categories editing mode', () => {
component.isEditingModeCategories = false;
component.onToggleCategoriesEdit();
expect(component.isCategoriesPanelExpanded).toBe(component.isEditingModeCategories);
expect(component.categoryControlVisible).toBe(true);
expect(component.isEditingModeTags).toBe(false);
expect(component.currentGroup.editable).toBe(false);
});
it('should toggle Group editing mode', () => {
component.onToggleGroupEdit(mockGroup);
expect(component.isEditingModeGeneralInfo).toBe(false);
expect(component.currentGroup).toBe(mockGroup);
});
it('should show Snackbar when Editing Panel is Active', () => {
spyOn(component, 'isEditingPanel').and.returnValue(true);
component.onToggleGeneralInfoEdit();
expect(component.isEditingPanel).toHaveBeenCalled();
expect(showErrorSpy).toHaveBeenCalledWith('METADATA.BASIC.SAVE_OR_DISCARD_CHANGES');
});
});
describe('toggleEditMode', () => {
it('should toggle general editable', () => {
component.isEditingModeGeneralInfo = false;
component.onToggleGeneralInfoEdit();
expect(component.isEditingModeGeneralInfo).toBe(true);
});
it('should toggle tags editable', () => {
component.isEditingModeTags = false;
component.onToggleTagsEdit();
expect(component.isEditingModeTags).toBe(true);
});
it('should toggle categories editable', () => {
component.isEditingModeCategories = false;
component.onToggleCategoriesEdit();
expect(component.isEditingModeCategories).toBe(true);
});
it('should toggle group editable', () => {
const group: CardViewGroup = {
editable: false,
expanded: false,
title: '',
properties: []
};
component.currentGroup = null;
component.onToggleGroupEdit(group);
expect(group.editable).toBe(true);
});
});
describe('Permission', () => {
beforeEach(() => {
component.readOnly = false;
component.node.allowableOperations = null;
component.ngOnInit();
});
it('should hide the general info edit button if node does not have `update` permissions', async () => {
fixture.detectChanges();
await fixture.whenStable();
expect(component.readOnly).toBeTrue();
expect(getToggleEditButton()).toBeNull();
});
it('should hide the tags edit button if node does not have `update` permissions', async () => {
fixture.detectChanges();
await fixture.whenStable();
expect(component.readOnly).toBeTrue();
expect(getTagsToggleEditButton()).toBeNull();
});
it('should hide the categories edit button if node does not have `update` permissions', async () => {
fixture.detectChanges();
await fixture.whenStable();
expect(component.readOnly).toBeTrue();
expect(getCategoriesToggleEditButton()).toBeNull();
});
it('should hide the groups edit button if node does not have `update` permissions', () => {
component.readOnly = false;
component.node.allowableOperations = null;
fixture.detectChanges();
expect(getGroupToggleEditButton()).toBeNull();
});
});
describe('hasToggleEdit', () => {
it('should return true when editable is false, readOnly is false, and hasAllowableOperations is true', () => {
component.isEditingModeGeneralInfo = false;
component.readOnly = false;
expect(component.canEditGeneralInfo).toBe(true);
});
it('should return false when editable is true', () => {
component.isEditingModeGeneralInfo = true;
component.readOnly = false;
fixture.detectChanges();
expect(component.canEditGeneralInfo).toBe(false);
expect(component.isEditingGeneralInfo).toBe(true);
});
});
describe('hasTagsToggleEdit', () => {
it('should have hasTagsToggleEdit property as expected', () => {
component.isEditingModeTags = false;
component.readOnly = false;
fixture.detectChanges();
expect(component.canEditTags).toBe(true);
});
it('should return false when editable is true', () => {
component.isEditingModeTags = true;
component.readOnly = false;
fixture.detectChanges();
expect(component.canEditTags).toBe(false);
expect(component.isEditingTags).toBe(true);
});
});
describe('hasGroupToggleEdit', () => {
it('should return true when group is not editable, not read-only', () => {
component.readOnly = false;
const group: CardViewGroup = {
title: 'Group Title',
properties: [],
expanded: true,
editable: false
};
const result = component.hasGroupToggleEdit(group);
expect(result).toBe(true);
});
it('should return true when group is editable, not read-only', () => {
component.readOnly = false;
const group: CardViewGroup = {
title: 'Group Title',
properties: [],
expanded: true,
editable: true
};
const result = component.isGroupToggleEditing(group);
expect(result).toBe(true);
});
});
describe('hasCategoriesToggleEdit', () => {
it('should have hasCategoriesToggleEdit property as expected', () => {
component.isEditingModeCategories = false;
component.readOnly = false;
expect(component.canEditCategories).toBe(true);
});
it('should return false when editable is true', () => {
component.isEditingModeCategories = true;
component.readOnly = false;
fixture.detectChanges();
expect(component.canEditCategories).toBe(false);
expect(component.isEditingCategories).toBe(true);
});
});
describe('Reseting', () => {
it('should reset properties on reset click', async () => {
component.changedProperties = { properties: { 'property-key': 'updated-value' } };
component.hasMetadataChanged = true;
component.tagNameControlVisible = true;
component.categoryControlVisible = true;
component.editable = true;
component.isEditingModeGeneralInfo = true;
component.readOnly = false;
const expectedNode = Object.assign({}, node, { name: 'some-modified-value' });
spyOn(nodesApiService, 'updateNode').and.returnValue(of(expectedNode));
@ -562,9 +774,7 @@ describe('ContentMetadataComponent', () => {
});
it('should hide card views group when the grouped properties are empty', async () => {
component.expanded = true;
spyOn(contentMetadataService, 'getGroupedProperties').and.returnValue(of([{ properties: [] } as any]));
spyOn(contentMetadataService, 'getGroupedProperties').and.stub();
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
@ -577,21 +787,7 @@ describe('ContentMetadataComponent', () => {
it('should display card views group when there is at least one property that is not empty', async () => {
component.expanded = true;
const cardViewGroup = {
title: 'Group 1',
properties: [
{
data: null,
default: null,
displayValue: 'DefaultName',
icon: '',
key: 'properties.cm:default',
label: 'To'
}
]
};
spyOn(contentMetadataService, 'getGroupedProperties').and.returnValue(of([{ properties: [cardViewGroup] } as any]));
spyOn(contentMetadataService, 'getGroupedProperties').and.stub();
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
@ -601,6 +797,33 @@ describe('ContentMetadataComponent', () => {
const basicPropertiesGroup = fixture.debugElement.query(By.css('.adf-metadata-grouped-properties-container mat-expansion-panel'));
expect(basicPropertiesGroup).toBeDefined();
});
it('should revert changes for general info panel on cancel', () => {
const spy = spyOn(contentMetadataService, 'getBasicProperties');
component.onCancelGeneralInfoEdit();
expect(spy).toHaveBeenCalled();
});
it('should revert changes for getGroupedProperties panel on cancel', () => {
spyOn(contentMetadataService, 'getGroupedProperties');
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
component.onCancelGroupEdit({} as CardViewGroup);
expect(contentMetadataService.getGroupedProperties).toHaveBeenCalledWith(expectedNode, 'custom-preset');
});
it('should revert changes for categories panel on cancel', () => {
const spy = spyOn(categoryService, 'getCategoryLinksForNode').and.returnValue(of(categoryPagingResponse));
component.displayCategories = true;
component.onCancelCategoriesEdit();
expect(spy).toHaveBeenCalledWith(expectedNode.id);
});
it('should revert changes for tags panel on cancel', () => {
const spy = spyOn(tagService, 'getTagsByNodeId').and.returnValue(of(mockTagPaging()));
component.displayTags = true;
component.onCancelTagsEdit();
expect(spy).toHaveBeenCalledWith(expectedNode.id);
});
});
describe('Properties displaying', () => {
@ -725,7 +948,7 @@ describe('ContentMetadataComponent', () => {
'cm:versionable': '*'
});
spyOn(classesApi, 'getClass').and.returnValue(Promise.resolve(verResponse));
getClassSpy.and.returnValue(Promise.resolve(verResponse));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
fixture.detectChanges();
@ -745,7 +968,7 @@ describe('ContentMetadataComponent', () => {
'cm:versionable': '*'
});
spyOn(classesApi, 'getClass').and.returnValue(Promise.resolve(verResponse));
getClassSpy.and.returnValue(Promise.resolve(verResponse));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
fixture.detectChanges();
@ -765,7 +988,7 @@ describe('ContentMetadataComponent', () => {
exclude: 'cm:versionable'
});
spyOn(classesApi, 'getClass').and.returnValue(Promise.resolve(verResponse));
getClassSpy.and.returnValue(Promise.resolve(verResponse));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
fixture.detectChanges();
@ -786,7 +1009,7 @@ describe('ContentMetadataComponent', () => {
'cm:versionable': '*'
});
spyOn(classesApi, 'getClass').and.returnValue(Promise.resolve(verResponse));
getClassSpy.and.returnValue(Promise.resolve(verResponse));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
fixture.detectChanges();
@ -806,7 +1029,7 @@ describe('ContentMetadataComponent', () => {
exclude: ['cm:versionable', 'cm:auditable']
});
spyOn(classesApi, 'getClass').and.returnValue(Promise.resolve(verResponse));
getClassSpy.and.returnValue(Promise.resolve(verResponse));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
fixture.detectChanges();
@ -830,7 +1053,7 @@ describe('ContentMetadataComponent', () => {
'exif:exif': ['exif:pixelXDimension', 'exif:pixelYDimension']
});
spyOn(classesApi, 'getClass').and.returnValue(Promise.resolve(exifResponse));
getClassSpy.and.returnValue(Promise.resolve(exifResponse));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
fixture.detectChanges();
@ -864,7 +1087,7 @@ describe('ContentMetadataComponent', () => {
'exif:exif': ['exif:pixelXDimension', 'exif:pixelYDimension']
});
spyOn(classesApi, 'getClass').and.returnValue(Promise.resolve(exifResponse));
getClassSpy.and.returnValue(Promise.resolve(exifResponse));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
fixture.detectChanges();
@ -880,28 +1103,21 @@ describe('ContentMetadataComponent', () => {
});
describe('Expand the panel', () => {
let expectedNode: Node;
beforeEach(() => {
expectedNode = { ...node, name: 'some-modified-value' };
spyOn(contentMetadataService, 'getGroupedProperties').and.returnValue(of(mockGroupProperties));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
component.isGeneralPanelExpanded = false;
});
it('should open and update drawer with expand section dynamically', async () => {
component.displayEmpty = true;
component.displayAspect = 'EXIF';
component.expanded = true;
component.displayEmpty = true;
fixture.detectChanges();
await fixture.whenStable();
let defaultProp = queryDom(fixture);
let exifProp = queryDom(fixture, 'EXIF');
let customProp = queryDom(fixture, 'CUSTOM');
expect(defaultProp.componentInstance.expanded).toBeFalsy();
expect(exifProp.componentInstance.expanded).toBeTruthy();
expect(customProp.componentInstance.expanded).toBeFalsy();
component.displayAspect = 'CUSTOM';
@ -909,23 +1125,7 @@ describe('ContentMetadataComponent', () => {
await fixture.whenStable();
defaultProp = queryDom(fixture);
exifProp = queryDom(fixture, 'EXIF');
customProp = queryDom(fixture, 'CUSTOM');
expect(defaultProp.componentInstance.expanded).toBeFalsy();
expect(exifProp.componentInstance.expanded).toBeFalsy();
expect(customProp.componentInstance.expanded).toBeTruthy();
component.displayAspect = 'Properties';
fixture.detectChanges();
await fixture.whenStable();
defaultProp = queryDom(fixture);
exifProp = queryDom(fixture, 'EXIF');
customProp = queryDom(fixture, 'CUSTOM');
expect(defaultProp.componentInstance.expanded).toBeTruthy();
expect(exifProp.componentInstance.expanded).toBeFalsy();
expect(customProp.componentInstance.expanded).toBeFalsy();
});
it('should not expand anything if input is wrong', async () => {
@ -937,11 +1137,16 @@ describe('ContentMetadataComponent', () => {
await fixture.whenStable();
const defaultProp = queryDom(fixture);
const exifProp = queryDom(fixture, 'EXIF');
const customProp = queryDom(fixture, 'CUSTOM');
expect(defaultProp.componentInstance.expanded).toBeFalsy();
expect(exifProp.componentInstance.expanded).toBeFalsy();
expect(customProp.componentInstance.expanded).toBeFalsy();
});
it('should expand the section when displayAspect set as Properties', async () => {
component.displayAspect = 'Properties';
component.ngOnInit();
fixture.detectChanges();
expect(component.isGeneralPanelExpanded).toBeTruthy();
});
});
@ -1053,7 +1258,8 @@ describe('ContentMetadataComponent', () => {
});
it('should render tags after loading tags after clicking on Cancel button', fakeAsync(() => {
component.editable = true;
component.isEditingModeTags = true;
component.readOnly = false;
fixture.detectChanges();
TestBed.inject(CardViewContentUpdateService).itemUpdated$.next({
changed: {}
@ -1061,9 +1267,8 @@ describe('ContentMetadataComponent', () => {
tick(500);
fixture.detectChanges();
spyOn(tagService, 'getTagsByNodeId').and.returnValue(of(tagPaging));
clickOnCancel();
component.editable = false;
findCancelTagsButton().click();
component.isEditingModeTags = false;
fixture.detectChanges();
const tagElements = findTagElements();
expect(tagElements).toHaveSize(2);
@ -1077,7 +1282,7 @@ describe('ContentMetadataComponent', () => {
component.ngOnInit();
fixture.detectChanges();
component.editable = true;
component.isEditingModeTags = true;
fixture.detectChanges();
expect(findTagElements()).toHaveSize(0);
});
@ -1087,7 +1292,7 @@ describe('ContentMetadataComponent', () => {
let tagsCreator: TagsCreatorComponent;
beforeEach(() => {
component.editable = true;
component.isEditingModeTags = true;
component.displayTags = true;
fixture.detectChanges();
tagsCreator = findTagsCreator();
@ -1097,51 +1302,29 @@ describe('ContentMetadataComponent', () => {
expect(tagsCreator.tagNameControlVisible).toBeFalse();
});
it('should hide showing tag input button after emitting tagNameControlVisibleChange event with true', () => {
tagsCreator.tagNameControlVisibleChange.emit(true);
fixture.detectChanges();
expect(findShowingTagInputButton().hasAttribute('hidden')).toBeTrue();
});
it('should show showing tag input button after emitting tagNameControlVisibleChange event with false', fakeAsync(() => {
tagsCreator.tagNameControlVisibleChange.emit(true);
fixture.detectChanges();
tick();
tagsCreator.tagNameControlVisibleChange.emit(false);
fixture.detectChanges();
tick(100);
expect(findShowingTagInputButton().hasAttribute('hidden')).toBeFalse();
}));
it('should have assigned correct mode', () => {
it('should load in create and assign mode by default', () => {
expect(tagsCreator.mode).toBe(TagsCreatorMode.CREATE_AND_ASSIGN);
});
it('should enable cancel button after emitting tagsChange event', () => {
component.readOnly = false;
tagsCreator.tagsChange.emit(['New tag 1', 'New tag 2', 'New tag 3']);
fixture.detectChanges();
expect(findCancelButton().disabled).toBeFalse();
expect(findCancelTagsButton().disabled).toBeFalse();
});
it('should enable save button after emitting tagsChange event', () => {
tagsCreator.tagsChange.emit(['New tag 1', 'New tag 2', 'New tag 3']);
component.readOnly = false;
fixture.detectChanges();
expect(findSaveButton().disabled).toBeFalse();
expect(findSaveTagsButton().disabled).toBeFalse();
});
it('should have assigned false to disabledTagsRemoving', () => {
expect(tagsCreator.disabledTagsRemoving).toBeFalse();
});
it('should have assigned true to disabledTagsRemoving after clicking on update button', () => {
tagsCreator.tagsChange.emit([]);
fixture.detectChanges();
clickOnSave();
expect(tagsCreator.disabledTagsRemoving).toBeTrue();
});
it('should have assigned false to disabledTagsRemoving if forkJoin fails', fakeAsync(() => {
it('should have assigned false to disabledTagsRemoving if forkJoin fails', () => {
const property = { key: 'properties.property-key', value: 'original-value' } as CardViewBaseItemModel;
const expectedNode = { ...node, name: 'some-modified-value' };
spyOn(nodesApiService, 'updateNode').and.returnValue(of(expectedNode));
@ -1152,24 +1335,15 @@ describe('ContentMetadataComponent', () => {
spyOn(tagService, 'assignTagsToNode').and.returnValue(EMPTY);
const tagName1 = tagPaging.list.entries[0].entry.tag;
const tagName2 = 'New tag 3';
component.isEditingModeTags = true;
component.readOnly = false;
updateService.update(property, 'updated-value');
tick(600);
fixture.detectChanges();
tagsCreator.tagsChange.emit([tagName1, tagName2]);
clickOnSave();
clickOnTagsSave();
expect(tagsCreator.disabledTagsRemoving).toBeFalse();
}));
it('should have assigned false to tagNameControlVisible after clicking on update button', () => {
tagsCreator.tagNameControlVisibleChange.emit(true);
tagsCreator.tagsChange.emit([]);
fixture.detectChanges();
clickOnSave();
expect(tagsCreator.tagNameControlVisible).toBeFalse();
});
describe('Setting tags', () => {
@ -1199,7 +1373,7 @@ describe('ContentMetadataComponent', () => {
});
it('should show tags creator if editable is true and displayTags is true', () => {
component.editable = true;
component.isEditingModeTags = true;
component.displayTags = true;
fixture.detectChanges();
expect(findTagsCreator()).toBeDefined();
@ -1263,7 +1437,8 @@ describe('ContentMetadataComponent', () => {
});
it('should render categories after discard changes button is clicked', fakeAsync(() => {
component.editable = true;
component.isEditingModeCategories = true;
component.readOnly = false;
fixture.detectChanges();
TestBed.inject(CardViewContentUpdateService).itemUpdated$.next({
changed: {}
@ -1272,7 +1447,7 @@ describe('ContentMetadataComponent', () => {
fixture.detectChanges();
clickOnCancel();
component.editable = false;
component.isEditingModeGeneralInfo = false;
fixture.detectChanges();
const categories = getCategories();
@ -1285,7 +1460,7 @@ describe('ContentMetadataComponent', () => {
}));
it('should be hidden when editable is true', () => {
component.editable = true;
component.isEditingModeCategories = true;
fixture.detectChanges();
expect(getCategories().length).toBe(0);
});
@ -1295,7 +1470,7 @@ describe('ContentMetadataComponent', () => {
let categoriesManagementComponent: CategoriesManagementComponent;
beforeEach(() => {
component.editable = true;
component.isEditingModeCategories = true;
component.displayCategories = true;
component.node.aspectNames.push('generalclassifiable');
spyOn(categoryService, 'getCategoryLinksForNode').and.returnValue(of(categoryPagingResponse));
@ -1307,23 +1482,7 @@ describe('ContentMetadataComponent', () => {
expect(categoriesManagementComponent.categoryNameControlVisible).toBeFalse();
});
it('should hide assign categories button when categoryNameControlVisible changes to true', () => {
categoriesManagementComponent.categoryNameControlVisibleChange.emit(true);
fixture.detectChanges();
expect(getAssignCategoriesBtn().hasAttribute('hidden')).toBeTrue();
});
it('should show assign categories button when categoryNameControlVisible changes to false', fakeAsync(() => {
categoriesManagementComponent.categoryNameControlVisibleChange.emit(true);
fixture.detectChanges();
tick();
categoriesManagementComponent.categoryNameControlVisibleChange.emit(false);
fixture.detectChanges();
tick(100);
expect(getAssignCategoriesBtn().hasAttribute('hidden')).toBeFalse();
}));
it('should have correct mode', () => {
it('should load with assign mode by default', () => {
expect(categoriesManagementComponent.managementMode).toBe(CategoriesManagementMode.ASSIGN);
});
@ -1339,24 +1498,17 @@ describe('ContentMetadataComponent', () => {
it('should enable discard and save buttons after emitting categories change event', () => {
categoriesManagementComponent.categoriesChange.emit([category1, category2]);
component.readOnly =false;
fixture.detectChanges();
expect(findCancelButton().disabled).toBeFalse();
expect(findSaveButton().disabled).toBeFalse();
expect(findSaveCategoriesButton().disabled).toBeFalse();
});
it('should not disable removal initially', () => {
expect(categoriesManagementComponent.disableRemoval).toBeFalse();
});
it('should disable removal on saving', () => {
categoriesManagementComponent.categoriesChange.emit([]);
fixture.detectChanges();
clickOnSave();
expect(categoriesManagementComponent.disableRemoval).toBeTrue();
});
it('should not disable removal if forkJoin fails', fakeAsync(() => {
it('should not disable removal if forkJoin fails', () => {
const property = { key: 'properties.property-key', value: 'original-value' } as CardViewBaseItemModel;
const expectedNode = { ...node, name: 'some-modified-value' };
spyOn(nodesApiService, 'updateNode').and.returnValue(of(expectedNode));
@ -1367,24 +1519,14 @@ describe('ContentMetadataComponent', () => {
spyOn(categoryService, 'linkNodeToCategory').and.returnValue(throwError({}));
updateService.update(property, 'updated-value');
tick(600);
component.readOnly = false;
component.isEditingModeCategories = true;
fixture.detectChanges();
categoriesManagementComponent.categoriesChange.emit([category1, category2]);
clickOnSave();
findSaveCategoriesButton();
expect(categoriesManagementComponent.disableRemoval).toBeFalse();
discardPeriodicTasks();
flush();
}));
it('should set categoryNameControlVisible to false after saving', () => {
categoriesManagementComponent.categoryNameControlVisibleChange.emit(true);
categoriesManagementComponent.categoriesChange.emit([]);
fixture.detectChanges();
clickOnSave();
expect(categoriesManagementComponent.categoryNameControlVisible).toBeFalse();
});
describe('Setting categories', () => {
@ -1410,7 +1552,7 @@ describe('ContentMetadataComponent', () => {
it('should render correct custom panel with title and component', () => {
component.customPanels = [{ panelTitle: 'testTitle', component: 'testComponent' }];
fixture.detectChanges();
const panelTitle = fixture.debugElement.queryAll(By.css('mat-panel-title'))[1].nativeElement;
const panelTitle = fixture.debugElement.query(By.css('.adf-metadata-custom-panel-title .adf-metadata-properties-title')).nativeElement;
const customComponent = fixture.debugElement.query(By.css('adf-dynamic-component')).nativeElement;
expect(panelTitle.innerText).toEqual('testTitle');
expect(customComponent).toBeDefined();

View File

@ -15,23 +15,22 @@
* limitations under the License.
*/
import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core';
import {
Category,
CategoryEntry,
CategoryLinkBody,
CategoryPaging,
Node,
TagBody,
TagEntry,
TagPaging
} from '@alfresco/js-api';
Component,
Input,
OnChanges,
OnDestroy,
OnInit,
SimpleChanges,
ViewEncapsulation
} from '@angular/core';
import { Category, CategoryEntry, CategoryLinkBody, CategoryPaging, Node, TagBody, TagEntry, TagPaging } from '@alfresco/js-api';
import { forkJoin, Observable, of, Subject, zip } from 'rxjs';
import {
AppConfigService,
CardViewBaseItemModel,
CardViewItem,
LogService,
NotificationService,
TranslationService,
UpdateNotification
} from '@alfresco/adf-core';
@ -44,6 +43,8 @@ import { TagsCreatorMode } from '../../../tag/tags-creator/tags-creator-mode';
import { TagService } from '../../../tag/services/tag.service';
import { CategoryService } from '../../../category/services/category.service';
import { CategoriesManagementMode } from '../../../category/categories-management/categories-management-mode';
import { AllowableOperationsEnum } from '../../../common/models/allowable-operations.enum';
import { ContentService } from '../../../common/services/content.service';
const DEFAULT_SEPARATOR = ', ';
@ -61,17 +62,6 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
@Input()
node: Node;
/** Toggles whether the edit button should be shown */
@Input()
set editable(editable: boolean) {
this._editable = editable;
this._assignedTags = [...this.tags];
}
get editable(): boolean {
return this._editable;
}
/** Toggles whether to display empty values in the card view */
@Input()
displayEmpty: boolean = false;
@ -119,9 +109,15 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
@Input()
customPanels: ContentMetadataCustomPanel[] = [];
/**
* (optional) This flag sets the metadata in read-only mode,
* preventing changes.
*/
@Input()
readOnly = false;
private _assignedTags: string[] = [];
private assignedTagsEntries: TagEntry[] = [];
private _editable = false;
private _tagsCreatorMode = TagsCreatorMode.CREATE_AND_ASSIGN;
private _tags: string[] = [];
private targetProperty: CardViewBaseItemModel;
@ -140,16 +136,25 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
categoriesManagementMode = CategoriesManagementMode.ASSIGN;
categoryControlVisible = false;
classifiableChanged = this.classifiableChangedSubject.asObservable();
isGeneralPanelExpanded = true;
isTagPanelExpanded: boolean;
isCategoriesPanelExpanded: boolean;
currentGroup: CardViewGroup;
isEditingModeGeneralInfo = false;
isEditingModeTags = false;
isEditingModeCategories = false;
constructor(
private contentMetadataService: ContentMetadataService,
private cardViewContentUpdateService: CardViewContentUpdateService,
private nodesApiService: NodesApiService,
private logService: LogService,
private translationService: TranslationService,
private appConfig: AppConfigService,
private tagService: TagService,
private categoryService: CategoryService
private categoryService: CategoryService,
private contentService: ContentService,
private notificationService: NotificationService
) {
this.copyToClipboardAction = this.appConfig.get<boolean>('content-metadata.copy-to-clipboard-action');
this.multiValueSeparator = this.appConfig.get<string>('content-metadata.multi-value-pipe-separator') || DEFAULT_SEPARATOR;
@ -158,23 +163,32 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
ngOnInit() {
this.cardViewContentUpdateService.itemUpdated$
.pipe(
debounceTime(500),
takeUntil(this.onDestroy$))
.subscribe(
(updatedNode: UpdateNotification) => {
this.hasMetadataChanged = true;
this.targetProperty = updatedNode.target;
this.updateChanges(updatedNode.changed);
}
);
.pipe(debounceTime(500), takeUntil(this.onDestroy$))
.subscribe((updatedNode: UpdateNotification) => {
this.hasMetadataChanged = true;
this.targetProperty = updatedNode.target;
this.updateChanges(updatedNode.changed);
});
this.cardViewContentUpdateService.updatedAspect$.pipe(
debounceTime(500),
takeUntil(this.onDestroy$))
.subscribe((node) => this.loadProperties(node));
this.cardViewContentUpdateService.updatedAspect$
.pipe(debounceTime(500), takeUntil(this.onDestroy$))
.subscribe((node) => {
this.node.aspectNames = node?.aspectNames;
this.loadProperties(node);
});
this.loadProperties(this.node);
this.verifyAllowableOperations();
if (this.displayAspect === 'Properties') {
this.isGeneralPanelExpanded = true;
}
}
private verifyAllowableOperations() {
if (!this.node?.allowableOperations || !this.contentService.hasAllowableOperations(this.node, AllowableOperationsEnum.UPDATE)) {
this.readOnly = true;
}
}
get assignedTags(): string[] {
@ -194,14 +208,11 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
}
protected handleUpdateError(error: Error) {
this.logService.error(error);
let statusCode = 0;
try {
statusCode = JSON.parse(error.message).error.statusCode;
} catch {
}
} catch {}
let message = `METADATA.ERRORS.${statusCode}`;
@ -219,6 +230,10 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
if (changes.node && !changes.node.firstChange) {
this.loadProperties(changes.node.currentValue);
}
if(changes?.readOnly && changes?.readOnly?.currentValue) {
this.cancelEditing();
this.groupedProperties$ = this.contentMetadataService.getGroupedProperties(this.node, this.preset);
}
}
ngOnDestroy() {
@ -239,14 +254,36 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
});
}
/**
* Called after clicking save button. It confirms all changes done for metadata and hides both category and tag name controls.
* Before clicking on that button they are not saved.
*/
saveChanges() {
private onSave(event?: MouseEvent) {
event?.stopPropagation();
this.onSaveChanges();
this.cancelEditing();
}
onSaveGeneralInfoChanges(event?: MouseEvent) {
this.onSave(event);
}
onSaveTagsChanges(event?: MouseEvent) {
this.onSave(event);
}
onSaveCategoriesChanges(event?: MouseEvent) {
this.onSave(event);
}
onSaveGroupChanges(group: CardViewGroup, event?: MouseEvent) {
this.onSave(event);
group.editable = false;
}
private onSaveChanges() {
this._saving = true;
this.tagNameControlVisible = false;
this.categoryControlVisible = false;
if (this.hasContentTypeChanged(this.changedProperties)) {
this.contentMetadataService.openConfirmDialog(this.changedProperties).subscribe(() => {
this.updateNode();
@ -285,9 +322,168 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
this.categoryControlVisible = false;
}
cancelChanges() {
// Returns the editing state of the panel
isEditingPanel(): boolean {
return (
(this.isEditingModeGeneralInfo || this.isEditingModeTags || this.isEditingModeCategories || this.currentGroup?.editable) && this.hasMetadataChanged
);
}
onToggleGeneralInfoEdit(event?: MouseEvent) {
event?.stopPropagation();
if (this.isEditingPanel()) {
this.notificationService.showError('METADATA.BASIC.SAVE_OR_DISCARD_CHANGES');
return;
}
const currentMode = this.isEditingModeGeneralInfo;
this.cancelEditing();
this.isEditingModeGeneralInfo = !currentMode;
this.isGeneralPanelExpanded = true;
this.groupedProperties$ = this.contentMetadataService.getGroupedProperties(this.node, this.preset);
}
onToggleTagsEdit(event?: MouseEvent) {
event?.stopPropagation();
if (this.isEditingPanel()) {
this.notificationService.showError('METADATA.BASIC.SAVE_OR_DISCARD_CHANGES');
return;
}
const currentValue = this.isEditingModeTags;
this.cancelEditing();
this.isEditingModeTags = !currentValue;
this.isTagPanelExpanded = this.isEditingModeTags;
this.tagNameControlVisible = true;
this.groupedProperties$ = this.contentMetadataService.getGroupedProperties(this.node, this.preset);
}
private cancelEditing() {
this.isEditingModeGeneralInfo = false;
this.isEditingModeCategories = false;
this.isEditingModeTags = false;
}
onCancelGeneralInfoEdit(event?: MouseEvent) {
event?.stopPropagation();
this.cancelEditing();
this.revertChanges();
this.loadProperties(this.node);
this.basicProperties$ = this.getProperties(this.node);
}
onCancelCategoriesEdit(event?: MouseEvent) {
event?.stopPropagation();
this.cancelEditing();
this.revertChanges();
this.loadCategoriesForNode(this.node.id);
const aspectNames = this.node.aspectNames || [];
if (!aspectNames.includes('generalclassifiable')) {
this.categories = [];
this.classifiableChangedSubject.next();
}
}
onCancelTagsEdit(event?: MouseEvent) {
event?.stopPropagation();
this.cancelEditing();
this.revertChanges();
this.basicProperties$ = this.getProperties(this.node);
this.loadTagsForNode(this.node.id);
}
onCancelGroupEdit(group: CardViewGroup, event?: MouseEvent) {
event?.stopPropagation();
this.cancelEditing();
this.revertChanges();
group.editable = false;
this.groupedProperties$ = this.contentMetadataService.getGroupedProperties(this.node, this.preset);
}
onToggleCategoriesEdit(event?: MouseEvent) {
event?.stopPropagation();
if (this.isEditingPanel()) {
this.notificationService.showError('METADATA.BASIC.SAVE_OR_DISCARD_CHANGES');
return;
}
const currentValue = this.isEditingModeCategories;
this.cancelEditing();
this.isEditingModeCategories = !currentValue;
this.isCategoriesPanelExpanded = this.isEditingModeCategories;
this.categoryControlVisible = true;
this.groupedProperties$ = this.contentMetadataService.getGroupedProperties(this.node, this.preset);
}
onToggleGroupEdit(group: CardViewGroup, event?: MouseEvent) {
event?.stopPropagation();
if (this.isEditingPanel()) {
this.notificationService.showError('METADATA.BASIC.SAVE_OR_DISCARD_CHANGES');
return;
}
this.cancelEditing();
if (this.currentGroup && this.currentGroup.title !== group.title) {
this.currentGroup.editable = false;
}
group.editable = !group.editable;
this.currentGroup = group.editable ? group : null;
if (group.editable) {
group.expanded = true;
}
}
get showEmptyTagMessage(): boolean {
return this.tags?.length === 0 && !this.isEditingModeTags;
}
get showEmptyCategoryMessage(): boolean {
return this.categories?.length === 0 && !this.isEditingModeCategories;
}
get canEditGeneralInfo(): boolean {
return !this.isEditingModeGeneralInfo && !this.readOnly;
}
get isEditingGeneralInfo(): boolean {
return this.isEditingModeGeneralInfo && !this.readOnly;
}
get canEditTags(): boolean {
return !this.isEditingModeTags && !this.readOnly;
}
get isEditingTags(): boolean {
return this.isEditingModeTags && !this.readOnly;
}
get canEditCategories(): boolean {
return !this.isEditingModeCategories && !this.readOnly;
}
get isEditingCategories(): boolean {
return this.isEditingModeCategories && !this.readOnly;
}
hasGroupToggleEdit(group: CardViewGroup): boolean {
return !group.editable && !this.readOnly;
}
isGroupToggleEditing(group: CardViewGroup): boolean {
return group.editable && !this.readOnly;
}
showGroup(group: CardViewGroup): boolean {
@ -300,10 +496,6 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
return groupTitle === this.displayAspect;
}
canExpandProperties(): boolean {
return !this.expanded || this.displayAspect === 'Properties';
}
keyDown(event: KeyboardEvent) {
if (event.keyCode === 37 || event.keyCode === 39) { // ArrowLeft && ArrowRight
event.stopPropagation();
@ -315,13 +507,15 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
updatedNode: this.nodesApiService.updateNode(this.node.id, this.changedProperties),
...(this.displayTags ? this.saveTags() : {}),
...(this.displayCategories ? this.saveCategories() : {})
}).pipe(
catchError((err) => {
this.cardViewContentUpdateService.updateElement(this.targetProperty);
this.handleUpdateError(err);
this._saving = false;
return of(null);
}))
})
.pipe(
catchError((err) => {
this.cardViewContentUpdateService.updateElement(this.targetProperty);
this.handleUpdateError(err);
this._saving = false;
return of(null);
})
)
.subscribe((result: any) => {
if (result) {
this.updateUndefinedNodeProperties(result.updatedNode);
@ -335,9 +529,9 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
this.loadTagsForNode(this.node.id);
}
if (this.displayCategories && !!result.LinkingCategories) {
this.assignedCategories = result.LinkingCategories.list ?
result.LinkingCategories.list.entries.map((entry: CategoryEntry) => entry.entry) :
[result.LinkingCategories.entry];
this.assignedCategories = result.LinkingCategories.list
? result.LinkingCategories.list.entries.map((entry: CategoryEntry) => entry.entry)
: [result.LinkingCategories.entry];
}
}
this._saving = false;
@ -358,12 +552,16 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
if (node) {
this.basicProperties$ = this.getProperties(node);
this.groupedProperties$ = this.contentMetadataService.getGroupedProperties(node, this.preset);
if (this.displayTags) {
this.loadTagsForNode(node.id);
}
if (this.displayCategories) {
this.loadCategoriesForNode(node.id);
if (!this.node.aspectNames.includes('generalclassifiable')) {
const aspectNames = node.aspectNames || [];
if (!aspectNames.includes('generalclassifiable')) {
this.categories = [];
this.classifiableChangedSubject.next();
}
@ -374,11 +572,14 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
private getProperties(node: Node) {
const properties$ = this.contentMetadataService.getBasicProperties(node);
const contentTypeProperty$ = this.contentMetadataService.getContentTypeProperty(node);
return zip(properties$, contentTypeProperty$)
.pipe(map(([properties, contentTypeProperty]) => {
const filteredProperties = contentTypeProperty.filter((property) => properties.findIndex((baseProperty) => baseProperty.key === property.key) === -1);
return zip(properties$, contentTypeProperty$).pipe(
map(([properties, contentTypeProperty]) => {
const filteredProperties = contentTypeProperty.filter(
(property) => properties.findIndex((baseProperty) => baseProperty.key === property.key) === -1
);
return [...properties, ...filteredProperties];
}));
})
);
}
private isEmpty(value: any): boolean {
@ -434,11 +635,14 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
}
});
if (this.tags.length) {
observables.tagsAssigning = this.tagService.assignTagsToNode(this.node.id, this.tags.map((tag) => {
const tagBody = new TagBody();
tagBody.tag = tag;
return tagBody;
}));
observables.tagsAssigning = this.tagService.assignTagsToNode(
this.node.id,
this.tags.map((tag) => {
const tagBody = new TagBody();
tagBody.tag = tag;
return tagBody;
})
);
}
}
return observables;

View File

@ -49,7 +49,9 @@ export const mockGroupProperties = [
clickCallBack: null,
displayValue: 400
}
]
],
editable: true,
expanded: true
},
{
title: 'CUSTOM',
@ -69,6 +71,8 @@ export const mockGroupProperties = [
clickCallBack: null,
displayValue: 400
}
]
],
editable: true,
expanded: true
}
];

View File

@ -24,6 +24,7 @@ import { ContentMetadataCardComponent } from './components/content-metadata-card
import { TagModule } from '../tag/tag.module';
import { CategoriesModule } from '../category/category.module';
import { ExtensionsModule } from '@alfresco/adf-extensions';
import { ContentMetadataHeaderComponent } from './components/content-metadata/content-metadata-header.component';
@NgModule({
imports: [
@ -32,11 +33,13 @@ import { ExtensionsModule } from '@alfresco/adf-extensions';
CoreModule,
TagModule,
CategoriesModule,
ExtensionsModule
ExtensionsModule,
ContentMetadataHeaderComponent
],
exports: [
ContentMetadataComponent,
ContentMetadataCardComponent
ContentMetadataCardComponent,
ContentMetadataHeaderComponent
],
declarations: [
ContentMetadataComponent,

View File

@ -20,4 +20,6 @@ import { CardViewItem } from '@alfresco/adf-core';
export interface CardViewGroup {
title: string;
properties: CardViewItem[];
editable?: boolean;
expanded?: boolean;
}

View File

@ -18,4 +18,5 @@
export interface ContentMetadataCustomPanel {
panelTitle: string;
component: string;
expanded?: boolean;
}

View File

@ -16,14 +16,15 @@
*/
export * from './components/content-metadata/content-metadata.component';
export * from './components/content-metadata/content-metadata-header.component';
export * from './components/content-metadata-card/content-metadata-card.component';
export * from './services/basic-properties.service';
export * from './services/content-metadata.service';
export * from './services/property-descriptors.service';
export * from './services/property-groups-translator.service';
export * from './services/config/content-metadata-config.factory';
export * from './services/content-type-property.service';
export * from './services/config/indifferent-config.service';
export * from './services/config/layout-oriented-config.service';
export * from './services/config/aspect-oriented-config.service';

View File

@ -25,7 +25,7 @@ import {
CardViewDateItemModel,
CardViewIntItemModel,
CardViewFloatItemModel,
LogService,
NotificationService,
CardViewBoolItemModel,
CardViewDatetimeItemModel,
CardViewSelectItemModel,
@ -42,7 +42,7 @@ describe('PropertyGroupTranslatorService', () => {
let propertyGroup: OrganisedPropertyGroup;
let property: Property;
let propertyValues: { [key: string]: any };
let logService: LogService;
let notificationService: NotificationService;
beforeEach(() => {
TestBed.configureTestingModule({
@ -51,7 +51,7 @@ describe('PropertyGroupTranslatorService', () => {
ContentTestingModule
]
});
logService = TestBed.inject(LogService);
notificationService = TestBed.inject(NotificationService);
service = TestBed.inject(PropertyGroupTranslatorService);
property = {
@ -137,7 +137,7 @@ describe('PropertyGroupTranslatorService', () => {
});
it('should log an error if unrecognised type is found', () => {
spyOn(logService, 'error').and.stub();
const showError = spyOn(notificationService, 'showError').and.stub();
property.name = 'FAS:PLAGUE';
property.title = 'The Faro Plague';
@ -149,7 +149,7 @@ describe('PropertyGroupTranslatorService', () => {
propertyGroups.push(Object.assign({}, propertyGroup));
service.translateToCardViewGroups(propertyGroups, propertyValues, null);
expect(logService.error).toHaveBeenCalledWith('Unknown type for mapping: daemonic:scorcher');
expect(showError).toHaveBeenCalledWith('Unknown type for mapping: daemonic:scorcher');
});
it('should fall back to single-line property type if unrecognised type is found', () => {

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { Injectable, inject } from '@angular/core';
import {
CardViewItemProperties,
CardViewItem,
@ -26,10 +26,10 @@ import {
CardViewDatetimeItemModel,
CardViewIntItemModel,
CardViewFloatItemModel,
LogService,
MultiValuePipe,
AppConfigService,
DecimalNumberPipe
DecimalNumberPipe,
NotificationService
} from '@alfresco/adf-core';
import { Property, CardViewGroup, OrganisedPropertyGroup } from '../interfaces/content-metadata.interfaces';
import { of } from 'rxjs';
@ -51,10 +51,10 @@ export const RECOGNISED_ECM_TYPES = [D_TEXT, D_MLTEXT, D_DATE, D_DATETIME, D_INT
providedIn: 'root'
})
export class PropertyGroupTranslatorService {
private notificationService = inject(NotificationService);
valueSeparator: string;
constructor(private logService: LogService,
private multiValuePipe: MultiValuePipe,
constructor(private multiValuePipe: MultiValuePipe,
private decimalNumberPipe: DecimalNumberPipe,
private appConfig: AppConfigService) {
this.valueSeparator = this.appConfig.get<string>('content-metadata.multi-value-pipe-separator');
@ -187,7 +187,7 @@ export class PropertyGroupTranslatorService {
private checkECMTypeValidity(ecmPropertyType: string) {
if (RECOGNISED_ECM_TYPES.indexOf(ecmPropertyType) === -1) {
this.logService.error(`Unknown type for mapping: ${ecmPropertyType}`);
this.notificationService.showError(`Unknown type for mapping: ${ecmPropertyType}`);
}
}

View File

@ -148,7 +148,7 @@
"NO_EXISTING_TAGS": "No Existing Tags",
"TITLE": "Create Tags",
"CREATE_TAG": "Create: {{tag}}",
"NAME": "Name",
"TAG_SEARCH_PLACEHOLDER": "Search Tags",
"ERRORS": {
"EXISTING_TAG": "Tag already exists",
"ALREADY_ADDED_TAG": "Tag is already added",
@ -158,8 +158,7 @@
"CREATE_TAGS": "Error while creating the tags"
},
"TOOLTIPS": {
"DELETE_TAG": "Delete tag",
"HIDE_INPUT": "Hide input"
"DELETE_TAG": "Delete tag"
},
"TAGS_LOADING": "Tags loading",
"CREATE_TAGS_SUCCESS": "Tags created successfully"
@ -169,7 +168,6 @@
"CATEGORIES_TITLE": "Categories",
"NO_CATEGORIES_CREATED": "No Categories created",
"NO_CATEGORIES_ASSIGNED": "No Categories assigned",
"ASSIGN_CATEGORIES": "Assign Categories",
"UNASSIGN_CATEGORY": "Unassign Category",
"DELETE_CATEGORY": "Delete Category",
"EXISTING_CATEGORIES": "Existing Categories:",
@ -178,7 +176,7 @@
"GENERIC_CREATE": "Create: {{name}}",
"NAME": "Category name",
"LOADING": "Loading",
"HIDE_INPUT": "Hide input",
"NO_CATEGORIES_ADDED": "There are currently no categories added",
"ERRORS": {
"NOT_FOUND": "Categories not found",
"REQUIRED": "Category name is required",
@ -187,7 +185,8 @@
"DUPLICATED_CATEGORY": "Category is already added",
"CREATE_CATEGORIES": "Error while creating categories",
"EXISTING_CATEGORIES": "Some categories already exist"
}
},
"CATEGORIES_SEARCH_PLACEHOLDER": "Search Categories"
},
"ADF_FILE_UPLOAD": {
"BUTTON": {
@ -488,7 +487,9 @@
"MODIFIED_DATE": "Modified Date",
"CONTENT_TYPE": "Content Type",
"TAGS": "Tags",
"ADD_TAG_TOOLTIP": "Add tag"
"NO_TAGS_ADDED": "There are currently no tags added",
"NO_ITEMS_MESSAGE": "There are currently no {{ groupTitle }} added",
"SAVE_OR_DISCARD_CHANGES": "Save or discard changes to continue"
},
"CONTENT_TYPE": {
"DIALOG": {

View File

@ -1,54 +1,37 @@
<div class="adf-tags-creation">
<div *ngIf="tagNameControlVisible" class="adf-tag-name-field">
<input #tagNameInput
class="adf-tag-search-field"
matInput
autocomplete="off"
[formControl]="tagNameControl"
(keyup.enter)="addTag()"
adf-auto-focus
placeholder="{{'TAG.TAGS_CREATOR.TAG_SEARCH_PLACEHOLDER' | translate}}"
/>
<mat-error *ngIf="tagNameControl.invalid && tagNameControl.touched">{{ tagNameErrorMessageKey | translate }}</mat-error>
</div>
<p
class="adf-no-tags-message"
*ngIf="!tags.length && !tagNameControlVisible">
*ngIf="showEmptyTagMessage">
{{ 'TAG.TAGS_CREATOR.NO_TAGS_CREATED' | translate }}
</p>
<div
class="adf-tags-list"
[class.adf-tags-list-fixed]="!tagNameControlVisible"
#tagsList>
<p
*ngFor="let tag of tags"
class="adf-tag adf-label-with-icon-button">
<span *ngFor="let tag of tags" class="adf-tag adf-label-with-icon-button">
{{ tag }}
<button
data-automation-id="remove-tag-button"
mat-icon-button
(click)="removeTag(tag)"
[attr.title]="'TAG.TAGS_CREATOR.TOOLTIPS.DELETE_TAG' | translate"
[disabled]="disabledTagsRemoving">
<mat-icon>remove</mat-icon>
[disabled]="disabledTagsRemoving"
class="adf-remove-tag">
<mat-icon>close</mat-icon>
</button>
</p>
</div>
<div
class="adf-tag-name-field"
*ngIf="(!tagNameControlVisible && tags.length) || tagNameControlVisible"
[hidden]="!tagNameControlVisible">
<mat-form-field *ngIf="tagNameControlVisible">
<mat-icon matPrefix>search</mat-icon>
<mat-label id="adf-tag-name-input-label">
{{ 'TAG.TAGS_CREATOR.NAME' | translate }}
</mat-label>
<input
#tagNameInput
matInput
autocomplete="off"
[formControl]="tagNameControl"
(keyup.enter)="addTag()"
aria-labelledby="adf-tag-name-input-label"
adf-auto-focus
/>
<mat-error [hidden]="!tagNameControl.invalid">{{ tagNameErrorMessageKey | translate }}</mat-error>
</mat-form-field>
<button
data-automation-id="hide-tag-name-input-button"
mat-icon-button
(click)="hideNameInput()"
[attr.title]="'TAG.TAGS_CREATOR.TOOLTIPS.HIDE_INPUT' | translate">
<mat-icon>remove</mat-icon>
</button>
</span>
</div>
</div>
<div

View File

@ -1,15 +1,25 @@
adf-tags-creator {
display: block;
margin-left: -24px;
.adf-label-with-icon-button {
display: flex;
justify-content: space-between;
background: var(--adf-metadata-buttons-background-color);
width: fit-content;
min-height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 16px;
padding: 6px 0;
padding-left: 12px;
.adf-remove-tag {
line-height: 24px;
height: 24px;
transform: scale(0.7);
}
}
.adf-no-tags-message {
margin-left: 9px;
margin-top: 28.5px;
margin-bottom: 0;
height: 30px;
@ -17,23 +27,22 @@ adf-tags-creator {
.adf-tag-name-field,
.adf-tag-name-field[hidden] {
display: flex;
align-items: center;
min-height: 57px;
padding-top: 10px;
margin-right: 12px;
}
mat-form-field {
width: 100%;
box-sizing: border-box;
padding-right: 3px;
font-size: 14px;
}
.adf-tag-search-field {
background: var(--adf-metadata-buttons-background-color);
height: 32px;
border-radius: 12px;
padding: 0 6px;
}
.adf-create-tag-label {
color: var(--theme-primary-color);
cursor: pointer;
margin-top: -1px;
padding-left: 27px;
padding-left: 12px;
overflow-wrap: anywhere;
display: inline-block;
padding-right: 12px;
@ -42,25 +51,21 @@ adf-tags-creator {
}
.adf-tags-list {
padding-left: 10px;
padding-right: 0;
overflow: auto;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.adf-tag {
margin-top: 0;
margin-top: 8px;
overflow-wrap: anywhere;
& + .adf-tag {
margin-top: -14px;
margin-top: 8px;
}
}
.adf-tags-creation {
padding-left: 27px;
padding-right: 22px;
}
.adf-existing-tags-panel {
border-top-left-radius: 6px;
border-top-right-radius: 6px;
@ -75,7 +80,7 @@ adf-tags-creator {
}
.adf-tags-list {
padding-left: 18px;
padding-left: 12px;
margin-top: -2px;
padding-right: 0;
display: flex;
@ -94,7 +99,7 @@ adf-tags-creator {
}
.mat-list-item-content-reverse {
padding: 0 6px;
padding: 0;
.mat-pseudo-checkbox {
display: none;
@ -126,6 +131,6 @@ adf-tags-creator {
[hidden] {
visibility: hidden;
display: unset;
display: none;
}
}

View File

@ -21,7 +21,7 @@ import { NotificationService } from '@alfresco/adf-core';
import { By } from '@angular/platform-browser';
import { TranslateModule } from '@ngx-translate/core';
import { MatIconModule } from '@angular/material/icon';
import { MatError, MatFormField, MatFormFieldModule } from '@angular/material/form-field';
import { MatError, MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
@ -38,8 +38,6 @@ describe('TagsCreatorComponent', () => {
let tagService: TagService;
let notificationService: NotificationService;
const tagNameFieldSelector = '.adf-tag-name-field';
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [TagsCreatorComponent],
@ -113,14 +111,6 @@ describe('TagsCreatorComponent', () => {
return elements.map(el => el.nativeElement);
}
/**
* Click at the hide name input button
*/
function clickAtHideNameInputButton() {
fixture.debugElement.query(By.css(`[data-automation-id="hide-tag-name-input-button"]`)).nativeElement.click();
fixture.detectChanges();
}
/**
* Get newly added tags
*
@ -301,34 +291,6 @@ describe('TagsCreatorComponent', () => {
});
describe('Tag name field', () => {
it('should be visible if tagNameControlVisible is true', () => {
component.tagNameControlVisible = true;
fixture.detectChanges();
const tagNameField = fixture.debugElement.query(By.css(tagNameFieldSelector));
expect(tagNameField).toBeTruthy();
expect(tagNameField.nativeElement.hasAttribute('hidden')).toBeFalsy();
expect(tagNameField.query(By.directive(MatFormField))).toBeTruthy();
});
it('should be hidden and cleared after clicking button for hiding input', fakeAsync(() => {
component.tagNameControlVisible = true;
typeTag('test');
fixture.detectChanges();
tick(100);
clickAtHideNameInputButton();
const tagNameField = fixture.debugElement.query(By.css(tagNameFieldSelector));
expect(tagNameField).toBeFalsy();
component.tagNameControlVisible = true;
fixture.detectChanges();
tick(100);
expect(getNameInput().value).toBe('');
}));
it('should input be autofocused', fakeAsync(() => {
component.tagNameControlVisible = true;
fixture.detectChanges();
@ -341,7 +303,6 @@ describe('TagsCreatorComponent', () => {
fixture.detectChanges();
tick(100);
clickAtHideNameInputButton();
component.tagNameControlVisible = true;
fixture.detectChanges();
tick(100);
@ -349,24 +310,13 @@ describe('TagsCreatorComponent', () => {
expect(getNameInput()).toBe(document.activeElement as HTMLInputElement);
}));
it('should be hidden and cleared on discard changes', fakeAsync(() => {
component.tagNameControlVisible = true;
component.tags = ['Passed tag 1', 'Passed tag 2'];
typeTag('test');
fixture.detectChanges();
tick(100);
expect(getNameInput().value).toBe('test');
component.tagNameControlVisible = false;
fixture.detectChanges();
tick(100);
expect(getNameInput()).toBeFalsy();
component.tagNameControlVisible = true;
fixture.detectChanges();
tick(100);
expect(getNameInput().value).toBe('');
}));
describe('showEmptyTagMessage', () => {
it('should return true when tags empty and non editable state', () => {
component.tags = [];
component.tagNameControlVisible = false;
expect(component.showEmptyTagMessage).toBeTrue();
});
});
describe('Errors', () => {
/**
@ -487,14 +437,6 @@ describe('TagsCreatorComponent', () => {
expect(getPanel()).toBeTruthy();
});
it('should not be visible when something has been typed and input has been hidden', fakeAsync(() => {
typeTag('some tag');
clickAtHideNameInputButton();
expect(getPanel()).toBeFalsy();
}));
it('should have correct label when mode is Create and Assign', fakeAsync(() => {
component.mode = TagsCreatorMode.CREATE_AND_ASSIGN;

View File

@ -104,7 +104,7 @@ export class TagsCreatorComponent implements OnInit, OnDestroy {
if (tagNameControlVisible) {
this._existingTagsPanelVisible = true;
setTimeout(() => {
this.tagNameInputElement.nativeElement.scrollIntoView();
this.tagNameInputElement?.nativeElement?.scrollIntoView();
});
} else {
this._existingTagsPanelVisible = false;
@ -127,11 +127,6 @@ export class TagsCreatorComponent implements OnInit, OnDestroy {
*/
@Output()
tagsChange = new EventEmitter<string[]>();
/**
* Emitted when input is showing or hiding.
*/
@Output()
tagNameControlVisibleChange = new EventEmitter<boolean>();
readonly nameErrorMessagesByErrors = new Map<keyof TagNameControlErrors, string>([
['duplicatedExistingTag', 'EXISTING_TAG'],
@ -219,6 +214,13 @@ export class TagsCreatorComponent implements OnInit, OnDestroy {
return this._tagNameControl;
}
/*
* Returns `true` if tags empty and non editable state, otherwise `false`
*/
get showEmptyTagMessage(): boolean {
return this.tags?.length === 0 && !this.tagNameControlVisible;
}
get existingTags(): TagEntry[] {
return this._existingTags;
}
@ -243,17 +245,6 @@ export class TagsCreatorComponent implements OnInit, OnDestroy {
return this._existingTagsPanelVisible;
}
/**
* Hide input for typing name for new tag or for searching. When input is hidden then panel of existing tags is hidden as well.
*/
hideNameInput(): void {
this.tagNameControlVisible = false;
this._existingTagsPanelVisible = false;
this.existingTagsPanelVisibilityChange.emit(this.existingTagsPanelVisible);
this.tagNameControlVisibleChange.emit(this.tagNameControlVisible);
this.clearTagNameInput();
}
/**
* Add tags to top list using value which is set in input. Adding tag is not allowed when value in input is invalid
* or if user is still typing what means that validation for input is not called yet.
@ -261,7 +252,6 @@ export class TagsCreatorComponent implements OnInit, OnDestroy {
addTag(): void {
if (!this._typing && !this.tagNameControl.invalid) {
this.tags.push(this.tagNameControl.value.trim());
this.hideNameInput();
this.clearTagNameInput();
this.checkScrollbarVisibility();
this.tagsChange.emit(this.tags);

View File

@ -26,6 +26,9 @@ import { takeUntil } from 'rxjs/operators';
export abstract class BaseCardView<T extends CardViewItem> implements OnDestroy {
protected cardViewUpdateService = inject(CardViewUpdateService);
@Input()
editable = false;
@Input()
property: T;
@ -39,6 +42,18 @@ export abstract class BaseCardView<T extends CardViewItem> implements OnDestroy
});
}
get isEditable(): boolean {
return this.editable && this.property.editable;
}
get isReadonlyProperty(): boolean {
return this.editable && !this.property.editable;
}
get hasIcon(): boolean {
return !!this.property.icon;
}
ngOnDestroy(): void {
this.destroy$.next(true);
this.destroy$.complete();

View File

@ -33,11 +33,7 @@ export class CardViewArrayItemComponent extends BaseCardView<CardViewArrayItemMo
}
showClickableIcon(): boolean {
return this.hasIcon() && this.isClickable();
}
hasIcon(): boolean {
return !!this.property.icon;
return this.hasIcon && this.isClickable();
}
displayCount(): number {

View File

@ -1,9 +1,9 @@
<ng-container *ngIf="!property.isEmpty() || isEditable()">
<ng-container *ngIf="!property.isEmpty() || isEditable">
<div class="adf-property-value">
<mat-checkbox [attr.data-automation-id]="'card-boolean-' + property.key"
[attr.title]="'CORE.METADATA.ACTIONS.TOGGLE' | translate"
[checked]="property.displayValue"
[disabled]="!isEditable()"
[disabled]="!isEditable"
color="primary"
(change)="changed($event)">
<div [attr.data-automation-id]="'card-boolean-label-' + property.key"

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Component, Input } from '@angular/core';
import { Component } from '@angular/core';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { CardViewBoolItemModel } from '../../models/card-view-boolitem.model';
import { BaseCardView } from '../base-card-view';
@ -26,13 +26,6 @@ import { BaseCardView } from '../base-card-view';
})
export class CardViewBoolItemComponent extends BaseCardView<CardViewBoolItemModel> {
@Input()
editable: boolean;
isEditable() {
return this.editable && this.property.editable;
}
changed(change: MatCheckboxChange) {
this.cardViewUpdateService.update({ ...this.property } as CardViewBoolItemModel, change.checked );
this.property.value = change.checked;

View File

@ -1,11 +1,12 @@
<label class="adf-property-label"
[attr.data-automation-id]="'card-dateitem-label-' + property.key"
*ngIf="showProperty() || isEditable()"
[attr.for]="'card-view-dateitem-' + property.key">
*ngIf="showProperty() || isEditable"
[attr.for]="'card-view-dateitem-' + property.key"
[ngClass]="{'adf-property-readonly-value': isReadonlyProperty, 'adf-property-value-editable': editable}">
{{ property.label | translate }}
</label>
<div class="adf-property-value adf-property-value-padding-top">
<span *ngIf="!isEditable() && !property.multivalued"
<div class="adf-property-value" [ngClass]="{'adf-property-value-editable': editable, 'adf-property-readonly-value': isReadonlyProperty}">
<span *ngIf="!isEditable && !property.multivalued"
[attr.data-automation-id]="'card-' + property.type + '-value-' + property.key">
<span *ngIf="showProperty()"
[attr.data-automation-id]="'card-dateitem-' + property.key"
@ -13,45 +14,47 @@
matTooltipShowDelay="1000"
[matTooltip]="'CORE.METADATA.ACTIONS.COPY_TO_CLIPBOARD' | translate">{{ property.displayValue}}</span>
</span>
<div *ngIf="isEditable() && !property.multivalued"
class="adf-dateitem-editable">
<div *ngIf="isEditable && !property.multivalued" class="adf-dateitem-editable">
<div class="adf-dateitem-editable-controls">
<span class="adf-datepicker-toggle"
[attr.data-automation-id]="'datepicker-label-toggle-' + property.key"
(click)="showDatePicker()">
<span *ngIf="showProperty(); else elseEmptyValueBlock"
[attr.data-automation-id]="'card-' + property.type + '-value-' + property.key">
[attr.data-automation-id]="'card-' + property.type + '-value-' + property.key">
{{ property.displayValue }}</span>
</span>
<mat-icon *ngIf="showClearAction()"
class="adf-date-reset-icon"
(click)="onDateClear()"
[attr.title]="'CORE.METADATA.ACTIONS.CLEAR' | translate"
[attr.data-automation-id]="'datepicker-date-clear-' + property.key">
<mat-icon
*ngIf="showClearAction()"
class="adf-date-reset-icon"
(click)="onDateClear()"
[attr.title]="'CORE.METADATA.ACTIONS.CLEAR' | translate"
[attr.data-automation-id]="'datepicker-date-clear-' + property.key">
clear
</mat-icon>
<mat-datetimepicker-toggle [attr.tabindex]="-1"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.data-automation-id]="'datepickertoggle-' + property.key"
[for]="datetimePicker">
<mat-datetimepicker-toggle
[attr.tabindex]="-1"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.data-automation-id]="'datepickertoggle-' + property.key"
[for]="datetimePicker">
</mat-datetimepicker-toggle>
</div>
<input class="adf-invisible-date-input"
[attr.tabIndex]="-1"
[matDatetimepicker]="datetimePicker"
[value]="valueDate"
(dateChange)="onDateChanged($event)"
[attr.id]="'card-view-dateitem-' + property.key"
>
<input
class="adf-invisible-date-input"
[attr.tabIndex]="-1"
[matDatetimepicker]="datetimePicker"
[value]="valueDate"
(dateChange)="onDateChanged($event)"
[attr.id]="'card-view-dateitem-' + property.key">
<mat-datetimepicker #datetimePicker
[type]="$any(property).type"
[timeInterval]="5"
[attr.data-automation-id]="'datepicker-' + property.key"
[startAt]="valueDate">
<mat-datetimepicker
#datetimePicker
[type]="$any(property).type"
[timeInterval]="5"
[attr.data-automation-id]="'datepicker-' + property.key"
[startAt]="valueDate">
</mat-datetimepicker>
</div>
<ng-template #elseEmptyValueBlock>
@ -60,41 +63,37 @@
<div *ngIf="property.multivalued"
class="adf-property-field adf-dateitem-chip-list-container adf-dateitem-editable">
<mat-chip-list #chipList
class="adf-textitem-chip-list">
<mat-chip *ngFor="let propertyValue of property.displayValue; let idx = index"
[removable]="isEditable()"
(removed)="removeValueFromList(idx)">
<mat-chip-list #chipList class="adf-textitem-chip-list">
<mat-chip
*ngFor="let propertyValue of property.displayValue; let idx = index"
[removable]="isEditable"
(removed)="removeValueFromList(idx)">
{{ propertyValue }}
<mat-icon *ngIf="isEditable()"
matChipRemove>cancel</mat-icon>
<mat-icon *ngIf="isEditable" matChipRemove>cancel</mat-icon>
</mat-chip>
</mat-chip-list>
<mat-form-field *ngIf="isEditable()"
class="adf-property-field adf-dateitem-editable-controls"
[floatLabel]="'never'"
(click)="showDatePicker()">
<input matInput
class="adf-invisible-date-input"
[attr.tabIndex]="-1"
[matDatetimepicker]="datetimePicker"
(dateChange)="addDateToList($event)"
[attr.id]="'card-view-dateitem-' + property.key"
>
<mat-datetimepicker-toggle [attr.tabindex]="-1"
matSuffix
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.data-automation-id]="'datepickertoggle-' + property.key"
[for]="datetimePicker">
<div *ngIf="isEditable" class="adf-property-field adf-dateitem-editable-controls" (click)="showDatePicker()">
<input
class="adf-invisible-date-input"
[attr.tabIndex]="-1"
[matDatetimepicker]="datetimePicker"
(dateChange)="addDateToList($event)"
[attr.id]="'card-view-dateitem-' + property.key">
<mat-datetimepicker-toggle
[attr.tabindex]="-1"
matSuffix
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.data-automation-id]="'datepickertoggle-' + property.key"
[for]="datetimePicker">
</mat-datetimepicker-toggle>
<mat-datetimepicker #datetimePicker
[type]="$any(property).type"
[timeInterval]="5"
[attr.data-automation-id]="'datepicker-' + property.key"
[startAt]="valueDate">
<mat-datetimepicker
#datetimePicker
[type]="$any(property).type"
[timeInterval]="5"
[attr.data-automation-id]="'datepicker-' + property.key"
[startAt]="valueDate">
</mat-datetimepicker>
</mat-form-field>
</div>
</div>
</div>

View File

@ -1,6 +1,21 @@
/* stylelint-disable no-descending-specificity */
.adf {
&-invisible-date-input {
.adf-card-view-dateitem {
.adf-property-value {
padding: 6px 0;
line-height: 20px;
border-bottom: 1px solid var(--adf-metadata-property-panel-border-color);
color: var(--adf-metadata-property-panel-title-color);
&.adf-property-value-editable {
border-radius: 6px;
border-bottom: inherit;
}
&.adf-property-readonly-value {
padding-left: 12px;
}
}
.adf-invisible-date-input {
height: 2px;
width: 0;
overflow: hidden;
@ -11,33 +26,24 @@
float: right;
}
&-dateitem-chip-list-container.adf-property-field {
.adf-dateitem-chip-list-container.adf-property-field {
margin-bottom: -7px !important;
border-bottom: 0;
cursor: pointer;
.adf-dateitem-editable-controls {
margin-top: 15px;
}
.mat-datetimepicker-toggle {
position: absolute;
right: 0;
top: -20px;
}
}
&-dateitem-editable {
.adf-dateitem-editable {
cursor: pointer;
border-bottom: 1px solid var(--adf-theme-foreground-text-color-042);
padding-bottom: 6px;
padding: 0 12px;
/* stylelint-disable-next-line no-descending-specificity */
&-controls {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 6px;
button.mat-icon-button {
.mat-icon-button {
line-height: 20px;
height: 20px;
width: 20px;
@ -47,29 +53,21 @@
width: 16px;
height: 16px;
opacity: 0.5;
}
&:hover mat-icon {
opacity: 1;
&:hover {
opacity: 1;
}
}
.adf-datepicker-toggle {
flex: 1 0 auto;
}
mat-icon.adf-date-reset-icon {
.adf-date-reset-icon {
line-height: 10px;
font-size: var(--theme-subheading-2-font-size);
width: 16px;
height: 16px;
position: relative;
top: 4px;
padding-left: 8px;
opacity: 0.3;
}
&:hover mat-icon.adf-date-reset-icon {
opacity: 1;
padding: 0 8px;
}
}
}

View File

@ -166,7 +166,7 @@ describe('CardViewDateItemComponent', () => {
component.property.editable = true;
fixture.detectChanges();
expect(component.isEditable()).toBe(false);
expect(component.isEditable).toBe(false);
const datePicker = fixture.debugElement.query(By.css(`[data-automation-id="datepicker-${component.property.key}"]`));
const datePickerToggle = fixture.debugElement.query(By.css(`[data-automation-id="datepickertoggle-${component.property.key}"]`));
expect(datePicker).toBeNull('Datepicker should NOT be in DOM');

View File

@ -20,7 +20,6 @@ import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
import { DatetimeAdapter, MAT_DATETIME_FORMATS, MatDatetimepickerComponent, MatDatetimepickerInputEvent } from '@mat-datetimepicker/core';
import { CardViewDateItemModel } from '../../models/card-view-dateitem.model';
import { UserPreferencesService, UserPreferenceValues } from '../../../common/services/user-preferences.service';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { BaseCardView } from '../base-card-view';
import { ClipboardService } from '../../../clipboard/clipboard.service';
@ -45,24 +44,16 @@ import { isValid } from 'date-fns';
})
export class CardViewDateItemComponent extends BaseCardView<CardViewDateItemModel> implements OnInit, OnDestroy {
@Input()
property: CardViewDateItemModel;
displayEmpty = true;
@Input()
editable: boolean = false;
@Input()
displayEmpty: boolean = true;
@Input()
displayClearAction: boolean = true;
displayClearAction = true;
@ViewChild('datetimePicker')
public datepicker: MatDatetimepickerComponent<any>;
valueDate: Date;
private onDestroy$ = new Subject<boolean>();
constructor(
private dateAdapter: DateAdapter<Date>,
private userPreferencesService: UserPreferencesService,
@ -75,7 +66,7 @@ export class CardViewDateItemComponent extends BaseCardView<CardViewDateItemMode
ngOnInit() {
this.userPreferencesService
.select(UserPreferenceValues.Locale)
.pipe(takeUntil(this.onDestroy$))
.pipe(takeUntil(this.destroy$))
.subscribe((locale) => {
this.property.locale = locale;
});
@ -94,8 +85,7 @@ export class CardViewDateItemComponent extends BaseCardView<CardViewDateItemMode
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
super.ngOnDestroy();
}
showProperty(): boolean {
@ -106,10 +96,6 @@ export class CardViewDateItemComponent extends BaseCardView<CardViewDateItemMode
return this.displayClearAction && (!this.property.isEmpty() || !!this.property.default);
}
isEditable(): boolean {
return this.editable && this.property.editable;
}
showDatePicker() {
this.datepicker.open();
}

View File

@ -2,11 +2,8 @@
class="adf-property-label">{{ property.label | translate }}</div>
<div class="adf-property-field">
<div *ngIf="!isEditable()"
class="adf-card-view__key-value-pairs__read-only adf-property-value">
<mat-table #table
[dataSource]="matTableValues"
class="mat-elevation-z8">
<div *ngIf="!isEditable" class="adf-card-view__key-value-pairs__read-only adf-property-value">
<mat-table #table [dataSource]="matTableValues" class="mat-elevation-z8">
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef>{{ 'CORE.CARDVIEW.KEYVALUEPAIRS.NAME' | translate }}
</mat-header-cell>
@ -23,50 +20,44 @@
</mat-table>
</div>
<div class="adf-card-view__key-value-pairs adf-property-value"
*ngIf="isEditable() && values && values.length">
<div class="adf-card-view__key-value-pairs adf-property-value" *ngIf="isEditable && values && values.length">
<div class="adf-card-view__key-value-pairs__row">
<div class="adf-card-view__key-value-pairs__col">{{ 'CORE.CARDVIEW.KEYVALUEPAIRS.NAME' | translate }}</div>
<div class="adf-card-view__key-value-pairs__col">{{ 'CORE.CARDVIEW.KEYVALUEPAIRS.VALUE' | translate }}</div>
<div class="adf-property-col-key">{{ 'CORE.CARDVIEW.KEYVALUEPAIRS.NAME' | translate }}</div>
<div class="adf-property-col-value">{{ 'CORE.CARDVIEW.KEYVALUEPAIRS.VALUE' | translate }}</div>
</div>
<div class="adf-card-view__key-value-pairs__row"
*ngFor="let item of values; let i = index">
<div class="adf-card-view__key-value-pairs__col">
<mat-form-field>
<input matInput
placeholder="{{ 'CORE.CARDVIEW.KEYVALUEPAIRS.NAME' | translate }}"
(blur)="onBlur(item.value)"
[attr.data-automation-id]="'card-'+ property.key +'-name-input-' + i"
[(ngModel)]="values[i].name">
</mat-form-field>
</div>
<div class="adf-card-view__key-value-pairs__col">
<mat-form-field>
<input matInput
placeholder="{{ 'CORE.CARDVIEW.KEYVALUEPAIRS.VALUE' | translate }}"
(blur)="onBlur(item.value)"
[attr.data-automation-id]="'card-'+ property.key +'-value-input-' + i"
[(ngModel)]="values[i].value">
<button matSuffix
mat-icon-button
(click)="remove(i)"
class="adf-card-view__key-value-pairs__remove-btn">
<mat-icon>close</mat-icon>
</button>
</mat-form-field>
</div>
<div class="adf-card-view__key-value-pairs__row" *ngFor="let item of values; let i = index">
<input
matInput
class="adf-property-col-key adf-property-value-input"
placeholder="{{ 'CORE.CARDVIEW.KEYVALUEPAIRS.NAME' | translate }}"
(blur)="onBlur(item.value)"
[attr.data-automation-id]="'card-'+ property.key +'-name-input-' + i"
[(ngModel)]="values[i].name">
<input
matInput
class="adf-property-col-value adf-property-value-input"
placeholder="{{ 'CORE.CARDVIEW.KEYVALUEPAIRS.VALUE' | translate }}"
(blur)="onBlur(item.value)"
[attr.data-automation-id]="'card-'+ property.key +'-value-input-' + i"
[(ngModel)]="values[i].value">
<button
mat-icon-button
(click)="remove(i)"
class="adf-property-col-delete">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
<div *ngIf="isEditable()"
class="adf-property-value adf-card-view__key-value-pairs__add-btn-container">
<div *ngIf="isEditable" class="adf-property-value adf-card-view__key-value-pairs__add-btn-container">
<button (click)="add()"
mat-button
class="adf-card-view__key-value-pairs__add-btn"
[attr.data-automation-id]="'card-key-value-pairs-button-' + property.key">
{{ 'CORE.CARDVIEW.KEYVALUEPAIRS.ADD' | translate }}
<mat-icon>add</mat-icon>
{{ 'CORE.CARDVIEW.KEYVALUEPAIRS.ADD' | translate }}
<mat-icon>add</mat-icon>
</button>
</div>
</div>

View File

@ -1,43 +1,48 @@
.adf-card-view {
&__key-value-pairs {
&__row {
display: flex;
justify-content: center;
}
.adf-card-view-key-value-pairs-item {
.adf-property-col-key {
width: 50%;
margin-right: 4px;
}
&__col {
width: 50%;
.adf-property-col-value {
margin-left: 4px;
width: 50%;
}
.mat-form-field {
width: 100%;
font-size: var(--theme-body-1-font-size);
.adf-property-col-delete {
width: auto;
}
.adf-card-view {
&__key-value-pairs {
&__row {
display: flex;
justify-content: center;
align-items: center;
}
.mat-form-field-appearance-legacy .mat-form-field-label {
color: var(--adf-theme-foreground-text-color-040) !important;
}
}
&__add-btn-container {
display: flex;
justify-content: center;
}
&__add-btn.mat-button {
margin-bottom: 20px;
}
&__read-only {
padding-bottom: 20px;
.mat-table {
box-shadow: none;
&__add-btn-container {
display: flex;
justify-content: center;
}
.mat-header-row,
.mat-row {
padding: 0;
&__add-btn.mat-button {
margin-bottom: 20px;
}
&__read-only {
padding-bottom: 20px;
.mat-table {
box-shadow: none;
}
.mat-header-row,
.mat-row {
padding: 0;
}
}
}
}
}

View File

@ -75,7 +75,7 @@ describe('CardViewKeyValuePairsItemComponent', () => {
component.ngOnChanges();
fixture.detectChanges();
expect(component.isEditable()).toBe(false);
expect(component.isEditable).toBe(false);
const table = fixture.debugElement.query(By.css('.adf-card-view__key-value-pairs__read-only'));
const form = fixture.debugElement.query(By.css('.adf-card-view__key-value-pairs'));
@ -111,7 +111,7 @@ describe('CardViewKeyValuePairsItemComponent', () => {
addButton.triggerEventHandler('click', null);
fixture.detectChanges();
const removeButton = fixture.debugElement.query(By.css('.adf-card-view__key-value-pairs__remove-btn'));
const removeButton = fixture.debugElement.query(By.css('.adf-property-col-delete'));
removeButton.triggerEventHandler('click', null);
fixture.detectChanges();

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Component, Input, OnChanges, ViewEncapsulation } from '@angular/core';
import { Component, OnChanges, ViewEncapsulation } from '@angular/core';
import { CardViewKeyValuePairsItemModel } from '../../models/card-view.models';
import { CardViewKeyValuePairsItemType } from '../../interfaces/card-view.interfaces';
import { MatTableDataSource } from '@angular/material/table';
@ -30,10 +30,6 @@ import { BaseCardView } from '../base-card-view';
})
export class CardViewKeyValuePairsItemComponent extends BaseCardView<CardViewKeyValuePairsItemModel> implements OnChanges {
@Input()
editable: boolean = false;
values: CardViewKeyValuePairsItemType[];
matTableValues: MatTableDataSource<CardViewKeyValuePairsItemType>;
@ -42,10 +38,6 @@ export class CardViewKeyValuePairsItemComponent extends BaseCardView<CardViewKey
this.matTableValues = new MatTableDataSource(this.values);
}
isEditable(): boolean {
return this.editable && this.property.editable;
}
add(): void {
this.values.push({ name: '', value: '' });
}
@ -55,8 +47,8 @@ export class CardViewKeyValuePairsItemComponent extends BaseCardView<CardViewKey
this.save(true);
}
onBlur(value): void {
if (value.length) {
onBlur(value: any): void {
if (value?.length) {
this.save();
}
}

View File

@ -26,9 +26,6 @@ import { BaseCardView } from '../base-card-view';
})
export class CardViewMapItemComponent extends BaseCardView<CardViewMapItemModel> {
@Input()
property: CardViewMapItemModel;
@Input()
displayEmpty: boolean = true;

View File

@ -1,28 +1,30 @@
<ng-container *ngIf="!property.isEmpty() || isEditable()">
<ng-container *ngIf="!property.isEmpty() || isEditable">
<div
[attr.data-automation-id]="'card-select-label-' + property.key"
class="adf-property-label"
[ngClass]="{
'adf-property-value-editable': isEditable,
'adf-property-readonly-value': isReadonlyProperty
}"
>{{ property.label | translate }}</div>
<div class="adf-property-field">
<div
*ngIf="!isEditable()"
class="adf-select-item-padding adf-property-value"
*ngIf="!isEditable"
class="adf-property-value adf-property-read-only"
[attr.data-automation-id]="'select-readonly-value-' + property.key"
data-automation-class="read-only-value">{{ (property.displayValue | async) | translate }}
</div>
<div *ngIf="isEditable()">
<mat-form-field class="adf-select-item-padding-editable adf-property-value">
<div *ngIf="isEditable">
<mat-form-field class="adf-property-value" [ngClass]="{'adf-property-value-editable': isEditable}">
<mat-select
[(value)]="value"
[ngClass]="{ 'adf-property-readonly-value': isReadonlyProperty }"
panelClass="adf-select-filter"
(selectionChange)="onChange($event)"
data-automation-class="select-box"
[aria-label]="property.label | translate"
>
[aria-label]="property.label | translate">
<adf-select-filter-input *ngIf="showInputFilter" (change)="onFilterInputChange($event)"></adf-select-filter-input>
<mat-option *ngIf="showNoneOption()">{{ 'CORE.CARDVIEW.NONE' | translate }}</mat-option>
<mat-option *ngIf="displayNoneOption">{{ 'CORE.CARDVIEW.NONE' | translate }}</mat-option>
<mat-option
*ngFor="let option of list$ | async"
[value]="option.key">

View File

@ -1,22 +1,36 @@
.mat-form-field-type-mat-select {
width: 100%;
}
.adf-card-view-selectitem {
.adf-property-value {
width: 100%;
}
.adf-select-item-padding {
padding-top: 6px;
padding-bottom: 20px;
}
.adf-select-filter-input {
background: var(--adf-theme-background-card-color);
position: sticky;
top: 0;
left: 0;
right: 0;
z-index: 1;
}
.adf-select-item-padding-editable {
padding-top: 6px;
padding-bottom: 6px;
}
.adf-property-value-editable {
mat-select {
padding: 6px 0 6px 12px;
margin-top: 0;
border-radius: 6px;
.adf-select-filter-input {
background: var(--adf-theme-background-card-color);
position: sticky;
top: 0;
left: 0;
right: 0;
z-index: 1;
.mat-select-value {
color: var(--adf-metadata-action-button-clear-color);
}
}
}
.adf-property-read-only {
padding: 6px 0;
border-bottom: 1px solid var(--adf-metadata-property-panel-border-color);
color: var(--adf-metadata-property-panel-title-color);
}
.adf-property-readonly-value {
color: var(--adf-metadata-property-panel-label-color);
}
}

View File

@ -108,7 +108,7 @@ describe('CardViewSelectItemComponent', () => {
fixture.detectChanges();
expect(component.value).toEqual('two');
expect(component.isEditable()).toBe(true);
expect(component.isEditable).toBe(true);
const select = await loader.getHarness(MatSelectHarness);
await select.open();
@ -131,7 +131,7 @@ describe('CardViewSelectItemComponent', () => {
fixture.detectChanges();
expect(component.value).toEqual(2);
expect(component.isEditable()).toBe(true);
expect(component.isEditable).toBe(true);
const select = await loader.getHarness(MatSelectHarness);
await select.open();
@ -154,7 +154,7 @@ describe('CardViewSelectItemComponent', () => {
component.ngOnChanges();
fixture.detectChanges();
expect(component.isEditable()).toBe(true);
expect(component.isEditable).toBe(true);
const select = await loader.getHarness(MatSelectHarness);
await select.open();

View File

@ -17,7 +17,7 @@
import { Component, Input, OnChanges, OnDestroy, OnInit, inject, ViewEncapsulation } from '@angular/core';
import { CardViewSelectItemModel } from '../../models/card-view-selectitem.model';
import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { CardViewSelectItemOption } from '../../interfaces/card-view.interfaces';
import { MatSelectChange } from '@angular/material/select';
import { BaseCardView } from '../base-card-view';
@ -35,8 +35,6 @@ export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItem
private appConfig = inject(AppConfigService);
static HIDE_FILTER_LIMIT = 5;
@Input() editable: boolean = false;
@Input() options$: Observable<CardViewSelectItemOption<string | number>[]>;
@Input()
@ -46,11 +44,8 @@ export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItem
displayEmpty: boolean = true;
value: string | number;
filter$: BehaviorSubject<string> = new BehaviorSubject('');
filter$ = new BehaviorSubject<string>('');
showInputFilter: boolean = false;
private onDestroy$ = new Subject<void>();
list$: Observable<CardViewSelectItemOption<string | number>[]> = null;
ngOnChanges(): void {
@ -59,8 +54,8 @@ export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItem
ngOnInit() {
this.getOptions()
.pipe(takeUntil(this.onDestroy$))
.subscribe((options: CardViewSelectItemOption<string>[]) => {
.pipe(takeUntil(this.destroy$))
.subscribe((options) => {
this.showInputFilter = options.length > this.optionsLimit;
});
@ -71,21 +66,17 @@ export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItem
this.filter$.next(value.toString());
}
isEditable(): boolean {
return this.editable && this.property.editable;
}
getOptions(): Observable<CardViewSelectItemOption<string | number>[]> {
private getOptions(): Observable<CardViewSelectItemOption<string | number>[]> {
return this.options$ || this.property.options$;
}
getList(): Observable<CardViewSelectItemOption<string | number>[]> {
return combineLatest([this.getOptions(), this.filter$])
.pipe(
map(([items, filter]) => items.filter((item: CardViewSelectItemOption<string>) =>
map(([items, filter]) => items.filter((item) =>
filter ? item.label.toLowerCase().includes(filter.toLowerCase())
: true)),
takeUntil(this.onDestroy$)
takeUntil(this.destroy$)
);
}
@ -95,17 +86,12 @@ export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItem
this.property.value = selectedOption;
}
showNoneOption() {
return this.displayNoneOption;
}
get showProperty(): boolean {
return this.displayEmpty || !this.property.isEmpty();
}
ngOnDestroy() {
this.onDestroy$.next();
this.onDestroy$.complete();
super.ngOnDestroy();
}
private get optionsLimit(): number {

View File

@ -1,70 +1,70 @@
<div [ngSwitch]="templateType">
<div *ngSwitchDefault>
<mat-form-field class="adf-property-field adf-card-textitem-field"
[ngClass]="{ 'adf-property-read-only': !isEditable, 'adf-property-field-has-error mat-form-field-invalid': isEditable && hasErrors }"
[floatLabel]="'never'"
[ngClass]="{
'adf-property-read-only': !isEditable
}"
[floatLabel]="'always'"
appearance="standard">
<mat-label *ngIf="showProperty || isEditable" [attr.data-automation-id]="'card-textitem-label-' + property.key" class="adf-property-label">
<mat-label *ngIf="showProperty || isEditable" [attr.data-automation-id]="'card-textitem-label-' + property.key" class="adf-property-label"
[ngClass]="{
'adf-property-value-editable': editable,
'adf-property-readonly-value': isReadonlyProperty
}">
{{ property.label | translate }}
</mat-label>
<input matInput
*ngIf="!property.multiline"
class="adf-property-value"
title="{{property.label | translate }}"
[placeholder]="property.default"
[attr.aria-label]="property.label | translate"
[formControl]="textInput"
(dblclick)="copyToClipboard(property.displayValue)"
matTooltipShowDelay="1000"
[matTooltip]="'CORE.METADATA.ACTIONS.COPY_TO_CLIPBOARD' | translate"
[matTooltipDisabled]="isEditable"
[attr.data-automation-id]="'card-textitem-value-' + property.key"
(keydown)="undoText($event)">
<textarea matInput
*ngIf="property.multiline"
title="{{property.label | translate }}"
[cdkTextareaAutosize]="true"
[cdkAutosizeMaxRows]="1"
[cdkAutosizeMaxRows]="5"
class="adf-property-value"
[placeholder]="property.default"
[attr.aria-label]="property.label | translate"
[formControl]="textInput"
[attr.data-automation-id]="'card-textitem-value-' + property.key">
</textarea>
<button
*ngIf="isEditable"
matSuffix
class="adf-textitem-clear-icon"
[attr.aria-label]="'CORE.METADATA.ACTIONS.CLEAR' | translate"
(click)="clearValue()">
<mat-icon>cancel</mat-icon>
</button>
<button
*ngIf="isEditable"
matSuffix
class="adf-textitem-edit-icon"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate">
<mat-icon>mode_edit</mat-icon>
</button>
<input matInput
*ngIf="!property.multiline"
class="adf-property-value"
[ngClass]="{
'adf-property-value-editable': editable,
'adf-property-readonly-value': isReadonlyProperty,
'adf-property-value-has-error': isEditable && hasErrors
}"
title="{{property.label | translate }}"
[placeholder]="property.default"
[attr.aria-label]="property.label | translate"
[formControl]="textInput"
(dblclick)="copyToClipboard(property.displayValue)"
matTooltipShowDelay="1000"
[matTooltip]="'CORE.METADATA.ACTIONS.COPY_TO_CLIPBOARD' | translate"
[matTooltipDisabled]="isEditable"
[attr.data-automation-id]="'card-textitem-value-' + property.key"
(keydown)="undoText($event)">
<textarea matInput
*ngIf="property.multiline"
title="{{property.label | translate }}"
[cdkTextareaAutosize]="true"
[cdkAutosizeMaxRows]="1"
[cdkAutosizeMaxRows]="5"
class="adf-property-value"
[ngClass]="{
'adf-property-value-editable': editable,
'adf-property-readonly-value': isReadonlyProperty
}"
[placeholder]="property.default"
[attr.aria-label]="property.label | translate"
[formControl]="textInput"
[attr.data-automation-id]="'card-textitem-value-' + property.key">
</textarea>
</mat-form-field>
</div>
<div *ngSwitchCase="'chipsTemplate'"
class="adf-property-field adf-textitem-chip-list-container">
<mat-label *ngIf="showLabelForChips" [attr.data-automation-id]="'card-textitem-label-' + property.key" class="adf-property-label">
class="adf-property-field adf-textitem-chip-list-container"
[ngClass]="{'adf-property-read-only': !isEditable}">
<mat-label *ngIf="showLabelForChips" [attr.data-automation-id]="'card-textitem-label-' + property.key" class="adf-property-label"
[ngClass]="{'adf-property-value-editable': editable}">
{{ property.label | translate }}
</mat-label>
<mat-chip-list #chipList
class="adf-textitem-chip-list">
<mat-chip-list #chipList class="adf-textitem-chip-list">
<mat-chip *ngFor="let propertyValue of editedValue; let idx = index"
[removable]="isEditable"
(removed)="removeValueFromList(idx)">
{{ propertyValue }}
<mat-icon *ngIf="isEditable"
matChipRemove>cancel</mat-icon>
<mat-icon *ngIf="isEditable" matChipRemove>cancel</mat-icon>
</mat-chip>
</mat-chip-list>
@ -74,6 +74,10 @@
[floatLabel]="'never'">
<input matInput
class="adf-property-value"
[ngClass]="{
'adf-property-value-editable': editable,
'adf-property-readonly-value': isReadonlyProperty
}"
title="{{property.label | translate }}"
[placeholder]="editedValue ? '' : property.default | translate"
[attr.aria-label]="property.label | translate"
@ -81,8 +85,6 @@
[matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addValueToList($event)"
[attr.data-automation-id]="'card-textitem-editchipinput-' + property.key">
<mat-icon matSuffix
class="adf-textitem-edit-icon">mode_edit</mat-icon>
</mat-form-field>
</div>
@ -100,8 +102,13 @@
<input matInput
[type]=property.inputType
class="adf-property-value"
title="{{property.label | translate }}"
[ngClass]="{ 'adf-textitem-clickable-value': !isEditable }"
title="{{ property.label | translate }}"
[ngClass]="{
'adf-property-value-editable': editable,
'adf-textitem-clickable-value': !isEditable,
'adf-property-readonly-value': isReadonlyProperty,
'adf-property-value-has-error': isEditable && hasErrors
}"
[placeholder]="property.default"
[attr.aria-label]="property.label | translate"
[(ngModel)]="editedValue"
@ -125,9 +132,10 @@
<span class="adf-textitem-default-value">{{ property.default | translate }}</span>
</div>
<mat-error [attr.data-automation-id]="'card-textitem-error-' + property.key"
class="adf-textitem-editable-error"
*ngIf="isEditable && hasErrors">
<mat-error
*ngIf="isEditable && hasErrors"
class="adf-textitem-error"
[attr.data-automation-id]="'card-textitem-error-' + property.key">
<ul>
<li *ngFor="let error of errors">{{ error.message | translate: error }}</li>
</ul>

View File

@ -1,204 +1,67 @@
.adf {
&-textitem-edit-icon.mat-icon {
font-size: var(--theme-subheading-2-font-size);
width: 16px;
height: 16px;
color: var(--adf-theme-foreground-text-color-025);
.adf-card-view-textitem {
.adf-textitem-error {
font-size: var(--theme-caption-font-size);
padding-top: 6px;
ul {
margin: 0;
padding: 0;
list-style-type: none;
li {
margin: 0;
padding: 0;
}
}
}
&-textitem-action.mat-icon-button {
.adf-textitem-action {
width: 20px;
height: 20px;
line-height: 20px;
color: var(--adf-theme-foreground-text-color-025);
}
&-textitem-action:hover,
&-textitem-action:focus {
color: var(--adf-theme-foreground-text-color);
}
&-update-icon {
padding-left: 13px;
}
&-textitem-readonly {
cursor: pointer !important;
&:hover .adf-textitem-action,
&:focus .adf-textitem-action {
&:hover,
&:focus {
color: var(--adf-theme-foreground-text-color);
}
}
&-textitem-chip-list-container {
margin-bottom: 25px !important;
margin-top: 6px;
.adf-textitem-chip-list-container {
.mat-form-field-label {
margin-top: 6px;
}
}
&-textitem-clickable {
cursor: pointer !important;
.adf-textitem-clickable {
cursor: pointer;
padding-top: 3px;
.adf-textitem-edit-icon.mat-icon {
line-height: 16px;
}
&:hover .adf-textitem-action {
.adf-textitem-action:hover {
color: var(--adf-theme-foreground-text-color);
}
}
&-textitem-clickable-value {
cursor: pointer !important;
color: var(--theme-primary-color) !important;
}
&-textitem-editable {
&-controls {
display: flex;
align-items: center;
mat-icon:not(.adf-button-disabled):hover {
color: var(--adf-theme-foreground-text-color);
cursor: pointer !important;
}
mat-form-field {
width: 100%;
}
input:focus,
textarea:focus {
border: 1px solid var(--theme-accent-color-a200);
}
}
&-error {
font-size: var(--theme-caption-font-size);
padding-top: 6px;
ul {
margin: 0;
padding: 0;
list-style-type: none;
li {
margin: 0;
padding: 0;
}
}
}
}
&-textitem-default-value {
color: var(--adf-theme-foreground-text-color-054);
}
&-textitem-editable .mat-form-field-wrapper {
margin: 0;
padding-bottom: 0;
}
&-textitem-editable .mat-form-field-underline {
display: none;
}
&-textitem-editable .mat-form-field-infix {
padding: 0;
border-top: none;
}
&-textitem-editable .mat-form-field-label-wrapper {
padding-top: 2em;
position: static;
}
&-textitem-editable .mat-form-field-label {
top: 4px;
}
&-textitem-editable .mat-input-element {
font-family: inherit;
position: relative;
padding-top: 6px;
}
&-textitem-editable .mat-input-element:focus {
padding: 5px;
left: -6px;
top: 0;
}
&-textitem-editable input.mat-input-element {
margin-bottom: 2px;
}
&-textitem-editable input.mat-input-element:focus {
margin-bottom: -8px;
}
&-textitem-scroll {
overflow-x: auto;
white-space: nowrap;
display: block;
&::-webkit-scrollbar {
height: 5px;
}
&:hover::-webkit-scrollbar-thumb {
display: block;
background-color: var(--adf-theme-foreground-text-color-025);
border-radius: 2px;
}
}
&-textitem-multiline {
display: block;
}
}
.adf-property-field {
.adf-property-clear-value,
.adf-textitem-edit-value {
width: 30px;
height: 30px;
}
}
.mat-form-field:not(.adf-textitem-chip-list-input) {
.mat-form-field-suffix {
.adf-textitem-edit-icon {
display: block;
}
.adf-textitem-clear-icon {
display: none;
}
.adf-textitem-edit-icon,
.adf-textitem-clear-icon {
background: transparent;
border: none;
&:hover {
.adf-property-field {
.mat-input-element {
color: var(--theme-primary-color);
cursor: pointer;
}
}
}
&:hover {
.adf-textitem-edit-icon {
display: none;
}
.adf-textitem-default-value {
color: var(--adf-theme-foreground-text-color-054);
}
.adf-textitem-clear-icon {
display: block;
}
}
.adf-property-read-only {
border-bottom: 1px solid var(--adf-metadata-property-panel-border-color);
}
.adf-property-readonly-value {
color: var(--adf-metadata-property-panel-label-color);
}
.adf-property-value-editable {
color: var(--adf-metadata-property-panel-title-color);
}
}

View File

@ -189,24 +189,6 @@ describe('CardViewTextItemComponent', () => {
expect(value).toBe('Lorem ipsum');
});
it('should render the edit icon in case of editable:true', () => {
component.editable = true;
component.property.editable = true;
fixture.detectChanges();
const editIcon = fixture.debugElement.query(By.css('.adf-textitem-edit-icon'));
expect(editIcon).not.toBeNull('Edit icon should be shown');
});
it('should NOT render the edit icon in case of editable:false', async () => {
component.editable = false;
fixture.detectChanges();
await fixture.whenStable();
const editIcon = fixture.debugElement.query(By.css('.adf-textitem-edit-icon'));
expect(editIcon).toBeNull('Edit icon should NOT be shown');
});
it('should NOT render the picker and toggle in case of editable:true but (general) editable:false', async () => {
component.editable = false;
component.property.editable = true;
@ -313,6 +295,20 @@ describe('CardViewTextItemComponent', () => {
const labelElement = fixture.debugElement.query(By.css(`.adf-property-label`));
expect(labelElement).toBeNull();
});
it('should return true when editable is true, and property.editable is false', () => {
component.editable = true;
component.property.editable = false;
fixture.detectChanges();
expect(component.isReadonlyProperty).toBe(true);
});
it('should return false when editable is false, and property.editable is false', () => {
component.editable = false;
component.property.editable = false;
fixture.detectChanges();
expect(component.isReadonlyProperty).toBe(false);
});
});
describe('clickable', () => {
@ -491,26 +487,6 @@ describe('CardViewTextItemComponent', () => {
);
});
it('should clear value when clear value icon is clicked', async () => {
spyOn(component, 'update');
component.property.value = 'testValue';
component.property.icon = 'FAKE_ICON';
component.property.clickable = true;
component.property.editable = true;
component.editable = true;
component.property.isValid = () => true;
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
const clickEl = fixture.debugElement.query(By.css(`.adf-textitem-clear-icon`));
clickEl.triggerEventHandler('click', new MouseEvent('click'));
fixture.detectChanges();
const elementValue = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`));
expect(elementValue.nativeElement.textContent).toEqual('');
expect(component.update).toHaveBeenCalled();
});
});
describe('Update', () => {
@ -593,7 +569,7 @@ describe('CardViewTextItemComponent', () => {
component.editable = true;
fixture.detectChanges();
const errorMessage: HTMLElement = fixture.debugElement.nativeElement.querySelector('.adf-textitem-editable-error');
const errorMessage: HTMLElement = fixture.debugElement.nativeElement.querySelector('.adf-textitem-error');
expect(errorMessage.textContent).toBe(expectedErrorMessages[0].message);
});
@ -602,13 +578,13 @@ describe('CardViewTextItemComponent', () => {
component.editable = true;
fixture.detectChanges();
let errorMessage: HTMLElement = fixture.debugElement.nativeElement.querySelector('.adf-textitem-editable-error');
let errorMessage: HTMLElement = fixture.debugElement.nativeElement.querySelector('.adf-textitem-error');
expect(errorMessage.textContent).toBe(expectedErrorMessages[0].message);
component.editable = false;
fixture.detectChanges();
errorMessage = fixture.debugElement.nativeElement.querySelector('.adf-textitem-editable-error');
errorMessage = fixture.debugElement.nativeElement.querySelector('.adf-textitem-error');
expect(errorMessage).toBeNull();
});

View File

@ -24,7 +24,6 @@ import { TranslationService } from '../../../translation/translation.service';
import { CardViewItemValidator } from '../../interfaces/card-view-item-validator.interface';
import { UntypedFormControl } from '@angular/forms';
import { debounceTime, takeUntil, filter } from 'rxjs/operators';
import { Subject } from 'rxjs';
export const DEFAULT_SEPARATOR = ', ';
const templateTypes = {
@ -44,30 +43,24 @@ const templateTypes = {
})
export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemModel> implements OnChanges, OnDestroy {
@Input()
editable: boolean = false;
displayEmpty = true;
@Input()
displayEmpty: boolean = true;
copyToClipboardAction = true;
@Input()
copyToClipboardAction: boolean = true;
@Input()
useChipsForMultiValueProperty: boolean = true;
useChipsForMultiValueProperty = true;
@Input()
multiValueSeparator: string = DEFAULT_SEPARATOR;
@Input()
displayLabelForChips: boolean = false;
displayLabelForChips = false;
editedValue: string | string[];
errors: CardViewItemValidator[];
templateType: string;
textInput: UntypedFormControl = new UntypedFormControl();
private onDestroy$ = new Subject<boolean>();
textInput = new UntypedFormControl();
constructor(private clipboardService: ClipboardService, private translateService: TranslationService, private cd: ChangeDetectorRef) {
super();
@ -79,7 +72,7 @@ export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemMode
.pipe(
filter((textInputValue) => textInputValue !== this.editedValue && textInputValue !== null),
debounceTime(50),
takeUntil(this.onDestroy$)
takeUntil(this.destroy$)
)
.subscribe((textInputValue) => {
this.editedValue = textInputValue;
@ -198,8 +191,7 @@ export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemMode
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
super.ngOnDestroy();
}
get showProperty(): boolean {
@ -210,18 +202,10 @@ export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemMode
return this.hasIcon && this.editable;
}
get isEditable(): boolean {
return this.editable && this.property.editable;
}
get isClickable(): boolean {
return this.property.clickable;
}
get hasIcon(): boolean {
return !!this.property.icon;
}
get hasErrors(): boolean {
return !!this.errors?.length;
}

View File

@ -1,28 +1,110 @@
/* stylelint-disable no-descending-specificity */
.adf-property-list {
background: var(--adf-card-view-background);
border: var(--adf-card-view-border);
border-color: var(--adf-card-view-border-color);
border-radius: var(--adf-card-view-border-radius);
.adf-property {
margin-bottom: 20px;
.adf-property-label {
color: var(--adf-metadata-property-panel-text-color);
display: flex;
padding: 6px 0;
line-height: 20px;
.adf-property-value-padding-top {
margin-top: 6px;
&.adf-property-value-editable {
color: var(--adf-metadata-property-panel-title-color);
}
&.adf-property-readonly-value {
color: var(--adf-metadata-property-panel-label-color);
}
}
.adf-property {
.adf-property-field {
width: 100%;
margin-bottom: -25px;
.mat-form-field-underline {
display: none;
}
.mat-input-element {
text-overflow: ellipsis;
color: var(--adf-metadata-property-panel-title-color);
margin-top: 32px;
padding: 6px 0;
line-height: 20px;
}
.adf-card-view__key-value-pairs__row {
.mat-input-element {
margin-top: 0;
padding-left: 12px;
}
}
.mat-form-field-infix {
display: flex;
border-top-width: 0;
padding: 0;
}
.mat-form-field-flex {
padding-top: 0;
}
.mat-form-field-wrapper {
padding-bottom: 0;
}
.mat-form-field-label {
margin-top: 6px;
padding: 6px 0;
justify-content: center;
display: flex;
flex-direction: column;
height: 20px;
transform: translateY(-1.3437em) scale(1);
}
.mat-form-field-label-wrapper {
padding-top: 0;
top: -1px;
}
}
}
.adf-property-value {
color: var(--adf-metadata-property-panel-text-color);
&.adf-property-value-editable {
color: var(--adf-metadata-property-panel-title-color);
background-color: var(--adf-metadata-buttons-background-color);
border-radius: 6px;
&.mat-input-element {
color: var(--adf-metadata-action-button-clear-color);
padding: 6px 0 6px 12px;
margin: 32px 0 0;
}
}
&.adf-property-readonly-value {
color: var(--adf-metadata-property-panel-label-color);
&.mat-input-element {
color: var(--adf-metadata-property-panel-label-color);
}
}
&-has-error {
border: 1px solid var(--theme-warn-color);
}
&-input {
background-color: var(--adf-metadata-buttons-background-color);
border-radius: 6px;
padding: 6px 0;
margin: 0;
width: 100%;
}
}
}

View File

@ -186,7 +186,7 @@ export class ThumbnailService {
*
* @returns URL string
*/
public getDefaultMimeTypeIcon(): string {
getDefaultMimeTypeIcon(): string {
return DEFAULT_ICON;
}
}

View File

@ -193,7 +193,7 @@
},
"METADATA": {
"BASIC": {
"HEADER": "Properties",
"HEADER": "General info",
"NAME": "Name",
"TITLE": "Title",
"DESCRIPTION": "Description",
@ -401,10 +401,6 @@
"SIDEBAR": {
"THUMBNAILS": {
"PAGE": "Page {{ pageNum }}"
},
"METADATA": {
"MORE_INFORMATION": "More information",
"LESS_INFORMATION": "Less information"
}
},
"PDF_DIALOG": {
@ -573,5 +569,8 @@
"ADF_DROPDOWN": {
"LOADING": "Loading...",
"SELECTION_ARIA_LABEL": "{{placeholder}} combobox {{selectedOption}}"
},
"INFO_DRAWER": {
"ICON": "Node Icon"
}
}

View File

@ -1,5 +1,6 @@
<div *ngIf="showHeader" class="adf-info-drawer-layout-header">
<div class="adf-info-drawer-layout-header-title">
<ng-content select="[info-drawer-node-icon]"></ng-content>
<ng-content select="[info-drawer-title]"></ng-content>
</div>
<div class="adf-info-drawer-layout-header-buttons">

View File

@ -14,44 +14,44 @@ $adf-info-drawer-icon-size-half: 24px !default;
overflow: auto;
width: 100%;
background-color: var(--theme-background-color);
background-color: var(--theme-card-background-color);
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.27);
& .mat-tab-label {
font-weight: bold;
height: 32px;
text-align: left;
text-transform: uppercase;
opacity: 1;
}
&-header {
padding: 13px 0 0 23px;
padding: 8px 0 0 24px;
display: flex;
justify-content: space-between;
align-items: center;
justify-content: space-between;
height: 56px;
margin-bottom: 40px;
&-buttons {
padding-right: 18px;
mat-icon {
cursor: pointer;
}
}
&-title {
width: 197px;
height: 32px;
display: flex;
font-size: $adf-info-drawer-layout-title-font-size;
line-height: 1.6;
letter-spacing: -0.5px;
line-height: 28px;
letter-spacing: 0.15px;
text-align: left;
align-items: start;
color: var(--adf-theme-foreground-text-color-054);
overflow: hidden;
& > div {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--adf-metadata-property-panel-title-color);
}
}
}

View File

@ -1,13 +1,13 @@
<adf-info-drawer-layout [showHeader]="showHeader">
<div role="heading" aria-level="1" *ngIf="title" info-drawer-title>{{ title | translate }}</div>
<img *ngIf="icon" class="adf-info-drawer-icon" alt="{{ 'INFO_DRAWER.ICON' | translate }}" src="{{ icon }}" info-drawer-node-icon>
<div *ngIf="title" role="heading" aria-level="1" title="{{ title | translate }}" info-drawer-title>{{ title | translate }}</div>
<ng-content *ngIf="!title" info-drawer-title select="[info-drawer-title]"></ng-content>
<ng-content info-drawer-buttons select="[info-drawer-buttons]"></ng-content>
<ng-container info-drawer-content *ngIf="showTabLayout(); then tabLayout else singleLayout"></ng-container>
<ng-template #tabLayout>
<mat-tab-group [(selectedIndex)]="selectedIndex" class="adf-info-drawer-tabs" (selectedTabChange)="onTabChange($event)">
<mat-tab-group [(selectedIndex)]="selectedIndex" class="adf-info-drawer-tabs" (selectedTabChange)="onTabChange($event)" [animationDuration]="0">
<mat-tab *ngFor="let contentBlock of contentBlocks"
#tab
[labelClass]="[

View File

@ -1,12 +1,21 @@
.adf {
&-info-drawer {
display: block;
.adf-metadata-properties-panel {
display: block;
margin: 0;
}
.mat-tab-label {
min-width: 0;
}
& &-layout {
.adf-info-drawer-icon {
display: inline-block;
vertical-align: middle;
margin-right: 10px;
}
&-content {
padding: 0;
@ -38,16 +47,16 @@
}
.mat-ink-bar {
height: 4px;
height: 2px;
}
.mat-tab-body {
padding: 10px;
overflow-y: hidden;
}
.mat-tab-body-content {
/* stylelint-disable */
overflow: initial;
overflow: auto;
/* stylelint-enable */
}
}

View File

@ -28,8 +28,8 @@ import { ESCAPE } from '@angular/cdk/keycodes';
describe('InfoDrawerComponent', () => {
let element: HTMLElement;
let component: InfoDrawerComponent;
let fixture: ComponentFixture<InfoDrawerComponent>;
let translateService: TranslateService;
let fixture: ComponentFixture<InfoDrawerComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
@ -87,24 +87,27 @@ describe('InfoDrawerComponent', () => {
@Component({
template: `
<adf-info-drawer [selectedIndex]="tabIndex" title="Fake Title Custom">
<adf-info-drawer-tab label="Tab1">
</adf-info-drawer-tab>
<adf-info-drawer-tab label="Tab2">
</adf-info-drawer-tab>
<adf-info-drawer-tab label="Tab3" icon="tab-icon">
</adf-info-drawer-tab>
</adf-info-drawer>
`
<adf-info-drawer [selectedIndex]="tabIndex" [icon]="icon" title="Fake Title Custom">
<adf-info-drawer-tab label="Tab1">
</adf-info-drawer-tab>
<adf-info-drawer-tab label="Tab2">
</adf-info-drawer-tab>
<adf-info-drawer-tab label="Tab3" icon="tab-icon">
</adf-info-drawer-tab>
</adf-info-drawer>
`
})
class CustomInfoDrawerComponent extends InfoDrawerComponent {
tabIndex: number;
icon: string;
}
describe('Custom InfoDrawer', () => {
let fixture: ComponentFixture<CustomInfoDrawerComponent>;
let component: CustomInfoDrawerComponent;
let translateService: TranslateService;
const getNodeIcon = () =>
fixture.debugElement.queryAll(By.css('[info-drawer-node-icon]'));
beforeEach(() => {
TestBed.configureTestingModule({
@ -139,7 +142,7 @@ describe('Custom InfoDrawer', () => {
fixture.detectChanges();
const tab: any = fixture.debugElement.queryAll(By.css('.mat-tab-label-active'));
expect(tab.length).toBe(1);
expect(tab[0].nativeElement.innerText).toContain('TAB1');
expect(tab[0].nativeElement.innerText).toContain('Tab1');
});
it('should select the tab 2 (index 1)', () => {
@ -147,7 +150,7 @@ describe('Custom InfoDrawer', () => {
fixture.detectChanges();
const tab: any = fixture.debugElement.queryAll(By.css('.mat-tab-label-active'));
expect(tab.length).toBe(1);
expect(tab[0].nativeElement.innerText).toContain('TAB2');
expect(tab[0].nativeElement.innerText).toContain('Tab2');
});
it('should render a tab with icon', () => {
@ -157,21 +160,33 @@ describe('Custom InfoDrawer', () => {
expect(tab[0].nativeElement.innerText).not.toBe('TAB3');
expect(tab[0].nativeElement.innerText).toContain('tab-icon');
});
it('should render a icon with title', () => {
component.icon = '/assets/images/ft_ic_miscellaneous.svg';
fixture.detectChanges();
const icon = getNodeIcon();
const srcAttribute = icon[0].nativeElement.getAttribute('src');
expect(icon.length).toBe(1);
expect(srcAttribute).toContain('/assets/images/ft_ic_miscellaneous.svg');
});
});
@Component({
template: `
<adf-info-drawer [showHeader]="showHeader" title="Fake Visibility Info Drawer Title">
<adf-info-drawer [showHeader]="showHeader" [icon]="icon" title="Fake Visibility Info Drawer Title">
</adf-info-drawer>
`
`
})
class VisibilityInfoDrawerComponent extends InfoDrawerComponent {
showHeader: boolean;
icon: string;
}
describe('Header visibility InfoDrawer', () => {
let fixture: ComponentFixture<VisibilityInfoDrawerComponent>;
let component: VisibilityInfoDrawerComponent;
const getNodeIcon = () =>
fixture.debugElement.queryAll(By.css('[info-drawer-node-icon]'));
beforeEach(() => {
TestBed.configureTestingModule({
@ -189,18 +204,24 @@ describe('Header visibility InfoDrawer', () => {
});
it('should show info drawer header by default', () => {
component.icon = '/assets/images/ft_ic_miscellaneous.svg';
fixture.detectChanges();
const title: any = fixture.debugElement.queryAll(By.css('[info-drawer-title]'));
const icon = getNodeIcon();
const srcAttribute = icon[0].nativeElement.getAttribute('src');
expect(title.length).toBe(1);
expect(icon.length).toBe(1);
expect(srcAttribute).toContain('/assets/images/ft_ic_miscellaneous.svg');
expect(title[0].nativeElement.innerText).toBe('Fake Visibility Info Drawer Title');
expect(component.showHeader).toEqual(true);
});
it('should not show info drawer header when showHeader is false', () => {
fixture.detectChanges();
it('should not show info drawer header with icon when showHeader is false', () => {
component.showHeader = false;
fixture.detectChanges();
const title: any = fixture.debugElement.queryAll(By.css('[info-drawer-title]'));
const icon = getNodeIcon();
expect(title.length).toBe(0);
expect(icon.length).toBe(0);
});
});

View File

@ -47,6 +47,9 @@ export class InfoDrawerComponent {
@Input()
title: string|null = null;
@Input()
icon: string | null = null;
/** The selected index tab. */
@Input()
selectedIndex: number = 0;

View File

@ -1,3 +1,4 @@
/* stylelint-disable scss/no-global-function-names */
@use './reference-variables' as *;
@use '@angular/material' as mat;
@ -52,7 +53,6 @@
--adf-identity-user-info-line-height: $adf-ref-line-height,
--adf-identity-user-info-font-size: var(--theme-adf-picture-1-font-size),
--adf-user-info-container-margin-right: $adf-ref-margin-right,
--adf-info-drawer-tab-default-color: mat.get-color-from-palette($accent),
--adf-info-drawer-tab-default-background: mat.get-color-from-palette($background, card),
--adf-info-drawer-tab-default-bottom-line:$adf-ref-tab-bottom-line-default,
@ -70,8 +70,13 @@
--adf-people-cloud-input-label-focus-color: mat.get-color-from-palette($primary),
--adf-people-cloud-autosuggest-result-active-color: mat.get-color-from-palette($foreground, text),
--adf-people-cloud-autosuggest-result-disabled-color: mat.get-color-from-palette($foreground, secondary-text),
--adf-people-cloud-input-caption-error-color: mat.get-color-from-palette($warn)
--adf-people-cloud-input-caption-error-color: mat.get-color-from-palette($warn),
--adf-metadata-property-panel-border-color: $adf-ref-metadata-property-panel-border-color,
--adf-metadata-buttons-background-color: $adf-ref-metadata-buttons-background-color,
--adf-metadata-action-button-clear-color: $adf-ref-metadata-action-button-clear-color,
--adf-metadata-property-panel-text-color: $adf-ref-metadata-property-panel-text-color,
--adf-metadata-property-panel-label-color: $adf-ref-metadata-property-panel-label-color,
--adf-metadata-property-panel-title-color: $adf-ref-metadata-property-panel-title-color,
);
// propagates SCSS variables into the CSS variables scope

View File

@ -57,7 +57,6 @@
--adf-theme-foreground-text-color-075: mat.get-color-from-palette($foreground, text, 0.75),
--adf-theme-foreground-text-color-064: mat.get-color-from-palette($foreground, text, 0.64),
--adf-theme-foreground-text-color-054: mat.get-color-from-palette($foreground, text, 0.54),
--adf-theme-foreground-text-color-042: mat.get-color-from-palette($foreground, text, 0.42),
--adf-theme-foreground-text-color-040: mat.get-color-from-palette($foreground, text, 0.4),
--adf-theme-foreground-text-color-027: mat.get-color-from-palette($foreground, text, 0.27),
--adf-theme-foreground-text-color-025: mat.get-color-from-palette($foreground, text, 0.25),

View File

@ -17,3 +17,9 @@ $adf-ref-line-height: 40px;
$adf-ref-margin-right: 8px;
$adf-ref-tab-bottom-line-default: unset;
$adf-ref-tab-bottom-line-active: unset;
$adf-ref-metadata-property-panel-border-color: rgba(0, 0, 0, 0.12);
$adf-ref-metadata-buttons-background-color: rgba(33, 33, 33, 0.05);
$adf-ref-metadata-action-button-clear-color: rgba(33, 35, 40, 0.698);
$adf-ref-metadata-property-panel-text-color: rgba(33, 35, 40, 0.7);
$adf-ref-metadata-property-panel-label-color: rgba(33, 33, 33, 0.24);
$adf-ref-metadata-property-panel-title-color: rgb(33, 33, 33);

View File

@ -1,7 +1,18 @@
<adf-content-metadata [displayDefaultProperties]="displayDefaultProperties" [expanded]="expanded" [node]="node"
[preset]="preset" [displayEmpty]="displayEmpty" [editable]="editable" [multi]="multi"
[displayAspect]="displayAspect" [copyToClipboardAction]="copyToClipboardAction"
[useChipsForMultiValueProperty]="useChipsForMultiValueProperty" *ngIf="!loading; else loadingTemplate">
<adf-content-metadata
[displayDefaultProperties]="displayDefaultProperties"
[expanded]="expanded"
[node]="node"
[preset]="preset"
[displayEmpty]="displayEmpty"
[readOnly]="!editable"
[multi]="multi"
[displayAspect]="displayAspect"
[copyToClipboardAction]="copyToClipboardAction"
[useChipsForMultiValueProperty]="useChipsForMultiValueProperty"
[displayTags]="false"
[displayCategories]="false"
*ngIf="!loading; else loadingTemplate"
>
</adf-content-metadata>
<ng-template #loadingTemplate>
<div class="adf-properties-viewer-wrapper-loading">

View File

@ -222,17 +222,6 @@ describe('TaskHeaderCloudComponent', () => {
const loading = fixture.debugElement.query(By.css('.adf-task-header-loading'));
expect(loading).toBeTruthy();
});
it('should not render edit icon if the task in assigned state and assingned user is different from current logged-in user', () => {
isTaskEditableSpy.and.returnValue(false);
fixture.detectChanges();
const priorityEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-priority"]`));
const descriptionEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-description"]`));
const dueDateEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="datepickertoggle-dueDate"]`));
expect(priorityEditIcon).toBeNull();
expect(descriptionEditIcon).toBeNull();
expect(dueDateEditIcon).toBeNull();
});
});
describe('Task with parentTaskId', () => {
@ -328,9 +317,7 @@ describe('TaskHeaderCloudComponent', () => {
it('should render edit icon if the task in assigned state and assingee should be current user', () => {
fixture.detectChanges();
const descriptionEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="header-description"] [class*="adf-textitem-edit-icon"]`));
const dueDateEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="datepickertoggle-dueDate"]`));
expect(descriptionEditIcon).not.toBeNull();
expect(dueDateEditIcon).not.toBeNull();
});
@ -372,16 +359,6 @@ describe('TaskHeaderCloudComponent', () => {
const value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-clickable-icon-assignee"]`));
expect(value).toBeNull();
});
it('should not render edit icon if the task in created state and not assigned', () => {
fixture.detectChanges();
const priorityEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-priority"]`));
const descriptionEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-description"]`));
const dueDateEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="datepickertoggle-dueDate"]`));
expect(priorityEditIcon).toBeNull();
expect(descriptionEditIcon).toBeNull();
expect(dueDateEditIcon).toBeNull();
});
});
describe('Completed Task', () => {
@ -404,16 +381,6 @@ describe('TaskHeaderCloudComponent', () => {
const value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-clickable-icon-assignee"]`));
expect(value).toBeNull('Edit icon should NOT be shown');
});
it('should not render edit icon if the task in completed state', () => {
fixture.detectChanges();
const priorityEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-priority"]`));
const descriptionEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-description"]`));
const dueDateEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="datepickertoggle-dueDate"]`));
expect(priorityEditIcon).toBeNull();
expect(descriptionEditIcon).toBeNull();
expect(dueDateEditIcon).toBeNull();
});
});
describe('Suspended Task', () => {
@ -436,16 +403,6 @@ describe('TaskHeaderCloudComponent', () => {
const value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-clickable-icon-assignee"]`));
expect(value).toBeNull();
});
it('should not render edit icon if the task in suspended state', () => {
fixture.detectChanges();
const priorityEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-priority"]`));
const descriptionEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-description"]`));
const dueDateEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="datepickertoggle-dueDate"]`));
expect(priorityEditIcon).toBeNull();
expect(descriptionEditIcon).toBeNull();
expect(dueDateEditIcon).toBeNull();
});
});
describe('Task with candidates', () => {

View File

@ -25,7 +25,7 @@ export class CardTextItemPage {
clearButton = 'button[data-automation-id*="card-textitem-reset"]';
field = '[data-automation-id*="card-textitem-value"]';
labelLocator = '[data-automation-id*="card-textitem-label"]';
errorMessage = '.adf-textitem-editable-error';
errorMessage = '.adf-textitem-error';
clickableElement = '.adf-textitem-clickable';
readOnlyField = '.adf-property-read-only';